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
233 changes: 233 additions & 0 deletions example/src/tests/utils/encoding_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,226 @@ const SUITE = 'utils';

const toU8 = (ab: ArrayBuffer): Uint8Array => new Uint8Array(ab);

// --- Range arguments ---

test(
SUITE,
"[Node.js] bufferToString: Return empty string if start >= buffer's length.",
() => {
const ab = stringToBuffer('abc', 'ascii');
expect(bufferToString(ab, 'ascii', 3)).to.equal('');
expect(bufferToString(ab, 'ascii', Number.POSITIVE_INFINITY)).to.equal('');
expect(bufferToString(ab, 'ascii', 3.14, 3)).to.equal('');
expect(
bufferToString(ab, 'ascii', 'Infinity' as unknown as number, 3),
).to.equal('');
},
);

test(SUITE, '[Node.js] bufferToString: Return empty string if end <= 0', () => {
const ab = stringToBuffer('abc', 'ascii');
expect(bufferToString(ab, 'ascii', 1, 0)).to.equal('');
expect(bufferToString(ab, 'ascii', 1, -1.2)).to.equal('');
expect(bufferToString(ab, 'ascii', 1, -100)).to.equal('');
expect(bufferToString(ab, 'ascii', 1, Number.NEGATIVE_INFINITY)).to.equal('');
});

test(
SUITE,
'[Node.js] bufferToString: If start < 0, start will be taken as zero.',
() => {
const ab = stringToBuffer('abc', 'ascii');
const starts = [
-1,
-1.99,
Number.NEGATIVE_INFINITY,
'-1',
'-1.99',
'-Infinity',
] as const;

for (const start of starts) {
expect(
bufferToString(ab, 'ascii', start as unknown as number, 3),
).to.equal('abc');
}
},
);

test(
SUITE,
'[Node.js] bufferToString: If start is an invalid integer, start will be taken as zero.',
() => {
const ab = stringToBuffer('abc', 'ascii');
const starts = ['node.js', {}, [], NaN, null, undefined, false, ''];

for (const start of starts) {
expect(
bufferToString(ab, 'ascii', start as unknown as number, 3),
).to.equal('abc');
}
},
);

test(
SUITE,
'[Node.js] bufferToString: Use start values that coerce to integers',
() => {
const ab = stringToBuffer('abc', 'ascii');
const cases: Array<[unknown, string]> = [
['-1', 'abc'],
['1', 'bc'],
['-Infinity', 'abc'],
['3', ''],
[Number(3), ''],
['3.14', ''],
['1.99', 'bc'],
['-1.99', 'abc'],
[1.99, 'bc'],
[true, 'bc'],
];

for (const [start, expected] of cases) {
expect(
bufferToString(ab, 'ascii', start as unknown as number, 3),
).to.equal(expected);
}
},
);

test(
SUITE,
"[Node.js] bufferToString: If end > buffer's length, end will be taken as buffer's length.",
() => {
const ab = stringToBuffer('abc', 'ascii');
const ends = [5, 6.99, Number.POSITIVE_INFINITY, '5', '6.99', 'Infinity'];

for (const end of ends) {
expect(bufferToString(ab, 'ascii', 0, end as unknown as number)).to.equal(
'abc',
);
}
},
);

test(
SUITE,
'[Node.js] bufferToString: Handle invalid end values according to Buffer.prototype.toString() coercion rules.',
() => {
const ab = stringToBuffer('abc', 'ascii');
const cases: Array<[unknown, string]> = [
['node.js', ''],
[{}, ''],
[NaN, ''],
[undefined, 'abc'],
[null, ''],
[[], ''],
[false, ''],
['', ''],
];

for (const [end, expected] of cases) {
expect(bufferToString(ab, 'ascii', 0, end as unknown as number)).to.equal(
expected,
);
}
expect(bufferToString(ab, 'ascii', 0)).to.equal('abc');
},
);

test(
SUITE,
'[Node.js] bufferToString: Use end values that coerce to integers',
() => {
const ab = stringToBuffer('abc', 'ascii');
const cases: Array<[unknown, string]> = [
['-1', ''],
['1', 'a'],
['-Infinity', ''],
['3', 'abc'],
[Number(3), 'abc'],
['3.14', 'abc'],
['1.99', 'a'],
['-1.99', ''],
[1.99, 'a'],
[true, 'a'],
];

for (const [end, expected] of cases) {
expect(bufferToString(ab, 'ascii', 0, end as unknown as number)).to.equal(
expected,
);
}
},
);

test(
SUITE,
'[Node.js] bufferToString: Test hex/base64/base64url partial range encoding',
() => {
const hex = new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe])
.buffer as ArrayBuffer;
expect(bufferToString(hex, 'hex')).to.equal('deadbeefcafe');
expect(bufferToString(hex, 'hex', 0, 3)).to.equal('deadbe');
expect(bufferToString(hex, 'hex', 2, 5)).to.equal('beefca');
expect(bufferToString(hex, 'hex', 4)).to.equal('cafe');
expect(bufferToString(hex, 'hex', 6)).to.equal('');
expect(bufferToString(hex, 'hex', 0, 0)).to.equal('');

