Skip to content

Commit de7408f

Browse files
committed
Okio NSInputeStream Source extensions
Until square/okio#1123 is merged and released.
1 parent a9c206e commit de7408f

File tree

7 files changed

+452
-2
lines changed

7 files changed

+452
-2
lines changed

couchbase-lite/src/appleMain/kotlin/com/couchbase/lite/kmp/Blob.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import com.udobny.kmp.DelegatedClass
77
import com.udobny.kmp.ext.toByteArray
88
import com.udobny.kmp.ext.toNSData
99
import okio.*
10+
import okio.temp.inputStream
11+
import okio.temp.source
1012
import platform.Foundation.*
1113

1214
public actual class Blob
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright (C) 2020 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// TODO: workaround until these extensions are merged and released in Okio
18+
// https://github.com/square/okio/pull/1123
19+
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "INVISIBLE_SETTER")
20+
21+
package okio.temp
22+
23+
import kotlinx.cinterop.CPointer
24+
import kotlinx.cinterop.CPointerVar
25+
import kotlinx.cinterop.Pinned
26+
import kotlinx.cinterop.UnsafeNumber
27+
import kotlinx.cinterop.addressOf
28+
import kotlinx.cinterop.convert
29+
import kotlinx.cinterop.pin
30+
import kotlinx.cinterop.pointed
31+
import kotlinx.cinterop.reinterpret
32+
import kotlinx.cinterop.usePinned
33+
import kotlinx.cinterop.value
34+
import okio.*
35+
import platform.Foundation.NSData
36+
import platform.Foundation.NSError
37+
import platform.Foundation.NSInputStream
38+
import platform.Foundation.NSLocalizedDescriptionKey
39+
import platform.Foundation.NSUnderlyingErrorKey
40+
import platform.darwin.NSInteger
41+
import platform.darwin.NSUInteger
42+
import platform.darwin.NSUIntegerVar
43+
import platform.posix.memcpy
44+
import platform.posix.uint8_tVar
45+
46+
/** Returns an input stream that reads from this source. */
47+
fun BufferedSource.inputStream(): NSInputStream = BufferedSourceInputStream(this)
48+
49+
@OptIn(UnsafeNumber::class)
50+
private class BufferedSourceInputStream(
51+
private val bufferedSource: BufferedSource,
52+
) : NSInputStream(NSData()) {
53+
54+
private var error: NSError? = null
55+
private var pinnedBuffer: Pinned<ByteArray>? = null
56+
57+
override fun streamError(): NSError? = error
58+
59+
override fun open() {
60+
// no-op
61+
}
62+
63+
override fun read(buffer: CPointer<uint8_tVar>?, maxLength: NSUInteger): NSInteger {
64+
try {
65+
if (bufferedSource is RealBufferedSource) {
66+
if (bufferedSource.closed) throw IOException("closed")
67+
if (bufferedSource.exhausted()) return 0
68+
}
69+
70+
val toRead = minOf(maxLength.toInt(), bufferedSource.buffer.size).toInt()
71+
return bufferedSource.buffer.readNative(buffer, toRead).convert()
72+
} catch (e: Exception) {
73+
error = e.toNSError()
74+
return -1
75+
}
76+
}
77+
78+
override fun getBuffer(
79+
buffer: CPointer<CPointerVar<uint8_tVar>>?,
80+
length: CPointer<NSUIntegerVar>?,
81+
): Boolean {
82+
if (bufferedSource.buffer.size > 0) {
83+
bufferedSource.buffer.head?.let { s ->
84+
pinnedBuffer?.unpin()
85+
s.data.pin().let {
86+
pinnedBuffer = it
87+
buffer?.pointed?.value = it.addressOf(s.pos).reinterpret()
88+
length?.pointed?.value = (s.limit - s.pos).convert()
89+
return true
90+
}
91+
}
92+
}
93+
return false
94+
}
95+
96+
override fun hasBytesAvailable(): Boolean = bufferedSource.buffer.size > 0
97+
98+
override fun close() {
99+
pinnedBuffer?.unpin()
100+
pinnedBuffer = null
101+
bufferedSource.close()
102+
}
103+
104+
override fun description(): String = "$bufferedSource.inputStream()"
105+
106+
private fun Exception.toNSError(): NSError {
107+
return NSError(
108+
"Kotlin",
109+
0,
110+
mapOf(
111+
NSLocalizedDescriptionKey to message,
112+
NSUnderlyingErrorKey to this,
113+
),
114+
)
115+
}
116+
117+
private fun Buffer.readNative(sink: CPointer<uint8_tVar>?, maxLength: Int): Int {
118+
val s = head ?: return 0
119+
val toCopy = minOf(maxLength, s.limit - s.pos)
120+
s.data.usePinned {
121+
memcpy(sink, it.addressOf(s.pos), toCopy.convert())
122+
}
123+
124+
s.pos += toCopy
125+
size -= toCopy.toLong()
126+
127+
if (s.pos == s.limit) {
128+
head = s.pop()
129+
SegmentPool.recycle(s)
130+
}
131+
132+
return toCopy
133+
}
134+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright (C) 2020 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// TODO: workaround until these extensions are merged and released in Okio
18+
// https://github.com/square/okio/pull/1123
19+
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "INVISIBLE_SETTER")
20+
21+
package okio.temp
22+
23+
import kotlinx.cinterop.UnsafeNumber
24+
import kotlinx.cinterop.addressOf
25+
import kotlinx.cinterop.convert
26+
import kotlinx.cinterop.reinterpret
27+
import kotlinx.cinterop.usePinned
28+
import okio.*
29+
import platform.Foundation.NSInputStream
30+
import platform.darwin.UInt8Var
31+
32+
/** Returns a source that reads from `in`. */
33+
fun NSInputStream.source(): Source = NSInputStreamSource(this)
34+
35+
@OptIn(UnsafeNumber::class)
36+
private open class NSInputStreamSource(
37+
private val input: NSInputStream,
38+
) : Source {
39+
40+
init {
41+
input.open()
42+
}
43+
44+
override fun read(sink: Buffer, byteCount: Long): Long {
45+
if (byteCount == 0L) return 0L
46+
require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
47+
val tail = sink.writableSegment(1)
48+
val maxToCopy = minOf(byteCount, Segment.SIZE - tail.limit)
49+
val bytesRead = tail.data.usePinned {
50+
val bytes = it.addressOf(tail.limit).reinterpret<UInt8Var>()
51+
input.read(bytes, maxToCopy.convert()).toLong()
52+
}
53+
if (bytesRead < 0) throw IOException(input.streamError?.localizedDescription)
54+
if (bytesRead == 0L) {
55+
if (tail.pos == tail.limit) {
56+
// We allocated a tail segment, but didn't end up needing it. Recycle!
57+
sink.head = tail.pop()
58+
SegmentPool.recycle(tail)
59+
}
60+
return -1
61+
}
62+
tail.limit += bytesRead.toInt()
63+
sink.size += bytesRead
64+
return bytesRead.convert()
65+
}
66+
67+
override fun close() = input.close()
68+
69+
override fun timeout() = Timeout.NONE
70+
71+
override fun toString() = "source($input)"
72+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright (C) 2020 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// TODO: workaround until these extensions are merged and released in Okio
18+
// https://github.com/square/okio/pull/1123
19+
@file:Suppress("INVISIBLE_MEMBER")
20+
21+
package okio.temp
22+
23+
import kotlin.test.Test
24+
import kotlin.test.assertEquals
25+
import kotlin.test.assertFalse
26+
import kotlin.test.assertNotNull
27+
import kotlin.test.assertTrue
28+
import kotlinx.cinterop.CPointerVar
29+
import kotlinx.cinterop.addressOf
30+
import kotlinx.cinterop.alloc
31+
import kotlinx.cinterop.convert
32+
import kotlinx.cinterop.get
33+
import kotlinx.cinterop.memScoped
34+
import kotlinx.cinterop.ptr
35+
import kotlinx.cinterop.reinterpret
36+
import kotlinx.cinterop.usePinned
37+
import kotlinx.cinterop.value
38+
import okio.*
39+
import platform.Foundation.NSInputStream
40+
import platform.darwin.NSUIntegerVar
41+
import platform.darwin.UInt8Var
42+
43+
class AppleBufferedSourceTest {
44+
@Test fun bufferInputStream() {
45+
val source = Buffer()
46+
source.writeUtf8("abc")
47+
testInputStream(source.inputStream())
48+
}
49+
50+
@Test fun realBufferedSourceInputStream() {
51+
val source = Buffer()
52+
source.writeUtf8("abc")
53+
testInputStream(RealBufferedSource(source).inputStream())
54+
}
55+
56+
private fun testInputStream(nsis: NSInputStream) {
57+
nsis.open()
58+
val byteArray = ByteArray(4)
59+
byteArray.usePinned {
60+
val cPtr = it.addressOf(0).reinterpret<UInt8Var>()
61+
62+
byteArray.fill(-5)
63+
assertEquals(3, nsis.read(cPtr, 4U))
64+
assertEquals("[97, 98, 99, -5]", byteArray.contentToString())
65+
66+
byteArray.fill(-7)
67+
assertEquals(0, nsis.read(cPtr, 4U))
68+
assertEquals("[-7, -7, -7, -7]", byteArray.contentToString())
69+
}
70+
}
71+
72+
@Test fun nsInputStreamGetBuffer() {
73+
val source = Buffer()
74+
source.writeUtf8("abc")
75+
76+
val nsis = source.inputStream()
77+
nsis.open()
78+
assertTrue(nsis.hasBytesAvailable)
79+
80+
memScoped {
81+
val bufferPtr = alloc<CPointerVar<UInt8Var>>()
82+
val lengthPtr = alloc<NSUIntegerVar>()
83+
assertTrue(nsis.getBuffer(bufferPtr.ptr, lengthPtr.ptr))
84+
85+
val length = lengthPtr.value
86+
assertNotNull(length)
87+
assertEquals(3.convert(), length)
88+
89+
val buffer = bufferPtr.value
90+
assertNotNull(buffer)
91+
assertEquals('a'.code.convert(), buffer[0])
92+
assertEquals('b'.code.convert(), buffer[1])
93+
assertEquals('c'.code.convert(), buffer[2])
94+
}
95+
}
96+
97+
@Test fun nsInputStreamClose() {
98+
val buffer = Buffer()
99+
buffer.writeUtf8("abc")
100+
val source = RealBufferedSource(buffer)
101+
assertFalse(source.closed)
102+
103+
val nsis = source.inputStream()
104+
nsis.open()
105+
nsis.close()
106+
assertTrue(source.closed)
107+
108+
val byteArray = ByteArray(4)
109+
byteArray.usePinned {
110+
val cPtr = it.addressOf(0).reinterpret<UInt8Var>()
111+
112+
byteArray.fill(-5)
113+
assertEquals(-1, nsis.read(cPtr, 4U))
114+
assertNotNull(nsis.streamError)
115+
assertEquals("closed", nsis.streamError?.localizedDescription)
116+
assertEquals("[-5, -5, -5, -5]", byteArray.contentToString())
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)