Address review comments on Buffer methods for checked-dynamic values

This commit is contained in:
Chris Tate
2026-10-21 09:57:56 -05:00
parent f2fd1c85a2
commit 94f040b3df
5 changed files with 328 additions and 31 deletions
@@ -1479,6 +1479,8 @@ function lowerBufferInstanceMethod(
if (encName === "utf16le") align = 2;
} else if (vT?.kind === "bytes" && vT.elem === "u8") {
needle = lowerer.lowerExpr(args[0]!);
// A Buffer needle under utf16le also searches whole 2-byte units.
if (encName === "utf16le") align = 2;
} else {
lowerer.noLowering(
`.${name} of '${lowerer.checker.typeToString(lowerer.typeOf(args[0]!))}' values`,
+55 -9
View File
@@ -1489,8 +1489,10 @@ ScrStr *scr_strdec_end(const ScrStr *enc, double pending) {
static void scr_bytes_to_str_bounds(const ScrBytes *b, double start, double end,
size_t *s0_out, size_t *e0_out) {
size_t len = b->len;
size_t s0 = start <= 0 ? 0 : ((size_t)start > len ? len : (size_t)start);
size_t e0 = end <= 0 ? 0 : ((size_t)end > len ? len : (size_t)end);
/* Clamped before the conversion: NaN and values at or below 0 are 0,
* values at or past the length (Infinity included) are the length. */
size_t s0 = !(start > 0) ? 0 : start >= (double)len ? len : (size_t)start;
size_t e0 = !(end > 0) ? 0 : end >= (double)len ? len : (size_t)end;
if (e0 < s0) e0 = s0;
*s0_out = s0;
*e0_out = e0;
@@ -3204,15 +3206,61 @@ double scr_bytes_compare(const ScrBytes *src, const ScrBytes *target, double nar
return sn < tn ? -1 : 1;
}
/* node_buffer.cc's IndexOfBuffer/IndexOfString for the UCS2 encoding over
* an encoded needle: buffer.js clamps the byteOffset to the int32 range,
* IndexOfOffset picks the start, the search runs over whole 2-byte units of
* the haystack (an odd trailing byte of either side is ignored), and a match
* is reported at its byte position. */
static double scr_bytes_index_of_ucs2(const ScrBytes *haystack, const ScrBytes *needle, double at, bool fwd) {
int64_t length = (int64_t)haystack->len, needle_length = (int64_t)needle->len;
/* NaN (an omitted or non-numeric byteOffset) searches everything. */
if (at > 2147483647.0) at = 2147483647.0;
else if (at < -2147483648.0) at = -2147483648.0;
int64_t offset_i64 = at != at ? (fwd ? 0 : length) : (int64_t)trunc(at);
int64_t opt_offset;
if (offset_i64 < 0) {
if (offset_i64 + length >= 0) opt_offset = length + offset_i64;
else opt_offset = fwd || needle_length == 0 ? 0 : -1;
} else if (offset_i64 + needle_length <= length) {
opt_offset = offset_i64;
} else if (needle_length == 0) {
opt_offset = length;
} else {
opt_offset = fwd ? -1 : length - 1;
}
size_t search_end = haystack->len & ~(size_t)1;
if (needle_length == 0) return (double)(opt_offset < (int64_t)search_end ? opt_offset : (int64_t)search_end);
if (length == 0 || opt_offset <= -1) return -1;
size_t offset = (size_t)opt_offset;
if (offset >= search_end) {
if (fwd || search_end == 0) return -1;
offset = search_end - 1;
}
if ((fwd && needle->len + offset > search_end) || needle->len > search_end) return -1;
if (search_end < 2 || needle->len < 2) return -1;
size_t units = search_end / 2, needle_units = needle->len / 2, start = offset / 2;
if (units < needle_units) return -1;
size_t last = units - needle_units;
if (fwd) {
for (size_t p = start; p <= last; p++)
if (!memcmp(haystack->data + 2 * p, needle->data, 2 * needle_units)) return (double)(2 * p);
return -1;
}
for (size_t p = start < last ? start : last;; p--) {
if (!memcmp(haystack->data + 2 * p, needle->data, 2 * needle_units)) return (double)(2 * p);
if (p == 0) return -1;
}
}
/* The shared search core: needle bytes, a coerced byteOffset (trunc; NaN
* means 'search everything' — 0 forward, the end backward; negatives
* count from the end, and a still-negative backward search answers -1),
* and the utf16le alignment stride. */
* and the alignment: 2 searches utf16le units (scr_bytes_index_of_ucs2). */
double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, double align, bool fwd) {
SCR_BYTES_SNAPSHOT(b);
SCR_BYTES_SNAPSHOT(needle);
if (align == 2) return scr_bytes_index_of_ucs2(b, needle, off, fwd);
size_t len = b->len, nlen = needle->len;
size_t step = align == 2 ? 2 : 1;
double o = (off != off) ? (fwd ? 0 : (double)len) : trunc(off);
if (o < 0) {
o += (double)len;
@@ -3225,17 +3273,15 @@ double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off,
if (nlen > len) return -1;
if (fwd) {
size_t start = o > (double)len ? len : (size_t)o;
if (step == 2) start += start % 2;
for (size_t i = start; i + nlen <= len; i += step) {
for (size_t i = start; i + nlen <= len; i++) {
if (memcmp(b->data + i, needle->data, nlen) == 0) return (double)i;
}
return -1;
}
size_t start = o > (double)(len - nlen) ? len - nlen : (size_t)o;
if (step == 2) start -= start % 2;
for (size_t i = start;; i -= step) {
for (size_t i = start;; i--) {
if (memcmp(b->data + i, needle->data, nlen) == 0) return (double)i;
if (i < step) break;
if (i == 0) break;
}
return -1;
}
+148 -21
View File
@@ -187,6 +187,91 @@ static ScrStr *scr_bytes_dyn_quoted(const ScrStr *text) {
return scr_jb_finish(&jb);
}
/* Appends Node's inspect of an ArrayBuffer at indentation level `indent`:
* the first 100 bytes as hex pairs, on one line when it fits the 80-column
* break length (reduceToSingleString), one entry per line otherwise. */
static void scr_bytes_dyn_inspect_array_buffer(ScrJsonBuf *jb, const ScrBytes *owner, size_t indent) {
static const char digits[] = "0123456789abcdef";
size_t len = owner->len * scr_bytes_elem_size(owner->elem);
size_t shown = len < 100 ? len : 100;
ScrJsonBuf contents;
scr_jb_init(&contents);
scr_jb_puts(&contents, "[Uint8Contents]: <");
for (size_t i = 0; i < shown; i++) {
uint8_t byte;
scr_bytes_read(owner, i, &byte, 1);
if (i) scr_jb_putc(&contents, ' ');
scr_jb_putc(&contents, digits[byte >> 4]);
scr_jb_putc(&contents, digits[byte & 15]);
}
if (len > shown) {
char more[48];
snprintf(more, sizeof more, " ... %zu more byte%s", len - shown, len - shown > 1 ? "s" : "");
scr_jb_puts(&contents, more);
}
scr_jb_putc(&contents, '>');
ScrStr *first = scr_jb_finish(&contents);
char second[40];
int second_len = snprintf(second, sizeof second, "[byteLength]: %zu", len);
/* isBelowBreakLength with two entries, start = 2 + indentation + "ArrayBuffer {" + 10. */
size_t total = 2 + (2 + indent + 13 + 10) + first->len + (size_t)second_len;
if (total <= 80) {
scr_jb_puts(jb, "ArrayBuffer { ");
scr_jb_put_str(jb, first);
scr_jb_puts(jb, ", ");
scr_jb_puts(jb, second);
scr_jb_puts(jb, " }");
} else {
scr_jb_puts(jb, "ArrayBuffer {\n");
for (size_t i = 0; i < indent + 2; i++) scr_jb_putc(jb, ' ');
scr_jb_put_str(jb, first);
scr_jb_puts(jb, ",\n");
for (size_t i = 0; i < indent + 2; i++) scr_jb_putc(jb, ' ');
scr_jb_puts(jb, second);
scr_jb_putc(jb, '\n');
for (size_t i = 0; i < indent; i++) scr_jb_putc(jb, ' ');
scr_jb_putc(jb, '}');
}
scr_str_release(first);
}
/* ERR_INVALID_ARG_VALUE for an empty ArrayBufferView fill value: Node's
* inspect of the view (a Buffer, an empty typed array, or a DataView with
* its buffer), cut to 128 characters followed by "...". */
static ScrStr *scr_bytes_dyn_empty_view_message(const ScrDyn *value) {
const ScrBytes *view = value->v.bytes;
ScrJsonBuf jb;
scr_jb_init(&jb);
if (value->buffer) {
scr_jb_puts(&jb, "<Buffer >");
} else if (!view->is_data_view) {
scr_jb_puts(&jb, scr_bytes_elem_name(view->elem));
scr_jb_puts(&jb, "(0) []");
} else {
const ScrBytes *owner = view->backing ? view->backing : view;
char line[64];
snprintf(line, sizeof line, "DataView {\n [byteLength]: 0,\n [byteOffset]: %zu,\n [buffer]: ",
(size_t)(view->data - owner->data));
scr_jb_puts(&jb, line);
scr_bytes_dyn_inspect_array_buffer(&jb, owner, 2);
scr_jb_puts(&jb, "\n}");
}
ScrStr *inspected = scr_jb_finish(&jb);
ScrJsonBuf out;
scr_jb_init(&out);
scr_jb_puts(&out, "The argument 'value' is invalid. Received ");
if (inspected->len > 128) {
ScrStr *head = scr_str_new(inspected->data, 128);
scr_jb_put_str(&out, head);
scr_str_release(head);
scr_jb_puts(&out, "...");
} else {
scr_jb_put_str(&out, inspected);
}
scr_str_release(inspected);
return scr_jb_finish(&out);
}
/* buf.fill(value[, offset[, end]][, encoding]): buffer.js's _fill. A string
* takes its encoding from the first string among offset and end; numbers and
* other non-view values fill with ToUint32(value) & 255. Answers the receiver. */
@@ -244,9 +329,23 @@ static ScrDyn *scr_bytes_dyn_fill(ScrDyn *recv, ScrDyn *const *args, size_t argc
/* An empty string fills with zeros. */
out = text->len == 0 ? scr_bytes_fill_num(b, 0, 2, from, to) : scr_bytes_fill(b, pattern, 2, from, to);
scr_bytes_release(pattern);
} else if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view &&
value->v.bytes->elem == SCR_BYTES_U8) {
out = scr_bytes_fill(b, value->v.bytes, 2, from, to);
} else if (value->kind == SCR_DYN_BYTES) {
/* Any ArrayBufferView repeats its raw bytes: a byte view over the
* value's storage (a typed array of wider elements, or a DataView). */
const ScrBytes *view = value->v.bytes;
if (view->len == 0) {
ScrStr *message = scr_bytes_dyn_empty_view_message(value);
scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_INVALID_ARG_VALUE");
scr_str_release(message);
return NULL;
}
ScrBytes pattern = { .rc = 1,
.len = view->len * scr_bytes_elem_size(view->elem),
.elem = SCR_BYTES_U8,
.data = view->data,
.backing = NULL,
.shared = view->shared };
out = scr_bytes_fill(b, &pattern, 2, from, to);
} else {
double n;
if (!scr_dyn_number_coerce_js(value, &n)) return NULL;
@@ -256,21 +355,27 @@ static ScrDyn *scr_bytes_dyn_fill(ScrDyn *recv, ScrDyn *const *args, size_t argc
scr_bytes_release(out);
return scr_dyn_retain(recv);
}
/* buf.toString(encoding, start, end): the window clamps to
* [0, length] with NaN as 0, an empty window answers "" before the
* encoding resolves, and an undefined encoding is utf8. */
/* buf.toString(encoding, start, end): the window clamps to [0, length]
* with NaN as 0 (infinite and out-of-range values included), an empty
* window answers "" before the encoding resolves, and an undefined encoding
* is utf8. */
static ScrDyn *scr_bytes_dyn_to_string(ScrBytes *b, ScrDyn *const *args, size_t argc) {
const ScrDyn *encoding = scr_bytes_dyn_arg(args, argc, 0);
const ScrDyn *start = scr_bytes_dyn_arg(args, argc, 1);
const ScrDyn *end = scr_bytes_dyn_arg(args, argc, 2);
double from = 0, to = (double)b->len;
const double len = (double)b->len;
double from = NAN, to = len;
if (!scr_bytes_dyn_undef(start) && !scr_dyn_number_coerce_js(start, &from)) return NULL;
if (!scr_bytes_dyn_undef(end) && !scr_dyn_number_coerce_js(end, &to)) return NULL;
if (from != from) from = 0;
if (to != to) to = 0;
from = trunc(from);
to = trunc(to);
if (from >= (double)b->len || to <= from || to <= 0) return scr_bytes_dyn_str("", 0);
/* `if (start <= 0) start = 0; else if (start >= len) return ''; else
* start = MathTrunc(start) || 0`, then `end > len` clamps to the length
* and other ends truncate (NaN as 0), so both stay within [0, len]. */
if (from <= 0) from = 0;
else if (from >= len) return scr_bytes_dyn_str("", 0);
else from = from == from ? trunc(from) : 0;
if (to > len) to = len;
else to = to == to ? trunc(to) : 0;
if (to <= from) return scr_bytes_dyn_str("", 0);
ScrStr *enc = scr_bytes_dyn_undef(encoding) ? scr_str_new("utf8", 4) : scr_bytes_dyn_encoding(encoding);
if (!enc) return NULL;
ScrStr *text = scr_bytes_to_str_range(b, enc, from, to);
@@ -313,6 +418,23 @@ static ScrDyn *scr_bytes_dyn_copy(const ScrBytes *b, ScrDyn *const *args, size_t
return scr_dyn_new_num(scr_bytes_copy_into(b, target->v.bytes, 3, offsets[0], offsets[1], offsets[2]));
}
/* Whether an encoding name is one of the utf16le spellings, in any ASCII case. */
static bool scr_bytes_dyn_is_ucs2(const ScrStr *raw) {
static const char *const names[] = { "utf16le", "utf-16le", "ucs2", "ucs-2" };
for (size_t i = 0; i < sizeof names / sizeof names[0]; i++) {
size_t n = strlen(names[i]);
if (raw->len != n) continue;
size_t k = 0;
for (; k < n; k++) {
char c = raw->data[k];
if (c >= 'A' && c <= 'Z') c = (char)(c + ('a' - 'A'));
if (c != names[i][k]) break;
}
if (k == n) return true;
}
return false;
}
/* buf.indexOf/lastIndexOf/includes(value[, byteOffset][, encoding]):
* buffer.js's bidirectionalIndexOf. A string byteOffset is the encoding;
* the offset clamps to the int32 range, and NaN searches everything. */
@@ -333,25 +455,30 @@ static ScrDyn *scr_bytes_dyn_index_of(const ScrBytes *b, ScrDyn *const *args, si
if (value->kind == SCR_DYN_NUM) {
found = scr_bytes_index_of_num(b, value->v.num, at, fwd);
} else {
ScrStr *enc = NULL;
ScrStr *raw = NULL;
if (!scr_bytes_dyn_undef(encoding)) {
ScrStr *raw = scr_dyn_string_coerce_js(encoding);
raw = scr_dyn_string_coerce_js(encoding);
if (!raw) return NULL;
/* An unknown encoding throws for a string needle only. */
enc = value->kind == SCR_DYN_STR ? scr_bytes_normalize_encoding(raw) : NULL;
scr_str_release(raw);
if (value->kind == SCR_DYN_STR && !enc) return NULL;
}
if (value->kind == SCR_DYN_STR) {
if (!enc) enc = scr_str_new("utf8", 4);
/* An unknown encoding throws for a string needle. */
ScrStr *enc = raw ? scr_bytes_normalize_encoding(raw) : scr_str_new("utf8", 4);
if (raw) scr_str_release(raw);
if (!enc) return NULL;
bool ucs2 = enc->len == 7 && !memcmp(enc->data, "utf16le", 7);
ScrBytes *needle = scr_bytes_from_str(value->v.str, enc);
found = scr_bytes_index_of(b, needle, at, enc->len == 7 && !memcmp(enc->data, "utf16le", 7) ? 2 : 1, fwd);
found = scr_bytes_index_of(b, needle, at, ucs2 ? 2 : 1, fwd);
scr_bytes_release(needle);
scr_str_release(enc);
} else if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view &&
value->v.bytes->elem == SCR_BYTES_U8) {
found = scr_bytes_index_of(b, value->v.bytes, at, 1, fwd);
/* A Buffer needle searches 2-byte units under a utf16le encoding and
* bytes under any other, an unknown name included. */
bool ucs2 = raw && scr_bytes_dyn_is_ucs2(raw);
if (raw) scr_str_release(raw);
found = scr_bytes_index_of(b, value->v.bytes, at, ucs2 ? 2 : 1, fwd);
} else {
if (raw) scr_str_release(raw);
scr_dyn_arg_type_fail("value", "one of type number or string or an instance of Buffer or Uint8Array", value);
return NULL;
}
+122
View File
@@ -143,3 +143,125 @@ console.log(box.text.indexOf("an"), box.text.lastIndexOf("an"), box.text.include
console.log(box.list.indexOf(2), box.list.includes(4), box.list.fill(0, 1).join(","), box.list.toString());
console.log(Array.from(box.bytes.fill(9, 2)), box.bytes.toString());
console.log(box.sink.write("x"), box.sink.written, box.sink.toString(1, 2));
// fill with other ArrayBufferViews repeats their raw bytes; an empty view is
// ERR_INVALID_ARG_VALUE with Node's inspect of the view.
const tm = (name, f) => {
try {
const v = f();
console.log(name, Buffer.isBuffer(v) ? `<${v.toString("hex")}>` : JSON.stringify(v));
} catch (e) {
console.log(name, e.name, e.code, JSON.stringify(e.message));
}
};
function Held(bytes) {
this.b = bytes;
}
const held = (bytes) => new Held(bytes).b;
const filled = (n) => held(Buffer.alloc(n));
tm("fill Uint16Array", () => filled(6).fill(new Uint16Array([0x0102])));
tm("fill DataView", () => filled(6).fill(new DataView(new Uint8Array([7, 8, 9]).buffer)));
tm("fill DataView window", () => filled(6).fill(new DataView(new Uint8Array([1, 2, 3, 4]).buffer, 1, 2)));
tm("fill Float64Array", () => filled(10).fill(new Float64Array([1])));
tm("fill subarray", () => filled(6).fill(new Uint16Array([1, 2, 3]).subarray(1)));
tm("fill Int8Array range", () => filled(6).fill(new Int8Array([-1, 2]), 1, 4));
tm("fill empty Uint16Array", () => filled(6).fill(new Uint16Array(0)));
tm("fill empty Uint8Array", () => filled(6).fill(new Uint8Array(0)));
for (const [size, offset] of [[0, 0], [1, 1], [6, 6], [7, 7], [8, 8], [120, 3], [101, 101]])
tm(`fill empty DataView ${size}/${offset}`, () => filled(6).fill(new DataView(new ArrayBuffer(size), offset, 0)));
tm("fill empty DataView over bytes", () => filled(6).fill(new DataView(new Uint8Array(200).fill(171).buffer, 7, 0)));
// Buffer needles under utf16le search whole 2-byte units.
const units = held(Buffer.from("abcabc", "utf16le"));
const split = held(Buffer.from([0x61, 0x00, 0x62, 0x00, 0x00, 0x62, 0x00, 0x63]));
const N = (bytes) => Buffer.from(bytes);
tm("indexOf utf16le needle", () => [split.indexOf(N([0x62, 0x00]), 0, "ucs2"), split.indexOf(N([0x62, 0x00]))]);
tm("indexOf utf16le odd position", () => [
split.indexOf(N([0x00, 0x62]), 0, "ucs2"),
split.indexOf(N([0x00, 0x62]), 0, "UTF-16LE"),
split.indexOf(N([0x00, 0x62])),
]);
tm("lastIndexOf utf16le needle", () => [
units.lastIndexOf(N([0x62, 0x00]), undefined, "ucs2"),
split.lastIndexOf(N([0x00, 0x62]), undefined, "ucs2"),
...[5, 3, 2, 1, -100, 100].map((at) => split.lastIndexOf(N([0x62, 0x00]), at, "ucs2")),
]);
tm("indexOf utf16le offsets", () => [1, 2, 3, -5, 7, 2.9].map((at) => split.indexOf(N([0x62, 0x00]), at, "ucs2")));
tm("indexOf utf16le short needles", () => [
split.indexOf(N([0x62]), 0, "ucs2"),
held(N([0x62])).indexOf(N([0x62]), 0, "ucs2"),
split.indexOf(Buffer.alloc(0), 3, "ucs2"),
split.indexOf(Buffer.alloc(0), 30, "ucs2"),
split.indexOf(N([0x62, 0, 0]), 0, "ucs2"),
]);
tm("indexOf utf16le odd lengths", () => [
held(N([1, 0, 2, 0, 3])).indexOf(N([2, 0, 3]), 0, "ucs2"),
held(N([1, 0, 2, 0, 3, 0])).indexOf(N([2, 0, 3]), 0, "ucs2"),
held(N([1, 2, 1, 2, 1, 2])).indexOf(N([2, 1, 2]), 0, "ucs2"),
held(N([1, 0, 2, 0, 3])).lastIndexOf(N([3]), undefined, "ucs2"),
held(N([0, 2, 0, 2, 0])).lastIndexOf(N([2, 0]), undefined, "ucs2"),
]);
tm("indexOf buffer needle other encodings", () => [
split.indexOf(N([0x62, 0x00]), 0, "nope"),
split.indexOf(N([0x00, 0x62]), 0, "nope"),
split.indexOf(N([0x00, 0x62]), 0, "latin1"),
]);
tm("indexOf utf16le strings", () => [
split.indexOf("b", 0, "ucs2"),
units.lastIndexOf("b", undefined, "ucs2"),
units.indexOf("b", 3, "ucs2"),
units.lastIndexOf("b", 3, "ucs2"),
units.indexOf("", 3, "ucs2"),
units.indexOf("", 30, "ucs2"),
units.lastIndexOf("", 30, "ucs2"),
split.indexOf("戀", 0, "ucs2"),
]);
tm("includes utf16le needle", () => [split.includes(N([0x00, 0x62]), 0, "ucs2"), split.includes(N([0x62, 0x00]), 3, "ucs2")]);
// toString windows with infinite and out-of-range bounds.
const word = held(Buffer.from("hello"));
tm("toString infinite bounds", () => [
word.toString("utf8", 1, Infinity),
word.toString("utf8", -Infinity, 2),
word.toString("utf8", Infinity),
word.toString("utf8", NaN, 3),
word.toString("utf8", 1e300, 2),
word.toString("utf8", 1, 1e300),
word.toString("utf8", -1e300, 1e300),
word.toString("utf8", 0, -Infinity),
word.toString("utf8", 2, NaN),
word.toString("utf8", 0.5, 4.5),
word.toString("hex", -0.5, -0),
word.toString("latin1", 4.9),
word.toString(undefined, undefined, Infinity),
]);
// The same searches and windows on Buffers the compiler types statically.
const typed = Buffer.from([0x61, 0x00, 0x62, 0x00, 0x00, 0x62, 0x00, 0x63]);
const typedUnits = Buffer.from("abcabc", "utf16le");
console.log(
"typed utf16le",
typed.indexOf(Buffer.from([0x00, 0x62]), 0, "ucs2"),
Buffer.from([0x62]).indexOf(Buffer.from([0x62]), 0, "ucs2"),
typed.lastIndexOf(Buffer.from([0x62, 0x00]), 5, "ucs2"),
typed.indexOf(Buffer.from([0x62, 0x00]), 3, "ucs2"),
typed.includes(Buffer.from([0x00, 0x62]), 0, "utf16le"),
typed.indexOf("b", 0, "ucs2"),
typedUnits.lastIndexOf("b", 100, "ucs2"),
typedUnits.lastIndexOf("b", 3, "ucs2"),
typedUnits.indexOf("", 30, "ucs2"),
typedUnits.indexOf("b", 1e300, "ucs2"),
typedUnits.indexOf("c", -1e300, "ucs2"),
);
const typedWord = Buffer.from("hello");
console.log(
"typed windows",
JSON.stringify([
typedWord.toString("utf8", 1, Infinity),
typedWord.toString("utf8", -Infinity, 2),
typedWord.toString("utf8", Infinity),
typedWord.toString("utf8", NaN, 3),
typedWord.toString("utf8", 1, 1e300),
typedWord.toString("hex", 0, -Infinity),
]),
);
@@ -1 +1 @@
{ "plain": 7800, "san": 39000 }
{ "plain": 12000, "san": 39000 }