const hello = stringToBuffer('Hello, World!', 'utf8');
expect(bufferToString(hello, 'base64')).to.equal('SGVsbG8sIFdvcmxkIQ==');
expect(bufferToString(hello, 'base64', 0, 5)).to.equal('SGVsbG8=');
expect(bufferToString(hello, 'base64', 7)).to.equal('V29ybGQh');
expect(bufferToString(hello, 'base64', 0, 0)).to.equal('');

expect(bufferToString(hello, 'base64url')).to.equal('SGVsbG8sIFdvcmxkIQ');
expect(bufferToString(hello, 'base64url', 0, 5)).to.equal('SGVsbG8');
expect(bufferToString(hello, 'base64url', 7)).to.equal('V29ybGQh');
expect(bufferToString(hello, 'base64url', 0, 0)).to.equal('');
},
);

test(
SUITE,
'[Node.js] bufferToString: Test with pool-allocated buffer (has non-zero byteOffset)',
() => {
const data = toU8(stringToBuffer('test data for hex encoding', 'utf8'));
const backing = new Uint8Array(data.length + 8);
backing.set(data, 4);

const poolBuf = backing.subarray(4, 4 + data.length);
const exactHex = bufferToString(data.buffer as ArrayBuffer, 'hex');
const dataHex = bufferToString(stringToBuffer('data', 'utf8'), 'hex');

expect(
bufferToString(
poolBuf.buffer as ArrayBuffer,
'hex',
poolBuf.byteOffset,
poolBuf.byteOffset + poolBuf.byteLength,
),
).to.equal(exactHex);
expect(
bufferToString(
poolBuf.buffer as ArrayBuffer,
'hex',
poolBuf.byteOffset + 5,
poolBuf.byteOffset + 9,
),
).to.equal(dataHex);
},
);

test(SUITE, '[Node.js] bufferToString: Test an invalid slice end.', () => {
const ab = new Uint8Array([1, 2, 3, 4, 5]).buffer as ArrayBuffer;
const b2 = bufferToString(ab, 'hex', 1, 10000);
const b3 = bufferToString(ab, 'hex', 1, 5);
const b4 = bufferToString(ab, 'hex', 1);

expect(b2).to.equal(b3);
expect(b2).to.equal(b4);
});

// --- Hex ---

