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; 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..468a94317 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. + * 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 the [path] is pointing to 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..d6309c8ba 100644 --- a/core/common/test/files/SmokeFileTest.kt +++ b/core/common/test/files/SmokeFileTest.kt @@ -336,7 +336,40 @@ class SmokeFileTest { } assertEquals("second third", SystemFileSystem.source(path).buffered().use { it.readString() }) + } + + @Test + fun resolve() { + 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") + SystemFileSystem.createDirectories(tgt) + + 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")) + } } 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..0485dfc35 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(path.file.absolutePath) + 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(