From d56216aca9728d2c3c4c7ac8967445c5afb67ec0 Mon Sep 17 00:00:00 2001 From: MohammedKHC Date: Thu, 12 Feb 2026 13:07:06 +0200 Subject: [PATCH 1/5] Fix `FileHandle` thread safety on native platforms Replace no-op Lock with AtomicBoolean/AtomicInt for proper synchronization. --- .../commonMain/kotlin/okio/CommonPlatform.kt | 6 -- okio/src/commonMain/kotlin/okio/FileHandle.kt | 72 +++++++------------ okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt | 17 ----- .../nonJvmMain/kotlin/okio/NonJvmPlatform.kt | 18 ----- 4 files changed, 26 insertions(+), 87 deletions(-) diff --git a/okio/src/commonMain/kotlin/okio/CommonPlatform.kt b/okio/src/commonMain/kotlin/okio/CommonPlatform.kt index e073d0b342..3b56155f1e 100644 --- a/okio/src/commonMain/kotlin/okio/CommonPlatform.kt +++ b/okio/src/commonMain/kotlin/okio/CommonPlatform.kt @@ -26,12 +26,6 @@ internal expect fun String.asUtf8ToByteArray(): ByteArray // TODO make internal https://youtrack.jetbrains.com/issue/KT-37316 expect class ArrayIndexOutOfBoundsException(message: String?) : IndexOutOfBoundsException -expect class Lock - -expect inline fun Lock.withLock(action: () -> T): T - -internal expect fun newLock(): Lock - expect open class IOException(message: String?, cause: Throwable?) : Exception { constructor(message: String?) constructor() diff --git a/okio/src/commonMain/kotlin/okio/FileHandle.kt b/okio/src/commonMain/kotlin/okio/FileHandle.kt index d05c3c9939..4e540f0c14 100644 --- a/okio/src/commonMain/kotlin/okio/FileHandle.kt +++ b/okio/src/commonMain/kotlin/okio/FileHandle.kt @@ -15,6 +15,12 @@ */ package okio +import kotlin.concurrent.atomics.AtomicBoolean +import kotlin.concurrent.atomics.AtomicInt +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.concurrent.atomics.decrementAndFetch +import kotlin.concurrent.atomics.plusAssign + /** * An open file for reading and writing; using either streaming and random access. * @@ -32,6 +38,7 @@ package okio * File handles may be used by multiple threads concurrently. But the individual sources and sinks * produced by a file handle are not safe for concurrent use. */ +@OptIn(ExperimentalAtomicApi::class) abstract class FileHandle( /** * True if this handle supports both reading and writing. If this is false all write operations @@ -44,15 +51,13 @@ abstract class FileHandle( * True once the file handle is closed. Resources should be released with [protectedClose] once * this is true and [openStreamCount] is 0. */ - private var closed = false + private var closed = AtomicBoolean(false) /** * Reference count of the number of open sources and sinks on this file handle. Resources should * be released with [protectedClose] once this is 0 and [closed] is true. */ - private var openStreamCount = 0 - - val lock: Lock = newLock() + private var openStreamCount = AtomicInt(0) /** * Reads at least 1, and up to [byteCount] bytes from this starting at [fileOffset] and copies @@ -66,9 +71,7 @@ abstract class FileHandle( arrayOffset: Int, byteCount: Int, ): Int { - lock.withLock { - check(!closed) { "closed" } - } + check(!closed.load()) { "closed" } return protectedRead(fileOffset, array, arrayOffset, byteCount) } @@ -78,9 +81,7 @@ abstract class FileHandle( */ @Throws(IOException::class) fun read(fileOffset: Long, sink: Buffer, byteCount: Long): Long { - lock.withLock { - check(!closed) { "closed" } - } + check(!closed.load()) { "closed" } return readNoCloseCheck(fileOffset, sink, byteCount) } @@ -89,9 +90,7 @@ abstract class FileHandle( */ @Throws(IOException::class) fun size(): Long { - lock.withLock { - check(!closed) { "closed" } - } + check(!closed.load()) { "closed" } return protectedSize() } @@ -102,9 +101,7 @@ abstract class FileHandle( @Throws(IOException::class) fun resize(size: Long) { check(readWrite) { "file handle is read-only" } - lock.withLock { - check(!closed) { "closed" } - } + check(!closed.load()) { "closed" } return protectedResize(size) } @@ -116,9 +113,7 @@ abstract class FileHandle( byteCount: Int, ) { check(readWrite) { "file handle is read-only" } - lock.withLock { - check(!closed) { "closed" } - } + check(!closed.load()) { "closed" } return protectedWrite(fileOffset, array, arrayOffset, byteCount) } @@ -126,9 +121,7 @@ abstract class FileHandle( @Throws(IOException::class) fun write(fileOffset: Long, source: Buffer, byteCount: Long) { check(readWrite) { "file handle is read-only" } - lock.withLock { - check(!closed) { "closed" } - } + check(!closed.load()) { "closed" } writeNoCloseCheck(fileOffset, source, byteCount) } @@ -136,9 +129,7 @@ abstract class FileHandle( @Throws(IOException::class) fun flush() { check(readWrite) { "file handle is read-only" } - lock.withLock { - check(!closed) { "closed" } - } + check(!closed.load()) { "closed" } return protectedFlush() } @@ -148,10 +139,8 @@ abstract class FileHandle( */ @Throws(IOException::class) fun source(fileOffset: Long = 0L): Source { - lock.withLock { - check(!closed) { "closed" } - openStreamCount++ - } + check(!closed.load()) { "closed" } + openStreamCount += 1 return FileHandleSource(this, fileOffset) } @@ -218,10 +207,8 @@ abstract class FileHandle( @Throws(IOException::class) fun sink(fileOffset: Long = 0L): Sink { check(readWrite) { "file handle is read-only" } - lock.withLock { - check(!closed) { "closed" } - openStreamCount++ - } + check(!closed.load()) { "closed" } + openStreamCount += 1 return FileHandleSink(this, fileOffset) } @@ -284,11 +271,8 @@ abstract class FileHandle( @Throws(IOException::class) final override fun close() { - lock.withLock { - if (closed) return - closed = true - if (openStreamCount != 0) return - } + if (!closed.compareAndSet(expectedValue = false, newValue = true)) return + if (openStreamCount.load() != 0) return protectedClose() } @@ -407,10 +391,8 @@ abstract class FileHandle( override fun close() { if (closed) return closed = true - fileHandle.lock.withLock { - fileHandle.openStreamCount-- - if (fileHandle.openStreamCount != 0 || !fileHandle.closed) return@close - } + if (fileHandle.openStreamCount.decrementAndFetch() != 0) return + if (!fileHandle.closed.load()) return fileHandle.protectedClose() } } @@ -433,10 +415,8 @@ abstract class FileHandle( override fun close() { if (closed) return closed = true - fileHandle.lock.withLock { - fileHandle.openStreamCount-- - if (fileHandle.openStreamCount != 0 || !fileHandle.closed) return@close - } + if (fileHandle.openStreamCount.decrementAndFetch() != 0) return + if (!fileHandle.closed.load()) return fileHandle.protectedClose() } } diff --git a/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt b/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt index a95b242538..2ee2f13a18 100644 --- a/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt +++ b/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt @@ -16,11 +16,6 @@ */ package okio -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock as jvmWithLock -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - internal actual fun ByteArray.toUtf8String(): String = String(this, Charsets.UTF_8) internal actual fun String.asUtf8ToByteArray(): ByteArray = toByteArray(Charsets.UTF_8) @@ -28,18 +23,6 @@ internal actual fun String.asUtf8ToByteArray(): ByteArray = toByteArray(Charsets // TODO remove if https://youtrack.jetbrains.com/issue/KT-20641 provides a better solution actual typealias ArrayIndexOutOfBoundsException = java.lang.ArrayIndexOutOfBoundsException -actual typealias Lock = ReentrantLock - -internal actual fun newLock(): Lock = ReentrantLock() - -actual inline fun Lock.withLock(action: () -> T): T { - contract { - callsInPlace(action, InvocationKind.EXACTLY_ONCE) - } - - return jvmWithLock(action) -} - actual typealias IOException = java.io.IOException actual typealias ProtocolException = java.net.ProtocolException diff --git a/okio/src/nonJvmMain/kotlin/okio/NonJvmPlatform.kt b/okio/src/nonJvmMain/kotlin/okio/NonJvmPlatform.kt index ffe9253f4c..50b229b71b 100644 --- a/okio/src/nonJvmMain/kotlin/okio/NonJvmPlatform.kt +++ b/okio/src/nonJvmMain/kotlin/okio/NonJvmPlatform.kt @@ -16,8 +16,6 @@ package okio -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract import okio.internal.commonAsUtf8ToByteArray import okio.internal.commonToUtf8String @@ -31,22 +29,6 @@ actual open class ArrayIndexOutOfBoundsException actual constructor( message: String?, ) : IndexOutOfBoundsException(message) -actual class Lock { - companion object { - val instance = Lock() - } -} - -internal actual fun newLock(): Lock = Lock.instance - -actual inline fun Lock.withLock(action: () -> T): T { - contract { - callsInPlace(action, InvocationKind.EXACTLY_ONCE) - } - - return action() -} - actual open class IOException actual constructor( message: String?, cause: Throwable?, From a8e3a366243dc4f5a84c6dfc33b24df7c69f75e5 Mon Sep 17 00:00:00 2001 From: MohammedKHC Date: Thu, 12 Feb 2026 15:22:22 +0200 Subject: [PATCH 2/5] Implement `Lock` for native platforms. --- .../commonMain/kotlin/okio/CommonPlatform.kt | 6 ++ okio/src/commonMain/kotlin/okio/FileHandle.kt | 72 ++++++++++++------- okio/src/jsMain/kotlin/okio/JsPlatform.kt | 19 +++++ okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt | 17 +++++ .../kotlin/okio/WindowsPlatform.kt | 40 +++++++++++ okio/src/unixMain/kotlin/okio/UnixPlatform.kt | 41 +++++++++++ okio/src/wasmMain/kotlin/okio/WasmPlatform.kt | 21 ++++++ 7 files changed, 190 insertions(+), 26 deletions(-) create mode 100644 okio/src/mingwX64Main/kotlin/okio/WindowsPlatform.kt create mode 100644 okio/src/unixMain/kotlin/okio/UnixPlatform.kt diff --git a/okio/src/commonMain/kotlin/okio/CommonPlatform.kt b/okio/src/commonMain/kotlin/okio/CommonPlatform.kt index 3b56155f1e..e073d0b342 100644 --- a/okio/src/commonMain/kotlin/okio/CommonPlatform.kt +++ b/okio/src/commonMain/kotlin/okio/CommonPlatform.kt @@ -26,6 +26,12 @@ internal expect fun String.asUtf8ToByteArray(): ByteArray // TODO make internal https://youtrack.jetbrains.com/issue/KT-37316 expect class ArrayIndexOutOfBoundsException(message: String?) : IndexOutOfBoundsException +expect class Lock + +expect inline fun Lock.withLock(action: () -> T): T + +internal expect fun newLock(): Lock + expect open class IOException(message: String?, cause: Throwable?) : Exception { constructor(message: String?) constructor() diff --git a/okio/src/commonMain/kotlin/okio/FileHandle.kt b/okio/src/commonMain/kotlin/okio/FileHandle.kt index 4e540f0c14..d05c3c9939 100644 --- a/okio/src/commonMain/kotlin/okio/FileHandle.kt +++ b/okio/src/commonMain/kotlin/okio/FileHandle.kt @@ -15,12 +15,6 @@ */ package okio -import kotlin.concurrent.atomics.AtomicBoolean -import kotlin.concurrent.atomics.AtomicInt -import kotlin.concurrent.atomics.ExperimentalAtomicApi -import kotlin.concurrent.atomics.decrementAndFetch -import kotlin.concurrent.atomics.plusAssign - /** * An open file for reading and writing; using either streaming and random access. * @@ -38,7 +32,6 @@ import kotlin.concurrent.atomics.plusAssign * File handles may be used by multiple threads concurrently. But the individual sources and sinks * produced by a file handle are not safe for concurrent use. */ -@OptIn(ExperimentalAtomicApi::class) abstract class FileHandle( /** * True if this handle supports both reading and writing. If this is false all write operations @@ -51,13 +44,15 @@ abstract class FileHandle( * True once the file handle is closed. Resources should be released with [protectedClose] once * this is true and [openStreamCount] is 0. */ - private var closed = AtomicBoolean(false) + private var closed = false /** * Reference count of the number of open sources and sinks on this file handle. Resources should * be released with [protectedClose] once this is 0 and [closed] is true. */ - private var openStreamCount = AtomicInt(0) + private var openStreamCount = 0 + + val lock: Lock = newLock() /** * Reads at least 1, and up to [byteCount] bytes from this starting at [fileOffset] and copies @@ -71,7 +66,9 @@ abstract class FileHandle( arrayOffset: Int, byteCount: Int, ): Int { - check(!closed.load()) { "closed" } + lock.withLock { + check(!closed) { "closed" } + } return protectedRead(fileOffset, array, arrayOffset, byteCount) } @@ -81,7 +78,9 @@ abstract class FileHandle( */ @Throws(IOException::class) fun read(fileOffset: Long, sink: Buffer, byteCount: Long): Long { - check(!closed.load()) { "closed" } + lock.withLock { + check(!closed) { "closed" } + } return readNoCloseCheck(fileOffset, sink, byteCount) } @@ -90,7 +89,9 @@ abstract class FileHandle( */ @Throws(IOException::class) fun size(): Long { - check(!closed.load()) { "closed" } + lock.withLock { + check(!closed) { "closed" } + } return protectedSize() } @@ -101,7 +102,9 @@ abstract class FileHandle( @Throws(IOException::class) fun resize(size: Long) { check(readWrite) { "file handle is read-only" } - check(!closed.load()) { "closed" } + lock.withLock { + check(!closed) { "closed" } + } return protectedResize(size) } @@ -113,7 +116,9 @@ abstract class FileHandle( byteCount: Int, ) { check(readWrite) { "file handle is read-only" } - check(!closed.load()) { "closed" } + lock.withLock { + check(!closed) { "closed" } + } return protectedWrite(fileOffset, array, arrayOffset, byteCount) } @@ -121,7 +126,9 @@ abstract class FileHandle( @Throws(IOException::class) fun write(fileOffset: Long, source: Buffer, byteCount: Long) { check(readWrite) { "file handle is read-only" } - check(!closed.load()) { "closed" } + lock.withLock { + check(!closed) { "closed" } + } writeNoCloseCheck(fileOffset, source, byteCount) } @@ -129,7 +136,9 @@ abstract class FileHandle( @Throws(IOException::class) fun flush() { check(readWrite) { "file handle is read-only" } - check(!closed.load()) { "closed" } + lock.withLock { + check(!closed) { "closed" } + } return protectedFlush() } @@ -139,8 +148,10 @@ abstract class FileHandle( */ @Throws(IOException::class) fun source(fileOffset: Long = 0L): Source { - check(!closed.load()) { "closed" } - openStreamCount += 1 + lock.withLock { + check(!closed) { "closed" } + openStreamCount++ + } return FileHandleSource(this, fileOffset) } @@ -207,8 +218,10 @@ abstract class FileHandle( @Throws(IOException::class) fun sink(fileOffset: Long = 0L): Sink { check(readWrite) { "file handle is read-only" } - check(!closed.load()) { "closed" } - openStreamCount += 1 + lock.withLock { + check(!closed) { "closed" } + openStreamCount++ + } return FileHandleSink(this, fileOffset) } @@ -271,8 +284,11 @@ abstract class FileHandle( @Throws(IOException::class) final override fun close() { - if (!closed.compareAndSet(expectedValue = false, newValue = true)) return - if (openStreamCount.load() != 0) return + lock.withLock { + if (closed) return + closed = true + if (openStreamCount != 0) return + } protectedClose() } @@ -391,8 +407,10 @@ abstract class FileHandle( override fun close() { if (closed) return closed = true - if (fileHandle.openStreamCount.decrementAndFetch() != 0) return - if (!fileHandle.closed.load()) return + fileHandle.lock.withLock { + fileHandle.openStreamCount-- + if (fileHandle.openStreamCount != 0 || !fileHandle.closed) return@close + } fileHandle.protectedClose() } } @@ -415,8 +433,10 @@ abstract class FileHandle( override fun close() { if (closed) return closed = true - if (fileHandle.openStreamCount.decrementAndFetch() != 0) return - if (!fileHandle.closed.load()) return + fileHandle.lock.withLock { + fileHandle.openStreamCount-- + if (fileHandle.openStreamCount != 0 || !fileHandle.closed) return@close + } fileHandle.protectedClose() } } diff --git a/okio/src/jsMain/kotlin/okio/JsPlatform.kt b/okio/src/jsMain/kotlin/okio/JsPlatform.kt index 04350d157a..93a75c47cd 100644 --- a/okio/src/jsMain/kotlin/okio/JsPlatform.kt +++ b/okio/src/jsMain/kotlin/okio/JsPlatform.kt @@ -15,6 +15,9 @@ */ package okio +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + /* * This file exposes Node.js `os` and `path` APIs to Kotlin/JS, using reasonable default behaviors * if those symbols aren't available. @@ -50,3 +53,19 @@ private val path: dynamic internal val tmpdir: String get() = os?.tmpdir() as? String ?: "/tmp" + +actual class Lock { + companion object { + val instance = Lock() + } +} + +internal actual fun newLock(): Lock = Lock.instance + +actual inline fun Lock.withLock(action: () -> T): T { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + return action() +} diff --git a/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt b/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt index 2ee2f13a18..a95b242538 100644 --- a/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt +++ b/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt @@ -16,6 +16,11 @@ */ package okio +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock as jvmWithLock +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + internal actual fun ByteArray.toUtf8String(): String = String(this, Charsets.UTF_8) internal actual fun String.asUtf8ToByteArray(): ByteArray = toByteArray(Charsets.UTF_8) @@ -23,6 +28,18 @@ internal actual fun String.asUtf8ToByteArray(): ByteArray = toByteArray(Charsets // TODO remove if https://youtrack.jetbrains.com/issue/KT-20641 provides a better solution actual typealias ArrayIndexOutOfBoundsException = java.lang.ArrayIndexOutOfBoundsException +actual typealias Lock = ReentrantLock + +internal actual fun newLock(): Lock = ReentrantLock() + +actual inline fun Lock.withLock(action: () -> T): T { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + return jvmWithLock(action) +} + actual typealias IOException = java.io.IOException actual typealias ProtocolException = java.net.ProtocolException diff --git a/okio/src/mingwX64Main/kotlin/okio/WindowsPlatform.kt b/okio/src/mingwX64Main/kotlin/okio/WindowsPlatform.kt new file mode 100644 index 0000000000..7e238426a7 --- /dev/null +++ b/okio/src/mingwX64Main/kotlin/okio/WindowsPlatform.kt @@ -0,0 +1,40 @@ +package okio + +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.ref.createCleaner +import platform.windows.CloseHandle +import platform.windows.CreateMutexA +import platform.windows.INFINITE +import platform.windows.ReleaseMutex +import platform.windows.WaitForSingleObject + +actual class Lock { + val mutex = CreateMutexA( + null, + 0, + null + ) ?: throw lastErrorToIOException() + + @Suppress("unused") + @OptIn(ExperimentalNativeApi::class) + private val cleaner = createCleaner(mutex) { + CloseHandle(mutex) + } +} + +internal actual fun newLock(): Lock = Lock() + +actual inline fun Lock.withLock(action: () -> T): T { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + try { + WaitForSingleObject(mutex, INFINITE) + return action() + } finally { + ReleaseMutex(mutex) + } +} diff --git a/okio/src/unixMain/kotlin/okio/UnixPlatform.kt b/okio/src/unixMain/kotlin/okio/UnixPlatform.kt new file mode 100644 index 0000000000..1b7ad65487 --- /dev/null +++ b/okio/src/unixMain/kotlin/okio/UnixPlatform.kt @@ -0,0 +1,41 @@ +package okio + +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.ref.createCleaner +import kotlinx.cinterop.alloc +import kotlinx.cinterop.free +import kotlinx.cinterop.nativeHeap +import kotlinx.cinterop.ptr +import platform.posix.* + +actual class Lock { + val mutex = nativeHeap.alloc().apply { + if (pthread_mutex_init(ptr, null) != 0) { + throw errnoToIOException(errno) + } + } + + @Suppress("unused") + @OptIn(ExperimentalNativeApi::class) + private val cleaner = createCleaner(mutex) { + pthread_mutex_destroy(it.ptr) + nativeHeap.free(it) + } +} + +internal actual fun newLock(): Lock = Lock() + +actual inline fun Lock.withLock(action: () -> T): T { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + try { + pthread_mutex_lock(mutex.ptr) + return action() + } finally { + pthread_mutex_unlock(mutex.ptr) + } +} diff --git a/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt b/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt index 4a874ec826..e7d3d63052 100644 --- a/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt +++ b/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt @@ -15,5 +15,26 @@ */ package okio +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + internal actual val PLATFORM_DIRECTORY_SEPARATOR: String get() = "/" + +actual class Lock { + companion object { + val instance = Lock() + } +} + +internal actual fun newLock(): Lock = Lock.instance + +@OptIn(ExperimentalContracts::class) +actual inline fun Lock.withLock(action: () -> T): T { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + return action() +} From 6868148eba684300592255b5f988a26399edbfdf Mon Sep 17 00:00:00 2001 From: MohammedKHC Date: Fri, 13 Feb 2026 21:30:33 +0200 Subject: [PATCH 3/5] Add square copyright notice to new files. --- .../mingwX64Main/kotlin/okio/WindowsPlatform.kt | 15 +++++++++++++++ okio/src/unixMain/kotlin/okio/UnixPlatform.kt | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/okio/src/mingwX64Main/kotlin/okio/WindowsPlatform.kt b/okio/src/mingwX64Main/kotlin/okio/WindowsPlatform.kt index 7e238426a7..f329c36225 100644 --- a/okio/src/mingwX64Main/kotlin/okio/WindowsPlatform.kt +++ b/okio/src/mingwX64Main/kotlin/okio/WindowsPlatform.kt @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2026 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.contracts.InvocationKind diff --git a/okio/src/unixMain/kotlin/okio/UnixPlatform.kt b/okio/src/unixMain/kotlin/okio/UnixPlatform.kt index 1b7ad65487..cf0ae7f952 100644 --- a/okio/src/unixMain/kotlin/okio/UnixPlatform.kt +++ b/okio/src/unixMain/kotlin/okio/UnixPlatform.kt @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2026 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.contracts.InvocationKind From 6450cde64fa4f5e2ced1de4cdb4768028e020051 Mon Sep 17 00:00:00 2001 From: Mohammed Khaled Date: Thu, 19 Feb 2026 04:21:01 +0000 Subject: [PATCH 4/5] Apply requested changes. --- okio/src/jsMain/kotlin/okio/JsPlatform.kt | 8 ++------ okio/src/unixMain/kotlin/okio/UnixPlatform.kt | 7 ++++++- okio/src/wasmMain/kotlin/okio/WasmPlatform.kt | 8 ++------ 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/okio/src/jsMain/kotlin/okio/JsPlatform.kt b/okio/src/jsMain/kotlin/okio/JsPlatform.kt index 93a75c47cd..fe31c4c23a 100644 --- a/okio/src/jsMain/kotlin/okio/JsPlatform.kt +++ b/okio/src/jsMain/kotlin/okio/JsPlatform.kt @@ -54,13 +54,9 @@ private val path: dynamic internal val tmpdir: String get() = os?.tmpdir() as? String ?: "/tmp" -actual class Lock { - companion object { - val instance = Lock() - } -} +actual typealias Lock = Unit -internal actual fun newLock(): Lock = Lock.instance +internal actual fun newLock(): Lock = Unit actual inline fun Lock.withLock(action: () -> T): T { contract { diff --git a/okio/src/unixMain/kotlin/okio/UnixPlatform.kt b/okio/src/unixMain/kotlin/okio/UnixPlatform.kt index cf0ae7f952..d207fc3e7e 100644 --- a/okio/src/unixMain/kotlin/okio/UnixPlatform.kt +++ b/okio/src/unixMain/kotlin/okio/UnixPlatform.kt @@ -23,7 +23,12 @@ import kotlinx.cinterop.alloc import kotlinx.cinterop.free import kotlinx.cinterop.nativeHeap import kotlinx.cinterop.ptr -import platform.posix.* +import platform.posix.errno +import platform.posix.pthread_mutex_destroy +import platform.posix.pthread_mutex_init +import platform.posix.pthread_mutex_lock +import platform.posix.pthread_mutex_t +import platform.posix.pthread_mutex_unlock actual class Lock { val mutex = nativeHeap.alloc().apply { diff --git a/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt b/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt index e7d3d63052..50004f3bac 100644 --- a/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt +++ b/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt @@ -22,13 +22,9 @@ import kotlin.contracts.contract internal actual val PLATFORM_DIRECTORY_SEPARATOR: String get() = "/" -actual class Lock { - companion object { - val instance = Lock() - } -} +actual typealias Lock = Unit -internal actual fun newLock(): Lock = Lock.instance +internal actual fun newLock(): Lock = Unit @OptIn(ExperimentalContracts::class) actual inline fun Lock.withLock(action: () -> T): T { From f56aae25780bb77c13a3fed4e142a97e8c83ba5b Mon Sep 17 00:00:00 2001 From: MohammedKHC Date: Thu, 19 Feb 2026 07:43:57 +0200 Subject: [PATCH 5/5] Don't use createCleaner for native Lock. --- okio/src/commonMain/kotlin/okio/CommonPlatform.kt | 1 + okio/src/commonMain/kotlin/okio/FileHandle.kt | 1 + okio/src/jsMain/kotlin/okio/JsPlatform.kt | 1 + okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt | 1 + .../src/mingwX64Main/kotlin/okio/WindowsPlatform.kt | 9 +++------ okio/src/unixMain/kotlin/okio/UnixPlatform.kt | 13 +++++-------- okio/src/wasmMain/kotlin/okio/WasmPlatform.kt | 1 + 7 files changed, 13 insertions(+), 14 deletions(-) diff --git a/okio/src/commonMain/kotlin/okio/CommonPlatform.kt b/okio/src/commonMain/kotlin/okio/CommonPlatform.kt index e073d0b342..308fd1396b 100644 --- a/okio/src/commonMain/kotlin/okio/CommonPlatform.kt +++ b/okio/src/commonMain/kotlin/okio/CommonPlatform.kt @@ -31,6 +31,7 @@ expect class Lock expect inline fun Lock.withLock(action: () -> T): T internal expect fun newLock(): Lock +internal expect inline fun Lock.destroy() expect open class IOException(message: String?, cause: Throwable?) : Exception { constructor(message: String?) diff --git a/okio/src/commonMain/kotlin/okio/FileHandle.kt b/okio/src/commonMain/kotlin/okio/FileHandle.kt index d05c3c9939..385487f263 100644 --- a/okio/src/commonMain/kotlin/okio/FileHandle.kt +++ b/okio/src/commonMain/kotlin/okio/FileHandle.kt @@ -289,6 +289,7 @@ abstract class FileHandle( closed = true if (openStreamCount != 0) return } + lock.destroy() protectedClose() } diff --git a/okio/src/jsMain/kotlin/okio/JsPlatform.kt b/okio/src/jsMain/kotlin/okio/JsPlatform.kt index fe31c4c23a..6205c8009f 100644 --- a/okio/src/jsMain/kotlin/okio/JsPlatform.kt +++ b/okio/src/jsMain/kotlin/okio/JsPlatform.kt @@ -57,6 +57,7 @@ internal val tmpdir: String actual typealias Lock = Unit internal actual fun newLock(): Lock = Unit +internal actual inline fun Lock.destroy() = Unit actual inline fun Lock.withLock(action: () -> T): T { contract { diff --git a/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt b/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt index a95b242538..431f81cbaa 100644 --- a/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt +++ b/okio/src/jvmMain/kotlin/okio/-JvmPlatform.kt @@ -31,6 +31,7 @@ actual typealias ArrayIndexOutOfBoundsException = java.lang.ArrayIndexOutOfBound actual typealias Lock = ReentrantLock internal actual fun newLock(): Lock = ReentrantLock() +internal actual inline fun Lock.destroy() = Unit actual inline fun Lock.withLock(action: () -> T): T { contract { diff --git a/okio/src/mingwX64Main/kotlin/okio/WindowsPlatform.kt b/okio/src/mingwX64Main/kotlin/okio/WindowsPlatform.kt index f329c36225..945461cf94 100644 --- a/okio/src/mingwX64Main/kotlin/okio/WindowsPlatform.kt +++ b/okio/src/mingwX64Main/kotlin/okio/WindowsPlatform.kt @@ -17,29 +17,26 @@ package okio import kotlin.contracts.InvocationKind import kotlin.contracts.contract -import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.ref.createCleaner import platform.windows.CloseHandle import platform.windows.CreateMutexA import platform.windows.INFINITE import platform.windows.ReleaseMutex import platform.windows.WaitForSingleObject -actual class Lock { +actual class Lock : Closeable { val mutex = CreateMutexA( null, 0, null ) ?: throw lastErrorToIOException() - @Suppress("unused") - @OptIn(ExperimentalNativeApi::class) - private val cleaner = createCleaner(mutex) { + override fun close() { CloseHandle(mutex) } } internal actual fun newLock(): Lock = Lock() +internal actual inline fun Lock.destroy() = close() actual inline fun Lock.withLock(action: () -> T): T { contract { diff --git a/okio/src/unixMain/kotlin/okio/UnixPlatform.kt b/okio/src/unixMain/kotlin/okio/UnixPlatform.kt index d207fc3e7e..4bc3ddfca4 100644 --- a/okio/src/unixMain/kotlin/okio/UnixPlatform.kt +++ b/okio/src/unixMain/kotlin/okio/UnixPlatform.kt @@ -17,8 +17,6 @@ package okio import kotlin.contracts.InvocationKind import kotlin.contracts.contract -import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.ref.createCleaner import kotlinx.cinterop.alloc import kotlinx.cinterop.free import kotlinx.cinterop.nativeHeap @@ -30,22 +28,21 @@ import platform.posix.pthread_mutex_lock import platform.posix.pthread_mutex_t import platform.posix.pthread_mutex_unlock -actual class Lock { +actual class Lock : Closeable { val mutex = nativeHeap.alloc().apply { if (pthread_mutex_init(ptr, null) != 0) { throw errnoToIOException(errno) } } - @Suppress("unused") - @OptIn(ExperimentalNativeApi::class) - private val cleaner = createCleaner(mutex) { - pthread_mutex_destroy(it.ptr) - nativeHeap.free(it) + override fun close() { + pthread_mutex_destroy(mutex.ptr) + nativeHeap.free(mutex) } } internal actual fun newLock(): Lock = Lock() +internal actual inline fun Lock.destroy() = close() actual inline fun Lock.withLock(action: () -> T): T { contract { diff --git a/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt b/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt index 50004f3bac..fa54b701b9 100644 --- a/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt +++ b/okio/src/wasmMain/kotlin/okio/WasmPlatform.kt @@ -25,6 +25,7 @@ internal actual val PLATFORM_DIRECTORY_SEPARATOR: String actual typealias Lock = Unit internal actual fun newLock(): Lock = Unit +internal actual inline fun Lock.destroy() = Unit @OptIn(ExperimentalContracts::class) actual inline fun Lock.withLock(action: () -> T): T {