Skip to content

Commit

Permalink
Support ByteString to/from NSData conversions (#384)
Browse files Browse the repository at this point in the history
* Support ByteString to/from NSData conversions

Closes #266
  • Loading branch information
fzhinkin authored Sep 17, 2024
1 parent 2b77370 commit da34f10
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 0 deletions.
7 changes: 7 additions & 0 deletions bytestring/api/kotlinx-io-bytestring.klib.api
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Klib ABI Dump
// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, js, linuxArm32Hfp, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, wasmWasi, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
// Alias: apple => [iosArm64, iosSimulatorArm64, iosX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
Expand Down Expand Up @@ -84,3 +85,9 @@ final fun kotlinx.io.bytestring/ByteString(): kotlinx.io.bytestring/ByteString /
final fun kotlinx.io.bytestring/ByteString(kotlin/ByteArray...): kotlinx.io.bytestring/ByteString // kotlinx.io.bytestring/ByteString|ByteString(kotlin.ByteArray...){}[0]
final fun kotlinx.io.bytestring/ByteString(kotlin/UByteArray...): kotlinx.io.bytestring/ByteString // kotlinx.io.bytestring/ByteString|ByteString(kotlin.UByteArray...){}[0]
final inline fun kotlinx.io.bytestring/buildByteString(kotlin/Int = ..., kotlin/Function1<kotlinx.io.bytestring/ByteStringBuilder, kotlin/Unit>): kotlinx.io.bytestring/ByteString // kotlinx.io.bytestring/buildByteString|buildByteString(kotlin.Int;kotlin.Function1<kotlinx.io.bytestring.ByteStringBuilder,kotlin.Unit>){}[0]

// Targets: [apple]
final fun (kotlinx.io.bytestring/ByteString).kotlinx.io.bytestring/toNSData(): platform.Foundation/NSData // kotlinx.io.bytestring/toNSData|toNSData@kotlinx.io.bytestring.ByteString(){}[0]

// Targets: [apple]
final fun (platform.Foundation/NSData).kotlinx.io.bytestring/toByteString(): kotlinx.io.bytestring/ByteString // kotlinx.io.bytestring/toByteString|toByteString@platform.Foundation.NSData(){}[0]
47 changes: 47 additions & 0 deletions bytestring/apple/src/ByteStringApple.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2017-2024 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.bytestring

import kotlinx.cinterop.*
import kotlinx.io.bytestring.unsafe.UnsafeByteStringApi
import kotlinx.io.bytestring.unsafe.UnsafeByteStringOperations
import platform.Foundation.NSData
import platform.Foundation.create

/**
* Returns a new [NSData] instance initialized with bytes copied from [this] ByteString.
*
* @sample kotlinx.io.bytestring.samples.ByteStringSamplesApple.nsDataConversion
*/
@OptIn(UnsafeNumber::class, BetaInteropApi::class, ExperimentalForeignApi::class)
public fun ByteString.toNSData(): NSData {
if (isEmpty()) {
return NSData()
}
val data = getBackingArrayReference()
return data.usePinned {
NSData.create(bytes = it.addressOf(0), length = data.size.convert())
}
}

/**
* Returns a new [ByteString] holding data copied from [this] NSData.
*
* @sample kotlinx.io.bytestring.samples.ByteStringSamplesApple.nsDataConversion
*/
@OptIn(ExperimentalForeignApi::class, UnsafeNumber::class, UnsafeByteStringApi::class)
public fun NSData.toByteString(): ByteString {
val l = length.toLong()
if (l == 0L) {
return ByteString.EMPTY
}
if (l > Int.MAX_VALUE) {
throw IllegalArgumentException("NSData content is to long to read as byte array: $l")
}
return UnsafeByteStringOperations.wrapUnsafe(
bytes!!.readBytes(l.toInt())
)
}
72 changes: 72 additions & 0 deletions bytestring/apple/test/ByteStringAppleTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2017-2024 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.bytestring

import kotlinx.cinterop.*
import kotlinx.io.bytestring.unsafe.UnsafeByteStringApi
import kotlinx.io.bytestring.unsafe.UnsafeByteStringOperations
import platform.Foundation.NSData
import platform.Foundation.create
import platform.posix.memset
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.test.*

@OptIn(UnsafeNumber::class)
class ByteStringAppleTest {
@OptIn(ExperimentalForeignApi::class)
@Test
fun toNSData() {
val emptyData = ByteString().toNSData()
assertEquals(0u, emptyData.length)

val copy = ByteString(0, 1, 2, 3, 4, 5).toNSData()
assertContentEquals(byteArrayOf(0, 1, 2, 3, 4, 5), copy.bytes!!.readBytes(copy.length.convert()))
}

@OptIn(BetaInteropApi::class, ExperimentalEncodingApi::class)
@Test
fun fromNSData() {
assertTrue(NSData().toByteString().isEmpty())
val src = NSData.create(
base64EncodedString = Base64.Default.encode(byteArrayOf(0, 1, 2, 3, 4, 5)),
options = 0u
)!!
val copy = src.toByteString()
assertContentEquals(byteArrayOf(0, 1, 2, 3, 4, 5), copy.toByteArray())
}

@OptIn(UnsafeByteStringApi::class, ExperimentalForeignApi::class)
@Test
fun toNSDataDataIntegrity() {
val mutableArray = byteArrayOf(0, 0, 0, 0, 0, 0)
// Don't try that at home, kids!
val cursedString = UnsafeByteStringOperations.wrapUnsafe(mutableArray)
val nsData = cursedString.toNSData()

mutableArray.fill(42)
// NSData should hold a copy
assertContentEquals(ByteArray(6), nsData.bytes!!.readBytes(6))
}

@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
@Test
fun fromNSDataIntegrity() = memScoped {
val length = 6
val data = allocArray<ByteVar>(length)
memset(data, 0, length.convert())

val cursedData = NSData.create(
bytesNoCopy = data, length = length.convert(),
freeWhenDone = false
)

val byteString = cursedData.toByteString()
memset(data, 42, length.convert())

assertContentEquals(ByteArray(length), byteString.toByteArray())
}
}
30 changes: 30 additions & 0 deletions bytestring/apple/test/samples/samplesApple.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2017-2024 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.bytestring.samples

import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.UnsafeNumber
import kotlinx.io.bytestring.*
import platform.Foundation.*
import kotlin.test.*

class ByteStringSamplesApple {
@OptIn(UnsafeNumber::class, ExperimentalForeignApi::class, ExperimentalStdlibApi::class)
@Test
fun nsDataConversion() {
val originalByteString: ByteString = "Compress me, please!".encodeToByteString()

val compressedNSData: NSData = originalByteString.toNSData().compressedDataUsingAlgorithm(
algorithm = NSDataCompressionAlgorithmZlib,
error = null
)!!

val compressedByteString: ByteString = compressedNSData.toByteString()
assertEquals("73cecf2d284a2d2e56c84dd55128c8494d2c4e550400", compressedByteString.toHexString())
// If there's no zlib-flate on your path, you can test it using:
// zlib.decompress(binascii.unhexlify("73cecf2d284a2d2e56c84dd55128c8494d2c4e550400"), -15)
}
}

0 comments on commit da34f10

Please sign in to comment.