aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSorin Basca <sorinbasca@google.com>2024-01-02 13:53:35 +0000
committerSorin Basca <sorinbasca@google.com>2024-01-02 13:53:35 +0000
commita75418b72b18516fe6a15bdcdeed2a3d74b55a5b (patch)
tree017105cdf675e5facc600b9a1ea09e15fabdac1f
parent71d77aa9ce22996dec43a463569dd5eee61fd063 (diff)
parent08af0faf025606573de5ea048d55cb857ca8f085 (diff)
downloadokio-a75418b72b18516fe6a15bdcdeed2a3d74b55a5b.tar.gz
Merge commit '08af0faf025606573de5ea048d55cb857ca8f085' into HEAD
Bug: 313924276 Test: m Change-Id: I4782c05e8bb232bc37b005e052909572d8099430
-rw-r--r--CHANGELOG.md29
-rw-r--r--METADATA4
-rw-r--r--android-test/build.gradle1
-rw-r--r--docs/releasing.md2
-rw-r--r--gradle.properties2
-rw-r--r--okio-fakefilesystem/README.md4
-rw-r--r--okio-fakefilesystem/build.gradle60
-rw-r--r--okio-fakefilesystem/gradle.properties2
-rw-r--r--okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt391
-rw-r--r--okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/time.kt42
-rw-r--r--okio/build.gradle2
-rw-r--r--okio/src/appleMain/kotlin/okio/posixVariant.kt45
-rw-r--r--okio/src/commonMain/kotlin/okio/-Platform.kt6
-rw-r--r--okio/src/commonMain/kotlin/okio/FileMetadata.kt106
-rw-r--r--okio/src/commonMain/kotlin/okio/FileSystem.kt360
-rw-r--r--okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt206
-rw-r--r--okio/src/commonMain/kotlin/okio/Path.kt213
-rw-r--r--okio/src/commonMain/kotlin/okio/internal/Path.kt266
-rw-r--r--okio/src/commonTest/kotlin/okio/AbstractFileSystemTest.kt670
-rw-r--r--okio/src/commonTest/kotlin/okio/FakeFileSystemTest.kt304
-rw-r--r--okio/src/commonTest/kotlin/okio/ForwardingFileSystemTest.kt146
-rw-r--r--okio/src/commonTest/kotlin/okio/PathTest.kt365
-rw-r--r--okio/src/commonTest/kotlin/okio/SystemFileSystemTest.kt28
-rw-r--r--okio/src/commonTest/kotlin/okio/time.kt39
-rw-r--r--okio/src/jsMain/kotlin/okio/-Platform.kt35
-rw-r--r--okio/src/jsMain/kotlin/okio/FileSink.kt50
-rw-r--r--okio/src/jsMain/kotlin/okio/FileSource.kt55
-rw-r--r--okio/src/jsMain/kotlin/okio/NodeJsFileSystem.kt159
-rw-r--r--okio/src/jsMain/kotlin/okio/fs.kt173
-rw-r--r--okio/src/jsMain/kotlin/okio/os.kt26
-rw-r--r--okio/src/jvmMain/kotlin/okio/-Platform.kt17
-rw-r--r--okio/src/jvmMain/kotlin/okio/JvmSystemFileSystem.kt103
-rw-r--r--okio/src/jvmMain/kotlin/okio/NioSystemFileSystem.kt76
-rw-r--r--okio/src/jvmMain/kotlin/okio/Path.kt106
-rw-r--r--okio/src/jvmTest/java/okio/FileSystemJavaTest.java96
-rw-r--r--okio/src/jvmTest/kotlin/okio/JvmTest.kt50
-rw-r--r--okio/src/linuxX64Main/kotlin/okio/posixVariant.kt45
-rw-r--r--okio/src/mingwX64Main/kotlin/okio/-Platform.kt37
-rw-r--r--okio/src/mingwX64Main/kotlin/okio/posixVariant.kt99
-rw-r--r--okio/src/mingwX64Main/kotlin/okio/windows.kt54
-rw-r--r--okio/src/nativeMain/kotlin/okio/-Platform.kt20
-rw-r--r--okio/src/nativeMain/kotlin/okio/FileSink.kt81
-rw-r--r--okio/src/nativeMain/kotlin/okio/FileSource.kt76
-rw-r--r--okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt111
-rw-r--r--okio/src/nativeMain/kotlin/okio/cinterop.kt68
-rw-r--r--okio/src/nativeMain/kotlin/okio/posixVariant.kt31
-rw-r--r--okio/src/nativeMain/kotlin/okio/sizetVariant.kt33
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/-Platform.kt2
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/Path.kt77
-rw-r--r--okio/src/sizet32Main/kotlin/okio/sizetVariant.kt39
-rw-r--r--okio/src/sizet64Main/kotlin/okio/sizetVariant.kt39
-rw-r--r--okio/src/unixMain/kotlin/okio/-Platform.kt29
-rw-r--r--okio/src/unixMain/kotlin/okio/posixVariant.kt66
-rw-r--r--settings.gradle1
54 files changed, 5143 insertions, 4 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48028d0a..d3ebcebb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,35 @@
Change Log
==========
+## Version 3.0.0-alpha.1
+
+_2021-01-07_
+
+* New: Experimental file system API. The `Path`, `FileMetadata`, `FileSystem` and
+ `ForwardingFileSystem` types are subject to API changes in a future release.
+* New: Experimental `okio-fakefilesystem` artifact.
+
+
+## Version 2.10.0
+
+_2021-01-07_
+
+* New: Support Windows (mingwX64) in multiplatform.
+* New: Support watchOS (watchosArm32, watchosArm64, watchosX86) in multiplatform.
+* New: Support `HashingSource`, `HashingSink`, buffer hash functions, and `UnsafeCursor` on non-JVM
+ platforms. Previously these were all JVM-only.
+* New: Implement `Closeable` on `Sink` and `Source` on non-JVM platforms. Okio now includes a
+ multiplatform `okio.Closeable` interface and corresponding `use {}` extension. Closing resources
+ when you're done with them shouldn't be JVM-only!
+* New: `Sink.hashingSink` and `Source.hashingSource` functions that accept
+ `java.security.MessageDigest` and `javax.crypto.Mac` instances. Use these when your hash function
+ isn't built-in.
+* Fix: Don't crash with a `ShortBufferException` in `CipherSink` and `CipherSource` on Android.
+ (Android may throw a `ShortBufferException` even if the buffer is not too short. We now
+ avoid this problem!)
+* Upgrade: [Kotlin 1.4.20][kotlin_1_4_20].
+
+
## Version 2.9.0
_2020-10-04_
diff --git a/METADATA b/METADATA
index 659dfff2..7ef910ae 100644
--- a/METADATA
+++ b/METADATA
@@ -14,7 +14,7 @@ third_party {
type: GIT
value: "https://github.com/square/okio/"
}
- version: "47fb0ddcd0bcf768a897dff723a1699341eea10f"
- last_upgrade_date { year: 2021 month: 4 day: 6 }
+ version: "08af0faf025606573de5ea048d55cb857ca8f085"
+ last_upgrade_date { year: 2024 month: 1 day: 2 }
license_type: NOTICE
}
diff --git a/android-test/build.gradle b/android-test/build.gradle
index 56fcda76..6e792b67 100644
--- a/android-test/build.gradle
+++ b/android-test/build.gradle
@@ -44,6 +44,7 @@ android {
sourceSets {
named("androidTest") {
it.java.srcDirs += [
+ project.file("../okio-fakefilesystem/src/commonMain/kotlin"),
project.file("../okio/src/commonMain/kotlin"),
project.file("../okio/src/commonTest/java"),
project.file("../okio/src/commonTest/kotlin"),
diff --git a/docs/releasing.md b/docs/releasing.md
index 94a1a34c..f0aac1cf 100644
--- a/docs/releasing.md
+++ b/docs/releasing.md
@@ -82,7 +82,7 @@ Cutting a Release
gradle.properties
sed -i "" \
"s/\"com.squareup.okio:\([^\:]*\):[^\"]*\"/\"com.squareup.okio:\1:$RELEASE_VERSION\"/g" \
- `find . -name "README.md"`
+ `find . -name "index.md"`
./gradlew clean publish
```
diff --git a/gradle.properties b/gradle.properties
index 21f92f07..0c575bbc 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -8,7 +8,7 @@ android.useAndroidX=true
systemProp.org.gradle.internal.publish.checksums.insecure=true
GROUP=com.squareup.okio
-VERSION_NAME=2.11.0-SNAPSHOT
+VERSION_NAME=3.0.0-SNAPSHOT
POM_DESCRIPTION=A modern I/O API for Java
diff --git a/okio-fakefilesystem/README.md b/okio-fakefilesystem/README.md
new file mode 100644
index 00000000..a5220da5
--- /dev/null
+++ b/okio-fakefilesystem/README.md
@@ -0,0 +1,4 @@
+Okio Testing (pre-release)
+--------------------------
+
+This module contains test implementations of Okio types.
diff --git a/okio-fakefilesystem/build.gradle b/okio-fakefilesystem/build.gradle
new file mode 100644
index 00000000..6e4d55c6
--- /dev/null
+++ b/okio-fakefilesystem/build.gradle
@@ -0,0 +1,60 @@
+apply plugin: 'org.jetbrains.kotlin.multiplatform'
+
+kotlin {
+ jvm {
+ withJava()
+ }
+ if (kmpJsEnabled) {
+ js {
+ configure([compilations.main, compilations.test]) {
+ tasks.getByName(compileKotlinTaskName).kotlinOptions {
+ moduleKind = "umd"
+ sourceMap = true
+ metaInfo = true
+ }
+ }
+ nodejs {
+ testTask {
+ useMocha {
+ timeout = "30s"
+ }
+ }
+ }
+ }
+ }
+ if (kmpNativeEnabled) {
+ iosX64()
+ iosArm64()
+ watchosArm32()
+ watchosArm64()
+ watchosX86()
+ // Required to generate tests tasks: https://youtrack.jetbrains.com/issue/KT-26547
+ linuxX64()
+ macosX64()
+ mingwX64()
+ }
+ sourceSets {
+ commonMain {
+ dependencies {
+ api deps.kotlin.stdLib.common
+ api deps.kotlin.time
+ api project(":okio")
+ }
+ }
+ }
+}
+
+tasks.withType(JavaCompile) {
+ options.encoding = 'UTF-8'
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+// modify these lines for MANIFEST.MF properties or for specific bnd instructions
+project.ext.bndManifest = '''
+ Export-Package: okio.fakefilesystem
+ Automatic-Module-Name: okio.fakefilesystem
+ Bundle-SymbolicName: com.squareup.okio.fakefilesystem
+ '''
+
+apply from: "$rootDir/okio/jvm/jvm.gradle"
+apply from: "$rootDir/gradle/gradle-mvn-mpp-push.gradle"
diff --git a/okio-fakefilesystem/gradle.properties b/okio-fakefilesystem/gradle.properties
new file mode 100644
index 00000000..1820218e
--- /dev/null
+++ b/okio-fakefilesystem/gradle.properties
@@ -0,0 +1,2 @@
+POM_ARTIFACT_ID=okio-fakefilesystem
+POM_NAME=Okio Fake File System
diff --git a/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt b/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt
new file mode 100644
index 00000000..fb426744
--- /dev/null
+++ b/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/FakeFileSystem.kt
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.fakefilesystem
+
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+import okio.Buffer
+import okio.ByteString
+import okio.ExperimentalFileSystem
+import okio.FileMetadata
+import okio.FileNotFoundException
+import okio.FileSystem
+import okio.IOException
+import okio.Path
+import okio.Path.Companion.toPath
+import okio.Sink
+import okio.Source
+import okio.Timeout
+import okio.fakefilesystem.FakeFileSystem.Element.Directory
+import okio.fakefilesystem.FakeFileSystem.Element.File
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmName
+
+/**
+ * A fully in-memory file system useful for testing. It includes features to support writing
+ * better tests.
+ *
+ * Use [openPaths] to see which paths have been opened for read or write, but not yet closed. Tests
+ * should call [checkNoOpenFiles] in `tearDown()` to confirm that no file streams were leaked.
+ *
+ * By default this file system permits deletion and removal of open files. Configure
+ * [windowsLimitations] to true to throw an [IOException] when asked to delete or rename an open
+ * file.
+ */
+@ExperimentalFileSystem
+class FakeFileSystem(
+ private val windowsLimitations: Boolean = false,
+ private val workingDirectory: Path = (if (windowsLimitations) "F:\\".toPath() else "/".toPath()),
+
+ @JvmField
+ val clock: Clock = Clock.System
+) : FileSystem() {
+
+ init {
+ require(workingDirectory.isAbsolute) {
+ "expected an absolute path but was $workingDirectory"
+ }
+ }
+
+ /** Keys are canonical paths. Each value is either a [Directory] or a [ByteString]. */
+ private val elements = mutableMapOf<Path, Element>()
+
+ /** Files that are currently open and need to be closed to avoid resource leaks. */
+ private val openFiles = mutableListOf<OpenFile>()
+
+ /**
+ * Canonical paths for every file and directory in this file system. This omits file system roots
+ * like `C:\` and `/`.
+ */
+ @get:JvmName("allPaths")
+ val allPaths: Set<Path>
+ get() {
+ val result = mutableListOf<Path>()
+ for (path in elements.keys) {
+ if (path.isRoot) continue
+ result += path
+ }
+ result.sort()
+ return result.toSet()
+ }
+
+ /**
+ * Canonical paths currently opened for reading or writing in the order they were opened. This may
+ * contain duplicates if a single path is open by multiple readers.
+ *
+ * Note that this may contain paths not present in [allPaths]. This occurs if a file is deleted
+ * while it is still open.
+ *
+ * The returned list is ordered by the order that the paths were opened.
+ */
+ @get:JvmName("openPaths")
+ val openPaths: List<Path>
+ get() = openFiles.map { it.canonicalPath }
+
+ /**
+ * Confirm that all files that have been opened on this file system (with [source], [sink], and
+ * [appendingSink]) have since been closed. Call this in your test's `tearDown()` function to
+ * confirm that your program hasn't leaked any open files.
+ *
+ * Forgetting to close a file on a real file system is a severe error that may lead to a program
+ * crash. The operating system enforces a limit on how many files may be open simultaneously. On
+ * Linux this is [getrlimit] and is commonly adjusted with the `ulimit` command.
+ *
+ * [getrlimit]: https://man7.org/linux/man-pages/man2/getrlimit.2.html
+ *
+ * @throws IllegalStateException if any files are open when this function is called.
+ */
+ fun checkNoOpenFiles() {
+ val firstOpenFile = openFiles.firstOrNull() ?: return
+ throw IllegalStateException(
+ """
+ |expected 0 open files, but found:
+ | ${openFiles.joinToString(separator = "\n ") { it.canonicalPath.toString() }}
+ """.trimMargin(),
+ firstOpenFile.backtrace
+ )
+ }
+
+ override fun canonicalize(path: Path): Path {
+ val canonicalPath = workingDirectory / path
+
+ if (canonicalPath !in elements) {
+ throw FileNotFoundException("no such file: $path")
+ }
+
+ return canonicalPath
+ }
+
+ override fun metadataOrNull(path: Path): FileMetadata? {
+ val canonicalPath = workingDirectory / path
+ var element = elements[canonicalPath]
+
+ // If the path is a root, create it on demand.
+ if (element == null && path.isRoot) {
+ element = Directory(createdAt = clock.now())
+ elements[path] = element
+ }
+
+ return element?.metadata
+ }
+
+ override fun list(dir: Path): List<Path> {
+ val canonicalPath = workingDirectory / dir
+ val element = requireDirectory(canonicalPath)
+
+ element.access(now = clock.now())
+ val paths = elements.keys.filterTo(mutableListOf()) { it.parent == canonicalPath }
+ paths.sort()
+ return paths
+ }
+
+ override fun source(file: Path): Source {
+ val canonicalPath = workingDirectory / file
+ val element = elements[canonicalPath] ?: throw FileNotFoundException("no such file: $file")
+
+ if (element !is File) {
+ throw IOException("not a file: $file")
+ }
+
+ val openFile = OpenFile(canonicalPath, Exception("file opened for reading here"))
+ openFiles += openFile
+ element.access(now = clock.now())
+ return FakeFileSource(openFile, Buffer().write(element.data))
+ }
+
+ override fun sink(file: Path): Sink {
+ return newSink(file, append = false)
+ }
+
+ override fun appendingSink(file: Path): Sink {
+ return newSink(file, append = true)
+ }
+
+ private fun newSink(file: Path, append: Boolean): Sink {
+ val canonicalPath = workingDirectory / file
+ val now = clock.now()
+
+ val existing = elements[canonicalPath]
+ if (existing is Directory) {
+ throw IOException("destination is a directory: $file")
+ }
+ val parent = requireDirectory(canonicalPath.parent)
+ parent.access(now, true)
+
+ val openFile = OpenFile(canonicalPath, Exception("file opened for writing here"))
+ openFiles += openFile
+ val regularFile = File(createdAt = existing?.createdAt ?: now)
+ val result = FakeFileSink(openFile, regularFile)
+ if (append && existing is File) {
+ result.buffer.write(existing.data)
+ regularFile.data = existing.data
+ }
+ elements[canonicalPath] = regularFile
+ regularFile.access(now = now, modified = true)
+ return result
+ }
+
+ override fun createDirectory(dir: Path) {
+ val canonicalPath = workingDirectory / dir
+
+ if (elements[canonicalPath] != null) {
+ throw IOException("already exists: $dir")
+ }
+ requireDirectory(canonicalPath.parent)
+
+ elements[canonicalPath] = Directory(createdAt = clock.now())
+ }
+
+ override fun atomicMove(source: Path, target: Path) {
+ val canonicalSource = workingDirectory / source
+ val canonicalTarget = workingDirectory / target
+
+ val targetElement = elements[canonicalTarget]
+ val sourceElement = elements[canonicalSource]
+
+ // Universal constraints.
+ if (targetElement is Directory) {
+ throw IOException("target is a directory: $target")
+ }
+ requireDirectory(canonicalTarget.parent)
+ if (windowsLimitations) {
+ // Windows-only constraints.
+ openFileOrNull(canonicalSource)?.let {
+ throw IOException("source is open $source", it.backtrace)
+ }
+ openFileOrNull(canonicalTarget)?.let {
+ throw IOException("target is open $target", it.backtrace)
+ }
+ } else {
+ // UNIX-only constraints.
+ if (sourceElement is Directory && targetElement is File) {
+ throw IOException("source is a directory and target is a file")
+ }
+ }
+
+ val removed = elements.remove(canonicalSource)
+ ?: throw FileNotFoundException("source doesn't exist: $source")
+ elements[canonicalTarget] = removed
+ }
+
+ override fun delete(path: Path) {
+ val canonicalPath = workingDirectory / path
+
+ if (elements.keys.any { it.parent == canonicalPath }) {
+ throw IOException("non-empty directory")
+ }
+
+ if (windowsLimitations) {
+ openFileOrNull(canonicalPath)?.let {
+ throw IOException("file is open $path", it.backtrace)
+ }
+ }
+
+ if (elements.remove(canonicalPath) == null) {
+ throw FileNotFoundException("no such file: $path")
+ }
+ }
+
+ /**
+ * Gets the directory at [path], creating it if [path] is a file system root.
+ *
+ * @throws IOException if the named directory is not a root and does not exist, or if it does
+ * exist but is not a directory.
+ */
+ private fun requireDirectory(path: Path?): Directory {
+ if (path == null) throw IOException("directory does not exist")
+
+ // If the path is a directory, return it!
+ val element = elements[path]
+ if (element is Directory) return element
+
+ // If the path is a root, create a directory for it on demand.
+ if (path.isRoot) {
+ val root = Directory(createdAt = clock.now())
+ elements[path] = root
+ return root
+ }
+
+ if (element == null) throw FileNotFoundException("no such directory: $path")
+
+ throw IOException("not a directory: $path")
+ }
+
+ private sealed class Element(
+ val createdAt: Instant
+ ) {
+ var lastModifiedAt: Instant = createdAt
+ var lastAccessedAt: Instant = createdAt
+
+ class File(createdAt: Instant) : Element(createdAt) {
+ var data: ByteString = ByteString.EMPTY
+
+ override val metadata: FileMetadata
+ get() = FileMetadata(
+ isRegularFile = true,
+ size = data.size.toLong(),
+ createdAt = createdAt,
+ lastModifiedAt = lastModifiedAt,
+ lastAccessedAt = lastAccessedAt
+ )
+ }
+
+ class Directory(createdAt: Instant) : Element(createdAt) {
+ override val metadata: FileMetadata
+ get() = FileMetadata(
+ isDirectory = true,
+ createdAt = createdAt,
+ lastModifiedAt = lastModifiedAt,
+ lastAccessedAt = lastAccessedAt
+ )
+ }
+
+ fun access(now: Instant, modified: Boolean = false) {
+ lastAccessedAt = now
+ if (modified) {
+ lastModifiedAt = now
+ }
+ }
+
+ abstract val metadata: FileMetadata
+ }
+
+ private fun openFileOrNull(canonicalPath: Path): OpenFile? {
+ return openFiles.firstOrNull { it.canonicalPath == canonicalPath }
+ }
+
+ private class OpenFile(
+ val canonicalPath: Path,
+ val backtrace: Throwable
+ )
+
+ /** Reads data from [buffer], removing itself from [openPathsMutable] when closed. */
+ private inner class FakeFileSource(
+ private val openFile: OpenFile,
+ private val buffer: Buffer
+ ) : Source {
+ private var closed = false
+
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ check(!closed) { "closed" }
+ return buffer.read(sink, byteCount)
+ }
+
+ override fun timeout() = Timeout.NONE
+
+ override fun close() {
+ if (closed) return
+ closed = true
+ openFiles -= openFile
+ }
+
+ override fun toString() = "source(${openFile.canonicalPath})"
+ }
+
+ /** Writes data to [path]. */
+ private inner class FakeFileSink(
+ private val openFile: OpenFile,
+ private val file: File
+ ) : Sink {
+ val buffer = Buffer()
+ private var closed = false
+
+ override fun write(source: Buffer, byteCount: Long) {
+ check(!closed) { "closed" }
+ buffer.write(source, byteCount)
+ }
+
+ override fun flush() {
+ check(!closed) { "closed" }
+ file.data = buffer.snapshot()
+ file.access(now = clock.now(), modified = true)
+ }
+
+ override fun timeout() = Timeout.NONE
+
+ override fun close() {
+ if (closed) return
+ closed = true
+ file.data = buffer.snapshot()
+ file.access(now = clock.now(), modified = true)
+ openFiles -= openFile
+ }
+
+ override fun toString() = "sink(${openFile.canonicalPath})"
+ }
+
+ override fun toString() = "FakeFileSystem"
+}
diff --git a/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/time.kt b/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/time.kt
new file mode 100644
index 00000000..d3f3e6b6
--- /dev/null
+++ b/okio-fakefilesystem/src/commonMain/kotlin/okio/fakefilesystem/time.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:JvmName("-Time")
+package okio.fakefilesystem
+
+import kotlinx.datetime.Instant
+import okio.ExperimentalFileSystem
+import okio.FileMetadata
+import kotlin.jvm.JvmName
+
+@JvmName("newFileMetadata")
+@ExperimentalFileSystem
+internal fun FileMetadata(
+ isRegularFile: Boolean = false,
+ isDirectory: Boolean = false,
+ size: Long? = null,
+ createdAt: Instant? = null,
+ lastModifiedAt: Instant? = null,
+ lastAccessedAt: Instant? = null
+): FileMetadata {
+ return FileMetadata(
+ isRegularFile = isRegularFile,
+ isDirectory = isDirectory,
+ size = size,
+ createdAtMillis = createdAt?.toEpochMilliseconds(),
+ lastModifiedAtMillis = lastModifiedAt?.toEpochMilliseconds(),
+ lastAccessedAtMillis = lastAccessedAt?.toEpochMilliseconds()
+ )
+}
diff --git a/okio/build.gradle b/okio/build.gradle
index 980ecb0a..24502c6c 100644
--- a/okio/build.gradle
+++ b/okio/build.gradle
@@ -81,6 +81,8 @@ kotlin {
implementation deps.kotlin.test.common
implementation deps.kotlin.test.annotations
implementation deps.kotlin.time
+
+ implementation project(":okio-fakefilesystem")
}
}
nonJvmMain {
diff --git a/okio/src/appleMain/kotlin/okio/posixVariant.kt b/okio/src/appleMain/kotlin/okio/posixVariant.kt
new file mode 100644
index 00000000..69bbd792
--- /dev/null
+++ b/okio/src/appleMain/kotlin/okio/posixVariant.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.alloc
+import kotlinx.cinterop.memScoped
+import kotlinx.cinterop.ptr
+import platform.posix.ENOENT
+import platform.posix.S_IFDIR
+import platform.posix.S_IFMT
+import platform.posix.S_IFREG
+import platform.posix.errno
+import platform.posix.stat
+
+@ExperimentalFileSystem
+internal actual fun PosixFileSystem.variantMetadataOrNull(path: Path): FileMetadata? {
+ return memScoped {
+ val stat = alloc<stat>()
+ if (platform.posix.lstat(path.toString(), stat.ptr) != 0) {
+ if (errno == ENOENT) return null
+ throw errnoToIOException(errno)
+ }
+ return@memScoped FileMetadata(
+ isRegularFile = stat.st_mode.toInt() and S_IFMT == S_IFREG,
+ isDirectory = stat.st_mode.toInt() and S_IFMT == S_IFDIR,
+ size = stat.st_size,
+ createdAtMillis = stat.st_ctimespec.epochMillis,
+ lastModifiedAtMillis = stat.st_mtimespec.epochMillis,
+ lastAccessedAtMillis = stat.st_atimespec.epochMillis
+ )
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/-Platform.kt b/okio/src/commonMain/kotlin/okio/-Platform.kt
index 4790d3cd..a2ae9262 100644
--- a/okio/src/commonMain/kotlin/okio/-Platform.kt
+++ b/okio/src/commonMain/kotlin/okio/-Platform.kt
@@ -16,6 +16,12 @@
package okio
+@ExperimentalFileSystem
+internal expect val PLATFORM_FILE_SYSTEM: FileSystem
+
+@ExperimentalFileSystem
+internal expect val PLATFORM_TEMPORARY_DIRECTORY: Path
+
internal expect fun ByteArray.toUtf8String(): String
internal expect fun String.asUtf8ToByteArray(): ByteArray
diff --git a/okio/src/commonMain/kotlin/okio/FileMetadata.kt b/okio/src/commonMain/kotlin/okio/FileMetadata.kt
new file mode 100644
index 00000000..4c9d2070
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/FileMetadata.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+/**
+ * Description of a file or another object referenced by a path.
+ *
+ * In simple use a file system is a mechanism for organizing files and directories on a local
+ * storage device. In practice file systems are more capable and their contents more varied. For
+ * example, a path may refer to:
+ *
+ * * An operating system process that consumes data, produces data, or both. For example, reading
+ * from the `/dev/urandom` file on Linux returns a unique sequence of pseudorandom bytes to each
+ * reader.
+ *
+ * * A stream that connects a pair of programs together. A pipe is a special file that a producing
+ * program writes to and a consuming program reads from. Both programs operate concurrently. The
+ * size of a pipe is not well defined: the writer can write as much data as the reader is able to
+ * read.
+ *
+ * * A file on a remote file system. The performance and availability of remote files may be quite
+ * different from that of local files!
+ *
+ * * A symbolic link (symlink) to another path. When attempting to access this path the file system
+ * will follow the link and return data from the target path.
+ *
+ * * The same content as another path without a symlink. On UNIX file systems an inode is an
+ * anonymous handle to a file's content, and multiple paths may target the same inode without any
+ * other relationship to one another. A consequence of this design is that a directory with three
+ * 1 GiB files may only need 1 GiB on the storage device.
+ *
+ * This class does not attempt to model these rich file system features! It exposes a limited view
+ * useful for programs with only basic file system needs. Be cautious of the potential consequences
+ * of special files when writing programs that operate on a file system.
+ *
+ * File metadata is subject to change, and code that operates on file systems should defend against
+ * changes to the file that occur between reading metadata and subsequent operations.
+ */
+@ExperimentalFileSystem
+class FileMetadata(
+ /** True if this file is a container of bytes. If this is true, then [size] is non-null. */
+ val isRegularFile: Boolean,
+
+ /** True if the path refers to a directory that contains 0 or more child paths. */
+ val isDirectory: Boolean,
+
+ /**
+ * Returns the number of bytes readable from this file. The amount of storage resources consumed
+ * by this file may be larger (due to block size overhead, redundant copies for RAID, etc.), or
+ * smaller (due to file system compression, shared inodes, etc).
+ */
+ val size: Long?,
+
+ /**
+ * Returns the system time of the host computer when this file was created, if the host file
+ * system supports this feature. This is typically available on Windows NTFS file systems and not
+ * available on UNIX or Windows FAT file systems.
+ */
+ val createdAtMillis: Long?,
+
+ /**
+ * Returns the system time of the host computer when this file was most recently written.
+ *
+ * Note that the accuracy of the returned time may be much more coarse than its precision. In
+ * particular, this value is expressed with millisecond precision but may be accessed at
+ * second- or day-accuracy only.
+ */
+ val lastModifiedAtMillis: Long?,
+
+ /**
+ * Returns the system time of the host computer when this file was most recently read or written.
+ *
+ * Note that the accuracy of the returned time may be much more coarse than its precision. In
+ * particular, this value is expressed with millisecond precision but may be accessed at
+ * second- or day-accuracy only.
+ */
+ val lastAccessedAtMillis: Long?
+) {
+ override fun equals(other: Any?) = other is FileMetadata && toString() == other.toString()
+
+ override fun hashCode() = toString().hashCode()
+
+ override fun toString(): String {
+ val fields = mutableListOf<String>()
+ if (isRegularFile) fields += "isRegularFile"
+ if (isDirectory) fields += "isDirectory"
+ if (size != null) fields += "byteCount=$size"
+ if (createdAtMillis != null) fields += "createdAt=$createdAtMillis"
+ if (lastModifiedAtMillis != null) fields += "lastModifiedAt=$lastModifiedAtMillis"
+ if (lastAccessedAtMillis != null) fields += "lastAccessedAt=$lastAccessedAtMillis"
+ return fields.joinToString(separator = ", ", prefix = "FileMetadata(", postfix = ")")
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/FileSystem.kt b/okio/src/commonMain/kotlin/okio/FileSystem.kt
new file mode 100644
index 00000000..8bf42f8a
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/FileSystem.kt
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.FileSystem.Companion.SYSTEM
+import kotlin.jvm.JvmField
+
+/**
+ * Read and write access to a hierarchical collection of files, addressed by [paths][Path]. This
+ * is a natural interface to the [current computer's local file system][SYSTEM].
+ *
+ * Not Just the Local File System
+ * ------------------------------
+ *
+ * Other implementations are possible:
+ *
+ * * `FakeFileSystem` is an in-memory file system suitable for testing. Note that this class is
+ * included in the `okio-fakefilesystem` artifact.
+ *
+ * * A ZIP file system could provide access to the contents of a `.zip` file.
+ *
+ * * A remote file system could access files over the network.
+ *
+ * * A [decorating file system][ForwardingFileSystem] could apply monitoring, encryption,
+ * compression, or filtering to another file system implementation.
+ *
+ * For improved capability and testability, consider structuring your classes to dependency inject
+ * a `FileSystem` rather than using [SYSTEM] directly.
+ *
+ * Limited API
+ * -----------
+ *
+ * This interface is limited in which file system features it supports. Applications that need rich
+ * file system features should use another API, possibly alongside this API.
+ *
+ * This class cannot create special file types like hard links, symlinks, pipes, or mounts. Reading
+ * or writing these files works as if they were regular files.
+ *
+ * It cannot read or write file access control features like the UNIX `chmod` and Windows access
+ * control lists. It does honor these controls and will fail with an [IOException] if privileges
+ * are insufficient!
+ *
+ * It cannot lock files, or query which files are locked.
+ *
+ * It cannot watch the file system for changes.
+ *
+ * Multiplatform
+ * -------------
+ *
+ * This class supports a matrix of Kotlin platforms (JVM, Kotlin/Native, Kotlin/JS) and operating
+ * systems (Linux, macOS, and Windows). It attempts to balance working similarly across platforms
+ * with being consistent with the local operating system.
+ *
+ * This is a blocking API which limits its applicability on concurrent Node.js services. File
+ * operations will block the event loop (and all JavaScript execution!) until they complete.
+ *
+ * It supports the path schemes of both Windows (like `C:\Users`) and UNIX (like `/home`). Note that
+ * path resolution rules differ by platform.
+ *
+ * Differences vs. Java IO APIs
+ * ----------------------------
+ *
+ * The `java.io.File` class is Java's original file system API. The `delete` and `renameTo` methods
+ * return false if the operation failed. The `list` method returns null if the file isn't a
+ * directory or could not be listed. This class always throws `IOExceptions` when operations don't
+ * succeed.
+ *
+ * The `java.nio.Path` and `java.nio.Files` classes are the entry points of Java's new file system
+ * API. Each `Path` instance is scoped to a particular file system, though that is often implicit
+ * because the `Paths.get()` function automatically uses the default (ie. system) file system.
+ * In Okio's API paths are just identifiers; you must use a specific `FileSystem` object to do
+ * I/O with.
+ */
+@ExperimentalFileSystem
+abstract class FileSystem {
+ /**
+ * Resolves [path] against the current working directory and symlinks in this file system. The
+ * returned path identifies the same file as [path], but with an absolute path that does not
+ * include any symbolic links.
+ *
+ * This is similar to `File.getCanonicalFile()` on the JVM and `realpath` on POSIX. Unlike
+ * `File.getCanonicalFile()`, this throws if the file doesn't exist.
+ *
+ * @throws IOException if [path] cannot be resolved. This will occur if the file doesn't exist,
+ * if the current working directory doesn't exist or is inaccessible, or if another failure
+ * occurs while resolving the path.
+ */
+ @Throws(IOException::class)
+ abstract fun canonicalize(path: Path): Path
+
+ /**
+ * Returns metadata of the file, directory, or object identified by [path].
+ *
+ * @throws IOException if [path] does not exist or its metadata cannot be read.
+ */
+ @Throws(IOException::class)
+ fun metadata(path: Path): FileMetadata {
+ return metadataOrNull(path) ?: throw FileNotFoundException("no such file: $path")
+ }
+
+ /**
+ * Returns metadata of the file, directory, or object identified by [path]. This returns null if
+ * there is no file at [path].
+ *
+ * @throws IOException if [path] cannot be accessed due to a connectivity problem, permissions
+ * problem, or other issue.
+ */
+ @Throws(IOException::class)
+ abstract fun metadataOrNull(path: Path): FileMetadata?
+
+ /**
+ * Returns true if [path] identifies an object on this file system.
+ *
+ * @throws IOException if [path] cannot be accessed due to a connectivity problem, permissions
+ * problem, or other issue.
+ */
+ @Throws(IOException::class)
+ fun exists(path: Path): Boolean {
+ return metadataOrNull(path) != null
+ }
+
+ /**
+ * Returns the children of the directory identified by [dir]. The returned list is sorted using
+ * natural ordering.
+ *
+ * @throws IOException if [dir] does not exist, is not a directory, or cannot be read. A directory
+ * cannot be read if the current process doesn't have access to [dir], or if there's a loop of
+ * symbolic links, or if any name is too long.
+ */
+ @Throws(IOException::class)
+ abstract fun list(dir: Path): List<Path>
+
+ /**
+ * Returns a source that reads the bytes of [file] from beginning to end.
+ *
+ * @throws IOException if [file] does not exist, is not a file, or cannot be read. A file cannot
+ * be read if the current process doesn't have access to [file], if there's a loop of symbolic
+ * links, or if any name is too long.
+ */
+ @Throws(IOException::class)
+ abstract fun source(file: Path): Source
+
+ /**
+ * Creates a source to read [file], executes [readerAction] to read it, and then closes the
+ * source. This is a compact way to read the contents of a file.
+ */
+ inline fun <T> read(file: Path, readerAction: BufferedSource.() -> T): T {
+ return source(file).buffer().use {
+ it.readerAction()
+ }
+ }
+
+ /**
+ * Returns a sink that writes bytes to [file] from beginning to end. If [file] already exists it
+ * will be replaced with the new data.
+ *
+ * @throws IOException if [file] cannot be written. A file cannot be written if its enclosing
+ * directory does not exist, if the current process doesn't have access to [file], if there's
+ * a loop of symbolic links, or if any name is too long.
+ */
+ @Throws(IOException::class)
+ abstract fun sink(file: Path): Sink
+
+ /**
+ * Creates a sink to write [file], executes [writerAction] to write it, and then closes the sink.
+ * This is a compact way to write a file.
+ */
+ inline fun <T> write(file: Path, writerAction: BufferedSink.() -> T): T {
+ return sink(file).buffer().use {
+ it.writerAction()
+ }
+ }
+
+ /**
+ * Returns a sink that appends bytes to the end of [file], creating it if it doesn't already
+ * exist.
+ *
+ * @throws IOException if [file] cannot be written. A file cannot be written if its enclosing
+ * directory does not exist, if the current process doesn't have access to [file], if there's
+ * a loop of symbolic links, or if any name is too long.
+ */
+ @Throws(IOException::class)
+ abstract fun appendingSink(file: Path): Sink
+
+ /**
+ * Creates a directory at the path identified by [dir].
+ *
+ * @throws IOException if [dir]'s parent does not exist, is not a directory, or cannot be written.
+ * A directory cannot be created if it already exists, if the current process doesn't have
+ * access, if there's a loop of symbolic links, or if any name is too long.
+ */
+ @Throws(IOException::class)
+ abstract fun createDirectory(dir: Path)
+
+ /**
+ * Creates a directory at the path identified by [dir], and any enclosing parent path directories,
+ * recursively.
+ *
+ * @throws IOException if any [metadata] or [createDirectory] operation fails.
+ */
+ @Throws(IOException::class)
+ fun createDirectories(dir: Path) {
+ // Compute the sequence of directories to create.
+ val directories = ArrayDeque<Path>()
+ var path: Path? = dir
+ while (path != null && !exists(path)) {
+ directories.addFirst(path)
+ path = path.parent
+ }
+
+ // Create them.
+ for (toCreate in directories) {
+ createDirectory(toCreate)
+ }
+ }
+
+ /**
+ * Moves [source] to [target] in-place if the underlying file system supports it. If [target]
+ * exists, it is first removed. If `source == target`, this operation does nothing. This may be
+ * used to move a file or a directory.
+ *
+ * **Only as Atomic as the Underlying File System Supports**
+ *
+ * FAT and NTFS file systems cannot atomically move a file over an existing file. If the target
+ * file already exists, the move is performed into two steps:
+ *
+ * 1. Atomically delete the target file.
+ * 2. Atomically rename the source file to the target file.
+ *
+ * The delete step and move step are each atomic but not atomic in aggregate! If this process
+ * crashes, the host operating system crashes, or the hardware fails it is possible that the
+ * delete step will succeed and the rename will not.
+ *
+ * **Entire-file or nothing**
+ *
+ * These are the possible results of this operation:
+ *
+ * * This operation returns normally, the source file is absent, and the target file contains the
+ * data previously held by the source file. This is the success case.
+ *
+ * * The operation throws an [IOException] and the file system is unchanged. For example, this
+ * occurs if this process lacks permissions to perform the move.
+ *
+ * * This operation throws an [IOException], the target file is deleted, but the source file is
+ * unchanged. This is the partial failure case described above and is only possible on
+ * file systems like FAT and NTFS that do not support atomic file replacement. Typically in
+ * such cases this operation won't return at all because the process or operating system has
+ * also crashed.
+ *
+ * There is no failure mode where the target file holds a subset of the bytes of the source file.
+ * If the rename step cannot be performed atomically, this function will throw an [IOException]
+ * before attempting a move. Typically this occurs if the source and target files are on different
+ * physical volumes.
+ *
+ * **Non-Atomic Moves**
+ *
+ * If you need to move files across volumes, use [copy] followed by [delete], and change your
+ * application logic to recover should the copy step suffer a partial failure.
+ *
+ * @throws IOException if the move cannot be performed, or cannot be performed atomically. Moves
+ * fail if the source doesn't exist, if the target is not writable, if the target already
+ * exists and cannot be replaced, or if the move would cause physical or quota limits to be
+ * exceeded. This list of potential problems is not exhaustive.
+ */
+ @Throws(IOException::class)
+ abstract fun atomicMove(source: Path, target: Path)
+
+ /**
+ * Copies all of the bytes from the file at [source] to the file at [target]. This does not copy
+ * file metadata like last modified time, permissions, or extended attributes.
+ *
+ * This function is not atomic; a failure may leave [target] in an inconsistent state. For
+ * example, [target] may be empty or contain only a prefix of [source].
+ *
+ * @throws IOException if [source] cannot be read or if [target] cannot be written.
+ */
+ @Throws(IOException::class)
+ open fun copy(source: Path, target: Path) {
+ source(source).use { bytesIn ->
+ sink(target).buffer().use { bytesOut ->
+ bytesOut.writeAll(bytesIn)
+ }
+ }
+ }
+
+ /**
+ * Deletes the file or directory at [path].
+ *
+ * @throws IOException if there is nothing at [path] to delete, or if there is a file or directory
+ * but it could not be deleted. Deletes fail if the current process doesn't have access, if
+ * the file system is readonly, or if [path] is a non-empty directory. This list of potential
+ * problems is not exhaustive.
+ */
+ @Throws(IOException::class)
+ abstract fun delete(path: Path)
+
+ /**
+ * Recursively deletes all children of [fileOrDirectory] if it is a directory, then deletes
+ * [fileOrDirectory] itself.
+ *
+ * This function does not defend against race conditions. For example, if child files are created
+ * or deleted in [fileOrDirectory] while this function is executing, this may fail with an
+ * [IOException].
+ *
+ * @throws IOException if any [metadata], [list], or [delete] operation fails.
+ */
+ @Throws(IOException::class)
+ open fun deleteRecursively(fileOrDirectory: Path) {
+ val stack = ArrayDeque<Path>()
+ stack += fileOrDirectory
+
+ while (stack.isNotEmpty()) {
+ val toDelete = stack.removeLast()
+
+ val metadata = metadata(toDelete)
+ val children = if (metadata.isDirectory) list(toDelete) else listOf()
+
+ if (children.isNotEmpty()) {
+ stack += toDelete
+ stack += children
+ } else {
+ delete(toDelete)
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * The current process's host file system. Use this instance directly, or dependency inject a
+ * [FileSystem] to make code testable.
+ */
+ @JvmField
+ val SYSTEM: FileSystem = PLATFORM_FILE_SYSTEM
+
+ /**
+ * Returns a writable temporary directory on [SYSTEM].
+ *
+ * This is platform-specific.
+ *
+ * * **JVM and Android**: the path in the `java.io.tmpdir` system property
+ * * **Linux, iOS, and macOS**: the path in the `TMPDIR` environment variable.
+ * * **Windows**: the first non-null of `TEMP`, `TMP`, and `USERPROFILE` environment variables.
+ */
+ @JvmField
+ val SYSTEM_TEMPORARY_DIRECTORY: Path = PLATFORM_TEMPORARY_DIRECTORY
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt b/okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt
new file mode 100644
index 00000000..f6dddbb7
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlin.jvm.JvmName
+
+/**
+ * A [FileSystem] that forwards calls to another, intended for subclassing.
+ *
+ * ### Fault Injection
+ *
+ * You can use this to deterministically trigger file system failures in tests. This is useful to
+ * confirm that your program behaves correctly even if its file system operations fail. For example,
+ * this subclass fails every access of files named `unlucky.txt`:
+ *
+ * ```
+ * val faultyFileSystem = object : ForwardingFileSystem(FileSystem.SYSTEM) {
+ * override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
+ * if (path.name == "unlucky.txt") throw IOException("synthetic failure!")
+ * return path
+ * }
+ * }
+ * ```
+ *
+ * You can fail specific operations by overriding them directly:
+ *
+ * ```
+ * val faultyFileSystem = object : ForwardingFileSystem(FileSystem.SYSTEM) {
+ * override fun delete(path: Path) {
+ * throw IOException("synthetic failure!")
+ * }
+ * }
+ * ```
+ *
+ * ### Observability
+ *
+ * You can extend this to verify which files your program accesses. This is a testing file system
+ * that records accesses as they happen:
+ *
+ * ```
+ * class LoggingFileSystem : ForwardingFileSystem(FileSystem.SYSTEM) {
+ * val log = mutableListOf<String>()
+ *
+ * override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
+ * log += "$functionName($parameterName=$path)"
+ * return path
+ * }
+ * }
+ * ```
+ *
+ * This makes it easy for tests to assert exactly which files were accessed.
+ *
+ * ```
+ * @Test
+ * fun testMergeJsonReports() {
+ * createSampleJsonReports()
+ * loggingFileSystem.log.clear()
+ *
+ * mergeJsonReports()
+ *
+ * assertThat(loggingFileSystem.log).containsExactly(
+ * "list(dir=json_reports)",
+ * "source(file=json_reports/2020-10.json)",
+ * "source(file=json_reports/2020-12.json)",
+ * "source(file=json_reports/2020-11.json)",
+ * "sink(file=json_reports/2020-all.json)"
+ * )
+ * }
+ * ```
+ *
+ * ### Transformations
+ *
+ * Subclasses can transform file names and content.
+ *
+ * For example, your program may be written to operate on a well-known directory like `/etc/` or
+ * `/System`. You can rewrite paths to make such operations safer to test.
+ *
+ * You may also transform file content to apply application-layer encryption or compression. This
+ * is particularly useful in situations where it's difficult or impossible to enable those features
+ * in the underlying file system.
+ *
+ * ### Abstract Functions Only
+ *
+ * Some file system functions like [copy] are implemented by using other features. These are the
+ * non-abstract functions in the [FileSystem] interface.
+ *
+ * **This class forwards only the abstract functions;** non-abstract functions delegate to the
+ * other functions of this class. If desired, subclasses may override non-abstract functions to
+ * forward them.
+ */
+@ExperimentalFileSystem
+abstract class ForwardingFileSystem(
+ /** [FileSystem] to which this instance is delegating. */
+ @get:JvmName("delegate")
+ val delegate: FileSystem
+) : FileSystem() {
+
+ /**
+ * Invoked each time a path is passed as a parameter to this file system. This returns the path to
+ * pass to [delegate], which should be [path] itself or a path on [delegate] that corresponds to
+ * it.
+ *
+ * Subclasses may override this to log accesses, fail on unexpected accesses, or map paths across
+ * file systems.
+ *
+ * The base implementation returns [path].
+ *
+ * Note that this function will be called twice for calls to [atomicMove]; once for the source
+ * file and once for the target file.
+ *
+ * @param path the path passed to any of the functions of this.
+ * @param functionName a string like "canonicalize", "metadataOrNull", or "appendingSink".
+ * @param parameterName a string like "path", "file", "source", or "target".
+ * @return the path to pass to [delegate] for the same parameter.
+ */
+ open fun onPathParameter(path: Path, functionName: String, parameterName: String): Path = path
+
+ /**
+ * Invoked each time a path is returned by [delegate]. This returns the path to return to the
+ * caller, which should be [path] itself or a path on this that corresponds to it.
+ *
+ * Subclasses may override this to log accesses, fail on unexpected path accesses, or map
+ * directories or path names.
+ *
+ * The base implementation returns [path].
+ *
+ * @param path the path returned by any of the functions of this.
+ * @param functionName a string like "canonicalize" or "list".
+ * @return the path to return to the caller.
+ */
+ open fun onPathResult(path: Path, functionName: String): Path = path
+
+ @Throws(IOException::class)
+ override fun canonicalize(path: Path): Path {
+ val path = onPathParameter(path, "canonicalize", "path")
+ val result = delegate.canonicalize(path)
+ return onPathResult(result, "canonicalize")
+ }
+
+ @Throws(IOException::class)
+ override fun metadataOrNull(path: Path): FileMetadata? {
+ val path = onPathParameter(path, "metadataOrNull", "path")
+ return delegate.metadataOrNull(path)
+ }
+
+ @Throws(IOException::class)
+ override fun list(dir: Path): List<Path> {
+ val dir = onPathParameter(dir, "list", "dir")
+ val result = delegate.list(dir)
+ val paths = result.mapTo(mutableListOf()) { onPathResult(it, "list") }
+ paths.sort()
+ return paths
+ }
+
+ @Throws(IOException::class)
+ override fun source(file: Path): Source {
+ val file = onPathParameter(file, "source", "file")
+ return delegate.source(file)
+ }
+
+ @Throws(IOException::class)
+ override fun sink(file: Path): Sink {
+ val file = onPathParameter(file, "sink", "file")
+ return delegate.sink(file)
+ }
+
+ @Throws(IOException::class)
+ override fun appendingSink(file: Path): Sink {
+ val file = onPathParameter(file, "appendingSink", "file")
+ return delegate.appendingSink(file)
+ }
+
+ @Throws(IOException::class)
+ override fun createDirectory(dir: Path) {
+ val dir = onPathParameter(dir, "createDirectory", "dir")
+ delegate.createDirectory(dir)
+ }
+
+ @Throws(IOException::class)
+ override fun atomicMove(source: Path, target: Path) {
+ val source = onPathParameter(source, "atomicMove", "source")
+ val target = onPathParameter(target, "atomicMove", "target")
+ delegate.atomicMove(source, target)
+ }
+
+ @Throws(IOException::class)
+ override fun delete(path: Path) {
+ val path = onPathParameter(path, "delete", "path")
+ delegate.delete(path)
+ }
+
+ override fun toString() = "${this::class.simpleName}($delegate)"
+}
diff --git a/okio/src/commonMain/kotlin/okio/Path.kt b/okio/src/commonMain/kotlin/okio/Path.kt
new file mode 100644
index 00000000..ff865167
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/Path.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.Path.Companion.toPath
+
+/**
+ * A hierarchical address on a file system. A path is an identifier only; a [FileSystem] is required
+ * to access the file that a path refers to, if any.
+ *
+ * UNIX and Windows Paths
+ * ----------------------
+ *
+ * Paths follow different rules on UNIX vs. Windows operating systems. On UNIX operating systems
+ * (including Linux, Android, macOS, and iOS), the `/` slash character separates path segments. On
+ * Windows, the `\` backslash character separates path segments. The two platforms each have their
+ * own rules for path resolution. This class implements all rules on all platforms; for example you
+ * can model a Linux path in a native Windows application.
+ *
+ * Absolute and Relative Paths
+ * ---------------------------
+ *
+ * * **Absolute paths** identify a location independent of any working directory. On UNIX, absolute
+ * paths are prefixed with a slash, `/`. On Windows, absolute paths are one of two forms. The
+ * first is a volume letter, a colon, and a backslash, like `C:\`. The second is called a
+ * Universal Naming Convention (UNC) path, and it is prefixed by two backslashes `\\`. The term
+ * ‘fully-qualified path’ is a synonym of ‘absolute path’.
+ *
+ * * **Relative paths** are everything else. On their own, relative paths do not identify a
+ * location on a file system; they are relative to the system's current working directory. Use
+ * [FileSystem.canonicalize] to convert a relative path to its absolute path on a particular
+ * file system.
+ *
+ * There are some special cases when working with relative paths.
+ *
+ * On Windows, each volume (like `A:\` and `C:\`) has its own current working directory. A path
+ * prefixed with a volume letter and colon but no slash (like `A:letter.doc`) is relative to the
+ * working directory on the named volume. For example, if the working directory on `A:\` is
+ * `A:\jesse`, then the path `A:letter.doc` resolves to `A:\jesse\letter.doc`.
+ *
+ * The path string `C:\Windows` is an absolute path when following Windows rules and a relative
+ * path when following UNIX rules. For example, if the current working directory is
+ * `/Users/jesse`, then `C:\Windows` resolves to `/Users/jesse/C:/Windows`.
+ *
+ * This class decides which rules to follow by inspecting the first slash character in the path
+ * string. If the path contains no slash characters, it uses the host platform's rules. Or you may
+ * explicitly specify which rules to use by specifying the `directorySeparator` parameter in
+ * [toPath]. Pass `"/"` to get UNIX rules and `"\"` to get Windows rules.
+ *
+ * Path Traversal
+ * --------------
+ *
+ * After the optional path root (like `/` on UNIX, like `X:\` or `\\` on Windows), the remainder of
+ * the path is a sequence of segments separated by `/` or `\` characters. Segments satisfy these
+ * rules:
+ *
+ * * Segments are always non-empty.
+ * * If the segment is `.`, then the full path must be `.`.
+ * * If the segment is `..`, then the the path must be relative. All `..` segments precede all
+ * other segments.
+ *
+ * The only path that ends with `/` is the file system root, `/`. The dot path `.` is a relative
+ * path that resolves to whichever path it is resolved against.
+ *
+ * The [name] is the last segment in a path. It is typically a file or directory name, like
+ * `README.md` or `Desktop`. The name may be another special value:
+ *
+ * * The empty string is the name of the file system root path (full path `/`).
+ * * `.` is the name of the identity relative path (full path `.`).
+ * * `..` is the name of a path consisting of only `..` segments (such as `../../..`).
+ *
+ * Comparing Paths
+ * ---------------
+ *
+ * Path implements [Comparable], [equals], and [hashCode]. If two paths are equal then they operate
+ * on the same file on the file system.
+ *
+ * Note that the converse is not true: **if two paths are non-equal, they may still resolve to the
+ * same file on the file system.** Here are some of the ways non-equal paths resolve to the same
+ * file:
+ *
+ * * **Case differences.** The default file system on macOS is case-insensitive. The paths
+ * `/Users/jesse/notes.txt` and `/USERS/JESSE/NOTES.TXT` are non-equal but these paths resolve to
+ * the same file.
+ * * **Mounting differences.** Volumes may be mounted at multiple paths. On macOS,
+ * `/Users/jesse/notes.txt` and `/Volumes/Macintosh HD/Users/jesse/notes.txt` typically resolve
+ * to the same file. On Windows, `C:\project\notes.txt` and `\\localhost\c$\project\notes.txt`
+ * typically resolve to the same file.
+ * * **Hard links.** UNIX file systems permit multiple paths to refer for same file. The paths may
+ * be wildly different, like `/Users/jesse/bruce_wayne.vcard` and
+ * `/Users/jesse/batman.vcard`, but changes via either path are reflected in both.
+ * * **Symlinks.** Symlinks permit multiple paths and directories to refer to the same file. On
+ * macOS `/tmp` is symlinked to `/private/tmp`, so `/tmp/notes.txt` and `/private/tmp/notes.txt`
+ * resolve to the same file.
+ *
+ * To test whether two paths refer to the same file, try [FileSystem.canonicalize] first. This
+ * follows symlinks and looks up the preserved casing for case-insensitive case-preserved paths.
+ * **This method does not guarantee a unique result, however.** For example, each hard link to a
+ * file may return its own canonical path.
+ *
+ * Paths are sorted in case-sensitive order.
+ *
+ * Sample Paths
+ * ------------
+ *
+ * <table>
+ * <tr><th> Path <th> Parent <th> Name <th> Notes </tr>
+ * <tr><td> `/` <td> null <td> (empty) <td> root </tr>
+ * <tr><td> `/home/jesse/notes.txt` <td> `/home/jesse` <td> `notes.txt` <td> absolute path </tr>
+ * <tr><td> `project/notes.txt` <td> `project` <td> `notes.txt` <td> relative path </tr>
+ * <tr><td> `../../project/notes.txt` <td> `../../project` <td> `notes.txt` <td> relative path with traversal </tr>
+ * <tr><td> `../../..` <td> null <td> `..` <td> relative path with traversal </tr>
+ * <tr><td> `.` <td> null <td> `.` <td> current working directory </tr>
+ * <tr><td> `C:\` <td> null <td> (empty) <td> volume root (Windows) </tr>
+ * <tr><td> `C:\Windows\notepad.exe` <td> `C:\Windows` <td> `notepad.exe` <td> volume absolute path (Windows) </tr>
+ * <tr><td> `\` <td> null <td> (empty) <td> absolute path (Windows) </tr>
+ * <tr><td> `\Windows\notepad.exe` <td> `\Windows` <td> `notepad.exe` <td> absolute path (Windows) </tr>
+ * <tr><td> `C:` <td> null <td> (empty) <td> volume-relative path (Windows) </tr>
+ * <tr><td> `C:project\notes.txt` <td> `C:project` <td> `notes.txt` <td> volume-relative path (Windows) </tr>
+ * <tr><td> `\\server` <td> null <td> `server` <td> UNC server (Windows) </tr>
+ * <tr><td> `\\server\project\notes.txt` <td> `\\server\project` <td> `notes.txt` <td> UNC absolute path (Windows) </tr>
+ * </table>
+ */
+@ExperimentalFileSystem
+expect class Path internal constructor(slash: ByteString, bytes: ByteString) : Comparable<Path> {
+ internal val slash: ByteString
+ internal val bytes: ByteString
+
+ val isAbsolute: Boolean
+
+ val isRelative: Boolean
+
+ /**
+ * This is the volume letter like "C" on Windows paths that starts with a volume letter. For
+ * example, on the path "C:\Windows" this returns "C". This property is null if this is not a
+ * Windows path, or if it doesn't have a volume letter.
+ *
+ * Note that paths that start with a volume letter are not necessarily absolute paths. For
+ * example, the path "C:notepad.exe" is relative to whatever the current working directory is on
+ * the C: drive.
+ */
+ val volumeLetter: Char?
+
+ val nameBytes: ByteString
+
+ val name: String
+
+ /**
+ * Returns the path immediately enclosing this path.
+ *
+ * This returns null if this has no parent. That includes these paths:
+ *
+ * * The file system root (`/`)
+ * * The identity relative path (`.`)
+ * * A Windows volume root (like `C:\`)
+ * * A Windows Universal Naming Convention (UNC) root path (`\\server`)
+ * * A reference to the current working directory on a Windows volume (`C:`).
+ * * A series of relative paths (like `..` and `../..`).
+ */
+ val parent: Path?
+
+ /**
+ * Returns true if this is an absolute path with no parent. UNIX paths have a single root, `/`.
+ * Each volume on Windows is its own root, like `C:\` and `D:\`. Windows UNC paths like `\\server`
+ * are also roots.
+ */
+ val isRoot: Boolean
+
+ /**
+ * Returns a path that resolves [child] relative to this path.
+ *
+ * If [child] is an [absolute path][isAbsolute] or [has a volume letter][hasVolumeLetter] then
+ * this function is equivalent to `child.toPath()`.
+ */
+ operator fun div(child: String): Path
+
+ /**
+ * Returns a path that resolves [child] relative to this path.
+ *
+ * If [child] is an [absolute path][isAbsolute] or [has a volume letter][hasVolumeLetter] then
+ * this function is equivalent to `child.toPath()`.
+ */
+ operator fun div(child: Path): Path
+
+ override fun compareTo(other: Path): Int
+
+ override fun equals(other: Any?): Boolean
+
+ override fun hashCode(): Int
+
+ override fun toString(): String
+
+ companion object {
+ val DIRECTORY_SEPARATOR: String
+
+ fun String.toPath(): Path
+
+ fun String.toPath(directorySeparator: String?): Path
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/internal/Path.kt b/okio/src/commonMain/kotlin/okio/internal/Path.kt
new file mode 100644
index 00000000..ae924b47
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/internal/Path.kt
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.internal
+
+import okio.Buffer
+import okio.ByteString
+import okio.ByteString.Companion.encodeUtf8
+import okio.ExperimentalFileSystem
+import okio.Path
+
+private val SLASH = "/".encodeUtf8()
+private val BACKSLASH = "\\".encodeUtf8()
+private val ANY_SLASH = "/\\".encodeUtf8()
+private val DOT = ".".encodeUtf8()
+private val DOT_DOT = "..".encodeUtf8()
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonIsAbsolute(): Boolean {
+ return bytes.startsWith(slash) ||
+ (volumeLetter != null && bytes.size > 2 && bytes[2] == '\\'.toByte())
+}
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonIsRelative(): Boolean {
+ return !isAbsolute
+}
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonVolumeLetter(): Char? {
+ if (slash != BACKSLASH) return null
+ if (bytes.size < 2) return null
+ if (bytes[1] != ':'.toByte()) return null
+ val c = bytes[0].toChar()
+ if (c !in 'a'..'z' && c !in 'A'..'Z') return null
+ return c
+}
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonNameBytes(): ByteString {
+ val lastSlash = bytes.lastIndexOf(slash)
+ return when {
+ lastSlash != -1 -> bytes.substring(lastSlash + 1)
+ volumeLetter != null && bytes.size == 2 -> ByteString.EMPTY // "C:" has no name.
+ else -> bytes
+ }
+}
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonName(): String {
+ return nameBytes.utf8()
+}
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonParent(): Path? {
+ if (bytes == DOT || bytes == slash || lastSegmentIsDotDot()) {
+ return null // Terminal path.
+ }
+
+ val lastSlash = bytes.lastIndexOf(slash)
+ when {
+ lastSlash == 2 && volumeLetter != null -> {
+ if (bytes.size == 3) return null // "C:\" has no parent.
+ return Path(slash, bytes.substring(endIndex = 3)) // Keep the trailing '\' in C:\.
+ }
+ lastSlash == 1 && bytes.startsWith(BACKSLASH) -> {
+ return null // "\\server" is a UNC path with no parent.
+ }
+ lastSlash == -1 && volumeLetter != null -> {
+ if (bytes.size == 2) return null // "C:" has no parent.
+ return Path(slash, bytes.substring(endIndex = 2)) // C: is volume-relative.
+ }
+ lastSlash == -1 -> {
+ return Path(slash, DOT) // Parent is the current working directory.
+ }
+ lastSlash == 0 -> {
+ return Path(slash, bytes.substring(endIndex = 1)) // Parent is the filesystem root '/'.
+ }
+ else -> {
+ return Path(slash, bytes.substring(endIndex = lastSlash))
+ }
+ }
+}
+
+@ExperimentalFileSystem
+private fun Path.lastSegmentIsDotDot(): Boolean {
+ if (bytes.endsWith(DOT_DOT)) {
+ if (bytes.size == 2) return true // ".." is the whole string.
+ if (bytes.rangeEquals(bytes.size - 3, slash, 0, 1)) return true // Ends with "/.." or "\..".
+ }
+ return false
+}
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonIsRoot(): Boolean {
+ return parent == null && isAbsolute
+}
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonResolve(child: String): Path {
+ return div(Buffer().writeUtf8(child).toPath(slash))
+}
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonResolve(child: Path): Path {
+ if (child.isAbsolute || child.volumeLetter != null) return child
+
+ val buffer = Buffer()
+ buffer.write(bytes)
+ if (buffer.size > 0) {
+ buffer.write(slash)
+ }
+ buffer.write(child.bytes)
+ return buffer.toPath(directorySeparator = slash)
+}
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonCompareTo(other: Path): Int {
+ val bytesResult = bytes.compareTo(other.bytes)
+ if (bytesResult != 0) return bytesResult
+ return slash.compareTo(other.slash)
+}
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonEquals(other: Any?): Boolean {
+ return other is Path && other.bytes == bytes && other.slash == slash
+}
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonHashCode(): Int {
+ return bytes.hashCode() xor slash.hashCode()
+}
+
+@ExperimentalFileSystem
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun Path.commonToString(): String {
+ return bytes.utf8()
+}
+
+@ExperimentalFileSystem
+fun String.commonToPath(directorySeparator: String? = null): Path {
+ return Buffer().writeUtf8(this).toPath(directorySeparator?.toSlash())
+}
+
+/** Consume the buffer and return it as a path. */
+@ExperimentalFileSystem
+internal fun Buffer.toPath(directorySeparator: ByteString? = null): Path {
+ var slash = directorySeparator
+ val result = Buffer()
+
+ // Consume the absolute path prefix, like `/`, `\\`, `C:`, or `C:\` and write the
+ // canonicalized prefix to result.
+ var leadingSlashCount = 0
+ while (rangeEquals(0L, SLASH) || rangeEquals(0L, BACKSLASH)) {
+ val byte = readByte()
+ slash = slash ?: byte.toSlash()
+ leadingSlashCount++
+ }
+ if (leadingSlashCount >= 2 && slash == BACKSLASH) {
+ // This is a Windows UNC path, like \\server\directory\file.txt.
+ result.write(slash)
+ result.write(slash)
+ } else if (leadingSlashCount > 0) {
+ // This is platform-dependent:
+ // * On UNIX: a absolute path like /home
+ // * On Windows: this is relative to the current volume, like \Windows.
+ result.write(slash!!)
+ } else {
+ // This path doesn't start with any slash. We must initialize the slash character to use.
+ val limit = indexOfElement(ANY_SLASH)
+ slash = slash ?: when (limit) {
+ -1L -> Path.DIRECTORY_SEPARATOR.toSlash()
+ else -> get(limit).toSlash()
+ }
+ if (startsWithVolumeLetterAndColon(slash)) {
+ if (limit == 2L) {
+ result.write(this, 3L) // Absolute on a named volume, like `C:\`.
+ } else {
+ result.write(this, 2L) // Relative to the named volume, like `C:`.
+ }
+ }
+ }
+
+ val absolute = result.size > 0
+
+ val canonicalParts = mutableListOf<ByteString>()
+ while (!exhausted()) {
+ val limit = indexOfElement(ANY_SLASH)
+
+ val part: ByteString
+ if (limit == -1L) {
+ part = readByteString()
+ } else {
+ part = readByteString(limit)
+ readByte()
+ }
+
+ if (part == DOT_DOT) {
+ if (!absolute && (canonicalParts.isEmpty() || canonicalParts.last() == DOT_DOT)) {
+ canonicalParts.add(part) // '..' doesn't pop '..' for relative paths.
+ } else {
+ canonicalParts.removeLastOrNull()
+ }
+ } else if (part != DOT && part != ByteString.EMPTY) {
+ canonicalParts.add(part)
+ }
+ }
+
+ for (i in 0 until canonicalParts.size) {
+ if (i > 0) result.write(slash)
+ result.write(canonicalParts[i])
+ }
+ if (result.size == 0L) {
+ result.write(DOT)
+ }
+
+ return Path(slash, result.readByteString())
+}
+
+private fun String.toSlash(): ByteString {
+ return when (this) {
+ "/" -> SLASH
+ "\\" -> BACKSLASH
+ else -> throw IllegalArgumentException("not a directory separator: $this")
+ }
+}
+
+private fun Byte.toSlash(): ByteString {
+ return when (toInt()) {
+ '/'.toInt() -> SLASH
+ '\\'.toInt() -> BACKSLASH
+ else -> throw IllegalArgumentException("not a directory separator: $this")
+ }
+}
+
+private fun Buffer.startsWithVolumeLetterAndColon(slash: ByteString): Boolean {
+ if (slash != BACKSLASH) return false
+ if (size < 2) return false
+ if (get(1) != ':'.toByte()) return false
+ val b = get(0).toChar()
+ return b in 'a'..'z' || b in 'A'..'Z'
+}
diff --git a/okio/src/commonTest/kotlin/okio/AbstractFileSystemTest.kt b/okio/src/commonTest/kotlin/okio/AbstractFileSystemTest.kt
new file mode 100644
index 00000000..669138f5
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/AbstractFileSystemTest.kt
@@ -0,0 +1,670 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+import okio.ByteString.Companion.toByteString
+import okio.Path.Companion.toPath
+import okio.fakefilesystem.FakeFileSystem
+import kotlin.random.Random
+import kotlin.test.BeforeTest
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.time.ExperimentalTime
+import kotlin.time.seconds
+
+/** This test assumes that okio-files/ is the current working directory when executed. */
+@ExperimentalTime
+@ExperimentalFileSystem
+abstract class AbstractFileSystemTest(
+ val clock: Clock,
+ val fileSystem: FileSystem,
+ val windowsLimitations: Boolean,
+ temporaryDirectory: Path
+) {
+ val base: Path = temporaryDirectory / "${this::class.simpleName}-${randomToken()}"
+ private val isJs = fileSystem::class.simpleName?.startsWith("NodeJs") ?: false
+
+ @BeforeTest
+ fun setUp() {
+ fileSystem.createDirectory(base)
+ }
+
+ @Test
+ fun canonicalizeDotReturnsCurrentWorkingDirectory() {
+ if (fileSystem is FakeFileSystem || fileSystem is ForwardingFileSystem) return
+ val cwd = fileSystem.canonicalize(".".toPath())
+ val cwdString = cwd.toString()
+ assertTrue(cwdString) {
+ cwdString.endsWith("okio${Path.DIRECTORY_SEPARATOR}okio") ||
+ cwdString.endsWith("${Path.DIRECTORY_SEPARATOR}okio-parent-okio-test") || // JS
+ cwdString.contains("/CoreSimulator/Devices/") || // iOS simulator.
+ cwdString == "/" // Android emulator.
+ }
+ }
+
+ @Test
+ fun canonicalizeNoSuchFile() {
+ assertFailsWith<FileNotFoundException> {
+ fileSystem.canonicalize(base / "no-such-file")
+ }
+ }
+
+ @Test
+ fun list() {
+ val target = base / "list"
+ target.writeUtf8("hello, world!")
+ val entries = fileSystem.list(base)
+ assertTrue(entries.toString()) { target in entries }
+ }
+
+ @Test
+ fun listResultsAreSorted() {
+ val fileA = base / "a"
+ val fileB = base / "b"
+ val fileC = base / "c"
+ val fileD = base / "d"
+
+ // Create files in a different order than the sorted order, so a file system that returns files
+ // in creation-order or reverse-creation order won't pass by accident.
+ fileD.writeUtf8("fileD")
+ fileB.writeUtf8("fileB")
+ fileC.writeUtf8("fileC")
+ fileA.writeUtf8("fileA")
+
+ val entries = fileSystem.list(base)
+ assertEquals(entries, listOf(fileA, fileB, fileC, fileD))
+ }
+
+ @Test
+ fun listNoSuchDirectory() {
+ assertFailsWith<FileNotFoundException> {
+ fileSystem.list(base / "no-such-directory")
+ }
+ }
+
+ @Test
+ fun listFile() {
+ val target = base / "list"
+ target.writeUtf8("hello, world!")
+ assertFailsWith<IOException> {
+ fileSystem.list(target)
+ }
+ }
+
+ @Test
+ fun fileSourceNoSuchDirectory() {
+ assertFailsWith<FileNotFoundException> {
+ fileSystem.source(base / "no-such-directory" / "file")
+ }
+ }
+
+ @Test
+ fun fileSource() {
+ val path = base / "file-source"
+ path.writeUtf8("hello, world!")
+
+ val source = fileSystem.source(path)
+ val buffer = Buffer()
+ assertTrue(source.read(buffer, 100L) == 13L)
+ assertEquals(-1L, source.read(buffer, 100L))
+ assertEquals("hello, world!", buffer.readUtf8())
+ source.close()
+ }
+
+ @Test
+ fun readPath() {
+ val path = base / "read-path"
+ val string = "hello, read with a Path"
+ path.writeUtf8(string)
+
+ val result = fileSystem.read(path) {
+ assertEquals("hello", readUtf8(5))
+ assertEquals(", read with ", readUtf8(12))
+ assertEquals("a Path", readUtf8())
+ return@read "success"
+ }
+ assertEquals("success", result)
+ }
+
+ @Test
+ fun fileSink() {
+ val path = base / "file-sink"
+ val sink = fileSystem.sink(path)
+ val buffer = Buffer().writeUtf8("hello, world!")
+ sink.write(buffer, buffer.size)
+ sink.close()
+ assertTrue(path in fileSystem.list(base))
+ assertEquals(0, buffer.size)
+ assertEquals("hello, world!", path.readUtf8())
+ }
+
+ @Test
+ fun writePath() {
+ val path = base / "write-path"
+ val content = fileSystem.write(path) {
+ val string = "hello, write with a Path"
+ writeUtf8(string)
+ return@write string
+ }
+ assertTrue(path in fileSystem.list(base))
+ assertEquals(content, path.readUtf8())
+ }
+
+ @Test
+ fun appendingSinkAppendsToExistingFile() {
+ val path = base / "appending-sink-appends-to-existing-file"
+ path.writeUtf8("hello, world!\n")
+ val sink = fileSystem.appendingSink(path)
+ val buffer = Buffer().writeUtf8("this is added later!")
+ sink.write(buffer, buffer.size)
+ sink.close()
+ assertTrue(path in fileSystem.list(base))
+ assertEquals("hello, world!\nthis is added later!", path.readUtf8())
+ }
+
+ @Test
+ fun appendingSinkDoesNotImpactExistingFile() {
+ val path = base / "appending-sink-does-not-impact-existing-file"
+ path.writeUtf8("hello, world!\n")
+ val sink = fileSystem.appendingSink(path)
+ assertEquals("hello, world!\n", path.readUtf8())
+ sink.close()
+ assertEquals("hello, world!\n", path.readUtf8())
+ }
+
+ @Test
+ fun appendingSinkCreatesNewFile() {
+ val path = base / "appending-sink-creates-new-file"
+ val sink = fileSystem.appendingSink(path)
+ val buffer = Buffer().writeUtf8("this is all there is!")
+ sink.write(buffer, buffer.size)
+ sink.close()
+ assertTrue(path in fileSystem.list(base))
+ assertEquals("this is all there is!", path.readUtf8())
+ }
+
+ @Test
+ fun fileSinkFlush() {
+ val path = base / "file-sink"
+ val sink = fileSystem.sink(path)
+
+ val buffer = Buffer().writeUtf8("hello,")
+ sink.write(buffer, buffer.size)
+ sink.flush()
+ assertEquals("hello,", path.readUtf8())
+
+ buffer.writeUtf8(" world!")
+ sink.write(buffer, buffer.size)
+ sink.close()
+ assertEquals("hello, world!", path.readUtf8())
+ }
+
+ @Test
+ fun fileSinkNoSuchDirectory() {
+ assertFailsWith<FileNotFoundException> {
+ fileSystem.sink(base / "no-such-directory" / "file")
+ }
+ }
+
+ @Test
+ fun createDirectory() {
+ val path = base / "create-directory"
+ fileSystem.createDirectory(path)
+ assertTrue(path in fileSystem.list(base))
+ }
+
+ @Test
+ fun createDirectoryAlreadyExists() {
+ val path = base / "already-exists"
+ fileSystem.createDirectory(path)
+ assertFailsWith<IOException> {
+ fileSystem.createDirectory(path)
+ }
+ }
+
+ @Test
+ fun createDirectoryParentDirectoryDoesNotExist() {
+ val path = base / "no-such-directory" / "created"
+ assertFailsWith<IOException> {
+ fileSystem.createDirectory(path)
+ }
+ }
+
+ @Test
+ fun createDirectoriesSingle() {
+ val path = base / "create-directories-single"
+ fileSystem.createDirectories(path)
+ assertTrue(path in fileSystem.list(base))
+ assertTrue(fileSystem.metadata(path).isDirectory)
+ }
+
+ @Test
+ fun createDirectoriesAlreadyExists() {
+ val path = base / "already-exists"
+ fileSystem.createDirectory(path)
+ fileSystem.createDirectories(path)
+ assertTrue(fileSystem.metadata(path).isDirectory)
+ }
+
+ @Test
+ fun createDirectoriesParentDirectoryDoesNotExist() {
+ fileSystem.createDirectories(base / "a" / "b" / "c")
+ assertTrue(base / "a" in fileSystem.list(base))
+ assertTrue(base / "a" / "b" in fileSystem.list(base / "a"))
+ assertTrue(base / "a" / "b" / "c" in fileSystem.list(base / "a" / "b"))
+ assertTrue(fileSystem.metadata(base / "a" / "b" / "c").isDirectory)
+ }
+
+ @Test
+ fun createDirectoriesParentIsFile() {
+ val file = base / "simple-file"
+ file.writeUtf8("just a file")
+ assertFailsWith<IOException> {
+ fileSystem.createDirectories(file / "child")
+ }
+ }
+
+ @Test
+ fun atomicMoveFile() {
+ val source = base / "source"
+ source.writeUtf8("hello, world!")
+ val target = base / "target"
+ fileSystem.atomicMove(source, target)
+ assertEquals("hello, world!", target.readUtf8())
+ assertTrue(source !in fileSystem.list(base))
+ assertTrue(target in fileSystem.list(base))
+ }
+
+ @Test
+ fun atomicMoveDirectory() {
+ val source = base / "source"
+ fileSystem.createDirectory(source)
+ val target = base / "target"
+ fileSystem.atomicMove(source, target)
+ assertTrue(source !in fileSystem.list(base))
+ assertTrue(target in fileSystem.list(base))
+ }
+
+ @Test
+ fun atomicMoveSourceIsTarget() {
+ val source = base / "source"
+ source.writeUtf8("hello, world!")
+ fileSystem.atomicMove(source, source)
+ assertEquals("hello, world!", source.readUtf8())
+ assertTrue(source in fileSystem.list(base))
+ }
+
+ @Test
+ fun atomicMoveClobberExistingFile() {
+ val source = base / "source"
+ source.writeUtf8("hello, world!")
+ val target = base / "target"
+ target.writeUtf8("this file will be clobbered!")
+ fileSystem.atomicMove(source, target)
+ assertEquals("hello, world!", target.readUtf8())
+ assertTrue(source !in fileSystem.list(base))
+ assertTrue(target in fileSystem.list(base))
+ }
+
+ @Test
+ fun atomicMoveSourceDoesNotExist() {
+ val source = base / "source"
+ val target = base / "target"
+ assertFailsWith<FileNotFoundException> {
+ fileSystem.atomicMove(source, target)
+ }
+ }
+
+ @Test
+ fun atomicMoveSourceIsFileAndTargetIsDirectory() {
+ val source = base / "source"
+ source.writeUtf8("hello, world!")
+ val target = base / "target"
+ fileSystem.createDirectory(target)
+ assertFailsWith<IOException> {
+ fileSystem.atomicMove(source, target)
+ }
+ }
+
+ @Test
+ fun atomicMoveSourceIsDirectoryAndTargetIsFile() {
+ val source = base / "source"
+ fileSystem.createDirectory(source)
+ val target = base / "target"
+ target.writeUtf8("hello, world!")
+ expectIOExceptionOnEverythingButWindows {
+ fileSystem.atomicMove(source, target)
+ }
+ }
+
+ @Test
+ fun copyFile() {
+ val source = base / "source"
+ source.writeUtf8("hello, world!")
+ val target = base / "target"
+ fileSystem.copy(source, target)
+ assertTrue(target in fileSystem.list(base))
+ assertEquals("hello, world!", source.readUtf8())
+ assertEquals("hello, world!", target.readUtf8())
+ }
+
+ @Test
+ fun copySourceDoesNotExist() {
+ val source = base / "source"
+ val target = base / "target"
+ assertFailsWith<FileNotFoundException> {
+ fileSystem.copy(source, target)
+ }
+ assertFalse(target in fileSystem.list(base))
+ }
+
+ @Test
+ fun copyTargetIsClobbered() {
+ val source = base / "source"
+ source.writeUtf8("hello, world!")
+ val target = base / "target"
+ target.writeUtf8("this file will be clobbered!")
+ fileSystem.copy(source, target)
+ assertTrue(target in fileSystem.list(base))
+ assertEquals("hello, world!", target.readUtf8())
+ }
+
+ @Test
+ fun deleteFile() {
+ val path = base / "delete-file"
+ path.writeUtf8("delete me")
+ fileSystem.delete(path)
+ assertTrue(path !in fileSystem.list(base))
+ }
+
+ @Test
+ fun deleteEmptyDirectory() {
+ val path = base / "delete-empty-directory"
+ fileSystem.createDirectory(path)
+ fileSystem.delete(path)
+ assertTrue(path !in fileSystem.list(base))
+ }
+
+ @Test
+ fun deleteFailsOnNoSuchFile() {
+ val path = base / "no-such-file"
+ // TODO(jwilson): fix Windows to throw FileNotFoundException on deleting an absent file.
+ if (windowsLimitations) {
+ assertFailsWith<IOException> {
+ fileSystem.delete(path)
+ }
+ } else {
+ assertFailsWith<FileNotFoundException> {
+ fileSystem.delete(path)
+ }
+ }
+ }
+
+ @Test
+ fun deleteFailsOnNonemptyDirectory() {
+ val path = base / "non-empty-directory"
+ fileSystem.createDirectory(path)
+ (path / "file.txt").writeUtf8("inside directory")
+ assertFailsWith<IOException> {
+ fileSystem.delete(path)
+ }
+ }
+
+ @Test
+ fun deleteRecursivelyFile() {
+ val path = base / "delete-recursively-file"
+ path.writeUtf8("delete me")
+ fileSystem.deleteRecursively(path)
+ assertTrue(path !in fileSystem.list(base))
+ }
+
+ @Test
+ fun deleteRecursivelyEmptyDirectory() {
+ val path = base / "delete-recursively-empty-directory"
+ fileSystem.createDirectory(path)
+ fileSystem.deleteRecursively(path)
+ assertTrue(path !in fileSystem.list(base))
+ }
+
+ @Test
+ fun deleteRecursivelyFailsOnNoSuchFile() {
+ val path = base / "no-such-file"
+ assertFailsWith<FileNotFoundException> {
+ fileSystem.deleteRecursively(path)
+ }
+ }
+
+ @Test
+ fun deleteRecursivelyNonemptyDirectory() {
+ val path = base / "delete-recursively-non-empty-directory"
+ fileSystem.createDirectory(path)
+ (path / "file.txt").writeUtf8("inside directory")
+ fileSystem.deleteRecursively(path)
+ assertTrue(path !in fileSystem.list(base))
+ assertTrue((path / "file.txt") !in fileSystem.list(base))
+ }
+
+ @Test
+ fun deleteRecursivelyDeepHierarchy() {
+ fileSystem.createDirectory(base / "a")
+ fileSystem.createDirectory(base / "a" / "b")
+ fileSystem.createDirectory(base / "a" / "b" / "c")
+ (base / "a" / "b" / "c" / "d.txt").writeUtf8("inside deep hierarchy")
+ fileSystem.deleteRecursively(base / "a")
+ assertEquals(fileSystem.list(base), listOf())
+ }
+
+ @Test
+ fun fileMetadata() {
+ val minTime = clock.now().minFileSystemTime()
+ val path = base / "file-metadata"
+ path.writeUtf8("hello, world!")
+ val maxTime = clock.now().maxFileSystemTime()
+
+ val metadata = fileSystem.metadata(path)
+ assertTrue(metadata.isRegularFile)
+ assertFalse(metadata.isDirectory)
+ assertEquals(13, metadata.size)
+ assertInRange(metadata.createdAt, minTime, maxTime)
+ assertInRange(metadata.lastModifiedAt, minTime, maxTime)
+ assertInRange(metadata.lastAccessedAt, minTime, maxTime)
+ }
+
+ @Test
+ fun directoryMetadata() {
+ val minTime = clock.now().minFileSystemTime()
+ val path = base / "directory-metadata"
+ fileSystem.createDirectory(path)
+ val maxTime = clock.now().maxFileSystemTime()
+
+ val metadata = fileSystem.metadata(path)
+ assertFalse(metadata.isRegularFile)
+ assertTrue(metadata.isDirectory)
+ // Note that the size check is omitted; we'd expect null but the JVM returns values like 64.
+ assertInRange(metadata.createdAt, minTime, maxTime)
+ assertInRange(metadata.lastModifiedAt, minTime, maxTime)
+ assertInRange(metadata.lastAccessedAt, minTime, maxTime)
+ }
+
+ @Test
+ fun absentMetadataOrNull() {
+ val path = base / "no-such-file"
+ assertNull(fileSystem.metadataOrNull(path))
+ }
+
+ @Test
+ @Ignore
+ fun inaccessibleMetadata() {
+ // TODO(swankjesse): configure a test directory in CI that exists, but that this process doesn't
+ // have permission to read metadata of. Perhaps a file in another user's /home directory?
+ }
+
+ @Test
+ fun absentMetadata() {
+ val path = base / "no-such-file"
+ assertFailsWith<FileNotFoundException> {
+ fileSystem.metadata(path)
+ }
+ }
+
+ @Test
+ fun fileExists() {
+ val path = base / "file-exists"
+ assertFalse(fileSystem.exists(path))
+ path.writeUtf8("hello, world!")
+ assertTrue(fileSystem.exists(path))
+ }
+
+ @Test
+ fun directoryExists() {
+ val path = base / "directory-exists"
+ assertFalse(fileSystem.exists(path))
+ fileSystem.createDirectory(path)
+ assertTrue(fileSystem.exists(path))
+ }
+
+ @Test
+ fun deleteOpenForWritingFailsOnWindows() {
+ val file = base / "file.txt"
+ expectIOExceptionOnWindows(exceptJs = true) {
+ fileSystem.sink(file).use {
+ fileSystem.delete(file)
+ }
+ }
+ }
+
+ @Test
+ fun deleteOpenForReadingFailsOnWindows() {
+ val file = base / "file.txt"
+ file.writeUtf8("abc")
+ expectIOExceptionOnWindows(exceptJs = true) {
+ fileSystem.source(file).use {
+ fileSystem.delete(file)
+ }
+ }
+ }
+
+ @Test
+ fun renameSourceIsOpenFailsOnWindows() {
+ val from = base / "from.txt"
+ val to = base / "to.txt"
+ from.writeUtf8("source file")
+ to.writeUtf8("target file")
+ expectIOExceptionOnWindows(exceptJs = true) {
+ fileSystem.source(from).use {
+ fileSystem.atomicMove(from, to)
+ }
+ }
+ }
+
+ @Test
+ fun renameTargetIsOpenFailsOnWindows() {
+ val from = base / "from.txt"
+ val to = base / "to.txt"
+ from.writeUtf8("source file")
+ to.writeUtf8("target file")
+ expectIOExceptionOnWindows {
+ fileSystem.source(to).use {
+ fileSystem.atomicMove(from, to)
+ }
+ }
+ }
+
+ @Test
+ fun deleteContentsOfParentOfFileOpenForReadingFailsOnWindows() {
+ val parentA = (base / "a")
+ fileSystem.createDirectory(parentA)
+ val parentAB = parentA / "b"
+ fileSystem.createDirectory(parentAB)
+ val parentABC = parentAB / "c"
+ fileSystem.createDirectory(parentABC)
+ val file = parentABC / "file.txt"
+ file.writeUtf8("child file")
+ expectIOExceptionOnWindows {
+ fileSystem.source(file).use {
+ fileSystem.delete(file)
+ fileSystem.delete(parentABC)
+ fileSystem.delete(parentAB)
+ fileSystem.delete(parentA)
+ }
+ }
+ }
+
+ private fun expectIOExceptionOnWindows(exceptJs: Boolean = false, block: () -> Unit) {
+ val expectCrash = windowsLimitations && (!isJs || !exceptJs)
+ try {
+ block()
+ assertFalse(expectCrash)
+ } catch (_: IOException) {
+ assertTrue(expectCrash)
+ }
+ }
+
+ private fun expectIOExceptionOnEverythingButWindows(block: () -> Unit) {
+ try {
+ block()
+ assertTrue(windowsLimitations)
+ } catch (e: IOException) {
+ assertFalse(windowsLimitations)
+ }
+ }
+
+ private fun randomToken() = Random.nextBytes(16).toByteString(0, 16).hex()
+
+ fun Path.readUtf8(): String {
+ return fileSystem.source(this).buffer().use {
+ it.readUtf8()
+ }
+ }
+
+ fun Path.writeUtf8(string: String) {
+ fileSystem.sink(this).buffer().use {
+ it.writeUtf8(string)
+ }
+ }
+
+ /**
+ * Returns the earliest file system time that could be recorded for an event occurring at this
+ * instant. This truncates fractional seconds because most host file systems do not use precise
+ * timestamps for file metadata.
+ */
+ private fun Instant.minFileSystemTime(): Instant {
+ return Instant.fromEpochSeconds(epochSeconds)
+ }
+
+ /**
+ * Returns the latest file system time that could be recorded for an event occurring at this
+ * instant. This adds 2 seconds and truncates fractional seconds because file systems may defer
+ * assigning the timestamp.
+ *
+ * https://docs.microsoft.com/en-us/windows/win32/sysinfo/file-times
+ */
+ private fun Instant.maxFileSystemTime(): Instant {
+ return Instant.fromEpochSeconds(plus(2.seconds).epochSeconds)
+ }
+
+ private fun assertInRange(sampled: Instant?, minTime: Instant, maxTime: Instant) {
+ if (sampled == null) return
+ assertTrue("expected $sampled in $minTime..$maxTime") { sampled in minTime..maxTime }
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/FakeFileSystemTest.kt b/okio/src/commonTest/kotlin/okio/FakeFileSystemTest.kt
new file mode 100644
index 00000000..f57ecd64
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/FakeFileSystemTest.kt
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.Path.Companion.toPath
+import okio.fakefilesystem.FakeFileSystem
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+import kotlin.time.ExperimentalTime
+import kotlin.time.minutes
+
+@ExperimentalTime
+@ExperimentalFileSystem
+class FakeWindowsFileSystemTest : FakeFileSystemTest(
+ clock = FakeClock(),
+ windowsLimitations = true,
+ temporaryDirectory = "C:\\".toPath(),
+)
+
+@ExperimentalTime
+@ExperimentalFileSystem
+class FakeUnixFileSystemTest : FakeFileSystemTest(
+ clock = FakeClock(),
+ windowsLimitations = false,
+ temporaryDirectory = "/".toPath(),
+)
+
+@ExperimentalTime
+@ExperimentalFileSystem
+abstract class FakeFileSystemTest internal constructor(
+ clock: FakeClock,
+ windowsLimitations: Boolean,
+ temporaryDirectory: Path
+) : AbstractFileSystemTest(
+ clock = clock,
+ fileSystem = FakeFileSystem(windowsLimitations, clock = clock),
+ windowsLimitations = windowsLimitations,
+ temporaryDirectory = temporaryDirectory
+) {
+ private val fakeFileSystem: FakeFileSystem = fileSystem as FakeFileSystem
+ private val fakeClock: FakeClock = clock
+
+ @Test
+ fun openPathsIncludesOpenSink() {
+ val openPath = base / "open-file"
+ val sink = fileSystem.sink(openPath)
+ assertEquals(openPath, fakeFileSystem.openPaths.single())
+ sink.close()
+ assertTrue(fakeFileSystem.openPaths.isEmpty())
+ }
+
+ @Test
+ fun openPathsIncludesOpenSource() {
+ val openPath = base / "open-file"
+ openPath.writeUtf8("hello, world!")
+ assertTrue(fakeFileSystem.openPaths.isEmpty())
+ val source = fileSystem.source(openPath)
+ assertEquals(openPath, fakeFileSystem.openPaths.single())
+ source.close()
+ assertTrue(fakeFileSystem.openPaths.isEmpty())
+ }
+
+ @Test
+ fun openPathsIsOpenOrder() {
+ val fileA = base / "a"
+ val fileB = base / "b"
+ val fileC = base / "c"
+ val fileD = base / "d"
+
+ assertEquals(fakeFileSystem.openPaths, listOf())
+ val sinkD = fileSystem.sink(fileD)
+ assertEquals(fakeFileSystem.openPaths, listOf(fileD))
+ val sinkB = fileSystem.sink(fileB)
+ assertEquals(fakeFileSystem.openPaths, listOf(fileD, fileB))
+ val sinkC = fileSystem.sink(fileC)
+ assertEquals(fakeFileSystem.openPaths, listOf(fileD, fileB, fileC))
+ val sinkA = fileSystem.sink(fileA)
+ assertEquals(fakeFileSystem.openPaths, listOf(fileD, fileB, fileC, fileA))
+ val sinkB2 = fileSystem.sink(fileB)
+ assertEquals(fakeFileSystem.openPaths, listOf(fileD, fileB, fileC, fileA, fileB))
+ sinkD.close()
+ assertEquals(fakeFileSystem.openPaths, listOf(fileB, fileC, fileA, fileB))
+ sinkB2.close()
+ assertEquals(fakeFileSystem.openPaths, listOf(fileB, fileC, fileA))
+ sinkB.close()
+ assertEquals(fakeFileSystem.openPaths, listOf(fileC, fileA))
+ sinkC.close()
+ assertEquals(fakeFileSystem.openPaths, listOf(fileA))
+ sinkA.close()
+ assertEquals(fakeFileSystem.openPaths, listOf())
+ }
+
+ @Test
+ fun allPathsIncludesFile() {
+ val file = base / "all-files-includes-file"
+ file.writeUtf8("hello, world!")
+ assertEquals(fakeFileSystem.allPaths, setOf(base, file))
+ }
+
+ @Test
+ fun allPathsIsSorted() {
+ val fileA = base / "a"
+ val fileB = base / "b"
+ val fileC = base / "c"
+ val fileD = base / "d"
+
+ // Create files in a different order than the sorted order, so a file system that returns files
+ // in creation-order or reverse-creation order won't pass by accident.
+ fileD.writeUtf8("fileD")
+ fileB.writeUtf8("fileB")
+ fileC.writeUtf8("fileC")
+ fileA.writeUtf8("fileA")
+
+ assertEquals(fakeFileSystem.allPaths.toList(), listOf(base, fileA, fileB, fileC, fileD))
+ }
+
+ @Test
+ fun allPathsIncludesDirectory() {
+ val dir = base / "all-files-includes-directory"
+ fileSystem.createDirectory(dir)
+ assertEquals(fakeFileSystem.allPaths, setOf(base, dir))
+ }
+
+ @Test
+ fun allPathsDoesNotIncludeDeletedFile() {
+ val file = base / "all-files-does-not-include-deleted-file"
+ file.writeUtf8("hello, world!")
+ fileSystem.delete(file)
+ assertEquals(fakeFileSystem.allPaths, setOf(base))
+ }
+
+ @Test
+ fun allPathsDoesNotIncludeDeletedOpenFile() {
+ if (windowsLimitations) return // Can't delete open files with Windows' limitations.
+
+ val file = base / "all-files-does-not-include-deleted-open-file"
+ val sink = fileSystem.sink(file)
+ assertEquals(fakeFileSystem.allPaths, setOf(base, file))
+ fileSystem.delete(file)
+ assertEquals(fakeFileSystem.allPaths, setOf(base))
+ sink.close()
+ }
+
+ @Test
+ fun fileLastAccessedTime() {
+ val path = base / "file-last-accessed-time"
+
+ fakeClock.sleep(1.minutes)
+ path.writeUtf8("hello, world!")
+ val createdAt = clock.now()
+
+ fakeClock.sleep(1.minutes)
+ path.writeUtf8("hello again!")
+ val modifiedAt = clock.now()
+
+ fakeClock.sleep(1.minutes)
+ path.readUtf8()
+ val accessedAt = clock.now()
+
+ val metadata = fileSystem.metadata(path)
+ assertEquals(createdAt, metadata.createdAt)
+ assertEquals(modifiedAt, metadata.lastModifiedAt)
+ assertEquals(accessedAt, metadata.lastAccessedAt)
+ }
+
+ @Test
+ fun directoryLastAccessedTime() {
+ val path = base / "directory-last-accessed-time"
+
+ fakeClock.sleep(1.minutes)
+ fileSystem.createDirectory(path)
+ val createdAt = clock.now()
+
+ fakeClock.sleep(1.minutes)
+ (path / "child").writeUtf8("hello world!")
+ val modifiedAt = clock.now()
+
+ fakeClock.sleep(1.minutes)
+ fileSystem.list(path)
+ val accessedAt = clock.now()
+
+ val metadata = fileSystem.metadata(path)
+ assertEquals(createdAt, metadata.createdAt)
+ assertEquals(modifiedAt, metadata.lastModifiedAt)
+ assertEquals(accessedAt, metadata.lastAccessedAt)
+ }
+
+ @Test
+ fun checkNoOpenFilesThrowsOnOpenSource() {
+ val path = base / "check-no-open-files-open-source"
+ path.writeUtf8("hello, world!")
+ val exception = fileSystem.source(path).use { source ->
+ assertFailsWith<IllegalStateException> {
+ fakeFileSystem.checkNoOpenFiles()
+ }
+ }
+
+ assertEquals(
+ """
+ |expected 0 open files, but found:
+ | $path
+ """.trimMargin(),
+ exception.message
+ )
+ assertEquals("file opened for reading here", exception.cause?.message)
+
+ // Now that the source is closed this is safe.
+ fakeFileSystem.checkNoOpenFiles()
+ }
+
+ @Test
+ fun checkNoOpenFilesThrowsOnOpenSink() {
+ val path = base / "check-no-open-files-open-sink"
+ val exception = fileSystem.sink(path).use { source ->
+ assertFailsWith<IllegalStateException> {
+ fakeFileSystem.checkNoOpenFiles()
+ }
+ }
+
+ assertEquals(
+ """
+ |expected 0 open files, but found:
+ | $path
+ """.trimMargin(),
+ exception.message
+ )
+ assertEquals("file opened for writing here", exception.cause?.message)
+
+ // Now that the source is closed this is safe.
+ fakeFileSystem.checkNoOpenFiles()
+ }
+
+ @Test
+ fun createDirectoriesForVolumeLetterRoot() {
+ val path = "X:\\".toPath()
+ fileSystem.createDirectories(path)
+ assertTrue(fileSystem.metadata(path).isDirectory)
+ }
+
+ @Test
+ fun createDirectoriesForChildOfVolumeLetterRoot() {
+ val path = "X:\\path".toPath()
+ fileSystem.createDirectories(path)
+ assertTrue(fileSystem.metadata(path).isDirectory)
+ }
+
+ @Test
+ fun createDirectoriesForUnixRoot() {
+ val path = "/".toPath()
+ fileSystem.createDirectories(path)
+ assertTrue(fileSystem.metadata(path).isDirectory)
+ }
+
+ @Test
+ fun createDirectoriesForChildOfUnixRoot() {
+ val path = "/path".toPath()
+ fileSystem.createDirectories(path)
+ assertTrue(fileSystem.metadata(path).isDirectory)
+ }
+
+ @Test
+ fun createDirectoriesForUncRoot() {
+ val path = "\\\\server".toPath()
+ fileSystem.createDirectories(path)
+ assertTrue(fileSystem.metadata(path).isDirectory)
+ }
+
+ @Test
+ fun createDirectoriesForChildOfUncRoot() {
+ val path = "\\\\server\\project".toPath()
+ fileSystem.createDirectories(path)
+ assertTrue(fileSystem.metadata(path).isDirectory)
+ }
+
+ @Test
+ fun workingDirectoryMustBeAbsolute() {
+ val exception = assertFailsWith<IllegalArgumentException> {
+ FakeFileSystem(workingDirectory = "some/relative/path".toPath())
+ }
+ assertEquals("expected an absolute path but was some/relative/path", exception.message)
+ }
+
+ @Test
+ fun metadataForRootsGeneratedOnDemand() {
+ assertTrue(fileSystem.metadata("X:\\".toPath()).isDirectory)
+ assertTrue(fileSystem.metadata("/".toPath()).isDirectory)
+ assertTrue(fileSystem.metadata("\\\\server".toPath()).isDirectory)
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/ForwardingFileSystemTest.kt b/okio/src/commonTest/kotlin/okio/ForwardingFileSystemTest.kt
new file mode 100644
index 00000000..2f9669d5
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/ForwardingFileSystemTest.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.datetime.Clock
+import okio.Path.Companion.toPath
+import okio.fakefilesystem.FakeFileSystem
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+import kotlin.time.ExperimentalTime
+
+@ExperimentalTime
+@ExperimentalFileSystem
+class ForwardingFileSystemTest : AbstractFileSystemTest(
+ clock = Clock.System,
+ fileSystem = object : ForwardingFileSystem(FakeFileSystem()) {},
+ windowsLimitations = false,
+ temporaryDirectory = "/".toPath()
+) {
+ @Test
+ fun pathBlocking() {
+ val forwardingFileSystem = object : ForwardingFileSystem(fileSystem) {
+ override fun delete(path: Path) {
+ throw IOException("synthetic failure!")
+ }
+
+ override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
+ if (path.name.contains("blocked")) throw IOException("blocked path!")
+ return path
+ }
+ }
+
+ forwardingFileSystem.createDirectory(base / "okay")
+ assertFailsWith<IOException> {
+ forwardingFileSystem.createDirectory(base / "blocked")
+ }
+ }
+
+ @Test
+ fun operationBlocking() {
+ val forwardingFileSystem = object : ForwardingFileSystem(fileSystem) {
+ override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
+ if (functionName == "delete") throw IOException("blocked operation!")
+ return path
+ }
+ }
+
+ forwardingFileSystem.createDirectory(base / "operation-blocking")
+ assertFailsWith<IOException> {
+ forwardingFileSystem.delete(base / "operation-blocking")
+ }
+ }
+
+ @Test
+ fun pathMapping() {
+ val prefix = "/mapped"
+ val source = base / "source"
+ val mappedSource = (prefix + source).toPath()
+ val target = base / "target"
+ val mappedTarget = (prefix + target).toPath()
+
+ source.writeUtf8("hello, world!")
+
+ val forwardingFileSystem = object : ForwardingFileSystem(fileSystem) {
+ override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
+ return path.toString().removePrefix(prefix).toPath()
+ }
+
+ override fun onPathResult(path: Path, functionName: String): Path {
+ return (prefix + path).toPath()
+ }
+ }
+
+ forwardingFileSystem.copy(mappedSource, mappedTarget)
+ assertTrue(target in fileSystem.list(base))
+ assertTrue(mappedTarget in forwardingFileSystem.list(base))
+ assertEquals("hello, world!", source.readUtf8())
+ assertEquals("hello, world!", target.readUtf8())
+ }
+
+ /**
+ * Path mapping might impact the sort order. Confirm that list() returns elements in sorted order
+ * even if that order is different in the delegate file system.
+ */
+ @Test
+ fun pathMappingImpactedBySorting() {
+ val az = base / "az"
+ val by = base / "by"
+ val cx = base / "cx"
+ az.writeUtf8("az")
+ by.writeUtf8("by")
+ cx.writeUtf8("cx")
+
+ val forwardingFileSystem = object : ForwardingFileSystem(fileSystem) {
+ override fun onPathResult(path: Path, functionName: String): Path {
+ return path.parent!! / path.name.reversed()
+ }
+ }
+
+ assertEquals(fileSystem.list(base), listOf(base / "az", base / "by", base / "cx"))
+ assertEquals(forwardingFileSystem.list(base), listOf(base / "xc", base / "yb", base / "za"))
+ }
+
+ @Test
+ fun copyIsNotForwarded() {
+ val log = mutableListOf<String>()
+
+ val delegate = object : ForwardingFileSystem(fileSystem) {
+ override fun copy(source: Path, target: Path) {
+ throw AssertionError("unexpected call to copy()")
+ }
+ }
+
+ val forwardingFileSystem = object : ForwardingFileSystem(delegate) {
+ override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
+ log += "$functionName($parameterName=$path)"
+ return path
+ }
+ }
+
+ val source = base / "source"
+ source.writeUtf8("hello, world!")
+ val target = base / "target"
+ forwardingFileSystem.copy(source, target)
+ assertTrue(target in fileSystem.list(base))
+ assertEquals("hello, world!", source.readUtf8())
+ assertEquals("hello, world!", target.readUtf8())
+
+ assertEquals(log, listOf("source(file=$source)", "sink(file=$target)"))
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/PathTest.kt b/okio/src/commonTest/kotlin/okio/PathTest.kt
new file mode 100644
index 00000000..92754a78
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/PathTest.kt
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.Path.Companion.toPath
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+@ExperimentalFileSystem
+class PathTest {
+ @Test
+ fun unixRoot() {
+ val path = "/".toPath("/")
+ assertEquals("/", path.toString())
+ assertNull(path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("", path.name)
+ assertTrue(path.isAbsolute)
+ assertTrue(path.isRoot)
+ }
+
+ @Test
+ fun unixAbsolutePath() {
+ val path = "/home/jesse/todo.txt".toPath("/")
+ assertEquals("/home/jesse/todo.txt", path.toString())
+ assertEquals("/home/jesse".toPath("/"), path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("todo.txt", path.name)
+ assertTrue(path.isAbsolute)
+ assertFalse(path.isRoot)
+ }
+
+ @Test
+ fun unixRelativePath() {
+ val path = "project/todo.txt".toPath("/")
+ assertEquals("project/todo.txt", path.toString())
+ assertEquals("project".toPath("/"), path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("todo.txt", path.name)
+ assertFalse(path.isAbsolute)
+ assertFalse(path.isRoot)
+ }
+
+ @Test
+ fun unixRelativePathWithDots() {
+ val path = "../../project/todo.txt".toPath("/")
+ assertEquals("../../project/todo.txt", path.toString())
+ assertEquals("../../project".toPath("/"), path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("todo.txt", path.name)
+ assertFalse(path.isAbsolute)
+ assertFalse(path.isRoot)
+ }
+
+ @Test
+ fun unixRelativeSeriesOfDotDots() {
+ val path = "../../..".toPath("/")
+ assertEquals("../../..", path.toString())
+ assertNull(path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("..", path.name)
+ assertFalse(path.isAbsolute)
+ assertFalse(path.isRoot)
+ }
+
+ @Test
+ fun unixAbsoluteSeriesOfDotDots() {
+ val path = "/../../..".toPath("/")
+ assertEquals("/", path.toString())
+ assertNull(path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("", path.name)
+ assertTrue(path.isAbsolute)
+ assertTrue(path.isRoot)
+ }
+
+ @Test
+ fun unixAbsoluteSingleDot() {
+ val path = "/.".toPath("/")
+ assertEquals("/", path.toString())
+ assertNull(path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("", path.name)
+ assertTrue(path.isAbsolute)
+ assertTrue(path.isRoot)
+ }
+
+ @Test
+ fun unixRelativeDoubleDots() {
+ val path = "..".toPath("/")
+ assertEquals("..", path.toString())
+ assertNull(path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("..", path.name)
+ assertFalse(path.isAbsolute)
+ assertFalse(path.isRoot)
+ }
+
+ @Test
+ fun unixRelativeSingleDot() {
+ val path = ".".toPath("/")
+ assertEquals(".", path.toString())
+ assertNull(path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals(".", path.name)
+ assertFalse(path.isAbsolute)
+ assertFalse(path.isRoot)
+ }
+
+ @Test
+ fun windowsVolumeLetter() {
+ val path = "C:\\".toPath("\\")
+ assertEquals("C:\\", path.toString())
+ assertNull(path.parent)
+ assertEquals('C', path.volumeLetter)
+ assertEquals("", path.name)
+ assertTrue(path.isAbsolute)
+ assertTrue(path.isRoot)
+ }
+
+ @Test
+ fun windowsAbsolutePathWithVolumeLetter() {
+ val path = "C:\\Windows\\notepad.exe".toPath("\\")
+ assertEquals("C:\\Windows\\notepad.exe", path.toString())
+ assertEquals("C:\\Windows".toPath("\\"), path.parent)
+ assertEquals('C', path.volumeLetter)
+ assertEquals("notepad.exe", path.name)
+ assertTrue(path.isAbsolute)
+ assertFalse(path.isRoot)
+ }
+
+ @Test
+ fun windowsAbsolutePath() {
+ val path = "\\".toPath("\\")
+ assertEquals("\\", path.toString())
+ assertEquals(null, path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("", path.name)
+ assertTrue(path.isAbsolute)
+ assertTrue(path.isRoot)
+ }
+
+ @Test
+ fun windowsAbsolutePathWithoutVolumeLetter() {
+ val path = "\\Windows\\notepad.exe".toPath("\\")
+ assertEquals("\\Windows\\notepad.exe", path.toString())
+ assertEquals("\\Windows".toPath("\\"), path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("notepad.exe", path.name)
+ assertTrue(path.isAbsolute)
+ assertFalse(path.isRoot)
+ }
+
+ @Test
+ fun windowsRelativePathWithVolumeLetter() {
+ val path = "C:Windows\\notepad.exe".toPath("\\")
+ assertEquals("C:Windows\\notepad.exe", path.toString())
+ assertEquals("C:Windows".toPath("\\"), path.parent)
+ assertEquals('C', path.volumeLetter)
+ assertEquals("notepad.exe", path.name)
+ assertFalse(path.isAbsolute)
+ assertFalse(path.isRoot)
+ }
+
+ @Test
+ fun windowsVolumeLetterRelative() {
+ val path = "C:".toPath("\\")
+ assertEquals("C:", path.toString())
+ assertNull(path.parent)
+ assertEquals('C', path.volumeLetter)
+ assertEquals("", path.name)
+ assertFalse(path.isAbsolute)
+ assertFalse(path.isRoot)
+ }
+
+ @Test
+ fun windowsRelativePath() {
+ val path = "Windows\\notepad.exe".toPath("\\")
+ assertEquals("Windows\\notepad.exe", path.toString())
+ assertEquals("Windows".toPath("\\"), path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("notepad.exe", path.name)
+ assertFalse(path.isAbsolute)
+ assertFalse(path.isRoot)
+ }
+
+ @Test
+ fun windowsUncServer() {
+ val path = "\\\\server".toPath("\\")
+ assertEquals("\\\\server", path.toString())
+ assertNull(path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("server", path.name)
+ assertTrue(path.isAbsolute)
+ assertTrue(path.isRoot)
+ }
+
+ @Test
+ fun windowsUncAbsolutePath() {
+ val path = "\\\\server\\project\\notes.txt".toPath("\\")
+ assertEquals("\\\\server\\project\\notes.txt", path.toString())
+ assertEquals("\\\\server\\project".toPath("\\"), path.parent)
+ assertNull(path.volumeLetter)
+ assertEquals("notes.txt", path.name)
+ assertTrue(path.isAbsolute)
+ assertFalse(path.isRoot)
+ }
+
+ @Test
+ fun absolutePathTraversalWithDivOperator() {
+ val root = "/".toPath()
+ assertEquals("/home".toPath(), root / "home")
+ assertEquals("/home/jesse".toPath(), root / "home" / "jesse")
+ assertEquals("/home".toPath(), root / "home" / "jesse" / "..")
+ assertEquals("/home/jake".toPath(), root / "home" / "jesse" / ".." / "jake")
+ }
+
+ @Test
+ fun relativePathTraversalWithDivOperator() {
+ val cwd = ".".toPath("/")
+ assertEquals("home".toPath("/"), cwd / "home")
+ assertEquals("home/jesse".toPath("/"), cwd / "home" / "jesse")
+ assertEquals("home".toPath("/"), cwd / "home" / "jesse" / "..")
+ assertEquals("home/jake".toPath("/"), cwd / "home" / "jesse" / ".." / "jake")
+ }
+
+ @Test
+ fun relativePathTraversalWithDots() {
+ val cwd = ".".toPath("/")
+ assertEquals("..".toPath("/"), cwd / "..")
+ assertEquals("../..".toPath("/"), cwd / ".." / "..")
+ assertEquals("../../etc".toPath("/"), cwd / ".." / ".." / "etc")
+ assertEquals("../../etc/passwd".toPath("/"), cwd / ".." / ".." / "etc" / "passwd")
+ }
+
+ @Test
+ fun pathTraversalBaseIgnoredIfChildIsAnAbsolutePath() {
+ assertEquals("/home".toPath(), "".toPath("/") / "/home")
+ assertEquals("/home".toPath(), "relative".toPath("/") / "/home")
+ assertEquals("/home".toPath(), "/base".toPath("/") / "/home")
+ assertEquals("/home".toPath(), "/".toPath("/") / "/home")
+ }
+
+ @Test
+ fun stringToAbsolutePath() {
+ assertEquals("/", "/".toPath().toString())
+ assertEquals("/a", "/a".toPath().toString())
+ assertEquals("/a", "/a/".toPath().toString())
+ assertEquals("/a/b/c", "/a/b/c".toPath().toString())
+ assertEquals("/a/b/c", "/a/b/c/".toPath().toString())
+ }
+
+ @Test
+ fun stringToAbsolutePathWithTraversal() {
+ assertEquals("/", "/..".toPath().toString())
+ assertEquals("/", "/../".toPath().toString())
+ assertEquals("/", "/../..".toPath().toString())
+ assertEquals("/", "/../../".toPath().toString())
+ }
+
+ @Test
+ fun stringToAbsolutePathWithEmptySegments() {
+ assertEquals("/", "//".toPath().toString())
+ assertEquals("/a", "//a".toPath().toString())
+ assertEquals("/a", "/a//".toPath().toString())
+ assertEquals("/a", "//a//".toPath().toString())
+ assertEquals("/a/b", "/a/b//".toPath().toString())
+ }
+
+ @Test
+ fun stringToAbsolutePathWithDots() {
+ assertEquals("/", "/./".toPath().toString())
+ assertEquals("/a", "/./a".toPath().toString())
+ assertEquals("/a", "/a/./".toPath().toString())
+ assertEquals("/a", "/a//.".toPath().toString())
+ assertEquals("/a", "/./a//".toPath().toString())
+ assertEquals("/a", "/a/.".toPath().toString())
+ assertEquals("/a", "//a/./".toPath().toString())
+ assertEquals("/a", "//a/./.".toPath().toString())
+ assertEquals("/a/b", "/a/./b/".toPath().toString())
+ }
+
+ @Test
+ fun stringToRelativePath() {
+ assertEquals(".", "".toPath().toString())
+ assertEquals(".", ".".toPath().toString())
+ assertEquals("a", "a/".toPath().toString())
+ assertEquals("a/b", "a/b".toPath().toString())
+ assertEquals("a/b", "a/b/".toPath().toString())
+ assertEquals("a/b/c/d", "a/b/c/d".toPath().toString())
+ assertEquals("a/b/c/d", "a/b/c/d/".toPath().toString())
+ }
+
+ @Test
+ fun stringToRelativePathWithTraversal() {
+ assertEquals("..", "..".toPath().toString())
+ assertEquals("..", "../".toPath().toString())
+ assertEquals(".", "a/..".toPath().toString())
+ assertEquals(".", "a/../".toPath().toString())
+ assertEquals("..", "a/../..".toPath().toString())
+ assertEquals("..", "a/../../".toPath().toString())
+ assertEquals("../..", "a/../../..".toPath().toString())
+ assertEquals("../../b", "../../b".toPath().toString())
+ assertEquals("../../b", "a/../../../b".toPath().toString())
+ assertEquals("../../c", "a/../../../b/../c".toPath().toString())
+ }
+
+ @Test
+ fun stringToRelativePathWithEmptySegments() {
+ assertEquals("a", "a//".toPath().toString())
+ assertEquals("a/b", "a//b".toPath().toString())
+ assertEquals("a/b", "a/b//".toPath().toString())
+ assertEquals("a/b", "a//b//".toPath().toString())
+ assertEquals("a/b/c", "a/b/c//".toPath().toString())
+ }
+
+ @Test
+ fun stringToRelativePathWithDots() {
+ assertEquals(".", ".".toPath().toString())
+ assertEquals(".", "./".toPath().toString())
+ assertEquals(".", "././".toPath().toString())
+ assertEquals(".", "././a/..".toPath().toString())
+ assertEquals("a", "a/./".toPath().toString())
+ assertEquals("a/b", "a/./b".toPath().toString())
+ assertEquals("a/b", "a/b/./".toPath().toString())
+ assertEquals("a/b", "a/b//.".toPath().toString())
+ assertEquals("a/b", "a/./b//".toPath().toString())
+ assertEquals("a/b", "a/b/.".toPath().toString())
+ assertEquals("a/b", "a//b/./".toPath().toString())
+ assertEquals("a/b", "a//b/./.".toPath().toString())
+ assertEquals("a/b/c", "a/b/./c/".toPath().toString())
+ }
+
+ @Test
+ fun composingWindowsPath() {
+ assertEquals("C:\\Windows\\notepad.exe".toPath(), "C:\\".toPath() / "Windows" / "notepad.exe")
+ }
+
+ @Test
+ fun windowsResolveAbsolutePath() {
+ assertEquals("\\Users".toPath(), "C:\\Windows".toPath() / "\\Users")
+ }
+
+ @Test
+ fun windowsPathTraversalUp() {
+ assertEquals("C:\\z".toPath(), "C:\\x\\y\\..\\..\\..\\z".toPath())
+ assertEquals("C:..\\z".toPath(), "C:x\\y\\..\\..\\..\\z".toPath())
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/SystemFileSystemTest.kt b/okio/src/commonTest/kotlin/okio/SystemFileSystemTest.kt
new file mode 100644
index 00000000..df1d620a
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/SystemFileSystemTest.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.datetime.Clock
+import kotlin.time.ExperimentalTime
+
+@ExperimentalTime
+@ExperimentalFileSystem
+class SystemFileSystemTest : AbstractFileSystemTest(
+ clock = Clock.System,
+ fileSystem = FileSystem.SYSTEM,
+ windowsLimitations = Path.DIRECTORY_SEPARATOR == "\\",
+ temporaryDirectory = FileSystem.SYSTEM_TEMPORARY_DIRECTORY
+)
diff --git a/okio/src/commonTest/kotlin/okio/time.kt b/okio/src/commonTest/kotlin/okio/time.kt
new file mode 100644
index 00000000..36fdf94a
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/time.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.datetime.Instant
+
+@ExperimentalFileSystem
+internal val FileMetadata.createdAt: Instant?
+ get() {
+ val createdAt = createdAtMillis ?: return null
+ return Instant.fromEpochMilliseconds(createdAt)
+ }
+
+@ExperimentalFileSystem
+internal val FileMetadata.lastModifiedAt: Instant?
+ get() {
+ val lastModifiedAt = lastModifiedAtMillis ?: return null
+ return Instant.fromEpochMilliseconds(lastModifiedAt)
+ }
+
+@ExperimentalFileSystem
+internal val FileMetadata.lastAccessedAt: Instant?
+ get() {
+ val lastAccessedAt = lastAccessedAtMillis ?: return null
+ return Instant.fromEpochMilliseconds(lastAccessedAt)
+ }
diff --git a/okio/src/jsMain/kotlin/okio/-Platform.kt b/okio/src/jsMain/kotlin/okio/-Platform.kt
new file mode 100644
index 00000000..25f93c3e
--- /dev/null
+++ b/okio/src/jsMain/kotlin/okio/-Platform.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.Path.Companion.toPath
+
+@ExperimentalFileSystem
+internal actual val PLATFORM_FILE_SYSTEM: FileSystem
+ get() = NodeJsFileSystem
+
+@ExperimentalFileSystem
+internal actual val PLATFORM_TEMPORARY_DIRECTORY: Path
+ get() = tmpdir().toPath()
+
+internal actual val PLATFORM_DIRECTORY_SEPARATOR: String
+ get() {
+ // TODO(swankjesse): return path.path.sep instead, once it has @JsNonModule
+ return when (platform()) {
+ "win32" -> "\\"
+ else -> "/"
+ }
+ }
diff --git a/okio/src/jsMain/kotlin/okio/FileSink.kt b/okio/src/jsMain/kotlin/okio/FileSink.kt
new file mode 100644
index 00000000..c286cdfe
--- /dev/null
+++ b/okio/src/jsMain/kotlin/okio/FileSink.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import org.khronos.webgl.Uint8Array
+
+internal class FileSink(
+ private val fd: Number
+) : Sink {
+ private var closed = false
+
+ override fun write(source: Buffer, byteCount: Long) {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ require(source.size >= byteCount) { "source.size=${source.size} < byteCount=$byteCount" }
+ check(!closed) { "closed" }
+
+ val data = Uint8Array(byteCount.toInt())
+ data.set(source.readByteArray().toTypedArray())
+ val writtenByteCount = writeSync(fd, data)
+ if (writtenByteCount.toLong() != byteCount) {
+ throw IOException("expected $byteCount but was $writtenByteCount")
+ }
+ }
+
+ override fun flush() {
+ }
+
+ override fun timeout(): Timeout {
+ return Timeout.NONE
+ }
+
+ override fun close() {
+ if (closed) return
+ closed = true
+ closeSync(fd)
+ }
+}
diff --git a/okio/src/jsMain/kotlin/okio/FileSource.kt b/okio/src/jsMain/kotlin/okio/FileSource.kt
new file mode 100644
index 00000000..1b4f496f
--- /dev/null
+++ b/okio/src/jsMain/kotlin/okio/FileSource.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import org.khronos.webgl.Uint8Array
+import org.khronos.webgl.get
+
+internal class FileSource(
+ private val fd: Number
+) : Source {
+ var closed = false
+
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ check(!closed) { "closed" }
+
+ val data = Uint8Array(byteCount.toInt())
+ val readByteCount = readSync(
+ fd = fd,
+ buffer = data,
+ length = byteCount,
+ offset = 0,
+ position = null
+ ).toInt()
+
+ if (readByteCount == 0) return -1L
+
+ for (i in 0 until readByteCount) {
+ sink.writeByte(data[i].toInt())
+ }
+
+ return readByteCount.toLong()
+ }
+
+ override fun timeout(): Timeout = Timeout.NONE
+
+ override fun close() {
+ if (closed) return
+ closed = true
+ closeSync(fd)
+ }
+}
diff --git a/okio/src/jsMain/kotlin/okio/NodeJsFileSystem.kt b/okio/src/jsMain/kotlin/okio/NodeJsFileSystem.kt
new file mode 100644
index 00000000..18f344e1
--- /dev/null
+++ b/okio/src/jsMain/kotlin/okio/NodeJsFileSystem.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.Path.Companion.toPath
+
+/**
+ * Use [Node.js APIs][node_fs] to implement the Okio file system interface.
+ *
+ * This class needs to make calls to some fs APIs that have multiple competing overloads. To
+ * unambiguously select an overload this passes `undefined` as the target type to some functions.
+ *
+ * [node_fs]: https://nodejs.org/dist/latest-v14.x/docs/api/fs.html
+ */
+@ExperimentalFileSystem
+internal object NodeJsFileSystem : FileSystem() {
+ private var S_IFMT = 0xf000 // fs.constants.S_IFMT
+ private var S_IFREG = 0x8000 // fs.constants.S_IFREG
+ private var S_IFDIR = 0x4000 // fs.constants.S_IFDIR
+
+ override fun canonicalize(path: Path): Path {
+ try {
+ val canonicalPath = realpathSync(path.toString())
+ return canonicalPath.toString().toPath()
+ } catch (e: Throwable) {
+ throw e.toIOException()
+ }
+ }
+
+ override fun metadataOrNull(path: Path): FileMetadata? {
+ val stat = try {
+ statSync(path.toString())
+ } catch (e: Throwable) {
+ if (e.errorCode == "ENOENT") return null // "No such file or directory".
+ throw IOException(e.message)
+ }
+ return FileMetadata(
+ isRegularFile = stat.mode.toInt() and S_IFMT == S_IFREG,
+ isDirectory = stat.mode.toInt() and S_IFMT == S_IFDIR,
+ size = stat.size.toLong(),
+ createdAtMillis = stat.ctimeMs.toLong(),
+ lastModifiedAtMillis = stat.mtimeMs.toLong(),
+ lastAccessedAtMillis = stat.atimeMs.toLong()
+ )
+ }
+
+ /**
+ * Returns the error code on this `SystemError`. This uses `asDynamic()` because our JS bindings
+ * don't (yet) include the `SystemError` type.
+ *
+ * https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_class_systemerror
+ * https://nodejs.org/dist/latest-v14.x/docs/api/errors.html#errors_common_system_errors
+ */
+ private val Throwable.errorCode
+ get() = asDynamic().code
+
+ override fun list(dir: Path): List<Path> {
+ try {
+ val opendir = opendirSync(dir.toString())
+ try {
+ val result = mutableListOf<Path>()
+ while (true) {
+ val dirent = opendir.readSync() ?: break
+ result += dir / dirent.name
+ }
+ result.sort()
+ return result
+ } finally {
+ opendir.closeSync()
+ }
+ } catch (e: Throwable) {
+ throw e.toIOException()
+ }
+ }
+
+ override fun source(file: Path): Source {
+ try {
+ val fd = openSync(file.toString(), flags = "r")
+ return FileSource(fd)
+ } catch (e: Throwable) {
+ throw e.toIOException()
+ }
+ }
+
+ override fun sink(file: Path): Sink {
+ try {
+ val fd = openSync(file.toString(), flags = "w")
+ return FileSink(fd)
+ } catch (e: Throwable) {
+ throw e.toIOException()
+ }
+ }
+
+ override fun appendingSink(file: Path): Sink {
+ try {
+ val fd = openSync(file.toString(), flags = "a")
+ return FileSink(fd)
+ } catch (e: Throwable) {
+ throw e.toIOException()
+ }
+ }
+
+ override fun createDirectory(dir: Path) {
+ try {
+ mkdirSync(dir.toString())
+ } catch (e: Throwable) {
+ throw e.toIOException()
+ }
+ }
+
+ override fun atomicMove(source: Path, target: Path) {
+ try {
+ renameSync(source.toString(), target.toString())
+ } catch (e: Throwable) {
+ throw e.toIOException()
+ }
+ }
+
+ /**
+ * We don't know if [path] is a file or a directory, but we don't (yet) have an API to delete
+ * either type. Just try each in sequence.
+ *
+ * TODO(jwilson): switch to fs.rmSync() when our minimum requirements are Node 14.14.0.
+ */
+ override fun delete(path: Path) {
+ try {
+ unlinkSync(path.toString())
+ return
+ } catch (e: Throwable) {
+ }
+ try {
+ rmdirSync(path.toString())
+ } catch (e: Throwable) {
+ throw e.toIOException()
+ }
+ }
+
+ private fun Throwable.toIOException(): IOException {
+ return when (errorCode) {
+ "ENOENT" -> FileNotFoundException(message)
+ else -> IOException(message)
+ }
+ }
+
+ override fun toString() = "NodeJsSystemFileSystem"
+}
diff --git a/okio/src/jsMain/kotlin/okio/fs.kt b/okio/src/jsMain/kotlin/okio/fs.kt
new file mode 100644
index 00000000..17f965ff
--- /dev/null
+++ b/okio/src/jsMain/kotlin/okio/fs.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This class declares the subset of Node.js file system APIs that we need in Okio.
+ *
+ *
+ * Why not Dukat?
+ * --------------
+ *
+ * This file does manually what ideally [Dukat] would do automatically.
+ *
+ * Dukat's generated stubs need awkward call sites to disambiguate overloads. For example, to call
+ * `mkdirSync()` we must specify an options parameter even though we just want the default:
+ *
+ * mkdirSync(dir.toString(), options = undefined as MakeDirectoryOptions?)
+ *
+ * By defining our own externals, we can omit the unwanted optional parameter from the declaration.
+ * This leads to nicer calling code!
+ *
+ * mkdirSync(dir.toString())
+ *
+ * Dukat also gets the nullability wrong for `Dirent.readSync()`.
+ *
+ *
+ * Why not Kotlinx-nodejs?
+ * -----------------------
+ *
+ * Even better than using Dukat directly would be to use the [official artifact][kotlinx_nodejs],
+ * itself generated with Dukat. We also don't use the official Node.js artifact for the reasons
+ * above, and also because it has an unstable API.
+ *
+ *
+ * Updating this file
+ * ------------------
+ *
+ * To declare new external APIs, run Dukat to generate a full set of Node stubs. The easiest way to
+ * do this is to add an NPM dependency on `@types/node` in `jsMain`, like this:
+ *
+ * ```
+ * jsMain {
+ * ...
+ * dependencies {
+ * implementation(npm('@types/node', '14.14.16', true))
+ * ...
+ * }
+ * }
+ * ```
+ *
+ * This will create a file with a full set of APIs to copy-paste from.
+ *
+ * ```
+ * okio/build/externals/okio-parent-okio/src/fs.fs.module_node.kt
+ * ```
+ *
+ * [Dukat]: https://github.com/kotlin/dukat
+ * [kotlinx_nodejs]: https://github.com/Kotlin/kotlinx-nodejs
+ */
+@file:JsModule("fs")
+@file:JsNonModule
+package okio
+
+import org.khronos.webgl.Uint8Array
+import kotlin.js.Date
+
+internal external fun closeSync(fd: Number)
+
+internal external fun mkdirSync(path: String): String?
+
+internal external fun openSync(path: String, flags: String): Number
+
+internal external fun opendirSync(path: String): Dir
+
+internal external fun readSync(fd: Number, buffer: Uint8Array, offset: Number, length: Number, position: Number?): Number
+
+internal external fun realpathSync(path: String): dynamic /* String | Buffer */
+
+internal external fun renameSync(oldPath: String, newPath: String)
+
+internal external fun rmdirSync(path: String)
+
+internal external fun statSync(path: String): Stats
+
+internal external fun unlinkSync(path: String)
+
+internal external fun writeSync(fd: Number, buffer: Uint8Array): Number
+
+internal open external class Dir {
+ open var path: String
+ open fun closeSync()
+ // Note that dukat's signature of readSync() returns a non-nullable Dirent; that's incorrect.
+ open fun readSync(): Dirent?
+}
+
+internal open external class Dirent {
+ open fun isFile(): Boolean
+ open fun isDirectory(): Boolean
+ open fun isBlockDevice(): Boolean
+ open fun isCharacterDevice(): Boolean
+ open fun isSymbolicLink(): Boolean
+ open fun isFIFO(): Boolean
+ open fun isSocket(): Boolean
+ open var name: String
+}
+
+internal external interface StatsBase<T> {
+ fun isFile(): Boolean
+ fun isDirectory(): Boolean
+ fun isBlockDevice(): Boolean
+ fun isCharacterDevice(): Boolean
+ fun isSymbolicLink(): Boolean
+ fun isFIFO(): Boolean
+ fun isSocket(): Boolean
+ var dev: T
+ var ino: T
+ var mode: T
+ var nlink: T
+ var uid: T
+ var gid: T
+ var rdev: T
+ var size: T
+ var blksize: T
+ var blocks: T
+ var atimeMs: T
+ var mtimeMs: T
+ var ctimeMs: T
+ var birthtimeMs: T
+ var atime: Date
+ var mtime: Date
+ var ctime: Date
+ var birthtime: Date
+}
+
+internal open external class Stats : StatsBase<Number> {
+ override fun isFile(): Boolean
+ override fun isDirectory(): Boolean
+ override fun isBlockDevice(): Boolean
+ override fun isCharacterDevice(): Boolean
+ override fun isSymbolicLink(): Boolean
+ override fun isFIFO(): Boolean
+ override fun isSocket(): Boolean
+ override var dev: Number
+ override var ino: Number
+ override var mode: Number
+ override var nlink: Number
+ override var uid: Number
+ override var gid: Number
+ override var rdev: Number
+ override var size: Number
+ override var blksize: Number
+ override var blocks: Number
+ override var atimeMs: Number
+ override var mtimeMs: Number
+ override var ctimeMs: Number
+ override var birthtimeMs: Number
+ override var atime: Date
+ override var mtime: Date
+ override var ctime: Date
+ override var birthtime: Date
+}
diff --git a/okio/src/jsMain/kotlin/okio/os.kt b/okio/src/jsMain/kotlin/okio/os.kt
new file mode 100644
index 00000000..d35cea5e
--- /dev/null
+++ b/okio/src/jsMain/kotlin/okio/os.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * See `fs.kt` for information on what this file does and how to keep it up-to-date.
+ */
+@file:JsModule("os")
+@file:JsNonModule
+package okio
+
+internal external fun tmpdir(): String
+
+internal external fun platform(): String
diff --git a/okio/src/jvmMain/kotlin/okio/-Platform.kt b/okio/src/jvmMain/kotlin/okio/-Platform.kt
index 4edb3ce0..95e7ee52 100644
--- a/okio/src/jvmMain/kotlin/okio/-Platform.kt
+++ b/okio/src/jvmMain/kotlin/okio/-Platform.kt
@@ -17,6 +17,23 @@
@file:JvmName("-Platform")
package okio
+import okio.Path.Companion.toPath
+
+@ExperimentalFileSystem
+internal actual val PLATFORM_FILE_SYSTEM: FileSystem
+ get() {
+ try {
+ Class.forName("java.nio.file.Files")
+ return NioSystemFileSystem()
+ } catch (e: ClassNotFoundException) {
+ return JvmSystemFileSystem()
+ }
+ }
+
+@ExperimentalFileSystem
+internal actual val PLATFORM_TEMPORARY_DIRECTORY: Path
+ get() = System.getProperty("java.io.tmpdir").toPath()
+
internal actual fun ByteArray.toUtf8String(): String = String(this, Charsets.UTF_8)
internal actual fun String.asUtf8ToByteArray(): ByteArray = toByteArray(Charsets.UTF_8)
diff --git a/okio/src/jvmMain/kotlin/okio/JvmSystemFileSystem.kt b/okio/src/jvmMain/kotlin/okio/JvmSystemFileSystem.kt
new file mode 100644
index 00000000..fdcfa64e
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/JvmSystemFileSystem.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.Path.Companion.toOkioPath
+
+/**
+ * A file system that adapts `java.io`.
+ *
+ * This base class is used on Android API levels 15 (our minimum supported API) through 26
+ * (the first release that includes java.nio.file).
+ */
+@ExperimentalFileSystem
+internal open class JvmSystemFileSystem : FileSystem() {
+ override fun canonicalize(path: Path): Path {
+ val canonicalFile = path.toFile().canonicalFile
+ if (!canonicalFile.exists()) throw FileNotFoundException("no such file")
+ return canonicalFile.toOkioPath()
+ }
+
+ override fun metadataOrNull(path: Path): FileMetadata? {
+ val file = path.toFile()
+ val isRegularFile = file.isFile
+ val isDirectory = file.isDirectory
+ val lastModifiedAtMillis = file.lastModified()
+ val size = file.length()
+
+ if (!isRegularFile &&
+ !isDirectory &&
+ lastModifiedAtMillis == 0L &&
+ size == 0L &&
+ !file.exists()
+ ) {
+ return null
+ }
+
+ return FileMetadata(
+ isRegularFile = isRegularFile,
+ isDirectory = isDirectory,
+ size = size,
+ createdAtMillis = null,
+ lastModifiedAtMillis = lastModifiedAtMillis,
+ lastAccessedAtMillis = null
+ )
+ }
+
+ override fun list(dir: Path): List<Path> {
+ val file = dir.toFile()
+ val entries = file.list()
+ if (entries == null) {
+ if (!file.exists()) throw FileNotFoundException("no such file $dir")
+ throw IOException("failed to list $dir")
+ }
+ val result = entries.mapTo(mutableListOf()) { dir / it }
+ result.sort()
+ return result
+ }
+
+ override fun source(file: Path): Source {
+ return file.toFile().source()
+ }
+
+ override fun sink(file: Path): Sink {
+ return file.toFile().sink()
+ }
+
+ override fun appendingSink(file: Path): Sink {
+ return file.toFile().sink(append = true)
+ }
+
+ override fun createDirectory(dir: Path) {
+ if (!dir.toFile().mkdir()) throw IOException("failed to create directory $dir")
+ }
+
+ override fun atomicMove(source: Path, target: Path) {
+ val renamed = source.toFile().renameTo(target.toFile())
+ if (!renamed) throw IOException("failed to move $source to $target")
+ }
+
+ override fun delete(path: Path) {
+ val file = path.toFile()
+ val deleted = file.delete()
+ if (!deleted) {
+ if (!file.exists()) throw FileNotFoundException("no such file $path")
+ else throw IOException("failed to delete $path")
+ }
+ }
+
+ override fun toString() = "JvmSystemFileSystem"
+}
diff --git a/okio/src/jvmMain/kotlin/okio/NioSystemFileSystem.kt b/okio/src/jvmMain/kotlin/okio/NioSystemFileSystem.kt
new file mode 100644
index 00000000..45487729
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/NioSystemFileSystem.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
+import java.nio.file.Files
+import java.nio.file.LinkOption
+import java.nio.file.NoSuchFileException
+import java.nio.file.StandardCopyOption.ATOMIC_MOVE
+import java.nio.file.StandardCopyOption.REPLACE_EXISTING
+import java.nio.file.attribute.BasicFileAttributes
+import java.nio.file.attribute.FileTime
+
+/**
+ * Extends [JvmSystemFileSystem] for platforms that support `java.nio.files` first introduced in
+ * Java 7 and Android 8.0 (API level 26).
+ */
+@ExperimentalFileSystem
+@IgnoreJRERequirement // Only used on platforms that support java.nio.file.
+internal class NioSystemFileSystem : JvmSystemFileSystem() {
+ override fun metadataOrNull(path: Path): FileMetadata? {
+ val nioPath = path.toNioPath()
+
+ val attributes = try {
+ Files.readAttributes(
+ nioPath,
+ BasicFileAttributes::class.java,
+ LinkOption.NOFOLLOW_LINKS
+ )
+ } catch (_: NoSuchFileException) {
+ return null
+ }
+
+ return FileMetadata(
+ isRegularFile = attributes.isRegularFile,
+ isDirectory = attributes.isDirectory,
+ size = attributes.size(),
+ createdAtMillis = attributes.creationTime()?.zeroToNull(),
+ lastModifiedAtMillis = attributes.lastModifiedTime()?.zeroToNull(),
+ lastAccessedAtMillis = attributes.lastAccessTime()?.zeroToNull()
+ )
+ }
+
+ /**
+ * Returns this time as a epoch millis. If this is 0L this returns null, because epoch time 0L is
+ * a special value that indicates the requested time was not available.
+ */
+ private fun FileTime.zeroToNull(): Long? {
+ return toMillis().takeIf { it != 0L }
+ }
+
+ override fun atomicMove(source: Path, target: Path) {
+ try {
+ Files.move(source.toNioPath(), target.toNioPath(), ATOMIC_MOVE, REPLACE_EXISTING)
+ } catch (e: NoSuchFileException) {
+ throw FileNotFoundException(e.message)
+ } catch (e: UnsupportedOperationException) {
+ throw IOException("atomic move not supported")
+ }
+ }
+
+ override fun toString() = "NioSystemFileSystem"
+}
diff --git a/okio/src/jvmMain/kotlin/okio/Path.kt b/okio/src/jvmMain/kotlin/okio/Path.kt
new file mode 100644
index 00000000..845ae908
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/Path.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.commonCompareTo
+import okio.internal.commonEquals
+import okio.internal.commonHashCode
+import okio.internal.commonIsAbsolute
+import okio.internal.commonIsRelative
+import okio.internal.commonIsRoot
+import okio.internal.commonName
+import okio.internal.commonNameBytes
+import okio.internal.commonParent
+import okio.internal.commonResolve
+import okio.internal.commonToPath
+import okio.internal.commonToString
+import okio.internal.commonVolumeLetter
+import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
+import java.io.File
+import java.nio.file.Paths
+import java.nio.file.Path as NioPath
+
+@ExperimentalFileSystem
+actual class Path internal actual constructor(
+ internal actual val slash: ByteString,
+ internal actual val bytes: ByteString
+) : Comparable<Path> {
+ actual val isAbsolute: Boolean
+ get() = commonIsAbsolute()
+
+ actual val isRelative: Boolean
+ get() = commonIsRelative()
+
+ @get:JvmName("volumeLetter")
+ actual val volumeLetter: Char?
+ get() = commonVolumeLetter()
+
+ @get:JvmName("nameBytes")
+ actual val nameBytes: ByteString
+ get() = commonNameBytes()
+
+ @get:JvmName("name")
+ actual val name: String
+ get() = commonName()
+
+ @get:JvmName("parent")
+ actual val parent: Path?
+ get() = commonParent()
+
+ actual val isRoot: Boolean
+ get() = commonIsRoot()
+
+ @JvmName("resolve")
+ actual operator fun div(child: String): Path = commonResolve(child)
+
+ @JvmName("resolve")
+ actual operator fun div(child: Path): Path = commonResolve(child)
+
+ fun toFile(): File = File(toString())
+
+ @IgnoreJRERequirement // Can only be invoked on platforms that have java.nio.file.
+ fun toNioPath(): NioPath = Paths.get(toString())
+
+ actual override fun compareTo(other: Path): Int = commonCompareTo(other)
+
+ actual override fun equals(other: Any?): Boolean = commonEquals(other)
+
+ actual override fun hashCode() = commonHashCode()
+
+ actual override fun toString() = commonToString()
+
+ actual companion object {
+ /**
+ * Either `/` (on UNIX-like systems including Android, iOS, and Linux) or `\` (on Windows
+ * systems).
+ */
+ @JvmField
+ actual val DIRECTORY_SEPARATOR: String = File.separator
+
+ @JvmName("get") @JvmStatic
+ actual fun String.toPath(): Path = commonToPath()
+
+ @JvmName("get") @JvmStatic
+ actual fun String.toPath(directorySeparator: String?): Path = commonToPath(directorySeparator)
+
+ @JvmName("get") @JvmStatic
+ fun File.toOkioPath(): Path = toString().toPath()
+
+ @JvmName("get") @JvmStatic
+ @IgnoreJRERequirement // Can only be invoked on platforms that have java.nio.file.
+ fun NioPath.toOkioPath(): Path = toString().toPath()
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/FileSystemJavaTest.java b/okio/src/jvmTest/java/okio/FileSystemJavaTest.java
new file mode 100644
index 00000000..0eab35bd
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/FileSystemJavaTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import okio.fakefilesystem.FakeFileSystem;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public final class FileSystemJavaTest {
+ @Test
+ public void pathApi() {
+ Path path = Path.get("/home/jesse/todo.txt");
+
+ assertThat(Path.get("/home/jesse").resolve("todo.txt")).isEqualTo(path);
+ assertThat(Path.get("/home/jesse/todo.txt", "/")).isEqualTo(path);
+ assertThat(path.isAbsolute()).isTrue();
+ assertThat(path.isRelative()).isFalse();
+ assertThat(path.isRoot()).isFalse();
+ assertThat(path.name()).isEqualTo("todo.txt");
+ assertThat(path.nameBytes()).isEqualTo(ByteString.encodeUtf8("todo.txt"));
+ assertThat(path.parent()).isEqualTo(Path.get("/home/jesse"));
+ assertThat(path.volumeLetter()).isNull();
+ }
+
+ @Test
+ public void directorySeparator() {
+ assertThat(Path.DIRECTORY_SEPARATOR).isIn("/", "\\");
+ }
+
+ /** Like the same test in JvmTest, but this is using the Java APIs. */
+ @Test
+ public void javaIoFileToOkioPath() {
+ String string = "/foo/bar/baz".replace("/", Path.DIRECTORY_SEPARATOR);
+ File javaIoFile = new File(string);
+ Path okioPath = Path.get(string);
+ assertThat(Path.get(javaIoFile)).isEqualTo(okioPath);
+ assertThat(okioPath.toFile()).isEqualTo(javaIoFile);
+ }
+
+ /** Like the same test in JvmTest, but this is using the Java APIs. */
+ @Test
+ public void nioPathToOkioPath() {
+ String string = "/foo/bar/baz".replace("/", okio.Path.DIRECTORY_SEPARATOR);
+ java.nio.file.Path nioPath = Paths.get(string);
+ Path okioPath = Path.get(string);
+ assertThat(Path.get(nioPath)).isEqualTo(okioPath);
+ assertThat((Object) okioPath.toNioPath()).isEqualTo(nioPath);
+ }
+
+ @Test
+ public void fileSystemApi() throws IOException {
+ assertThat(FileSystem.SYSTEM.metadata(FileSystem.SYSTEM_TEMPORARY_DIRECTORY)).isNotNull();
+ }
+
+ @Test
+ public void fakeFileSystemApi() {
+ FakeFileSystem fakeFileSystem = new FakeFileSystem();
+ assertThat(fakeFileSystem.clock).isNotNull();
+ assertThat(fakeFileSystem.allPaths()).isEmpty();
+ assertThat(fakeFileSystem.openPaths()).isEmpty();
+ fakeFileSystem.checkNoOpenFiles();
+ }
+
+ @Test
+ public void forwardingFileSystemApi() throws IOException {
+ FakeFileSystem fakeFileSystem = new FakeFileSystem();
+ final List<String> log = new ArrayList<>();
+ ForwardingFileSystem forwardingFileSystem = new ForwardingFileSystem(fakeFileSystem) {
+ @Override public Path onPathParameter(Path path, String functionName, String parameterName) {
+ log.add(functionName + "(" + parameterName + "=" + path + ")");
+ return path;
+ }
+ };
+ forwardingFileSystem.metadataOrNull(Path.get("/"));
+ assertThat(log).containsExactly("metadataOrNull(path=/)");
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/JvmTest.kt b/okio/src/jvmTest/kotlin/okio/JvmTest.kt
new file mode 100644
index 00000000..f7114bcf
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/JvmTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.Path.Companion.toOkioPath
+import okio.Path.Companion.toPath
+import org.assertj.core.api.Assertions.assertThat
+import java.io.File
+import java.nio.file.Paths
+import kotlin.test.Test
+
+@ExperimentalFileSystem
+class JvmTest {
+ @Test
+ fun baseDirectoryConsistentWithJavaIoFile() {
+ assertThat(FileSystem.SYSTEM.canonicalize(".".toPath()).toString())
+ .isEqualTo(File("").canonicalFile.toString())
+ }
+
+ @Test
+ fun javaIoFileToOkioPath() {
+ val string = "/foo/bar/baz".replace("/", Path.DIRECTORY_SEPARATOR)
+ val javaIoFile = File(string)
+ val okioPath = string.toPath()
+ assertThat(javaIoFile.toOkioPath()).isEqualTo(okioPath)
+ assertThat(okioPath.toFile()).isEqualTo(javaIoFile)
+ }
+
+ @Test
+ fun nioPathToOkioPath() {
+ val string = "/foo/bar/baz".replace("/", Path.DIRECTORY_SEPARATOR)
+ val nioPath = Paths.get(string)
+ val okioPath = string.toPath()
+ assertThat(nioPath.toOkioPath()).isEqualTo(okioPath)
+ assertThat(okioPath.toNioPath() as Any).isEqualTo(nioPath)
+ }
+}
diff --git a/okio/src/linuxX64Main/kotlin/okio/posixVariant.kt b/okio/src/linuxX64Main/kotlin/okio/posixVariant.kt
new file mode 100644
index 00000000..89dd77d0
--- /dev/null
+++ b/okio/src/linuxX64Main/kotlin/okio/posixVariant.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.alloc
+import kotlinx.cinterop.memScoped
+import kotlinx.cinterop.ptr
+import platform.posix.ENOENT
+import platform.posix.S_IFDIR
+import platform.posix.S_IFMT
+import platform.posix.S_IFREG
+import platform.posix.errno
+import platform.posix.stat
+
+@ExperimentalFileSystem
+internal actual fun PosixFileSystem.variantMetadataOrNull(path: Path): FileMetadata? {
+ return memScoped {
+ val stat = alloc<stat>()
+ if (platform.posix.lstat(path.toString(), stat.ptr) != 0) {
+ if (errno == ENOENT) return null
+ throw errnoToIOException(errno)
+ }
+ return@memScoped FileMetadata(
+ isRegularFile = stat.st_mode.toInt() and S_IFMT == S_IFREG,
+ isDirectory = stat.st_mode.toInt() and S_IFMT == S_IFDIR,
+ size = stat.st_size,
+ createdAtMillis = stat.st_ctim.epochMillis,
+ lastModifiedAtMillis = stat.st_mtim.epochMillis,
+ lastAccessedAtMillis = stat.st_atim.epochMillis
+ )
+ }
+}
diff --git a/okio/src/mingwX64Main/kotlin/okio/-Platform.kt b/okio/src/mingwX64Main/kotlin/okio/-Platform.kt
new file mode 100644
index 00000000..98eb85f3
--- /dev/null
+++ b/okio/src/mingwX64Main/kotlin/okio/-Platform.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.toKString
+import okio.Path.Companion.toPath
+import platform.posix.getenv
+
+@ExperimentalFileSystem
+internal actual val PLATFORM_TEMPORARY_DIRECTORY: Path
+ get() {
+ // Windows' built-in APIs check the TEMP, TMP, and USERPROFILE environment variables in order.
+ // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppatha?redirectedfrom=MSDN
+ val temp = getenv("TEMP")
+ if (temp != null) return temp.toKString().toPath()
+
+ val tmp = getenv("TMP")
+ if (tmp != null) return tmp.toKString().toPath()
+
+ val userProfile = getenv("USERPROFILE")
+ if (userProfile != null) return userProfile.toKString().toPath()
+
+ return "\\Windows\\TEMP".toPath()
+ }
diff --git a/okio/src/mingwX64Main/kotlin/okio/posixVariant.kt b/okio/src/mingwX64Main/kotlin/okio/posixVariant.kt
new file mode 100644
index 00000000..e6167bd6
--- /dev/null
+++ b/okio/src/mingwX64Main/kotlin/okio/posixVariant.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.alloc
+import kotlinx.cinterop.memScoped
+import kotlinx.cinterop.ptr
+import okio.Path.Companion.toPath
+import platform.posix.EACCES
+import platform.posix.ENOENT
+import platform.posix.PATH_MAX
+import platform.posix.S_IFDIR
+import platform.posix.S_IFMT
+import platform.posix.S_IFREG
+import platform.posix._fullpath
+import platform.posix._stat64
+import platform.posix.errno
+import platform.posix.free
+import platform.posix.mkdir
+import platform.posix.remove
+import platform.posix.rmdir
+import platform.windows.MOVEFILE_REPLACE_EXISTING
+import platform.windows.MoveFileExA
+
+internal actual val PLATFORM_DIRECTORY_SEPARATOR = "\\"
+
+@ExperimentalFileSystem
+internal actual fun PosixFileSystem.variantDelete(path: Path) {
+ val pathString = path.toString()
+
+ if (remove(pathString) == 0) return
+
+ // If remove failed with EACCES, it might be a directory. Try that.
+ if (errno == EACCES) {
+ if (rmdir(pathString) == 0) return
+ }
+
+ throw errnoToIOException(EACCES)
+}
+
+@ExperimentalFileSystem
+internal actual fun PosixFileSystem.variantMkdir(dir: Path): Int {
+ return mkdir(dir.toString())
+}
+
+@ExperimentalFileSystem
+internal actual fun PosixFileSystem.variantCanonicalize(path: Path): Path {
+ // Note that _fullpath() returns normally if the file doesn't exist.
+ val fullpath = _fullpath(null, path.toString(), PATH_MAX)
+ ?: throw errnoToIOException(errno)
+ try {
+ val pathString = Buffer().writeNullTerminated(fullpath).readUtf8()
+ if (platform.posix.access(pathString, 0) != 0 && errno == ENOENT) {
+ throw FileNotFoundException("no such file")
+ }
+ return pathString.toPath()
+ } finally {
+ free(fullpath)
+ }
+}
+
+@ExperimentalFileSystem
+internal actual fun PosixFileSystem.variantMetadataOrNull(path: Path): FileMetadata? {
+ return memScoped {
+ val stat = alloc<_stat64>()
+ if (_stat64(path.toString(), stat.ptr) != 0) {
+ if (errno == ENOENT) return null
+ throw errnoToIOException(errno)
+ }
+ return@memScoped FileMetadata(
+ isRegularFile = stat.st_mode.toInt() and S_IFMT == S_IFREG,
+ isDirectory = stat.st_mode.toInt() and S_IFMT == S_IFDIR,
+ size = stat.st_size,
+ createdAtMillis = stat.st_ctime * 1000L,
+ lastModifiedAtMillis = stat.st_mtime * 1000L,
+ lastAccessedAtMillis = stat.st_atime * 1000L
+ )
+ }
+}
+
+@ExperimentalFileSystem
+internal actual fun PosixFileSystem.variantMove(source: Path, target: Path) {
+ if (MoveFileExA(source.toString(), target.toString(), MOVEFILE_REPLACE_EXISTING) == 0) {
+ throw lastErrorToIOException()
+ }
+}
diff --git a/okio/src/mingwX64Main/kotlin/okio/windows.kt b/okio/src/mingwX64Main/kotlin/okio/windows.kt
new file mode 100644
index 00000000..a317f564
--- /dev/null
+++ b/okio/src/mingwX64Main/kotlin/okio/windows.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.ByteVarOf
+import kotlinx.cinterop.allocArray
+import kotlinx.cinterop.memScoped
+import platform.windows.DWORD
+import platform.windows.ERROR_FILE_NOT_FOUND
+import platform.windows.ERROR_PATH_NOT_FOUND
+import platform.windows.FORMAT_MESSAGE_FROM_SYSTEM
+import platform.windows.FORMAT_MESSAGE_IGNORE_INSERTS
+import platform.windows.FormatMessageA
+import platform.windows.GetLastError
+import platform.windows.LANG_NEUTRAL
+import platform.windows.SUBLANG_DEFAULT
+
+internal fun lastErrorToIOException(): IOException {
+ val lastError = GetLastError()
+ return when (lastError.toInt()) {
+ ERROR_FILE_NOT_FOUND, ERROR_PATH_NOT_FOUND -> FileNotFoundException(lastErrorString(lastError))
+ else -> IOException(lastErrorString(lastError))
+ }
+}
+
+internal fun lastErrorString(lastError: DWORD): String {
+ memScoped {
+ val messageMaxSize = 2048
+ val message = allocArray<ByteVarOf<Byte>>(messageMaxSize)
+ FormatMessageA(
+ dwFlags = (FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_IGNORE_INSERTS).toUInt(),
+ lpSource = null,
+ dwMessageId = lastError,
+ dwLanguageId = (SUBLANG_DEFAULT * 1024 + LANG_NEUTRAL).toUInt(), // MAKELANGID macro.
+ lpBuffer = message,
+ nSize = messageMaxSize.toUInt(),
+ Arguments = null
+ )
+ return Buffer().writeNullTerminated(message).readUtf8().trim()
+ }
+}
diff --git a/okio/src/nativeMain/kotlin/okio/-Platform.kt b/okio/src/nativeMain/kotlin/okio/-Platform.kt
new file mode 100644
index 00000000..812b8482
--- /dev/null
+++ b/okio/src/nativeMain/kotlin/okio/-Platform.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+@ExperimentalFileSystem
+internal actual val PLATFORM_FILE_SYSTEM: FileSystem
+ get() = PosixFileSystem
diff --git a/okio/src/nativeMain/kotlin/okio/FileSink.kt b/okio/src/nativeMain/kotlin/okio/FileSink.kt
new file mode 100644
index 00000000..8b2c2bc1
--- /dev/null
+++ b/okio/src/nativeMain/kotlin/okio/FileSink.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.CPointer
+import kotlinx.cinterop.addressOf
+import kotlinx.cinterop.usePinned
+import okio.Buffer.UnsafeCursor
+import platform.posix.FILE
+import platform.posix.errno
+import platform.posix.fclose
+import platform.posix.fflush
+
+/** Writes bytes to a file as a sink. */
+internal class FileSink(
+ private val file: CPointer<FILE>
+) : Sink {
+ private val unsafeCursor = UnsafeCursor()
+ private var closed = false
+
+ override fun write(
+ source: Buffer,
+ byteCount: Long
+ ) {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ require(source.size >= byteCount) { "source.size=${source.size} < byteCount=$byteCount" }
+ check(!closed) { "closed" }
+
+ var byteCount = byteCount
+ while (byteCount > 0) {
+ // Get the first segment, which we will read a contiguous range of bytes from.
+ val cursor = source.readUnsafe(unsafeCursor)
+ val segmentReadableByteCount = cursor.next()
+ val attemptCount = minOf(byteCount, segmentReadableByteCount.toLong()).toInt()
+
+ // Copy bytes from that segment into the file.
+ val bytesWritten = cursor.data!!.usePinned { pinned ->
+ variantFwrite(pinned.addressOf(cursor.start), attemptCount.toUInt(), file).toLong()
+ }
+
+ // Consume the bytes from the segment.
+ cursor.close()
+ source.skip(bytesWritten)
+ byteCount -= bytesWritten
+
+ // If the write was shorter than expected, some I/O failed.
+ if (bytesWritten < attemptCount) {
+ throw errnoToIOException(errno)
+ }
+ }
+ }
+
+ override fun flush() {
+ if (fflush(file) != 0) {
+ throw errnoToIOException(errno)
+ }
+ }
+
+ override fun timeout(): Timeout = Timeout.NONE
+
+ override fun close() {
+ if (closed) return
+ closed = true
+ if (fclose(file) != 0) {
+ throw errnoToIOException(errno)
+ }
+ }
+}
diff --git a/okio/src/nativeMain/kotlin/okio/FileSource.kt b/okio/src/nativeMain/kotlin/okio/FileSource.kt
new file mode 100644
index 00000000..2b7416da
--- /dev/null
+++ b/okio/src/nativeMain/kotlin/okio/FileSource.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.CPointer
+import kotlinx.cinterop.addressOf
+import kotlinx.cinterop.usePinned
+import okio.Buffer.UnsafeCursor
+import platform.posix.FILE
+import platform.posix.errno
+import platform.posix.fclose
+import platform.posix.feof
+import platform.posix.ferror
+
+/** Reads the bytes of a file as a source. */
+internal class FileSource(
+ private val file: CPointer<FILE>
+) : Source {
+ private val unsafeCursor = UnsafeCursor()
+ private var closed = false
+
+ override fun read(
+ sink: Buffer,
+ byteCount: Long
+ ): Long {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ check(!closed) { "closed" }
+ val sinkInitialSize = sink.size
+
+ // Request a writable segment in `sink`. We request at least 1024 bytes, unless the request is
+ // for smaller than that, in which case we request only that many bytes.
+ val cursor = sink.readAndWriteUnsafe(unsafeCursor)
+ val addedCapacityCount = cursor.expandBuffer(minByteCount = minOf(byteCount, 1024L).toInt())
+
+ // Now that we have a writable segment, figure out how many bytes to read. This is the smaller
+ // of the user's requested byte count, and the segment's writable capacity.
+ val attemptCount = minOf(byteCount, addedCapacityCount)
+
+ // Copy bytes from the file to the segment.
+ val bytesRead = cursor.data!!.usePinned { pinned ->
+ variantFread(pinned.addressOf(cursor.start), attemptCount.toUInt(), file).toLong()
+ }
+
+ // Remove new capacity that was added but not used.
+ cursor.resizeBuffer(sinkInitialSize + bytesRead)
+ cursor.close()
+
+ return when {
+ bytesRead == attemptCount -> bytesRead
+ feof(file) != 0 -> if (bytesRead == 0L) -1L else bytesRead
+ ferror(file) != 0 -> throw errnoToIOException(errno)
+ else -> bytesRead
+ }
+ }
+
+ override fun timeout(): Timeout = Timeout.NONE
+
+ override fun close() {
+ if (closed) return
+ closed = true
+ fclose(file)
+ }
+}
diff --git a/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt b/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt
new file mode 100644
index 00000000..bf8a71c3
--- /dev/null
+++ b/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.CPointer
+import kotlinx.cinterop.get
+import okio.Path.Companion.toPath
+import okio.internal.toPath
+import platform.posix.DIR
+import platform.posix.FILE
+import platform.posix.closedir
+import platform.posix.dirent
+import platform.posix.errno
+import platform.posix.fopen
+import platform.posix.opendir
+import platform.posix.readdir
+import platform.posix.set_posix_errno
+
+@ExperimentalFileSystem
+internal object PosixFileSystem : FileSystem() {
+ private val SELF_DIRECTORY_ENTRY = ".".toPath()
+ private val PARENT_DIRECTORY_ENTRY = "..".toPath()
+
+ override fun canonicalize(path: Path) = variantCanonicalize(path)
+
+ override fun metadataOrNull(path: Path): FileMetadata? {
+ return variantMetadataOrNull(path)
+ }
+
+ override fun list(dir: Path): List<Path> {
+ val opendir: CPointer<DIR> = opendir(dir.toString())
+ ?: throw errnoToIOException(errno)
+
+ try {
+ val result = mutableListOf<Path>()
+ val buffer = Buffer()
+
+ set_posix_errno(0) // If readdir() returns null it's either the end or an error.
+ while (true) {
+ val dirent: CPointer<dirent> = readdir(opendir) ?: break
+ val childPath = buffer.writeNullTerminated(
+ bytes = dirent[0].d_name
+ ).toPath()
+
+ if (childPath == SELF_DIRECTORY_ENTRY || childPath == PARENT_DIRECTORY_ENTRY) {
+ continue // exclude '.' and '..' from the results.
+ }
+
+ result += dir / childPath
+ }
+
+ if (errno != 0) throw errnoToIOException(errno)
+
+ result.sort()
+ return result
+ } finally {
+ closedir(opendir) // Ignore errno from closedir.
+ }
+ }
+
+ override fun source(file: Path): Source {
+ val openFile: CPointer<FILE> = fopen(file.toString(), "r")
+ ?: throw errnoToIOException(errno)
+ return FileSource(openFile)
+ }
+
+ override fun sink(file: Path): Sink {
+ val openFile: CPointer<FILE> = fopen(file.toString(), "w")
+ ?: throw errnoToIOException(errno)
+ return FileSink(openFile)
+ }
+
+ override fun appendingSink(file: Path): Sink {
+ val openFile: CPointer<FILE> = fopen(file.toString(), "a")
+ ?: throw errnoToIOException(errno)
+ return FileSink(openFile)
+ }
+
+ override fun createDirectory(dir: Path) {
+ val result = variantMkdir(dir)
+ if (result != 0) {
+ throw errnoToIOException(errno)
+ }
+ }
+
+ override fun atomicMove(
+ source: Path,
+ target: Path
+ ) {
+ variantMove(source, target)
+ }
+
+ override fun delete(path: Path) {
+ variantDelete(path)
+ }
+
+ override fun toString() = "PosixSystemFileSystem"
+}
diff --git a/okio/src/nativeMain/kotlin/okio/cinterop.kt b/okio/src/nativeMain/kotlin/okio/cinterop.kt
new file mode 100644
index 00000000..9c6850f0
--- /dev/null
+++ b/okio/src/nativeMain/kotlin/okio/cinterop.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.ByteVarOf
+import kotlinx.cinterop.CPointer
+import kotlinx.cinterop.get
+import kotlinx.cinterop.set
+import platform.posix.ENOENT
+import platform.posix.strerror
+
+internal fun Buffer.writeNullTerminated(bytes: CPointer<ByteVarOf<Byte>>): Buffer = apply {
+ var pos = 0
+ while (true) {
+ val byte = bytes[pos++].toInt()
+ if (byte == 0) {
+ break
+ } else {
+ writeByte(byte)
+ }
+ }
+}
+
+internal fun Buffer.write(
+ source: CPointer<ByteVarOf<Byte>>,
+ offset: Int = 0,
+ byteCount: Int
+): Buffer = apply {
+ for (i in offset until offset + byteCount) {
+ writeByte(source[i].toInt())
+ }
+}
+
+internal fun Buffer.read(
+ sink: CPointer<ByteVarOf<Byte>>,
+ offset: Int = 0,
+ byteCount: Int
+): Buffer = apply {
+ for (i in offset until offset + byteCount) {
+ sink[i] = readByte()
+ }
+}
+
+internal fun errnoToIOException(errno: Int): IOException {
+ val message = strerror(errno)
+ val messageString = if (message != null) {
+ Buffer().writeNullTerminated(message).readUtf8()
+ } else {
+ "errno: $errno"
+ }
+ return when (errno) {
+ ENOENT -> FileNotFoundException(messageString)
+ else -> IOException(messageString)
+ }
+}
diff --git a/okio/src/nativeMain/kotlin/okio/posixVariant.kt b/okio/src/nativeMain/kotlin/okio/posixVariant.kt
new file mode 100644
index 00000000..4744d047
--- /dev/null
+++ b/okio/src/nativeMain/kotlin/okio/posixVariant.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+@ExperimentalFileSystem
+internal expect fun PosixFileSystem.variantDelete(path: Path)
+
+@ExperimentalFileSystem
+internal expect fun PosixFileSystem.variantMkdir(dir: Path): Int
+
+@ExperimentalFileSystem
+internal expect fun PosixFileSystem.variantCanonicalize(path: Path): Path
+
+@ExperimentalFileSystem
+internal expect fun PosixFileSystem.variantMetadataOrNull(path: Path): FileMetadata?
+
+@ExperimentalFileSystem
+internal expect fun PosixFileSystem.variantMove(source: Path, target: Path)
diff --git a/okio/src/nativeMain/kotlin/okio/sizetVariant.kt b/okio/src/nativeMain/kotlin/okio/sizetVariant.kt
new file mode 100644
index 00000000..b1c29c6d
--- /dev/null
+++ b/okio/src/nativeMain/kotlin/okio/sizetVariant.kt
@@ -0,0 +1,33 @@
+/*
+* Copyright (C) 2020 Square, Inc.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+package okio
+
+import kotlinx.cinterop.ByteVar
+import kotlinx.cinterop.ByteVarOf
+import kotlinx.cinterop.CPointer
+import platform.posix.FILE
+
+internal expect fun variantFread(
+ target: CPointer<ByteVarOf<Byte>>,
+ byteCount: UInt,
+ file: CPointer<FILE>
+): UInt
+
+internal expect fun variantFwrite(
+ target: CPointer<ByteVar>,
+ byteCount: UInt,
+ file: CPointer<FILE>
+): UInt
diff --git a/okio/src/nonJvmMain/kotlin/okio/-Platform.kt b/okio/src/nonJvmMain/kotlin/okio/-Platform.kt
index 90fcd36d..73ed2637 100644
--- a/okio/src/nonJvmMain/kotlin/okio/-Platform.kt
+++ b/okio/src/nonJvmMain/kotlin/okio/-Platform.kt
@@ -19,6 +19,8 @@ package okio
import okio.internal.commonAsUtf8ToByteArray
import okio.internal.commonToUtf8String
+internal expect val PLATFORM_DIRECTORY_SEPARATOR: String
+
internal actual fun ByteArray.toUtf8String(): String = commonToUtf8String()
internal actual fun String.asUtf8ToByteArray(): ByteArray = commonAsUtf8ToByteArray()
diff --git a/okio/src/nonJvmMain/kotlin/okio/Path.kt b/okio/src/nonJvmMain/kotlin/okio/Path.kt
new file mode 100644
index 00000000..31e679de
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/Path.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.commonCompareTo
+import okio.internal.commonEquals
+import okio.internal.commonHashCode
+import okio.internal.commonIsAbsolute
+import okio.internal.commonIsRelative
+import okio.internal.commonIsRoot
+import okio.internal.commonName
+import okio.internal.commonNameBytes
+import okio.internal.commonParent
+import okio.internal.commonResolve
+import okio.internal.commonToPath
+import okio.internal.commonToString
+import okio.internal.commonVolumeLetter
+
+@ExperimentalFileSystem
+actual class Path internal actual constructor(
+ internal actual val slash: ByteString,
+ internal actual val bytes: ByteString
+) : Comparable<Path> {
+ actual val isAbsolute: Boolean
+ get() = commonIsAbsolute()
+
+ actual val isRelative: Boolean
+ get() = commonIsRelative()
+
+ actual val volumeLetter: Char?
+ get() = commonVolumeLetter()
+
+ actual val nameBytes: ByteString
+ get() = commonNameBytes()
+
+ actual val name: String
+ get() = commonName()
+
+ actual val parent: Path?
+ get() = commonParent()
+
+ actual val isRoot: Boolean
+ get() = commonIsRoot()
+
+ actual operator fun div(child: String): Path = commonResolve(child)
+
+ actual operator fun div(child: Path): Path = commonResolve(child)
+
+ actual override fun compareTo(other: Path): Int = commonCompareTo(other)
+
+ actual override fun equals(other: Any?): Boolean = commonEquals(other)
+
+ actual override fun hashCode() = commonHashCode()
+
+ actual override fun toString() = commonToString()
+
+ actual companion object {
+ actual val DIRECTORY_SEPARATOR: String = PLATFORM_DIRECTORY_SEPARATOR
+
+ actual fun String.toPath(): Path = commonToPath()
+
+ actual fun String.toPath(directorySeparator: String?): Path = commonToPath(directorySeparator)
+ }
+}
diff --git a/okio/src/sizet32Main/kotlin/okio/sizetVariant.kt b/okio/src/sizet32Main/kotlin/okio/sizetVariant.kt
new file mode 100644
index 00000000..0495d62d
--- /dev/null
+++ b/okio/src/sizet32Main/kotlin/okio/sizetVariant.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.ByteVar
+import kotlinx.cinterop.ByteVarOf
+import kotlinx.cinterop.CPointer
+import platform.posix.FILE
+import platform.posix.fread
+import platform.posix.fwrite
+
+internal actual fun variantFread(
+ target: CPointer<ByteVarOf<Byte>>,
+ byteCount: UInt,
+ file: CPointer<FILE>
+): UInt {
+ return fread(target, 1, byteCount, file)
+}
+
+internal actual fun variantFwrite(
+ target: CPointer<ByteVar>,
+ byteCount: UInt,
+ file: CPointer<FILE>
+): UInt {
+ return fwrite(target, 1, byteCount, file)
+}
diff --git a/okio/src/sizet64Main/kotlin/okio/sizetVariant.kt b/okio/src/sizet64Main/kotlin/okio/sizetVariant.kt
new file mode 100644
index 00000000..e024addf
--- /dev/null
+++ b/okio/src/sizet64Main/kotlin/okio/sizetVariant.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.ByteVar
+import kotlinx.cinterop.ByteVarOf
+import kotlinx.cinterop.CPointer
+import platform.posix.FILE
+import platform.posix.fread
+import platform.posix.fwrite
+
+internal actual fun variantFread(
+ target: CPointer<ByteVarOf<Byte>>,
+ byteCount: UInt,
+ file: CPointer<FILE>
+): UInt {
+ return fread(target, 1, byteCount.toULong(), file).toUInt()
+}
+
+internal actual fun variantFwrite(
+ target: CPointer<ByteVar>,
+ byteCount: UInt,
+ file: CPointer<FILE>
+): UInt {
+ return fwrite(target, 1, byteCount.toULong(), file).toUInt()
+}
diff --git a/okio/src/unixMain/kotlin/okio/-Platform.kt b/okio/src/unixMain/kotlin/okio/-Platform.kt
new file mode 100644
index 00000000..07dcfda3
--- /dev/null
+++ b/okio/src/unixMain/kotlin/okio/-Platform.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.toKString
+import okio.Path.Companion.toPath
+import platform.posix.getenv
+
+@ExperimentalFileSystem
+internal actual val PLATFORM_TEMPORARY_DIRECTORY: Path
+ get() {
+ val tmpdir = getenv("TMPDIR")
+ if (tmpdir != null) return tmpdir.toKString().toPath()
+
+ return "/tmp".toPath()
+ }
diff --git a/okio/src/unixMain/kotlin/okio/posixVariant.kt b/okio/src/unixMain/kotlin/okio/posixVariant.kt
new file mode 100644
index 00000000..1c9ae416
--- /dev/null
+++ b/okio/src/unixMain/kotlin/okio/posixVariant.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.toPath
+import platform.posix.errno
+import platform.posix.free
+import platform.posix.mkdir
+import platform.posix.realpath
+import platform.posix.remove
+import platform.posix.rename
+import platform.posix.timespec
+
+internal actual val PLATFORM_DIRECTORY_SEPARATOR = "/"
+
+@ExperimentalFileSystem
+internal actual fun PosixFileSystem.variantDelete(path: Path) {
+ val result = remove(path.toString())
+ if (result != 0) {
+ throw errnoToIOException(errno)
+ }
+}
+
+@ExperimentalFileSystem
+internal actual fun PosixFileSystem.variantMkdir(dir: Path): Int {
+ return mkdir(dir.toString(), 0b111111111 /* octal 777 */)
+}
+
+@ExperimentalFileSystem
+internal actual fun PosixFileSystem.variantCanonicalize(path: Path): Path {
+ // Note that realpath() fails if the file doesn't exist.
+ val fullpath = realpath(path.toString(), null)
+ ?: throw errnoToIOException(errno)
+ try {
+ return Buffer().writeNullTerminated(fullpath).toPath()
+ } finally {
+ free(fullpath)
+ }
+}
+
+@ExperimentalFileSystem
+internal actual fun PosixFileSystem.variantMove(
+ source: Path,
+ target: Path
+) {
+ val result = rename(source.toString(), target.toString())
+ if (result != 0) {
+ throw errnoToIOException(errno)
+ }
+}
+
+internal val timespec.epochMillis: Long
+ get() = tv_sec * 1000L + tv_sec / 1_000_000L
diff --git a/settings.gradle b/settings.gradle
index 97f9d9e9..08ea10d4 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,6 +1,7 @@
rootProject.name = 'okio-parent'
include ':okio'
+include ':okio-fakefilesystem'
include ':okio:jvm:japicmp'
include ':okio:jvm:jmh'
include ':samples'