test(SUITE, 'hex encode empty buffer', () => {
Expand Down Expand Up @@ -643,6 +863,19 @@ test(SUITE, '[Node.js] Decodes UTF-16LE bytes back to Japanese text.', () => {
);
});

test(
SUITE,
'[Node.js] Decode UTF-16LE bytes back to Japanese text from byte offset 1.',
() => {
const bytes = new Uint8Array([
0xff, 0x42, 0x30, 0x44, 0x30, 0x46, 0x30, 0x48, 0x30, 0x4a, 0x30,
]);
expect(bufferToString(bytes.buffer as ArrayBuffer, 'utf16le', 1)).to.equal(
'あいうえお',
);
},
);

// --- Latin1 / Binary ---

test(
Expand Down
38 changes: 25 additions & 13 deletions packages/react-native-quick-crypto/cpp/utils/HybridUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,44 +234,56 @@ bool HybridUtils::timingSafeEqual(const std::shared_ptr<ArrayBuffer>& a, const s
facebook::jsi::Value HybridUtils::bufferToJsiString(facebook::jsi::Runtime& runtime, const facebook::jsi::Value&,
const facebook::jsi::Value* args, size_t argCount) {
// Runtime argument check from react-native-nitro-modules/cpp/core/HybridFunction.hpp
if (argCount != 2) [[unlikely]] {
if (argCount != 4) [[unlikely]] {
throw facebook::jsi::JSError(runtime,
"`Utils.bufferToString(...)` expected 2 arguments, but received " + std::to_string(argCount) + "!");
"`Utils.bufferToString(...)` expected 4 arguments, but received " + std::to_string(argCount) + "!");
}

// Exception wrapper from react-native-nitro-modules/cpp/core/HybridFunction.hpp
try {
// bufferToString(buffer: ArrayBuffer, encoding: string): string; Defined in utils/conversion.ts
// bufferToString(buffer: ArrayBuffer, encoding: string, start: number, end: number): string;
// Defined in utils/conversion.ts
auto buffer = JSIConverter<std::shared_ptr<ArrayBuffer>>::fromJSI(runtime, args[0]);
std::string encoding = JSIConverter<std::string>::fromJSI(runtime, args[1]);
const size_t bufferSize = buffer->size();
// `start` and `end` are normalized in the TS code
// so it's safe to use `static_cast<size_t>` here
const size_t start = static_cast<size_t>(JSIConverter<double>::fromJSI(runtime, args[2]));
const size_t end = static_cast<size_t>(JSIConverter<double>::fromJSI(runtime, args[3]));
if (start > end || end > bufferSize) {
// This should never happen if called from the TS wrapper
// Add this check to avoid out of bounds access
throw std::runtime_error("Invalid start/end value");
}
const size_t offset = start;
const size_t length = end - start;

const auto* data = reinterpret_cast<const uint8_t*>(buffer->data());
size_t len = buffer->size();
const auto* data = reinterpret_cast<const uint8_t*>(buffer->data() + offset);

if (encoding == "hex") {
return facebook::jsi::String::createFromUtf8(runtime, encodeHex(data, len));
return facebook::jsi::String::createFromUtf8(runtime, encodeHex(data, length));
}
if (encoding == "base64") {
return facebook::jsi::String::createFromUtf8(runtime, encodeBase64(data, len));
return facebook::jsi::String::createFromUtf8(runtime, encodeBase64(data, length));
}
if (encoding == "base64url") {
return facebook::jsi::String::createFromUtf8(runtime, encodeBase64Url(data, len));
return facebook::jsi::String::createFromUtf8(runtime, encodeBase64Url(data, length));
}
if (encoding == "utf8" || encoding == "utf-8") {
return facebook::jsi::String::createFromUtf8(runtime, data, len);
return facebook::jsi::String::createFromUtf8(runtime, data, length);
}
if (encoding == "latin1" || encoding == "binary") {
return facebook::jsi::String::createFromUtf8(runtime, encodeLatin1(data, len));
return facebook::jsi::String::createFromUtf8(runtime, encodeLatin1(data, length));
}
if (encoding == "ascii") {
std::string result(reinterpret_cast<const char*>(data), len);
std::string result(reinterpret_cast<const char*>(data), length);
for (auto& c : result) {
c &= 0x7F;
}
return facebook::jsi::String::createFromUtf8(runtime, result);
}
if (encoding == "utf16le") {
return createUtf16LeString(runtime, data, len);
return createUtf16LeString(runtime, data, length);
}
throw std::runtime_error("Unsupported encoding: " + encoding);
} catch (const std::exception& exception) {
Expand Down Expand Up @@ -329,7 +341,7 @@ facebook::jsi::Value HybridUtils::jsiStringToBuffer(facebook::jsi::Runtime& runt
void HybridUtils::loadHybridMethods() {
HybridUtilsSpec::loadHybridMethods();
registerHybrids(this, [](Prototype& prototype) {
prototype.registerRawHybridMethod("bufferToString", 2, &HybridUtils::bufferToJsiString);
prototype.registerRawHybridMethod("bufferToString", 4, &HybridUtils::bufferToJsiString);
prototype.registerRawHybridMethod("stringToBuffer", 2, &HybridUtils::jsiStringToBuffer);
});
}
Expand Down
44 changes: 38 additions & 6 deletions packages/react-native-quick-crypto/src/utils/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import type { ABV, BinaryLikeNode, BufferLike } from './types';
import { Platform } from 'react-native';

type UtilsWithStringConverter = Utils & {
bufferToString(buffer: ArrayBuffer, encoding: string): string;
bufferToString(
buffer: ArrayBuffer,
encoding: string,
start?: number,
end?: number,
): string;
stringToBuffer(str: string, encoding: string): ArrayBuffer;
};

Expand Down Expand Up @@ -223,19 +228,46 @@ export function binaryLikeToArrayBuffer(
);
}

export function ab2str(buf: ArrayBuffer, encoding: string = 'hex'): string {
export function ab2str(
buf: ArrayBuffer,
encoding: string = 'hex',
start?: number,
end?: number,
): string {
if (nativeBufferToStringEncodings.has(encoding)) {
return utils.bufferToString(buf, encoding);
return bufferToString(buf, encoding, start, end);
}
return CraftzdogBuffer.from(buf).toString(encoding);

return CraftzdogBuffer.from(buf).toString(encoding, start, end);
}

/** Native C++ buffer-to-string — exposed for benchmarking */
/** Native C++ buffer-to-string with arguments normalization*/
export function bufferToString(
buf: ArrayBuffer,
encoding: string = 'hex',
start?: number,
end?: number,
): string {
return utils.bufferToString(buf, encoding);
// https://github.com/nodejs/node/blob/v24.15.0/lib/buffer.js#L915-L928
if (start === undefined || start < 0) {
start = 0;
} else if (start >= buf.byteLength) {
return '';
} else {
start = Math.trunc(start) || 0;
}

if (end === undefined || end > buf.byteLength) {
end = buf.byteLength;
} else {
end = Math.trunc(end) || 0;
}

if (end <= start) {
return '';
}

return utils.bufferToString(buf, encoding, start, end);
}

/** Native C++ string-to-buffer — exposed for benchmarking */
Expand Down
Loading