Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions okio-fakefilesystem/api/okio-fakefilesystem.api
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public final class okio/fakefilesystem/FakeFileSystem : okio/FileSystem {
public final fun getWorkingDirectory ()Lokio/Path;
public fun list (Lokio/Path;)Ljava/util/List;
public fun listOrNull (Lokio/Path;)Ljava/util/List;
public fun lock (Lokio/Path;Lokio/LockMode;)Lokio/FileLock;
public fun metadataOrNull (Lokio/Path;)Lokio/FileMetadata;
public final fun openPaths ()Ljava/util/List;
public fun openReadOnly (Lokio/Path;)Lokio/FileHandle;
Expand All @@ -42,3 +43,8 @@ public final class okio/fakefilesystem/FakeFileSystem : okio/FileSystem {
public fun toString ()Ljava/lang/String;
}

public final class okio/fakefilesystem/FakeFileSystemLock {
public fun <init> (Lokio/Path;)V
public final fun lock (Lokio/LockMode;)Lokio/FileLock;
}

Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import okio.ArrayIndexOutOfBoundsException
import okio.Buffer
import okio.ByteString
import okio.FileHandle
import okio.FileLock
import okio.FileMetadata
import okio.FileNotFoundException
import okio.FileSystem
import okio.IOException
import okio.LockMode
import okio.Path
import okio.Path.Companion.toPath
import okio.Sink
Expand Down Expand Up @@ -103,6 +105,9 @@ class FakeFileSystem private constructor(
/** Files that are currently open and need to be closed to avoid resource leaks. */
private val openFiles = mutableListOf<OpenFile>()

/** Lock coordination points. */
private val locks = mutableMapOf<Path, FakeFileSystemLock>()

/** Forbid all access after [close]. */
private var closed = false

Expand Down Expand Up @@ -804,4 +809,26 @@ class FakeFileSystem private constructor(
}

override fun toString() = "FakeFileSystem"

/**
* Obtain an Exclusive or Shared Lock on [path].
*
* @throws IOException if [path] does not exist, the FileSystem or the path does not
* support file locking, or the lock cannot be acquired. This method does not wait for the lock
* to become available.
*/
@Throws(IOException::class)
override fun lock(
path: Path,
mode: LockMode,
): FileLock {
val canonicalPath = canonicalizeInternal(path)
return locks.getOrPut(canonicalPath) {
FakeFileSystemLock(canonicalPath)
}.lock(mode)
}
}

expect class FakeFileSystemLock constructor(path: Path) {
fun lock(mode: LockMode): FileLock
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2025 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")

package okio.fakefilesystem

import java.util.concurrent.locks.StampedLock
import okio.FileLock
import okio.IOException
import okio.LockMode
import okio.Path

actual class FakeFileSystemLock actual constructor(
private val path: Path,
) {
private val readWriteLock = StampedLock()

actual fun lock(mode: LockMode): FileLock {
val stamp = when (mode) {
LockMode.Shared -> readWriteLock.tryReadLock()
LockMode.Exclusive -> readWriteLock.tryWriteLock()
}

if (stamp == 0L) {
throw IOException("Could not lock $path")
}

return object : FileLock {
override val isShared: Boolean
get() = mode == LockMode.Shared

override val isValid: Boolean
get() = readWriteLock.validate(stamp)

override fun close() {
readWriteLock.unlock(stamp)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2025 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okio.fakefilesystem

import okio.FileLock
import okio.LockMode
import okio.Path

actual class FakeFileSystemLock actual constructor(path: Path) {
actual fun lock(mode: LockMode): FileLock {
TODO("Not yet implemented")
}
}
16 changes: 16 additions & 0 deletions okio/api/okio.api
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,12 @@ public abstract class okio/FileHandle : java/io/Closeable {
public final fun write (J[BII)V
}

public abstract interface class okio/FileLock : java/lang/AutoCloseable {
public fun getFileHandle ()Lokio/FileHandle;
public abstract fun isShared ()Z
public abstract fun isValid ()Z
}

public final class okio/FileMetadata {
public fun <init> ()V
public fun <init> (ZZLokio/Path;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/Map;)V
Expand Down Expand Up @@ -486,6 +492,8 @@ public abstract class okio/FileSystem : java/io/Closeable {
public final fun listRecursively (Lokio/Path;)Lkotlin/sequences/Sequence;
public fun listRecursively (Lokio/Path;Z)Lkotlin/sequences/Sequence;
public static synthetic fun listRecursively$default (Lokio/FileSystem;Lokio/Path;ZILjava/lang/Object;)Lkotlin/sequences/Sequence;
public fun lock (Lokio/Path;Lokio/LockMode;)Lokio/FileLock;
public static synthetic fun lock$default (Lokio/FileSystem;Lokio/Path;Lokio/LockMode;ILjava/lang/Object;)Lokio/FileLock;
public final fun metadata (Lokio/Path;)Lokio/FileMetadata;
public abstract fun metadataOrNull (Lokio/Path;)Lokio/FileMetadata;
public abstract fun openReadOnly (Lokio/Path;)Lokio/FileHandle;
Expand Down Expand Up @@ -638,6 +646,14 @@ public final class okio/InflaterSource : okio/Source {
public fun timeout ()Lokio/Timeout;
}

public final class okio/LockMode : java/lang/Enum {
public static final field Exclusive Lokio/LockMode;
public static final field Shared Lokio/LockMode;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lokio/LockMode;
public static fun values ()[Lokio/LockMode;
}

public final class okio/Okio {
public static final fun appendingSink (Ljava/io/File;)Lokio/Sink;
public static final fun asResourceFileSystem (Ljava/lang/ClassLoader;)Lokio/FileSystem;
Expand Down
26 changes: 26 additions & 0 deletions okio/src/commonMain/kotlin/okio/FileSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,19 @@ expect abstract class FileSystem() : Closeable {
@Throws(IOException::class)
override fun close()

/**
* Obtain an Exclusive or Shared Lock on [path].
*
* @throws IOException if [path] does not exist, the FileSystem or the path does not
* support file locking, or the lock cannot be acquired. This method does not wait for the lock
* to become available.
*/
@Throws(IOException::class)
open fun lock(
path: Path,
mode: LockMode = LockMode.Exclusive,
): FileLock

companion object {
/**
* Returns a writable temporary directory on [SYSTEM].
Expand All @@ -408,3 +421,16 @@ expect abstract class FileSystem() : Closeable {
val SYSTEM_TEMPORARY_DIRECTORY: Path
}
}

enum class LockMode {
Exclusive,
Shared,
}

interface FileLock : AutoCloseable {
val isShared: Boolean
val isValid: Boolean

val fileHandle: FileHandle
get() = TODO()
}
112 changes: 112 additions & 0 deletions okio/src/commonTest/kotlin/okio/BaseFileLockTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright (C) 2025 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.BeforeTest
import kotlin.test.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue

abstract class BaseFileLockTest {
abstract val fileSystem: FileSystem
abstract val file1: Path

abstract fun isSupported(): Boolean

abstract fun isSharedLockSupported(): Boolean

@BeforeTest
fun createFile() {
fileSystem.write(file1) {}
}

@Test
fun testLockAndUnlock() {
if (!isSupported()) {
return
}

val lock = fileSystem.lock(file1, mode = LockMode.Exclusive)

assertTrue(lock.isValid)
assertFalse(lock.isShared)

lock.close()
assertFalse(lock.isValid)
}

@Test
fun testExclusiveLock() {
if (!isSupported()) {
return
}

val lock1 = fileSystem.lock(file1, mode = LockMode.Exclusive)

assertTrue(lock1.isValid)

val x2 = assertFailsWith<IOException> { fileSystem.lock(file1, mode = LockMode.Exclusive) }
assertLockFailure(x2, file1)

val x3 = assertFailsWith<IOException> { fileSystem.lock(file1, mode = LockMode.Shared) }
assertLockFailure(x3, file1)

lock1.close()

val lock4 = fileSystem.lock(file1, mode = LockMode.Exclusive)
assertTrue(lock4.isValid)
}

@Test
fun testSharedLock() {
if (!isSupported()) {
return
}

val lock1 = fileSystem.lock(file1, mode = LockMode.Shared)

assertTrue(lock1.isValid)
val x2 = assertFailsWith<IOException> { fileSystem.lock(file1, mode = LockMode.Exclusive) }
assertLockFailure(x2, file1)

val lock3 = if (isSharedLockSupported()) {
val lock3 = fileSystem.lock(file1, mode = LockMode.Shared)
assertTrue(lock3.isValid)
lock3
} else {
val x3 = assertFailsWith<IOException> { fileSystem.lock(file1, mode = LockMode.Shared) }
assertLockFailure(x3, file1)
null
}

assertTrue(lock1.isValid)

lock1.close()

if (lock3 != null) {
val x4 = assertFailsWith<IOException> { fileSystem.lock(file1, mode = LockMode.Exclusive) }
assertLockFailure(x4, file1)

lock3.close()
}

val lock5 = fileSystem.lock(file1, mode = LockMode.Exclusive)
assertTrue(lock5.isValid)
}

abstract fun assertLockFailure(ex: Exception, path: Path)
}
7 changes: 7 additions & 0 deletions okio/src/jsMain/kotlin/okio/FileSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ actual abstract class FileSystem : Closeable {
actual override fun close() {
}

actual open fun lock(
path: Path,
mode: LockMode,
): FileLock {
throw IOException("This file system does not support locking.")
}

actual companion object {
actual val SYSTEM_TEMPORARY_DIRECTORY: Path = tmpdir.toPath()
}
Expand Down
9 changes: 9 additions & 0 deletions okio/src/jvmMain/kotlin/okio/FileSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package okio

import java.nio.file.FileSystem as JavaNioFileSystem
import kotlin.Throws
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import okio.Path.Companion.toPath
Expand Down Expand Up @@ -143,6 +144,14 @@ actual abstract class FileSystem : Closeable {
actual override fun close() {
}

@Throws(IOException::class)
actual open fun lock(
path: Path,
mode: LockMode,
): FileLock {
throw IOException("This file system does not support locking.")
}

actual companion object {
/**
* The current process's host file system. Use this instance directly, or dependency inject a
Expand Down
Loading
Loading