mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
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:
co-authored by
Claude Opus 5.5
parent
8edd2bce8e
commit
9e63a937db
@@ -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
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6206-buffer-is-buffer-views.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6209-buffer-module-alias.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user