react-native-nitro-buffer 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -11
- package/cpp/HybridNitroBuffer.cpp +211 -3
- package/lib/utils.js +6 -0
- package/package.json +1 -2
- package/src/utils.ts +6 -0
package/README.md
CHANGED
|
@@ -14,23 +14,43 @@ A high-performance, Node.js compatible `Buffer` implementation for React Native,
|
|
|
14
14
|
|
|
15
15
|
`react-native-nitro-buffer` is significantly faster than other Buffer implementations for React Native.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
### Device: iPad Air 5 (M1) - Physical Device
|
|
18
18
|
|
|
19
19
|
| Operation | Nitro Buffer | Competitor (Craftz) | Improvement |
|
|
20
20
|
|:---|:---:|:---:|:---:|
|
|
21
|
-
| `fill(0)` | **0.019ms** | 10.
|
|
22
|
-
| `write(utf8)` | **2.
|
|
23
|
-
| `toString(utf8)` | **0.
|
|
24
|
-
| `toString(base64)` | **0.
|
|
25
|
-
| `from(base64)` | **1.
|
|
26
|
-
| `toString(hex)` | **4.
|
|
27
|
-
| `from(hex)` | **11.
|
|
28
|
-
| `
|
|
21
|
+
| `fill(0)` | **0.019ms** | 10.37ms | **~545x 🚀** |
|
|
22
|
+
| `write(utf8)` | **2.47ms** | 212.04ms | **~85x 🚀** |
|
|
23
|
+
| `toString(utf8)` | **0.89ms** | 169.16ms | **~190x 🚀** |
|
|
24
|
+
| `toString(base64)` | **0.69ms** | 3.40ms | **~4.9x 🚀** |
|
|
25
|
+
| `from(base64)` | **1.40ms** | 146.56ms | **~104x 🚀** |
|
|
26
|
+
| `toString(hex)` | **4.85ms** | 57.34ms | **~11.8x 🚀** |
|
|
27
|
+
| `from(hex)` | **11.06ms** | 138.04ms | **~12.5x 🚀** |
|
|
28
|
+
| `btoa(1MB)` | **3.00ms** | 45.90ms | **~15.3x 🚀** |
|
|
29
|
+
| `atob(1MB)` | **5.12ms** | 149.73ms | **~29.2x 🚀** |
|
|
30
|
+
| `alloc(1MB)` | 0.33ms | 0.09ms | 0.27x |
|
|
31
|
+
|
|
32
|
+
### Device: iPhone 16 Pro Simulator (Mac mini M4)
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
| Operation | Nitro Buffer | Competitor (Craftz) | Improvement |
|
|
35
|
+
|:---|:---:|:---:|:---:|
|
|
36
|
+
| `fill(0)` | **0.015ms** | 13.78ms | **~918x 🚀** |
|
|
37
|
+
| `write(utf8)` | **4.27ms** | 163.46ms | **~38x 🚀** |
|
|
38
|
+
| `toString(utf8)` | **0.93ms** | 141.56ms | **~152x 🚀** |
|
|
39
|
+
| `toString(base64)` | **1.71ms** | 4.71ms | **~3x 🚀** |
|
|
40
|
+
| `from(base64)` | **16.45ms** | 104.67ms | **~6x 🚀** |
|
|
41
|
+
| `toString(hex)` | **4.89ms** | 43.46ms | **~9x 🚀** |
|
|
42
|
+
| `from(hex)` | **17.93ms** | 95.00ms | **~5x 🚀** |
|
|
43
|
+
| `btoa(1MB)` | **1.13ms** | 34.87ms | **~31x 🚀** |
|
|
44
|
+
| `atob(1MB)` | **2.18ms** | 91.41ms | **~42x 🚀** |
|
|
45
|
+
| `alloc(1MB)` | 0.18ms | 0.03ms | 0.16x |
|
|
46
|
+
|
|
47
|
+
*> Benchmarks averaged over 50 iterations on 1MB Buffer operations.*
|
|
31
48
|
|
|
32
49
|
> [!NOTE]
|
|
33
|
-
> **About `alloc` Performance**: The slight difference in allocation time (~0.3ms) is due to the overhead of initializing the ES6 Class structure (`Object.setPrototypeOf`), which provides a cleaner and safer type inheritance model compared to the functional mixin approach. This one-time initialization cost is negligible compared to the massive **
|
|
50
|
+
> **About `alloc` Performance**: The slight difference in allocation time (~0.3ms) is due to the overhead of initializing the ES6 Class structure (`Object.setPrototypeOf`), which provides a cleaner and safer type inheritance model compared to the functional mixin approach. This one-time initialization cost is negligible compared to the massive **5x - 550x** performance gains in actual Buffer operations.
|
|
51
|
+
|
|
52
|
+
> [!TIP]
|
|
53
|
+
> **`atob`/`btoa` Optimization**: In modern React Native environments (Hermes), `global.atob` and `global.btoa` are natively implemented and highly optimized. `react-native-nitro-buffer` automatically detects and uses these native implementations if available, ensuring your app runs at peak performance while maintaining Node.js utility compatibility.
|
|
34
54
|
|
|
35
55
|
## 📦 Installation
|
|
36
56
|
|
|
@@ -124,6 +144,21 @@ This library achieves **100% API compatibility** with Node.js `Buffer`.
|
|
|
124
144
|
const cBuf = CraftzBuffer.from(nBuf); // Works!
|
|
125
145
|
```
|
|
126
146
|
|
|
147
|
+
## ⚠️ Compatibility Notes
|
|
148
|
+
|
|
149
|
+
### `toString('ascii')` Behavior
|
|
150
|
+
|
|
151
|
+
When decoding binary data with non-ASCII bytes (0x80-0xFF), `react-native-nitro-buffer` follows the **Node.js standard** by replacing invalid bytes with the Unicode replacement character (`U+FFFD`, displayed as `�`).
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
const buf = Buffer.from([0x48, 0x69, 0x80, 0xFF, 0x21]); // "Hi" + invalid bytes + "!"
|
|
155
|
+
buf.toString('ascii');
|
|
156
|
+
// Nitro (Node.js compatible): "Hi��!" (length: 5)
|
|
157
|
+
// @craftzdog/react-native-buffer: "Hi!" (length: 5) - incorrectly drops invalid bytes
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This ensures consistent behavior with Node.js when handling binary protocols like WebSocket messages containing mixed text and binary data (e.g., Microsoft TTS audio streams).
|
|
161
|
+
|
|
127
162
|
## 📄 License
|
|
128
163
|
|
|
129
164
|
ISC
|
|
@@ -217,6 +217,206 @@ double HybridNitroBuffer::write(const std::shared_ptr<ArrayBuffer> &buffer,
|
|
|
217
217
|
return actualWrite;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// UTF-8 replacement character (U+FFFD) encoded as UTF-8
|
|
221
|
+
static const char UTF8_REPLACEMENT[] = "\xEF\xBF\xBD";
|
|
222
|
+
|
|
223
|
+
// Fast validation: returns true if all data is valid UTF-8, false otherwise
|
|
224
|
+
// This allows us to use memcpy for the common case of valid UTF-8
|
|
225
|
+
static bool isValidUtf8(const uint8_t *data, size_t len) {
|
|
226
|
+
size_t i = 0;
|
|
227
|
+
while (i < len) {
|
|
228
|
+
uint8_t byte1 = data[i];
|
|
229
|
+
|
|
230
|
+
// ASCII (0x00-0x7F) - most common case
|
|
231
|
+
if (byte1 <= 0x7F) {
|
|
232
|
+
i++;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Invalid leading byte
|
|
237
|
+
if (byte1 < 0xC2 || byte1 > 0xF4) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 2-byte sequence (0xC2-0xDF)
|
|
242
|
+
if (byte1 <= 0xDF) {
|
|
243
|
+
if (i + 1 >= len || (data[i + 1] & 0xC0) != 0x80) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
i += 2;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 3-byte sequence (0xE0-0xEF)
|
|
251
|
+
if (byte1 <= 0xEF) {
|
|
252
|
+
if (i + 2 >= len)
|
|
253
|
+
return false;
|
|
254
|
+
uint8_t byte2 = data[i + 1];
|
|
255
|
+
uint8_t byte3 = data[i + 2];
|
|
256
|
+
if ((byte2 & 0xC0) != 0x80 || (byte3 & 0xC0) != 0x80)
|
|
257
|
+
return false;
|
|
258
|
+
// Check overlong and surrogate
|
|
259
|
+
if (byte1 == 0xE0 && byte2 < 0xA0)
|
|
260
|
+
return false;
|
|
261
|
+
if (byte1 == 0xED && byte2 >= 0xA0)
|
|
262
|
+
return false;
|
|
263
|
+
i += 3;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 4-byte sequence (0xF0-0xF4)
|
|
268
|
+
if (i + 3 >= len)
|
|
269
|
+
return false;
|
|
270
|
+
uint8_t byte2 = data[i + 1];
|
|
271
|
+
uint8_t byte3 = data[i + 2];
|
|
272
|
+
uint8_t byte4 = data[i + 3];
|
|
273
|
+
if ((byte2 & 0xC0) != 0x80 || (byte3 & 0xC0) != 0x80 ||
|
|
274
|
+
(byte4 & 0xC0) != 0x80)
|
|
275
|
+
return false;
|
|
276
|
+
if (byte1 == 0xF0 && byte2 < 0x90)
|
|
277
|
+
return false;
|
|
278
|
+
if (byte1 == 0xF4 && byte2 > 0x8F)
|
|
279
|
+
return false;
|
|
280
|
+
i += 4;
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Slow path: Decode UTF-8 with replacement for invalid bytes
|
|
286
|
+
// Only called when isValidUtf8 returns false
|
|
287
|
+
static std::string decodeUtf8WithReplacementSlow(const uint8_t *data,
|
|
288
|
+
size_t len) {
|
|
289
|
+
std::string result;
|
|
290
|
+
result.reserve(len + len / 10); // Add 10% for potential replacements
|
|
291
|
+
|
|
292
|
+
size_t i = 0;
|
|
293
|
+
while (i < len) {
|
|
294
|
+
uint8_t byte1 = data[i];
|
|
295
|
+
|
|
296
|
+
// ASCII (0x00-0x7F)
|
|
297
|
+
if (byte1 <= 0x7F) {
|
|
298
|
+
result.push_back(static_cast<char>(byte1));
|
|
299
|
+
i++;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Invalid leading byte (0x80-0xBF or 0xF8-0xFF)
|
|
304
|
+
if (byte1 < 0xC2 || byte1 > 0xF4) {
|
|
305
|
+
result.append(UTF8_REPLACEMENT);
|
|
306
|
+
i++;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 2-byte sequence (0xC2-0xDF)
|
|
311
|
+
if (byte1 <= 0xDF) {
|
|
312
|
+
if (i + 1 >= len || (data[i + 1] & 0xC0) != 0x80) {
|
|
313
|
+
result.append(UTF8_REPLACEMENT);
|
|
314
|
+
i++;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
result.push_back(static_cast<char>(byte1));
|
|
318
|
+
result.push_back(static_cast<char>(data[i + 1]));
|
|
319
|
+
i += 2;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 3-byte sequence (0xE0-0xEF)
|
|
324
|
+
if (byte1 <= 0xEF) {
|
|
325
|
+
if (i + 2 >= len) {
|
|
326
|
+
result.append(UTF8_REPLACEMENT);
|
|
327
|
+
i++;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
uint8_t byte2 = data[i + 1];
|
|
331
|
+
uint8_t byte3 = data[i + 2];
|
|
332
|
+
if ((byte2 & 0xC0) != 0x80 || (byte3 & 0xC0) != 0x80 ||
|
|
333
|
+
(byte1 == 0xE0 && byte2 < 0xA0) || (byte1 == 0xED && byte2 >= 0xA0)) {
|
|
334
|
+
result.append(UTF8_REPLACEMENT);
|
|
335
|
+
i++;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
result.push_back(static_cast<char>(byte1));
|
|
339
|
+
result.push_back(static_cast<char>(byte2));
|
|
340
|
+
result.push_back(static_cast<char>(byte3));
|
|
341
|
+
i += 3;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 4-byte sequence (0xF0-0xF4)
|
|
346
|
+
if (i + 3 >= len) {
|
|
347
|
+
result.append(UTF8_REPLACEMENT);
|
|
348
|
+
i++;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
uint8_t byte2 = data[i + 1];
|
|
352
|
+
uint8_t byte3 = data[i + 2];
|
|
353
|
+
uint8_t byte4 = data[i + 3];
|
|
354
|
+
if ((byte2 & 0xC0) != 0x80 || (byte3 & 0xC0) != 0x80 ||
|
|
355
|
+
(byte4 & 0xC0) != 0x80 || (byte1 == 0xF0 && byte2 < 0x90) ||
|
|
356
|
+
(byte1 == 0xF4 && byte2 > 0x8F)) {
|
|
357
|
+
result.append(UTF8_REPLACEMENT);
|
|
358
|
+
i++;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
result.push_back(static_cast<char>(byte1));
|
|
362
|
+
result.push_back(static_cast<char>(byte2));
|
|
363
|
+
result.push_back(static_cast<char>(byte3));
|
|
364
|
+
result.push_back(static_cast<char>(byte4));
|
|
365
|
+
i += 4;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Decode UTF-8 with WHATWG-compliant error handling
|
|
372
|
+
// Uses fast path (memcpy) for valid UTF-8, slow path with replacement for
|
|
373
|
+
// invalid
|
|
374
|
+
static std::string decodeUtf8WithReplacement(const uint8_t *data, size_t len) {
|
|
375
|
+
// Fast path: if data is valid UTF-8, just copy it directly
|
|
376
|
+
if (isValidUtf8(data, len)) {
|
|
377
|
+
return std::string(reinterpret_cast<const char *>(data), len);
|
|
378
|
+
}
|
|
379
|
+
// Slow path: need to replace invalid sequences
|
|
380
|
+
return decodeUtf8WithReplacementSlow(data, len);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Decode as latin1/binary - each byte maps directly to Unicode code point
|
|
384
|
+
// 0x00-0xFF
|
|
385
|
+
static std::string decodeLatin1(const uint8_t *data, size_t len) {
|
|
386
|
+
std::string result;
|
|
387
|
+
result.reserve(len * 2); // Worst case: all bytes > 0x7F need 2 bytes in UTF-8
|
|
388
|
+
|
|
389
|
+
for (size_t i = 0; i < len; i++) {
|
|
390
|
+
uint8_t byte = data[i];
|
|
391
|
+
if (byte <= 0x7F) {
|
|
392
|
+
result.push_back(static_cast<char>(byte));
|
|
393
|
+
} else {
|
|
394
|
+
// Encode as 2-byte UTF-8 sequence
|
|
395
|
+
result.push_back(static_cast<char>(0xC0 | (byte >> 6)));
|
|
396
|
+
result.push_back(static_cast<char>(0x80 | (byte & 0x3F)));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Decode as ASCII - bytes > 0x7F are replaced with U+FFFD
|
|
404
|
+
static std::string decodeAscii(const uint8_t *data, size_t len) {
|
|
405
|
+
std::string result;
|
|
406
|
+
result.reserve(len);
|
|
407
|
+
|
|
408
|
+
for (size_t i = 0; i < len; i++) {
|
|
409
|
+
uint8_t byte = data[i];
|
|
410
|
+
if (byte <= 0x7F) {
|
|
411
|
+
result.push_back(static_cast<char>(byte));
|
|
412
|
+
} else {
|
|
413
|
+
result.append(UTF8_REPLACEMENT);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
|
|
220
420
|
std::string
|
|
221
421
|
HybridNitroBuffer::decode(const std::shared_ptr<ArrayBuffer> &buffer,
|
|
222
422
|
double offset, double length,
|
|
@@ -232,8 +432,15 @@ HybridNitroBuffer::decode(const std::shared_ptr<ArrayBuffer> &buffer,
|
|
|
232
432
|
size_t actualRead = std::min(available, count);
|
|
233
433
|
|
|
234
434
|
if (encoding == "utf8" || encoding == "utf-8") {
|
|
235
|
-
//
|
|
236
|
-
|
|
435
|
+
// WHATWG-compliant UTF-8 decoding with replacement character for invalid
|
|
436
|
+
// sequences
|
|
437
|
+
return decodeUtf8WithReplacement(data + start, actualRead);
|
|
438
|
+
} else if (encoding == "latin1" || encoding == "binary") {
|
|
439
|
+
// Each byte maps to Unicode code point 0x00-0xFF
|
|
440
|
+
return decodeLatin1(data + start, actualRead);
|
|
441
|
+
} else if (encoding == "ascii") {
|
|
442
|
+
// ASCII with replacement for non-ASCII bytes
|
|
443
|
+
return decodeAscii(data + start, actualRead);
|
|
237
444
|
} else if (encoding == "hex") {
|
|
238
445
|
std::string hex;
|
|
239
446
|
hex.reserve(actualRead * 2);
|
|
@@ -248,7 +455,8 @@ HybridNitroBuffer::decode(const std::shared_ptr<ArrayBuffer> &buffer,
|
|
|
248
455
|
return base64_encode(data + start, (unsigned int)actualRead);
|
|
249
456
|
}
|
|
250
457
|
|
|
251
|
-
|
|
458
|
+
// Default: UTF-8 with replacement
|
|
459
|
+
return decodeUtf8WithReplacement(data + start, actualRead);
|
|
252
460
|
}
|
|
253
461
|
|
|
254
462
|
double HybridNitroBuffer::compare(const std::shared_ptr<ArrayBuffer> &a,
|
package/lib/utils.js
CHANGED
|
@@ -8,9 +8,15 @@ exports.transcode = transcode;
|
|
|
8
8
|
exports.resolveObjectURL = resolveObjectURL;
|
|
9
9
|
const Buffer_1 = require("./Buffer");
|
|
10
10
|
function atob(data) {
|
|
11
|
+
if (typeof global.atob === 'function') {
|
|
12
|
+
return global.atob(data);
|
|
13
|
+
}
|
|
11
14
|
return Buffer_1.Buffer.from(data, 'base64').toString('binary');
|
|
12
15
|
}
|
|
13
16
|
function btoa(data) {
|
|
17
|
+
if (typeof global.btoa === 'function') {
|
|
18
|
+
return global.btoa(data);
|
|
19
|
+
}
|
|
14
20
|
return Buffer_1.Buffer.from(data, 'binary').toString('base64');
|
|
15
21
|
}
|
|
16
22
|
function isAscii(input) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nitro-buffer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Node.js compatible buffer module for React Native",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.js",
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"@types/node": "^18.19.130",
|
|
31
31
|
"@types/react": "*",
|
|
32
32
|
"jest": "^29.0.0",
|
|
33
|
-
"react-native-nitro-modules": "*",
|
|
34
33
|
"ts-jest": "^29.0.0",
|
|
35
34
|
"typescript": "^5.0.0"
|
|
36
35
|
},
|
package/src/utils.ts
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
import { Buffer } from './Buffer'
|
|
3
3
|
|
|
4
4
|
export function atob(data: string): string {
|
|
5
|
+
if (typeof global.atob === 'function') {
|
|
6
|
+
return global.atob(data)
|
|
7
|
+
}
|
|
5
8
|
return Buffer.from(data, 'base64').toString('binary')
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export function btoa(data: string): string {
|
|
12
|
+
if (typeof global.btoa === 'function') {
|
|
13
|
+
return global.btoa(data)
|
|
14
|
+
}
|
|
9
15
|
return Buffer.from(data, 'binary').toString('base64')
|
|
10
16
|
}
|
|
11
17
|
|