From 6bfa21d3deb854bbe1cb664e7e975d5f7dd3b106 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 19 Apr 2026 16:01:41 +0800 Subject: [PATCH 01/18] Remove duplicate if branch --- .../react-native-quick-crypto/cpp/utils/HybridUtils.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index 04d72ca8..719fdd2a 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -183,11 +183,7 @@ std::shared_ptr HybridUtils::stringToBuffer(const std::string& str, 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") { + if (encoding == "latin1" || encoding == "binary" || encoding == "ascii") { auto decoded = decodeLatin1(str); return ArrayBuffer::move(std::move(decoded)); } From f3635ae6f13fe1416c7720cce74c34d3b17af2af Mon Sep 17 00:00:00 2001 From: wh201906 Date: Fri, 24 Apr 2026 01:48:10 +0800 Subject: [PATCH 02/18] Add bufferToJsiString()/JsiStringToBuffer() to access raw JSI string --- .../cpp/utils/HybridUtils.cpp | 44 ++++++++++++++----- .../cpp/utils/HybridUtils.hpp | 11 ++++- .../generated/shared/c++/HybridUtilsSpec.cpp | 2 - .../generated/shared/c++/HybridUtilsSpec.hpp | 3 -- .../src/specs/utils.nitro.ts | 2 - .../src/utils/conversion.ts | 8 +++- 6 files changed, 48 insertions(+), 22 deletions(-) diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index 719fdd2a..1cf741b9 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -1,5 +1,6 @@ #include "HybridUtils.hpp" +#include #include #include #include @@ -142,52 +143,71 @@ 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) { +facebook::jsi::Value HybridUtils::bufferToJsiString(facebook::jsi::Runtime& runtime, const facebook::jsi::Value&, + const facebook::jsi::Value* args, size_t) { + // 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 encodeHex(data, len); + return facebook::jsi::String::createFromUtf8(runtime, encodeHex(data, len)); } if (encoding == "base64") { - return encodeBase64(data, len); + return facebook::jsi::String::createFromUtf8(runtime, encodeBase64(data, len)); } if (encoding == "base64url") { - return encodeBase64Url(data, len); + return facebook::jsi::String::createFromUtf8(runtime, encodeBase64Url(data, len)); } if (encoding == "utf8" || encoding == "utf-8") { - return std::string(reinterpret_cast(data), len); + return facebook::jsi::String::createFromUtf8(runtime, std::string(reinterpret_cast(data), len)); } if (encoding == "latin1" || encoding == "binary") { - return encodeLatin1(data, len); + 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 result; + return facebook::jsi::String::createFromUtf8(runtime, result); } throw std::runtime_error("Unsupported encoding: " + encoding); } -std::shared_ptr HybridUtils::stringToBuffer(const std::string& str, const std::string& encoding) { +facebook::jsi::Value HybridUtils::JsiStringToBuffer(facebook::jsi::Runtime& runtime, const facebook::jsi::Value&, + const facebook::jsi::Value* args, size_t) { + // stringToBuffer(str: string, encoding: string): ArrayBuffer; Defined in utils/conversion.ts + auto str = JSIConverter::fromJSI(runtime, args[0]); + std::string encoding = JSIConverter::fromJSI(runtime, args[1]); + if (encoding == "hex") { auto decoded = decodeHex(str); - return ArrayBuffer::move(std::move(decoded)); + return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); } if (encoding == "base64" || encoding == "base64url") { auto decoded = decodeBase64(str); - return ArrayBuffer::move(std::move(decoded)); + return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); } if (encoding == "utf8" || encoding == "utf-8") { - return ArrayBuffer::copy(reinterpret_cast(str.data()), str.size()); + return JSIConverter>::toJSI(runtime, + ArrayBuffer::copy(reinterpret_cast(str.data()), str.size())); } if (encoding == "latin1" || encoding == "binary" || encoding == "ascii") { auto decoded = decodeLatin1(str); - return ArrayBuffer::move(std::move(decoded)); + return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); } 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..aaeb50d9 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 4fddea8c..1009b1d9 100644 --- a/packages/react-native-quick-crypto/src/utils/conversion.ts +++ b/packages/react-native-quick-crypto/src/utils/conversion.ts @@ -4,7 +4,13 @@ import { NitroModules } from 'react-native-nitro-modules'; import type { Utils } from '../specs/utils.nitro'; import type { ABV, BinaryLikeNode, BufferLike } from './types'; -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'); /** * Converts supplied argument to an ArrayBuffer. Note this does not copy the From 5a5954099270a526e0be6a148668fe4c8a399b21 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Fri, 24 Apr 2026 01:49:09 +0800 Subject: [PATCH 03/18] Align behaviors with Nitro HybridFunction Add argument count check Wrap exceptions in the same style as Nitro HybridFunction --- .../cpp/utils/HybridUtils.cpp | 124 +++++++++++------- 1 file changed, 76 insertions(+), 48 deletions(-) diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index 1cf741b9..fbb84e26 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -144,62 +144,90 @@ bool HybridUtils::timingSafeEqual(const std::shared_ptr& a, const s } facebook::jsi::Value HybridUtils::bufferToJsiString(facebook::jsi::Runtime& runtime, const facebook::jsi::Value&, - const facebook::jsi::Value* args, size_t) { - // 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 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) + "!"); + } - const auto* data = reinterpret_cast(buffer->data()); - size_t len = buffer->size(); + // 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]); - if (encoding == "hex") { - return facebook::jsi::String::createFromUtf8(runtime, encodeHex(data, len)); - } - 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, std::string(reinterpret_cast(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; + const auto* data = reinterpret_cast(buffer->data()); + size_t len = buffer->size(); + + if (encoding == "hex") { + return facebook::jsi::String::createFromUtf8(runtime, encodeHex(data, len)); + } + 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, std::string(reinterpret_cast(data), len)); } - return facebook::jsi::String::createFromUtf8(runtime, result); + 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); + } + 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); } facebook::jsi::Value HybridUtils::JsiStringToBuffer(facebook::jsi::Runtime& runtime, const facebook::jsi::Value&, - const facebook::jsi::Value* args, size_t) { - // stringToBuffer(str: string, encoding: string): ArrayBuffer; Defined in utils/conversion.ts - auto str = JSIConverter::fromJSI(runtime, args[0]); - std::string encoding = JSIConverter::fromJSI(runtime, args[1]); - - if (encoding == "hex") { - auto decoded = decodeHex(str); - return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); - } - if (encoding == "base64" || encoding == "base64url") { - auto decoded = decodeBase64(str); - return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); - } - if (encoding == "utf8" || encoding == "utf-8") { - return JSIConverter>::toJSI(runtime, - ArrayBuffer::copy(reinterpret_cast(str.data()), str.size())); - } - if (encoding == "latin1" || encoding == "binary" || encoding == "ascii") { - auto decoded = decodeLatin1(str); - return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); + 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) + "!"); + } + + // 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 = JSIConverter::fromJSI(runtime, args[0]); + std::string encoding = JSIConverter::fromJSI(runtime, args[1]); + + if (encoding == "hex") { + auto decoded = decodeHex(str); + return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); + } + if (encoding == "base64" || encoding == "base64url") { + auto decoded = decodeBase64(str); + return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); + } + if (encoding == "utf8" || encoding == "utf-8") { + return JSIConverter>::toJSI(runtime, + ArrayBuffer::copy(reinterpret_cast(str.data()), str.size())); + } + if (encoding == "latin1" || encoding == "binary" || encoding == "ascii") { + auto decoded = decodeLatin1(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() { From 4474e5946eb1f6542082c9061a416a11a289d41e Mon Sep 17 00:00:00 2001 From: wh201906 Date: Fri, 24 Apr 2026 01:58:35 +0800 Subject: [PATCH 04/18] Refactor decodeBase64() --- .../cpp/utils/HybridUtils.cpp | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index fbb84e26..f423616a 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -60,7 +60,30 @@ namespace { return result; } - std::vector decodeBase64(const std::string& b64) { + std::vector decodeBase64(facebook::jsi::Runtime& runtime, const facebook::jsi::String& str) { + std::string b64; + + auto chunkCallback = [&b64](bool isAscii, const void* data, size_t num) { + if (num == 0) { + return; + } + + if (isAscii) { + b64.append(reinterpret_cast(data), num); + return; + } + + size_t offset = b64.size(); + b64.resize(offset + num); + const auto* src = reinterpret_cast(data); + auto* dst = b64.data() + offset; + for (size_t i = 0; i < num; i++) { + dst[i] = static_cast(src[i] & 0xFFu); + } + }; + + str.getStringData(runtime, chunkCallback); + if (b64.empty()) { return {}; } @@ -202,23 +225,24 @@ facebook::jsi::Value HybridUtils::JsiStringToBuffer(facebook::jsi::Runtime& runt // 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 = JSIConverter::fromJSI(runtime, args[0]); + auto str = args[0].asString(runtime); std::string encoding = JSIConverter::fromJSI(runtime, args[1]); if (encoding == "hex") { - auto decoded = decodeHex(str); + auto decoded = decodeHex(str.utf8(runtime)); return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); } if (encoding == "base64" || encoding == "base64url") { - auto decoded = decodeBase64(str); + auto decoded = decodeBase64(runtime, str); 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(str.data()), str.size())); + ArrayBuffer::copy(reinterpret_cast(utf8Str.data()), utf8Str.size())); } if (encoding == "latin1" || encoding == "binary" || encoding == "ascii") { - auto decoded = decodeLatin1(str); + auto decoded = decodeLatin1(str.utf8(runtime)); return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); } throw std::runtime_error("Unsupported encoding: " + encoding); From 2ea4efef7235b31b3dd13d1fce33aa8dd262764c Mon Sep 17 00:00:00 2001 From: wh201906 Date: Fri, 24 Apr 2026 02:01:52 +0800 Subject: [PATCH 05/18] Add decodeUtf16Le() --- .../cpp/utils/HybridUtils.cpp | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index f423616a..f26eebc4 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -1,6 +1,8 @@ #include "HybridUtils.hpp" #include +#include +#include #include #include #include @@ -110,6 +112,42 @@ namespace { return result; } + std::vector decodeUtf16Le(facebook::jsi::Runtime& runtime, const facebook::jsi::String& str) { + std::vector result; + + 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)); + + auto* dst = result.data() + offset; + if (isAscii) { + const auto* asciiSrc = reinterpret_cast(data); + for (size_t i = 0; i < num; i++, dst += 2) { + *dst = asciiSrc[i]; + } + return; + } + + const auto* utf16Src = reinterpret_cast(data); + if constexpr (std::endian::native == std::endian::little && sizeof(char16_t) == 2) { + std::memcpy(dst, utf16Src, num * 2); + return; + } + 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; + } + std::vector decodeLatin1(const std::string& str) { std::vector result; result.reserve(str.size()); @@ -245,6 +283,10 @@ facebook::jsi::Value HybridUtils::JsiStringToBuffer(facebook::jsi::Runtime& runt 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())); From 923f3bd6d9aabceb023d1671f899afabfaeeb810 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sat, 25 Apr 2026 14:42:25 +0800 Subject: [PATCH 06/18] Add fast encoding path for RN versions with createFromUtf16() And use createFromUtf8() override with less overhead --- .../cpp/utils/HybridUtils.cpp | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index f26eebc4..40705063 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -8,6 +8,18 @@ #include #include +#if __has_include() +#include +#if (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 79) +#define RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 0 +#else // (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 79) +#define RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 1 +#endif // (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 79) +#else // __has_include() +#pragma message("QuickCrypto: was not found, native bufferToString('utf16le') is disabled.") +#define RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 0 +#endif // __has_include() + #include "QuickCryptoUtils.hpp" #include "simdutf.h" @@ -112,6 +124,20 @@ namespace { return result; } + std::u16string encodeUtf16(const uint8_t* data, size_t len) { + // For !(std::endian::native == std::endian::little && sizeof(char16_t) == 2) + const size_t codeUnitCount = len / 2; + std::u16string result(codeUnitCount, u'\0'); + if (codeUnitCount == 0) { + return 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 result; + } + std::vector decodeUtf16Le(facebook::jsi::Runtime& runtime, const facebook::jsi::String& str) { std::vector result; @@ -231,7 +257,7 @@ facebook::jsi::Value HybridUtils::bufferToJsiString(facebook::jsi::Runtime& runt return facebook::jsi::String::createFromUtf8(runtime, encodeBase64Url(data, len)); } if (encoding == "utf8" || encoding == "utf-8") { - return facebook::jsi::String::createFromUtf8(runtime, std::string(reinterpret_cast(data), len)); + return facebook::jsi::String::createFromUtf8(runtime, data, len); } if (encoding == "latin1" || encoding == "binary") { return facebook::jsi::String::createFromUtf8(runtime, encodeLatin1(data, len)); @@ -243,6 +269,16 @@ facebook::jsi::Value HybridUtils::bufferToJsiString(facebook::jsi::Runtime& runt } return facebook::jsi::String::createFromUtf8(runtime, result); } +#if RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 + // facebook::jsi::String::createFromUtf16() is available in React Native v0.79.0 and later + if (encoding == "utf16le") { + if constexpr (std::endian::native == std::endian::little && sizeof(char16_t) == 2) { + return facebook::jsi::String::createFromUtf16(runtime, reinterpret_cast(data), len / 2); + } + auto encoded = encodeUtf16(data, len); + return facebook::jsi::String::createFromUtf16(runtime, encoded); + } +#endif throw std::runtime_error("Unsupported encoding: " + encoding); } catch (const std::exception& exception) { throw facebook::jsi::JSError(runtime, "Utils.bufferToString(...): " + std::string(exception.what())); @@ -276,8 +312,8 @@ facebook::jsi::Value HybridUtils::JsiStringToBuffer(facebook::jsi::Runtime& runt } if (encoding == "utf8" || encoding == "utf-8") { auto utf8Str = str.utf8(runtime); - return JSIConverter>::toJSI(runtime, - ArrayBuffer::copy(reinterpret_cast(utf8Str.data()), utf8Str.size())); + 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)); From db8b382a4555637a57c9aa07d25ac797cfb164a0 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sat, 25 Apr 2026 14:44:30 +0800 Subject: [PATCH 07/18] Route native string conversion encodings by runtime and RN version --- .../src/utils/conversion.ts | 60 +++++++++++++++---- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/packages/react-native-quick-crypto/src/utils/conversion.ts b/packages/react-native-quick-crypto/src/utils/conversion.ts index 1009b1d9..28f09d37 100644 --- a/packages/react-native-quick-crypto/src/utils/conversion.ts +++ b/packages/react-native-quick-crypto/src/utils/conversion.ts @@ -3,6 +3,7 @@ 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'; type UtilsWithStringConverter = Utils & { bufferToString(buffer: ArrayBuffer, encoding: string): string; @@ -12,6 +13,50 @@ type UtilsWithStringConverter = Utils & { const utils = NitroModules.createHybridObject('Utils'); +const isHermes = + (global as { HermesInternal?: unknown }).HermesInternal != null; + +// v0.77.0, https://github.com/facebook/react-native/commit/6ab7b70241fe65e4138dffc4590ed60d46454e5d +const canGetU16StringFromJsiString = !( + Platform.constants.reactNativeVersion.major == 0 && + Platform.constants.reactNativeVersion.minor < 77 +); + +// 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']; +const textNativeEncodings = ['utf8', 'utf-8', 'latin1', 'binary', 'ascii']; + +// Only enable native string -> buffer conversions on Hermes +// On non-Hermes runtimes, extracting string data can go through runtime-specific fallback paths which handle invalid UTF-16 strings differently. +// For example, JSStringGetUTF8CString() of JSC truncates JS strings when meets unpaired surrogates. +// +// utf16le is only valid when fast jsi::String.utf16() is available. +const nativeStringToBufferEncodings = new Set( + isHermes + ? [ + ...baseNativeEncodings, + ...textNativeEncodings, + ...(canGetU16StringFromJsiString ? ['utf16le'] : []), + ] + : [], +); + +// JSStringCreateWithUTF8CString() of JSC only accepts null-terminated UTF-8 strings without length argument +// baseNativeEncodings doesn't produce outputs with embedded '\0' so they are always enabled. +// For textNativeEncodings, the behavior of native buffer -> string conversions can be inconsistent across Hermes/JSC/V8, so they are disabled for non-Hermes runtimes. +// +// utf16le is only valid when fast jsi::String::createFromUtf16() is available. +const nativeBufferToStringEncodings = new Set([ + ...baseNativeEncodings, + ...(isHermes ? textNativeEncodings : []), + ...(isHermes && canCreateJsiStringFromUtf16 ? ['utf16le'] : []), +]); + /** * Converts supplied argument to an ArrayBuffer. Note this does not copy the * data so it is faster than toArrayBuffer. Not copying is important for @@ -103,7 +148,7 @@ export function binaryLikeToArrayBuffer( ); } - if (nativeEncodings.has(encoding)) { + if (nativeStringToBufferEncodings.has(encoding)) { return utils.stringToBuffer(input, encoding); } const buffer = CraftzdogBuffer.from(input, encoding); @@ -161,19 +206,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); From b8a69032b8110142a156b005a4249164556ee24a Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sat, 25 Apr 2026 16:37:29 +0800 Subject: [PATCH 08/18] Fix RN version constraints --- .../cpp/utils/HybridUtils.cpp | 35 +++++++++++++++++-- .../src/utils/conversion.ts | 9 ++--- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index 40705063..6e409882 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -10,14 +10,31 @@ #if __has_include() #include + +// jsi::String::createFromUtf16() is available in v0.79.0: +// https://github.com/facebook/react-native/commit/d9d824055e9f24614abd5657f9fc89a6ab3f2da2 #if (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 79) +#pragma message("QuickCrypto: Native bufferToString('utf16le') is disabled.") #define RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 0 #else // (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 79) +#pragma message("QuickCrypto: Native bufferToString('utf16le') is enabled.") #define RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 1 #endif // (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 79) -#else // __has_include() -#pragma message("QuickCrypto: was not found, native bufferToString('utf16le') is disabled.") + +// jsi::String::getStringData() is available in v0.78.0: +// https://github.com/facebook/react-native/commit/c6f12254d16d87978383c08065a626d437e60450 +#if (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 78) +#pragma message("QuickCrypto: Native getStringData() fast path is disabled.") +#define RNQC_NATIVE_GET_STRING_DATA 0 +#else // (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 78) +#pragma message("QuickCrypto: Native getStringData() fast path is enabled.") +#define RNQC_NATIVE_GET_STRING_DATA 1 +#endif // (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 78) + +#else // __has_include() +#pragma message("QuickCrypto: was not found, The faster native path and native utf16 support are disabled.") #define RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 0 +#define RNQC_NATIVE_GET_STRING_DATA 0 #endif // __has_include() #include "QuickCryptoUtils.hpp" @@ -75,6 +92,8 @@ namespace { } std::vector decodeBase64(facebook::jsi::Runtime& runtime, const facebook::jsi::String& str) { + +#if RNQC_NATIVE_GET_STRING_DATA std::string b64; auto chunkCallback = [&b64](bool isAscii, const void* data, size_t num) { @@ -97,6 +116,9 @@ namespace { }; str.getStringData(runtime, chunkCallback); +#else // RNQC_NATIVE_GET_STRING_DATA + std::string b64 = str.utf8(runtime); +#endif if (b64.empty()) { return {}; @@ -124,6 +146,7 @@ namespace { return result; } +#if RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 std::u16string encodeUtf16(const uint8_t* data, size_t len) { // For !(std::endian::native == std::endian::little && sizeof(char16_t) == 2) const size_t codeUnitCount = len / 2; @@ -137,7 +160,10 @@ namespace { } return result; } +#endif // RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 +#if RNQC_NATIVE_GET_STRING_DATA + // decodeUtf16Le() is not available for jsi::String::utf8() std::vector decodeUtf16Le(facebook::jsi::Runtime& runtime, const facebook::jsi::String& str) { std::vector result; @@ -173,6 +199,7 @@ namespace { str.getStringData(runtime, chunkCallback); return result; } +#endif // RNQC_NATIVE_GET_STRING_DATA std::vector decodeLatin1(const std::string& str) { std::vector result; @@ -278,7 +305,7 @@ facebook::jsi::Value HybridUtils::bufferToJsiString(facebook::jsi::Runtime& runt auto encoded = encodeUtf16(data, len); return facebook::jsi::String::createFromUtf16(runtime, encoded); } -#endif +#endif // RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 throw std::runtime_error("Unsupported encoding: " + encoding); } catch (const std::exception& exception) { throw facebook::jsi::JSError(runtime, "Utils.bufferToString(...): " + std::string(exception.what())); @@ -319,10 +346,12 @@ facebook::jsi::Value HybridUtils::JsiStringToBuffer(facebook::jsi::Runtime& runt auto decoded = decodeLatin1(str.utf8(runtime)); return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); } +#if RNQC_NATIVE_GET_STRING_DATA if (encoding == "utf16le") { auto decoded = decodeUtf16Le(runtime, str); return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); } +#endif // RNQC_NATIVE_GET_STRING_DATA throw std::runtime_error("Unsupported encoding: " + encoding); } catch (const std::exception& exception) { throw facebook::jsi::JSError(runtime, "Utils.stringToBuffer(...): " + std::string(exception.what())); diff --git a/packages/react-native-quick-crypto/src/utils/conversion.ts b/packages/react-native-quick-crypto/src/utils/conversion.ts index 28f09d37..1b4007a0 100644 --- a/packages/react-native-quick-crypto/src/utils/conversion.ts +++ b/packages/react-native-quick-crypto/src/utils/conversion.ts @@ -16,10 +16,11 @@ const utils = const isHermes = (global as { HermesInternal?: unknown }).HermesInternal != null; -// v0.77.0, https://github.com/facebook/react-native/commit/6ab7b70241fe65e4138dffc4590ed60d46454e5d +// 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 < 77 + Platform.constants.reactNativeVersion.minor < 78 ); // v0.79.0, https://github.com/facebook/react-native/commit/d9d824055e9f24614abd5657f9fc89a6ab3f2da2 @@ -34,8 +35,6 @@ const textNativeEncodings = ['utf8', 'utf-8', 'latin1', 'binary', 'ascii']; // Only enable native string -> buffer conversions on Hermes // On non-Hermes runtimes, extracting string data can go through runtime-specific fallback paths which handle invalid UTF-16 strings differently. // For example, JSStringGetUTF8CString() of JSC truncates JS strings when meets unpaired surrogates. -// -// utf16le is only valid when fast jsi::String.utf16() is available. const nativeStringToBufferEncodings = new Set( isHermes ? [ @@ -49,8 +48,6 @@ const nativeStringToBufferEncodings = new Set( // JSStringCreateWithUTF8CString() of JSC only accepts null-terminated UTF-8 strings without length argument // baseNativeEncodings doesn't produce outputs with embedded '\0' so they are always enabled. // For textNativeEncodings, the behavior of native buffer -> string conversions can be inconsistent across Hermes/JSC/V8, so they are disabled for non-Hermes runtimes. -// -// utf16le is only valid when fast jsi::String::createFromUtf16() is available. const nativeBufferToStringEncodings = new Set([ ...baseNativeEncodings, ...(isHermes ? textNativeEncodings : []), From 01a50f452142b4a0c86dda69844c87be4d78d3f2 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 26 Apr 2026 22:43:07 +0800 Subject: [PATCH 09/18] Add all test cases --- example/src/benchmarks/encoding/encoding.ts | 533 +++++++++++++++++++- 1 file changed, 515 insertions(+), 18 deletions(-) diff --git a/example/src/benchmarks/encoding/encoding.ts b/example/src/benchmarks/encoding/encoding.ts index be7364af..fa37699e 100644 --- a/example/src/benchmarks/encoding/encoding.ts +++ b/example/src/benchmarks/encoding/encoding.ts @@ -22,28 +22,45 @@ 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 hex_1MB = bufferToString(ab1MB, 'hex'); +const hex_32B = bufferToString(ab32B, 'hex'); const base64_1MB = bufferToString(ab1MB, 'base64'); -const hex32B = bufferToString(ab32B, 'hex'); const base64_32B = bufferToString(ab32B, 'base64'); +const latin1_1MB_ascii = bufferToString(ab1MB_ascii, 'latin1'); +const latin1_32B_ascii = bufferToString(ab32B_ascii, 'latin1'); +const latin1_1MB_non_ascii = bufferToString(ab1MB, 'latin1'); +const latin1_32B_non_ascii = bufferToString(ab32B, 'latin1'); +const utf16le_1MB_ascii = bufferToString(ab1MB_ascii, 'utf16le'); +const utf16le_32B_ascii = bufferToString(ab32B_ascii, 'utf16le'); +const utf16le_1MB_non_ascii = bufferToString(ab1MB, 'utf16le'); +const utf16le_32B_non_ascii = bufferToString(ab32B, 'utf16le'); +const utf8_1MB_ascii = bufferToString(ab1MB_ascii, 'utf8'); +const utf8_32B_ascii = bufferToString(ab32B_ascii, 'utf8'); +const utf8_1MB_non_ascii = bufferToString(ab1MB, 'utf8'); +const utf8_32B_non_ascii = bufferToString(ab32B, 'utf8'); // --- Encode benchmarks (ArrayBuffer → string) --- @@ -123,6 +140,234 @@ const encode_base64_1mb: BenchFn = () => { return bench; }; +const encode_latin1_32b: BenchFn = () => { + const bench = new Bench({ + name: 'latin1 encode 32B', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + bufferToString(ab32B, 'latin1'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab32B, 'latin1'); + }); + + return bench; +}; + +const encode_latin1_1mb: BenchFn = () => { + const bench = new Bench({ + name: 'latin1 encode 1MB', + iterations: 10, + warmupIterations: 2, + time: 0, + }); + + bench + .add('rnqc', () => { + bufferToString(ab1MB, 'latin1'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab1MB, 'latin1'); + }); + + return bench; +}; + +const encode_latin1_32b_ascii: BenchFn = () => { + const bench = new Bench({ + name: 'latin1 encode 32B (ASCII only)', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + bufferToString(ab32B_ascii, 'latin1'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab32B_ascii, 'latin1'); + }); + + return bench; +}; + +const encode_latin1_1mb_ascii: BenchFn = () => { + const bench = new Bench({ + name: 'latin1 encode 1MB (ASCII only)', + iterations: 10, + warmupIterations: 2, + time: 0, + }); + + bench + .add('rnqc', () => { + bufferToString(ab1MB_ascii, 'latin1'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab1MB_ascii, 'latin1'); + }); + + return bench; +}; + +const encode_utf8_32b: BenchFn = () => { + const bench = new Bench({ + name: 'utf8 encode 32B', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + bufferToString(ab32B, 'utf8'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab32B, 'utf8'); + }); + + return bench; +}; + +const encode_utf8_1mb: BenchFn = () => { + const bench = new Bench({ + name: 'utf8 encode 1MB', + iterations: 10, + warmupIterations: 2, + time: 0, + }); + + bench + .add('rnqc', () => { + bufferToString(ab1MB, 'utf8'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab1MB, 'utf8'); + }); + + return bench; +}; + +const encode_utf8_32b_ascii: BenchFn = () => { + const bench = new Bench({ + name: 'utf8 encode 32B (ASCII only)', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + bufferToString(ab32B_ascii, 'utf8'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab32B_ascii, 'utf8'); + }); + + return bench; +}; + +const encode_utf8_1mb_ascii: BenchFn = () => { + const bench = new Bench({ + name: 'utf8 encode 1MB (ASCII only)', + iterations: 10, + warmupIterations: 2, + time: 0, + }); + + bench + .add('rnqc', () => { + bufferToString(ab1MB_ascii, 'utf8'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab1MB_ascii, 'utf8'); + }); + + return bench; +}; + +const encode_utf16le_32b: BenchFn = () => { + const bench = new Bench({ + name: 'utf16le encode 32B', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + bufferToString(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', () => { + bufferToString(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', () => { + bufferToString(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', () => { + bufferToString(ab1MB_ascii, 'utf16le'); + }) + .add('Buffer polyfill', () => { + ab2str_old(ab1MB_ascii, 'utf16le'); + }); + + return bench; +}; + // --- Decode benchmarks (string → ArrayBuffer) --- const decode_hex_32b: BenchFn = () => { @@ -135,10 +380,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 +399,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 +446,265 @@ const decode_base64_1mb: BenchFn = () => { return bench; }; +const decode_latin1_32b: BenchFn = () => { + const bench = new Bench({ + name: 'latin1 decode 32B', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + stringToBuffer(latin1_32B_non_ascii, 'latin1'); + }) + .add('Buffer polyfill', () => { + stringToBuffer_old(latin1_32B_non_ascii, 'latin1'); + }); + + return bench; +}; + +const decode_latin1_1mb: BenchFn = () => { + const bench = new Bench({ + name: 'latin1 decode 1MB', + iterations: 10, + warmupIterations: 2, + time: 0, + }); + + bench + .add('rnqc', () => { + stringToBuffer(latin1_1MB_non_ascii, 'latin1'); + }) + .add('Buffer polyfill', () => { + stringToBuffer_old(latin1_1MB_non_ascii, 'latin1'); + }); + + return bench; +}; + +const decode_latin1_32b_ascii: BenchFn = () => { + const bench = new Bench({ + name: 'latin1 decode 32B (ASCII only)', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + stringToBuffer(latin1_32B_ascii, 'latin1'); + }) + .add('Buffer polyfill', () => { + stringToBuffer_old(latin1_32B_ascii, 'latin1'); + }); + + return bench; +}; + +const decode_latin1_1mb_ascii: BenchFn = () => { + const bench = new Bench({ + name: 'latin1 decode 1MB (ASCII only)', + iterations: 10, + warmupIterations: 2, + time: 0, + }); + + bench + .add('rnqc', () => { + stringToBuffer(latin1_1MB_ascii, 'latin1'); + }) + .add('Buffer polyfill', () => { + stringToBuffer_old(latin1_1MB_ascii, 'latin1'); + }); + + return bench; +}; + +const decode_utf8_32b: BenchFn = () => { + const bench = new Bench({ + name: 'utf8 decode 32B', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + stringToBuffer(utf8_32B_non_ascii, 'utf8'); + }) + .add('Buffer polyfill', () => { + stringToBuffer_old(utf8_32B_non_ascii, 'utf8'); + }); + + return bench; +}; + +const decode_utf8_1mb: BenchFn = () => { + const bench = new Bench({ + name: 'utf8 decode 1MB', + iterations: 10, + warmupIterations: 2, + time: 0, + }); + + bench + .add('rnqc', () => { + stringToBuffer(utf8_1MB_non_ascii, 'utf8'); + }) + .add('Buffer polyfill', () => { + stringToBuffer_old(utf8_1MB_non_ascii, 'utf8'); + }); + + return bench; +}; + +const decode_utf8_32b_ascii: BenchFn = () => { + const bench = new Bench({ + name: 'utf8 decode 32B (ASCII only)', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + stringToBuffer(utf8_32B_ascii, 'utf8'); + }) + .add('Buffer polyfill', () => { + stringToBuffer_old(utf8_32B_ascii, 'utf8'); + }); + + return bench; +}; + +const decode_utf8_1mb_ascii: BenchFn = () => { + const bench = new Bench({ + name: 'utf8 decode 1MB (ASCII only)', + iterations: 10, + warmupIterations: 2, + time: 0, + }); + + bench + .add('rnqc', () => { + stringToBuffer(utf8_1MB_ascii, 'utf8'); + }) + .add('Buffer polyfill', () => { + stringToBuffer_old(utf8_1MB_ascii, 'utf8'); + }); + + return bench; +}; + +const decode_utf16le_32b: BenchFn = () => { + const bench = new Bench({ + name: 'utf16le decode 32B', + iterations: 100, + warmupIterations: 10, + time: 0, + }); + + bench + .add('rnqc', () => { + stringToBuffer(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', () => { + stringToBuffer(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', () => { + stringToBuffer(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', () => { + stringToBuffer(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_latin1_32b, + encode_latin1_1mb, + encode_latin1_32b_ascii, + encode_latin1_1mb_ascii, + encode_utf8_32b, + encode_utf8_1mb, + encode_utf8_32b_ascii, + encode_utf8_1mb_ascii, + 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_latin1_32b, + decode_latin1_1mb, + decode_latin1_32b_ascii, + decode_latin1_1mb_ascii, + decode_utf8_32b, + decode_utf8_1mb, + decode_utf8_32b_ascii, + decode_utf8_1mb_ascii, + decode_utf16le_32b, + decode_utf16le_1mb, + decode_utf16le_32b_ascii, + decode_utf16le_1mb_ascii, ]; From 8118545b28817cbb3fbd10becdc26b4d2068dfc5 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 26 Apr 2026 22:45:28 +0800 Subject: [PATCH 10/18] Revert "Refactor decodeBase64()" No significant performance improvements for normal cases --- .../cpp/utils/HybridUtils.cpp | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index 6e409882..ecfe32b9 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -91,35 +91,7 @@ namespace { return result; } - std::vector decodeBase64(facebook::jsi::Runtime& runtime, const facebook::jsi::String& str) { - -#if RNQC_NATIVE_GET_STRING_DATA - std::string b64; - - auto chunkCallback = [&b64](bool isAscii, const void* data, size_t num) { - if (num == 0) { - return; - } - - if (isAscii) { - b64.append(reinterpret_cast(data), num); - return; - } - - size_t offset = b64.size(); - b64.resize(offset + num); - const auto* src = reinterpret_cast(data); - auto* dst = b64.data() + offset; - for (size_t i = 0; i < num; i++) { - dst[i] = static_cast(src[i] & 0xFFu); - } - }; - - str.getStringData(runtime, chunkCallback); -#else // RNQC_NATIVE_GET_STRING_DATA - std::string b64 = str.utf8(runtime); -#endif - + std::vector decodeBase64(const std::string& b64) { if (b64.empty()) { return {}; } @@ -334,7 +306,7 @@ facebook::jsi::Value HybridUtils::JsiStringToBuffer(facebook::jsi::Runtime& runt return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); } if (encoding == "base64" || encoding == "base64url") { - auto decoded = decodeBase64(runtime, str); + auto decoded = decodeBase64(str.utf8(runtime)); return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); } if (encoding == "utf8" || encoding == "utf-8") { From 6080d2306405399e815784305de336c4ef128bc2 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 26 Apr 2026 23:26:06 +0800 Subject: [PATCH 11/18] Reduce the scope of this PR --- example/src/benchmarks/encoding/encoding.ts | 328 ------------------ .../src/utils/conversion.ts | 46 ++- 2 files changed, 22 insertions(+), 352 deletions(-) diff --git a/example/src/benchmarks/encoding/encoding.ts b/example/src/benchmarks/encoding/encoding.ts index fa37699e..0b60047c 100644 --- a/example/src/benchmarks/encoding/encoding.ts +++ b/example/src/benchmarks/encoding/encoding.ts @@ -49,18 +49,10 @@ const hex_1MB = bufferToString(ab1MB, 'hex'); const hex_32B = bufferToString(ab32B, 'hex'); const base64_1MB = bufferToString(ab1MB, 'base64'); const base64_32B = bufferToString(ab32B, 'base64'); -const latin1_1MB_ascii = bufferToString(ab1MB_ascii, 'latin1'); -const latin1_32B_ascii = bufferToString(ab32B_ascii, 'latin1'); -const latin1_1MB_non_ascii = bufferToString(ab1MB, 'latin1'); -const latin1_32B_non_ascii = bufferToString(ab32B, 'latin1'); const utf16le_1MB_ascii = bufferToString(ab1MB_ascii, 'utf16le'); const utf16le_32B_ascii = bufferToString(ab32B_ascii, 'utf16le'); const utf16le_1MB_non_ascii = bufferToString(ab1MB, 'utf16le'); const utf16le_32B_non_ascii = bufferToString(ab32B, 'utf16le'); -const utf8_1MB_ascii = bufferToString(ab1MB_ascii, 'utf8'); -const utf8_32B_ascii = bufferToString(ab32B_ascii, 'utf8'); -const utf8_1MB_non_ascii = bufferToString(ab1MB, 'utf8'); -const utf8_32B_non_ascii = bufferToString(ab32B, 'utf8'); // --- Encode benchmarks (ArrayBuffer → string) --- @@ -140,158 +132,6 @@ const encode_base64_1mb: BenchFn = () => { return bench; }; -const encode_latin1_32b: BenchFn = () => { - const bench = new Bench({ - name: 'latin1 encode 32B', - iterations: 100, - warmupIterations: 10, - time: 0, - }); - - bench - .add('rnqc', () => { - bufferToString(ab32B, 'latin1'); - }) - .add('Buffer polyfill', () => { - ab2str_old(ab32B, 'latin1'); - }); - - return bench; -}; - -const encode_latin1_1mb: BenchFn = () => { - const bench = new Bench({ - name: 'latin1 encode 1MB', - iterations: 10, - warmupIterations: 2, - time: 0, - }); - - bench - .add('rnqc', () => { - bufferToString(ab1MB, 'latin1'); - }) - .add('Buffer polyfill', () => { - ab2str_old(ab1MB, 'latin1'); - }); - - return bench; -}; - -const encode_latin1_32b_ascii: BenchFn = () => { - const bench = new Bench({ - name: 'latin1 encode 32B (ASCII only)', - iterations: 100, - warmupIterations: 10, - time: 0, - }); - - bench - .add('rnqc', () => { - bufferToString(ab32B_ascii, 'latin1'); - }) - .add('Buffer polyfill', () => { - ab2str_old(ab32B_ascii, 'latin1'); - }); - - return bench; -}; - -const encode_latin1_1mb_ascii: BenchFn = () => { - const bench = new Bench({ - name: 'latin1 encode 1MB (ASCII only)', - iterations: 10, - warmupIterations: 2, - time: 0, - }); - - bench - .add('rnqc', () => { - bufferToString(ab1MB_ascii, 'latin1'); - }) - .add('Buffer polyfill', () => { - ab2str_old(ab1MB_ascii, 'latin1'); - }); - - return bench; -}; - -const encode_utf8_32b: BenchFn = () => { - const bench = new Bench({ - name: 'utf8 encode 32B', - iterations: 100, - warmupIterations: 10, - time: 0, - }); - - bench - .add('rnqc', () => { - bufferToString(ab32B, 'utf8'); - }) - .add('Buffer polyfill', () => { - ab2str_old(ab32B, 'utf8'); - }); - - return bench; -}; - -const encode_utf8_1mb: BenchFn = () => { - const bench = new Bench({ - name: 'utf8 encode 1MB', - iterations: 10, - warmupIterations: 2, - time: 0, - }); - - bench - .add('rnqc', () => { - bufferToString(ab1MB, 'utf8'); - }) - .add('Buffer polyfill', () => { - ab2str_old(ab1MB, 'utf8'); - }); - - return bench; -}; - -const encode_utf8_32b_ascii: BenchFn = () => { - const bench = new Bench({ - name: 'utf8 encode 32B (ASCII only)', - iterations: 100, - warmupIterations: 10, - time: 0, - }); - - bench - .add('rnqc', () => { - bufferToString(ab32B_ascii, 'utf8'); - }) - .add('Buffer polyfill', () => { - ab2str_old(ab32B_ascii, 'utf8'); - }); - - return bench; -}; - -const encode_utf8_1mb_ascii: BenchFn = () => { - const bench = new Bench({ - name: 'utf8 encode 1MB (ASCII only)', - iterations: 10, - warmupIterations: 2, - time: 0, - }); - - bench - .add('rnqc', () => { - bufferToString(ab1MB_ascii, 'utf8'); - }) - .add('Buffer polyfill', () => { - ab2str_old(ab1MB_ascii, 'utf8'); - }); - - return bench; -}; - const encode_utf16le_32b: BenchFn = () => { const bench = new Bench({ name: 'utf16le encode 32B', @@ -446,158 +286,6 @@ const decode_base64_1mb: BenchFn = () => { return bench; }; -const decode_latin1_32b: BenchFn = () => { - const bench = new Bench({ - name: 'latin1 decode 32B', - iterations: 100, - warmupIterations: 10, - time: 0, - }); - - bench - .add('rnqc', () => { - stringToBuffer(latin1_32B_non_ascii, 'latin1'); - }) - .add('Buffer polyfill', () => { - stringToBuffer_old(latin1_32B_non_ascii, 'latin1'); - }); - - return bench; -}; - -const decode_latin1_1mb: BenchFn = () => { - const bench = new Bench({ - name: 'latin1 decode 1MB', - iterations: 10, - warmupIterations: 2, - time: 0, - }); - - bench - .add('rnqc', () => { - stringToBuffer(latin1_1MB_non_ascii, 'latin1'); - }) - .add('Buffer polyfill', () => { - stringToBuffer_old(latin1_1MB_non_ascii, 'latin1'); - }); - - return bench; -}; - -const decode_latin1_32b_ascii: BenchFn = () => { - const bench = new Bench({ - name: 'latin1 decode 32B (ASCII only)', - iterations: 100, - warmupIterations: 10, - time: 0, - }); - - bench - .add('rnqc', () => { - stringToBuffer(latin1_32B_ascii, 'latin1'); - }) - .add('Buffer polyfill', () => { - stringToBuffer_old(latin1_32B_ascii, 'latin1'); - }); - - return bench; -}; - -const decode_latin1_1mb_ascii: BenchFn = () => { - const bench = new Bench({ - name: 'latin1 decode 1MB (ASCII only)', - iterations: 10, - warmupIterations: 2, - time: 0, - }); - - bench - .add('rnqc', () => { - stringToBuffer(latin1_1MB_ascii, 'latin1'); - }) - .add('Buffer polyfill', () => { - stringToBuffer_old(latin1_1MB_ascii, 'latin1'); - }); - - return bench; -}; - -const decode_utf8_32b: BenchFn = () => { - const bench = new Bench({ - name: 'utf8 decode 32B', - iterations: 100, - warmupIterations: 10, - time: 0, - }); - - bench - .add('rnqc', () => { - stringToBuffer(utf8_32B_non_ascii, 'utf8'); - }) - .add('Buffer polyfill', () => { - stringToBuffer_old(utf8_32B_non_ascii, 'utf8'); - }); - - return bench; -}; - -const decode_utf8_1mb: BenchFn = () => { - const bench = new Bench({ - name: 'utf8 decode 1MB', - iterations: 10, - warmupIterations: 2, - time: 0, - }); - - bench - .add('rnqc', () => { - stringToBuffer(utf8_1MB_non_ascii, 'utf8'); - }) - .add('Buffer polyfill', () => { - stringToBuffer_old(utf8_1MB_non_ascii, 'utf8'); - }); - - return bench; -}; - -const decode_utf8_32b_ascii: BenchFn = () => { - const bench = new Bench({ - name: 'utf8 decode 32B (ASCII only)', - iterations: 100, - warmupIterations: 10, - time: 0, - }); - - bench - .add('rnqc', () => { - stringToBuffer(utf8_32B_ascii, 'utf8'); - }) - .add('Buffer polyfill', () => { - stringToBuffer_old(utf8_32B_ascii, 'utf8'); - }); - - return bench; -}; - -const decode_utf8_1mb_ascii: BenchFn = () => { - const bench = new Bench({ - name: 'utf8 decode 1MB (ASCII only)', - iterations: 10, - warmupIterations: 2, - time: 0, - }); - - bench - .add('rnqc', () => { - stringToBuffer(utf8_1MB_ascii, 'utf8'); - }) - .add('Buffer polyfill', () => { - stringToBuffer_old(utf8_1MB_ascii, 'utf8'); - }); - - return bench; -}; - const decode_utf16le_32b: BenchFn = () => { const bench = new Bench({ name: 'utf16le decode 32B', @@ -679,14 +367,6 @@ export default [ encode_hex_1mb, encode_base64_32b, encode_base64_1mb, - encode_latin1_32b, - encode_latin1_1mb, - encode_latin1_32b_ascii, - encode_latin1_1mb_ascii, - encode_utf8_32b, - encode_utf8_1mb, - encode_utf8_32b_ascii, - encode_utf8_1mb_ascii, encode_utf16le_32b, encode_utf16le_1mb, encode_utf16le_32b_ascii, @@ -695,14 +375,6 @@ export default [ decode_hex_1mb, decode_base64_32b, decode_base64_1mb, - decode_latin1_32b, - decode_latin1_1mb, - decode_latin1_32b_ascii, - decode_latin1_1mb_ascii, - decode_utf8_32b, - decode_utf8_1mb, - decode_utf8_32b_ascii, - decode_utf8_1mb_ascii, decode_utf16le_32b, decode_utf16le_1mb, decode_utf16le_32b_ascii, diff --git a/packages/react-native-quick-crypto/src/utils/conversion.ts b/packages/react-native-quick-crypto/src/utils/conversion.ts index 1b4007a0..e8773650 100644 --- a/packages/react-native-quick-crypto/src/utils/conversion.ts +++ b/packages/react-native-quick-crypto/src/utils/conversion.ts @@ -29,30 +29,28 @@ const canCreateJsiStringFromUtf16 = !( Platform.constants.reactNativeVersion.minor < 79 ); -const baseNativeEncodings = ['hex', 'base64', 'base64url']; -const textNativeEncodings = ['utf8', 'utf-8', 'latin1', 'binary', 'ascii']; - -// Only enable native string -> buffer conversions on Hermes -// On non-Hermes runtimes, extracting string data can go through runtime-specific fallback paths which handle invalid UTF-16 strings differently. -// For example, JSStringGetUTF8CString() of JSC truncates JS strings when meets unpaired surrogates. -const nativeStringToBufferEncodings = new Set( - isHermes - ? [ - ...baseNativeEncodings, - ...textNativeEncodings, - ...(canGetU16StringFromJsiString ? ['utf16le'] : []), - ] - : [], -); - -// JSStringCreateWithUTF8CString() of JSC only accepts null-terminated UTF-8 strings without length argument -// baseNativeEncodings doesn't produce outputs with embedded '\0' so they are always enabled. -// For textNativeEncodings, the behavior of native buffer -> string conversions can be inconsistent across Hermes/JSC/V8, so they are disabled for non-Hermes runtimes. -const nativeBufferToStringEncodings = new Set([ - ...baseNativeEncodings, - ...(isHermes ? textNativeEncodings : []), - ...(isHermes && canCreateJsiStringFromUtf16 ? ['utf16le'] : []), -]); +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'); + } +} /** * Converts supplied argument to an ArrayBuffer. Note this does not copy the From c47dfede2d0bfaeb379a391c388d03a7c984a550 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 26 Apr 2026 23:48:44 +0800 Subject: [PATCH 12/18] Use safer fallbacks in utf16le encoding benchmarks --- example/src/benchmarks/encoding/encoding.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/example/src/benchmarks/encoding/encoding.ts b/example/src/benchmarks/encoding/encoding.ts index 0b60047c..0033724f 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'; @@ -142,7 +145,7 @@ const encode_utf16le_32b: BenchFn = () => { bench .add('rnqc', () => { - bufferToString(ab32B, 'utf16le'); + ab2str(ab32B, 'utf16le'); }) .add('Buffer polyfill', () => { ab2str_old(ab32B, 'utf16le'); @@ -161,7 +164,7 @@ const encode_utf16le_1mb: BenchFn = () => { bench .add('rnqc', () => { - bufferToString(ab1MB, 'utf16le'); + ab2str(ab1MB, 'utf16le'); }) .add('Buffer polyfill', () => { ab2str_old(ab1MB, 'utf16le'); @@ -180,7 +183,7 @@ const encode_utf16le_32b_ascii: BenchFn = () => { bench .add('rnqc', () => { - bufferToString(ab32B_ascii, 'utf16le'); + ab2str(ab32B_ascii, 'utf16le'); }) .add('Buffer polyfill', () => { ab2str_old(ab32B_ascii, 'utf16le'); @@ -199,7 +202,7 @@ const encode_utf16le_1mb_ascii: BenchFn = () => { bench .add('rnqc', () => { - bufferToString(ab1MB_ascii, 'utf16le'); + ab2str(ab1MB_ascii, 'utf16le'); }) .add('Buffer polyfill', () => { ab2str_old(ab1MB_ascii, 'utf16le'); @@ -296,7 +299,7 @@ const decode_utf16le_32b: BenchFn = () => { bench .add('rnqc', () => { - stringToBuffer(utf16le_32B_non_ascii, 'utf16le'); + binaryLikeToArrayBuffer(utf16le_32B_non_ascii, 'utf16le'); }) .add('Buffer polyfill', () => { stringToBuffer_old(utf16le_32B_non_ascii, 'utf16le'); @@ -315,7 +318,7 @@ const decode_utf16le_1mb: BenchFn = () => { bench .add('rnqc', () => { - stringToBuffer(utf16le_1MB_non_ascii, 'utf16le'); + binaryLikeToArrayBuffer(utf16le_1MB_non_ascii, 'utf16le'); }) .add('Buffer polyfill', () => { stringToBuffer_old(utf16le_1MB_non_ascii, 'utf16le'); @@ -334,7 +337,7 @@ const decode_utf16le_32b_ascii: BenchFn = () => { bench .add('rnqc', () => { - stringToBuffer(utf16le_32B_ascii, 'utf16le'); + binaryLikeToArrayBuffer(utf16le_32B_ascii, 'utf16le'); }) .add('Buffer polyfill', () => { stringToBuffer_old(utf16le_32B_ascii, 'utf16le'); @@ -353,7 +356,7 @@ const decode_utf16le_1mb_ascii: BenchFn = () => { bench .add('rnqc', () => { - stringToBuffer(utf16le_1MB_ascii, 'utf16le'); + binaryLikeToArrayBuffer(utf16le_1MB_ascii, 'utf16le'); }) .add('Buffer polyfill', () => { stringToBuffer_old(utf16le_1MB_ascii, 'utf16le'); From bfa4fb15748355bbf493c35a66620c394697aacb Mon Sep 17 00:00:00 2001 From: wh201906 Date: Mon, 27 Apr 2026 00:22:19 +0800 Subject: [PATCH 13/18] Add test cases for UTF16-LE encoding --- example/src/tests/utils/encoding_tests.ts | 78 +++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/example/src/tests/utils/encoding_tests.ts b/example/src/tests/utils/encoding_tests.ts index 24e3c362..d84a96c0 100644 --- a/example/src/tests/utils/encoding_tests.ts +++ b/example/src/tests/utils/encoding_tests.ts @@ -578,6 +578,84 @@ 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( + 'あいうえお', + ); +}); + +test( + SUITE, + '[Node.js] Decodes UTF-16LE bytes correctly from a sliced buffer starting at byte offset 1.', + () => { + const bytes = new Uint8Array([ + 0xff, 0x42, 0x30, 0x44, 0x30, 0x46, 0x30, 0x48, 0x30, 0x4a, 0x30, + ]); + expect( + bufferToString(bytes.slice(1).buffer as ArrayBuffer, 'utf16le'), + ).to.equal('あいうえお'); + }, +); + // --- Latin1 / Binary --- test( From 397326f637f55d0187fb046b9536048ffb3e55c1 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 28 Apr 2026 09:11:54 +0800 Subject: [PATCH 14/18] Unify naming style --- packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp | 4 ++-- packages/react-native-quick-crypto/cpp/utils/HybridUtils.hpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index ecfe32b9..72cafc6a 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -287,7 +287,7 @@ facebook::jsi::Value HybridUtils::bufferToJsiString(facebook::jsi::Runtime& runt } } -facebook::jsi::Value HybridUtils::JsiStringToBuffer(facebook::jsi::Runtime& runtime, const facebook::jsi::Value&, +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]] { @@ -337,7 +337,7 @@ void HybridUtils::loadHybridMethods() { HybridUtilsSpec::loadHybridMethods(); registerHybrids(this, [](Prototype& prototype) { prototype.registerRawHybridMethod("bufferToString", 2, &HybridUtils::bufferToJsiString); - prototype.registerRawHybridMethod("stringToBuffer", 2, &HybridUtils::JsiStringToBuffer); + prototype.registerRawHybridMethod("stringToBuffer", 2, &HybridUtils::jsiStringToBuffer); }); } diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.hpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.hpp index aaeb50d9..4d8bb108 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.hpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.hpp @@ -17,7 +17,7 @@ class HybridUtils : public HybridUtilsSpec { 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, + facebook::jsi::Value jsiStringToBuffer(facebook::jsi::Runtime& runtime, const facebook::jsi::Value& thisArg, const facebook::jsi::Value* args, size_t argCount); }; From a0c982c6f2ca13a29f61d9c00a0fedadf864ec55 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 28 Apr 2026 21:09:32 +0800 Subject: [PATCH 15/18] Add comments in decodeUtf16Le() --- .../react-native-quick-crypto/cpp/utils/HybridUtils.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index 72cafc6a..c7673812 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -145,22 +145,26 @@ namespace { } size_t offset = result.size(); - result.resize(offset + (num * 2)); + 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 } return; } const auto* utf16Src = reinterpret_cast(data); if constexpr (std::endian::native == std::endian::little && sizeof(char16_t) == 2) { + // Fast&direct copy path for expected endianness and char16_t size 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); From 02d389d5be134d4acde3895b7b7b15ddda2039af Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 28 Apr 2026 21:10:02 +0800 Subject: [PATCH 16/18] Use ab2str() for pre-encoded strings to prevent RN constraints --- example/src/benchmarks/encoding/encoding.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/example/src/benchmarks/encoding/encoding.ts b/example/src/benchmarks/encoding/encoding.ts index 0033724f..17290a37 100644 --- a/example/src/benchmarks/encoding/encoding.ts +++ b/example/src/benchmarks/encoding/encoding.ts @@ -48,14 +48,14 @@ const ab32B_ascii = generateData(32, true); const ab32B = generateData(32, false); // Pre-encode strings for decode benchmarks -const hex_1MB = bufferToString(ab1MB, 'hex'); -const hex_32B = bufferToString(ab32B, 'hex'); -const base64_1MB = bufferToString(ab1MB, 'base64'); -const base64_32B = bufferToString(ab32B, 'base64'); -const utf16le_1MB_ascii = bufferToString(ab1MB_ascii, 'utf16le'); -const utf16le_32B_ascii = bufferToString(ab32B_ascii, 'utf16le'); -const utf16le_1MB_non_ascii = bufferToString(ab1MB, 'utf16le'); -const utf16le_32B_non_ascii = bufferToString(ab32B, 'utf16le'); +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) --- From 6c052fef7980ea542cc993d1feb4c5f33d25a975 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 28 Apr 2026 21:13:10 +0800 Subject: [PATCH 17/18] Use C++ concept to detect features of jsi::String 1. REACT_NATIVE_VERSION_xxx macros are introduced in RN v0.79.0, so RNQC_NATIVE_GET_STRING_DATA won't work as expected in the old implementation 2. Both QuickCrypto.podspec and android/CMakeLists.txt explicitly specify C++20 so it's safe to use concept 3. In the old implementation, It's semantically possible that HybridUtils.cpp failed to import ReactNativeVersion.h and disabled native utf16 paths while conversion.ts still tries to access them. This commit fixes it. --- .../cpp/utils/HybridUtils.cpp | 165 ++++++++---------- 1 file changed, 77 insertions(+), 88 deletions(-) diff --git a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp index c7673812..43c09b4c 100644 --- a/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp +++ b/packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp @@ -8,35 +8,6 @@ #include #include -#if __has_include() -#include - -// jsi::String::createFromUtf16() is available in v0.79.0: -// https://github.com/facebook/react-native/commit/d9d824055e9f24614abd5657f9fc89a6ab3f2da2 -#if (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 79) -#pragma message("QuickCrypto: Native bufferToString('utf16le') is disabled.") -#define RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 0 -#else // (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 79) -#pragma message("QuickCrypto: Native bufferToString('utf16le') is enabled.") -#define RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 1 -#endif // (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 79) - -// jsi::String::getStringData() is available in v0.78.0: -// https://github.com/facebook/react-native/commit/c6f12254d16d87978383c08065a626d437e60450 -#if (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 78) -#pragma message("QuickCrypto: Native getStringData() fast path is disabled.") -#define RNQC_NATIVE_GET_STRING_DATA 0 -#else // (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 78) -#pragma message("QuickCrypto: Native getStringData() fast path is enabled.") -#define RNQC_NATIVE_GET_STRING_DATA 1 -#endif // (REACT_NATIVE_VERSION_MAJOR == 0 && REACT_NATIVE_VERSION_MINOR < 78) - -#else // __has_include() -#pragma message("QuickCrypto: was not found, The faster native path and native utf16 support are disabled.") -#define RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 0 -#define RNQC_NATIVE_GET_STRING_DATA 0 -#endif // __has_include() - #include "QuickCryptoUtils.hpp" #include "simdutf.h" @@ -45,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') @@ -118,64 +108,72 @@ namespace { return result; } -#if RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 - std::u16string encodeUtf16(const uint8_t* data, size_t len) { - // For !(std::endian::native == std::endian::little && sizeof(char16_t) == 2) - const size_t codeUnitCount = len / 2; - std::u16string result(codeUnitCount, u'\0'); - if (codeUnitCount == 0) { - 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)); + 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); } - return result; + throw std::runtime_error("Unsupported encoding: utf16le"); } -#endif // RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 - -#if RNQC_NATIVE_GET_STRING_DATA - // decodeUtf16Le() is not available for jsi::String::utf8() - std::vector decodeUtf16Le(facebook::jsi::Runtime& runtime, const facebook::jsi::String& str) { - std::vector result; - - 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' + 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; + } - 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 + 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; } - return; - } - const auto* utf16Src = reinterpret_cast(data); - if constexpr (std::endian::native == std::endian::little && sizeof(char16_t) == 2) { - // Fast&direct copy path for expected endianness and char16_t size - 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); - } - }; + 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; + str.getStringData(runtime, chunkCallback); + return result; + } + throw std::runtime_error("Unsupported encoding: utf16le"); } -#endif // RNQC_NATIVE_GET_STRING_DATA std::vector decodeLatin1(const std::string& str) { std::vector result; @@ -272,16 +270,9 @@ facebook::jsi::Value HybridUtils::bufferToJsiString(facebook::jsi::Runtime& runt } return facebook::jsi::String::createFromUtf8(runtime, result); } -#if RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 - // facebook::jsi::String::createFromUtf16() is available in React Native v0.79.0 and later if (encoding == "utf16le") { - if constexpr (std::endian::native == std::endian::little && sizeof(char16_t) == 2) { - return facebook::jsi::String::createFromUtf16(runtime, reinterpret_cast(data), len / 2); - } - auto encoded = encodeUtf16(data, len); - return facebook::jsi::String::createFromUtf16(runtime, encoded); + return createUtf16LeString(runtime, data, len); } -#endif // RNQC_HAS_NATIVE_CREATE_STRING_FROM_UTF16 throw std::runtime_error("Unsupported encoding: " + encoding); } catch (const std::exception& exception) { throw facebook::jsi::JSError(runtime, "Utils.bufferToString(...): " + std::string(exception.what())); @@ -322,12 +313,10 @@ facebook::jsi::Value HybridUtils::jsiStringToBuffer(facebook::jsi::Runtime& runt auto decoded = decodeLatin1(str.utf8(runtime)); return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); } -#if RNQC_NATIVE_GET_STRING_DATA if (encoding == "utf16le") { auto decoded = decodeUtf16Le(runtime, str); return JSIConverter>::toJSI(runtime, ArrayBuffer::move(std::move(decoded))); } -#endif // RNQC_NATIVE_GET_STRING_DATA throw std::runtime_error("Unsupported encoding: " + encoding); } catch (const std::exception& exception) { throw facebook::jsi::JSError(runtime, "Utils.stringToBuffer(...): " + std::string(exception.what())); From 7a3d503fe806fa75831734f703c59f5b96be437d Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 28 Apr 2026 21:14:01 +0800 Subject: [PATCH 18/18] Remove a useless test case bufferToString() doesn't provide offset(start) argument --- example/src/tests/utils/encoding_tests.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/example/src/tests/utils/encoding_tests.ts b/example/src/tests/utils/encoding_tests.ts index d84a96c0..12408e0d 100644 --- a/example/src/tests/utils/encoding_tests.ts +++ b/example/src/tests/utils/encoding_tests.ts @@ -643,19 +643,6 @@ test(SUITE, '[Node.js] Decodes UTF-16LE bytes back to Japanese text.', () => { ); }); -test( - SUITE, - '[Node.js] Decodes UTF-16LE bytes correctly from a sliced buffer starting at byte offset 1.', - () => { - const bytes = new Uint8Array([ - 0xff, 0x42, 0x30, 0x44, 0x30, 0x46, 0x30, 0x48, 0x30, 0x4a, 0x30, - ]); - expect( - bufferToString(bytes.slice(1).buffer as ArrayBuffer, 'utf16le'), - ).to.equal('あいうえお'); - }, -); - // --- Latin1 / Binary --- test(