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
2 changes: 2 additions & 0 deletions packages/compiler/src/backend/llvm/lib-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"buffer.fromDyn": "scr_buffer_from_dyn",
"buffer.concat": "scr_buffer_concat",
"buffer.concatLen": "scr_buffer_concat_len",
"buffer.concatDyn": "scr_buffer_concat_dyn",
"buffer.concatLenDyn": "scr_buffer_concat_len_dyn",
"buffer.alloc": "scr_buffer_alloc",
"buffer.allocUnsafe": "scr_buffer_alloc_unsafe",
"buffer.allocUnsafeSlow": "scr_buffer_alloc_unsafe_slow",
Expand Down
22 changes: 17 additions & 5 deletions packages/compiler/src/frontend/lowering/containers/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1511,11 +1511,23 @@ function lowerBufferStaticValue(
} else {
list = lowerer.lowerExpr(argNode);
if (!typeEquals(list.type, arrayOf(BYTES_U8))) {
lowerer.noLowering(
`Buffer.concat of '${lowerer.fmt(list.type)}' values`,
argNode,
"one Uint8Array[]/Buffer[] value (or literal) is the lowered list shape",
);
// Any other list (JavaScript's untyped `const chunks = []`, or a
// union of arrays): the runtime checks it as Node does (an Array of
// Uint8Arrays, the ERR_INVALID_ARG_TYPE errors otherwise) before
// concatenating.
const value: IrExpr =
list.type.kind === "dyn" ? list : { kind: "dynFrom", value: list, type: DYN, loc };
if (args.length === 2) {
const total = lowerer.lowerExprExpecting(args[1]!, F64);
return {
kind: "libCall",
fn: "buffer.concatLenDyn",
args: [value, total],
type: BYTES_U8,
loc,
};
}
return { kind: "libCall", fn: "buffer.concatDyn", args: [value], type: BYTES_U8, loc };
}
}
if (args.length === 2) {
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/ir/builtin-effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"crypto.scryptCb",
"buffer.concat",
"buffer.concatLen",
"buffer.concatDyn",
"buffer.concatLenDyn",
"buffer.fromDyn",
"buffer.fromStrChecked",
"buffer.alloc",
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/ir/builtin-signatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2624,6 +2624,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* opaque reference inputs retain explicit runtime refusals. */
"buffer.fromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 },
"buffer.concat": { argTypes: [arrayOf(BYTES_U8)], result: BYTES_U8 },
"buffer.concatDyn": { argTypes: [DYN], result: BYTES_U8 },
/** The pooled Buffer factories (Node's allocate/fromStringFast/
* fromArrayLike): allocUnsafe, Buffer.from of strings, Uint8Arrays,
* number[] and checked values, and concat return views into the
Expand All @@ -2650,6 +2651,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* zero-padded to the total. THROWS Node's 'length' RangeError on a
* negative/non-integer total (may-throw seed). */
"buffer.concatLen": { argTypes: [arrayOf(BYTES_U8), F64], result: BYTES_U8 },
"buffer.concatLenDyn": { argTypes: [DYN, F64], result: BYTES_U8 },
// The checked-dynamic compare/equals validators (Node's argument
// ladders over dyn-boxed invalid-input probes).
/** The checked-dynamic compare/equals validators (scr_bytes_io.c) —
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5973-buffer-concat-untyped.js"
],
"diags": []
}
4 changes: 4 additions & 0 deletions packages/runtime/src/scr_bytes.h
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,10 @@ ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */
ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding);
ScrBytes *scr_buffer_concat(const ScrArr *list);
ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total);
/* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list
* and each element, then the typed concat. NULL with the exception pending. */
ScrBytes *scr_buffer_concat_dyn(const struct ScrDyn *list);
ScrBytes *scr_buffer_concat_len_dyn(const struct ScrDyn *list, double total);
/* Drops the pool's slab reference and restores Buffer.poolSize (process
* exit audits and library session resets). */
void scr_buffer_pool_reset(void);
Expand Down
50 changes: 50 additions & 0 deletions packages/runtime/src/scr_bytes_io.c
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,56 @@ static ScrBytes *scr_buffer_from_array_like(const ScrDyn *value, double length)
return out;
}

