diff --git a/okio/src/commonMain/kotlin/okio/Path.kt b/okio/src/commonMain/kotlin/okio/Path.kt index 48da285b06..b8a9bf84a9 100644 --- a/okio/src/commonMain/kotlin/okio/Path.kt +++ b/okio/src/commonMain/kotlin/okio/Path.kt @@ -15,12 +15,7 @@ */ package okio -import okio.ByteString.Companion.EMPTY -import okio.ByteString.Companion.encodeUtf8 import okio.Path.Companion.toPath -import kotlin.jvm.JvmName -import kotlin.jvm.JvmOverloads -import kotlin.jvm.JvmStatic /** * A hierarchical address on a file system. A path is an identifier only; a [FileSystem] is required @@ -140,20 +135,13 @@ import kotlin.jvm.JvmStatic * */ @ExperimentalFileSystem -class Path private constructor( - private val slash: ByteString, - private val bytes: ByteString -) : Comparable { - init { - require(slash == SLASH || slash == BACKSLASH) - } +expect class Path internal constructor(slash: ByteString, bytes: ByteString) : Comparable { + internal val slash: ByteString + internal val bytes: ByteString val isAbsolute: Boolean - get() = bytes.startsWith(slash) || - (volumeLetter != null && bytes.size > 2 && bytes[2] == '\\'.toByte()) val isRelative: Boolean - get() = !isAbsolute /** * This is the volume letter like "C" on Windows paths that starts with a volume letter. For @@ -164,31 +152,11 @@ class Path private constructor( * example, the path "C:notepad.exe" is relative to whatever the current working directory is on * the C: drive. */ - @get:JvmName("volumeLetter") val volumeLetter: Char? - get() { - 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 - } - @get:JvmName("nameBytes") val nameBytes: ByteString - get() { - val lastSlash = bytes.lastIndexOf(slash) - return when { - lastSlash != -1 -> bytes.substring(lastSlash + 1) - volumeLetter != null && bytes.size == 2 -> EMPTY // "C:" has no name. - else -> bytes - } - } - @get:JvmName("name") val name: String - get() = nameBytes.utf8() /** * Returns the path immediately enclosing this path. @@ -202,37 +170,7 @@ class Path private constructor( * * A reference to the current working directory on a Windows volume (`C:`). * * A series of relative paths (like `..` and `../..`). */ - @get:JvmName("parent") val parent: Path? - get() { - 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 file system root '/'. - } - else -> { - return Path(slash, bytes.substring(endIndex = lastSlash)) - } - } - } /** * Returns true if this is an absolute path with no parent. UNIX paths have a single root, `/`. @@ -240,15 +178,6 @@ class Path private constructor( * are also roots. */ val isRoot: Boolean - get() = parent == null && isAbsolute - - private fun 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 - } /** * Returns a path that resolves [child] relative to this path. @@ -256,10 +185,7 @@ class Path private constructor( * If [child] is an [absolute path][isAbsolute] or [has a volume letter][hasVolumeLetter] then * this function is equivalent to `child.toPath()`. */ - @JvmName("resolve") - operator fun div(child: String): Path { - return div(Buffer().writeUtf8(child).toPath(slash)) - } + operator fun div(child: String): Path /** * Returns a path that resolves [child] relative to this path. @@ -267,142 +193,21 @@ class Path private constructor( * If [child] is an [absolute path][isAbsolute] or [has a volume letter][hasVolumeLetter] then * this function is equivalent to `child.toPath()`. */ - @JvmName("resolve") - operator fun div(child: Path): Path { - if (child.isAbsolute || child.volumeLetter != null) return child + operator fun div(child: Path): Path - val buffer = Buffer() - buffer.write(bytes) - if (buffer.size > 0) { - buffer.write(slash) - } - buffer.write(child.bytes) - return buffer.toPath(directorySeparator = slash) - } + override fun compareTo(other: Path): Int - override fun compareTo(other: Path): Int { - val bytesResult = bytes.compareTo(other.bytes) - if (bytesResult != 0) return bytesResult - return slash.compareTo(other.slash) - } + override fun equals(other: Any?): Boolean - override fun equals(other: Any?): Boolean { - return other is Path && other.bytes == bytes && other.slash == slash - } + override fun hashCode(): Int - override fun hashCode() = bytes.hashCode() xor slash.hashCode() - - override fun toString() = bytes.utf8() + override fun toString(): String companion object { - private val SLASH = "/".encodeUtf8() - private val BACKSLASH = "\\".encodeUtf8() - private val ANY_SLASH = "/\\".encodeUtf8() - private val DOT = ".".encodeUtf8() - private val DOT_DOT = "..".encodeUtf8() - - val directorySeparator = DIRECTORY_SEPARATOR - - @JvmName("get") @JvmOverloads @JvmStatic - fun String.toPath(directorySeparator: String? = null): Path = - Buffer().writeUtf8(this).toPath(directorySeparator?.toSlash()) - - /** Consume the buffer and return it as a path. */ - 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 -> 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() - 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") - } - } + val directorySeparator: String - private fun Byte.toSlash(): ByteString { - return when (toInt()) { - '/'.toInt() -> SLASH - '\\'.toInt() -> BACKSLASH - else -> throw IllegalArgumentException("not a directory separator: $this") - } - } + fun String.toPath(): Path - 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' - } + 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 0000000000..80626a598e --- /dev/null +++ b/okio/src/commonMain/kotlin/okio/internal/Path.kt @@ -0,0 +1,267 @@ +/* + * 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.DIRECTORY_SEPARATOR +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 -> 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() + 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/jvmMain/kotlin/okio/Path.kt b/okio/src/jvmMain/kotlin/okio/Path.kt new file mode 100644 index 0000000000..409a9b6d4e --- /dev/null +++ b/okio/src/jvmMain/kotlin/okio/Path.kt @@ -0,0 +1,85 @@ +/* + * 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 { + 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) + + 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 directorySeparator: String = DIRECTORY_SEPARATOR + + @JvmName("get") @JvmStatic + actual fun String.toPath(): Path = commonToPath() + + @JvmName("get") @JvmStatic + actual fun String.toPath(directorySeparator: String?): Path = commonToPath(directorySeparator) + } +} diff --git a/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt b/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt index f92c1c5d05..bf8a71c31a 100644 --- a/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt +++ b/okio/src/nativeMain/kotlin/okio/PosixFileSystem.kt @@ -18,6 +18,7 @@ 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 diff --git a/okio/src/nonJvmMain/kotlin/okio/Path.kt b/okio/src/nonJvmMain/kotlin/okio/Path.kt new file mode 100644 index 0000000000..3b4891b6a3 --- /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 { + 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 directorySeparator: String = DIRECTORY_SEPARATOR + + actual fun String.toPath(): Path = commonToPath() + + actual fun String.toPath(directorySeparator: String?): Path = commonToPath(directorySeparator) + } +} diff --git a/okio/src/unixMain/kotlin/okio/posixVariant.kt b/okio/src/unixMain/kotlin/okio/posixVariant.kt index 8b499a477c..4c7ace1698 100644 --- a/okio/src/unixMain/kotlin/okio/posixVariant.kt +++ b/okio/src/unixMain/kotlin/okio/posixVariant.kt @@ -15,7 +15,7 @@ */ package okio -import okio.Path.Companion.toPath +import okio.internal.toPath import platform.posix.errno import platform.posix.free import platform.posix.mkdir