diff --git a/example/src/benchmarks/encoding/encoding.ts b/example/src/benchmarks/encoding/encoding.ts index be7364af..17290a37 100644 --- a/example/src/benchmarks/encoding/encoding.ts +++ b/example/src/benchmarks/encoding/encoding.ts @@ -3,6 +3,9 @@ import { stringToBuffer, Buffer as CraftzdogBuffer, } from 'react-native-quick-crypto'; +// For utf16le, the native implementation could be disabled for non-Hermes runtimes or older versions of RN. +// Use the fallbacks to meature the performance without causing errors, even if it could use Buffer polyfill. +import { ab2str, binaryLikeToArrayBuffer } from 'react-native-quick-crypto'; import type { BenchFn } from '../../types/benchmarks'; import { Bench } from 'tinybench'; @@ -22,28 +25,37 @@ function stringToBuffer_old( } // Generate test data -const generate1MB = (): ArrayBuffer => { - const bytes = new Uint8Array(1024 * 1024); - for (let i = 0; i < bytes.length; i++) { - bytes[i] = i & 0xff; +const generateData = (size: number, asciiOnly: boolean = true): ArrayBuffer => { + if (size < 2 || size % 2 !== 0) { + throw new Error('Size must be at least 2 and even'); + } + const bytes = new Uint8Array(size); // Implicitly filled with 0 + // Fill ASCII characters in UTF-16LE code units, which can also be represented as binary/ASCII/Latin1/UTF-8 + for (let i = 0; i < bytes.length; i += 2) { + bytes[i] = i & 0x7f; + } + if (!asciiOnly) { + // \xC3\xA9 in UTF-8 or \uA9C3 in UTF-16LE + bytes[0] = 0xc3; + bytes[1] = 0xa9; } return bytes.buffer as ArrayBuffer; }; -const ab1MB = generate1MB(); -const ab32B = new Uint8Array(32).buffer as ArrayBuffer; // typical hash digest size -// Fill 32B with non-zero data -new Uint8Array(ab32B).set([ - 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe, 0x01, 0x23, 0x45, 0x67, 0x89, - 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10, 0x11, 0x22, - 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, -]); +const ab1MB_ascii = generateData(1024 * 1024, true); +const ab1MB = generateData(1024 * 1024, false); +const ab32B_ascii = generateData(32, true); +const ab32B = generateData(32, false); // Pre-encode strings for decode benchmarks -const hex1MB = bufferToString(ab1MB, 'hex'); -const base64_1MB = bufferToString(ab1MB, 'base64'); -const hex32B = bufferToString(ab32B, 'hex'); -const base64_32B = bufferToString(ab32B, 'base64'); +const hex_1MB = ab2str(ab1MB, 'hex'); +const hex_32B = ab2str(ab32B, 'hex'); +const base64_1MB = ab2str(ab1MB, 'base64'); +const base64_32B = ab2str(ab32B, 'base64'); +const utf16le_1MB_ascii = ab2str(ab1MB_ascii, 'utf16le'); +const utf16le_32B_ascii = ab2str(ab32B_ascii, 'utf16le'); +const utf16le_1MB_non_ascii = ab2str(ab1MB, 'utf16le'); +const utf16le_32B_non_ascii = ab2str(ab32B, 'utf16le'); // --- Encode benchmarks (ArrayBuffer → string) --- @@ -123,6 +135,82 @@ const encode_base64_1mb: BenchFn = () => { return bench; }; +const encode_utf16le_32b: BenchFn = () => { + const bench = new Bench({ + name: 'utf16le encode 32B', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + ab2str(ab32B, 'utf16le'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab32B, 'utf16le'); + }); + + return bench; +}; + +const encode_utf16le_1mb: BenchFn = () => { + const bench = new Bench({ + name: 'utf16le encode 1MB', + iterations: 10, + warmupIterations: 2, + time: 0, + }); + + bench + .add('rnqc', () => { + ab2str(ab1MB, 'utf16le'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab1MB, 'utf16le'); + }); + + return bench; +}; + +const encode_utf16le_32b_ascii: BenchFn = () => { + const bench = new Bench({ + name: 'utf16le encode 32B (ASCII only)', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + ab2str(ab32B_ascii, 'utf16le'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab32B_ascii, 'utf16le'); + }); + + return bench; +}; + +const encode_utf16le_1mb_ascii: BenchFn = () => { + const bench = new Bench({ + name: 'utf16le encode 1MB (ASCII only)', + iterations: 10, + warmupIterations: 2, + time: 0, + }); + + bench + .add('rnqc', () => { + ab2str(ab1MB_ascii, 'utf16le'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab1MB_ascii, 'utf16le'); + }); + + return bench; +}; + // --- Decode benchmarks (string → ArrayBuffer) --- const decode_hex_32b: BenchFn = () => { @@ -135,10 +223,10 @@ const decode_hex_32b: BenchFn = () => { bench .add('rnqc', () => { - stringToBuffer(hex32B, 'hex'); + stringToBuffer(hex_32B, 'hex'); }) .add('Buffer polyfill', () => { - stringToBuffer_old(hex32B, 'hex'); + stringToBuffer_old(hex_32B, 'hex'); }); return bench; @@ -154,10 +242,10 @@ const decode_hex_1mb: BenchFn = () => { bench .add('rnqc', () => { - stringToBuffer(hex1MB, 'hex'); + stringToBuffer(hex_1MB, 'hex'); }) .add('Buffer polyfill', () => { - stringToBuffer_old(hex1MB, 'hex'); + stringToBuffer_old(hex_1MB, 'hex'); }); return bench; @@ -201,13 +289,97 @@ const decode_base64_1mb: BenchFn = () => { return bench; }; +const decode_utf16le_32b: BenchFn = () => { + const bench = new Bench({ + name: 'utf16le decode 32B', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + binaryLikeToArrayBuffer(utf16le_32B_non_ascii, 'utf16le'); + }) + .add('Buffer polyfill', () => { + stringToBuffer_old(utf16le_32B_non_ascii, 'utf16le'); + }); + + return bench; +}; + +const decode_utf16le_1mb: BenchFn = () => { + const bench = new Bench({ + name: 'utf16le decode 1MB', + iterations: 10, + warmupIterations: 2, + time: 0, + }); + + bench + .add('rnqc', () => { + binaryLikeToArrayBuffer(utf16le_1MB_non_ascii, 'utf16le'); + }) + .add('Buffer polyfill', () => { + stringToBuffer_old(utf16le_1MB_non_ascii, 'utf16le'); + }); + + return bench; +}; + +const decode_utf16le_32b_ascii: BenchFn = () => { + const bench = new Bench({ + name: 'utf16le decode 32B (ASCII only)', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + binaryLikeToArrayBuffer(utf16le_32B_ascii, 'utf16le'); + }) + .add('Buffer polyfill', () => { + stringToBuffer_old(utf16le_32B_ascii, 'utf16le'); + }); + + return bench; +}; + +const decode_utf16le_1mb_ascii: BenchFn = () => { + const bench = new Bench({ + name: 'utf16le decode 1MB (ASCII only)', + iterations: 10, + warmupIterations: 2, + time: 0, + }); + + bench + .add('rnqc', () => { + binaryLikeToArrayBuffer(utf16le_1MB_ascii, 'utf16le'); + }) + .add('Buffer polyfill', () => { + stringToBuffer_old(utf16le_1MB_ascii, 'utf16le'); + }); + + return bench; +}; + export default [ encode_hex_32b, encode_hex_1mb, encode_base64_32b, encode_base64_1mb, + encode_utf16le_32b, + encode_utf16le_1mb, + encode_utf16le_32b_ascii, + encode_utf16le_1mb_ascii, decode_hex_32b, decode_hex_1mb, decode_base64_32b, decode_base64_1mb, + decode_utf16le_32b, + decode_utf16le_1mb, + decode_utf16le_32b_ascii, + decode_utf16le_1mb_ascii, ]; diff --git a/example/src/tests/utils/encoding_tests.ts b/example/src/tests/utils/encoding_tests.ts index 24e3c362..12408e0d 100644 --- a/example/src/tests/utils/encoding_tests.ts +++ b/example/src/tests/utils/encoding_tests.ts @@ -578,6 +578,71 @@ test( }, ); +// --- UTF-16LE --- + +test(SUITE, '[Node.js] Roundtrips ASCII text through utf16le encoding.', () => { + const str = 'foo'; + const ab = stringToBuffer(str, 'utf16le'); + expect(bufferToString(ab, 'utf16le')).to.equal(str); +}); + +test( + SUITE, + 'Roundtrips UTF-16LE text containing an unpaired high surrogate.', + () => { + const str = 'A\uD83DB'; + const ab = stringToBuffer(str, 'utf16le'); + expect(toU8(ab)).to.deep.equal( + new Uint8Array([0x41, 0x00, 0x3d, 0xd8, 0x42, 0x00]), + ); + expect(bufferToString(ab, 'utf16le')).to.equal(str); + }, +); + +test( + SUITE, + 'Roundtrips UTF-16LE text containing an unpaired low surrogate.', + () => { + const str = 'A\uDC00B'; + const ab = stringToBuffer(str, 'utf16le'); + expect(toU8(ab)).to.deep.equal( + new Uint8Array([0x41, 0x00, 0x00, 0xdc, 0x42, 0x00]), + ); + expect(bufferToString(ab, 'utf16le')).to.equal(str); + }, +); + +test(SUITE, '[Node.js] UTF-16LE encoding of "über"', () => { + expect(toU8(stringToBuffer('über', 'utf16le'))).to.deep.equal( + new Uint8Array([252, 0, 98, 0, 101, 0, 114, 0]), + ); +}); + +test(SUITE, '[Node.js] UTF-16LE encoding of "привет"', () => { + const encoded = toU8(stringToBuffer('привет', 'utf16le')); + expect(encoded).to.deep.equal( + new Uint8Array([63, 4, 64, 4, 56, 4, 50, 4, 53, 4, 66, 4]), + ); + expect(bufferToString(encoded.buffer as ArrayBuffer, 'utf16le')).to.equal( + 'привет', + ); +}); + +test(SUITE, '[Node.js] UTF-16LE encoding of Thumbs up sign (U+1F44D)', () => { + expect(toU8(stringToBuffer('\uD83D\uDC4D', 'utf16le'))).to.deep.equal( + new Uint8Array([0x3d, 0xd8, 0x4d, 0xdc]), + ); +}); + +test(SUITE, '[Node.js] Decodes UTF-16LE bytes back to Japanese text.', () => { + const bytes = new Uint8Array([ + 0x42, 0x30, 0x44, 0x30, 0x46, 0x30, 0x48, 0x30, 0x4a, 0x30, + ]); + expect(bufferToString(bytes.buffer as ArrayBuffer, 'utf16le')).to.equal( + 'あいうえお', + ); +}); + // --- Latin1 / Binary --- test( diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index 04d72ca8..43c09b4c 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -1,5 +1,8 @@ #include "HybridUtils.hpp" +#include +#include +#include #include #include #include @@ -13,6 +16,25 @@ namespace margelo::nitro::crypto { namespace { constexpr char kHexChars[] = "0123456789abcdef"; + constexpr bool kCanDirectCopyUtf16 = std::endian::native == std::endian::little && sizeof(char16_t) == 2; + + // Probe if jsi::String::createFromUtf16() is available + // jsi::String::createFromUtf16(Runtime& runtime, const char16_t* utf16, size_t length) + // and + // jsi::String::createFromUtf16(Runtime& runtime, const std::u16string& utf16) are available in RN v0.79.0 and later: + // https://github.com/facebook/react-native/commit/d9d824055e9f24614abd5657f9fc89a6ab3f2da2 + template + concept HasStringCreateFromUtf16 = requires(facebook::jsi::Runtime& runtime, const char16_t* utf16, size_t length) { + JSIString::createFromUtf16(runtime, utf16, length); + }; + + // Probe if jsi::String::getStringData() is available + // jsi::String::getStringData() is available in RN v0.78.0 and later: + // https://github.com/facebook/react-native/commit/c6f12254d16d87978383c08065a626d437e60450 + template + concept HasStringGetStringData = requires(const JSIString& str, facebook::jsi::Runtime& runtime, void (*cb)(bool, const void*, size_t)) { + str.getStringData(runtime, cb); + }; int hexCharToVal(char c) { if (c >= '0' && c <= '9') @@ -86,6 +108,73 @@ namespace { return result; } + template + JSIString createUtf16LeString(facebook::jsi::Runtime& runtime, const uint8_t* data, size_t len) { + if constexpr (HasStringCreateFromUtf16) { + if constexpr (kCanDirectCopyUtf16) { + // Fast&direct copy path + return JSIString::createFromUtf16(runtime, reinterpret_cast(data), len / 2); + } + // Slow path for unexpected endianness/char16_t size + const size_t codeUnitCount = len / 2; + std::u16string result(codeUnitCount, u'\0'); + if (codeUnitCount == 0) { + return JSIString::createFromUtf16(runtime, result); + } + + for (size_t i = 0; i < codeUnitCount; i++) { + result[i] = static_cast(static_cast(data[i * 2]) | (static_cast(data[i * 2 + 1]) << 8)); + } + return JSIString::createFromUtf16(runtime, result); + } + throw std::runtime_error("Unsupported encoding: utf16le"); + } + + template + std::vector decodeUtf16Le(facebook::jsi::Runtime& runtime, const JSIString& str) { + if constexpr (HasStringGetStringData) { + std::vector result; + // str.utf8() cannot preserve raw UTF-16 code units such as unpaired surrogates. + // Use jsi::String::getStringData() instead. + auto chunkCallback = [&result](bool isAscii, const void* data, size_t num) { + if (num == 0) { + return; + } + + size_t offset = result.size(); + result.resize(offset + (num * 2)); // This fills the buffer with '\0' + + auto* dst = result.data() + offset; + if (isAscii) { + // Widen ASCII characters from char into char16_t + const auto* asciiSrc = reinterpret_cast(data); + for (size_t i = 0; i < num; i++, dst += 2) { + *dst = asciiSrc[i]; + // *(dst + 1) = '\0' is unnecessary because the buffer is zero filled in resize() + } + return; + } + + const auto* utf16Src = reinterpret_cast(data); + if constexpr (kCanDirectCopyUtf16) { + // Fast&direct copy path + std::memcpy(dst, utf16Src, num * 2); + return; + } + // Slow path for unexpected endianness/char16_t size + for (size_t i = 0; i < num; i++) { + const uint16_t codeUnit = static_cast(utf16Src[i]); + dst[i * 2 + 0] = static_cast(codeUnit & 0xFFu); + dst[i * 2 + 1] = static_cast(codeUnit >> 8); + } + }; + + str.getStringData(runtime, chunkCallback); + return result; + } + throw std::runtime_error("Unsupported encoding: utf16le"); + } + std::vector decodeLatin1(const std::string& str) { std::vector result; result.reserve(str.size()); @@ -142,56 +231,107 @@ bool HybridUtils::timingSafeEqual(const std::shared_ptr& a, const s return CRYPTO_memcmp(a->data(), b->data(), aLen) == 0; } -std::string HybridUtils::bufferToString(const std::shared_ptr& buffer, const std::string& encoding) { - const auto* data = reinterpret_cast(buffer->data()); - size_t len = buffer->size(); - - if (encoding == "hex") { - return encodeHex(data, len); +facebook::jsi::Value HybridUtils::bufferToJsiString(facebook::jsi::Runtime& runtime, const facebook::jsi::Value&, + const facebook::jsi::Value* args, size_t argCount) { + // Runtime argument check from react-native-nitro-modules/cpp/core/HybridFunction.hpp + if (argCount != 2) [[unlikely]] { + throw facebook::jsi::JSError(runtime, + "`Utils.bufferToString(...)` expected 2 arguments, but received " + std::to_string(argCount) + "!"); } - if (encoding == "base64") { - return encodeBase64(data, len); - } - if (encoding == "base64url") { - return encodeBase64Url(data, len); - } - if (encoding == "utf8" || encoding == "utf-8") { - return std::string(reinterpret_cast(data), len); - } - if (encoding == "latin1" || encoding == "binary") { - return encodeLatin1(data, len); - } - if (encoding == "ascii") { - std::string result(reinterpret_cast(data), len); - for (auto& c : result) { - c &= 0x7F; + + // Exception wrapper from react-native-nitro-modules/cpp/core/HybridFunction.hpp + try { + // bufferToString(buffer: ArrayBuffer, encoding: string): string; Defined in utils/conversion.ts + auto buffer = JSIConverter>::fromJSI(runtime, args[0]); + std::string encoding = JSIConverter::fromJSI(runtime, args[1]); + + const auto* data = reinterpret_cast(buffer->data()); + size_t len = buffer->size(); + + if (encoding == "hex") { + return facebook::jsi::String::createFromUtf8(runtime, encodeHex(data, len)); } - return result; + if (encoding == "base64") { + return facebook::jsi::String::createFromUtf8(runtime, encodeBase64(data, len)); + } + if (encoding == "base64url") { + return facebook::jsi::String::createFromUtf8(runtime, encodeBase64Url(data, len)); + } + if (encoding == "utf8" || encoding == "utf-8") { + return facebook::jsi::String::createFromUtf8(runtime, data, len); + } + if (encoding == "latin1" || encoding == "binary") { + return facebook::jsi::String::createFromUtf8(runtime, encodeLatin1(data, len)); + } + if (encoding == "ascii") { + std::string result(reinterpret_cast(data), len); + for (auto& c : result) { + c &= 0x7F; + } + return facebook::jsi::String::createFromUtf8(runtime, result); + } + if (encoding == "utf16le") { + return createUtf16LeString(runtime, data, len); + } + throw std::runtime_error("Unsupported encoding: " + encoding); + } catch (const std::exception& exception) { + throw facebook::jsi::JSError(runtime, "Utils.bufferToString(...): " + std::string(exception.what())); + } catch (...) { + throw facebook::jsi::JSError(runtime, + "`Utils.bufferToString(...)` threw an unknown " + TypeInfo::getCurrentExceptionName() + " error."); } - throw std::runtime_error("Unsupported encoding: " + encoding); } -std::shared_ptr HybridUtils::stringToBuffer(const std::string& str, const std::string& encoding) { - if (encoding == "hex") { - auto decoded = decodeHex(str); - return ArrayBuffer::move(std::move(decoded)); - } - if (encoding == "base64" || encoding == "base64url") { - auto decoded = decodeBase64(str); - return ArrayBuffer::move(std::move(decoded)); +facebook::jsi::Value HybridUtils::jsiStringToBuffer(facebook::jsi::Runtime& runtime, const facebook::jsi::Value&, + const facebook::jsi::Value* args, size_t argCount) { + // Runtime argument check from react-native-nitro-modules/cpp/core/HybridFunction.hpp + if (argCount != 2) [[unlikely]] { + throw facebook::jsi::JSError(runtime, + "`Utils.stringToBuffer(...)` expected 2 arguments, but received " + std::to_string(argCount) + "!"); } - if (encoding == "utf8" || encoding == "utf-8") { - return ArrayBuffer::copy(reinterpret_cast(str.data()), str.size()); - } - if (encoding == "latin1" || encoding == "binary") { - auto decoded = decodeLatin1(str); - return ArrayBuffer::move(std::move(decoded)); - } - if (encoding == "ascii") { - auto decoded = decodeLatin1(str); - return ArrayBuffer::move(std::move(decoded)); + + // Exception wrapper from react-native-nitro-modules/cpp/core/HybridFunction.hpp + try { + // stringToBuffer(str: string, encoding: string): ArrayBuffer; Defined in utils/conversion.ts + auto str = args[0].asString(runtime); + std::string encoding = JSIConverter::fromJSI(runtime, args[1]); + + if (encoding == "hex") { + auto decoded = decodeHex(str.utf8(runtime)); + return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); + } + if (encoding == "base64" || encoding == "base64url") { + auto decoded = decodeBase64(str.utf8(runtime)); + return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); + } + if (encoding == "utf8" || encoding == "utf-8") { + auto utf8Str = str.utf8(runtime); + return JSIConverter>::toJSI( + runtime, ArrayBuffer::copy(reinterpret_cast(utf8Str.data()), utf8Str.size())); + } + if (encoding == "latin1" || encoding == "binary" || encoding == "ascii") { + auto decoded = decodeLatin1(str.utf8(runtime)); + return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); + } + if (encoding == "utf16le") { + auto decoded = decodeUtf16Le(runtime, str); + return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); + } + throw std::runtime_error("Unsupported encoding: " + encoding); + } catch (const std::exception& exception) { + throw facebook::jsi::JSError(runtime, "Utils.stringToBuffer(...): " + std::string(exception.what())); + } catch (...) { + throw facebook::jsi::JSError(runtime, + "`Utils.stringToBuffer(...)` threw an unknown " + TypeInfo::getCurrentExceptionName() + " error."); } - throw std::runtime_error("Unsupported encoding: " + encoding); +} + +void HybridUtils::loadHybridMethods() { + HybridUtilsSpec::loadHybridMethods(); + registerHybrids(this, [](Prototype& prototype) { + prototype.registerRawHybridMethod("bufferToString", 2, &HybridUtils::bufferToJsiString); + prototype.registerRawHybridMethod("stringToBuffer", 2, &HybridUtils::jsiStringToBuffer); + }); } } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.hpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.hpp index 7e158f16..4d8bb108 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.hpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.hpp @@ -10,8 +10,15 @@ class HybridUtils : public HybridUtilsSpec { public: bool timingSafeEqual(const std::shared_ptr& a, const std::shared_ptr& b) override; - std::string bufferToString(const std::shared_ptr& buffer, const std::string& encoding) override; - std::shared_ptr stringToBuffer(const std::string& str, const std::string& encoding) override; + + protected: + void loadHybridMethods() override; + + private: + facebook::jsi::Value bufferToJsiString(facebook::jsi::Runtime& runtime, const facebook::jsi::Value& thisArg, + const facebook::jsi::Value* args, size_t argCount); + facebook::jsi::Value jsiStringToBuffer(facebook::jsi::Runtime& runtime, const facebook::jsi::Value& thisArg, + const facebook::jsi::Value* args, size_t argCount); }; } // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridUtilsSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridUtilsSpec.cpp index fb367bf3..a3cae7e4 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridUtilsSpec.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridUtilsSpec.cpp @@ -15,8 +15,6 @@ namespace margelo::nitro::crypto { // load custom methods/properties registerHybrids(this, [](Prototype& prototype) { prototype.registerHybridMethod("timingSafeEqual", &HybridUtilsSpec::timingSafeEqual); - prototype.registerHybridMethod("bufferToString", &HybridUtilsSpec::bufferToString); - prototype.registerHybridMethod("stringToBuffer", &HybridUtilsSpec::stringToBuffer); }); } diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridUtilsSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridUtilsSpec.hpp index ea23ca05..904ae0c0 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridUtilsSpec.hpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridUtilsSpec.hpp @@ -16,7 +16,6 @@ #include -#include namespace margelo::nitro::crypto { @@ -50,8 +49,6 @@ namespace margelo::nitro::crypto { public: // Methods virtual bool timingSafeEqual(const std::shared_ptr& a, const std::shared_ptr& b) = 0; - virtual std::string bufferToString(const std::shared_ptr& buffer, const std::string& encoding) = 0; - virtual std::shared_ptr stringToBuffer(const std::string& str, const std::string& encoding) = 0; protected: // Hybrid Setup diff --git a/packages/react-native-quick-crypto/src/specs/utils.nitro.ts b/packages/react-native-quick-crypto/src/specs/utils.nitro.ts index 25b30795..86a7e1da 100644 --- a/packages/react-native-quick-crypto/src/specs/utils.nitro.ts +++ b/packages/react-native-quick-crypto/src/specs/utils.nitro.ts @@ -2,6 +2,4 @@ import { type HybridObject } from 'react-native-nitro-modules'; export interface Utils extends HybridObject<{ ios: 'c++'; android: 'c++' }> { timingSafeEqual(a: ArrayBuffer, b: ArrayBuffer): boolean; - bufferToString(buffer: ArrayBuffer, encoding: string): string; - stringToBuffer(str: string, encoding: string): ArrayBuffer; } diff --git a/packages/react-native-quick-crypto/src/utils/conversion.ts b/packages/react-native-quick-crypto/src/utils/conversion.ts index e8de35f5..04199f27 100644 --- a/packages/react-native-quick-crypto/src/utils/conversion.ts +++ b/packages/react-native-quick-crypto/src/utils/conversion.ts @@ -3,8 +3,54 @@ import { Buffer as SafeBuffer } from 'safe-buffer'; import { NitroModules } from 'react-native-nitro-modules'; import type { Utils } from '../specs/utils.nitro'; import type { ABV, BinaryLikeNode, BufferLike } from './types'; +import { Platform } from 'react-native'; -const utils = NitroModules.createHybridObject('Utils'); +type UtilsWithStringConverter = Utils & { + bufferToString(buffer: ArrayBuffer, encoding: string): string; + stringToBuffer(str: string, encoding: string): ArrayBuffer; +}; + +const utils = + NitroModules.createHybridObject('Utils'); + +const isHermes = + (global as { HermesInternal?: unknown }).HermesInternal != null; + +// v0.78.0, https://github.com/facebook/react-native/commit/c6f12254d16d87978383c08065a626d437e60450 +// Use jsi::String::getStringData() rather than jsi::String::utf16() +const canGetU16StringFromJsiString = !( + Platform.constants.reactNativeVersion.major == 0 && + Platform.constants.reactNativeVersion.minor < 78 +); + +// v0.79.0, https://github.com/facebook/react-native/commit/d9d824055e9f24614abd5657f9fc89a6ab3f2da2 +const canCreateJsiStringFromUtf16 = !( + Platform.constants.reactNativeVersion.major == 0 && + Platform.constants.reactNativeVersion.minor < 79 +); + +const baseNativeEncodings = [ + 'hex', + 'base64', + 'base64url', + 'utf8', + 'utf-8', + 'latin1', + 'binary', + 'ascii', +]; +const nativeStringToBufferEncodings = new Set(baseNativeEncodings); +const nativeBufferToStringEncodings = new Set(baseNativeEncodings); + +// The fast and lossless paths for utf16le are only available on Hermes +if (isHermes) { + if (canGetU16StringFromJsiString) { + nativeStringToBufferEncodings.add('utf16le'); + } + if (canCreateJsiStringFromUtf16) { + nativeBufferToStringEncodings.add('utf16le'); + } +} /** * Returns the underlying ArrayBuffer of a Buffer / TypedArray view **without @@ -100,7 +146,7 @@ export function binaryLikeToArrayBuffer( ); } - if (nativeEncodings.has(encoding)) { + if (nativeStringToBufferEncodings.has(encoding)) { return utils.stringToBuffer(input, encoding); } const buffer = CraftzdogBuffer.from(input, encoding); @@ -158,19 +204,8 @@ export function binaryLikeToArrayBuffer( ); } -const nativeEncodings = new Set([ - 'hex', - 'base64', - 'base64url', - 'utf8', - 'utf-8', - 'latin1', - 'binary', - 'ascii', -]); - export function ab2str(buf: ArrayBuffer, encoding: string = 'hex'): string { - if (nativeEncodings.has(encoding)) { + if (nativeBufferToStringEncodings.has(encoding)) { return utils.bufferToString(buf, encoding); } return CraftzdogBuffer.from(buf).toString(encoding);