From afe491efb8db7687c1a1064de6635e2b4abe918f Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 26 Mar 2024 21:40:49 -0700 Subject: [PATCH] Feat: Add Kotlin file system API --- app/build.gradle | 1 + .../zhanghai/kotlin/filesystem/AccessMode.kt | 7 + .../kotlin/filesystem/BasicCopyFileOption.kt | 7 + .../filesystem/BasicDirectoryStreamOption.kt | 6 + .../filesystem/BasicFileContentOption.kt | 10 + .../kotlin/filesystem/CopyFileOption.kt | 3 + .../kotlin/filesystem/CreateFileOption.kt | 3 + .../kotlin/filesystem/DirectoryEntry.kt | 11 + .../kotlin/filesystem/DirectoryStream.kt | 7 + .../filesystem/DirectoryStreamOption.kt | 3 + .../zhanghai/kotlin/filesystem/FileContent.kt | 82 + .../kotlin/filesystem/FileContentOption.kt | 3 + .../kotlin/filesystem/FileMetadata.kt | 15 + .../kotlin/filesystem/FileMetadataOption.kt | 3 + .../kotlin/filesystem/FileMetadataView.kt | 13 + .../zhanghai/kotlin/filesystem/FileStore.kt | 7 + .../kotlin/filesystem/FileStoreMetadata.kt | 15 + .../zhanghai/kotlin/filesystem/FileSystem.kt | 118 + .../kotlin/filesystem/FileSystemException.kt | 10 + .../kotlin/filesystem/FileSystemExceptions.kt | 57 + .../kotlin/filesystem/FileSystemProvider.kt | 7 + .../kotlin/filesystem/FileSystemRegistry.kt | 31 + .../me/zhanghai/kotlin/filesystem/FileType.kt | 8 + .../zhanghai/kotlin/filesystem/LinkOption.kt | 5 + .../me/zhanghai/kotlin/filesystem/Path.kt | 253 + .../me/zhanghai/kotlin/filesystem/Paths.kt | 118 + .../kotlin/filesystem/PlatformFileSystem.kt | 13 + .../java/me/zhanghai/kotlin/filesystem/Uri.kt | 283 ++ .../filesystem/internal/AsciiCharSet.kt | 63 + .../kotlin/filesystem/internal/ByteStrings.kt | 13 + .../kotlin/filesystem/internal/Lists.kt | 37 + .../kotlin/filesystem/internal/UriParser.kt | 4364 +++++++++++++++++ .../kotlin/filesystem/internal/UriParsers.kt | 267 + .../kotlin/filesystem/io/AsyncCloseable.kt | 36 + .../kotlin/filesystem/io/AsyncFlushable.kt | 8 + .../kotlin/filesystem/io/AsyncSink.kt | 31 + .../kotlin/filesystem/io/AsyncSource.kt | 26 + .../filesystem/posix/PosixFileMetadata.kt | 23 + .../filesystem/posix/PosixFileMetadataView.kt | 11 + .../kotlin/filesystem/posix/PosixFileType.kt | 12 + .../kotlin/filesystem/posix/PosixModeBit.kt | 13 + .../filesystem/posix/PosixModeOption.kt | 5 + 42 files changed, 6008 insertions(+) create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/AccessMode.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/BasicCopyFileOption.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/BasicDirectoryStreamOption.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/BasicFileContentOption.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/CopyFileOption.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/CreateFileOption.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryEntry.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryStream.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryStreamOption.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileContent.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileContentOption.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadata.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadataOption.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadataView.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileStore.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileStoreMetadata.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystem.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemException.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemExceptions.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemProvider.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemRegistry.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/FileType.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/LinkOption.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/Path.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/Paths.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/PlatformFileSystem.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/Uri.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/internal/AsciiCharSet.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/internal/ByteStrings.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/internal/Lists.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/internal/UriParser.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/internal/UriParsers.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncCloseable.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncFlushable.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncSink.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncSource.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixFileMetadata.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixFileMetadataView.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixFileType.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixModeBit.kt create mode 100644 app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixModeOption.kt diff --git a/app/build.gradle b/app/build.gradle index 4c02e423e..b20e0b586 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -119,6 +119,7 @@ dependencies { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'org.jetbrains.kotlinx:kotlinx-io-core:0.3.2' // kotlinx-coroutines-android depends on kotlin-stdlib-jdk8 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" def kotlinx_coroutines_version = '1.8.0' diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/AccessMode.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/AccessMode.kt new file mode 100644 index 000000000..83c8cb4d9 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/AccessMode.kt @@ -0,0 +1,7 @@ +package me.zhanghai.kotlin.filesystem + +public enum class AccessMode { + READ, + WRITE, + EXECUTE +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/BasicCopyFileOption.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/BasicCopyFileOption.kt new file mode 100644 index 000000000..a8a7c96f1 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/BasicCopyFileOption.kt @@ -0,0 +1,7 @@ +package me.zhanghai.kotlin.filesystem + +public enum class BasicCopyFileOption : CopyFileOption { + REPLACE_EXISTING, + COPY_METADATA, + ATOMIC_MOVE +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/BasicDirectoryStreamOption.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/BasicDirectoryStreamOption.kt new file mode 100644 index 000000000..9113ed3cf --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/BasicDirectoryStreamOption.kt @@ -0,0 +1,6 @@ +package me.zhanghai.kotlin.filesystem + +public enum class BasicDirectoryStreamOption : DirectoryStreamOption { + READ_TYPE, + READ_METADATA +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/BasicFileContentOption.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/BasicFileContentOption.kt new file mode 100644 index 000000000..71a85f76c --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/BasicFileContentOption.kt @@ -0,0 +1,10 @@ +package me.zhanghai.kotlin.filesystem + +public enum class BasicFileContentOption : FileContentOption { + READ, + WRITE, + APPEND, + TRUNCATE_EXISTING, + CREATE, + CREATE_NEW +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/CopyFileOption.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/CopyFileOption.kt new file mode 100644 index 000000000..bffadaf9e --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/CopyFileOption.kt @@ -0,0 +1,3 @@ +package me.zhanghai.kotlin.filesystem + +public interface CopyFileOption diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/CreateFileOption.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/CreateFileOption.kt new file mode 100644 index 000000000..0c0ad1f8a --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/CreateFileOption.kt @@ -0,0 +1,3 @@ +package me.zhanghai.kotlin.filesystem + +public interface CreateFileOption : FileContentOption diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryEntry.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryEntry.kt new file mode 100644 index 000000000..35b1c233a --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryEntry.kt @@ -0,0 +1,11 @@ +package me.zhanghai.kotlin.filesystem + +import kotlinx.io.bytestring.ByteString + +public interface DirectoryEntry { + public val name: ByteString + + public val type: FileType? + + public val metadata: FileMetadata? +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryStream.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryStream.kt new file mode 100644 index 000000000..f827d39ac --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryStream.kt @@ -0,0 +1,7 @@ +package me.zhanghai.kotlin.filesystem + +import me.zhanghai.kotlin.filesystem.io.AsyncCloseable + +public interface DirectoryStream : AsyncCloseable { + public suspend fun read(): DirectoryEntry? +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryStreamOption.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryStreamOption.kt new file mode 100644 index 000000000..db10341ec --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/DirectoryStreamOption.kt @@ -0,0 +1,3 @@ +package me.zhanghai.kotlin.filesystem + +public interface DirectoryStreamOption diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileContent.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileContent.kt new file mode 100644 index 000000000..4b7aa77a1 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileContent.kt @@ -0,0 +1,82 @@ +package me.zhanghai.kotlin.filesystem + +import kotlinx.io.Buffer +import kotlinx.io.IOException +import me.zhanghai.kotlin.filesystem.io.AsyncCloseable +import me.zhanghai.kotlin.filesystem.io.AsyncSink +import me.zhanghai.kotlin.filesystem.io.AsyncSource +import kotlin.concurrent.Volatile +import kotlin.coroutines.cancellation.CancellationException + +public interface FileContent : AsyncCloseable { + @Throws(CancellationException::class, IOException::class) + public suspend fun readAtMostTo(position: Long, sink: Buffer, byteCount: Long): Long + + @Throws(CancellationException::class, IOException::class) + public suspend fun write(position: Long, source: Buffer, byteCount: Long) + + @Throws(CancellationException::class, IOException::class) public suspend fun getSize() + + @Throws(CancellationException::class, IOException::class) public suspend fun setSize(size: Long) + + @Throws(CancellationException::class, IOException::class) public suspend fun sync() +} + +public fun FileContent.openSource(position: Long = 0): AsyncSource = + FileContentSource(this, position) + +private class FileContentSource(private val fileContent: FileContent, private var position: Long) : + AsyncSource { + @Volatile private var closed: Boolean = false + + @Throws(CancellationException::class, IOException::class) + override suspend fun readAtMostTo(sink: Buffer, byteCount: Long): Long { + checkNotClosed() + val read = fileContent.readAtMostTo(position, sink, byteCount) + if (read != -1L) { + position += read + } + return read + } + + private fun checkNotClosed() { + if (!closed) { + throw IOException("Source is closed") + } + } + + @Throws(CancellationException::class, IOException::class) + override suspend fun close() { + closed = true + } +} + +public fun FileContent.openSink(position: Long = 0): AsyncSink = FileContentSink(this, position) + +private class FileContentSink(private val fileContent: FileContent, private var position: Long) : + AsyncSink { + @Volatile private var closed: Boolean = false + + @Throws(CancellationException::class, IOException::class) + override suspend fun write(source: Buffer, byteCount: Long) { + checkNotClosed() + fileContent.write(position, source, byteCount) + position += byteCount + } + + @Throws(CancellationException::class, IOException::class) + override suspend fun flush() { + checkNotClosed() + } + + private fun checkNotClosed() { + if (!closed) { + throw IOException("Sink is closed") + } + } + + @Throws(CancellationException::class, IOException::class) + override suspend fun close() { + closed = true + } +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileContentOption.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileContentOption.kt new file mode 100644 index 000000000..ab385a68c --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileContentOption.kt @@ -0,0 +1,3 @@ +package me.zhanghai.kotlin.filesystem + +public interface FileContentOption diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadata.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadata.kt new file mode 100644 index 000000000..6d4e1e02d --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadata.kt @@ -0,0 +1,15 @@ +package me.zhanghai.kotlin.filesystem + +public interface FileMetadata { + public val id: Any + + public val type: FileType + + public val size: Long + + public val lastModificationTimeMillis: Long + + public val lastAccessTimeMillis: Long? + + public val creationTimeMillis: Long? +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadataOption.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadataOption.kt new file mode 100644 index 000000000..ce1e86de4 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadataOption.kt @@ -0,0 +1,3 @@ +package me.zhanghai.kotlin.filesystem + +public interface FileMetadataOption diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadataView.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadataView.kt new file mode 100644 index 000000000..4de26a260 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileMetadataView.kt @@ -0,0 +1,13 @@ +package me.zhanghai.kotlin.filesystem + +import me.zhanghai.kotlin.filesystem.io.AsyncCloseable + +public interface FileMetadataView : AsyncCloseable { + public suspend fun readMetadata(): FileMetadata + + public suspend fun setTimes( + lastModificationTimeMillis: Long? = null, + lastAccessTimeMillis: Long? = null, + creationTimeMillis: Long? = null + ) +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileStore.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileStore.kt new file mode 100644 index 000000000..4ea4cc910 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileStore.kt @@ -0,0 +1,7 @@ +package me.zhanghai.kotlin.filesystem + +import me.zhanghai.kotlin.filesystem.io.AsyncCloseable + +public interface FileStore : AsyncCloseable { + public suspend fun readMetadata(): FileStoreMetadata +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileStoreMetadata.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileStoreMetadata.kt new file mode 100644 index 000000000..d3981032f --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileStoreMetadata.kt @@ -0,0 +1,15 @@ +package me.zhanghai.kotlin.filesystem + +import kotlinx.io.bytestring.ByteString + +public interface FileStoreMetadata { + public val type: ByteString + + public val blockSize: Long + + public val totalSpace: Long + + public val freeSpace: Long + + public val availableSpace: Long +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystem.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystem.kt new file mode 100644 index 000000000..6c1a3cdad --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystem.kt @@ -0,0 +1,118 @@ +package me.zhanghai.kotlin.filesystem + +import kotlinx.io.IOException +import kotlinx.io.bytestring.ByteString +import me.zhanghai.kotlin.filesystem.io.AsyncSink +import me.zhanghai.kotlin.filesystem.io.AsyncSource +import me.zhanghai.kotlin.filesystem.io.use +import me.zhanghai.kotlin.filesystem.io.withCloseable +import kotlin.coroutines.cancellation.CancellationException + +public interface FileSystem { + public val rootUri: Uri + + public val rootDirectory: Path + get() = Path.of(rootUri, true, emptyList()) + + public val defaultDirectory: Path + + @Throws(CancellationException::class, IOException::class) + public suspend fun getRealPath(path: Path): Path + + @Throws(CancellationException::class, IOException::class) + public suspend fun checkAccess(path: Path, vararg modes: AccessMode) + + @Throws(CancellationException::class, IOException::class) + public suspend fun openMetadataView( + file: Path, + vararg options: FileMetadataOption + ): FileMetadataView + + @Throws(CancellationException::class, IOException::class) + public suspend fun readMetadata(file: Path, vararg options: FileMetadataOption): FileMetadata = + openMetadataView(file, *options).use { it.readMetadata() } + + @Throws(CancellationException::class, IOException::class) + public suspend fun openContent(file: Path, vararg options: FileContentOption): FileContent + + @Throws(CancellationException::class, IOException::class) + public suspend fun openSource(file: Path, vararg options: FileContentOption): AsyncSource { + require(BasicFileContentOption.WRITE !in options) { BasicFileContentOption.WRITE } + require(BasicFileContentOption.APPEND !in options) { BasicFileContentOption.APPEND } + return openContent(file, *options).let { it.openSource().withCloseable(it) } + } + + @Throws(CancellationException::class, IOException::class) + public suspend fun openSink( + file: Path, + vararg options: FileContentOption = OPEN_SINK_OPTIONS_DEFAULT + ): AsyncSink { + require(BasicFileContentOption.READ !in options) { BasicFileContentOption.READ } + require( + BasicFileContentOption.WRITE in options || BasicFileContentOption.APPEND in options + ) { + "Missing ${BasicFileContentOption.WRITE} or ${BasicFileContentOption.APPEND}" + } + return openContent(file, *options).let { it.openSink().withCloseable(it) } + } + + @Throws(CancellationException::class, IOException::class) + public suspend fun openDirectoryStream( + directory: Path, + vararg options: DirectoryStreamOption + ): DirectoryStream + + @Throws(CancellationException::class, IOException::class) + public suspend fun readDirectory( + directory: Path, + vararg options: DirectoryStreamOption + ): List = + openDirectoryStream(directory, *options).use { directoryStream -> + buildList { + while (true) { + val directoryEntry = directoryStream.read() ?: break + this += directory.resolve(directoryEntry.name) + } + } + } + + @Throws(CancellationException::class, IOException::class) + public suspend fun createDirectory(directory: Path, vararg options: CreateFileOption) + + @Throws(CancellationException::class, IOException::class) + public suspend fun readSymbolicLink(link: Path): ByteString + + @Throws(CancellationException::class, IOException::class) + public suspend fun createSymbolicLink( + link: Path, + target: ByteString, + vararg options: CreateFileOption + ) + + @Throws(CancellationException::class, IOException::class) + public suspend fun createHardLink(link: Path, existing: Path) + + @Throws(CancellationException::class, IOException::class) public suspend fun delete(path: Path) + + @Throws(CancellationException::class, IOException::class) + public suspend fun isSameFile(path1: Path, path2: Path): Boolean + + @Throws(CancellationException::class, IOException::class) + public suspend fun copy(source: Path, target: Path, vararg options: CopyFileOption) + + @Throws(CancellationException::class, IOException::class) + public suspend fun move(source: Path, target: Path, vararg options: CopyFileOption) + + @Throws(CancellationException::class, IOException::class) + public suspend fun openFileStore(path: Path): FileStore + + public companion object { + @PublishedApi + internal val OPEN_SINK_OPTIONS_DEFAULT: Array = + arrayOf( + BasicFileContentOption.WRITE, + BasicFileContentOption.TRUNCATE_EXISTING, + BasicFileContentOption.CREATE + ) + } +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemException.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemException.kt new file mode 100644 index 000000000..b73bccfa1 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemException.kt @@ -0,0 +1,10 @@ +package me.zhanghai.kotlin.filesystem + +import kotlinx.io.IOException + +public open class FileSystemException( + public val file: Path?, + public val otherFile: Path? = null, + message: String? = null, + cause: Throwable? = null +) : IOException(message, cause) diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemExceptions.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemExceptions.kt new file mode 100644 index 000000000..25bcc1326 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemExceptions.kt @@ -0,0 +1,57 @@ +package me.zhanghai.kotlin.filesystem + +public class AccessDeniedException( + file: Path?, + otherFile: Path? = null, + message: String? = null, + cause: Throwable? = null +) : FileSystemException(file, otherFile, message, cause) + +public class AtomicMoveNotSupportedException( + file: Path?, + otherFile: Path? = null, + message: String? = null, + cause: Throwable? = null +) : FileSystemException(file, otherFile, message, cause) + +public class DirectoryNotEmptyException( + file: Path?, + otherFile: Path? = null, + message: String? = null, + cause: Throwable? = null +) : FileSystemException(file, otherFile, message, cause) + +public class FileAlreadyExistsException( + file: Path?, + otherFile: Path? = null, + message: String? = null, + cause: Throwable? = null +) : FileSystemException(file, otherFile, message, cause) + +public class FileSystemLoopException( + file: Path?, + otherFile: Path? = null, + message: String? = null, + cause: Throwable? = null +) : FileSystemException(file, otherFile, message, cause) + +public class NoSuchFileException( + file: Path?, + otherFile: Path? = null, + message: String? = null, + cause: Throwable? = null +) : FileSystemException(file, otherFile, message, cause) + +public class NotDirectoryException( + file: Path?, + otherFile: Path? = null, + message: String? = null, + cause: Throwable? = null +) : FileSystemException(file, otherFile, message, cause) + +public class NotLinkException( + file: Path?, + otherFile: Path? = null, + message: String? = null, + cause: Throwable? = null +) : FileSystemException(file, otherFile, message, cause) diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemProvider.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemProvider.kt new file mode 100644 index 000000000..e42e62984 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemProvider.kt @@ -0,0 +1,7 @@ +package me.zhanghai.kotlin.filesystem + +public interface FileSystemProvider { + public val scheme: String + + public fun createFileSystem(rootUri: Uri): FileSystem +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemRegistry.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemRegistry.kt new file mode 100644 index 000000000..01c591b3a --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileSystemRegistry.kt @@ -0,0 +1,31 @@ +package me.zhanghai.kotlin.filesystem + +// TODO: Make thread safe +public object FileSystemRegistry { + private val providers: MutableMap = mutableMapOf() + + private val fileSystems: MutableMap = mutableMapOf() + + public fun getProviders(): Map = providers.toMap() + + public fun getProvider(scheme: String): FileSystemProvider? = providers[scheme] + + public fun removeProvider(scheme: String): FileSystemProvider? = providers.remove(scheme) + + public fun getFileSystems(): Map = fileSystems.toMap() + + public fun getFileSystem(rootUri: Uri): FileSystem? = fileSystems[rootUri] + + public fun getOrCreateFileSystem(rootUri: Uri): FileSystem { + fileSystems[rootUri]?.let { + return it + } + val provider = providers[rootUri.scheme] + requireNotNull(provider) { "No file system provider for scheme \"${rootUri.scheme}\"" } + return provider.createFileSystem(rootUri) + } + + public fun removeFileSystem(rootUri: Uri): FileSystem? = fileSystems.remove(rootUri) +} + +public expect val FileSystemRegistry.platformFileSystem: PlatformFileSystem diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/FileType.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileType.kt new file mode 100644 index 000000000..70f7f43ea --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/FileType.kt @@ -0,0 +1,8 @@ +package me.zhanghai.kotlin.filesystem + +public enum class FileType { + REGULAR_FILE, + DIRECTORY, + SYMBOLIC_LINK, + OTHER +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/LinkOption.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/LinkOption.kt new file mode 100644 index 000000000..257f74f2c --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/LinkOption.kt @@ -0,0 +1,5 @@ +package me.zhanghai.kotlin.filesystem + +public enum class LinkOption : FileContentOption, FileMetadataOption { + NO_FOLLOW_LINKS +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/Path.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/Path.kt new file mode 100644 index 000000000..36f679771 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/Path.kt @@ -0,0 +1,253 @@ +package me.zhanghai.kotlin.filesystem + +import kotlinx.io.bytestring.ByteString +import kotlinx.io.bytestring.append +import kotlinx.io.bytestring.buildByteString +import kotlinx.io.bytestring.encodeToByteString +import kotlinx.io.bytestring.isNotEmpty +import me.zhanghai.kotlin.filesystem.internal.compareTo +import me.zhanghai.kotlin.filesystem.internal.contains +import me.zhanghai.kotlin.filesystem.internal.endsWith +import me.zhanghai.kotlin.filesystem.internal.startsWith +import kotlin.math.min + +public class Path +private constructor( + public val rootUri: Uri, + public val isAbsolute: Boolean, + public val names: List, + @Suppress("UNUSED_PARAMETER") any: Any? +) : Comparable { + public val scheme: String + get() = rootUri.scheme!! + + public val fileName: ByteString? + get() = names.lastOrNull() + + public fun getParent(): Path? { + val lastIndex = names.lastIndex + return if (lastIndex >= 0) { + Path(rootUri, isAbsolute, names.subList(0, lastIndex), null) + } else { + null + } + } + + public fun subPath(startIndex: Int, endIndex: Int): Path = + if (startIndex == 0 && endIndex == names.size) { + this + } else { + Path(rootUri, isAbsolute, names.subList(startIndex, endIndex), null) + } + + public fun startsWith(other: Path): Boolean { + if (this === other) { + return true + } + if (rootUri != other.rootUri) { + return false + } + return isAbsolute == other.isAbsolute && names.startsWith(other.names) + } + + public fun endsWith(other: Path): Boolean { + if (this === other) { + return true + } + if (rootUri != other.rootUri) { + return false + } + return if (other.isAbsolute) { + isAbsolute && names == other.names + } else { + names.endsWith(other.names) + } + } + + public fun normalize(): Path { + var newNames: MutableList? = null + for ((index, name) in names.withIndex()) { + when (name) { + NAME_DOT -> + if (newNames == null) { + newNames = names.subList(0, index).toMutableList() + } + NAME_DOT_DOT -> + if (newNames != null) { + when (newNames.lastOrNull()) { + null -> + if (!isAbsolute) { + newNames += name + } + NAME_DOT_DOT -> newNames += name + else -> newNames.removeLast() + } + } else { + when (names.getOrNull(index - 1)) { + null -> + if (isAbsolute) { + newNames = mutableListOf() + } + NAME_DOT_DOT -> {} + else -> newNames = names.subList(0, index - 1).toMutableList() + } + } + else -> + if (newNames != null) { + newNames += name + } + } + } + if (newNames == null) { + return this + } + return Path(rootUri, isAbsolute, newNames, null) + } + + public fun resolve(fileName: ByteString): Path = + Path(rootUri, isAbsolute, names + fileName, null) + + public fun resolve(other: Path): Path { + require(rootUri == other.rootUri) { "Cannot resolve a path with a different root URI" } + return if (other.isAbsolute) { + other + } else { + Path(rootUri, isAbsolute, names + other.names, null) + } + } + + public fun resolveSibling(fileName: ByteString): Path { + check(names.isNotEmpty()) { "Cannot resolve sibling of an empty path" } + return Path( + rootUri, + isAbsolute, + names.toMutableList().apply { set(lastIndex, fileName) }, + null + ) + } + + public fun relativize(other: Path): Path { + if (this === other) { + return Path(rootUri, false, emptyList(), null) + } + require(rootUri == other.rootUri) { "Cannot relativize a path with a different root URI" } + require(isAbsolute == other.isAbsolute) { + "Cannot relativize a path with a different absoluteness" + } + if (names.isEmpty()) { + return if (other.isAbsolute) { + Path(rootUri, false, other.names, null) + } else { + other + } + } + val namesSize = names.size + val otherNamesSize = other.names.size + val minNamesSize = min(namesSize, otherNamesSize) + var commonNamesSize = 0 + while (commonNamesSize < minNamesSize) { + if (names[commonNamesSize] != other.names[commonNamesSize]) { + break + } + ++commonNamesSize + } + val newNames = names.subList(0, commonNamesSize).toMutableList() + repeat(namesSize - commonNamesSize) { newNames += NAME_DOT_DOT } + newNames += other.names.subList(commonNamesSize, otherNamesSize) + return Path(rootUri, false, newNames, null) + } + + public fun toUri(): Uri { + check(isAbsolute) { "Cannot convert a relative path to URI" } + val decodedPath = buildByteString { + for (name in names) { + append(NAME_SEPARATOR_BYTE) + append(name) + } + } + return rootUri.copyDecoded(decodedPath = decodedPath) + } + + override fun compareTo(other: Path): Int { + rootUri.compareTo(other.rootUri).let { + if (it != 0) { + return it + } + } + isAbsolute.compareTo(other.isAbsolute).let { + if (it != 0) { + return it + } + } + return names.compareTo(other.names) + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null || this::class != other::class) { + return false + } + other as Path + return rootUri == other.rootUri && isAbsolute == other.isAbsolute && names == other.names + } + + override fun hashCode(): Int { + var result = rootUri.hashCode() + result = 31 * result + isAbsolute.hashCode() + result = 31 * result + names.hashCode() + return result + } + + override fun toString(): String = "Path(rootUri=$rootUri, isAbsolute=$isAbsolute, names=$names)" + + public companion object { + private val NAME_DOT = ".".encodeToByteString() + private val NAME_DOT_DOT = "..".encodeToByteString() + private const val NAME_SEPARATOR_CHAR = '/' + private const val NAME_SEPARATOR_BYTE = NAME_SEPARATOR_CHAR.code.toByte() + private const val NAME_SEPARATOR_STRING = "/" + + public fun of(rootUri: Uri, isAbsolute: Boolean, names: List): Path { + requireNotNull(rootUri.scheme) { "Missing scheme in path root URI \"$rootUri\"" } + require(rootUri.encodedPath == NAME_SEPARATOR_STRING) { + "Path is not root in path root URI \"$rootUri\"" + } + for (name in names) { + require(name.isNotEmpty()) { "Empty name in path name \"$name\$" } + require(NAME_SEPARATOR_BYTE !in name) { "Name separator in path name \"$name\"" } + } + return Path(rootUri, isAbsolute, names, null) + } + + public fun fromUri(uri: Uri): Path { + requireNotNull(uri.scheme) { "Missing scheme in path URI \"$uri\"" } + val encodedPath = uri.encodedPath + require(encodedPath.isNotEmpty()) { "Empty path in path URI \"$uri\"" } + require(encodedPath[0] == NAME_SEPARATOR_CHAR) { "Relative path in path URI \"$uri\"" } + val rootUri = uri.copyEncoded(encodedPath = NAME_SEPARATOR_STRING) + val names = buildList { + val decodedPath = uri.decodedPath + val decodedPathSize = decodedPath.size + var nameStart = 0 + var nameEnd = nameStart + while (nameEnd < decodedPathSize) { + if (decodedPath[nameEnd] == NAME_SEPARATOR_BYTE) { + if (nameEnd != nameStart) { + this += decodedPath.substring(nameStart, nameEnd) + } + nameStart = nameEnd + 1 + nameEnd = nameStart + } else { + ++nameEnd + } + } + if (nameEnd != nameStart) { + this += decodedPath.substring(nameStart, nameEnd) + } + } + return Path(rootUri, true, names, null) + } + } +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/Paths.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/Paths.kt new file mode 100644 index 000000000..998acfd15 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/Paths.kt @@ -0,0 +1,118 @@ +package me.zhanghai.kotlin.filesystem + +import kotlinx.io.IOException +import kotlinx.io.bytestring.ByteString +import kotlinx.io.bytestring.encodeToByteString +import me.zhanghai.kotlin.filesystem.io.AsyncSink +import me.zhanghai.kotlin.filesystem.io.AsyncSource +import kotlin.coroutines.cancellation.CancellationException + +public inline fun Path.Companion.fromPlatformPath(platformPath: ByteString): Path = + FileSystemRegistry.platformFileSystem.getPath(platformPath) + +public inline fun Path.Companion.fromPlatformPath(platformPath: String): Path = + fromPlatformPath(platformPath.encodeToByteString()) + +public inline fun Path.toPlatformPath(): ByteString = + FileSystemRegistry.platformFileSystem.toPlatformPath(this) + +public inline fun Path.toPlatformPathString(): String = toPlatformPath().toString() + +public inline fun Path.getOrCreateFileSystem(): FileSystem = + FileSystemRegistry.getOrCreateFileSystem(this.rootUri) + +public inline fun Path.toAbsolutePath(): Path = + getOrCreateFileSystem().defaultDirectory.resolve(this) + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.toRealPath(): Path = getOrCreateFileSystem().getRealPath(this) + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.checkAccess(vararg modes: AccessMode) { + getOrCreateFileSystem().checkAccess(this, *modes) +} + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.openMetadataView( + vararg options: FileMetadataOption +): FileMetadataView = getOrCreateFileSystem().openMetadataView(this, *options) + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.readMetadata(vararg options: FileMetadataOption): FileMetadata = + getOrCreateFileSystem().readMetadata(this, *options) + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.openContent(vararg options: FileContentOption): FileContent = + getOrCreateFileSystem().openContent(this, *options) + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.openSource(vararg options: FileContentOption): AsyncSource = + getOrCreateFileSystem().openSource(this, *options) + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.openSink( + vararg options: FileContentOption = FileSystem.OPEN_SINK_OPTIONS_DEFAULT +): AsyncSink = getOrCreateFileSystem().openSink(this, *options) + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.openDirectoryStream( + vararg options: DirectoryStreamOption +): DirectoryStream = getOrCreateFileSystem().openDirectoryStream(this, *options) + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.readDirectory(vararg options: DirectoryStreamOption): List = + getOrCreateFileSystem().readDirectory(this, *options) + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.createDirectory(vararg options: CreateFileOption) { + getOrCreateFileSystem().createDirectory(this, *options) +} + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.readSymbolicLink(): ByteString = + getOrCreateFileSystem().readSymbolicLink(this) + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.createSymbolicLinkTo( + target: ByteString, + vararg options: CreateFileOption +) { + getOrCreateFileSystem().createSymbolicLink(this, target, *options) +} + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.createHardLinkTo(existing: Path) { + getOrCreateFileSystem().createHardLink(this, existing) +} + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.delete() { + getOrCreateFileSystem().delete(this) +} + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.deleteIfExists(): Boolean = + try { + delete() + true + } catch (e: NoSuchFileException) { + false + } + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.isSameFileAs(other: Path): Boolean = + getOrCreateFileSystem().isSameFile(this, other) + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.copyTo(target: Path, vararg options: CopyFileOption) { + getOrCreateFileSystem().copy(this, target, *options) +} + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.moveTo(target: Path, vararg options: CopyFileOption) { + getOrCreateFileSystem().move(this, target, *options) +} + +@Throws(CancellationException::class, IOException::class) +public suspend inline fun Path.openFileStore(): FileStore = + getOrCreateFileSystem().openFileStore(this) diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/PlatformFileSystem.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/PlatformFileSystem.kt new file mode 100644 index 000000000..e1513f8b6 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/PlatformFileSystem.kt @@ -0,0 +1,13 @@ +package me.zhanghai.kotlin.filesystem + +import kotlinx.io.bytestring.ByteString + +public interface PlatformFileSystem : FileSystem { + public fun getPath(platformPath: ByteString): Path + + public fun toPlatformPath(path: Path): ByteString + + public companion object { + public const val SCHEME: String = "file" + } +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/Uri.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/Uri.kt new file mode 100644 index 000000000..c1d16de6b --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/Uri.kt @@ -0,0 +1,283 @@ +package me.zhanghai.kotlin.filesystem + +import kotlinx.io.bytestring.ByteString +import kotlinx.io.bytestring.indices +import me.zhanghai.kotlin.filesystem.internal.UriParser +import me.zhanghai.kotlin.filesystem.internal.decodePart +import me.zhanghai.kotlin.filesystem.internal.encodeFragment +import me.zhanghai.kotlin.filesystem.internal.encodeHost +import me.zhanghai.kotlin.filesystem.internal.encodePath +import me.zhanghai.kotlin.filesystem.internal.encodeQuery +import me.zhanghai.kotlin.filesystem.internal.encodeUserInfo +import me.zhanghai.kotlin.filesystem.internal.parse +import me.zhanghai.kotlin.filesystem.internal.requireValidEncodedFragment +import me.zhanghai.kotlin.filesystem.internal.requireValidEncodedHost +import me.zhanghai.kotlin.filesystem.internal.requireValidEncodedPath +import me.zhanghai.kotlin.filesystem.internal.requireValidEncodedQuery +import me.zhanghai.kotlin.filesystem.internal.requireValidEncodedUserInfo +import me.zhanghai.kotlin.filesystem.internal.requireValidPort +import me.zhanghai.kotlin.filesystem.internal.requireValidScheme +import kotlin.experimental.and + +// https://datatracker.ietf.org/doc/html/rfc3986 +public class Uri +internal constructor( + public val scheme: String?, + public val encodedUserInfo: String?, + public val encodedHost: String?, + public val port: Int?, + public val encodedPath: String, + public val encodedQuery: String?, + public val encodedFragment: String?, + @Suppress("UNUSED_PARAMETER") any: Any? +) : Comparable { + public val encodedAuthority: String? by + lazy(LazyThreadSafetyMode.NONE) { + if (encodedUserInfo != null || encodedHost != null || port != null) { + buildString { + if (encodedUserInfo != null) { + append(encodedUserInfo) + append('@') + } + if (encodedHost != null) { + append(encodedHost) + } + if (port != null) { + append(':') + append(port) + } + } + } else { + null + } + } + + public val decodedAuthority: ByteString? by + lazy(LazyThreadSafetyMode.NONE) { encodedAuthority?.let { UriParser.decodePart(it) } } + + public val decodedUserInfo: ByteString? by + lazy(LazyThreadSafetyMode.NONE) { encodedUserInfo?.let { UriParser.decodePart(it) } } + + public val decodedHost: ByteString? by + lazy(LazyThreadSafetyMode.NONE) { encodedHost?.let { UriParser.decodePart(it) } } + + public val decodedPath: ByteString by + lazy(LazyThreadSafetyMode.NONE) { encodedPath.let { UriParser.decodePart(it) } } + + public val decodedQuery: ByteString? by + lazy(LazyThreadSafetyMode.NONE) { encodedQuery?.let { UriParser.decodePart(it) } } + + public val decodedFragment: ByteString? by + lazy(LazyThreadSafetyMode.NONE) { encodedFragment?.let { UriParser.decodePart(it) } } + + public fun copyEncoded( + scheme: String? = this.scheme, + encodedUserInfo: String? = this.encodedUserInfo, + encodedHost: String? = this.encodedHost, + port: Int? = this.port, + encodedPath: String = this.encodedPath, + encodedQuery: String? = this.encodedQuery, + encodedFragment: String? = this.encodedFragment + ): Uri { + if (scheme !== this.scheme) { + UriParser.requireValidScheme(scheme) + } + if (encodedUserInfo !== this.encodedUserInfo) { + UriParser.requireValidEncodedUserInfo(encodedUserInfo) + } + if (encodedHost !== this.encodedHost) { + UriParser.requireValidEncodedHost(encodedHost) + } + UriParser.requireValidPort(port) + if (encodedPath != this.encodedPath) { + UriParser.requireValidEncodedPath(encodedPath, scheme != null) + } + if (encodedQuery != this.encodedQuery) { + UriParser.requireValidEncodedQuery(encodedQuery) + } + if (encodedFragment != this.encodedFragment) { + UriParser.requireValidEncodedFragment(encodedFragment) + } + return Uri( + scheme, + encodedUserInfo, + encodedHost, + port, + encodedPath, + encodedQuery, + encodedFragment, + null + ) + } + + public fun copyDecoded( + scheme: String? = this.scheme, + decodedUserInfo: ByteString? = BYTE_STRING_COPY, + decodedHost: ByteString? = BYTE_STRING_COPY, + port: Int? = this.port, + decodedPath: ByteString = BYTE_STRING_COPY, + decodedQuery: ByteString? = BYTE_STRING_COPY, + decodedFragment: ByteString? = BYTE_STRING_COPY + ): Uri { + if (scheme !== this.scheme) { + UriParser.requireValidScheme(scheme) + } + val encodedUserInfo = + if (decodedUserInfo === BYTE_STRING_COPY) { + encodedUserInfo + } else { + decodedUserInfo?.let { UriParser.encodeUserInfo(it) } + } + val encodedHost = + if (decodedHost === BYTE_STRING_COPY) { + encodedHost + } else { + decodedHost?.let { UriParser.encodeHost(it) } + } + UriParser.requireValidPort(port) + val encodedPath = + if (decodedPath === BYTE_STRING_COPY) { + encodedPath + } else { + decodedPath + .let { UriParser.encodePath(it) } + .also { UriParser.requireValidEncodedPath(it, scheme != null) } + } + val encodedQuery = + if (decodedQuery === BYTE_STRING_COPY) { + encodedQuery + } else { + decodedQuery?.let { UriParser.encodeQuery(it) } + } + val encodedFragment = + if (decodedFragment === BYTE_STRING_COPY) { + encodedFragment + } else { + decodedFragment?.let { UriParser.encodeFragment(it) } + } + return Uri( + scheme, + encodedUserInfo, + encodedHost, + port, + encodedPath, + encodedQuery, + encodedFragment, + null + ) + } + + private val string: String by + lazy(LazyThreadSafetyMode.NONE) { + buildString { + if (scheme != null) { + append(scheme) + append(':') + } + if (encodedHost != null) { + append("//") + if (encodedUserInfo != null) { + append(encodedUserInfo) + append('@') + } + append(encodedHost) + if (port != null) { + append(':') + append(port) + } + } + append(encodedPath) + if (encodedQuery != null) { + append('?') + append(encodedQuery) + } + if (encodedFragment != null) { + append('#') + append(encodedFragment) + } + } + } + + override fun compareTo(other: Uri): Int = string.compareTo(other.string) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null || this::class != other::class) { + return false + } + other as Uri + return string == other.string + } + + override fun hashCode(): Int = string.hashCode() + + override fun toString(): String = string + + public companion object { + public fun ofEncoded( + scheme: String? = null, + encodedUserInfo: String? = null, + encodedHost: String? = null, + port: Int? = null, + encodedPath: String = "", + encodedQuery: String? = null, + encodedFragment: String? = null + ): Uri { + UriParser.requireValidScheme(scheme) + UriParser.requireValidEncodedUserInfo(encodedUserInfo) + UriParser.requireValidEncodedHost(encodedHost) + UriParser.requireValidPort(port) + UriParser.requireValidEncodedPath(encodedPath, scheme != null) + UriParser.requireValidEncodedQuery(encodedQuery) + UriParser.requireValidEncodedFragment(encodedFragment) + return Uri( + scheme, + encodedUserInfo, + encodedHost, + port, + encodedPath, + encodedQuery, + encodedFragment, + null + ) + } + + public fun ofDecoded( + scheme: String? = null, + decodedUserInfo: ByteString? = null, + decodedHost: ByteString? = null, + port: Int? = null, + decodedPath: ByteString = BYTE_STRING_EMPTY, + decodedQuery: ByteString? = null, + decodedFragment: ByteString? = null + ): Uri { + UriParser.requireValidScheme(scheme) + val encodedUserInfo = decodedUserInfo?.let { UriParser.encodeUserInfo(it) } + val encodedHost = decodedHost?.let { UriParser.encodeHost(it) } + UriParser.requireValidPort(port) + val encodedPath = + decodedPath + .let { UriParser.encodePath(it) } + .also { UriParser.requireValidEncodedPath(it, scheme != null) } + val encodedQuery = decodedQuery?.let { UriParser.encodeQuery(it) } + val encodedFragment = decodedFragment?.let { UriParser.encodeFragment(it) } + return Uri( + scheme, + encodedUserInfo, + encodedHost, + port, + encodedPath, + encodedQuery, + encodedFragment, + null + ) + } + + public fun parse(uri: String): Uri = UriParser.parse(uri) + } +} + +private val BYTE_STRING_COPY = ByteString(0.toByte()) +private val BYTE_STRING_EMPTY = ByteString() diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/AsciiCharSet.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/AsciiCharSet.kt new file mode 100644 index 000000000..86d9d06ae --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/AsciiCharSet.kt @@ -0,0 +1,63 @@ +package me.zhanghai.kotlin.filesystem.internal + +internal class AsciiCharSet private constructor(private val mask0: Long, private val mask64: Long) { + fun matches(char: Char): Boolean = + when { + char < 64.toChar() -> (1L shl char.code) and mask0 != 0L + char < 128.toChar() -> (1L shl (char.code - 64)) and mask64 != 0L + else -> false + } + + infix fun and(other: AsciiCharSet): AsciiCharSet = + AsciiCharSet(mask0 and other.mask0, mask64 and other.mask64) + + fun inv(): AsciiCharSet = AsciiCharSet(mask0.inv(), mask64.inv()) + + infix fun or(other: AsciiCharSet): AsciiCharSet = + AsciiCharSet(mask0 or other.mask0, mask64 or other.mask64) + + companion object { + fun of(char: Char): AsciiCharSet { + var mask64 = 0L + var mask128 = 0L + when { + char < 64.toChar() -> mask64 = mask64 or (1L shl char.code) + char < 128.toChar() -> mask128 = mask128 or (1L shl (char.code - 64)) + else -> throw IllegalArgumentException("Non-ASCII char '$char'") + } + return AsciiCharSet(mask64, mask128) + } + + fun of(chars: String): AsciiCharSet { + var mask64 = 0L + var mask128 = 0L + for (char in chars) { + when { + char < 64.toChar() -> mask64 = mask64 or (1L shl char.code) + char < 128.toChar() -> mask128 = mask128 or (1L shl (char.code - 64)) + else -> throw IllegalArgumentException("Non-ASCII char '$char'") + } + } + return AsciiCharSet(mask64, mask128) + } + + fun ofRange(startChar: Char, endCharInclusive: Char): AsciiCharSet { + require(endCharInclusive < 128.toChar()) { + "Non-ASCII endCharInclusive ('$endCharInclusive')" + } + require(startChar <= endCharInclusive) { + "startChar ('$startChar') > endCharInclusive ('$endCharInclusive')" + } + var mask64 = 0L + var mask128 = 0L + for (char in startChar..endCharInclusive) { + if (char < 64.toChar()) { + mask64 = mask64 or (1L shl char.code) + } else { + mask128 = mask128 or (1L shl (char.code - 64)) + } + } + return AsciiCharSet(mask64, mask128) + } + } +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/ByteStrings.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/ByteStrings.kt new file mode 100644 index 000000000..ba8a5d030 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/ByteStrings.kt @@ -0,0 +1,13 @@ +package me.zhanghai.kotlin.filesystem.internal + +import kotlinx.io.bytestring.ByteString +import kotlinx.io.bytestring.indexOf + +internal inline operator fun ByteString.contains(byte: Byte): Boolean = indexOf(byte) != -1 + +internal inline fun ByteString.first(): Byte = this[0] + +internal inline fun ByteString.last(): Byte = this[lastIndex] + +internal val ByteString.lastIndex: Int + inline get() = size - 1 diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/Lists.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/Lists.kt new file mode 100644 index 000000000..a1a96ad88 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/Lists.kt @@ -0,0 +1,37 @@ +package me.zhanghai.kotlin.filesystem.internal + +import kotlin.math.min + +internal fun > List.compareTo(other: List): Int { + val size = size + val otherSize = other.size + val commonSize = min(size, otherSize) + for (i in 0 ..< commonSize) { + this[i].compareTo(other[i]).let { + if (it != 0) { + return it + } + } + } + return size.compareTo(otherSize) +} + +internal fun List.startsWith(other: List): Boolean { + val size = size + val otherSize = other.size + return when { + size == otherSize -> this == other + size > otherSize -> subList(0, otherSize) == other + else -> false + } +} + +internal fun List.endsWith(other: List): Boolean { + val size = size + val otherSize = other.size + return when { + size == otherSize -> this == other + size > otherSize -> subList(size - otherSize, size) == other + else -> false + } +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/UriParser.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/UriParser.kt new file mode 100644 index 000000000..df2c3d2bd --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/UriParser.kt @@ -0,0 +1,4364 @@ +package me.zhanghai.kotlin.filesystem.`internal` + +/** + * ``` + * URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] + * + * hier-part = "//" authority path-abempty + * / path-absolute + * / path-rootless + * / path-empty + * + * URI-reference = URI / relative-ref + * + * absolute-URI = scheme ":" hier-part [ "?" query ] + * + * relative-ref = relative-part [ "?" query ] [ "#" fragment ] + * + * relative-part = "//" authority path-abempty + * / path-absolute + * / path-noscheme + * / path-empty + * + * scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + * + * authority = [ userinfo "@" ] host [ ":" port ] + * userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) + * host = IP-literal / IPv4address / reg-name + * port = *DIGIT + * + * IP-literal = "[" ( IPv6address / IPvFuture ) "]" + * + * IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) + * + * IPv6address = 6( h16 ":" ) ls32 + * / "::" 5( h16 ":" ) ls32 + * / [ h16 ] "::" 4( h16 ":" ) ls32 + * / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + * / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + * / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + * / [ *4( h16 ":" ) h16 ] "::" ls32 + * / [ *5( h16 ":" ) h16 ] "::" h16 + * / [ *6( h16 ":" ) h16 ] "::" + * + * h16 = 1*4HEXDIG + * ls32 = ( h16 ":" h16 ) / IPv4address + * IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet + * + * dec-octet = DIGIT ; 0-9 + * / %x31-39 DIGIT ; 10-99 + * / "1" 2DIGIT ; 100-199 + * / "2" %x30-34 DIGIT ; 200-249 + * / "25" %x30-35 ; 250-255 + * + * reg-name = *( unreserved / pct-encoded / sub-delims ) + * + * path = path-abempty ; begins with "/" or is empty + * / path-absolute ; begins with "/" but not "//" + * / path-noscheme ; begins with a non-colon segment + * / path-rootless ; begins with a segment + * / path-empty ; zero characters + * + * path-abempty = *( "/" segment ) + * path-absolute = "/" [ segment-nz *( "/" segment ) ] + * path-noscheme = segment-nz-nc *( "/" segment ) + * path-rootless = segment-nz *( "/" segment ) + * path-empty = 0 + * + * segment = *pchar + * segment-nz = 1*pchar + * segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) + * ; non-zero-length segment without any colon ":" + * + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * + * query = *( pchar / "/" / "?" ) + * + * fragment = *( pchar / "/" / "?" ) + * + * pct-encoded = "%" HEXDIG HEXDIG + * + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * reserved = gen-delims / sub-delims + * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + * ``` + */ +@Suppress( + "LABEL_NAME_CLASH", + "MemberVisibilityCanBePrivate", + "NAME_SHADOWING", + "RedundantVisibilityModifier", + "UnnecessaryVariable", +) +internal object UriParser { + public fun parseUri( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterUri(input, startIndex) + val index = startIndex + return run { + var index = index + parseScheme(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseHierPart(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '?') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseQuery(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '#') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseFragment(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitUri(input, startIndex, it) } + } + + public fun parseHierPart( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterHierPart(input, startIndex) + val index = startIndex + return run { + run { + var index = index + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '/') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '/') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseAuthority(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + parsePathAbempty(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + parsePathAbsolute(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parsePathRootless(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parsePathEmpty(input, index, listener).let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitHierPart(input, startIndex, it) } + } + + public fun parseUriReference( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterUriReference(input, startIndex) + val index = startIndex + return run { + parseUri(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parseRelativeRef(input, index, listener).let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitUriReference(input, startIndex, it) } + } + + public fun parseAbsoluteUri( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterAbsoluteUri(input, startIndex) + val index = startIndex + return run { + var index = index + parseScheme(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseHierPart(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '?') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseQuery(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitAbsoluteUri(input, startIndex, it) } + } + + public fun parseRelativeRef( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterRelativeRef(input, startIndex) + val index = startIndex + return run { + var index = index + parseRelativePart(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '?') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseQuery(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '#') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseFragment(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitRelativeRef(input, startIndex, it) } + } + + public fun parseRelativePart( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterRelativePart(input, startIndex) + val index = startIndex + return run { + run { + var index = index + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '/') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '/') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseAuthority(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + parsePathAbempty(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + parsePathAbsolute(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parsePathNoscheme(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parsePathEmpty(input, index, listener).let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitRelativePart(input, startIndex, it) } + } + + public fun parseScheme( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterScheme(input, startIndex) + val index = startIndex + return run { + var index = index + parseAlpha(input, index).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + run { + var index = index + var count = 0 + while (true) { + parseAlpha(input, index).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it != -1) { + return@run it + } + } + parseDigit(input, index).let { + if (it != -1) { + return@run it + } + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + when (char) { + '.', + '-', + '+' -> index + 1 + else -> -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it != -1) { + return@run it + } + } + -1 + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitScheme(input, startIndex, it) } + } + + public fun parseAuthority( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterAuthority(input, startIndex) + val index = startIndex + return run { + var index = index + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + parseUserinfo(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> -1 + char < 128.toChar() -> + if (char == '@') { + index + 1 + } else { + -1 + } + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseHost(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parsePort(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitAuthority(input, startIndex, it) } + } + + public fun parseUserinfo( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterUserinfo(input, startIndex) + val index = startIndex + return run { + run { + var index = index + var count = 0 + while (true) { + parseUnreserved(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it != -1) { + return@run it + } + } + parsePctEncoded(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parseSubDelims(input, index, listener).let { + if (it != -1) { + return@run it + } + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitUserinfo(input, startIndex, it) } + } + + public fun parseHost( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterHost(input, startIndex) + val index = startIndex + return run { + parseIpLiteral(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parseIpv4Address(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parseRegName(input, index, listener).let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitHost(input, startIndex, it) } + } + + public fun parsePort( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterPort(input, startIndex) + val index = startIndex + return run { + var index = index + var count = 0 + while (true) { + parseDigit(input, index).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .also { listener?.exitPort(input, startIndex, it) } + } + + public fun parseIpLiteral( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterIpLiteral(input, startIndex) + val index = startIndex + return run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> -1 + char < 128.toChar() -> + if (char == '[') { + index + 1 + } else { + -1 + } + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + parseIpv6Address(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parseIpvfuture(input, index, listener).let { + if (it != -1) { + return@run it + } + } + -1 + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> -1 + char < 128.toChar() -> + if (char == ']') { + index + 1 + } else { + -1 + } + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitIpLiteral(input, startIndex, it) } + } + + public fun parseIpvfuture( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterIpvfuture(input, startIndex) + val index = startIndex + return run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> -1 + char < 128.toChar() -> + when (char) { + 'v', + 'V' -> index + 1 + else -> -1 + } + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + var count = 0 + while (true) { + parseHexdig(input, index).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + if (count < 1) { + return@run -1 + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '.') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + run { + var index = index + var count = 0 + while (true) { + parseUnreserved(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + if (count < 1) { + return@run -1 + } + index + } + .let { + if (it != -1) { + return@run it + } + } + parseSubDelims(input, index, listener).let { + if (it != -1) { + return@run it + } + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it != -1) { + return@run it + } + } + -1 + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitIpvfuture(input, startIndex, it) } + } + + public fun parseIpv6Address( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterIpv6Address(input, startIndex) + val index = startIndex + return run { + run { + var index = index + run { + var index = index + run { + var index = index + var count = 0 + while (count < 6) { + parseH16(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + if (count < 6) { + return@run -1 + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseLs32(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + var index = index + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + var index = index + var count = 0 + while (count < 5) { + parseH16(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + if (count < 5) { + return@run -1 + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseLs32(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + var index = index + run { + var index = index + var count = 0 + while (count < 1) { + parseH16(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + var index = index + var count = 0 + while (count < 4) { + parseH16(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + if (count < 4) { + return@run -1 + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseLs32(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + var index = index + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + run { + var index = index + run { + var index = index + var count = 0 + while (count < 1) { + parseH16(input, index, listener) + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseH16(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + var index = index + var count = 0 + while (count < 3) { + parseH16(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + if (count < 3) { + return@run -1 + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseLs32(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + var index = index + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + run { + var index = index + run { + var index = index + var count = 0 + while (count < 2) { + parseH16(input, index, listener) + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseH16(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + var index = index + var count = 0 + while (count < 2) { + parseH16(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + if (count < 2) { + return@run -1 + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseLs32(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + var index = index + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + run { + var index = index + run { + var index = index + var count = 0 + while (count < 3) { + parseH16(input, index, listener) + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseH16(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseH16(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseLs32(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + var index = index + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + run { + var index = index + run { + var index = index + var count = 0 + while (count < 4) { + parseH16(input, index, listener) + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseH16(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseLs32(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + var index = index + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + run { + var index = index + run { + var index = index + var count = 0 + while (count < 5) { + parseH16(input, index, listener) + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseH16(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseH16(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + var index = index + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + run { + var index = index + run { + var index = index + var count = 0 + while (count < 6) { + parseH16(input, index, listener) + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseH16(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitIpv6Address(input, startIndex, it) } + } + + public fun parseH16( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterH16(input, startIndex) + val index = startIndex + return run { + var index = index + var count = 0 + while (count < 4) { + parseHexdig(input, index).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + if (count < 1) { + return@run -1 + } + index + } + .also { listener?.exitH16(input, startIndex, it) } + } + + public fun parseLs32( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterLs32(input, startIndex) + val index = startIndex + return run { + run { + var index = index + parseH16(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseH16(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + parseIpv4Address(input, index, listener).let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitLs32(input, startIndex, it) } + } + + public fun parseIpv4Address( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterIpv4Address(input, startIndex) + val index = startIndex + return run { + var index = index + parseDecOctet(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '.') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseDecOctet(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '.') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseDecOctet(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '.') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseDecOctet(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitIpv4Address(input, startIndex, it) } + } + + public fun parseDecOctet( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterDecOctet(input, startIndex) + val index = startIndex + return run { + parseDigit(input, index).let { + if (it != -1) { + return@run it + } + } + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (((1UL shl char.code) and 0x3FE000000000000UL) != 0UL) { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseDigit(input, index).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '1') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + var count = 0 + while (count < 2) { + parseDigit(input, index).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + if (count < 2) { + return@run -1 + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '2') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (((1UL shl char.code) and 0x1F000000000000UL) != 0UL) { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseDigit(input, index).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + var index = index + run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '2') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '5') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (((1UL shl char.code) and 0x3F000000000000UL) != 0UL) { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitDecOctet(input, startIndex, it) } + } + + public fun parseRegName( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterRegName(input, startIndex) + val index = startIndex + return run { + run { + var index = index + var count = 0 + while (true) { + parseUnreserved(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it != -1) { + return@run it + } + } + parsePctEncoded(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parseSubDelims(input, index, listener).let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitRegName(input, startIndex, it) } + } + + public fun parsePath( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterPath(input, startIndex) + val index = startIndex + return run { + parsePathAbempty(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parsePathAbsolute(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parsePathNoscheme(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parsePathRootless(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parsePathEmpty(input, index, listener).let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitPath(input, startIndex, it) } + } + + public fun parsePathAbempty( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterPathAbempty(input, startIndex) + val index = startIndex + return run { + var index = index + run { + var index = index + var count = 0 + while (true) { + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '/') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseSegment(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitPathAbempty(input, startIndex, it) } + } + + public fun parsePathAbsolute( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterPathAbsolute(input, startIndex) + val index = startIndex + return run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '/') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + var count = 0 + while (count < 1) { + run { + var index = index + parseSegmentNz(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + var index = index + var count = 0 + while (true) { + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '/') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseSegment(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitPathAbsolute(input, startIndex, it) } + } + + public fun parsePathNoscheme( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterPathNoscheme(input, startIndex) + val index = startIndex + return run { + var index = index + parseSegmentNzNc(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + var index = index + var count = 0 + while (true) { + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '/') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseSegment(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitPathNoscheme(input, startIndex, it) } + } + + public fun parsePathRootless( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterPathRootless(input, startIndex) + val index = startIndex + return run { + var index = index + parseSegmentNz(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + run { + var index = index + run { + var index = index + var count = 0 + while (true) { + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '/') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseSegment(input, index, listener).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitPathRootless(input, startIndex, it) } + } + + public fun parsePathEmpty( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterPathEmpty(input, startIndex) + val index = startIndex + return run { + var index = index + var count = 0 + while (count < 0) { + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> -1 + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .also { listener?.exitPathEmpty(input, startIndex, it) } + } + + public fun parseSegment( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterSegment(input, startIndex) + val index = startIndex + return run { + var index = index + var count = 0 + while (true) { + parsePchar(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .also { listener?.exitSegment(input, startIndex, it) } + } + + public fun parseSegmentNz( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterSegmentNz(input, startIndex) + val index = startIndex + return run { + var index = index + var count = 0 + while (true) { + parsePchar(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + if (count < 1) { + return@run -1 + } + index + } + .also { listener?.exitSegmentNz(input, startIndex, it) } + } + + public fun parseSegmentNzNc( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterSegmentNzNc(input, startIndex) + val index = startIndex + return run { + run { + var index = index + var count = 0 + while (true) { + parseUnreserved(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + if (count < 1) { + return@run -1 + } + index + } + .let { + if (it != -1) { + return@run it + } + } + parsePctEncoded(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parseSubDelims(input, index, listener).let { + if (it != -1) { + return@run it + } + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> -1 + char < 128.toChar() -> + if (char == '@') { + index + 1 + } else { + -1 + } + else -> -1 + } + } + .let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitSegmentNzNc(input, startIndex, it) } + } + + public fun parsePchar( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterPchar(input, startIndex) + val index = startIndex + return run { + parseUnreserved(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parsePctEncoded(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parseSubDelims(input, index, listener).let { + if (it != -1) { + return@run it + } + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == ':') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> + if (char == '@') { + index + 1 + } else { + -1 + } + else -> -1 + } + } + .let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitPchar(input, startIndex, it) } + } + + public fun parseQuery( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterQuery(input, startIndex) + val index = startIndex + return run { + run { + var index = index + var count = 0 + while (true) { + parsePchar(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + when (char) { + '?', + '/' -> index + 1 + else -> -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitQuery(input, startIndex, it) } + } + + public fun parseFragment( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterFragment(input, startIndex) + val index = startIndex + return run { + run { + var index = index + var count = 0 + while (true) { + parsePchar(input, index, listener).let { + if (it == -1) { + return@let false + } + ++count + index = it + true + } || break + } + index + } + .let { + if (it != -1) { + return@run it + } + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + when (char) { + '?', + '/' -> index + 1 + else -> -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitFragment(input, startIndex, it) } + } + + public fun parsePctEncoded( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterPctEncoded(input, startIndex) + val index = startIndex + return run { + var index = index + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (char == '%') { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .let { + if (it == -1) { + return@run -1 + } + index = it + } + parseHexdig(input, index).let { + if (it == -1) { + return@run -1 + } + index = it + } + parseHexdig(input, index).let { + if (it == -1) { + return@run -1 + } + index = it + } + index + } + .also { listener?.exitPctEncoded(input, startIndex, it) } + } + + public fun parseUnreserved( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterUnreserved(input, startIndex) + val index = startIndex + return run { + parseAlpha(input, index).let { + if (it != -1) { + return@run it + } + } + parseDigit(input, index).let { + if (it != -1) { + return@run it + } + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + when (char) { + '.', + '-' -> index + 1 + else -> -1 + } + char < 128.toChar() -> + when (char) { + '~', + '_' -> index + 1 + else -> -1 + } + else -> -1 + } + } + .let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitUnreserved(input, startIndex, it) } + } + + public fun parseReserved( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterReserved(input, startIndex) + val index = startIndex + return run { + parseGenDelims(input, index, listener).let { + if (it != -1) { + return@run it + } + } + parseSubDelims(input, index, listener).let { + if (it != -1) { + return@run it + } + } + -1 + } + .also { listener?.exitReserved(input, startIndex, it) } + } + + public fun parseGenDelims( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterGenDelims(input, startIndex) + val index = startIndex + return run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (((1UL shl char.code) and 0x8400800800000000UL) != 0UL) { + index + 1 + } else { + -1 + } + char < 128.toChar() -> + when (char) { + ']', + '[', + '@' -> index + 1 + else -> -1 + } + else -> -1 + } + } + .also { listener?.exitGenDelims(input, startIndex, it) } + } + + public fun parseSubDelims( + input: String, + startIndex: Int = 0, + listener: Listener? = null, + ): Int { + listener?.enterSubDelims(input, startIndex) + val index = startIndex + return run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (((1UL shl char.code) and 0x28001FD200000000UL) != 0UL) { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + .also { listener?.exitSubDelims(input, startIndex, it) } + } + + private fun parseAlpha(input: String, startIndex: Int): Int { + val index = startIndex + return run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> -1 + char < 128.toChar() -> + if (((1UL shl (char.code - 64)) and 0x7FFFFFE07FFFFFEUL) != 0UL) { + index + 1 + } else { + -1 + } + else -> -1 + } + } + } + + private fun parseDigit(input: String, startIndex: Int): Int { + val index = startIndex + return run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> + if (((1UL shl char.code) and 0x3FF000000000000UL) != 0UL) { + index + 1 + } else { + -1 + } + char < 128.toChar() -> -1 + else -> -1 + } + } + } + + private fun parseHexdig(input: String, startIndex: Int): Int { + val index = startIndex + return run { + parseDigit(input, index).let { + if (it != -1) { + return@run it + } + } + run { + if (index >= input.length) { + return@run -1 + } + val char = input[index] + when { + char < 64.toChar() -> -1 + char < 128.toChar() -> + if (((1UL shl (char.code - 64)) and 0x7EUL) != 0UL) { + index + 1 + } else { + -1 + } + else -> -1 + } + } + .let { + if (it != -1) { + return@run it + } + } + -1 + } + } + + public interface Listener { + public fun enterUri(input: String, startIndex: Int) {} + + public fun exitUri( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterHierPart(input: String, startIndex: Int) {} + + public fun exitHierPart( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterUriReference(input: String, startIndex: Int) {} + + public fun exitUriReference( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterAbsoluteUri(input: String, startIndex: Int) {} + + public fun exitAbsoluteUri( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterRelativeRef(input: String, startIndex: Int) {} + + public fun exitRelativeRef( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterRelativePart(input: String, startIndex: Int) {} + + public fun exitRelativePart( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterScheme(input: String, startIndex: Int) {} + + public fun exitScheme( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterAuthority(input: String, startIndex: Int) {} + + public fun exitAuthority( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterUserinfo(input: String, startIndex: Int) {} + + public fun exitUserinfo( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterHost(input: String, startIndex: Int) {} + + public fun exitHost( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterPort(input: String, startIndex: Int) {} + + public fun exitPort( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterIpLiteral(input: String, startIndex: Int) {} + + public fun exitIpLiteral( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterIpvfuture(input: String, startIndex: Int) {} + + public fun exitIpvfuture( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterIpv6Address(input: String, startIndex: Int) {} + + public fun exitIpv6Address( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterH16(input: String, startIndex: Int) {} + + public fun exitH16( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterLs32(input: String, startIndex: Int) {} + + public fun exitLs32( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterIpv4Address(input: String, startIndex: Int) {} + + public fun exitIpv4Address( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterDecOctet(input: String, startIndex: Int) {} + + public fun exitDecOctet( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterRegName(input: String, startIndex: Int) {} + + public fun exitRegName( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterPath(input: String, startIndex: Int) {} + + public fun exitPath( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterPathAbempty(input: String, startIndex: Int) {} + + public fun exitPathAbempty( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterPathAbsolute(input: String, startIndex: Int) {} + + public fun exitPathAbsolute( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterPathNoscheme(input: String, startIndex: Int) {} + + public fun exitPathNoscheme( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterPathRootless(input: String, startIndex: Int) {} + + public fun exitPathRootless( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterPathEmpty(input: String, startIndex: Int) {} + + public fun exitPathEmpty( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterSegment(input: String, startIndex: Int) {} + + public fun exitSegment( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterSegmentNz(input: String, startIndex: Int) {} + + public fun exitSegmentNz( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterSegmentNzNc(input: String, startIndex: Int) {} + + public fun exitSegmentNzNc( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterPchar(input: String, startIndex: Int) {} + + public fun exitPchar( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterQuery(input: String, startIndex: Int) {} + + public fun exitQuery( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterFragment(input: String, startIndex: Int) {} + + public fun exitFragment( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterPctEncoded(input: String, startIndex: Int) {} + + public fun exitPctEncoded( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterUnreserved(input: String, startIndex: Int) {} + + public fun exitUnreserved( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterReserved(input: String, startIndex: Int) {} + + public fun exitReserved( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterGenDelims(input: String, startIndex: Int) {} + + public fun exitGenDelims( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + + public fun enterSubDelims(input: String, startIndex: Int) {} + + public fun exitSubDelims( + input: String, + startIndex: Int, + endIndex: Int, + ) {} + } +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/UriParsers.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/UriParsers.kt new file mode 100644 index 000000000..ecccd1338 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/internal/UriParsers.kt @@ -0,0 +1,267 @@ +package me.zhanghai.kotlin.filesystem.internal + +import kotlinx.io.bytestring.ByteString +import kotlinx.io.bytestring.buildByteString +import kotlinx.io.bytestring.decodeToString +import me.zhanghai.kotlin.filesystem.Uri +import kotlin.experimental.and +import kotlin.experimental.or + +internal fun UriParser.parse(input: String): Uri { + val listener = UriParserListener() + parseUriReference(input, listener = listener).let { + require(it == input.length) { "Invalid URI \"$input\"" } + } + return listener.createUri() +} + +private class UriParserListener : UriParser.Listener { + private var scheme: String? = null + private var encodedUserInfo: String? = null + private var encodedHost: String? = null + private var port: Int? = null + private var encodedPath: String? = null + private var encodedQuery: String? = null + private var encodedFragment: String? = null + + override fun exitScheme(input: String, startIndex: Int, endIndex: Int) { + if (endIndex != -1) { + scheme = input.substring(startIndex, endIndex) + } + } + + override fun exitUserinfo(input: String, startIndex: Int, endIndex: Int) { + if (endIndex != -1) { + encodedUserInfo = input.substring(startIndex, endIndex) + } + } + + override fun exitHost(input: String, startIndex: Int, endIndex: Int) { + if (endIndex != -1) { + encodedHost = input.substring(startIndex, endIndex) + } + } + + override fun exitPort(input: String, startIndex: Int, endIndex: Int) { + if (endIndex != -1) { + port = input.substring(startIndex, endIndex).toInt() + } + } + + override fun exitPathAbempty(input: String, startIndex: Int, endIndex: Int) { + exitPathCommon(input, startIndex, endIndex) + } + + override fun exitPathAbsolute(input: String, startIndex: Int, endIndex: Int) { + exitPathCommon(input, startIndex, endIndex) + } + + override fun exitPathNoscheme(input: String, startIndex: Int, endIndex: Int) { + exitPathCommon(input, startIndex, endIndex) + } + + override fun exitPathRootless(input: String, startIndex: Int, endIndex: Int) { + exitPathCommon(input, startIndex, endIndex) + } + + override fun exitPathEmpty(input: String, startIndex: Int, endIndex: Int) { + exitPathCommon(input, startIndex, endIndex) + } + + private fun exitPathCommon(input: String, startIndex: Int, endIndex: Int) { + if (endIndex != -1) { + encodedPath = input.substring(startIndex, endIndex) + } + } + + override fun exitQuery(input: String, startIndex: Int, endIndex: Int) { + if (endIndex != -1) { + encodedQuery = input.substring(startIndex, endIndex) + } + } + + override fun exitFragment(input: String, startIndex: Int, endIndex: Int) { + if (endIndex != -1) { + encodedFragment = input.substring(startIndex, endIndex) + } + } + + fun createUri(): Uri = + Uri( + scheme, + encodedUserInfo, + encodedHost, + port, + encodedPath!!, + encodedQuery, + encodedFragment, + null + ) +} + +internal fun UriParser.requireValidScheme(scheme: String?) { + if (scheme != null) { + require(parseScheme(scheme) == scheme.length) { "Invalid URI scheme \"$scheme\"" } + } +} + +internal fun UriParser.requireValidEncodedUserInfo(encodedUserInfo: String?) { + if (encodedUserInfo != null) { + require(parseUserinfo(encodedUserInfo) == encodedUserInfo.length) { + "Invalid URI user info \"$encodedUserInfo\"" + } + } +} + +internal fun UriParser.requireValidEncodedHost(encodedHost: String?) { + if (encodedHost != null) { + require(parseHost(encodedHost) == encodedHost.length) { + "Invalid URI host \"$encodedHost\"" + } + } +} + +@Suppress("UnusedReceiverParameter") +internal fun UriParser.requireValidPort(port: Int?) { + if (port != null) { + require(port >= 0) { "Invalid URI port $port" } + } +} + +internal fun UriParser.requireValidEncodedPath(encodedPath: String, hasScheme: Boolean) { + if (parsePathAbempty(encodedPath) == encodedPath.length) { + return + } + if (parsePathAbsolute(encodedPath) == encodedPath.length) { + return + } + if (hasScheme) { + if (parsePathRootless(encodedPath) == encodedPath.length) { + return + } + } else { + if (parsePathNoscheme(encodedPath) == encodedPath.length) { + return + } + } + if (parsePathEmpty(encodedPath) == encodedPath.length) { + return + } + throw IllegalArgumentException("Invalid URI path \"$encodedPath\"") +} + +internal fun UriParser.requireValidEncodedQuery(encodedQuery: String?) { + if (encodedQuery != null) { + require(parseQuery(encodedQuery) == encodedQuery.length) { + "Invalid URI query \"$encodedQuery\"" + } + } +} + +internal fun UriParser.requireValidEncodedFragment(encodedFragment: String?) { + if (encodedFragment != null) { + require(parseFragment(encodedFragment) == encodedFragment.length) { + "Invalid URI fragment \"$encodedFragment\"" + } + } +} + +@Suppress("UnusedReceiverParameter") +internal fun UriParser.encodeUserInfo(decodedUserInfo: ByteString): String = + encodePart(decodedUserInfo, CHAR_SET_USERINFO) + +internal fun UriParser.encodeHost(decodedHost: ByteString): String { + if ( + decodedHost.size > 2 && + decodedHost.first() == '['.code.toByte() && + decodedHost.last() == ']'.code.toByte() + ) { + val encodedHost = decodedHost.decodeToString() + if (parseIpLiteral(encodedHost) == encodedHost.length) { + return encodedHost + } + } + return encodePart(decodedHost, CHAR_SET_REG_NAME) +} + +@Suppress("UnusedReceiverParameter") +internal fun UriParser.encodePath(decodedPath: ByteString): String = + encodePart(decodedPath, CHAR_SET_PATH) + +@Suppress("UnusedReceiverParameter") +internal fun UriParser.encodeQuery(decodedQuery: ByteString): String = + encodePart(decodedQuery, CHAR_SET_QUERY) + +@Suppress("UnusedReceiverParameter") +internal fun UriParser.encodeFragment(decodedFragment: ByteString): String = + encodePart(decodedFragment, CHAR_SET_FRAGMENT) + +// https://datatracker.ietf.org/doc/html/rfc3986#appendix-A +private val CHAR_SET_ALPHA = AsciiCharSet.ofRange('A', 'Z') or AsciiCharSet.ofRange('a', 'z') +private val CHAR_SET_DIGIT = AsciiCharSet.ofRange('0', '9') +private val CHAR_SET_SUB_DELIMS = AsciiCharSet.of("!$&'()*+,;=") +private val CHAR_SET_UNRESERVED = CHAR_SET_ALPHA or CHAR_SET_DIGIT or AsciiCharSet.of("-._~") +private val CHAR_SET_USERINFO = CHAR_SET_UNRESERVED or CHAR_SET_SUB_DELIMS or AsciiCharSet.of(':') +private val CHAR_SET_REG_NAME = CHAR_SET_UNRESERVED or CHAR_SET_SUB_DELIMS +private val CHAR_SET_PCHAR = CHAR_SET_UNRESERVED or CHAR_SET_SUB_DELIMS or AsciiCharSet.of(":@") +private val CHAR_SET_PATH = CHAR_SET_PCHAR or AsciiCharSet.of('/') +private val CHAR_SET_QUERY = CHAR_SET_PCHAR or AsciiCharSet.of("/?") +private val CHAR_SET_FRAGMENT = CHAR_SET_PCHAR or AsciiCharSet.of("/?") + +private fun encodePart( + decodedPart: ByteString, + charSet: AsciiCharSet, + startIndex: Int = 0, + endIndex: Int = decodedPart.size +): String = buildString { + for (i in startIndex ..< endIndex) { + val byte = decodedPart[i] + val char = byte.toInt().toChar() + if (charSet.matches(char)) { + append(char) + } else { + append('%') + append((((byte.toInt() ushr 4).toByte() and 0x0F)).hexEncode()) + append((byte and 0x0F).hexEncode()) + } + } +} + +private fun Byte.hexEncode(): Char = + when (this) { + in 0..9 -> '0' + toInt() + in 10..15 -> 'A' + (toInt() - 10) + else -> throw IllegalArgumentException("Non-half byte $this in URI percent-encoding") + } + +@Suppress("UnusedReceiverParameter") +internal fun UriParser.decodePart(encodedPart: String): ByteString = buildByteString { + var index = 0 + val length = encodedPart.length + while (index < length) { + when (val char = encodedPart[index]) { + '%' -> { + require(index + 3 <= length) { + "Incomplete URI percent-encoding \"${encodedPart.substring(index)}\"" + } + val halfByte1 = encodedPart[index + 1].hexDecode() + val halfByte2 = encodedPart[index + 2].hexDecode() + val byte = (halfByte1.toInt() shl 4).toByte() or halfByte2 + append(byte) + index += 3 + } + else -> { + append(char.code.toByte()) + ++index + } + } + } +} + +private fun Char.hexDecode(): Byte = + when (this) { + in '0'..'9' -> (this - '0').toByte() + in 'A'..'F' -> (10 + (this - 'A')).toByte() + in 'a'..'f' -> (10 + (this - 'a')).toByte() + else -> throw IllegalArgumentException("Invalid character '$this' in URI percent-encoding") + } diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncCloseable.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncCloseable.kt new file mode 100644 index 000000000..7a5bbb765 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncCloseable.kt @@ -0,0 +1,36 @@ +package me.zhanghai.kotlin.filesystem.io + +import kotlinx.io.IOException +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.coroutines.cancellation.CancellationException + +public interface AsyncCloseable { + @Throws(CancellationException::class, IOException::class) public suspend fun close() +} + +@OptIn(ExperimentalContracts::class) +public suspend inline fun T.use(block: (T) -> R): R { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + var throwable: Throwable? = null + try { + return block(this) + } catch (t: Throwable) { + throwable = t + throw t + } finally { + // Work around compiler error about smart cast and "captured by a changing closure" + @Suppress("NAME_SHADOWING") val throwable = throwable + when { + this == null -> {} + throwable == null -> close() + else -> + try { + close() + } catch (closeThrowable: Throwable) { + throwable.addSuppressed(closeThrowable) + } + } + } +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncFlushable.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncFlushable.kt new file mode 100644 index 000000000..f1b172ab9 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncFlushable.kt @@ -0,0 +1,8 @@ +package me.zhanghai.kotlin.filesystem.io + +import kotlinx.io.IOException +import kotlin.coroutines.cancellation.CancellationException + +public interface AsyncFlushable { + @Throws(CancellationException::class, IOException::class) public suspend fun flush() +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncSink.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncSink.kt new file mode 100644 index 000000000..8a6102dbd --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncSink.kt @@ -0,0 +1,31 @@ +package me.zhanghai.kotlin.filesystem.io + +import kotlinx.io.Buffer +import kotlinx.io.IOException +import kotlin.coroutines.cancellation.CancellationException + +public interface AsyncSink : AsyncCloseable, AsyncFlushable { + @Throws(CancellationException::class, IOException::class) + public suspend fun write(source: Buffer, byteCount: Long) +} + +internal fun AsyncSink.withCloseable(closeable: AsyncCloseable): AsyncSink = + CloseableAsyncSink(this, closeable) + +private class CloseableAsyncSink( + private val sink: AsyncSink, + private val closeable: AsyncCloseable +) : AsyncSink { + override suspend fun write(source: Buffer, byteCount: Long) { + sink.write(source, byteCount) + } + + override suspend fun flush() { + sink.flush() + } + + override suspend fun close() { + sink.close() + closeable.close() + } +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncSource.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncSource.kt new file mode 100644 index 000000000..d5fc351d0 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/io/AsyncSource.kt @@ -0,0 +1,26 @@ +package me.zhanghai.kotlin.filesystem.io + +import kotlinx.io.Buffer +import kotlinx.io.IOException +import kotlin.coroutines.cancellation.CancellationException + +public interface AsyncSource : AsyncCloseable { + @Throws(CancellationException::class, IOException::class) + public suspend fun readAtMostTo(sink: Buffer, byteCount: Long): Long +} + +internal fun AsyncSource.withCloseable(closeable: AsyncCloseable): AsyncSource = + CloseableAsyncSource(this, closeable) + +private class CloseableAsyncSource( + private val source: AsyncSource, + private val closeable: AsyncCloseable +) : AsyncSource { + override suspend fun readAtMostTo(sink: Buffer, byteCount: Long): Long = + source.readAtMostTo(sink, byteCount) + + override suspend fun close() { + source.close() + closeable.close() + } +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixFileMetadata.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixFileMetadata.kt new file mode 100644 index 000000000..c5cc54ffa --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixFileMetadata.kt @@ -0,0 +1,23 @@ +package me.zhanghai.kotlin.filesystem.posix + +import me.zhanghai.kotlin.filesystem.FileMetadata +import me.zhanghai.kotlin.filesystem.FileType + +public interface PosixFileMetadata : FileMetadata { + public val posixType: PosixFileType + + override val type: FileType + get() = + when (posixType) { + PosixFileType.DIRECTORY -> FileType.DIRECTORY + PosixFileType.REGULAR_FILE -> FileType.REGULAR_FILE + PosixFileType.SYMBOLIC_LINK -> FileType.SYMBOLIC_LINK + else -> FileType.OTHER + } + + public val mode: Set + + public val userId: Int + + public val groupId: Int +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixFileMetadataView.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixFileMetadataView.kt new file mode 100644 index 000000000..89385f9e2 --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixFileMetadataView.kt @@ -0,0 +1,11 @@ +package me.zhanghai.kotlin.filesystem.posix + +import me.zhanghai.kotlin.filesystem.FileMetadataView + +public interface PosixFileMetadataView : FileMetadataView { + override suspend fun readMetadata(): PosixFileMetadata + + public suspend fun setMode(mode: Set) + + public suspend fun setOwnership(userId: Int? = null, groupId: Int? = null) +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixFileType.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixFileType.kt new file mode 100644 index 000000000..5644e97df --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixFileType.kt @@ -0,0 +1,12 @@ +package me.zhanghai.kotlin.filesystem.posix + +public enum class PosixFileType { + DIRECTORY, + CHARACTER_DEVICE, + BLOCK_DEVICE, + REGULAR_FILE, + FIFO, + SYMBOLIC_LINK, + SOCKET, + OTHER +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixModeBit.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixModeBit.kt new file mode 100644 index 000000000..ab57c15ea --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixModeBit.kt @@ -0,0 +1,13 @@ +package me.zhanghai.kotlin.filesystem.posix + +public enum class PosixModeBit { + OWNER_READ, + OWNER_WRITE, + OWNER_EXECUTE, + GROUP_READ, + GROUP_WRITE, + GROUP_EXECUTE, + OTHERS_READ, + OTHERS_WRITE, + OTHERS_EXECUTE +} diff --git a/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixModeOption.kt b/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixModeOption.kt new file mode 100644 index 000000000..cab7253ba --- /dev/null +++ b/app/src/main/java/me/zhanghai/kotlin/filesystem/posix/PosixModeOption.kt @@ -0,0 +1,5 @@ +package me.zhanghai.kotlin.filesystem.posix + +import me.zhanghai.kotlin.filesystem.CreateFileOption + +public data class PosixModeOption(public val mode: Set) : CreateFileOption