From da34f108015b38f2a45863c8b84a5cf9606c91d1 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Tue, 17 Sep 2024 15:07:14 -0400 Subject: [PATCH] Support ByteString to/from NSData conversions (#384) * Support ByteString to/from NSData conversions Closes #266 --- bytestring/api/kotlinx-io-bytestring.klib.api | 7 ++ bytestring/apple/src/ByteStringApple.kt | 47 ++++++++++++ bytestring/apple/test/ByteStringAppleTest.kt | 72 +++++++++++++++++++ bytestring/apple/test/samples/samplesApple.kt | 30 ++++++++ 4 files changed, 156 insertions(+) create mode 100644 bytestring/apple/src/ByteStringApple.kt create mode 100644 bytestring/apple/test/ByteStringAppleTest.kt create mode 100644 bytestring/apple/test/samples/samplesApple.kt diff --git a/bytestring/api/kotlinx-io-bytestring.klib.api b/bytestring/api/kotlinx-io-bytestring.klib.api index 9dcca2ee..0f090e71 100644 --- a/bytestring/api/kotlinx-io-bytestring.klib.api +++ b/bytestring/api/kotlinx-io-bytestring.klib.api @@ -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 @@ -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/ByteString // kotlinx.io.bytestring/buildByteString|buildByteString(kotlin.Int;kotlin.Function1){}[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] diff --git a/bytestring/apple/src/ByteStringApple.kt b/bytestring/apple/src/ByteStringApple.kt new file mode 100644 index 00000000..786a4ba8 --- /dev/null +++ b/bytestring/apple/src/ByteStringApple.kt @@ -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()) + ) +} diff --git a/bytestring/apple/test/ByteStringAppleTest.kt b/bytestring/apple/test/ByteStringAppleTest.kt new file mode 100644 index 00000000..702bbf5a --- /dev/null +++ b/bytestring/apple/test/ByteStringAppleTest.kt @@ -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(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()) + } +} diff --git a/bytestring/apple/test/samples/samplesApple.kt b/bytestring/apple/test/samples/samplesApple.kt new file mode 100644 index 00000000..c2d6432b --- /dev/null +++ b/bytestring/apple/test/samples/samplesApple.kt @@ -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) + } +}