Skip to content

Commit f85bebd

Browse files
authored
Support path resolution (#228)
Fixes #223
1 parent 5d8982c commit f85bebd

File tree

10 files changed

+97
-8
lines changed

10 files changed

+97
-8
lines changed

core/api/kotlinx-io-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ public abstract interface class kotlinx/io/files/FileSystem {
212212
public static synthetic fun delete$default (Lkotlinx/io/files/FileSystem;Lkotlinx/io/files/Path;ZILjava/lang/Object;)V
213213
public abstract fun exists (Lkotlinx/io/files/Path;)Z
214214
public abstract fun metadataOrNull (Lkotlinx/io/files/Path;)Lkotlinx/io/files/FileMetadata;
215+
public abstract fun resolve (Lkotlinx/io/files/Path;)Lkotlinx/io/files/Path;
215216
public abstract fun sink (Lkotlinx/io/files/Path;Z)Lkotlinx/io/RawSink;
216217
public static synthetic fun sink$default (Lkotlinx/io/files/FileSystem;Lkotlinx/io/files/Path;ZILjava/lang/Object;)Lkotlinx/io/RawSink;
217218
public abstract fun source (Lkotlinx/io/files/Path;)Lkotlinx/io/RawSource;

core/apple/src/files/FileSystemApple.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,12 @@ internal actual fun mkdirImpl(path: String) {
4646
throw IOException("mkdir failed: ${strerror(errno)?.toKString()}")
4747
}
4848
}
49+
50+
internal actual fun realpathImpl(path: String): String {
51+
val res = realpath(path, null) ?: throw IllegalStateException()
52+
try {
53+
return res.toKString()
54+
} finally {
55+
free(res)
56+
}
57+
}

core/common/src/files/FileSystem.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,19 @@ public sealed interface FileSystem {
129129
* @param path the path to get the metadata for.
130130
*/
131131
public fun metadataOrNull(path: Path): FileMetadata?
132+
133+
/**
134+
* Returns an absolute path to the same file or directory the [path] is pointing to.
135+
* All symbolic links are solved, extra path separators and references to current (`.`) or
136+
* parent (`..`) directories are removed.
137+
* If the [path] is a relative path then it'll be resolved against current working directory.
138+
* If there is no file or directory to which the [path] is pointing to then [FileNotFoundException] will be thrown.
139+
*
140+
* @param path the path to resolve.
141+
* @return a resolved path.
142+
* @throws FileNotFoundException if there is no file or directory corresponding to the specified path.
143+
*/
144+
public fun resolve(path: Path): Path
132145
}
133146

134147
internal abstract class SystemFileSystemImpl : FileSystem

core/common/test/files/SmokeFileTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,40 @@ class SmokeFileTest {
336336
}
337337
assertEquals("second third",
338338
SystemFileSystem.source(path).buffered().use { it.readString() })
339+
}
340+
341+
@Test
342+
fun resolve() {
343+
assertFailsWith<FileNotFoundException>("Non-existing path resolution should fail") {
344+
SystemFileSystem.resolve(createTempPath())
345+
}
346+
347+
val cwd = SystemFileSystem.resolve(Path("."))
348+
val parentRel = Path("..")
349+
assertEquals(cwd.parent, SystemFileSystem.resolve(parentRel))
339350

351+
assertEquals(cwd, SystemFileSystem.resolve(cwd),
352+
"Absolute path resolution should not alter the path")
353+
354+
// root
355+
// |-> a -> b
356+
// |-> c -> d
357+
val root = createTempPath()
358+
SystemFileSystem.createDirectories(Path(root, "a", "b"))
359+
val tgt = Path(root, "c", "d")
360+
SystemFileSystem.createDirectories(tgt)
361+
362+
val src = Path(root, "a", "..", "a", ".", "b", "..", "..", "c", ".", "d")
363+
try {
364+
// root/a/../a/./b/../../c/./d -> root/c/d
365+
assertEquals(SystemFileSystem.resolve(tgt), SystemFileSystem.resolve(src))
366+
} finally {
367+
// TODO: remove as soon as recursive file removal is implemented
368+
SystemFileSystem.delete(Path(root, "a", "b"))
369+
SystemFileSystem.delete(Path(root, "a"))
370+
SystemFileSystem.delete(Path(root, "c", "d"))
371+
SystemFileSystem.delete(Path(root, "c"))
372+
}
340373
}
341374

342375
private fun constructAbsolutePath(vararg parts: String): String {

core/js/src/files/FileSystemJs.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl()
115115
check(buffer !== null) { "Module 'buffer' was not found" }
116116
return FileSink(path, append)
117117
}
118+
119+
override fun resolve(path: Path): Path {
120+
check(fs !== null) { "Module 'fs' was not found" }
121+
if (!exists(path)) throw FileNotFoundException(path.path)
122+
return Path(fs.realpathSync.native(path.path) as String)
123+
}
118124
}
119125

120126
public actual val SystemTemporaryDirectory: Path

core/jvm/src/files/FileSystemJvm.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl()
9191
override fun source(path: Path): RawSource = FileInputStream(path.file).asSource()
9292

9393
override fun sink(path: Path, append: Boolean): RawSink = FileOutputStream(path.file, append).asSink()
94+
95+
override fun resolve(path: Path): Path {
96+
if (!path.file.exists()) throw FileNotFoundException(path.file.absolutePath)
97+
return Path(path.file.canonicalFile)
98+
}
9499
}
95100

96101
@JvmField

core/mingw/src/files/FileSystemMingw.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ package kotlinx.io.files
1010
import kotlinx.cinterop.*
1111
import kotlinx.io.IOException
1212
import platform.posix.*
13-
import platform.windows.GetLastError
14-
import platform.windows.MOVEFILE_REPLACE_EXISTING
15-
import platform.windows.MoveFileExA
16-
import platform.windows.PathIsRelativeA
13+
import platform.windows.*
1714

1815
private const val WindowsPathSeparator: Char = '\\'
1916

@@ -49,3 +46,14 @@ internal actual fun mkdirImpl(path: String) {
4946
throw IOException("mkdir failed: ${strerror(errno)?.toKString()}")
5047
}
5148
}
49+
50+
private const val MAX_PATH_LENGTH = 32767
51+
52+
internal actual fun realpathImpl(path: String): String {
53+
memScoped {
54+
val buffer = allocArray<CHARVar>(MAX_PATH_LENGTH)
55+
val len = GetFullPathNameA(path, MAX_PATH_LENGTH.convert(), buffer, null)
56+
if (len == 0u) throw IllegalStateException()
57+
return buffer.toKString()
58+
}
59+
}

core/native/src/files/FileSystemNative.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl()
8080
}
8181
}
8282

83+
override fun resolve(path: Path): Path {
84+
if (!exists(path)) throw FileNotFoundException(path.path)
85+
return Path(realpathImpl(path.path))
86+
}
87+
8388
override fun source(path: Path): RawSource {
8489
val openFile: CPointer<FILE>? = fopen(path.path, "rb")
8590
if (openFile == null) {
@@ -102,6 +107,8 @@ internal expect fun atomicMoveImpl(source: Path, destination: Path)
102107

103108
internal expect fun mkdirImpl(path: String)
104109

110+
internal expect fun realpathImpl(path: String): String
111+
105112
public actual open class FileNotFoundException actual constructor(
106113
message: String?
107114
) : IOException(message)

core/unix/src/files/FileSystemUnix.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,23 @@ import kotlinx.cinterop.UnsafeNumber
1212
import kotlinx.cinterop.convert
1313
import kotlinx.cinterop.toKString
1414
import kotlinx.io.IOException
15-
import platform.posix.errno
16-
import platform.posix.mkdir
17-
import platform.posix.rename
18-
import platform.posix.strerror
15+
import platform.posix.*
1916

2017
internal actual fun atomicMoveImpl(source: Path, destination: Path) {
2118
if (rename(source.path, destination.path) != 0) {
2219
throw IOException("Move failed: ${strerror(errno)?.toKString()}")
2320
}
2421
}
2522

23+
internal actual fun realpathImpl(path: String): String {
24+
val result = realpath(path, null) ?: throw IllegalStateException()
25+
try {
26+
return result.toKString()
27+
} finally {
28+
free(result)
29+
}
30+
}
31+
2632
internal actual fun mkdirImpl(path: String) {
2733
if (mkdir(path, PermissionAllowAll.convert()) != 0) {
2834
throw IOException("mkdir failed: ${strerror(errno)?.toKString()}")

core/wasm/src/files/FileSystemWasm.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl()
3030

3131
override fun metadataOrNull(path: Path): FileMetadata = unsupported()
3232

33+
override fun resolve(path: Path): Path = unsupported()
3334
}
3435

3536
public actual open class FileNotFoundException actual constructor(

0 commit comments

Comments
 (0)