Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c0f042f
NSInputStream.asSource() and Source.asNSInputStream()
jeffdgr8 Jul 12, 2023
13bf3b1
Move Exception.toNSError() to -Util file
jeffdgr8 Jul 12, 2023
3386ab3
Make isClosed explicitly private
jeffdgr8 Jul 12, 2023
6b4bb80
Code review feedback
jeffdgr8 Jul 12, 2023
8ca65a2
Implement NSStreamStatus
jeffdgr8 Jul 12, 2023
2be70a6
Open NSInputStream on first read
jeffdgr8 Jul 12, 2023
54e247c
Unknown error when no streamError description
jeffdgr8 Jul 12, 2023
dccd22a
NSOutputStream.asSink() and Sink.asNSOutputStream()
jeffdgr8 Jul 13, 2023
2ba8bed
Support SinkNSOutputStream NSStreamDataWrittenToMemoryStreamKey
jeffdgr8 Jul 13, 2023
d7b8e1d
Override SourceNSInputStream.propertyForKey as no-op
jeffdgr8 Jul 13, 2023
62757c5
Mark status property @Volatile
jeffdgr8 Jul 13, 2023
613e2be
Code review feedback and fixes
jeffdgr8 Jul 13, 2023
e5f5c27
Fix reading byte as int
jeffdgr8 Jul 17, 2023
f5dfc1a
Buffer.snapshotAsNSData() for NSStreamDataWrittenToMemoryStreamKey
jeffdgr8 Jul 17, 2023
c9d1f44
Test SinkNSOutputStream with data longer than Segment
jeffdgr8 Jul 17, 2023
242fda0
Open streams on init
jeffdgr8 Jul 18, 2023
ead4f78
Update core/apple/src/-Util.kt
jeffdgr8 Jul 18, 2023
f9c9305
Update core/apple/src/BuffersApple.kt
jeffdgr8 Jul 18, 2023
18ff1d0
Update core/apple/test/NSOutputStreamSinkTest.kt
jeffdgr8 Jul 18, 2023
c4eb1b9
Update core/apple/test/SinkNSOutputStreamTest.kt
jeffdgr8 Jul 18, 2023
56bb0e4
Update core/apple/test/samples/samplesApple.kt
jeffdgr8 Jul 18, 2023
9d53c8e
Update core/apple/test/utilApple.kt
jeffdgr8 Jul 18, 2023
4e3ff87
Add samplesApple.kt to Dokka samples
jeffdgr8 Jul 18, 2023
aa5830c
Verify buffer != null and maxLength >= 0
jeffdgr8 Jul 18, 2023
6f076d9
Check isClosed() in SinkNSOutputStream.streamStatus
jeffdgr8 Jul 18, 2023
f789518
Use assertFailsWith
jeffdgr8 Jul 18, 2023
feb3145
Update core/apple/src/BuffersApple.kt
jeffdgr8 Jul 18, 2023
3508104
Update core/apple/src/SinksApple.kt
jeffdgr8 Jul 18, 2023
9e71d4b
Don't close a stream with the error status
jeffdgr8 Jul 18, 2023
f40d472
Use malloc and NSData.dataWithBytesNoCopy:length:
jeffdgr8 Jul 18, 2023
caba9b4
Better variable names
jeffdgr8 Jul 18, 2023
afab5b7
Use uint8_tVar (same typealias, but matches function signature)
jeffdgr8 Jul 18, 2023
365c354
Test SourceNSInputStream with long input data
jeffdgr8 Jul 18, 2023
a19c463
Add apple source set to bytestring module
jeffdgr8 Jul 18, 2023
0021412
Add NSInputStream from file test
jeffdgr8 Jul 19, 2023
211c5f5
Remove @Volatile annotations
jeffdgr8 Jul 19, 2023
ae33893
Remove getBuffer() implementation
jeffdgr8 Jul 19, 2023
5d14977
createTempFile() not working on Apple platforms
jeffdgr8 Jul 19, 2023
5cfafa2
Merge remote-tracking branch 'upstream/develop' into nsinputstream
jeffdgr8 Jul 19, 2023
e9fcaeb
Add apple source set
jeffdgr8 Jul 19, 2023
50fe63f
SourceNSInputStream run loop delegate support
jeffdgr8 Jul 20, 2023
89a4f80
Test subscribe after open
jeffdgr8 Jul 20, 2023
e296c01
Check run loop on postEvent()
jeffdgr8 Jul 20, 2023
7b3ab06
SinkNSOutputStream run loop delegate support
jeffdgr8 Jul 20, 2023
c74845e
lockWithTimeout() with better failure logging
jeffdgr8 Jul 22, 2023
d2d040d
Synchronize access to read variable for entire event handler
jeffdgr8 Jul 22, 2023
630423c
Only catch TimeoutCancellationException
jeffdgr8 Jul 22, 2023
110e56c
Code review feedback and fixes
jeffdgr8 Aug 1, 2023
6d034e9
Check for NSStreamStatusAtEnd after read
jeffdgr8 Aug 2, 2023
ed5902e
Post NSStreamEventErrorOccurred on exhausted error
jeffdgr8 Aug 2, 2023
07318d0
Revert check for NSStreamStatusAtEnd after read
jeffdgr8 Aug 2, 2023
4897741
Add suggested doc comments
jeffdgr8 Aug 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions buildSrc/src/main/kotlin/Platforms.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ fun KotlinMultiplatformExtension.configureNativePlatforms() {
mingwX64()
}

