Skip to content

Commit

Permalink
Resolve path slashes on File instantiation (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
05nelsonm authored Dec 10, 2023
1 parent 9df25a9 commit 157cac8
Show file tree
Hide file tree
Showing 26 changed files with 369 additions and 142 deletions.
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@

A very simple `File` API for Kotlin Multiplatform. It gets the job done.

For `Jvm`, `File` is `typealias` to `java.io.File`
For `Jvm`, `File` is `typealias` to `java.io.File`. `File` for `nonJvm` is
operationally equivalent to `Jvm` for consistency across platforms.

```kotlin
import io.matthewnelson.kmp.file.*

fun commonMain(f: File) {
SYSTEM_PATH_SEPARATOR
SYSTEM_TEMP_DIRECTORY
// System path separator character (`/` or `\`)
SysPathSep
// System temporary directory
SysTempDir

f.isAbsolute()
f.exists()
Expand All @@ -44,9 +47,13 @@ fun commonMain(f: File) {
f.canonicalPath()
f.canonicalFile()

// equivalent to File("/some/path")
val file = "/some/path".toFile()

// resolve child paths
val child = f.resolve("child")
child.resolve(f)
val child = file.resolve("child")
println(child.path) // >> `/some/path/child`
println(child.resolve(file).path) // >> `/some/path` (file is rooted)

// normalized File (e.g. removal of . and ..)
f.normalize()
Expand Down
5 changes: 2 additions & 3 deletions library/file/api/file.api
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ public final class io/matthewnelson/kmp/file/File {
public static final fun path (Ljava/io/File;)Ljava/lang/String;
public static final fun resolve (Ljava/io/File;Ljava/lang/String;)Ljava/io/File;
public static final fun toFile (Ljava/lang/String;)Ljava/io/File;
public static final fun toFile (Ljava/lang/String;Ljava/lang/String;)Ljava/io/File;
}

public final class io/matthewnelson/kmp/file/FileJvm {
public static final field SYSTEM_PATH_SEPARATOR C
public static final field SYSTEM_TEMP_DIRECTORY Ljava/io/File;
public static final field SysPathSep C
public static final field SysTempDir Ljava/io/File;
public static final fun normalize (Ljava/io/File;)Ljava/io/File;
public static final fun readBytes (Ljava/io/File;)[B
public static final fun readUtf8 (Ljava/io/File;)Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,46 @@ package io.matthewnelson.kmp.file
import kotlin.jvm.JvmName

/**
* Most all systems it will be `/`, but for Windows because it is
* special it will be `\`
* The operating system's path separator character
* */
public expect val SYSTEM_PATH_SEPARATOR: Char
public expect val SysPathSep: Char

/**
* The system temporary directory
* */
public expect val SYSTEM_TEMP_DIRECTORY: File
public expect val SysTempDir: File

public fun String.toFile(): File = File(this)

/**
* A File
* */
public expect class File {

public constructor(pathname: String)
public constructor(parent: String, child: String)
public constructor(parent: File, child: String)
public expect class File(pathname: String) {

// Not exposing any secondary constructors because
// Jvm has undocumented behavior that cannot be
// modified because it's typealias.
//
// java.io.File's secondary constructors take 2
// arguments and concatenate the paths together. If
// the first argument is empty though, the result
// will always contain the system path separator as
// the first character.
//
// println(File("", "child").path) >> "/child"
// println(File(File(""), "child").path) >> "/child"
// println(File("", "./child").path) >> "/./child"
//
// So for Unix, the "child" argument now magically
// becomes absolute instead of relative to the current
// working directory.
//
// This could be dangerous if someone were to do:
//
// File(fileFromSomewhereElse, "child")
//
// thinking that it would simply be appended to the
// parent.

public fun isAbsolute(): Boolean

Expand All @@ -49,7 +71,7 @@ public expect class File {

// use .name
internal fun getName(): String
// use .parent
// use .parentPath
internal fun getParent(): String?
// use .parentFile
internal fun getParentFile(): File?
Expand All @@ -69,9 +91,6 @@ public expect class File {
internal fun getCanonicalFile(): File
}

public fun String.toFile(): File = File(this)
public fun String.toFile(child: String): File = File(this, child)

@get:JvmName("name")
public val File.name: String get() = getName()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ class AbsoluteUnitTest {
// should be relative for all platforms (even windows)
assertFalse("C:something".toFile().isAbsolute())
assertFalse("C:".toFile().isAbsolute())
assertFalse("".toFile().isAbsolute())
assertFalse(".".toFile().isAbsolute())
assertFalse("..".toFile().isAbsolute())
assertFalse("./something".toFile().isAbsolute())
assertFalse("../something".toFile().isAbsolute())
assertFalse("some/path".toFile().isAbsolute())

// TODO: Fix isAbsolute for Nodejs on windows
if (isNodejs && isWindows) return
Expand All @@ -48,8 +54,8 @@ class AbsoluteUnitTest {
if (isSimulator) return

val rootDir = PROJECT_DIR_PATH.substringBeforeLast(
"library"
.toFile("file")
"library".toFile()
.resolve("file")
.path
)

Expand All @@ -64,6 +70,6 @@ class AbsoluteUnitTest {
// `C:\Users\path\to\current\working\dir\C:relative`
// for a relative path of:
// `C:relative`
assertTrue(absolute.endsWith("${SYSTEM_PATH_SEPARATOR}relative"))
assertTrue(absolute.endsWith("${SysPathSep}relative"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ fun randomName(): String = Random
.nextBytes(16)
.encodeToString(Base16)

fun randomTemp(): File = SYSTEM_TEMP_DIRECTORY
fun randomTemp(): File = SysTempDir
.resolve(randomName())

fun ByteArray.sha256(): String = SHA256()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,49 @@ import kotlin.test.assertEquals

class FileUnitTest {

// TODO: Implement toPath() (Native)
// @Test
// fun givenFile_whenTrailingSlashes_thenAreRemoved() {
// val expected = "expected"
//
// (0..5).forEach { times ->
// val file = File(buildString {
// append(expected)
// repeat(times) { append(SYSTEM_PATH_SEPARATOR) }
// })
//
// assertEquals(expected, file.path)
// }
// }

@Test
fun givenFile_whenToString_thenPrintsPath() {
val expected = "something"
assertEquals(expected, expected.toFile().toString())
}

@Test
fun givenFile_whenInsaneSlashes_thenAreResolved() {
// windows should replace all unix path separators with `\` before
assertEquals("relative${SysPathSep}path", "relative////path///".toFile().path)
assertEquals("relative${SysPathSep}path${SysPathSep}.", "relative////path///.".toFile().path)
assertEquals(".${SysPathSep}..", "./..".toFile().path)

assertEquals(".", ".".toFile().path)
assertEquals("..", "..".toFile().path)
assertEquals("...", "...".toFile().path)
assertEquals("....", "....".toFile().path)

if (isWindows) {
assertEquals("\\", "\\".toFile().path)
assertEquals("\\Relative", "\\Relative".toFile().path)
assertEquals("\\Relative", "/Relative".toFile().path)
assertEquals("\\Relative\\path", "/Relative/path".toFile().path)

assertEquals("\\\\", "\\\\".toFile().path)
assertEquals("\\\\", "\\\\\\".toFile().path)
assertEquals("\\\\", "\\//\\".toFile().path)
assertEquals("\\\\Absolute", "\\\\Absolute".toFile().path)
assertEquals("\\\\Absolute", "\\\\\\Absolute".toFile().path)
assertEquals("\\\\Absolute", "//Absolute".toFile().path)
assertEquals("\\\\Absolute", "///Absolute".toFile().path)
assertEquals("\\\\Absolute\\path", "///Absolute//path".toFile().path)

assertEquals("C:\\", "C://".toFile().path)
assertEquals("F:", "F:".toFile().path)
assertEquals("F:\\", "F:\\\\\\".toFile().path)
} else {
assertEquals("/", "/".toFile().path)
assertEquals("/", "////".toFile().path)
assertEquals("\\", "\\".toFile().path)
assertEquals("\\\\", "\\\\".toFile().path)
assertEquals("/absolute", "//absolute".toFile().path)
assertEquals("/absolute\\/path", "///absolute\\////path".toFile().path)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,13 @@ class ParentUnitTest {
assertEquals(expected, expected.toFile().resolve("anything").parentPath)
}

// TODO: Implement toPath() (Native)
// @Test
// fun givenFile_whenHasTrailingSlashes_thenIgnoresThem() {
// val path = buildString {
// append(' ')
// repeat(3) { append(SYSTEM_PATH_SEPARATOR) }
// }
//
// assertNull(File(path).parent)
// }
@Test
fun givenFile_whenHasTrailingSlashes_thenIgnoresThem() {
val path = buildString {
append(' ')
repeat(3) { append(SysPathSep) }
}

assertNull(path.toFile().parentPath)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,18 @@
**/
package io.matthewnelson.kmp.file

import kotlin.test.Test
import kotlin.test.assertEquals

class ResolveUnitTest {

// TODO
@Test
fun givenFile_whenResolve_thenIsExpected() {
assertEquals("", "".toFile().resolve("").path)
assertEquals("c", "".toFile().resolve("c").path)
assertEquals("p${SysPathSep}c", "p".toFile().resolve("c").path)
assertEquals("p${SysPathSep}..", "p".toFile().resolve("..").path)
assertEquals("p${SysPathSep}p2${SysPathSep}..", "p".toFile().resolve("p2").resolve("..").path)
assertEquals("${SysPathSep}c", "p".toFile().resolve("${SysPathSep}c").path)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.fail

class WriteUnitTest {

Expand Down Expand Up @@ -67,4 +68,16 @@ class WriteUnitTest {
tmp.delete()
}
}

@Test
fun givenFile_whenDirDoesNotExist_thenThrowsIOException() {
val tmp = randomTemp().resolve(randomName()).resolve(randomName())

try {
tmp.writeUtf8("Hello World!")
fail()
} catch (_: IOException) {
// pass
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import kotlinx.cinterop.toKString
import platform.posix.getenv

@OptIn(ExperimentalForeignApi::class)
public actual val SYSTEM_TEMP_DIRECTORY: File by lazy {
public actual val SysTempDir: File by lazy {
val tmpdir = getenv("TMPDIR")
(tmpdir?.toKString() ?: "/tmp").toFile()
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ import io.matthewnelson.kmp.file.internal.*
import io.matthewnelson.kmp.file.internal.errorCode
import io.matthewnelson.kmp.file.internal.path_sep

public actual val SYSTEM_PATH_SEPARATOR: Char by lazy {
public actual val SysPathSep: Char by lazy {
try {
path_sep.first()
} catch (_: Throwable) {
'/'
}
}

public actual val SYSTEM_TEMP_DIRECTORY: File by lazy {
public actual val SysTempDir: File by lazy {
try {
os_tmpdir()
} catch (_: Throwable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,17 @@
package io.matthewnelson.kmp.file.internal

import io.matthewnelson.kmp.file.DelicateFileApi
import io.matthewnelson.kmp.file.SysPathSep
import io.matthewnelson.kmp.file.toIOException

internal actual val IsWindows: Boolean by lazy {
try {
os_platform() == "win32"
} catch (_: Throwable) {
SysPathSep == '\\'
}
}

internal actual fun fs_chmod(path: String, mode: String) {
try {
fs_chmodSync(path, mode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,7 @@
**/
package io.matthewnelson.kmp.file

import io.matthewnelson.kmp.file.internal.os_platform

actual val isJvm: Boolean = false
actual val isNative: Boolean = false
actual val isNodejs: Boolean = true
actual val isSimulator: Boolean = false
actual val isWindows: Boolean by lazy {
try {
os_platform() == "win32"
} catch (_: Throwable) {
SYSTEM_PATH_SEPARATOR == '\\'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ import kotlin.io.resolve as _resolve
import kotlin.io.writeBytes as _writeBytes
import kotlin.io.writeText as _writeText

public actual typealias File = java.io.File

@JvmField
public actual val SYSTEM_PATH_SEPARATOR: Char = File.separatorChar
public actual val SysPathSep: Char = File.separatorChar

@JvmField
public actual val SYSTEM_TEMP_DIRECTORY: File = System
public actual val SysTempDir: File = System
.getProperty("java.io.tmpdir")
.toFile()

public actual typealias File = java.io.File

public actual fun File.normalize(): File = _normalize()

@Throws(IOException::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import kotlinx.cinterop.toKString
import platform.posix.getenv

@OptIn(ExperimentalForeignApi::class)
public actual val SYSTEM_TEMP_DIRECTORY: File by lazy {
public actual val SysTempDir: File by lazy {
val tmpdir = getenv("TMPDIR")
(tmpdir?.toKString() ?: "/tmp").toFile()
}
Loading

0 comments on commit 157cac8

Please sign in to comment.