Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 128 additions & 12 deletions example/src/tests/utils/encoding_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,12 +767,6 @@ test(SUITE, 'utf8 encode/decode multibyte', () => {
expect(bufferToString(ab, 'utf-8')).to.equal(str);
});

test(SUITE, 'utf8 alias "utf8" works', () => {
const str = 'test';
const ab = stringToBuffer(str, 'utf8');
expect(bufferToString(ab, 'utf8')).to.equal(str);
});

test(SUITE, '[Node.js] Test for proper UTF-8 Encoding', () => {
expect(toU8(stringToBuffer('\u00fcber', 'utf8'))).to.deep.equal(
new Uint8Array([195, 188, 98, 101, 114]),
Expand Down Expand Up @@ -800,12 +794,6 @@ 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.',
Expand Down Expand Up @@ -876,6 +864,44 @@ test(
},
);

// --- General ---

test(
SUITE,
'[Node.js] Try to create 0-length buffers. Should not throw.',
() => {
const encodings = [
'utf8',
'utf16le',
'ascii',
'latin1',
'binary',
'base64',
'base64url',
'hex',
] as const;

for (const encoding of encodings) {
const ab = stringToBuffer('', encoding);
expect(ab.byteLength).to.equal(0);
expect(bufferToString(ab, encoding)).to.equal('');
}
},
);

test(
SUITE,
"[Node.js] Buffer.from('foo', encoding).toString(encoding) returns 'foo'.",
() => {
const encodings = ['utf8', 'utf16le', 'ascii', 'latin1', 'binary'] as const;

for (const encoding of encodings) {
const ab = stringToBuffer('foo', encoding);
expect(bufferToString(ab, encoding)).to.equal('foo');
}
},
);

// --- Latin1 / Binary ---

test(
Expand All @@ -899,6 +925,42 @@ test(
},
);

test(SUITE, '[Node.js] Data "Hello, ÆÊÎÖÿ".', () => {
const str = 'Hello, ÆÊÎÖÿ';
const expected = new Uint8Array([
...Array.from('Hello, ', c => c.charCodeAt(0)),
0xc6,
0xca,
0xce,
0xd6,
0xff,
]);
const ab = stringToBuffer(str, 'latin1');

expect(toU8(ab)).to.deep.equal(expected);
expect(bufferToString(expected.buffer as ArrayBuffer, 'latin1')).to.equal(
str,
);
});

test(
SUITE,
'[Node.js] Verify that StringBytes::Write converts two-byte characters to one-byte characters, even if there is no valid one-byte representation.',
() => {
const expected = new Uint8Array([
...Array.from('Hello, ', c => c.charCodeAt(0)),
0x16,
0x4c,
]);
const ab = stringToBuffer('Hello, 世界', 'latin1');

expect(toU8(ab)).to.deep.equal(expected);
expect(bufferToString(ab, 'latin1')).to.equal(
String.fromCharCode(...expected),
);
},
);

test(SUITE, 'latin1 roundtrip all byte values 0x00-0xFF', () => {
const bytes = new Uint8Array(256);
for (let i = 0; i < 256; i++) bytes[i] = i;
Expand Down Expand Up @@ -947,8 +1009,62 @@ test(
},
);

test(
SUITE,
'[Node.js] Manually controlled string for checking binary output',
() => {
const ucs2Control = 'a\u0000';
const writeStr = 'a';
const bytes = toU8(stringToBuffer(writeStr, 'utf16le'));

expect(bytes[0]).to.equal(0x61);
expect(bytes[1]).to.equal(0);
expect(bufferToString(bytes.buffer as ArrayBuffer, 'latin1')).to.equal(
ucs2Control,
);
expect(bufferToString(bytes.buffer as ArrayBuffer, 'binary')).to.equal(
ucs2Control,
);
},
);

// --- ASCII ---

test(SUITE, '[Node.js] ASCII slice test', () => {
{
const asciiString = 'hello world';
const bytes = new Uint8Array(128);

for (let i = 0; i < asciiString.length; i++) {
bytes[i] = asciiString.charCodeAt(i);
}
const asciiSlice = bufferToString(
bytes.buffer as ArrayBuffer,
'ascii',
0,
asciiString.length,
);

expect(asciiSlice).to.equal(asciiString);
}

{
const asciiString = 'hello world';
const offset = 100;
const bytes = new Uint8Array(128);

bytes.set(toU8(stringToBuffer(asciiString, 'ascii')), offset);
const asciiSlice = bufferToString(
bytes.buffer as ArrayBuffer,
'ascii',
offset,
offset + asciiString.length,
);

expect(asciiSlice).to.equal(asciiString);
}
});

test(SUITE, 'ascii roundtrip printable ASCII', () => {
const str = 'Hello, World! 123';
const ab = stringToBuffer(str, 'ascii');
Expand Down
50 changes: 48 additions & 2 deletions packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ namespace {
throw std::runtime_error("Unsupported encoding: utf16le");
}

std::vector<uint8_t> decodeLatin1(const std::string& str) {
std::vector<uint8_t> decodeLatin1FromUtf8(const std::string& str) {
std::vector<uint8_t> result;
result.reserve(str.size());
size_t i = 0;
Expand Down Expand Up @@ -204,6 +204,43 @@ namespace {
return result;
}

template <typename JSIString = facebook::jsi::String>
std::vector<uint8_t> decodeLatin1(facebook::jsi::Runtime& runtime, bool isHermes, const JSIString& str) {
if constexpr (HasStringGetStringData<JSIString>) {
if (isHermes) {
std::vector<uint8_t> result;
auto chunkCallback = [&result](bool isAscii, const void* data, size_t num) {
if (num == 0) {
return;
}

size_t offset = result.size();
result.reserve(offset + num); // Allocate buffer conservatively

if (isAscii) {
// Fast&direct copy path
const auto* asciiSrc = reinterpret_cast<const uint8_t*>(data);
result.insert(result.end(), asciiSrc, asciiSrc + num);
return;
}

result.resize(offset + num);
const auto* utf16Src = reinterpret_cast<const char16_t*>(data);
auto* dst = result.data() + offset;
for (size_t i = 0; i < num; i++) {
// Node.js-like behavior
dst[i] = static_cast<uint8_t>(utf16Src[i] & 0xFFu);
}
};

str.getStringData(runtime, chunkCallback);
return result;
}
}
// Slow path for non-Hermes runtime/old RN versions
return decodeLatin1FromUtf8(str.utf8(runtime));
}
Comment thread
boorad marked this conversation as resolved.

std::string encodeLatin1(const uint8_t* data, size_t len) {
if (len == 0) {
return {};
Expand Down Expand Up @@ -231,6 +268,15 @@ bool HybridUtils::timingSafeEqual(const std::shared_ptr<ArrayBuffer>& a, const s
return CRYPTO_memcmp(a->data(), b->data(), aLen) == 0;
}

bool HybridUtils::isHermesRuntime(facebook::jsi::Runtime& runtime) {
// Cache assumes runtimes are long-lived and calls happen on the JS thread.
if (cachedRuntime_ != &runtime) [[unlikely]] {
cachedRuntime_ = &runtime;
cachedIsHermesRuntime_ = runtime.global().hasProperty(runtime, "HermesInternal");
}
return cachedIsHermesRuntime_;
}
Comment thread
boorad marked this conversation as resolved.

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
Expand Down Expand Up @@ -322,7 +368,7 @@ facebook::jsi::Value HybridUtils::jsiStringToBuffer(facebook::jsi::Runtime& runt
runtime, ArrayBuffer::copy(reinterpret_cast<const uint8_t*>(utf8Str.data()), utf8Str.size()));
}
if (encoding == "latin1" || encoding == "binary" || encoding == "ascii") {
auto decoded = decodeLatin1(str.utf8(runtime));
auto decoded = decodeLatin1(runtime, isHermesRuntime(runtime), str);
return JSIConverter<std::shared_ptr<ArrayBuffer>>::toJSI(runtime, ArrayBuffer::move(std::move(decoded)));
}
if (encoding == "utf16le") {
Expand Down
4 changes: 4 additions & 0 deletions packages/react-native-quick-crypto/cpp/utils/HybridUtils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class HybridUtils : public HybridUtilsSpec {
void loadHybridMethods() override;

private:
facebook::jsi::Runtime* cachedRuntime_ = nullptr;
bool cachedIsHermesRuntime_ = false;

bool isHermesRuntime(facebook::jsi::Runtime& runtime);
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,
Expand Down