Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve path slashes on File instantiation #11

Merged
merged 6 commits into from
Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading