From 5d2acc3451e5e2d36e40ecc2359da78e761cfc4b Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 11 Oct 2023 17:15:28 +0200 Subject: [PATCH 1/5] Support path resolution --- core/apple/src/files/FileSystemApple.kt | 9 +++++++++ core/common/src/files/FileSystem.kt | 13 +++++++++++++ core/common/test/files/SmokeFileTest.kt | 19 +++++++++++++++++++ core/js/src/files/FileSystemJs.kt | 6 ++++++ core/jvm/src/files/FileSystemJvm.kt | 5 +++++ core/mingw/src/files/FileSystemMingw.kt | 16 ++++++++++++---- core/native/src/files/FileSystemNative.kt | 7 +++++++ core/unix/src/files/FileSystemUnix.kt | 14 ++++++++++---- core/wasm/src/files/FileSystemWasm.kt | 1 + 9 files changed, 82 insertions(+), 8 deletions(-) diff --git a/core/apple/src/files/FileSystemApple.kt b/core/apple/src/files/FileSystemApple.kt index 869ea7c53..f7c07dca0 100644 --- a/core/apple/src/files/FileSystemApple.kt +++ b/core/apple/src/files/FileSystemApple.kt @@ -46,3 +46,12 @@ internal actual fun mkdirImpl(path: String) { throw IOException("mkdir failed: ${strerror(errno)?.toKString()}") } } + +internal actual fun realpathImpl(path: String): String { + val res = realpath(path, null) ?: throw IllegalStateException() + try { + return res.toKString() + } finally { + free(res) + } +} diff --git a/core/common/src/files/FileSystem.kt b/core/common/src/files/FileSystem.kt index 0b3c5eb35..6a7c34022 100644 --- a/core/common/src/files/FileSystem.kt +++ b/core/common/src/files/FileSystem.kt @@ -129,6 +129,19 @@ public sealed interface FileSystem { * @param path the path to get the metadata for. */ public fun metadataOrNull(path: Path): FileMetadata? + + /** + * Returns an absolute path to the same file or directory the [path] is pointing to + * where all symbolic links are solved, extra path separators and references to current (`.`) or + * parent (`..`) directories are removed. + * If the [path] is a relative path then it'll be resolved against current working directory. + * If there is no file or directory to which [path] point then [FileNotFoundException] will be thrown. + * + * @param path the path to resolve. + * @return a resolved path. + * @throws FileNotFoundException if there is no file or directory corresponding to the specified path. + */ + public fun resolve(path: Path): Path } internal abstract class SystemFileSystemImpl : FileSystem diff --git a/core/common/test/files/SmokeFileTest.kt b/core/common/test/files/SmokeFileTest.kt index fe046a2ff..09230a544 100644 --- a/core/common/test/files/SmokeFileTest.kt +++ b/core/common/test/files/SmokeFileTest.kt @@ -336,7 +336,26 @@ class SmokeFileTest { } assertEquals("second third", SystemFileSystem.source(path).buffered().use { it.readString() }) + } + + @Test + fun resolve() { + assertFailsWith { SystemFileSystem.resolve(createTempPath()) } + + val root = createTempPath() + SystemFileSystem.createDirectories(Path(root, "a", "b")) + val tgt = Path(root, "c", "d") + SystemFileSystem.createDirectories(tgt) + val src = Path(root, "a", "..", "a", ".", "b", "..", "..", "c", ".", "d") + try { + assertEquals(SystemFileSystem.resolve(tgt), SystemFileSystem.resolve(src)) + } finally { + SystemFileSystem.delete(Path(root, "a", "b")) + SystemFileSystem.delete(Path(root, "a")) + SystemFileSystem.delete(Path(root, "c", "d")) + SystemFileSystem.delete(Path(root, "c")) + } } private fun constructAbsolutePath(vararg parts: String): String { diff --git a/core/js/src/files/FileSystemJs.kt b/core/js/src/files/FileSystemJs.kt index a19cc5ecc..7378510aa 100644 --- a/core/js/src/files/FileSystemJs.kt +++ b/core/js/src/files/FileSystemJs.kt @@ -115,6 +115,12 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl() check(buffer !== null) { "Module 'buffer' was not found" } return FileSink(path, append) } + + override fun resolve(path: Path): Path { + check(fs !== null) { "Module 'fs' was not found" } + if (!exists(path)) throw FileNotFoundException(path.path) + return Path(fs.realpathSync.native(path.path) as String) + } } public actual val SystemTemporaryDirectory: Path diff --git a/core/jvm/src/files/FileSystemJvm.kt b/core/jvm/src/files/FileSystemJvm.kt index 7463d6248..fb41a9bb5 100644 --- a/core/jvm/src/files/FileSystemJvm.kt +++ b/core/jvm/src/files/FileSystemJvm.kt @@ -91,6 +91,11 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl() override fun source(path: Path): RawSource = FileInputStream(path.file).asSource() override fun sink(path: Path, append: Boolean): RawSink = FileOutputStream(path.file, append).asSink() + + override fun resolve(path: Path): Path { + if (!path.file.exists()) throw FileNotFoundException() + return Path(path.file.canonicalFile) + } } @JvmField diff --git a/core/mingw/src/files/FileSystemMingw.kt b/core/mingw/src/files/FileSystemMingw.kt index 5c027ba24..7e98bc827 100644 --- a/core/mingw/src/files/FileSystemMingw.kt +++ b/core/mingw/src/files/FileSystemMingw.kt @@ -10,10 +10,7 @@ package kotlinx.io.files import kotlinx.cinterop.* import kotlinx.io.IOException import platform.posix.* -import platform.windows.GetLastError -import platform.windows.MOVEFILE_REPLACE_EXISTING -import platform.windows.MoveFileExA -import platform.windows.PathIsRelativeA +import platform.windows.* private const val WindowsPathSeparator: Char = '\\' @@ -49,3 +46,14 @@ internal actual fun mkdirImpl(path: String) { throw IOException("mkdir failed: ${strerror(errno)?.toKString()}") } } + +private const val MAX_PATH_LENGTH = 32767 + +internal actual fun realpathImpl(path: String): String { + memScoped { + val buffer = allocArray(MAX_PATH_LENGTH) + val len = GetFullPathNameA(path, MAX_PATH_LENGTH.convert(), buffer, null) + if (len == 0u) throw IllegalStateException() + return buffer.toKString() + } +} diff --git a/core/native/src/files/FileSystemNative.kt b/core/native/src/files/FileSystemNative.kt index 18551b711..bd7020603 100644 --- a/core/native/src/files/FileSystemNative.kt +++ b/core/native/src/files/FileSystemNative.kt @@ -80,6 +80,11 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl() } } + override fun resolve(path: Path): Path { + if (!exists(path)) throw FileNotFoundException(path.path) + return Path(realpathImpl(path.path)) + } + override fun source(path: Path): RawSource { val openFile: CPointer? = fopen(path.path, "rb") if (openFile == null) { @@ -102,6 +107,8 @@ internal expect fun atomicMoveImpl(source: Path, destination: Path) internal expect fun mkdirImpl(path: String) +internal expect fun realpathImpl(path: String): String + public actual open class FileNotFoundException actual constructor( message: String? ) : IOException(message) diff --git a/core/unix/src/files/FileSystemUnix.kt b/core/unix/src/files/FileSystemUnix.kt index a5ebb618a..a26fb4975 100644 --- a/core/unix/src/files/FileSystemUnix.kt +++ b/core/unix/src/files/FileSystemUnix.kt @@ -12,10 +12,7 @@ import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.convert import kotlinx.cinterop.toKString import kotlinx.io.IOException -import platform.posix.errno -import platform.posix.mkdir -import platform.posix.rename -import platform.posix.strerror +import platform.posix.* internal actual fun atomicMoveImpl(source: Path, destination: Path) { if (rename(source.path, destination.path) != 0) { @@ -23,6 +20,15 @@ internal actual fun atomicMoveImpl(source: Path, destination: Path) { } } +internal actual fun realpathImpl(path: String): String { + val result = realpath(path, null) ?: throw IllegalStateException() + try { + return result.toKString() + } finally { + free(result) + } +} + internal actual fun mkdirImpl(path: String) { if (mkdir(path, PermissionAllowAll.convert()) != 0) { throw IOException("mkdir failed: ${strerror(errno)?.toKString()}") diff --git a/core/wasm/src/files/FileSystemWasm.kt b/core/wasm/src/files/FileSystemWasm.kt index cfa7547ab..23da8784a 100644 --- a/core/wasm/src/files/FileSystemWasm.kt +++ b/core/wasm/src/files/FileSystemWasm.kt @@ -30,6 +30,7 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl() override fun metadataOrNull(path: Path): FileMetadata = unsupported() + override fun resolve(path: Path): Path = unsupported() } public actual open class FileNotFoundException actual constructor( From ff12d3f4acbaede8500f6400b4c59ea27d87087c Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 12 Oct 2023 09:05:11 +0200 Subject: [PATCH 2/5] Updated API dump --- core/api/kotlinx-io-core.api | 1 + 1 file changed, 1 insertion(+) diff --git a/core/api/kotlinx-io-core.api b/core/api/kotlinx-io-core.api index 8d64da076..f8018144c 100644 --- a/core/api/kotlinx-io-core.api +++ b/core/api/kotlinx-io-core.api @@ -212,6 +212,7 @@ public abstract interface class kotlinx/io/files/FileSystem { public static synthetic fun delete$default (Lkotlinx/io/files/FileSystem;Lkotlinx/io/files/Path;ZILjava/lang/Object;)V public abstract fun exists (Lkotlinx/io/files/Path;)Z public abstract fun metadataOrNull (Lkotlinx/io/files/Path;)Lkotlinx/io/files/FileMetadata; + public abstract fun resolve (Lkotlinx/io/files/Path;)Lkotlinx/io/files/Path; public abstract fun sink (Lkotlinx/io/files/Path;Z)Lkotlinx/io/RawSink; public static synthetic fun sink$default (Lkotlinx/io/files/FileSystem;Lkotlinx/io/files/Path;ZILjava/lang/Object;)Lkotlinx/io/RawSink; public abstract fun source (Lkotlinx/io/files/Path;)Lkotlinx/io/RawSource; From 52b73877965c394d179950418413694561f712b7 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 12 Oct 2023 12:11:05 +0200 Subject: [PATCH 3/5] Add a test case for paths relative to CWD --- core/common/test/files/SmokeFileTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/common/test/files/SmokeFileTest.kt b/core/common/test/files/SmokeFileTest.kt index 09230a544..ea382575d 100644 --- a/core/common/test/files/SmokeFileTest.kt +++ b/core/common/test/files/SmokeFileTest.kt @@ -356,6 +356,10 @@ class SmokeFileTest { SystemFileSystem.delete(Path(root, "c", "d")) SystemFileSystem.delete(Path(root, "c")) } + + val cwd = SystemFileSystem.resolve(Path(".")) + val parentRel = Path("..") + assertEquals(cwd.parent, SystemFileSystem.resolve(parentRel)) } private fun constructAbsolutePath(vararg parts: String): String { From 504e57ce4df0347dd2cfe4b3a20fc047c5bd75ec Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 12 Oct 2023 15:30:10 +0200 Subject: [PATCH 4/5] Cleanup --- core/common/src/files/FileSystem.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/common/src/files/FileSystem.kt b/core/common/src/files/FileSystem.kt index 6a7c34022..468a94317 100644 --- a/core/common/src/files/FileSystem.kt +++ b/core/common/src/files/FileSystem.kt @@ -131,11 +131,11 @@ public sealed interface FileSystem { public fun metadataOrNull(path: Path): FileMetadata? /** - * Returns an absolute path to the same file or directory the [path] is pointing to - * where all symbolic links are solved, extra path separators and references to current (`.`) or + * Returns an absolute path to the same file or directory the [path] is pointing to. + * All symbolic links are solved, extra path separators and references to current (`.`) or * parent (`..`) directories are removed. * If the [path] is a relative path then it'll be resolved against current working directory. - * If there is no file or directory to which [path] point then [FileNotFoundException] will be thrown. + * If there is no file or directory to which the [path] is pointing to then [FileNotFoundException] will be thrown. * * @param path the path to resolve. * @return a resolved path. From d0f2a912def7bf0e74622b466707287a786b8ea3 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 18 Oct 2023 09:54:08 +0200 Subject: [PATCH 5/5] Improved exception message, added an additional test case --- core/common/test/files/SmokeFileTest.kt | 20 +++++++++++++++----- core/jvm/src/files/FileSystemJvm.kt | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/core/common/test/files/SmokeFileTest.kt b/core/common/test/files/SmokeFileTest.kt index ea382575d..d6309c8ba 100644 --- a/core/common/test/files/SmokeFileTest.kt +++ b/core/common/test/files/SmokeFileTest.kt @@ -340,8 +340,20 @@ class SmokeFileTest { @Test fun resolve() { - assertFailsWith { SystemFileSystem.resolve(createTempPath()) } + assertFailsWith("Non-existing path resolution should fail") { + SystemFileSystem.resolve(createTempPath()) + } + + val cwd = SystemFileSystem.resolve(Path(".")) + val parentRel = Path("..") + assertEquals(cwd.parent, SystemFileSystem.resolve(parentRel)) + assertEquals(cwd, SystemFileSystem.resolve(cwd), + "Absolute path resolution should not alter the path") + + // root + // |-> a -> b + // |-> c -> d val root = createTempPath() SystemFileSystem.createDirectories(Path(root, "a", "b")) val tgt = Path(root, "c", "d") @@ -349,17 +361,15 @@ class SmokeFileTest { val src = Path(root, "a", "..", "a", ".", "b", "..", "..", "c", ".", "d") try { + // root/a/../a/./b/../../c/./d -> root/c/d assertEquals(SystemFileSystem.resolve(tgt), SystemFileSystem.resolve(src)) } finally { + // TODO: remove as soon as recursive file removal is implemented SystemFileSystem.delete(Path(root, "a", "b")) SystemFileSystem.delete(Path(root, "a")) SystemFileSystem.delete(Path(root, "c", "d")) SystemFileSystem.delete(Path(root, "c")) } - - val cwd = SystemFileSystem.resolve(Path(".")) - val parentRel = Path("..") - assertEquals(cwd.parent, SystemFileSystem.resolve(parentRel)) } private fun constructAbsolutePath(vararg parts: String): String { diff --git a/core/jvm/src/files/FileSystemJvm.kt b/core/jvm/src/files/FileSystemJvm.kt index fb41a9bb5..0485dfc35 100644 --- a/core/jvm/src/files/FileSystemJvm.kt +++ b/core/jvm/src/files/FileSystemJvm.kt @@ -93,7 +93,7 @@ public actual val SystemFileSystem: FileSystem = object : SystemFileSystemImpl() override fun sink(path: Path, append: Boolean): RawSink = FileOutputStream(path.file, append).asSink() override fun resolve(path: Path): Path { - if (!path.file.exists()) throw FileNotFoundException() + if (!path.file.exists()) throw FileNotFoundException(path.file.absolutePath) return Path(path.file.canonicalFile) } }