From e0548f278bec7d14f6405d891468bb09fd92b97a Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 21 Jun 2022 01:00:00 -0600 Subject: [PATCH 01/23] NSInputStream.source() and BufferedSource.inputStream(): NSInputStream --- .../appleMain/kotlin/okio/BufferedSource.kt | 58 ++++++++++++ okio/src/appleMain/kotlin/okio/Source.kt | 61 ++++++++++++ .../kotlin/okio/AppleBufferedSourceTest.kt | 44 +++++++++ .../appleTest/kotlin/okio/AppleSourceTest.kt | 94 +++++++++++++++++++ 4 files changed, 257 insertions(+) create mode 100644 okio/src/appleMain/kotlin/okio/BufferedSource.kt create mode 100644 okio/src/appleMain/kotlin/okio/Source.kt create mode 100644 okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt create mode 100644 okio/src/appleTest/kotlin/okio/AppleSourceTest.kt diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt new file mode 100644 index 0000000000..758e6b6089 --- /dev/null +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import kotlinx.cinterop.* +import platform.Foundation.NSData +import platform.Foundation.NSInputStream +import platform.darwin.NSInteger +import platform.darwin.NSUInteger +import platform.darwin.NSUIntegerVar +import platform.posix.memcpy +import platform.posix.uint8_tVar + +@OptIn(UnsafeNumber::class) +fun BufferedSource.inputStream(): NSInputStream { + return object : NSInputStream(NSData()) { + override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { + val bytes = ByteArray(maxLength.toInt()) + val read = this@inputStream.read(bytes, 0, maxLength.toInt()) + return if (read > 0) { + bytes.usePinned { + memcpy(buffer, it.addressOf(0), read.toULong()) + } + read.toLong() + } else { + 0 + } + } + + override fun getBuffer( + buffer: CPointer>?, + length: CPointer? + ): Boolean { + return false + } + + override fun hasBytesAvailable(): Boolean { + return buffer.size > 0 + } + + override fun close() = this@inputStream.close() + + override fun description(): String = "${this@inputStream}.inputStream()" + } +} diff --git a/okio/src/appleMain/kotlin/okio/Source.kt b/okio/src/appleMain/kotlin/okio/Source.kt new file mode 100644 index 0000000000..afcdd3f3a5 --- /dev/null +++ b/okio/src/appleMain/kotlin/okio/Source.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import kotlinx.cinterop.* +import platform.Foundation.NSInputStream +import platform.darwin.UInt8Var + +fun NSInputStream.source(): Source = NSInputStreamSource(this) + +private open class NSInputStreamSource( + val input: NSInputStream +) : Source { + + init { + input.open() + } + + @OptIn(UnsafeNumber::class) + override fun read(sink: Buffer, byteCount: Long): Long { + if (byteCount == 0L) return 0L + require(byteCount >= 0L) { "byteCount < 0: $byteCount" } + val tail = sink.writableSegment(1) + val maxToCopy = minOf(byteCount, Segment.SIZE - tail.limit) + val bytesRead = tail.data.usePinned { + val bytes = it.addressOf(tail.limit).reinterpret() + input.read(bytes, maxToCopy.toULong()) + } + if (bytesRead < 0) throw IOException(input.streamError?.localizedDescription) + if (bytesRead == 0L) { + if (tail.pos == tail.limit) { + // We allocated a tail segment, but didn't end up needing it. Recycle! + sink.head = tail.pop() + SegmentPool.recycle(tail) + } + return -1 + } + tail.limit += bytesRead.toInt() + sink.size += bytesRead + return bytesRead + } + + override fun close() = input.close() + + override fun timeout() = Timeout.NONE + + override fun toString() = "source($input)" +} diff --git a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt new file mode 100644 index 0000000000..1a98c6ce33 --- /dev/null +++ b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.usePinned +import platform.darwin.UInt8Var + +class AppleBufferedSourceTest { + @Test fun bufferInputStream() { + val source = Buffer() + source.writeUtf8("abc") + + val byteArray = ByteArray(4) + byteArray.usePinned { + val cPtr = it.addressOf(0).reinterpret() + + byteArray.fill(-5) + val nsis = source.inputStream() + assertEquals(3, nsis.read(cPtr, 4).toLong()) + assertEquals("[97, 98, 99, -5]", byteArray.contentToString()) + + byteArray.fill(-7) + assertEquals(0, nsis.read(cPtr, 4).toLong()) + assertEquals("[-7, -7, -7, -7]", byteArray.contentToString()) + } + } +} diff --git a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt new file mode 100644 index 0000000000..f7f1d8afef --- /dev/null +++ b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail +import kotlinx.cinterop.allocArrayOf +import kotlinx.cinterop.memScoped +import platform.Foundation.NSData +import platform.Foundation.NSInputStream +import platform.Foundation.create + +class AppleSourceTest { + + @Test fun nsInputStreamSource() { + val nsis = NSInputStream(byteArrayOf(0x61).toNSData()) + val source = nsis.source() + val buffer = Buffer() + source.read(buffer, 1) + assertEquals("a", buffer.readUtf8()) + } + + @Test fun sourceFromInputStream() { + val nsis = NSInputStream( + ("a" + "b".repeat(SEGMENT_SIZE * 2) + "c").encodeToByteArray().toNSData() + ) + + // Source: ab...bc + val source: Source = nsis.source() + val sink = Buffer() + + // Source: b...bc. Sink: abb. + assertEquals(3, source.read(sink, 3)) + assertEquals("abb", sink.readUtf8(3)) + + // Source: b...bc. Sink: b...b. + assertEquals(SEGMENT_SIZE.toLong(), source.read(sink, 20000)) + assertEquals("b".repeat(SEGMENT_SIZE), sink.readUtf8()) + + // Source: b...bc. Sink: b...bc. + assertEquals((SEGMENT_SIZE - 1).toLong(), source.read(sink, 20000)) + assertEquals("b".repeat(SEGMENT_SIZE - 2) + "c", sink.readUtf8()) + + // Source and sink are empty. + assertEquals(-1, source.read(sink, 1)) + } + + @Test fun sourceFromInputStreamWithSegmentSize() { + val nsis = NSInputStream(ByteArray(SEGMENT_SIZE).toNSData()) + val source = nsis.source() + val sink = Buffer() + + assertEquals(SEGMENT_SIZE.toLong(), source.read(sink, SEGMENT_SIZE.toLong())) + assertEquals(-1, source.read(sink, SEGMENT_SIZE.toLong())) + + assertNoEmptySegments(sink) + } + + @Test fun sourceFromInputStreamBounds() { + val source = NSInputStream(ByteArray(100).toNSData()).source() + try { + source.read(Buffer(), -1) + fail() + } catch (expected: IllegalArgumentException) { + } + } + + private fun ByteArray.toNSData(): NSData = memScoped { + NSData.create(bytes = allocArrayOf(this@toNSData), length = size.toULong()) + } + + private fun assertNoEmptySegments(buffer: Buffer) { + assertTrue(segmentSizes(buffer).all { it != 0 }, "Expected all segments to be non-empty") + } + + companion object { + const val SEGMENT_SIZE = Segment.SIZE + } +} From 5578fd88d412771a9701984362940f6b35f305bd Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 21 Jun 2022 10:47:16 -0600 Subject: [PATCH 02/23] Set streamError and return -1 on read failure --- .../appleMain/kotlin/okio/BufferedSource.kt | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index 758e6b6089..f950c24e9c 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -16,8 +16,7 @@ package okio import kotlinx.cinterop.* -import platform.Foundation.NSData -import platform.Foundation.NSInputStream +import platform.Foundation.* import platform.darwin.NSInteger import platform.darwin.NSUInteger import platform.darwin.NSUIntegerVar @@ -27,17 +26,39 @@ import platform.posix.uint8_tVar @OptIn(UnsafeNumber::class) fun BufferedSource.inputStream(): NSInputStream { return object : NSInputStream(NSData()) { + + private var error: NSError? = null + + private fun Exception.toNSError(): NSError { + return NSError( + "Kotlin", + 0, + mapOf( + NSLocalizedDescriptionKey to message, + NSUnderlyingErrorKey to this + ) + ) + } + override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { val bytes = ByteArray(maxLength.toInt()) - val read = this@inputStream.read(bytes, 0, maxLength.toInt()) - return if (read > 0) { + val read = try { + this@inputStream.read(bytes, 0, maxLength.toInt()) + } catch (e: Exception) { + error = e.toNSError() + return -1 + } + if (read > 0) { bytes.usePinned { memcpy(buffer, it.addressOf(0), read.toULong()) } - read.toLong() - } else { - 0 + return read.toLong() } + return 0 + } + + override fun streamError(): NSError? { + return error } override fun getBuffer( From b95de575b29fb521656b52009beb01f7a5706a6f Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 21 Jun 2022 12:28:16 -0600 Subject: [PATCH 03/23] Implement NSInputStream.getBuffer() --- .../appleMain/kotlin/okio/BufferedSource.kt | 19 +++++++---- .../kotlin/okio/AppleBufferedSourceTest.kt | 33 +++++++++++++++++-- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index f950c24e9c..81528586a7 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -40,6 +40,8 @@ fun BufferedSource.inputStream(): NSInputStream { ) } + override fun streamError(): NSError? = error + override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { val bytes = ByteArray(maxLength.toInt()) val read = try { @@ -57,20 +59,23 @@ fun BufferedSource.inputStream(): NSInputStream { return 0 } - override fun streamError(): NSError? { - return error - } - override fun getBuffer( buffer: CPointer>?, length: CPointer? ): Boolean { + if (this@inputStream.buffer.size > 0) { + this@inputStream.buffer.head?.let { s -> + s.data.usePinned { + buffer?.pointed?.value = it.addressOf(s.pos).reinterpret() + length?.pointed?.value = (s.limit - s.pos).toULong() + return true + } + } + } return false } - override fun hasBytesAvailable(): Boolean { - return buffer.size > 0 - } + override fun hasBytesAvailable(): Boolean = buffer.size > 0 override fun close() = this@inputStream.close() diff --git a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt index 1a98c6ce33..1339c083df 100644 --- a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt @@ -17,11 +17,14 @@ package okio import kotlin.test.Test import kotlin.test.assertEquals -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.reinterpret -import kotlinx.cinterop.usePinned +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlinx.cinterop.* +import platform.darwin.NSUInteger +import platform.darwin.NSUIntegerVar import platform.darwin.UInt8Var +@OptIn(UnsafeNumber::class) class AppleBufferedSourceTest { @Test fun bufferInputStream() { val source = Buffer() @@ -41,4 +44,28 @@ class AppleBufferedSourceTest { assertEquals("[-7, -7, -7, -7]", byteArray.contentToString()) } } + + @Test fun nsInputStreamGetBuffer() { + val source = Buffer() + source.writeUtf8("abc") + + val nsis = source.inputStream() + assertTrue(nsis.hasBytesAvailable) + + memScoped { + val bufferPtr = alloc>() + val lengthPtr = alloc() + assertTrue(nsis.getBuffer(bufferPtr.ptr, lengthPtr.ptr)) + + val length = lengthPtr.value + assertNotNull(length) + assertEquals(3UL, length) + + val buffer = bufferPtr.value + assertNotNull(buffer) + assertEquals('a'.code.toUByte(), buffer[0]) + assertEquals('b'.code.toUByte(), buffer[1]) + assertEquals('c'.code.toUByte(), buffer[2]) + } + } } From ddb66c3afab8b978194360cfe1e745e58c1cb394 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 21 Jun 2022 14:54:40 -0600 Subject: [PATCH 04/23] convert() native types --- okio/src/appleMain/kotlin/okio/BufferedSource.kt | 6 +++--- okio/src/appleMain/kotlin/okio/Source.kt | 4 ++-- okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt | 6 +++--- okio/src/appleTest/kotlin/okio/AppleSourceTest.kt | 5 ++++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index 81528586a7..3251f04b9f 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -52,9 +52,9 @@ fun BufferedSource.inputStream(): NSInputStream { } if (read > 0) { bytes.usePinned { - memcpy(buffer, it.addressOf(0), read.toULong()) + memcpy(buffer, it.addressOf(0), read.convert()) } - return read.toLong() + return read.convert() } return 0 } @@ -67,7 +67,7 @@ fun BufferedSource.inputStream(): NSInputStream { this@inputStream.buffer.head?.let { s -> s.data.usePinned { buffer?.pointed?.value = it.addressOf(s.pos).reinterpret() - length?.pointed?.value = (s.limit - s.pos).toULong() + length?.pointed?.value = (s.limit - s.pos).convert() return true } } diff --git a/okio/src/appleMain/kotlin/okio/Source.kt b/okio/src/appleMain/kotlin/okio/Source.kt index afcdd3f3a5..558f0a430e 100644 --- a/okio/src/appleMain/kotlin/okio/Source.kt +++ b/okio/src/appleMain/kotlin/okio/Source.kt @@ -37,7 +37,7 @@ private open class NSInputStreamSource( val maxToCopy = minOf(byteCount, Segment.SIZE - tail.limit) val bytesRead = tail.data.usePinned { val bytes = it.addressOf(tail.limit).reinterpret() - input.read(bytes, maxToCopy.toULong()) + input.read(bytes, maxToCopy.convert()).toLong() } if (bytesRead < 0) throw IOException(input.streamError?.localizedDescription) if (bytesRead == 0L) { @@ -50,7 +50,7 @@ private open class NSInputStreamSource( } tail.limit += bytesRead.toInt() sink.size += bytesRead - return bytesRead + return bytesRead.convert() } override fun close() = input.close() diff --git a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt index 1339c083df..125756d0ce 100644 --- a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt @@ -36,11 +36,11 @@ class AppleBufferedSourceTest { byteArray.fill(-5) val nsis = source.inputStream() - assertEquals(3, nsis.read(cPtr, 4).toLong()) + assertEquals(3, nsis.read(cPtr, 4)) assertEquals("[97, 98, 99, -5]", byteArray.contentToString()) byteArray.fill(-7) - assertEquals(0, nsis.read(cPtr, 4).toLong()) + assertEquals(0, nsis.read(cPtr, 4)) assertEquals("[-7, -7, -7, -7]", byteArray.contentToString()) } } @@ -59,7 +59,7 @@ class AppleBufferedSourceTest { val length = lengthPtr.value assertNotNull(length) - assertEquals(3UL, length) + assertEquals(3.convert(), length) val buffer = bufferPtr.value assertNotNull(buffer) diff --git a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt index f7f1d8afef..9ac161ed2d 100644 --- a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt @@ -19,12 +19,15 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue import kotlin.test.fail +import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.allocArrayOf +import kotlinx.cinterop.convert import kotlinx.cinterop.memScoped import platform.Foundation.NSData import platform.Foundation.NSInputStream import platform.Foundation.create +@OptIn(UnsafeNumber::class) class AppleSourceTest { @Test fun nsInputStreamSource() { @@ -81,7 +84,7 @@ class AppleSourceTest { } private fun ByteArray.toNSData(): NSData = memScoped { - NSData.create(bytes = allocArrayOf(this@toNSData), length = size.toULong()) + NSData.create(bytes = allocArrayOf(this@toNSData), length = size.convert()) } private fun assertNoEmptySegments(buffer: Buffer) { From a6c3e169f1d0439fbe97eb5108d8604edc6974ab Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 21 Jun 2022 14:55:24 -0600 Subject: [PATCH 05/23] Test NSInputStream.close() --- .../kotlin/okio/AppleBufferedSourceTest.kt | 26 +++++++++++++++---- .../okio/internal/-RealBufferedSource.kt | 1 + 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt index 125756d0ce..2b2534bb8d 100644 --- a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt @@ -15,12 +15,8 @@ */ package okio -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue +import kotlin.test.* import kotlinx.cinterop.* -import platform.darwin.NSUInteger import platform.darwin.NSUIntegerVar import platform.darwin.UInt8Var @@ -68,4 +64,24 @@ class AppleBufferedSourceTest { assertEquals('c'.code.toUByte(), buffer[2]) } } + + @Test fun nsInputStreamClose() { + val buffer = Buffer() + buffer.writeUtf8("abc") + val source = RealBufferedSource(buffer) + assertFalse(source.closed) + + val nsis = source.inputStream() + nsis.close() + assertTrue(source.closed) + + val byteArray = ByteArray(4) + byteArray.usePinned { + val cPtr = it.addressOf(0).reinterpret() + + byteArray.fill(-5) + assertEquals(-1, nsis.read(cPtr, 4)) + assertEquals("[-5, -5, -5, -5]", byteArray.contentToString()) + } + } } diff --git a/okio/src/commonMain/kotlin/okio/internal/-RealBufferedSource.kt b/okio/src/commonMain/kotlin/okio/internal/-RealBufferedSource.kt index 8df646c743..3699fd06d3 100644 --- a/okio/src/commonMain/kotlin/okio/internal/-RealBufferedSource.kt +++ b/okio/src/commonMain/kotlin/okio/internal/-RealBufferedSource.kt @@ -129,6 +129,7 @@ internal inline fun RealBufferedSource.commonReadFully(sink: ByteArray) { internal inline fun RealBufferedSource.commonRead(sink: ByteArray, offset: Int, byteCount: Int): Int { checkOffsetAndCount(sink.size.toLong(), offset.toLong(), byteCount.toLong()) + check(!closed) { "closed" } if (buffer.size == 0L) { val read = source.read(buffer, Segment.SIZE.toLong()) From 7a8c292b179aec9a7487153f98d9435f9f62b69f Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 22 Jun 2022 10:44:55 -0600 Subject: [PATCH 06/23] Additional test assertions --- okio/src/appleMain/kotlin/okio/Source.kt | 4 ++-- okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt | 2 ++ okio/src/appleTest/kotlin/okio/AppleSourceTest.kt | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/Source.kt b/okio/src/appleMain/kotlin/okio/Source.kt index 558f0a430e..9c8f19fd89 100644 --- a/okio/src/appleMain/kotlin/okio/Source.kt +++ b/okio/src/appleMain/kotlin/okio/Source.kt @@ -21,15 +21,15 @@ import platform.darwin.UInt8Var fun NSInputStream.source(): Source = NSInputStreamSource(this) +@OptIn(UnsafeNumber::class) private open class NSInputStreamSource( - val input: NSInputStream + private val input: NSInputStream ) : Source { init { input.open() } - @OptIn(UnsafeNumber::class) override fun read(sink: Buffer, byteCount: Long): Long { if (byteCount == 0L) return 0L require(byteCount >= 0L) { "byteCount < 0: $byteCount" } diff --git a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt index 2b2534bb8d..4a0f230e94 100644 --- a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt @@ -81,6 +81,8 @@ class AppleBufferedSourceTest { byteArray.fill(-5) assertEquals(-1, nsis.read(cPtr, 4)) + assertNotNull(nsis.streamError) + assertEquals("closed", nsis.streamError?.localizedDescription) assertEquals("[-5, -5, -5, -5]", byteArray.contentToString()) } } diff --git a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt index 9ac161ed2d..efcd0e5b1e 100644 --- a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt @@ -29,7 +29,6 @@ import platform.Foundation.create @OptIn(UnsafeNumber::class) class AppleSourceTest { - @Test fun nsInputStreamSource() { val nsis = NSInputStream(byteArrayOf(0x61).toNSData()) val source = nsis.source() From b8d1ba949b78002bd1a0c86985b087f73c0c0830 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 22 Jun 2022 10:52:21 -0600 Subject: [PATCH 07/23] appleTest TestUtil --- .../kotlin/okio/AppleByteStringTest.kt | 2 ++ .../appleTest/kotlin/okio/AppleSourceTest.kt | 8 -------- okio/src/appleTest/kotlin/okio/TestUtil.kt | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 okio/src/appleTest/kotlin/okio/TestUtil.kt diff --git a/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt b/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt index 15acf0b4ad..e254a07d5c 100644 --- a/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt @@ -21,7 +21,9 @@ import platform.Foundation.NSUTF8StringEncoding import platform.Foundation.dataUsingEncoding import kotlin.test.Test import kotlin.test.assertEquals +import kotlinx.cinterop.UnsafeNumber +@OptIn(UnsafeNumber::class) class AppleByteStringTest { @Test fun nsDataToByteString() { val data = ("Hello" as NSString).dataUsingEncoding(NSUTF8StringEncoding) as NSData diff --git a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt index efcd0e5b1e..326c2861bc 100644 --- a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt @@ -82,14 +82,6 @@ class AppleSourceTest { } } - private fun ByteArray.toNSData(): NSData = memScoped { - NSData.create(bytes = allocArrayOf(this@toNSData), length = size.convert()) - } - - private fun assertNoEmptySegments(buffer: Buffer) { - assertTrue(segmentSizes(buffer).all { it != 0 }, "Expected all segments to be non-empty") - } - companion object { const val SEGMENT_SIZE = Segment.SIZE } diff --git a/okio/src/appleTest/kotlin/okio/TestUtil.kt b/okio/src/appleTest/kotlin/okio/TestUtil.kt new file mode 100644 index 0000000000..0af6e42d7c --- /dev/null +++ b/okio/src/appleTest/kotlin/okio/TestUtil.kt @@ -0,0 +1,20 @@ +@file:OptIn(UnsafeNumber::class) + +package okio + +import kotlin.test.assertTrue +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.allocArrayOf +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import platform.Foundation.NSData +import platform.Foundation.create + + +fun ByteArray.toNSData(): NSData = memScoped { + NSData.create(bytes = allocArrayOf(this@toNSData), length = size.convert()) +} + +fun assertNoEmptySegments(buffer: Buffer) { + assertTrue(segmentSizes(buffer).all { it != 0 }, "Expected all segments to be non-empty") +} From c319152fa18ac5594d93af78b619b3ddb5a8cbf1 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 22 Jun 2022 13:03:06 -0600 Subject: [PATCH 08/23] Add doc comments --- okio/src/appleMain/kotlin/okio/BufferedSource.kt | 1 + okio/src/appleMain/kotlin/okio/Source.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index 3251f04b9f..9999e6dbc0 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -23,6 +23,7 @@ import platform.darwin.NSUIntegerVar import platform.posix.memcpy import platform.posix.uint8_tVar +/** Returns an input stream that reads from this source. */ @OptIn(UnsafeNumber::class) fun BufferedSource.inputStream(): NSInputStream { return object : NSInputStream(NSData()) { diff --git a/okio/src/appleMain/kotlin/okio/Source.kt b/okio/src/appleMain/kotlin/okio/Source.kt index 9c8f19fd89..d4fc50a67d 100644 --- a/okio/src/appleMain/kotlin/okio/Source.kt +++ b/okio/src/appleMain/kotlin/okio/Source.kt @@ -19,6 +19,7 @@ import kotlinx.cinterop.* import platform.Foundation.NSInputStream import platform.darwin.UInt8Var +/** Returns a source that reads from `in`. */ fun NSInputStream.source(): Source = NSInputStreamSource(this) @OptIn(UnsafeNumber::class) From 506fa4f3a0d0ef38286f74fa939ab4aa2c0f06c6 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Wed, 22 Jun 2022 14:27:18 -0600 Subject: [PATCH 09/23] Resolve checks --- .../appleMain/kotlin/okio/BufferedSource.kt | 16 ++++++++++++++-- okio/src/appleMain/kotlin/okio/Source.kt | 6 +++++- .../kotlin/okio/AppleBufferedSourceTest.kt | 18 ++++++++++++++++-- .../kotlin/okio/AppleByteStringTest.kt | 2 +- .../appleTest/kotlin/okio/AppleSourceTest.kt | 10 ++-------- okio/src/appleTest/kotlin/okio/TestUtil.kt | 3 +-- 6 files changed, 39 insertions(+), 16 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index 9999e6dbc0..cb4331c24e 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -15,8 +15,20 @@ */ package okio -import kotlinx.cinterop.* -import platform.Foundation.* +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.CPointerVar +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.convert +import kotlinx.cinterop.pointed +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.usePinned +import kotlinx.cinterop.value +import platform.Foundation.NSData +import platform.Foundation.NSError +import platform.Foundation.NSInputStream +import platform.Foundation.NSLocalizedDescriptionKey +import platform.Foundation.NSUnderlyingErrorKey import platform.darwin.NSInteger import platform.darwin.NSUInteger import platform.darwin.NSUIntegerVar diff --git a/okio/src/appleMain/kotlin/okio/Source.kt b/okio/src/appleMain/kotlin/okio/Source.kt index d4fc50a67d..7278bfb3c4 100644 --- a/okio/src/appleMain/kotlin/okio/Source.kt +++ b/okio/src/appleMain/kotlin/okio/Source.kt @@ -15,7 +15,11 @@ */ package okio -import kotlinx.cinterop.* +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.convert +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.usePinned import platform.Foundation.NSInputStream import platform.darwin.UInt8Var diff --git a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt index 4a0f230e94..48edacce68 100644 --- a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt @@ -15,10 +15,24 @@ */ package okio -import kotlin.test.* -import kotlinx.cinterop.* +import kotlinx.cinterop.CPointerVar +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.convert +import kotlinx.cinterop.get +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.usePinned +import kotlinx.cinterop.value import platform.darwin.NSUIntegerVar import platform.darwin.UInt8Var +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue @OptIn(UnsafeNumber::class) class AppleBufferedSourceTest { diff --git a/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt b/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt index e254a07d5c..035e2786d2 100644 --- a/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt @@ -15,13 +15,13 @@ */ package okio +import kotlinx.cinterop.UnsafeNumber import platform.Foundation.NSData import platform.Foundation.NSString import platform.Foundation.NSUTF8StringEncoding import platform.Foundation.dataUsingEncoding import kotlin.test.Test import kotlin.test.assertEquals -import kotlinx.cinterop.UnsafeNumber @OptIn(UnsafeNumber::class) class AppleByteStringTest { diff --git a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt index 326c2861bc..bb72dce586 100644 --- a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt @@ -15,17 +15,11 @@ */ package okio +import kotlinx.cinterop.UnsafeNumber +import platform.Foundation.NSInputStream import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertTrue import kotlin.test.fail -import kotlinx.cinterop.UnsafeNumber -import kotlinx.cinterop.allocArrayOf -import kotlinx.cinterop.convert -import kotlinx.cinterop.memScoped -import platform.Foundation.NSData -import platform.Foundation.NSInputStream -import platform.Foundation.create @OptIn(UnsafeNumber::class) class AppleSourceTest { diff --git a/okio/src/appleTest/kotlin/okio/TestUtil.kt b/okio/src/appleTest/kotlin/okio/TestUtil.kt index 0af6e42d7c..2a34216d57 100644 --- a/okio/src/appleTest/kotlin/okio/TestUtil.kt +++ b/okio/src/appleTest/kotlin/okio/TestUtil.kt @@ -2,14 +2,13 @@ package okio -import kotlin.test.assertTrue import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.allocArrayOf import kotlinx.cinterop.convert import kotlinx.cinterop.memScoped import platform.Foundation.NSData import platform.Foundation.create - +import kotlin.test.assertTrue fun ByteArray.toNSData(): NSData = memScoped { NSData.create(bytes = allocArrayOf(this@toNSData), length = size.convert()) From aa0b6d39ee3d4f23828654a12e860157e40d5600 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Sat, 25 Jun 2022 16:56:00 -0600 Subject: [PATCH 10/23] Avoid data copy & read source to buffer --- .../appleMain/kotlin/okio/BufferedSource.kt | 44 ++++++++++++++----- .../kotlin/okio/AppleBufferedSourceTest.kt | 11 ++++- .../okio/internal/-RealBufferedSource.kt | 1 - 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index cb4331c24e..181ddba88e 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -56,20 +56,25 @@ fun BufferedSource.inputStream(): NSInputStream { override fun streamError(): NSError? = error override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { - val bytes = ByteArray(maxLength.toInt()) - val read = try { - this@inputStream.read(bytes, 0, maxLength.toInt()) + try { + val internalBuffer = this@inputStream.buffer + + if (this@inputStream is RealBufferedSource) { + if (closed) throw IOException("closed") + + if (internalBuffer.size == 0L) { + val count = this@inputStream.source.read(internalBuffer, Segment.SIZE.toLong()) + if (count == -1L) return 0 + } + } + + val toRead = minOf(maxLength.toInt(), internalBuffer.size).toInt() + return internalBuffer.readNative(buffer, toRead).convert() + } catch (e: Exception) { error = e.toNSError() return -1 } - if (read > 0) { - bytes.usePinned { - memcpy(buffer, it.addressOf(0), read.convert()) - } - return read.convert() - } - return 0 } override fun getBuffer( @@ -95,3 +100,22 @@ fun BufferedSource.inputStream(): NSInputStream { override fun description(): String = "${this@inputStream}.inputStream()" } } + +@OptIn(UnsafeNumber::class) +internal fun Buffer.readNative(sink: CPointer?, maxLength: Int): Int { + val s = head ?: return 0 + val toCopy = minOf(maxLength, s.limit - s.pos) + s.data.usePinned { + memcpy(sink, it.addressOf(s.pos), toCopy.convert()) + } + + s.pos += toCopy + size -= toCopy.toLong() + + if (s.pos == s.limit) { + head = s.pop() + SegmentPool.recycle(s) + } + + return toCopy +} diff --git a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt index 48edacce68..67a6c37ad9 100644 --- a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt @@ -33,19 +33,28 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue +import platform.Foundation.NSInputStream @OptIn(UnsafeNumber::class) class AppleBufferedSourceTest { @Test fun bufferInputStream() { val source = Buffer() source.writeUtf8("abc") + testInputStream(source.inputStream()) + } + + @Test fun realBufferedSourceInputStream() { + val source = Buffer() + source.writeUtf8("abc") + testInputStream(RealBufferedSource(source).inputStream()) + } + private fun testInputStream(nsis: NSInputStream) { val byteArray = ByteArray(4) byteArray.usePinned { val cPtr = it.addressOf(0).reinterpret() byteArray.fill(-5) - val nsis = source.inputStream() assertEquals(3, nsis.read(cPtr, 4)) assertEquals("[97, 98, 99, -5]", byteArray.contentToString()) diff --git a/okio/src/commonMain/kotlin/okio/internal/-RealBufferedSource.kt b/okio/src/commonMain/kotlin/okio/internal/-RealBufferedSource.kt index 3699fd06d3..8df646c743 100644 --- a/okio/src/commonMain/kotlin/okio/internal/-RealBufferedSource.kt +++ b/okio/src/commonMain/kotlin/okio/internal/-RealBufferedSource.kt @@ -129,7 +129,6 @@ internal inline fun RealBufferedSource.commonReadFully(sink: ByteArray) { internal inline fun RealBufferedSource.commonRead(sink: ByteArray, offset: Int, byteCount: Int): Int { checkOffsetAndCount(sink.size.toLong(), offset.toLong(), byteCount.toLong()) - check(!closed) { "closed" } if (buffer.size == 0L) { val read = source.read(buffer, Segment.SIZE.toLong()) From 9ac4e4dbee863766da7e88d0ae7e243b9c903fd1 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Mon, 27 Jun 2022 11:49:40 -0600 Subject: [PATCH 11/23] Resolve checks --- okio/src/appleMain/kotlin/okio/BufferedSource.kt | 1 - okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index 181ddba88e..333a3315e2 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -70,7 +70,6 @@ fun BufferedSource.inputStream(): NSInputStream { val toRead = minOf(maxLength.toInt(), internalBuffer.size).toInt() return internalBuffer.readNative(buffer, toRead).convert() - } catch (e: Exception) { error = e.toNSError() return -1 diff --git a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt index 67a6c37ad9..dea69f1711 100644 --- a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt @@ -26,6 +26,7 @@ import kotlinx.cinterop.ptr import kotlinx.cinterop.reinterpret import kotlinx.cinterop.usePinned import kotlinx.cinterop.value +import platform.Foundation.NSInputStream import platform.darwin.NSUIntegerVar import platform.darwin.UInt8Var import kotlin.test.Test @@ -33,7 +34,6 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue -import platform.Foundation.NSInputStream @OptIn(UnsafeNumber::class) class AppleBufferedSourceTest { From 72efc44c7dbf49f5c0a70b11dab444c82a7df46e Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Fri, 22 Jul 2022 15:34:16 -0600 Subject: [PATCH 12/23] Override open() as no-op --- .../appleMain/kotlin/okio/BufferedSource.kt | 98 ++++++++++--------- .../kotlin/okio/AppleBufferedSourceTest.kt | 3 + 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index 333a3315e2..ad8ace0584 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -35,69 +35,75 @@ import platform.darwin.NSUIntegerVar import platform.posix.memcpy import platform.posix.uint8_tVar +fun BufferedSource.inputStream(): NSInputStream = BufferedSourceInputStream(this) + /** Returns an input stream that reads from this source. */ @OptIn(UnsafeNumber::class) -fun BufferedSource.inputStream(): NSInputStream { - return object : NSInputStream(NSData()) { - - private var error: NSError? = null - - private fun Exception.toNSError(): NSError { - return NSError( - "Kotlin", - 0, - mapOf( - NSLocalizedDescriptionKey to message, - NSUnderlyingErrorKey to this - ) +private class BufferedSourceInputStream( + private val source: BufferedSource +) : NSInputStream(NSData()) { + + private var error: NSError? = null + + private fun Exception.toNSError(): NSError { + return NSError( + "Kotlin", + 0, + mapOf( + NSLocalizedDescriptionKey to message, + NSUnderlyingErrorKey to this ) - } + ) + } - override fun streamError(): NSError? = error + override fun streamError(): NSError? = error - override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { - try { - val internalBuffer = this@inputStream.buffer + override fun open() { + // no-op + } - if (this@inputStream is RealBufferedSource) { - if (closed) throw IOException("closed") + override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { + try { + val internalBuffer = source.buffer - if (internalBuffer.size == 0L) { - val count = this@inputStream.source.read(internalBuffer, Segment.SIZE.toLong()) - if (count == -1L) return 0 - } - } + if (source is RealBufferedSource) { + if (source.closed) throw IOException("closed") - val toRead = minOf(maxLength.toInt(), internalBuffer.size).toInt() - return internalBuffer.readNative(buffer, toRead).convert() - } catch (e: Exception) { - error = e.toNSError() - return -1 + if (internalBuffer.size == 0L) { + val count = source.source.read(internalBuffer, Segment.SIZE.toLong()) + if (count == -1L) return 0 + } } + + val toRead = minOf(maxLength.toInt(), internalBuffer.size).toInt() + return internalBuffer.readNative(buffer, toRead).convert() + } catch (e: Exception) { + error = e.toNSError() + return -1 } + } - override fun getBuffer( - buffer: CPointer>?, - length: CPointer? - ): Boolean { - if (this@inputStream.buffer.size > 0) { - this@inputStream.buffer.head?.let { s -> - s.data.usePinned { - buffer?.pointed?.value = it.addressOf(s.pos).reinterpret() - length?.pointed?.value = (s.limit - s.pos).convert() - return true - } + override fun getBuffer( + buffer: CPointer>?, + length: CPointer? + ): Boolean { + if (source.buffer.size > 0) { + source.buffer.head?.let { s -> + s.data.usePinned { + buffer?.pointed?.value = it.addressOf(s.pos).reinterpret() + length?.pointed?.value = (s.limit - s.pos).convert() + return true } } - return false } + return false + } - override fun hasBytesAvailable(): Boolean = buffer.size > 0 + override fun hasBytesAvailable(): Boolean = source.buffer.size > 0 - override fun close() = this@inputStream.close() + override fun close() = source.close() - override fun description(): String = "${this@inputStream}.inputStream()" - } + override fun description(): String = "$source.inputStream()" } @OptIn(UnsafeNumber::class) diff --git a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt index dea69f1711..e14e8e038c 100644 --- a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt @@ -50,6 +50,7 @@ class AppleBufferedSourceTest { } private fun testInputStream(nsis: NSInputStream) { + nsis.open() val byteArray = ByteArray(4) byteArray.usePinned { val cPtr = it.addressOf(0).reinterpret() @@ -69,6 +70,7 @@ class AppleBufferedSourceTest { source.writeUtf8("abc") val nsis = source.inputStream() + nsis.open() assertTrue(nsis.hasBytesAvailable) memScoped { @@ -95,6 +97,7 @@ class AppleBufferedSourceTest { assertFalse(source.closed) val nsis = source.inputStream() + nsis.open() nsis.close() assertTrue(source.closed) From 0f74ac765766d935be537db1aefa8966c9b800ac Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Fri, 22 Jul 2022 15:55:42 -0600 Subject: [PATCH 13/23] Rename variable to avoid ambiguity --- .../appleMain/kotlin/okio/BufferedSource.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index ad8ace0584..34d4f0da77 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -40,7 +40,7 @@ fun BufferedSource.inputStream(): NSInputStream = BufferedSourceInputStream(this /** Returns an input stream that reads from this source. */ @OptIn(UnsafeNumber::class) private class BufferedSourceInputStream( - private val source: BufferedSource + private val bufferedSource: BufferedSource ) : NSInputStream(NSData()) { private var error: NSError? = null @@ -64,13 +64,13 @@ private class BufferedSourceInputStream( override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { try { - val internalBuffer = source.buffer + val internalBuffer = bufferedSource.buffer - if (source is RealBufferedSource) { - if (source.closed) throw IOException("closed") + if (bufferedSource is RealBufferedSource) { + if (bufferedSource.closed) throw IOException("closed") if (internalBuffer.size == 0L) { - val count = source.source.read(internalBuffer, Segment.SIZE.toLong()) + val count = bufferedSource.source.read(internalBuffer, Segment.SIZE.toLong()) if (count == -1L) return 0 } } @@ -87,8 +87,8 @@ private class BufferedSourceInputStream( buffer: CPointer>?, length: CPointer? ): Boolean { - if (source.buffer.size > 0) { - source.buffer.head?.let { s -> + if (bufferedSource.buffer.size > 0) { + bufferedSource.buffer.head?.let { s -> s.data.usePinned { buffer?.pointed?.value = it.addressOf(s.pos).reinterpret() length?.pointed?.value = (s.limit - s.pos).convert() @@ -99,11 +99,11 @@ private class BufferedSourceInputStream( return false } - override fun hasBytesAvailable(): Boolean = source.buffer.size > 0 + override fun hasBytesAvailable(): Boolean = bufferedSource.buffer.size > 0 - override fun close() = source.close() + override fun close() = bufferedSource.close() - override fun description(): String = "$source.inputStream()" + override fun description(): String = "$bufferedSource.inputStream()" } @OptIn(UnsafeNumber::class) From 3191c976eb4032780b7689bb69b34fce294e0b69 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Fri, 22 Jul 2022 16:00:38 -0600 Subject: [PATCH 14/23] Move private functions in class --- .../appleMain/kotlin/okio/BufferedSource.kt | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index 34d4f0da77..f7503a2f49 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -45,17 +45,6 @@ private class BufferedSourceInputStream( private var error: NSError? = null - private fun Exception.toNSError(): NSError { - return NSError( - "Kotlin", - 0, - mapOf( - NSLocalizedDescriptionKey to message, - NSUnderlyingErrorKey to this - ) - ) - } - override fun streamError(): NSError? = error override fun open() { @@ -104,23 +93,33 @@ private class BufferedSourceInputStream( override fun close() = bufferedSource.close() override fun description(): String = "$bufferedSource.inputStream()" -} -@OptIn(UnsafeNumber::class) -internal fun Buffer.readNative(sink: CPointer?, maxLength: Int): Int { - val s = head ?: return 0 - val toCopy = minOf(maxLength, s.limit - s.pos) - s.data.usePinned { - memcpy(sink, it.addressOf(s.pos), toCopy.convert()) + private fun Exception.toNSError(): NSError { + return NSError( + "Kotlin", + 0, + mapOf( + NSLocalizedDescriptionKey to message, + NSUnderlyingErrorKey to this + ) + ) } - s.pos += toCopy - size -= toCopy.toLong() + private fun Buffer.readNative(sink: CPointer?, maxLength: Int): Int { + val s = head ?: return 0 + val toCopy = minOf(maxLength, s.limit - s.pos) + s.data.usePinned { + memcpy(sink, it.addressOf(s.pos), toCopy.convert()) + } - if (s.pos == s.limit) { - head = s.pop() - SegmentPool.recycle(s) - } + s.pos += toCopy + size -= toCopy.toLong() - return toCopy + if (s.pos == s.limit) { + head = s.pop() + SegmentPool.recycle(s) + } + + return toCopy + } } From 031a3a1e7b58215ae07d07df01cdb18da9bdcb03 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Sun, 24 Jul 2022 22:45:25 -0600 Subject: [PATCH 15/23] Code review feedback --- okio/src/appleMain/kotlin/okio/BufferedSource.kt | 7 ++----- okio/src/appleMain/kotlin/okio/{Source.kt => source.kt} | 0 2 files changed, 2 insertions(+), 5 deletions(-) rename okio/src/appleMain/kotlin/okio/{Source.kt => source.kt} (100%) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index f7503a2f49..721672e49c 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -35,9 +35,9 @@ import platform.darwin.NSUIntegerVar import platform.posix.memcpy import platform.posix.uint8_tVar +/** Returns an input stream that reads from this source. */ fun BufferedSource.inputStream(): NSInputStream = BufferedSourceInputStream(this) -/** Returns an input stream that reads from this source. */ @OptIn(UnsafeNumber::class) private class BufferedSourceInputStream( private val bufferedSource: BufferedSource @@ -58,10 +58,7 @@ private class BufferedSourceInputStream( if (bufferedSource is RealBufferedSource) { if (bufferedSource.closed) throw IOException("closed") - if (internalBuffer.size == 0L) { - val count = bufferedSource.source.read(internalBuffer, Segment.SIZE.toLong()) - if (count == -1L) return 0 - } + if (bufferedSource.exhausted()) return 0 } val toRead = minOf(maxLength.toInt(), internalBuffer.size).toInt() diff --git a/okio/src/appleMain/kotlin/okio/Source.kt b/okio/src/appleMain/kotlin/okio/source.kt similarity index 100% rename from okio/src/appleMain/kotlin/okio/Source.kt rename to okio/src/appleMain/kotlin/okio/source.kt From 3571012c6ab78f1c41865afe2c763b9369727499 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Sun, 24 Jul 2022 22:46:48 -0600 Subject: [PATCH 16/23] Replace additional RealBufferedSource.commonExhausted() logic --- .../kotlin/okio/internal/-RealBufferedSource.kt | 10 ++-------- .../src/jvmMain/kotlin/okio/RealBufferedSource.kt | 15 +++------------ 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/okio/src/commonMain/kotlin/okio/internal/-RealBufferedSource.kt b/okio/src/commonMain/kotlin/okio/internal/-RealBufferedSource.kt index 8df646c743..826a1ae04d 100644 --- a/okio/src/commonMain/kotlin/okio/internal/-RealBufferedSource.kt +++ b/okio/src/commonMain/kotlin/okio/internal/-RealBufferedSource.kt @@ -35,10 +35,7 @@ internal inline fun RealBufferedSource.commonRead(sink: Buffer, byteCount: Long) require(byteCount >= 0L) { "byteCount < 0: $byteCount" } check(!closed) { "closed" } - if (buffer.size == 0L) { - val read = source.read(buffer, Segment.SIZE.toLong()) - if (read == -1L) return -1L - } + if (exhausted()) return -1L val toRead = minOf(byteCount, buffer.size) return buffer.read(sink, toRead) @@ -130,10 +127,7 @@ internal inline fun RealBufferedSource.commonReadFully(sink: ByteArray) { internal inline fun RealBufferedSource.commonRead(sink: ByteArray, offset: Int, byteCount: Int): Int { checkOffsetAndCount(sink.size.toLong(), offset.toLong(), byteCount.toLong()) - if (buffer.size == 0L) { - val read = source.read(buffer, Segment.SIZE.toLong()) - if (read == -1L) return -1 - } + if (exhausted()) return -1 val toRead = okio.minOf(byteCount, buffer.size).toInt() return buffer.read(sink, offset, toRead) diff --git a/okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt b/okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt index 109ef1402e..5758c789fb 100644 --- a/okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt +++ b/okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt @@ -78,10 +78,7 @@ internal actual class RealBufferedSource actual constructor( commonRead(sink, offset, byteCount) override fun read(sink: ByteBuffer): Int { - if (buffer.size == 0L) { - val read = source.read(buffer, Segment.SIZE.toLong()) - if (read == -1L) return -1 - } + if (exhausted()) return -1 return buffer.read(sink) } @@ -143,10 +140,7 @@ internal actual class RealBufferedSource actual constructor( return object : InputStream() { override fun read(): Int { if (closed) throw IOException("closed") - if (buffer.size == 0L) { - val count = source.read(buffer, Segment.SIZE.toLong()) - if (count == -1L) return -1 - } + if (exhausted()) return -1 return buffer.readByte() and 0xff } @@ -154,10 +148,7 @@ internal actual class RealBufferedSource actual constructor( if (closed) throw IOException("closed") checkOffsetAndCount(data.size.toLong(), offset.toLong(), byteCount.toLong()) - if (buffer.size == 0L) { - val count = source.read(buffer, Segment.SIZE.toLong()) - if (count == -1L) return -1 - } + if (exhausted()) return -1 return buffer.read(data, offset, byteCount) } From 41c38a55cf036749afbfbeec280c879d57dc62f2 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Sun, 24 Jul 2022 23:28:01 -0600 Subject: [PATCH 17/23] Remove variable --- okio/src/appleMain/kotlin/okio/BufferedSource.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index 721672e49c..25a07095c5 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -53,16 +53,13 @@ private class BufferedSourceInputStream( override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { try { - val internalBuffer = bufferedSource.buffer - if (bufferedSource is RealBufferedSource) { if (bufferedSource.closed) throw IOException("closed") - if (bufferedSource.exhausted()) return 0 } - val toRead = minOf(maxLength.toInt(), internalBuffer.size).toInt() - return internalBuffer.readNative(buffer, toRead).convert() + val toRead = minOf(maxLength.toInt(), bufferedSource.buffer.size).toInt() + return bufferedSource.buffer.readNative(buffer, toRead).convert() } catch (e: Exception) { error = e.toNSError() return -1 From c59ea796a5fbe8b908892c756e1f21a7e093b40a Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Mon, 25 Jul 2022 09:52:42 -0600 Subject: [PATCH 18/23] Keep buffer pinned for getBuffer() caller Unpin on next call or on close() --- okio/src/appleMain/kotlin/okio/BufferedSource.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index 25a07095c5..c5660e0c8f 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -17,9 +17,11 @@ package okio import kotlinx.cinterop.CPointer import kotlinx.cinterop.CPointerVar +import kotlinx.cinterop.Pinned import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.addressOf import kotlinx.cinterop.convert +import kotlinx.cinterop.pin import kotlinx.cinterop.pointed import kotlinx.cinterop.reinterpret import kotlinx.cinterop.usePinned @@ -44,6 +46,7 @@ private class BufferedSourceInputStream( ) : NSInputStream(NSData()) { private var error: NSError? = null + private var pinnedBuffer: Pinned? = null override fun streamError(): NSError? = error @@ -72,7 +75,9 @@ private class BufferedSourceInputStream( ): Boolean { if (bufferedSource.buffer.size > 0) { bufferedSource.buffer.head?.let { s -> - s.data.usePinned { + pinnedBuffer?.unpin() + s.data.pin().let { + pinnedBuffer = it buffer?.pointed?.value = it.addressOf(s.pos).reinterpret() length?.pointed?.value = (s.limit - s.pos).convert() return true @@ -84,7 +89,10 @@ private class BufferedSourceInputStream( override fun hasBytesAvailable(): Boolean = bufferedSource.buffer.size > 0 - override fun close() = bufferedSource.close() + override fun close() { + pinnedBuffer?.unpin() + bufferedSource.close() + } override fun description(): String = "$bufferedSource.inputStream()" From fb883f92d1bd7f970f858d92d54e1269c41e92a3 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Mon, 25 Jul 2022 10:18:53 -0600 Subject: [PATCH 19/23] null pinnedBuffer after unpin() --- okio/src/appleMain/kotlin/okio/BufferedSource.kt | 1 + okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index c5660e0c8f..4a434bef81 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -91,6 +91,7 @@ private class BufferedSourceInputStream( override fun close() { pinnedBuffer?.unpin() + pinnedBuffer = null bufferedSource.close() } diff --git a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt index e14e8e038c..9e27aa1326 100644 --- a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt @@ -84,9 +84,9 @@ class AppleBufferedSourceTest { val buffer = bufferPtr.value assertNotNull(buffer) - assertEquals('a'.code.toUByte(), buffer[0]) - assertEquals('b'.code.toUByte(), buffer[1]) - assertEquals('c'.code.toUByte(), buffer[2]) + assertEquals('a'.code.convert(), buffer[0]) + assertEquals('b'.code.convert(), buffer[1]) + assertEquals('c'.code.convert(), buffer[2]) } } From a40e1c79db63d3571c8ba2f7e42f3ff4bd2bba33 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Tue, 28 Feb 2023 23:51:28 -0700 Subject: [PATCH 20/23] Fix lint errors --- okio/src/appleMain/kotlin/okio/BufferedSource.kt | 8 ++++---- .../kotlin/okio/{source.kt => NSInputStreamSource.kt} | 2 +- .../appleTest/kotlin/okio/AppleBufferedSourceTest.kt | 10 +++++----- okio/src/appleTest/kotlin/okio/AppleSourceTest.kt | 6 +++--- okio/src/appleTest/kotlin/okio/TestUtil.kt | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) rename okio/src/appleMain/kotlin/okio/{source.kt => NSInputStreamSource.kt} (98%) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index 4a434bef81..6d96e54b7e 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -42,7 +42,7 @@ fun BufferedSource.inputStream(): NSInputStream = BufferedSourceInputStream(this @OptIn(UnsafeNumber::class) private class BufferedSourceInputStream( - private val bufferedSource: BufferedSource + private val bufferedSource: BufferedSource, ) : NSInputStream(NSData()) { private var error: NSError? = null @@ -71,7 +71,7 @@ private class BufferedSourceInputStream( override fun getBuffer( buffer: CPointer>?, - length: CPointer? + length: CPointer?, ): Boolean { if (bufferedSource.buffer.size > 0) { bufferedSource.buffer.head?.let { s -> @@ -103,8 +103,8 @@ private class BufferedSourceInputStream( 0, mapOf( NSLocalizedDescriptionKey to message, - NSUnderlyingErrorKey to this - ) + NSUnderlyingErrorKey to this, + ), ) } diff --git a/okio/src/appleMain/kotlin/okio/source.kt b/okio/src/appleMain/kotlin/okio/NSInputStreamSource.kt similarity index 98% rename from okio/src/appleMain/kotlin/okio/source.kt rename to okio/src/appleMain/kotlin/okio/NSInputStreamSource.kt index 7278bfb3c4..4b39d92543 100644 --- a/okio/src/appleMain/kotlin/okio/source.kt +++ b/okio/src/appleMain/kotlin/okio/NSInputStreamSource.kt @@ -28,7 +28,7 @@ fun NSInputStream.source(): Source = NSInputStreamSource(this) @OptIn(UnsafeNumber::class) private open class NSInputStreamSource( - private val input: NSInputStream + private val input: NSInputStream, ) : Source { init { diff --git a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt index 9e27aa1326..ed13f659d6 100644 --- a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt @@ -15,6 +15,11 @@ */ package okio +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue import kotlinx.cinterop.CPointerVar import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.addressOf @@ -29,11 +34,6 @@ import kotlinx.cinterop.value import platform.Foundation.NSInputStream import platform.darwin.NSUIntegerVar import platform.darwin.UInt8Var -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue @OptIn(UnsafeNumber::class) class AppleBufferedSourceTest { diff --git a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt index bb72dce586..9d61429af7 100644 --- a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt @@ -15,11 +15,11 @@ */ package okio -import kotlinx.cinterop.UnsafeNumber -import platform.Foundation.NSInputStream import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.fail +import kotlinx.cinterop.UnsafeNumber +import platform.Foundation.NSInputStream @OptIn(UnsafeNumber::class) class AppleSourceTest { @@ -33,7 +33,7 @@ class AppleSourceTest { @Test fun sourceFromInputStream() { val nsis = NSInputStream( - ("a" + "b".repeat(SEGMENT_SIZE * 2) + "c").encodeToByteArray().toNSData() + ("a" + "b".repeat(SEGMENT_SIZE * 2) + "c").encodeToByteArray().toNSData(), ) // Source: ab...bc diff --git a/okio/src/appleTest/kotlin/okio/TestUtil.kt b/okio/src/appleTest/kotlin/okio/TestUtil.kt index 2a34216d57..3e0f8362eb 100644 --- a/okio/src/appleTest/kotlin/okio/TestUtil.kt +++ b/okio/src/appleTest/kotlin/okio/TestUtil.kt @@ -2,13 +2,13 @@ package okio +import kotlin.test.assertTrue import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.allocArrayOf import kotlinx.cinterop.convert import kotlinx.cinterop.memScoped import platform.Foundation.NSData import platform.Foundation.create -import kotlin.test.assertTrue fun ByteArray.toNSData(): NSData = memScoped { NSData.create(bytes = allocArrayOf(this@toNSData), length = size.convert()) From bbddc69ef2dcfb0b5c60867d5f8f7243cf650614 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Thu, 10 Aug 2023 00:31:45 -0600 Subject: [PATCH 21/23] Add NSOutputStream extensions Support NSRunLoop scheduling Other changes from kotlinx-io PR --- gradle/libs.versions.toml | 1 + okio/build.gradle.kts | 5 + .../appleMain/kotlin/okio/ApplePlatform.kt | 16 + .../src/appleMain/kotlin/okio/BufferedSink.kt | 172 ++++++++++ .../appleMain/kotlin/okio/BufferedSource.kt | 196 ++++++++---- .../src/appleMain/kotlin/okio/BuffersApple.kt | 100 ++++++ .../kotlin/okio/NSInputStreamSource.kt | 16 +- .../kotlin/okio/NSOutputStreamSink.kt | 76 +++++ .../kotlin/okio/AppleBufferedSourceTest.kt | 115 ------- .../appleTest/kotlin/okio/AppleSourceTest.kt | 82 ----- .../okio/BufferedSinkNSOutputStreamTest.kt | 188 +++++++++++ .../okio/BufferedSourceInputStreamTest.kt | 300 ++++++++++++++++++ .../kotlin/okio/NSInputStreamSourceTest.kt | 99 ++++++ .../kotlin/okio/NSOutputStreamSinkTest.kt | 65 ++++ okio/src/appleTest/kotlin/okio/TestUtil.kt | 82 ++++- 15 files changed, 1242 insertions(+), 271 deletions(-) create mode 100644 okio/src/appleMain/kotlin/okio/ApplePlatform.kt create mode 100644 okio/src/appleMain/kotlin/okio/BufferedSink.kt create mode 100644 okio/src/appleMain/kotlin/okio/BuffersApple.kt create mode 100644 okio/src/appleMain/kotlin/okio/NSOutputStreamSink.kt delete mode 100644 okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt delete mode 100644 okio/src/appleTest/kotlin/okio/AppleSourceTest.kt create mode 100644 okio/src/appleTest/kotlin/okio/BufferedSinkNSOutputStreamTest.kt create mode 100644 okio/src/appleTest/kotlin/okio/BufferedSourceInputStreamTest.kt create mode 100644 okio/src/appleTest/kotlin/okio/NSInputStreamSourceTest.kt create mode 100644 okio/src/appleTest/kotlin/okio/NSOutputStreamSinkTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e21c2004a3..413a750356 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ androidx-test-runner = { module = "androidx.test:runner", version = "1.5.2" } binaryCompatibilityValidator = { module = "org.jetbrains.kotlinx.binary-compatibility-validator:org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin", version = "0.13.2" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit" } +kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.7.3" } kotlin-time = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.4.0" } jmh-gradle-plugin = { module = "me.champeau.jmh:jmh-gradle-plugin", version = "0.7.1" } jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } diff --git a/okio/build.gradle.kts b/okio/build.gradle.kts index 223ad18f8d..43fa491117 100644 --- a/okio/build.gradle.kts +++ b/okio/build.gradle.kts @@ -132,6 +132,11 @@ kotlin { nativeTest.dependsOn(nonJvmTest) nativeTest.dependsOn(nonWasmTest) createSourceSet("appleTest", parent = nativeTest, children = appleTargets) + .apply { + dependencies { + implementation(libs.kotlin.coroutines) + } + } } } diff --git a/okio/src/appleMain/kotlin/okio/ApplePlatform.kt b/okio/src/appleMain/kotlin/okio/ApplePlatform.kt new file mode 100644 index 0000000000..af2b8648fd --- /dev/null +++ b/okio/src/appleMain/kotlin/okio/ApplePlatform.kt @@ -0,0 +1,16 @@ +package okio + +import kotlinx.cinterop.UnsafeNumber +import platform.Foundation.NSError +import platform.Foundation.NSLocalizedDescriptionKey +import platform.Foundation.NSUnderlyingErrorKey + +@OptIn(UnsafeNumber::class) +internal fun Exception.toNSError() = NSError( + domain = "Kotlin", + code = 0, + userInfo = mapOf( + NSLocalizedDescriptionKey to message, + NSUnderlyingErrorKey to this, + ), +) diff --git a/okio/src/appleMain/kotlin/okio/BufferedSink.kt b/okio/src/appleMain/kotlin/okio/BufferedSink.kt new file mode 100644 index 0000000000..ce5d57f54e --- /dev/null +++ b/okio/src/appleMain/kotlin/okio/BufferedSink.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.ref.WeakReference +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.convert +import platform.Foundation.NSError +import platform.Foundation.NSOutputStream +import platform.Foundation.NSRunLoop +import platform.Foundation.NSRunLoopMode +import platform.Foundation.NSStream +import platform.Foundation.NSStreamDataWrittenToMemoryStreamKey +import platform.Foundation.NSStreamDelegateProtocol +import platform.Foundation.NSStreamEvent +import platform.Foundation.NSStreamEventErrorOccurred +import platform.Foundation.NSStreamEventHasSpaceAvailable +import platform.Foundation.NSStreamEventOpenCompleted +import platform.Foundation.NSStreamPropertyKey +import platform.Foundation.NSStreamStatusClosed +import platform.Foundation.NSStreamStatusError +import platform.Foundation.NSStreamStatusNotOpen +import platform.Foundation.NSStreamStatusOpen +import platform.Foundation.NSStreamStatusOpening +import platform.Foundation.NSStreamStatusWriting +import platform.Foundation.performInModes +import platform.darwin.NSInteger +import platform.darwin.NSUInteger +import platform.posix.uint8_tVar + +/** + * Returns an output stream that writes to this sink. Closing the stream will also close this sink. + */ +fun BufferedSink.outputStream(): NSOutputStream = BufferedSinkNSOutputStream(this) + +@OptIn(UnsafeNumber::class, ExperimentalNativeApi::class) +private class BufferedSinkNSOutputStream( + private val sink: BufferedSink, +) : NSOutputStream(toMemory = Unit), NSStreamDelegateProtocol { + + private val isClosed: () -> Boolean = when (sink) { + is RealBufferedSink -> sink::closed + is Buffer -> { + { false } + } + } + + private var status = NSStreamStatusNotOpen + private var error: NSError? = null + set(value) { + status = NSStreamStatusError + field = value + postEvent(NSStreamEventErrorOccurred) + sink.close() + } + + override fun streamStatus() = if (status != NSStreamStatusError && isClosed()) NSStreamStatusClosed else status + + override fun streamError() = error + + override fun open() { + if (status == NSStreamStatusNotOpen) { + status = NSStreamStatusOpening + status = NSStreamStatusOpen + postEvent(NSStreamEventOpenCompleted) + postEvent(NSStreamEventHasSpaceAvailable) + } + } + + override fun close() { + if (status == NSStreamStatusError || status == NSStreamStatusNotOpen) return + status = NSStreamStatusClosed + runLoop = null + runLoopModes = listOf() + sink.close() + } + + override fun write(buffer: CPointer?, maxLength: NSUInteger): NSInteger { + if (streamStatus != NSStreamStatusOpen || buffer == null) return -1 + status = NSStreamStatusWriting + val toWrite = minOf(maxLength, Int.MAX_VALUE.convert()).toInt() + return try { + sink.buffer.write(buffer, toWrite) + sink.emitCompleteSegments() + status = NSStreamStatusOpen + toWrite.convert() + } catch (e: Exception) { + error = e.toNSError() + -1 + } + } + + override fun hasSpaceAvailable() = !isFinished + + private val isFinished + get() = when (streamStatus) { + NSStreamStatusClosed, NSStreamStatusError -> true + else -> false + } + + override fun propertyForKey(key: NSStreamPropertyKey): Any? = when (key) { + NSStreamDataWrittenToMemoryStreamKey -> sink.buffer.snapshotAsNSData() + else -> null + } + + override fun setProperty(property: Any?, forKey: NSStreamPropertyKey) = false + + // WeakReference as delegate should not be retained + // https://developer.apple.com/documentation/foundation/nsstream/1418423-delegate + private var _delegate: WeakReference? = null + private var runLoop: NSRunLoop? = null + private var runLoopModes = listOf() + + private fun postEvent(event: NSStreamEvent) { + val runLoop = runLoop ?: return + runLoop.performInModes(runLoopModes) { + if (runLoop == this.runLoop) { + delegateOrSelf.stream(this, event) + } + } + } + + override fun delegate() = _delegate?.value + + private val delegateOrSelf get() = delegate ?: this + + override fun setDelegate(delegate: NSStreamDelegateProtocol?) { + _delegate = delegate?.let { WeakReference(it) } + } + + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + // no-op + } + + override fun scheduleInRunLoop(aRunLoop: NSRunLoop, forMode: NSRunLoopMode) { + if (runLoop == null) { + runLoop = aRunLoop + } + if (runLoop == aRunLoop) { + runLoopModes += forMode + } + if (status == NSStreamStatusOpen) { + postEvent(NSStreamEventHasSpaceAvailable) + } + } + + override fun removeFromRunLoop(aRunLoop: NSRunLoop, forMode: NSRunLoopMode) { + if (aRunLoop == runLoop) { + runLoopModes -= forMode + if (runLoopModes.isEmpty()) { + runLoop = null + } + } + } + + override fun description() = "$sink.outputStream()" +} diff --git a/okio/src/appleMain/kotlin/okio/BufferedSource.kt b/okio/src/appleMain/kotlin/okio/BufferedSource.kt index 6d96e54b7e..cd6adbe5d0 100644 --- a/okio/src/appleMain/kotlin/okio/BufferedSource.kt +++ b/okio/src/appleMain/kotlin/okio/BufferedSource.kt @@ -15,114 +15,182 @@ */ package okio +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.ref.WeakReference import kotlinx.cinterop.CPointer import kotlinx.cinterop.CPointerVar -import kotlinx.cinterop.Pinned import kotlinx.cinterop.UnsafeNumber -import kotlinx.cinterop.addressOf import kotlinx.cinterop.convert -import kotlinx.cinterop.pin -import kotlinx.cinterop.pointed -import kotlinx.cinterop.reinterpret -import kotlinx.cinterop.usePinned -import kotlinx.cinterop.value import platform.Foundation.NSData import platform.Foundation.NSError import platform.Foundation.NSInputStream -import platform.Foundation.NSLocalizedDescriptionKey -import platform.Foundation.NSUnderlyingErrorKey +import platform.Foundation.NSRunLoop +import platform.Foundation.NSRunLoopMode +import platform.Foundation.NSStream +import platform.Foundation.NSStreamDelegateProtocol +import platform.Foundation.NSStreamEvent +import platform.Foundation.NSStreamEventEndEncountered +import platform.Foundation.NSStreamEventErrorOccurred +import platform.Foundation.NSStreamEventHasBytesAvailable +import platform.Foundation.NSStreamEventOpenCompleted +import platform.Foundation.NSStreamPropertyKey +import platform.Foundation.NSStreamStatusAtEnd +import platform.Foundation.NSStreamStatusClosed +import platform.Foundation.NSStreamStatusError +import platform.Foundation.NSStreamStatusNotOpen +import platform.Foundation.NSStreamStatusOpen +import platform.Foundation.NSStreamStatusOpening +import platform.Foundation.NSStreamStatusReading +import platform.Foundation.performInModes import platform.darwin.NSInteger import platform.darwin.NSUInteger import platform.darwin.NSUIntegerVar -import platform.posix.memcpy import platform.posix.uint8_tVar /** Returns an input stream that reads from this source. */ fun BufferedSource.inputStream(): NSInputStream = BufferedSourceInputStream(this) -@OptIn(UnsafeNumber::class) +@OptIn(UnsafeNumber::class, ExperimentalNativeApi::class) private class BufferedSourceInputStream( - private val bufferedSource: BufferedSource, -) : NSInputStream(NSData()) { + private val source: BufferedSource, +) : NSInputStream(NSData()), NSStreamDelegateProtocol { + private val isClosed: () -> Boolean = when (source) { + is RealBufferedSource -> source::closed + is Buffer -> { + { false } + } + } + + private var status = NSStreamStatusNotOpen private var error: NSError? = null - private var pinnedBuffer: Pinned? = null + set(value) { + status = NSStreamStatusError + field = value + source.close() + } - override fun streamError(): NSError? = error + override fun streamStatus() = if (status != NSStreamStatusError && isClosed()) NSStreamStatusClosed else status + + override fun streamError() = error override fun open() { - // no-op + if (status == NSStreamStatusNotOpen) { + status = NSStreamStatusOpening + status = NSStreamStatusOpen + postEvent(NSStreamEventOpenCompleted) + checkBytes() + } + } + + override fun close() { + if (status == NSStreamStatusError || status == NSStreamStatusNotOpen) return + status = NSStreamStatusClosed + runLoop = null + runLoopModes = listOf() + source.close() } override fun read(buffer: CPointer?, maxLength: NSUInteger): NSInteger { + if (streamStatus != NSStreamStatusOpen && streamStatus != NSStreamStatusAtEnd || buffer == null) return -1 + status = NSStreamStatusReading try { - if (bufferedSource is RealBufferedSource) { - if (bufferedSource.closed) throw IOException("closed") - if (bufferedSource.exhausted()) return 0 + if (source.exhausted()) { + status = NSStreamStatusAtEnd + return 0 } - - val toRead = minOf(maxLength.toInt(), bufferedSource.buffer.size).toInt() - return bufferedSource.buffer.readNative(buffer, toRead).convert() + val toRead = minOf(maxLength.toLong(), source.buffer.size, Int.MAX_VALUE.toLong()).toInt() + val read = source.buffer.read(buffer, toRead).convert() + status = NSStreamStatusOpen + checkBytes() + return read } catch (e: Exception) { error = e.toNSError() + postEvent(NSStreamEventErrorOccurred) return -1 } } - override fun getBuffer( - buffer: CPointer>?, - length: CPointer?, - ): Boolean { - if (bufferedSource.buffer.size > 0) { - bufferedSource.buffer.head?.let { s -> - pinnedBuffer?.unpin() - s.data.pin().let { - pinnedBuffer = it - buffer?.pointed?.value = it.addressOf(s.pos).reinterpret() - length?.pointed?.value = (s.limit - s.pos).convert() - return true + override fun getBuffer(buffer: CPointer>?, length: CPointer?) = false + + override fun hasBytesAvailable() = !isFinished + + private val isFinished + get() = when (streamStatus) { + NSStreamStatusClosed, NSStreamStatusError -> true + else -> false + } + + override fun propertyForKey(key: NSStreamPropertyKey): Any? = null + + override fun setProperty(property: Any?, forKey: NSStreamPropertyKey) = false + + // WeakReference as delegate should not be retained + // https://developer.apple.com/documentation/foundation/nsstream/1418423-delegate + private var _delegate: WeakReference? = null + private var runLoop: NSRunLoop? = null + private var runLoopModes = listOf() + + private fun postEvent(event: NSStreamEvent) { + val runLoop = runLoop ?: return + runLoop.performInModes(runLoopModes) { + if (runLoop == this.runLoop) { + delegateOrSelf.stream(this, event) + } + } + } + + private fun checkBytes() { + val runLoop = runLoop ?: return + runLoop.performInModes(runLoopModes) { + if (runLoop != this.runLoop || isFinished) return@performInModes + val event = try { + if (source.exhausted()) { + status = NSStreamStatusAtEnd + NSStreamEventEndEncountered + } else { + NSStreamEventHasBytesAvailable } + } catch (e: Exception) { + error = e.toNSError() + NSStreamEventErrorOccurred } + delegateOrSelf.stream(this, event) } - return false } - override fun hasBytesAvailable(): Boolean = bufferedSource.buffer.size > 0 + override fun delegate() = _delegate?.value - override fun close() { - pinnedBuffer?.unpin() - pinnedBuffer = null - bufferedSource.close() + private val delegateOrSelf get() = delegate ?: this + + override fun setDelegate(delegate: NSStreamDelegateProtocol?) { + _delegate = delegate?.let { WeakReference(it) } } - override fun description(): String = "$bufferedSource.inputStream()" - - private fun Exception.toNSError(): NSError { - return NSError( - "Kotlin", - 0, - mapOf( - NSLocalizedDescriptionKey to message, - NSUnderlyingErrorKey to this, - ), - ) + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + // no-op } - private fun Buffer.readNative(sink: CPointer?, maxLength: Int): Int { - val s = head ?: return 0 - val toCopy = minOf(maxLength, s.limit - s.pos) - s.data.usePinned { - memcpy(sink, it.addressOf(s.pos), toCopy.convert()) + override fun scheduleInRunLoop(aRunLoop: NSRunLoop, forMode: NSRunLoopMode) { + if (runLoop == null) { + runLoop = aRunLoop } - - s.pos += toCopy - size -= toCopy.toLong() - - if (s.pos == s.limit) { - head = s.pop() - SegmentPool.recycle(s) + if (runLoop == aRunLoop) { + runLoopModes += forMode + } + if (status == NSStreamStatusOpen) { + checkBytes() } + } - return toCopy + override fun removeFromRunLoop(aRunLoop: NSRunLoop, forMode: NSRunLoopMode) { + if (aRunLoop == runLoop) { + runLoopModes -= forMode + if (runLoopModes.isEmpty()) { + runLoop = null + } + } } + + override fun description(): String = "$source.inputStream()" } diff --git a/okio/src/appleMain/kotlin/okio/BuffersApple.kt b/okio/src/appleMain/kotlin/okio/BuffersApple.kt new file mode 100644 index 0000000000..9723d2905d --- /dev/null +++ b/okio/src/appleMain/kotlin/okio/BuffersApple.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(UnsafeNumber::class) + +package okio + +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.convert +import kotlinx.cinterop.plus +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.toKString +import kotlinx.cinterop.usePinned +import platform.Foundation.NSData +import platform.Foundation.create +import platform.Foundation.data +import platform.darwin.NSUIntegerMax +import platform.posix.errno +import platform.posix.malloc +import platform.posix.memcpy +import platform.posix.strerror +import platform.posix.uint8_tVar + +@OptIn(ExperimentalForeignApi::class) +internal fun Buffer.write(source: CPointer, maxLength: Int) { + require(maxLength >= 0) { "maxLength ($maxLength) must not be negative" } + + var currentOffset = 0 + while (currentOffset < maxLength) { + val tail = writableSegment(1) + + val toCopy = minOf(maxLength - currentOffset, Segment.SIZE - tail.limit) + tail.data.usePinned { + memcpy(it.addressOf(tail.pos), source + currentOffset, toCopy.convert()) + } + + currentOffset += toCopy + tail.limit += toCopy + } + size += maxLength +} + +internal fun Buffer.read(sink: CPointer, maxLength: Int): Int { + require(maxLength >= 0) { "maxLength ($maxLength) must not be negative" } + + val s = head ?: return 0 + val toCopy = minOf(maxLength, s.limit - s.pos) + s.data.usePinned { + memcpy(sink, it.addressOf(s.pos), toCopy.convert()) + } + + s.pos += toCopy + size -= toCopy.toLong() + + if (s.pos == s.limit) { + head = s.pop() + SegmentPool.recycle(s) + } + + return toCopy +} + +@OptIn(BetaInteropApi::class) +internal fun Buffer.snapshotAsNSData(): NSData { + if (size == 0L) return NSData.data() + + check(size.toULong() <= NSUIntegerMax) { "Buffer is too long ($size) to be converted into NSData." } + + val bytes = malloc(size.convert())?.reinterpret() + ?: throw Error("malloc failed: ${strerror(errno)?.toKString()}") + var curr = head + var index = 0 + do { + check(curr != null) { "Current segment is null" } + val pos = curr.pos + val length = curr.limit - pos + curr.data.usePinned { + memcpy(bytes + index, it.addressOf(pos), length.convert()) + } + curr = curr.next + index += length + } while (curr !== head) + return NSData.create(bytesNoCopy = bytes, length = size.convert()) +} diff --git a/okio/src/appleMain/kotlin/okio/NSInputStreamSource.kt b/okio/src/appleMain/kotlin/okio/NSInputStreamSource.kt index 4b39d92543..05eb935c91 100644 --- a/okio/src/appleMain/kotlin/okio/NSInputStreamSource.kt +++ b/okio/src/appleMain/kotlin/okio/NSInputStreamSource.kt @@ -21,7 +21,9 @@ import kotlinx.cinterop.convert import kotlinx.cinterop.reinterpret import kotlinx.cinterop.usePinned import platform.Foundation.NSInputStream -import platform.darwin.UInt8Var +import platform.Foundation.NSStreamStatusClosed +import platform.Foundation.NSStreamStatusNotOpen +import platform.posix.uint8_tVar /** Returns a source that reads from `in`. */ fun NSInputStream.source(): Source = NSInputStreamSource(this) @@ -32,19 +34,23 @@ private open class NSInputStreamSource( ) : Source { init { - input.open() + if (input.streamStatus == NSStreamStatusNotOpen) input.open() } override fun read(sink: Buffer, byteCount: Long): Long { + if (input.streamStatus == NSStreamStatusClosed) throw IOException("Stream Closed") + if (byteCount == 0L) return 0L require(byteCount >= 0L) { "byteCount < 0: $byteCount" } + val tail = sink.writableSegment(1) val maxToCopy = minOf(byteCount, Segment.SIZE - tail.limit) val bytesRead = tail.data.usePinned { - val bytes = it.addressOf(tail.limit).reinterpret() + val bytes = it.addressOf(tail.limit).reinterpret() input.read(bytes, maxToCopy.convert()).toLong() } - if (bytesRead < 0) throw IOException(input.streamError?.localizedDescription) + + if (bytesRead < 0L) throw IOException(input.streamError?.localizedDescription ?: "Unknown error") if (bytesRead == 0L) { if (tail.pos == tail.limit) { // We allocated a tail segment, but didn't end up needing it. Recycle! @@ -55,7 +61,7 @@ private open class NSInputStreamSource( } tail.limit += bytesRead.toInt() sink.size += bytesRead - return bytesRead.convert() + return bytesRead } override fun close() = input.close() diff --git a/okio/src/appleMain/kotlin/okio/NSOutputStreamSink.kt b/okio/src/appleMain/kotlin/okio/NSOutputStreamSink.kt new file mode 100644 index 0000000000..e7df3977f9 --- /dev/null +++ b/okio/src/appleMain/kotlin/okio/NSOutputStreamSink.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.convert +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.usePinned +import platform.Foundation.NSOutputStream +import platform.Foundation.NSStreamStatusClosed +import platform.Foundation.NSStreamStatusNotOpen +import platform.posix.uint8_tVar + +/** Returns a sink that writes to `out`. */ +fun NSOutputStream.sink(): Sink = OutputStreamSink(this) + +@OptIn(UnsafeNumber::class) +private open class OutputStreamSink( + private val out: NSOutputStream, +) : Sink { + + init { + if (out.streamStatus == NSStreamStatusNotOpen) out.open() + } + + override fun write(source: Buffer, byteCount: Long) { + if (out.streamStatus == NSStreamStatusClosed) throw IOException("Stream Closed") + + checkOffsetAndCount(source.size, 0, byteCount) + var remaining = byteCount + while (remaining > 0) { + val head = source.head!! + val toCopy = minOf(remaining, head.limit - head.pos).toInt() + val bytesWritten = head.data.usePinned { + val bytes = it.addressOf(head.pos).reinterpret() + out.write(bytes, toCopy.convert()).toLong() + } + + if (bytesWritten < 0L) throw IOException(out.streamError?.localizedDescription ?: "Unknown error") + if (bytesWritten == 0L) throw IOException("NSOutputStream reached capacity") + + head.pos += bytesWritten.toInt() + remaining -= bytesWritten + source.size -= bytesWritten + + if (head.pos == head.limit) { + source.head = head.pop() + SegmentPool.recycle(head) + } + } + } + + override fun flush() { + // no-op + } + + override fun close() = out.close() + + override fun timeout(): Timeout = Timeout.NONE + + override fun toString() = "RawSink($out)" +} diff --git a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt deleted file mode 100644 index ed13f659d6..0000000000 --- a/okio/src/appleTest/kotlin/okio/AppleBufferedSourceTest.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2020 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package okio - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue -import kotlinx.cinterop.CPointerVar -import kotlinx.cinterop.UnsafeNumber -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.alloc -import kotlinx.cinterop.convert -import kotlinx.cinterop.get -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.reinterpret -import kotlinx.cinterop.usePinned -import kotlinx.cinterop.value -import platform.Foundation.NSInputStream -import platform.darwin.NSUIntegerVar -import platform.darwin.UInt8Var - -@OptIn(UnsafeNumber::class) -class AppleBufferedSourceTest { - @Test fun bufferInputStream() { - val source = Buffer() - source.writeUtf8("abc") - testInputStream(source.inputStream()) - } - - @Test fun realBufferedSourceInputStream() { - val source = Buffer() - source.writeUtf8("abc") - testInputStream(RealBufferedSource(source).inputStream()) - } - - private fun testInputStream(nsis: NSInputStream) { - nsis.open() - val byteArray = ByteArray(4) - byteArray.usePinned { - val cPtr = it.addressOf(0).reinterpret() - - byteArray.fill(-5) - assertEquals(3, nsis.read(cPtr, 4)) - assertEquals("[97, 98, 99, -5]", byteArray.contentToString()) - - byteArray.fill(-7) - assertEquals(0, nsis.read(cPtr, 4)) - assertEquals("[-7, -7, -7, -7]", byteArray.contentToString()) - } - } - - @Test fun nsInputStreamGetBuffer() { - val source = Buffer() - source.writeUtf8("abc") - - val nsis = source.inputStream() - nsis.open() - assertTrue(nsis.hasBytesAvailable) - - memScoped { - val bufferPtr = alloc>() - val lengthPtr = alloc() - assertTrue(nsis.getBuffer(bufferPtr.ptr, lengthPtr.ptr)) - - val length = lengthPtr.value - assertNotNull(length) - assertEquals(3.convert(), length) - - val buffer = bufferPtr.value - assertNotNull(buffer) - assertEquals('a'.code.convert(), buffer[0]) - assertEquals('b'.code.convert(), buffer[1]) - assertEquals('c'.code.convert(), buffer[2]) - } - } - - @Test fun nsInputStreamClose() { - val buffer = Buffer() - buffer.writeUtf8("abc") - val source = RealBufferedSource(buffer) - assertFalse(source.closed) - - val nsis = source.inputStream() - nsis.open() - nsis.close() - assertTrue(source.closed) - - val byteArray = ByteArray(4) - byteArray.usePinned { - val cPtr = it.addressOf(0).reinterpret() - - byteArray.fill(-5) - assertEquals(-1, nsis.read(cPtr, 4)) - assertNotNull(nsis.streamError) - assertEquals("closed", nsis.streamError?.localizedDescription) - assertEquals("[-5, -5, -5, -5]", byteArray.contentToString()) - } - } -} diff --git a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt b/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt deleted file mode 100644 index 9d61429af7..0000000000 --- a/okio/src/appleTest/kotlin/okio/AppleSourceTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2020 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package okio - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.fail -import kotlinx.cinterop.UnsafeNumber -import platform.Foundation.NSInputStream - -@OptIn(UnsafeNumber::class) -class AppleSourceTest { - @Test fun nsInputStreamSource() { - val nsis = NSInputStream(byteArrayOf(0x61).toNSData()) - val source = nsis.source() - val buffer = Buffer() - source.read(buffer, 1) - assertEquals("a", buffer.readUtf8()) - } - - @Test fun sourceFromInputStream() { - val nsis = NSInputStream( - ("a" + "b".repeat(SEGMENT_SIZE * 2) + "c").encodeToByteArray().toNSData(), - ) - - // Source: ab...bc - val source: Source = nsis.source() - val sink = Buffer() - - // Source: b...bc. Sink: abb. - assertEquals(3, source.read(sink, 3)) - assertEquals("abb", sink.readUtf8(3)) - - // Source: b...bc. Sink: b...b. - assertEquals(SEGMENT_SIZE.toLong(), source.read(sink, 20000)) - assertEquals("b".repeat(SEGMENT_SIZE), sink.readUtf8()) - - // Source: b...bc. Sink: b...bc. - assertEquals((SEGMENT_SIZE - 1).toLong(), source.read(sink, 20000)) - assertEquals("b".repeat(SEGMENT_SIZE - 2) + "c", sink.readUtf8()) - - // Source and sink are empty. - assertEquals(-1, source.read(sink, 1)) - } - - @Test fun sourceFromInputStreamWithSegmentSize() { - val nsis = NSInputStream(ByteArray(SEGMENT_SIZE).toNSData()) - val source = nsis.source() - val sink = Buffer() - - assertEquals(SEGMENT_SIZE.toLong(), source.read(sink, SEGMENT_SIZE.toLong())) - assertEquals(-1, source.read(sink, SEGMENT_SIZE.toLong())) - - assertNoEmptySegments(sink) - } - - @Test fun sourceFromInputStreamBounds() { - val source = NSInputStream(ByteArray(100).toNSData()).source() - try { - source.read(Buffer(), -1) - fail() - } catch (expected: IllegalArgumentException) { - } - } - - companion object { - const val SEGMENT_SIZE = Segment.SIZE - } -} diff --git a/okio/src/appleTest/kotlin/okio/BufferedSinkNSOutputStreamTest.kt b/okio/src/appleTest/kotlin/okio/BufferedSinkNSOutputStreamTest.kt new file mode 100644 index 0000000000..e5824c5678 --- /dev/null +++ b/okio/src/appleTest/kotlin/okio/BufferedSinkNSOutputStreamTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.fail +import kotlinx.atomicfu.atomic +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.convert +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.usePinned +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import platform.CoreFoundation.CFRunLoopStop +import platform.Foundation.NSData +import platform.Foundation.NSDefaultRunLoopMode +import platform.Foundation.NSOutputStream +import platform.Foundation.NSStream +import platform.Foundation.NSStreamDataWrittenToMemoryStreamKey +import platform.Foundation.NSStreamDelegateProtocol +import platform.Foundation.NSStreamEvent +import platform.Foundation.NSStreamEventHasSpaceAvailable +import platform.Foundation.NSStreamEventOpenCompleted +import platform.Foundation.NSStreamStatusClosed +import platform.Foundation.NSStreamStatusNotOpen +import platform.Foundation.NSStreamStatusOpen +import platform.Foundation.NSThread +import platform.darwin.NSObject +import platform.darwin.NSUInteger +import platform.posix.uint8_tVar + +@OptIn(UnsafeNumber::class) +class BufferedSinkNSOutputStreamTest { + @Test + fun bufferOutputStream() { + testOutputStream(Buffer(), "abc") + testOutputStream(Buffer(), "a" + "b".repeat(Segment.SIZE * 2) + "c") + } + + @Test + fun realBufferedSinkOutputStream() { + testOutputStream(RealBufferedSink(Buffer()), "abc") + testOutputStream(RealBufferedSink(Buffer()), "a" + "b".repeat(Segment.SIZE * 2) + "c") + } + + private fun testOutputStream(sink: BufferedSink, input: String) { + val out = sink.outputStream() + val byteArray = input.encodeToByteArray() + val size: NSUInteger = input.length.convert() + byteArray.usePinned { + val cPtr = it.addressOf(0).reinterpret() + + assertEquals(NSStreamStatusNotOpen, out.streamStatus) + assertEquals(-1, out.write(cPtr, size)) + out.open() + assertEquals(NSStreamStatusOpen, out.streamStatus) + + assertEquals(size.convert(), out.write(cPtr, size)) + sink.flush() + when (sink) { + is Buffer -> { + val data = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData + assertContentEquals(byteArray, data.toByteArray()) + assertContentEquals(byteArray, sink.buffer.readByteArray()) + } + is RealBufferedSink -> assertContentEquals(byteArray, (sink.sink as Buffer).readByteArray()) + } + } + } + + @Test + fun nsOutputStreamClose() { + val buffer = Buffer() + val sink = RealBufferedSink(buffer) + assertFalse(sink.closed) + + val out = sink.outputStream() + out.open() + out.close() + assertTrue(sink.closed) + assertEquals(NSStreamStatusClosed, out.streamStatus) + + val byteArray = ByteArray(4) + byteArray.usePinned { + val cPtr = it.addressOf(0).reinterpret() + + assertEquals(-1, out.write(cPtr, 4U)) + assertNull(out.streamError) + assertTrue(sink.buffer.readByteArray().isEmpty()) + } + } + + @Test + fun delegateTest() { + val runLoop = startRunLoop() + + fun produceWithDelegate(out: NSOutputStream, data: String) { + val opened = Mutex(true) + val written = atomic(0) + val completed = Mutex(true) + + out.delegate = object : NSObject(), NSStreamDelegateProtocol { + val source = data.encodeToByteArray() + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + assertEquals("run-loop", NSThread.currentThread.name) + when (handleEvent) { + NSStreamEventOpenCompleted -> opened.unlock() + NSStreamEventHasSpaceAvailable -> { + if (source.isNotEmpty()) { + source.usePinned { + assertEquals( + data.length.convert(), + out.write(it.addressOf(written.value).reinterpret(), data.length.convert()), + ) + written.value += data.length + } + } + val writtenData = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData + assertEquals(data, writtenData.toByteArray().decodeToString()) + out.close() + completed.unlock() + } + else -> fail("unexpected event ${handleEvent.asString()}") + } + } + } + out.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) + out.open() + runBlocking { + opened.lockWithTimeout() + completed.lockWithTimeout() + } + assertEquals(data.length, written.value) + } + + produceWithDelegate(Buffer().outputStream(), "custom") + produceWithDelegate(Buffer().outputStream(), "") + CFRunLoopStop(runLoop.getCFRunLoop()) + } + + @Test + fun testSubscribeAfterOpen() { + val runLoop = startRunLoop() + + fun subscribeAfterOpen(out: NSOutputStream) { + val available = Mutex(true) + + out.delegate = object : NSObject(), NSStreamDelegateProtocol { + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + assertEquals("run-loop", NSThread.currentThread.name) + when (handleEvent) { + NSStreamEventOpenCompleted -> fail("opened before subscribe") + NSStreamEventHasSpaceAvailable -> available.unlock() + else -> fail("unexpected event ${handleEvent.asString()}") + } + } + } + out.open() + out.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) + runBlocking { + available.lockWithTimeout() + } + out.close() + } + + subscribeAfterOpen(Buffer().outputStream()) + CFRunLoopStop(runLoop.getCFRunLoop()) + } +} diff --git a/okio/src/appleTest/kotlin/okio/BufferedSourceInputStreamTest.kt b/okio/src/appleTest/kotlin/okio/BufferedSourceInputStreamTest.kt new file mode 100644 index 0000000000..58e12c7319 --- /dev/null +++ b/okio/src/appleTest/kotlin/okio/BufferedSourceInputStreamTest.kt @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.fail +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.locks.reentrantLock +import kotlinx.atomicfu.locks.withLock +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.convert +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.usePinned +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import platform.CoreFoundation.CFRunLoopStop +import platform.Foundation.NSDefaultRunLoopMode +import platform.Foundation.NSInputStream +import platform.Foundation.NSStream +import platform.Foundation.NSStreamDelegateProtocol +import platform.Foundation.NSStreamEvent +import platform.Foundation.NSStreamEventEndEncountered +import platform.Foundation.NSStreamEventHasBytesAvailable +import platform.Foundation.NSStreamEventOpenCompleted +import platform.Foundation.NSStreamStatusClosed +import platform.Foundation.NSStreamStatusNotOpen +import platform.Foundation.NSStreamStatusOpen +import platform.Foundation.NSThread +import platform.darwin.NSObject +import platform.posix.uint8_tVar + +@OptIn(UnsafeNumber::class) +class BufferedSourceInputStreamTest { + @Test + fun bufferInputStream() { + val source = Buffer() + source.writeUtf8("abc") + testInputStream(source.inputStream()) + } + + @Test + fun realBufferedSourceInputStream() { + val source = Buffer() + source.writeUtf8("abc") + testInputStream(RealBufferedSource(source).inputStream()) + } + + private fun testInputStream(input: NSInputStream) { + val byteArray = ByteArray(4) + byteArray.usePinned { + val cPtr = it.addressOf(0).reinterpret() + + assertEquals(NSStreamStatusNotOpen, input.streamStatus) + assertEquals(-1, input.read(cPtr, 4U)) + input.open() + assertEquals(NSStreamStatusOpen, input.streamStatus) + + byteArray.fill(-5) + assertEquals(3, input.read(cPtr, 4U)) + assertEquals("[97, 98, 99, -5]", byteArray.contentToString()) + + byteArray.fill(-7) + assertEquals(0, input.read(cPtr, 4U)) + assertEquals("[-7, -7, -7, -7]", byteArray.contentToString()) + } + } + + @Test + fun bufferInputStreamLongData() { + val source = Buffer() + source.writeUtf8("a" + "b".repeat(Segment.SIZE * 2) + "c") + testInputStreamLongData(source.inputStream()) + } + + @Test + fun realBufferedSourceInputStreamLongData() { + val source = Buffer() + source.writeUtf8("a" + "b".repeat(Segment.SIZE * 2) + "c") + testInputStreamLongData(RealBufferedSource(source).inputStream()) + } + + private fun testInputStreamLongData(input: NSInputStream) { + val lengthPlusOne = Segment.SIZE * 2 + 3 + val byteArray = ByteArray(lengthPlusOne) + byteArray.usePinned { + val cPtr = it.addressOf(0).reinterpret() + + assertEquals(NSStreamStatusNotOpen, input.streamStatus) + assertEquals(-1, input.read(cPtr, lengthPlusOne.convert())) + input.open() + assertEquals(NSStreamStatusOpen, input.streamStatus) + + byteArray.fill(-5) + assertEquals(Segment.SIZE.convert(), input.read(cPtr, lengthPlusOne.convert())) + assertEquals("[97${", 98".repeat(Segment.SIZE - 1)}${", -5".repeat(Segment.SIZE + 3)}]", byteArray.contentToString()) + + byteArray.fill(-6) + assertEquals(Segment.SIZE.convert(), input.read(cPtr, lengthPlusOne.convert())) + assertEquals("[98${", 98".repeat(Segment.SIZE - 1)}${", -6".repeat(Segment.SIZE + 3)}]", byteArray.contentToString()) + + byteArray.fill(-7) + assertEquals(2, input.read(cPtr, lengthPlusOne.convert())) + assertEquals("[98, 99${", -7".repeat(Segment.SIZE * 2 + 1)}]", byteArray.contentToString()) + + byteArray.fill(-8) + assertEquals(0, input.read(cPtr, lengthPlusOne.convert())) + assertEquals("[-8${", -8".repeat(lengthPlusOne - 1)}]", byteArray.contentToString()) + } + } + + @Test + fun nsInputStreamClose() { + val buffer = Buffer() + buffer.writeUtf8("abc") + val source = RealBufferedSource(buffer) + assertFalse(source.closed) + + val input = source.inputStream() + input.open() + input.close() + assertTrue(source.closed) + assertEquals(NSStreamStatusClosed, input.streamStatus) + + val byteArray = ByteArray(4) + byteArray.usePinned { + val cPtr = it.addressOf(0).reinterpret() + + byteArray.fill(-5) + assertEquals(-1, input.read(cPtr, 4U)) + assertNull(input.streamError) + assertEquals("[-5, -5, -5, -5]", byteArray.contentToString()) + } + } + + @Test + fun delegateTest() { + val runLoop = startRunLoop() + + fun consumeWithDelegate(input: NSInputStream, data: String) { + val opened = Mutex(true) + val read = atomic(0) + val completed = Mutex(true) + + input.delegate = object : NSObject(), NSStreamDelegateProtocol { + val sink = ByteArray(data.length) + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + assertEquals("run-loop", NSThread.currentThread.name) + when (handleEvent) { + NSStreamEventOpenCompleted -> opened.unlock() + NSStreamEventHasBytesAvailable -> { + sink.usePinned { + assertEquals(1, input.read(it.addressOf(read.value).reinterpret(), 1U)) + read.value++ + } + } + NSStreamEventEndEncountered -> { + assertEquals(data, sink.decodeToString()) + input.close() + completed.unlock() + } + else -> fail("unexpected event ${handleEvent.asString()}") + } + } + } + input.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) + input.open() + runBlocking { + opened.lockWithTimeout() + completed.lockWithTimeout() + } + assertEquals(data.length, read.value) + } + + consumeWithDelegate(Buffer().apply { writeUtf8("custom") }.inputStream(), "custom") + consumeWithDelegate(Buffer().inputStream(), "") + CFRunLoopStop(runLoop.getCFRunLoop()) + } + + @Test + fun testRunLoopSwitch() { + val runLoop1 = startRunLoop("run-loop-1") + val runLoop2 = startRunLoop("run-loop-2") + + fun consumeSwitching(input: NSInputStream, data: String) { + val opened = Mutex(true) + val readLock = reentrantLock() + var read = 0 + val completed = Mutex(true) + + input.delegate = object : NSObject(), NSStreamDelegateProtocol { + val sink = ByteArray(data.length) + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + // Ensure thread safe access to `read` between scheduled run loops + readLock.withLock { + if (read == 0) { + // until first read + assertEquals("run-loop-1", NSThread.currentThread.name) + } else { + // after first read + assertEquals("run-loop-2", NSThread.currentThread.name) + } + when (handleEvent) { + NSStreamEventOpenCompleted -> opened.unlock() + NSStreamEventHasBytesAvailable -> { + if (read == 0) { + // switch to other run loop before first read + input.removeFromRunLoop(runLoop1, NSDefaultRunLoopMode) + input.scheduleInRunLoop(runLoop2, NSDefaultRunLoopMode) + } else if (read >= data.length - 3) { + // unsubscribe before last read + input.removeFromRunLoop(runLoop2, NSDefaultRunLoopMode) + } + sink.usePinned { + val readBytes = input.read(it.addressOf(read).reinterpret(), 3U) + assertNotEquals(0, readBytes) + read += readBytes.toInt() + } + if (read == data.length) { + assertEquals(data, sink.decodeToString()) + completed.unlock() + } + } + NSStreamEventEndEncountered -> fail("$data shouldn't be subscribed") + else -> fail("unexpected event ${handleEvent.asString()}") + } + } + } + } + input.scheduleInRunLoop(runLoop1, NSDefaultRunLoopMode) + input.open() + runBlocking { + opened.lockWithTimeout() + completed.lockWithTimeout() + // wait a bit to be sure delegate is no longer called + delay(200) + } + input.close() + } + + consumeSwitching(Buffer().apply { writeUtf8("custom") }.inputStream(), "custom") + CFRunLoopStop(runLoop1.getCFRunLoop()) + CFRunLoopStop(runLoop2.getCFRunLoop()) + } + + @Test + fun testSubscribeAfterOpen() { + val runLoop = startRunLoop() + + fun subscribeAfterOpen(input: NSInputStream, data: String) { + val available = Mutex(true) + + input.delegate = object : NSObject(), NSStreamDelegateProtocol { + override fun stream(aStream: NSStream, handleEvent: NSStreamEvent) { + assertEquals("run-loop", NSThread.currentThread.name) + when (handleEvent) { + NSStreamEventOpenCompleted -> fail("opened before subscribe") + NSStreamEventHasBytesAvailable -> { + val sink = ByteArray(data.length) + sink.usePinned { + assertEquals(data.length.convert(), input.read(it.addressOf(0).reinterpret(), data.length.convert())) + } + assertEquals(data, sink.decodeToString()) + input.close() + available.unlock() + } + else -> fail("unexpected event ${handleEvent.asString()}") + } + } + } + input.open() + input.scheduleInRunLoop(runLoop, NSDefaultRunLoopMode) + runBlocking { + available.lockWithTimeout() + } + } + + subscribeAfterOpen(Buffer().apply { writeUtf8("custom") }.inputStream(), "custom") + CFRunLoopStop(runLoop.getCFRunLoop()) + } +} diff --git a/okio/src/appleTest/kotlin/okio/NSInputStreamSourceTest.kt b/okio/src/appleTest/kotlin/okio/NSInputStreamSourceTest.kt new file mode 100644 index 0000000000..d5146fda98 --- /dev/null +++ b/okio/src/appleTest/kotlin/okio/NSInputStreamSourceTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import okio.Path.Companion.toPath +import platform.Foundation.NSInputStream +import platform.Foundation.NSTemporaryDirectory +import platform.Foundation.NSURL +import platform.Foundation.NSUUID + +class NSInputStreamSourceTest { + @Test + fun nsInputStreamSource() { + val input = NSInputStream(data = byteArrayOf(0x61).toNSData()) + val source = input.source() + val buffer = Buffer() + assertEquals(1, source.read(buffer, 1L)) + assertEquals("a", buffer.readUtf8()) + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun nsInputStreamSourceFromFile() { + // can be replaced with createTempFile() when #183 is fixed + // https://github.com/Kotlin/kotlinx-io/issues/183 + val file = "${NSTemporaryDirectory()}${NSUUID().UUIDString()}" + try { + FileSystem.SYSTEM.write(file.toPath()) { + writeUtf8("example") + } + + val input = NSInputStream(uRL = NSURL.fileURLWithPath(file)) + val source = input.source() + val buffer = Buffer() + assertEquals(7, source.read(buffer, 10)) + assertEquals("example", buffer.readUtf8()) + } finally { + FileSystem.SYSTEM.delete(file.toPath(), false) + } + } + + @Test + fun sourceFromInputStream() { + val input = NSInputStream(data = ("a" + "b".repeat(Segment.SIZE * 2) + "c").encodeToByteArray().toNSData()) + + // Source: ab...bc + val source: Source = input.source() + val sink = Buffer() + + // Source: b...bc. Sink: abb. + assertEquals(3, source.read(sink, 3)) + assertEquals("abb", sink.readUtf8(3)) + + // Source: b...bc. Sink: b...b. + assertEquals(Segment.SIZE.toLong(), source.read(sink, 20000)) + assertEquals("b".repeat(Segment.SIZE), sink.readUtf8()) + + // Source: b...bc. Sink: b...bc. + assertEquals((Segment.SIZE - 1).toLong(), source.read(sink, 20000)) + assertEquals("b".repeat(Segment.SIZE - 2) + "c", sink.readUtf8()) + + // Source and sink are empty. + assertEquals(-1, source.read(sink, 1)) + } + + @Test + fun sourceFromInputStreamWithSegmentSize() { + val input = NSInputStream(data = ByteArray(Segment.SIZE).toNSData()) + val source = input.source() + val sink = Buffer() + + assertEquals(Segment.SIZE.toLong(), source.read(sink, Segment.SIZE.toLong())) + assertEquals(-1, source.read(sink, Segment.SIZE.toLong())) + + assertNoEmptySegments(sink) + } + + @Test + fun sourceFromInputStreamBounds() { + val source = NSInputStream(data = ByteArray(100).toNSData()).source() + assertFailsWith { source.read(Buffer(), -1) } + } +} diff --git a/okio/src/appleTest/kotlin/okio/NSOutputStreamSinkTest.kt b/okio/src/appleTest/kotlin/okio/NSOutputStreamSinkTest.kt new file mode 100644 index 0000000000..4519546a16 --- /dev/null +++ b/okio/src/appleTest/kotlin/okio/NSOutputStreamSinkTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okio + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.get +import kotlinx.cinterop.reinterpret +import platform.Foundation.NSData +import platform.Foundation.NSOutputStream +import platform.Foundation.NSStreamDataWrittenToMemoryStreamKey +import platform.Foundation.outputStreamToMemory + +class NSOutputStreamSinkTest { + @Test + @OptIn(UnsafeNumber::class) + fun nsOutputStreamSink() { + val out = NSOutputStream.outputStreamToMemory() + val sink = out.sink() + val buffer = Buffer().apply { + writeUtf8("a") + } + sink.write(buffer, 1L) + val data = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData + assertEquals(1U, data.length) + val bytes = data.bytes!!.reinterpret() + assertEquals(0x61, bytes[0]) + } + + @Test + fun sinkFromOutputStream() { + val data = Buffer().apply { + writeUtf8("a") + writeUtf8("b".repeat(9998)) + writeUtf8("c") + } + val out = NSOutputStream.outputStreamToMemory() + val sink = out.sink() + + sink.write(data, 3) + val outData = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData + val outString = outData.toByteArray().decodeToString() + assertEquals("abb", outString) + + sink.write(data, data.size) + val outData2 = out.propertyForKey(NSStreamDataWrittenToMemoryStreamKey) as NSData + val outString2 = outData2.toByteArray().decodeToString() + assertEquals("a" + "b".repeat(9998) + "c", outString2) + } +} diff --git a/okio/src/appleTest/kotlin/okio/TestUtil.kt b/okio/src/appleTest/kotlin/okio/TestUtil.kt index 3e0f8362eb..8c24bfb0b0 100644 --- a/okio/src/appleTest/kotlin/okio/TestUtil.kt +++ b/okio/src/appleTest/kotlin/okio/TestUtil.kt @@ -1,17 +1,89 @@ -@file:OptIn(UnsafeNumber::class) +@file:OptIn(UnsafeNumber::class, BetaInteropApi::class) package okio import kotlin.test.assertTrue +import kotlin.test.fail +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlinx.cinterop.BetaInteropApi import kotlinx.cinterop.UnsafeNumber -import kotlinx.cinterop.allocArrayOf +import kotlinx.cinterop.addressOf import kotlinx.cinterop.convert -import kotlinx.cinterop.memScoped +import kotlinx.cinterop.refTo +import kotlinx.cinterop.usePinned +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withTimeout import platform.Foundation.NSData +import platform.Foundation.NSDefaultRunLoopMode +import platform.Foundation.NSMachPort +import platform.Foundation.NSRunLoop +import platform.Foundation.NSStreamEvent +import platform.Foundation.NSStreamEventEndEncountered +import platform.Foundation.NSStreamEventErrorOccurred +import platform.Foundation.NSStreamEventHasBytesAvailable +import platform.Foundation.NSStreamEventHasSpaceAvailable +import platform.Foundation.NSStreamEventNone +import platform.Foundation.NSStreamEventOpenCompleted +import platform.Foundation.NSThread import platform.Foundation.create +import platform.Foundation.data +import platform.Foundation.run +import platform.posix.memcpy -fun ByteArray.toNSData(): NSData = memScoped { - NSData.create(bytes = allocArrayOf(this@toNSData), length = size.convert()) +internal fun ByteArray.toNSData() = if (isNotEmpty()) { + usePinned { + NSData.create(bytes = it.addressOf(0), length = size.convert()) + } +} else { + NSData.data() +} + +fun NSData.toByteArray() = ByteArray(length.toInt()).apply { + if (isNotEmpty()) { + memcpy(refTo(0), bytes, length) + } +} + +fun startRunLoop(name: String = "run-loop"): NSRunLoop { + val created = Mutex(true) + lateinit var runLoop: NSRunLoop + val thread = NSThread { + runLoop = NSRunLoop.currentRunLoop + runLoop.addPort(NSMachPort.port(), NSDefaultRunLoopMode) + created.unlock() + runLoop.run() + } + thread.name = name + thread.start() + runBlocking { + created.lockWithTimeout() + } + return runLoop +} + +suspend fun Mutex.lockWithTimeout(timeout: Duration = 5.seconds) { + class MutexSource : Throwable() + val source = MutexSource() + try { + withTimeout(timeout) { lock() } + } catch (e: TimeoutCancellationException) { + fail("Mutex never unlocked", source) + } +} + +fun NSStreamEvent.asString(): String { + return when (this) { + NSStreamEventNone -> "NSStreamEventNone" + NSStreamEventOpenCompleted -> "NSStreamEventOpenCompleted" + NSStreamEventHasBytesAvailable -> "NSStreamEventHasBytesAvailable" + NSStreamEventHasSpaceAvailable -> "NSStreamEventHasSpaceAvailable" + NSStreamEventErrorOccurred -> "NSStreamEventErrorOccurred" + NSStreamEventEndEncountered -> "NSStreamEventEndEncountered" + else -> "Unknown event $this" + } } fun assertNoEmptySegments(buffer: Buffer) { From 8b3fe7e0cae9416d0132b952ddaf8955098724f7 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Thu, 24 Aug 2023 15:29:28 -0600 Subject: [PATCH 22/23] Fix from https://github.com/Kotlin/kotlinx-io/issues/215 --- .../src/appleMain/kotlin/okio/BuffersApple.kt | 2 +- .../okio/BufferedSinkNSOutputStreamTest.kt | 26 +++++++++++++++++++ okio/src/appleTest/kotlin/okio/TestUtil.kt | 8 ++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/okio/src/appleMain/kotlin/okio/BuffersApple.kt b/okio/src/appleMain/kotlin/okio/BuffersApple.kt index 9723d2905d..a1814cc2b0 100644 --- a/okio/src/appleMain/kotlin/okio/BuffersApple.kt +++ b/okio/src/appleMain/kotlin/okio/BuffersApple.kt @@ -47,7 +47,7 @@ internal fun Buffer.write(source: CPointer, maxLength: Int) { val toCopy = minOf(maxLength - currentOffset, Segment.SIZE - tail.limit) tail.data.usePinned { - memcpy(it.addressOf(tail.pos), source + currentOffset, toCopy.convert()) + memcpy(it.addressOf(tail.limit), source + currentOffset, toCopy.convert()) } currentOffset += toCopy diff --git a/okio/src/appleTest/kotlin/okio/BufferedSinkNSOutputStreamTest.kt b/okio/src/appleTest/kotlin/okio/BufferedSinkNSOutputStreamTest.kt index e5824c5678..d8ea1e5cfc 100644 --- a/okio/src/appleTest/kotlin/okio/BufferedSinkNSOutputStreamTest.kt +++ b/okio/src/appleTest/kotlin/okio/BufferedSinkNSOutputStreamTest.kt @@ -48,8 +48,34 @@ import platform.darwin.NSObject import platform.darwin.NSUInteger import platform.posix.uint8_tVar +private fun NSOutputStream.write(vararg strings: String) { + for (str in strings) { + str.encodeToByteArray().apply { + assertEquals(size, this.write(this@write)) + } + } +} + @OptIn(UnsafeNumber::class) class BufferedSinkNSOutputStreamTest { + @Test + fun multipleWrites() { + val buffer = Buffer() + buffer.outputStream().apply { + open() + write("hello", " ", "world") + close() + } + assertEquals("hello world", buffer.readUtf8()) + + RealBufferedSink(buffer).outputStream().apply { + open() + write("hello", " ", "real", " sink") + close() + } + assertEquals("hello real sink", buffer.readUtf8()) + } + @Test fun bufferOutputStream() { testOutputStream(Buffer(), "abc") diff --git a/okio/src/appleTest/kotlin/okio/TestUtil.kt b/okio/src/appleTest/kotlin/okio/TestUtil.kt index 8c24bfb0b0..be06d742c4 100644 --- a/okio/src/appleTest/kotlin/okio/TestUtil.kt +++ b/okio/src/appleTest/kotlin/okio/TestUtil.kt @@ -11,6 +11,7 @@ import kotlinx.cinterop.UnsafeNumber import kotlinx.cinterop.addressOf import kotlinx.cinterop.convert import kotlinx.cinterop.refTo +import kotlinx.cinterop.reinterpret import kotlinx.cinterop.usePinned import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.runBlocking @@ -19,6 +20,7 @@ import kotlinx.coroutines.withTimeout import platform.Foundation.NSData import platform.Foundation.NSDefaultRunLoopMode import platform.Foundation.NSMachPort +import platform.Foundation.NSOutputStream import platform.Foundation.NSRunLoop import platform.Foundation.NSStreamEvent import platform.Foundation.NSStreamEventEndEncountered @@ -86,6 +88,12 @@ fun NSStreamEvent.asString(): String { } } +fun ByteArray.write(to: NSOutputStream): Int { + this.usePinned { + return to.write(it.addressOf(0).reinterpret(), size.convert()).convert() + } +} + fun assertNoEmptySegments(buffer: Buffer) { assertTrue(segmentSizes(buffer).all { it != 0 }, "Expected all segments to be non-empty") } From f5d63c0da99461238b8fe9582845c448dee944a3 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Thu, 24 Aug 2023 15:59:42 -0600 Subject: [PATCH 23/23] Remove comment --- okio/src/appleTest/kotlin/okio/NSInputStreamSourceTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/okio/src/appleTest/kotlin/okio/NSInputStreamSourceTest.kt b/okio/src/appleTest/kotlin/okio/NSInputStreamSourceTest.kt index d5146fda98..072849ed6a 100644 --- a/okio/src/appleTest/kotlin/okio/NSInputStreamSourceTest.kt +++ b/okio/src/appleTest/kotlin/okio/NSInputStreamSourceTest.kt @@ -37,8 +37,6 @@ class NSInputStreamSourceTest { @OptIn(ExperimentalStdlibApi::class) @Test fun nsInputStreamSourceFromFile() { - // can be replaced with createTempFile() when #183 is fixed - // https://github.com/Kotlin/kotlinx-io/issues/183 val file = "${NSTemporaryDirectory()}${NSUUID().UUIDString()}" try { FileSystem.SYSTEM.write(file.toPath()) {