diff --git a/okio/api/okio.api b/okio/api/okio.api index e124cd36a2..d112bfc9aa 100644 --- a/okio/api/okio.api +++ b/okio/api/okio.api @@ -477,6 +477,8 @@ public abstract class okio/FileSystem { public fun deleteRecursively (Lokio/Path;Z)V public static synthetic fun deleteRecursively$default (Lokio/FileSystem;Lokio/Path;ZILjava/lang/Object;)V public final fun exists (Lokio/Path;)Z + public fun extend (Lkotlin/reflect/KClass;Lokio/FileSystemExtension;)Lokio/FileSystem; + public fun extension (Lkotlin/reflect/KClass;)Lokio/FileSystemExtension; public static final fun get (Ljava/nio/file/FileSystem;)Lokio/FileSystem; public abstract fun list (Lokio/Path;)Ljava/util/List; public abstract fun listOrNull (Lokio/Path;)Ljava/util/List; @@ -499,7 +501,23 @@ public final class okio/FileSystem$Companion { public final fun get (Ljava/nio/file/FileSystem;)Lokio/FileSystem; } -public abstract class okio/ForwardingFileSystem : okio/FileSystem { +public abstract interface class okio/FileSystemExtension { + public abstract fun map (Lokio/FileSystemExtension$Mapping;)Lokio/FileSystemExtension; +} + +public abstract class okio/FileSystemExtension$Mapping { + public static final field Companion Lokio/FileSystemExtension$Mapping$Companion; + public fun ()V + public final fun chain (Lokio/FileSystemExtension$Mapping;)Lokio/FileSystemExtension$Mapping; + public abstract fun mapParameter (Lokio/Path;Ljava/lang/String;Ljava/lang/String;)Lokio/Path; + public abstract fun mapResult (Lokio/Path;Ljava/lang/String;)Lokio/Path; +} + +public final class okio/FileSystemExtension$Mapping$Companion { + public final fun getNONE ()Lokio/FileSystemExtension$Mapping; +} + +public class okio/ForwardingFileSystem : okio/FileSystem { public fun (Lokio/FileSystem;)V public fun appendingSink (Lokio/Path;Z)Lokio/Sink; public fun atomicMove (Lokio/Path;Lokio/Path;)V @@ -508,10 +526,13 @@ public abstract class okio/ForwardingFileSystem : okio/FileSystem { public fun createSymlink (Lokio/Path;Lokio/Path;)V public final fun delegate ()Lokio/FileSystem; public fun delete (Lokio/Path;Z)V + public fun extension (Lkotlin/reflect/KClass;)Lokio/FileSystemExtension; + public final fun getExtensionMapping ()Lokio/FileSystemExtension$Mapping; public fun list (Lokio/Path;)Ljava/util/List; public fun listOrNull (Lokio/Path;)Ljava/util/List; public fun listRecursively (Lokio/Path;Z)Lkotlin/sequences/Sequence; public fun metadataOrNull (Lokio/Path;)Lokio/FileMetadata; + public fun onExtension (Lkotlin/reflect/KClass;Lokio/FileSystemExtension;)Lokio/FileSystemExtension; public fun onPathParameter (Lokio/Path;Ljava/lang/String;Ljava/lang/String;)Lokio/Path; public fun onPathResult (Lokio/Path;Ljava/lang/String;)Lokio/Path; public fun openReadOnly (Lokio/Path;)Lokio/FileHandle; @@ -819,6 +840,10 @@ public final class okio/_JvmPlatformKt { public static final fun withLock (Ljava/util/concurrent/locks/ReentrantLock;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; } +public final class okio/internal/-FileSystem { + public static final fun commonExtend (Lokio/FileSystem;Lkotlin/reflect/KClass;Ljava/lang/Object;)Lokio/FileSystem; +} + public final class okio/internal/_Utf8Kt { public static final fun commonAsUtf8ToByteArray (Ljava/lang/String;)[B public static final fun commonToUtf8String ([BII)Ljava/lang/String; diff --git a/okio/build.gradle.kts b/okio/build.gradle.kts index eea0f82119..ed0c66d1ab 100644 --- a/okio/build.gradle.kts +++ b/okio/build.gradle.kts @@ -79,6 +79,7 @@ kotlin { val nonWasmTest by creating { dependencies { + implementation(libs.kotlin.test) implementation(libs.kotlin.time) implementation(projects.okioFakefilesystem) } diff --git a/okio/src/commonMain/kotlin/okio/FileSystem.kt b/okio/src/commonMain/kotlin/okio/FileSystem.kt index 9535880b62..74e780679c 100644 --- a/okio/src/commonMain/kotlin/okio/FileSystem.kt +++ b/okio/src/commonMain/kotlin/okio/FileSystem.kt @@ -15,6 +15,8 @@ */ package okio +import kotlin.reflect.KClass + /** * Read and write access to a hierarchical collection of files, addressed by [paths][Path]. This * is a natural interface to the current computer's local file system. @@ -376,6 +378,18 @@ expect abstract class FileSystem() { @Throws(IOException::class) abstract fun createSymlink(source: Path, target: Path) + /** + * Returns a new file system that forwards all calls to this, and that also returns [extension] + * when it is requested. + * + * When [extensionType] is requested on the returned file system, it will return [extension], + * regardless of what is returned by this file system. + */ + open fun extend(extensionType: KClass, extension: E): FileSystem + + /** Returns the extension for [type] if it exists, and null otherwise. */ + open fun extension(type: KClass): E? + companion object { /** * Returns a writable temporary directory on [SYSTEM]. diff --git a/okio/src/commonMain/kotlin/okio/FileSystemExtension.kt b/okio/src/commonMain/kotlin/okio/FileSystemExtension.kt new file mode 100644 index 0000000000..79bab5b93e --- /dev/null +++ b/okio/src/commonMain/kotlin/okio/FileSystemExtension.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import okio.FileSystemExtension.Mapping + +/** + * Marks an object that can be attached to a [FileSystem], and that supplements the file system's + * capabilities. + * + * Implementations must support transforms to input and output paths with [Mapping]. To simplify + * implementation, use [Mapping.NONE] by default and use [Mapping.chain] to combine mappings. + * + * ```kotlin + * class DiskUsageExtension private constructor( + * private val mapping: Mapping, + * ) : FileSystemExtension { + * constructor() : this(Mapping.NONE) + * + * override fun map(outer: Mapping): FileSystemExtension { + * return DiskUsageExtension(mapping.chain(outer)) + * } + * + * fun sizeOnDisk(path: Path): Long { + * val mappedPath = mapping.mapParameter(path, "sizeOnDisk", "path") + * return lookUpSizeOnDisk(mappedPath) + * } + * + * fun largestFiles(): Sequence { + * val largestFiles: Sequence = lookUpLargestFiles() + * return largestFiles.map { + * mapping.mapResult(it, "largestFiles") + * } + * } + * } + * ``` + */ +interface FileSystemExtension { + /** Returns a file system of the same type, that applies [outer] to all paths. */ + fun map(outer: Mapping): FileSystemExtension + + abstract class Mapping { + abstract fun mapParameter(path: Path, functionName: String, parameterName: String): Path + abstract fun mapResult(path: Path, functionName: String): Path + + fun chain(outer: Mapping): Mapping { + val inner = this + return object : Mapping() { + override fun mapParameter(path: Path, functionName: String, parameterName: String): Path { + return inner.mapParameter( + outer.mapParameter( + path, + functionName, + parameterName, + ), + functionName, + parameterName, + ) + } + + override fun mapResult(path: Path, functionName: String): Path { + return outer.mapResult( + inner.mapResult( + path, + functionName, + ), + functionName, + ) + } + } + } + + companion object { + val NONE = object : Mapping() { + override fun mapParameter(path: Path, functionName: String, parameterName: String) = path + override fun mapResult(path: Path, functionName: String) = path + } + } + } +} diff --git a/okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt b/okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt index 2fe5cd4f06..69b5b45cb2 100644 --- a/okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt +++ b/okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt @@ -16,6 +16,8 @@ package okio import kotlin.jvm.JvmName +import kotlin.reflect.KClass +import kotlin.reflect.cast /** * A [FileSystem] that forwards calls to another, intended for subclassing. @@ -101,11 +103,25 @@ import kotlin.jvm.JvmName * other functions of this class. If desired, subclasses may override non-abstract functions to * forward them. */ -abstract class ForwardingFileSystem( +open class ForwardingFileSystem internal constructor( /** [FileSystem] to which this instance is delegating. */ @get:JvmName("delegate") val delegate: FileSystem, + extensions: Map, Any>, ) : FileSystem() { + /** Extensions added at this layer. Additional extensions may exist in [delegate]. */ + internal val extensions = extensions.toMap() + + /** Maps paths with [onPathParameter] and [onPathResult]. */ + val extensionMapping: FileSystemExtension.Mapping = object : FileSystemExtension.Mapping() { + override fun mapParameter(path: Path, functionName: String, parameterName: String) = + onPathParameter(path, functionName, parameterName) + + override fun mapResult(path: Path, functionName: String) = + onPathResult(path, functionName) + } + + constructor(delegate: FileSystem) : this(delegate, emptyMap()) /** * Invoked each time a path is passed as a parameter to this file system. This returns the path to @@ -142,6 +158,16 @@ abstract class ForwardingFileSystem( */ open fun onPathResult(path: Path, functionName: String): Path = path + /** + * Invoked each time an extension is returned from [ForwardingFileSystem.extension]. + * + * Overrides of this function must call [FileSystemExtension.map] with [extensionMapping], + * otherwise path mapping will not be applied. Or call `super.onExtension()` to do this. + */ + open fun onExtension(type: KClass, extension: T): T { + return type.cast(extension.map(extensionMapping)) + } + @Throws(IOException::class) override fun canonicalize(path: Path): Path { val path = onPathParameter(path, "canonicalize", "path") @@ -238,5 +264,13 @@ abstract class ForwardingFileSystem( delegate.createSymlink(source, target) } + override fun extension(type: KClass): E? { + val result = extensions[type]?.let { type.cast(it) } + ?: delegate.extension(type) + ?: return null + + return onExtension(type, result) + } + override fun toString() = "${this::class.simpleName}($delegate)" } diff --git a/okio/src/commonMain/kotlin/okio/Okio.kt b/okio/src/commonMain/kotlin/okio/Okio.kt index a0a420bbc3..1b0464fb8e 100644 --- a/okio/src/commonMain/kotlin/okio/Okio.kt +++ b/okio/src/commonMain/kotlin/okio/Okio.kt @@ -72,3 +72,16 @@ inline fun T.use(block: (T) -> R): R { @Suppress("UNCHECKED_CAST") return result as R } + +/** + * Returns a new file system that forwards all calls to this, and that also returns [extension] + * when it is requested. + * + * When [E] is requested on the returned file system, it will return [extension]. If this file + * system already has an extension of this type, [extension] takes precedence. + */ +inline fun FileSystem.extend(extension: E): FileSystem = + extend(E::class, extension) + +/** Returns the extension for [E] if it exists, and null otherwise. */ +inline fun FileSystem.extension(): E? = extension(E::class) diff --git a/okio/src/commonMain/kotlin/okio/internal/FileSystem.kt b/okio/src/commonMain/kotlin/okio/internal/FileSystem.kt index 72c541d12d..9d2618128f 100644 --- a/okio/src/commonMain/kotlin/okio/internal/FileSystem.kt +++ b/okio/src/commonMain/kotlin/okio/internal/FileSystem.kt @@ -18,9 +18,11 @@ package okio.internal import kotlin.jvm.JvmName +import kotlin.reflect.KClass import okio.FileMetadata import okio.FileNotFoundException import okio.FileSystem +import okio.ForwardingFileSystem import okio.IOException import okio.Path import okio.buffer @@ -69,6 +71,23 @@ internal fun FileSystem.commonCopy(source: Path, target: Path) { } } +fun FileSystem.commonExtend(extensionType: KClass, extension: T): FileSystem { + // If this file system is already an extension wrapper, replace it rather than wrapping again. + // Note that this optimization doesn't apply to ForwardingFileSystem subclasses, only to the + // ForwardingFileSystem base class. + if (this::class == ForwardingFileSystem::class) { + this as ForwardingFileSystem + val newExtensions = extensions.toMutableMap() + newExtensions[extensionType] = extension + return ForwardingFileSystem(delegate, newExtensions) + } + + return ForwardingFileSystem( + delegate = this, + extensions = mapOf(extensionType to extension), + ) +} + @Throws(IOException::class) internal fun FileSystem.commonDeleteRecursively(fileOrDirectory: Path, mustExist: Boolean) { val sequence = sequence { diff --git a/okio/src/jsMain/kotlin/okio/FileSystem.kt b/okio/src/jsMain/kotlin/okio/FileSystem.kt index ea1bb4bf30..7f11e4a890 100644 --- a/okio/src/jsMain/kotlin/okio/FileSystem.kt +++ b/okio/src/jsMain/kotlin/okio/FileSystem.kt @@ -15,11 +15,13 @@ */ package okio +import kotlin.reflect.KClass import okio.Path.Companion.toPath import okio.internal.commonCopy import okio.internal.commonCreateDirectories import okio.internal.commonDeleteRecursively import okio.internal.commonExists +import okio.internal.commonExtend import okio.internal.commonListRecursively import okio.internal.commonMetadata @@ -84,6 +86,13 @@ actual abstract class FileSystem { actual abstract fun createSymlink(source: Path, target: Path) + actual open fun extend( + extensionType: KClass, + extension: E, + ): FileSystem = commonExtend(extensionType, extension) + + actual open fun extension(type: KClass): E? = null + actual companion object { actual val SYSTEM_TEMPORARY_DIRECTORY: Path = tmpdir.toPath() } diff --git a/okio/src/jvmMain/kotlin/okio/FileSystem.kt b/okio/src/jvmMain/kotlin/okio/FileSystem.kt index 7a552cc551..e55805cfad 100644 --- a/okio/src/jvmMain/kotlin/okio/FileSystem.kt +++ b/okio/src/jvmMain/kotlin/okio/FileSystem.kt @@ -16,12 +16,14 @@ package okio import java.nio.file.FileSystem as JavaNioFileSystem +import kotlin.reflect.KClass import okio.Path.Companion.toPath import okio.internal.ResourceFileSystem import okio.internal.commonCopy import okio.internal.commonCreateDirectories import okio.internal.commonDeleteRecursively import okio.internal.commonExists +import okio.internal.commonExtend import okio.internal.commonListRecursively import okio.internal.commonMetadata @@ -125,6 +127,13 @@ actual abstract class FileSystem { @Throws(IOException::class) actual abstract fun createSymlink(source: Path, target: Path) + actual open fun extend( + extensionType: KClass, + extension: E, + ): FileSystem = commonExtend(extensionType, extension) + + actual open fun extension(type: KClass): E? = null + actual companion object { /** * The current process's host file system. Use this instance directly, or dependency inject a diff --git a/okio/src/nativeMain/kotlin/okio/FileSystem.kt b/okio/src/nativeMain/kotlin/okio/FileSystem.kt index 7672c919b3..568ead3ac6 100644 --- a/okio/src/nativeMain/kotlin/okio/FileSystem.kt +++ b/okio/src/nativeMain/kotlin/okio/FileSystem.kt @@ -15,10 +15,12 @@ */ package okio +import kotlin.reflect.KClass import okio.internal.commonCopy import okio.internal.commonCreateDirectories import okio.internal.commonDeleteRecursively import okio.internal.commonExists +import okio.internal.commonExtend import okio.internal.commonListRecursively import okio.internal.commonMetadata @@ -102,6 +104,13 @@ actual abstract class FileSystem { @Throws(IOException::class) actual abstract fun createSymlink(source: Path, target: Path) + actual open fun extend( + extensionType: KClass, + extension: E, + ): FileSystem = commonExtend(extensionType, extension) + + actual open fun extension(type: KClass): E? = null + actual companion object { /** * The current process's host file system. Use this instance directly, or dependency inject a diff --git a/okio/src/nonWasmTest/kotlin/okio/FileSystemExtensionMappingTest.kt b/okio/src/nonWasmTest/kotlin/okio/FileSystemExtensionMappingTest.kt new file mode 100644 index 0000000000..537a7a1da5 --- /dev/null +++ b/okio/src/nonWasmTest/kotlin/okio/FileSystemExtensionMappingTest.kt @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import kotlin.test.Test +import kotlin.test.assertEquals +import okio.FileSystemExtension.Mapping +import okio.Path.Companion.toPath +import okio.fakefilesystem.FakeFileSystem + +class FileSystemExtensionMappingTest { + private val rawFileSystem = FakeFileSystem() + + @Test + fun happyPath() { + // Create a bunch of files in the /monday directory. + val mondayExtension = ChefExtension(rawFileSystem) + val mondayFs = rawFileSystem.extend(mondayExtension) + mondayFs.createDirectory("/monday".toPath()) + mondayFs.write("/monday/breakfast.txt".toPath()) { writeUtf8("croissant") } + mondayFs.write("/monday/lunch.txt".toPath()) { writeUtf8("peanut butter sandwich") } + mondayFs.write("/monday/dinner.txt".toPath()) { writeUtf8("pizza") } + + // Associate data with these files in the extension. + mondayExtension.setChef("/monday/dinner.txt".toPath(), "jesse") + mondayExtension.setChef("/monday/breakfast.txt".toPath(), "benoit") + mondayExtension.setChef("/monday/lunch.txt".toPath(), "jesse") + + // Confirm we can query the extension. + assertEquals( + listOf( + "/monday/dinner.txt".toPath(), + "/monday/lunch.txt".toPath(), + ), + mondayExtension.listForChef("/monday".toPath(), "jesse"), + ) + assertEquals( + listOf( + "/monday/breakfast.txt".toPath(), + ), + mondayExtension.listForChef("/monday".toPath(), "benoit"), + ) + + // Apply a path mapping transformation and confirm we can read the metadata. + val tuesdayFs = TuesdayFileSystem(mondayFs) + val tuesdayExtension = tuesdayFs.extension()!! + assertEquals( + listOf( + "/tuesday/dinner.txt".toPath(), + "/tuesday/lunch.txt".toPath(), + ), + tuesdayExtension.listForChef("/tuesday".toPath(), "jesse"), + ) + assertEquals( + listOf( + "/tuesday/breakfast.txt".toPath(), + ), + tuesdayExtension.listForChef("/tuesday".toPath(), "benoit"), + ) + + // We should also be able to write through the extension... + tuesdayFs.write("/tuesday/snack.txt".toPath()) { writeUtf8("doritos") } + tuesdayExtension.setChef("/tuesday/snack.txt".toPath(), "jake") + assertEquals( + listOf( + "/tuesday/snack.txt".toPath(), + ), + tuesdayExtension.listForChef("/tuesday".toPath(), "jake"), + ) + + // ...And the extension data should map all the way through to the source file system. + assertEquals( + listOf( + "/monday/snack.txt".toPath(), + ), + mondayExtension.listForChef("/monday".toPath(), "jake"), + ) + } + + @Test + fun chainTransformations() { + val mondayExtension = ChefExtension(rawFileSystem) + val mondayFs = rawFileSystem.extend(mondayExtension) + mondayFs.createDirectory("/monday".toPath()) + mondayFs.write("/monday/breakfast.txt".toPath()) { writeUtf8("croissant") } + mondayExtension.setChef("/monday/breakfast.txt".toPath(), "benoit") + + // Map /monday to /tuesday. + val tuesdayFs = TuesdayFileSystem(mondayFs) + + // Map / to /menu. + val menuFs = MenuFileSystem(tuesdayFs) + val menuExtension = menuFs.extension()!! + + // Confirm we can read through the mappings. + assertEquals( + listOf( + "/menu/tuesday/breakfast.txt".toPath(), + ), + menuExtension.listForChef("/menu/tuesday".toPath(), "benoit"), + ) + + // Confirm we can write through also. + menuFs.write("/menu/tuesday/lunch.txt".toPath()) { writeUtf8("tomato soup") } + menuExtension.setChef("/menu/tuesday/lunch.txt".toPath(), "jesse") + assertEquals( + "tomato soup", + mondayFs.read("/monday/lunch.txt".toPath()) { readUtf8() }, + ) + assertEquals( + listOf( + "/monday/lunch.txt".toPath(), + ), + mondayExtension.listForChef("/monday".toPath(), "jesse"), + ) + + // Each extension gets its own mapping. + assertEquals( + "tomato soup", + tuesdayFs.read("/tuesday/lunch.txt".toPath()) { readUtf8() }, + ) + assertEquals( + listOf( + "/tuesday/lunch.txt".toPath(), + ), + tuesdayFs.extension()!!.listForChef("/tuesday".toPath(), "jesse"), + ) + } + + /** + * This test extension associates paths with optional metadata: the chef of a file. + * + * When tests run there will be multiple instances of this extension, all sharing one [chefs] + * store, and each with its own [mapping]. The contents of [chefs] uses mapped paths for its keys. + * + * Real world extensions will have similar obligations for path mapping, but they'll likely do + * real things with the paths such as passing them to system APIs. + */ + class ChefExtension( + private val target: FileSystem, + private val chefs: MutableMap = mutableMapOf(), + private val mapping: Mapping = Mapping.NONE, + ) : FileSystemExtension { + override fun map(outer: Mapping) = ChefExtension(target, chefs, mapping.chain(outer)) + + fun setChef(path: Path, chef: String) { + val mappedPath = mapping.mapParameter(path, "set", "path") + chefs[mappedPath] = chef + } + + fun listForChef(dir: Path, chef: String): List { + val mappedDir = mapping.mapParameter(dir, "listForChef", "dir") + return target.list(mappedDir) + .filter { chefs[it] == chef } + .map { mapping.mapResult(it, "listForChef") } + } + } + + class TuesdayFileSystem(delegate: FileSystem) : ForwardingFileSystem(delegate) { + private val monday = "/monday".toPath() + private val tuesday = "/tuesday".toPath() + + override fun onPathParameter(path: Path, functionName: String, parameterName: String) = + monday / path.relativeTo(tuesday) + + override fun onPathResult(path: Path, functionName: String) = + tuesday / path.relativeTo(monday) + } + + class MenuFileSystem(delegate: FileSystem) : ForwardingFileSystem(delegate) { + private val root = "/".toPath() + private val menu = "/menu".toPath() + + override fun onPathParameter(path: Path, functionName: String, parameterName: String) = + root / path.relativeTo(menu) + + override fun onPathResult(path: Path, functionName: String) = + menu / path.relativeTo(root) + } +} diff --git a/okio/src/nonWasmTest/kotlin/okio/FileSystemExtensionsTest.kt b/okio/src/nonWasmTest/kotlin/okio/FileSystemExtensionsTest.kt new file mode 100644 index 0000000000..7a27d676e0 --- /dev/null +++ b/okio/src/nonWasmTest/kotlin/okio/FileSystemExtensionsTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNull +import okio.FileSystemExtension.Mapping +import okio.fakefilesystem.FakeFileSystem + +class FileSystemExtensionsTest { + @Test + fun happyPath() { + val fakeFileSystem = FakeFileSystem() + val extension = FooExtension(fakeFileSystem) + val fileSystemWithExtension = fakeFileSystem.extend(extension) + assertEquals(fileSystemWithExtension.extension(), extension) + } + + @Test + fun absentExtension() { + val fakeFileSystem = FakeFileSystem() + val extension = FooExtension(fakeFileSystem) + val fileSystemWithExtension = fakeFileSystem.extend(extension) + assertNull(fileSystemWithExtension.extension()) + } + + @Test + fun overrideExtension() { + val fakeFileSystem = FakeFileSystem() + val extension1 = FooExtension(fakeFileSystem) + val fileSystemWithExtension1 = fakeFileSystem.extend(extension1) + + val extension2 = FooExtension(fakeFileSystem) + val fileSystemWithExtension2 = fileSystemWithExtension1.extend(extension2) + assertEquals(fileSystemWithExtension2.extension(), extension2) + + // Doesn't interfere with any of the wrapped layers. + assertEquals(fileSystemWithExtension1.extension(), extension1) + assertNull(fakeFileSystem.extension(), null) + } + + @Test + fun forwardingFileSystemCoalesced() { + val fakeFileSystem = FakeFileSystem() + val fooExtension = FooExtension(fakeFileSystem) + val fileSystemWithFoo = fakeFileSystem.extend(fooExtension) + + val barExtension = BarExtension(fakeFileSystem) + val fileSystemWithFooAndBar = fileSystemWithFoo.extend(barExtension) + + assertEquals(fileSystemWithFooAndBar.extension(), fooExtension) + assertEquals(fileSystemWithFooAndBar.extension(), barExtension) + assertEquals((fileSystemWithFooAndBar as ForwardingFileSystem).delegate, fakeFileSystem) + } + + @Test + fun customForwardingFileSystemNotCoalesced() { + val fakeFileSystem = FakeFileSystem() + val fooExtension = FooExtension(fakeFileSystem) + val fileSystemWithFoo = object : ForwardingFileSystem( + delegate = fakeFileSystem, + extensions = mapOf(FooExtension::class to fooExtension), + ) { + // This extends ForwardingFileSystem. Usually this would be to add new capabilities. + } + + val barExtension = BarExtension(fakeFileSystem) + val fileSystemWithFooAndBar = fileSystemWithFoo.extend(barExtension) + + assertEquals(fileSystemWithFooAndBar.extension(), fooExtension) + assertEquals(fileSystemWithFooAndBar.extension(), barExtension) + assertNotEquals((fileSystemWithFooAndBar as ForwardingFileSystem).delegate, fakeFileSystem) + } + + class FooExtension( + val target: FileSystem, + ) : FileSystemExtension { + override fun map(outer: Mapping) = this + } + + class BarExtension( + val target: FileSystem, + ) : FileSystemExtension { + override fun map(outer: Mapping) = this + } +} diff --git a/okio/src/wasmMain/kotlin/okio/FileSystem.kt b/okio/src/wasmMain/kotlin/okio/FileSystem.kt index 2152a91b0c..add85050a9 100644 --- a/okio/src/wasmMain/kotlin/okio/FileSystem.kt +++ b/okio/src/wasmMain/kotlin/okio/FileSystem.kt @@ -15,11 +15,13 @@ */ package okio +import kotlin.reflect.KClass import okio.Path.Companion.toPath import okio.internal.commonCopy import okio.internal.commonCreateDirectories import okio.internal.commonDeleteRecursively import okio.internal.commonExists +import okio.internal.commonExtend import okio.internal.commonListRecursively import okio.internal.commonMetadata @@ -84,6 +86,13 @@ actual abstract class FileSystem { actual abstract fun createSymlink(source: Path, target: Path) + actual open fun extend( + extensionType: KClass, + extension: E, + ): FileSystem = commonExtend(extensionType, extension) + + actual open fun extension(type: KClass): E? = null + actual companion object { actual val SYSTEM_TEMPORARY_DIRECTORY: Path = "/tmp".toPath() }