/* Buffer.concat over an untyped list: Node's checks in Node's order (an
* Array, then the total of the two-argument form unless the list is empty,
* then every element a Uint8Array, ERR_INVALID_ARG_TYPE otherwise), then the
* typed concat over the elements. NULL with the exception pending. */
static ScrBytes *scr_buffer_concat_dyn_impl(const ScrDyn *list, double total, bool has_total) {
ScrDyn *materialized = NULL;
if (list->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(list) != NULL) {
materialized = scr_dyn_typed_ref_materialize(list);
list = materialized;
}
if (list->kind != SCR_DYN_ARR) {
scr_dyn_arg_type_fail("list", "an instance of Array", list);
scr_dyn_release(materialized);
return NULL;
}
size_t n = list->v.arr.len;
if (has_total && n > 0 && !scr_bytes_validate_off("length", total, 9007199254740991.0)) {
scr_dyn_release(materialized);
return NULL;
}
ScrArr *parts = scr_arr_new(SCR_ELEM_BYTES, (double)n);
for (size_t i = 0; i < n; i++) {
ScrDyn *item = scr_dyn_index_get(list, (double)i);
const ScrDyn *shown = item != NULL ? item : scr_dyn_undefined();
if (!scr_dyn_bytes_is(shown, SCR_BYTES_U8) || shown->v.bytes->is_data_view) {
char name[40];
snprintf(name, sizeof name, "list[%zu]", i);
scr_dyn_arg_type_fail(name, "an instance of Buffer or Uint8Array", shown);
scr_dyn_release(item);
scr_arr_release(parts);
scr_dyn_release(materialized);
return NULL;
}
scr_arr_push_ref(parts, scr_bytes_retain(shown->v.bytes));
scr_dyn_release(item);
}
Comment thread
vercel[bot] marked this conversation as resolved.
ScrBytes *out = has_total ? scr_buffer_concat_len(parts, total) : scr_buffer_concat(parts);
scr_arr_release(parts);
scr_dyn_release(materialized);
return out;
}

ScrBytes *scr_buffer_concat_dyn(const ScrDyn *list) {
return scr_buffer_concat_dyn_impl(list, 0, false);
}

ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) {
return scr_buffer_concat_dyn_impl(list, total, true);
}

ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) {
if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding);
if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes);
Expand Down
38 changes: 38 additions & 0 deletions tests/corpus/5973-buffer-concat-untyped.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Buffer.concat over an untyped list (JavaScript's `const chunks = []`):
// Uint8Array elements concatenate, the length form pads or truncates, and
// a non-array list or a non-Uint8Array element throws Node's TypeError. The
// length form checks the length before the elements, and an empty list
// returns before either.
const chunks = [];
chunks.push(Buffer.from("ab"));
chunks.push(new Uint8Array([99, 100]));
console.log(Buffer.concat(chunks).toString());
console.log(Buffer.concat(chunks, 6));
console.log(Buffer.concat(chunks, 3).toString());
console.log(Buffer.concat([]).length);

const lists = [[1], ["a"], [Buffer.from("x"), null], "x", 5, [new Uint16Array(1)]];
for (const list of lists) {
try {
Buffer.concat(list);
console.log("no error");
} catch (e) {
console.log(e.name, e.code, e.message);
}
}

const withLength = [
[[1], -1],
[[Buffer.from("a"), "b"], NaN],
[[1], 2],
[[], -1],
[[Buffer.from("a")], 1.5],
[[Buffer.from("a"), Buffer.from("b")], 2],
];
for (const [list, length] of withLength) {
try {
console.log("length", Buffer.concat(list, length).length);
} catch (e) {
console.log(e.name, e.code, e.message);
}
}
4 changes: 4 additions & 0 deletions tests/harness/corpus-costs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2160,6 +2160,7 @@
"5940-callback-helper-specialization.ts": 3109,
"5941-callback-specialization-recursion.ts": 3112,
"5942-callback-specialization-throws.ts": 3221,
"5942-local-class-observed-methods.ts": 5063,
"5943-fields-read-before-assignment.ts": 6856,
"5944-js-fields-read-before-assignment.js": 4247,
"5945-local-class-observed-methods.ts": 5063,
Expand All @@ -2179,6 +2180,7 @@
"5970-net-connect-host-name.ts": 2400,
"5971-buffer-chunks-to-string.ts": 2700,
"5972-net-set-encoding-listeners.ts": 2550,
"5973-buffer-concat-untyped.js": 1900,
"5973-large-closure-environment.ts": 4499,
"5974-typeof-module-exports.cjs": 2000,
"5975-zlib-class-values.ts": 2800,
Expand Down Expand Up @@ -5182,6 +5184,7 @@
"5940-callback-helper-specialization.ts": 4548,
"5941-callback-specialization-recursion.ts": 4448,
"5942-callback-specialization-throws.ts": 3443,
"5942-local-class-observed-methods.ts": 6212,
"5943-fields-read-before-assignment.ts": 72293,
"5944-js-fields-read-before-assignment.js": 40310,
"5945-local-class-observed-methods.ts": 6212,
Expand All @@ -5201,6 +5204,7 @@
"5970-net-connect-host-name.ts": 2800,
"5971-buffer-chunks-to-string.ts": 3100,
"5972-net-set-encoding-listeners.ts": 3000,
"5973-buffer-concat-untyped.js": 2900,
"5973-large-closure-environment.ts": 5145,
"5974-typeof-module-exports.cjs": 2500,
"5975-zlib-class-values.ts": 4200,
Expand Down
1 change: 0 additions & 1 deletion tests/harness/npm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ const KNOWN: Record<string, Known> = {
"web-streams": { build: "SC2013" },
"workspace-copied": { build: "SC2013" },
"workspace-unclean": { build: "SC2013" },
"zlib-shims": { run: "SC2020" },
};

const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name);
Expand Down
Loading