Test the Buffer brand at run time and alias the buffer module's Buffer

`Buffer.isBuffer` over a Uint8Array-typed value (safe-buffer's
`Buffer.from(...)` result, etag's input) refused at compile time because
the bytes IR shares storage between Buffer and Uint8Array. The view keeps
its Buffer brand at run time, so the test now reads it.

`require('buffer').Buffer`, `const { Buffer } = require('buffer')` and
`buffer.Buffer` through a namespace binding alias the global Buffer (the
module member is the same constructor), so their statics lower like the
global's. A file-scope `var` of the namespace form aliases too when nothing
reassigns it and the require-binding early-reference analysis finds no
earlier reader. `Buffer.from` over a JS-inferred `string | Uint8Array |
undefined` value takes the checked runtime form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-18 15:55:18 -07:00
co-authored by Claude Opus 5.5
parent 8edd2bce8e
commit 9e63a937db
12 changed files with 159 additions and 4 deletions
@@ -406,6 +406,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"buffer.fromStr": "scr_bytes_from_str",
"buffer.fromStrChecked": "scr_bytes_from_str_checked",
"buffer.brand": "scr_bytes_as_buffer",
"buffer.isBranded": "scr_bytes_is_buffer",
"buffer.fromDyn": "scr_buffer_from_dyn",
"buffer.concat": "scr_buffer_concat",
"buffer.concatLen": "scr_buffer_concat_len",
@@ -20,6 +20,7 @@ import {
arrayOf,
bytesOf,
funcOf,
isUnitType,
typeEquals,
} from "../../../ir/ir.js";
import { isJsSourceFile, locOf } from "../../program.js";
@@ -1408,7 +1409,14 @@ function lowerBufferStaticValue(
}
return { kind: "libCall", fn, args: [s, enc], type: BYTES_U8, loc };
}
if (args.length === 1 && srcIr?.kind === "bytes" && srcIr.elem === "u8") {
// A checker type with no mapping (pako's conditional `O extends
// { toText: true } ? string : Uint8Array` result) whose lowered
// value is a u8 view copies like the typed form.
if (
args.length === 1 &&
((srcIr?.kind === "bytes" && srcIr.elem === "u8") ||
(srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8"))
) {
return {
kind: "libCall",
fn: "buffer.poolFromBytes",
@@ -1426,6 +1434,34 @@ function lowerBufferStaticValue(
loc,
};
}
// A union of strings, u8 views and undefined/null (a JS-inferred
// npm result such as pako's deflate): the checked form dispatches
// on the runtime kind exactly as for an untyped JS value, Node's
// TypeError for the unit arms included.
if (
args.length === 1 &&
value.type.kind === "union" &&
lowerer.dynConvertible(value.type) &&
lowerer.unions
.get(value.type.unionId)
?.arms.every(
(arm) =>
arm.kind === "string" ||
(arm.kind === "bytes" && arm.elem === "u8") ||
isUnitType(arm),
) === true
) {
return {
kind: "libCall",
fn: "buffer.poolFromDyn",
args: [
lowerer.coerceToExpected(value, DYN),
{ kind: "strLit", value: "utf8", type: STRING, loc },
],
type: BYTES_U8,
loc,
};
}
}
}
lowerer.noLowering(
@@ -1626,6 +1662,12 @@ function lowerBufferStaticValue(
};
}
}
// A typed view of unknown provenance (a Uint8Array parameter,
// safe-buffer's Buffer.from result): Buffer is a brand of the view,
// which the runtime keeps, so the test reads it.
if (v.type.kind === "bytes") {
return { kind: "libCall", fn: "buffer.isBranded", args: [v], type: BOOL, loc };
}
lowerer.noLowering(
`Buffer.isBuffer of '${lowerer.fmt(v.type)}' values`,
args[0]!,
@@ -102,6 +102,7 @@ import {
isVarDeclared,
numericIteratorSourceOf,
provenanceElidedConstDecl,
stdlibBufferVarAlias,
} from "./lower-stmts.js";
import { objectFactoryGlobalType } from "./object-factory-new.js";
import { streamClassAliasDecl } from "./lower-stream.js";
@@ -1805,6 +1806,7 @@ export function collectGlobals(
// same test).
const stableStdlibAlias =
isConst ||
stdlibBufferVarAlias(lowerer, decl) ||
((list.flags & ts.NodeFlags.Let) !== 0 &&
ts.isIdentifier(decl.name) &&
stdlibGlobalAliasNameOf(lowerer, decl.initializer) !== null &&
@@ -67,6 +67,7 @@ import {
isJsSourceFile,
isNodeEsmFile,
locOf,
requireEarlyReference,
requireSpecOf,
} from "../program.js";
import {
@@ -78,6 +79,7 @@ import {
sideEffectFreeOptionValue,
stdlibGlobalAliasDecl,
stdlibGlobalAliasNameOf,
bufferNamespaceMember,
stdlibGlobalNameOf,
} from "./surfaces.js";
import { isNativeBuiltinValueInitializer } from "./lower-builtin-values.js";
@@ -4385,6 +4387,7 @@ export function lowerVarDecl(
// alias plumbing (see stdlibGlobalAliasDecl), no storage, no code.
const stableStdlibAlias =
!isLet ||
stdlibBufferVarAlias(lowerer, decl) ||
(ts.isVariableDeclarationList(decl.parent) &&
(decl.parent.flags & ts.NodeFlags.Let) !== 0 &&
ts.isIdentifier(decl.name) &&
@@ -10521,3 +10524,25 @@ export function lowerForStatement(lowerer: Lowerer, stmt: ts.ForStatement): IrSt
lowerer.scopes.pop();
}
}
/** safe-buffer's `var Buffer = buffer.Buffer` at file scope: a `var` alias
* of the global Buffer is the same alias plumbing as the const spelling
* when nothing reassigns it and no code that runs before the declaration
* can read it (the require-binding early-reference analysis), so the
* hoisted undefined is unobservable. */
export function stdlibBufferVarAlias(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
if (
!ts.isIdentifier(decl.name) ||
!ts.isVariableDeclarationList(decl.parent) ||
(decl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0 ||
decl.initializer === undefined ||
!bufferNamespaceMember(lowerer, decl.initializer)
)
return false;
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
return (
symbol !== undefined &&
bindingNeverReassigned(lowerer, symbol, decl) &&
requireEarlyReference(lowerer.program, decl) === null
);
}
@@ -22,7 +22,7 @@ import {
VOID,
arrayOf,
} from "../../ir/ir.js";
import { isNodeTypesPath, requireSpecOf } from "../program.js";
import { isJsSourceFile, isNodeTypesPath, requireSpecOf } from "../program.js";
import { ZLIB_CONSTANTS } from "./zlib-constants.js";
/** zlib stream instances: runtime Transform streams (scr_zlib_stream.c). */
@@ -3243,7 +3243,21 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin
// `Buffer` resolves to a user-file declaration and still misses.
const resolved =
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
if (!lowerer.isStdlibSymbol(resolved)) return null;
if (!lowerer.isStdlibSymbol(resolved)) {
// A CommonJS binding of the buffer module's Buffer (`var Buffer =
// require('buffer').Buffer`, `const { Buffer } = require('buffer')`
// — safe-buffer and compression): the module member IS the global
// constructor.
if (
decl !== undefined &&
(ts.isVariableDeclaration(decl) || ts.isBindingElement(decl)) &&
isJsSourceFile(decl.getSourceFile())
) {
const imported = lowerer.builtinImportOf(expr);
if (imported?.module === "buffer" && imported.member === "Buffer") return "Buffer";
}
return null;
}
return resolved.name === "global" ? "globalThis" : resolved.name;
}
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
@@ -3318,7 +3332,9 @@ export function stdlibGlobalAliasNameOf(
const name =
requireSpec === "process" || requireSpec === "node:process"
? "process"
: stdlibGlobalNameOf(lowerer, aliasExpr);
: bufferNamespaceMember(lowerer, aliasExpr)
? "Buffer"
: stdlibGlobalNameOf(lowerer, aliasExpr);
if (name === null) return null;
// These globals have provenance-aware call or member lowering, so a
// stable snapshot keeps the same dispatch as the original global.
@@ -3339,6 +3355,14 @@ export function stdlibGlobalAliasNameOf(
: null;
}
/** `buffer.Buffer` through a buffer-module namespace binding (`var buffer
* = require('buffer')`): the module's Buffer IS the global constructor. */
export function bufferNamespaceMember(lowerer: Lowerer, expr: ts.Expression): boolean {
if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== "Buffer") return false;
const member = lowerer.builtinMemberOf(expr);
return member?.module === "buffer" && member.member === "Buffer";
}
export function stdlibGlobalAliasDecl(
lowerer: Lowerer,
nameNode: ts.Node,
@@ -2634,6 +2634,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"buffer.fromStr": { argTypes: [STRING, STRING], result: BYTES_U8 },
"buffer.fromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 },
"buffer.brand": { argTypes: [BYTES_U8], result: BYTES_U8 },
/** Buffer.isBuffer over a typed view: whether the view carries the
* Buffer brand (any element kind; only Buffers do). Never throws. */
"buffer.isBranded": { argTypes: [null], result: BOOL },
/** Buffer.from on checked-native strings, bytes, arrays and data-only
* array-like/Buffer-JSON objects. The encoding is a normalized literal;
* non-string inputs ignore it. Copies the input and may throw during
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6206-buffer-is-buffer-views.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6209-buffer-module-alias.cjs"
],
"diags": []
}
+3
View File
@@ -293,6 +293,9 @@ ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes) {
return scr_bytes_retain(bytes);
}
/* Nothing on an existing path calls this. */
bool scr_bytes_is_buffer(const ScrBytes *bytes) { return bytes->is_buffer; }
void scr_bytes_release(ScrBytes *b) {
if (!b || b->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */
if (--b->rc == 0) {
+2
View File
@@ -159,6 +159,8 @@ ScrBytes *scr_bytes_new(ScrBytesElem elem, double n); /* +1 */
/* Shared u8 view over exactly the source view's raw bytes. Borrows; +1. */
ScrBytes *scr_bytes_raw_view(ScrBytes *bytes);
ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes); /* borrows; +1, Buffer factory only */
/* Buffer.isBuffer over a typed view: the view's Buffer brand. Borrows. */
bool scr_bytes_is_buffer(const ScrBytes *bytes);
/* Views retain the root allocation; offsets are relative to its complete
* byte storage, including when src is itself a subarray. */
ScrBytes *scr_bytes_buffer_view(ScrBytes *src, ScrBytesElem elem,
@@ -0,0 +1,16 @@
// Buffer.isBuffer over Uint8Array-typed values reads the view's Buffer
// brand: Buffer factories, Buffer subarrays and slices are Buffers, plain
// typed arrays and copies into them are not.
function check(view: Uint8Array): boolean {
return Buffer.isBuffer(view);
}
const plain = new Uint8Array([1, 2, 3]);
const buffer = Buffer.from(plain);
console.log(check(plain), check(buffer), check(Buffer.alloc(2)), check(Buffer.from("hey").subarray(1)));
console.log(check(plain.subarray(1)), check(new Uint8Array(buffer)), check(buffer.slice(1)), check(Buffer.concat([plain])));
const views: Uint8Array[] = [plain, buffer, new TextEncoder().encode("x"), Buffer.allocUnsafe(1)];
console.log(views.map((v) => Buffer.isBuffer(v)).join(","));
function words(view: Uint32Array): boolean {
return Buffer.isBuffer(view);
}
console.log(words(new Uint32Array(2)), Buffer.from(plain).toString("hex"), Buffer.from(views[2]!).toString());
+25
View File
@@ -0,0 +1,25 @@
// The buffer module's Buffer through CommonJS bindings — `var Buffer =
// require('buffer').Buffer` and a member of a stored require — is the
// global Buffer constructor, with its statics.
var Buffer = require("buffer").Buffer;
var buffer = require("buffer");
var B2 = buffer.Buffer;
const B3 = require("node:buffer").Buffer;
console.log(Buffer.from("hi").toString("hex"), Buffer.isBuffer(Buffer.alloc(1)), B2.byteLength("héllo"), B2.isBuffer("x"));
console.log(B3.concat([B2.from("a"), Buffer.from([98])]).toString(), B3.alloc(2, 1).toString("base64"));
function toBuffer(chunk) {
return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
}
console.log(toBuffer("abc").length, toBuffer(Buffer.from("xyz")).toString());
// Buffer.from over a JS-inferred `string | Uint8Array | undefined` result
// dispatches on the runtime kind, Node's TypeError for undefined included.
function make(flag) {
return flag ? "ab" : flag === false ? new Uint8Array([99, 100]) : undefined;
}
console.log(Buffer.from(make(true)).toString(), Buffer.from(make(false)).toString("hex"));
try {
Buffer.from(make(null));
} catch (e) {
console.log(e.name, e.code);
}