private val appleTargets = listOf(
val appleTargets = listOf(
"iosArm64",
"iosX64",
"iosSimulatorArm64",
Expand Down Expand Up @@ -64,7 +64,7 @@ private val androidTargets = listOf(
"androidNativeX86"
)

val nativeTargets = appleTargets + linuxTargets + mingwTargets + androidTargets
val nativeTargets = linuxTargets + mingwTargets + androidTargets

/**
* Creates a source set for a directory that isn't already a built-in platform. Use this to create
Expand Down
16 changes: 16 additions & 0 deletions core/apple/src/-Util.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package kotlinx.io

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
)
)
56 changes: 56 additions & 0 deletions core/apple/src/AppleCore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2017-2023 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
*/

package kotlinx.io

import kotlinx.cinterop.*
import platform.Foundation.NSInputStream
import platform.darwin.UInt8Var

/**
* Returns [RawSource] that reads from an input stream.
*
* Use [RawSource.buffered] to create a buffered source from it.
*
* @sample kotlinx.io.samples.KotlinxIoSamplesApple.inputStreamAsSource
*/
public fun NSInputStream.asSource(): RawSource = NSInputStreamSource(this)

private open class NSInputStreamSource(
private val input: NSInputStream,
) : RawSource {

init {
input.open()
}

@OptIn(UnsafeNumber::class)
override fun readAtMostTo(sink: Buffer, byteCount: Long): Long {
if (byteCount == 0L) return 0L
checkByteCount(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<UInt8Var>()
input.read(bytes, maxToCopy.convert()).toLong()
}
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 toString() = "source($input)"
}
98 changes: 98 additions & 0 deletions core/apple/src/SourcesApple.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2017-2023 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
*/

package kotlinx.io

import kotlinx.cinterop.*
import platform.Foundation.*
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. Closing the stream will also close this source.
*/
public fun Source.asNSInputStream(): NSInputStream = SourceNSInputStream(this)

@OptIn(InternalIoApi::class, UnsafeNumber::class)
private class SourceNSInputStream(
private val source: Source,
) : NSInputStream(NSData()) {

private val isClosed: () -> Boolean = when (source) {
is RealSource -> source::closed
is Buffer -> {
{ false }
}
}

private var error: NSError? = null
private var pinnedBuffer: Pinned<ByteArray>? = null

override fun streamError(): NSError? = error

override fun open() {
// no-op
}

override fun read(buffer: CPointer<uint8_tVar>?, maxLength: NSUInteger): NSInteger {
try {
if (isClosed()) throw IOException("Underlying source is closed.")
if (source.exhausted()) {
return 0
}
val toRead = minOf(maxLength.toInt(), source.buffer.size).toInt()
return source.buffer.readNative(buffer, toRead).convert()
} catch (e: Exception) {
error = e.toNSError()
return -1
}
}

override fun getBuffer(buffer: CPointer<CPointerVar<uint8_tVar>>?, length: CPointer<NSUIntegerVar>?): Boolean {
if (source.buffer.size > 0) {
source.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
}
}
}
return false
}

override fun hasBytesAvailable() = source.buffer.size > 0

override fun close() {
pinnedBuffer?.unpin()
pinnedBuffer = null
source.close()
}

override fun description() = "$source.inputStream()"

private fun Buffer.readNative(sink: CPointer<uint8_tVar>?, 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
}
}
73 changes: 73 additions & 0 deletions core/apple/test/NSInputStreamSourceTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2017-2023 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
*/

package kotlinx.io

import platform.Foundation.NSInputStream
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.fail

private const val SEGMENT_SIZE = Segment.SIZE

class NSInputStreamSourceTest {
@Test
fun nsInputStreamSource() {
val nsis = NSInputStream(byteArrayOf(0x61).toNSData())
val source = nsis.asSource()
val buffer = Buffer()
source.readAtMostTo(buffer, 1)
assertEquals("a", buffer.readString())
}

@Test
fun sourceFromInputStream() {
val nsis = NSInputStream(
("a" + "b".repeat(SEGMENT_SIZE * 2) + "c").encodeToByteArray().toNSData(),
)

// Source: ab...bc
val source: RawSource = nsis.asSource()
val sink = Buffer()

// Source: b...bc. Sink: abb.
assertEquals(3, source.readAtMostTo(sink, 3))
assertEquals("abb", sink.readString(3))

// Source: b...bc. Sink: b...b.
assertEquals(SEGMENT_SIZE.toLong(), source.readAtMostTo(sink, 20000))
assertEquals("b".repeat(SEGMENT_SIZE), sink.readString())

// Source: b...bc. Sink: b...bc.
assertEquals((SEGMENT_SIZE - 1).toLong(), source.readAtMostTo(sink, 20000))
assertEquals("b".repeat(SEGMENT_SIZE - 2) + "c", sink.readString())

// Source and sink are empty.
assertEquals(-1, source.readAtMostTo(sink, 1))
}

@Test
fun sourceFromInputStreamWithSegmentSize() {
val nsis = NSInputStream(ByteArray(SEGMENT_SIZE).toNSData())
val source = nsis.asSource()
val sink = Buffer()

assertEquals(SEGMENT_SIZE.toLong(), source.readAtMostTo(sink, SEGMENT_SIZE.toLong()))
assertEquals(-1, source.readAtMostTo(sink, SEGMENT_SIZE.toLong()))

assertNoEmptySegments(sink)
}

@Test
fun sourceFromInputStreamBounds() {
val source = NSInputStream(ByteArray(100).toNSData()).asSource()
try {
source.readAtMostTo(Buffer(), -1)
fail()
} catch (expected: IllegalArgumentException) {
// expected
}
}
}
95 changes: 95 additions & 0 deletions core/apple/test/SourceNSInputStreamTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2017-2023 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
*/

package kotlinx.io

import kotlinx.cinterop.*
import platform.Foundation.NSInputStream
import platform.darwin.NSUIntegerVar
import platform.darwin.UInt8Var
import kotlin.test.*

@OptIn(UnsafeNumber::class)
class SourceNSInputStreamTest {
@Test
fun bufferInputStream() {
val source = Buffer()
source.writeString("abc")
testInputStream(source.asNSInputStream())
}

@Test
fun realBufferedSourceInputStream() {
val source = Buffer()
source.writeString("abc")
testInputStream(RealSource(source).asNSInputStream())
}

private fun testInputStream(nsis: NSInputStream) {
nsis.open()
val byteArray = ByteArray(4)
byteArray.usePinned {
val cPtr = it.addressOf(0).reinterpret<UInt8Var>()

byteArray.fill(-5)
assertEquals(3, nsis.read(cPtr, 4U))
assertEquals("[97, 98, 99, -5]", byteArray.contentToString())

byteArray.fill(-7)
assertEquals(0, nsis.read(cPtr, 4U))
assertEquals("[-7, -7, -7, -7]", byteArray.contentToString())
}
}

@Test
fun nsInputStreamGetBuffer() {
val source = Buffer()
source.writeString("abc")

val nsis = source.asNSInputStream()
nsis.open()
assertTrue(nsis.hasBytesAvailable)

memScoped {
val bufferPtr = alloc<CPointerVar<UInt8Var>>()
val lengthPtr = alloc<NSUIntegerVar>()
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.writeString("abc")
val source = RealSource(buffer)
assertFalse(source.closed)

val nsis = source.asNSInputStream()
nsis.open()
nsis.close()
assertTrue(source.closed)

val byteArray = ByteArray(4)
byteArray.usePinned {
val cPtr = it.addressOf(0).reinterpret<UInt8Var>()

byteArray.fill(-5)
assertEquals(-1, nsis.read(cPtr, 4U))
assertNotNull(nsis.streamError)
assertEquals("Underlying source is closed.", nsis.streamError?.localizedDescription)
assertEquals("[-5, -5, -5, -5]", byteArray.contentToString())
}
}
}
17 changes: 17 additions & 0 deletions core/apple/test/samples/samplesApple.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package kotlinx.io.samples

import kotlinx.io.*
import platform.Foundation.NSInputStream
import kotlin.test.Test
import kotlin.test.assertContentEquals

class KotlinxIoSamplesApple {
@Test
fun inputStreamAsSource() {
val data = ByteArray(100) { it.toByte() }
val inputStream = NSInputStream(data.toNSData())

val receivedData = inputStream.asSource().buffered().readByteArray()
assertContentEquals(data, receivedData)
}
}
18 changes: 18 additions & 0 deletions core/apple/test/utilApple.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kotlinx.io

import kotlinx.cinterop.UnsafeNumber
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.convert
import kotlinx.cinterop.usePinned
import platform.Foundation.NSData
import platform.Foundation.create
import platform.Foundation.data

fun ByteArray.toNSData() = if (isNotEmpty()) {
usePinned {
@OptIn(UnsafeNumber::class)
NSData.create(bytes = it.addressOf(0), length = size.convert())
}
} else {
NSData.data()
}
Loading