Skip to content
Open
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
35 changes: 24 additions & 11 deletions src/js/node/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const parseEnv = $newRustFunction("node_util_binding.rs", "parseEnv", 1);
const NumberIsSafeInteger = Number.isSafeInteger;
const ObjectKeys = Object.keys;
const ObjectGetOwnPropertyNames = Object.getOwnPropertyNames;
var Error = globalThis.Error;
const ErrorCaptureStackTrace = Error.captureStackTrace;
const { uncurryThis, SafeMap } = require("internal/primordials");
const RegExpPrototypeExec = uncurryThis(RegExp.prototype.exec);

Expand Down Expand Up @@ -245,21 +247,25 @@ function getHexStyleCache() {
return hexStyleCache;
}

function buildStyleEntry(codes) {
const openNum = codes[0];
const closeNum = codes[1];
return {
__proto__: null,
openSeq: kEscape + openNum + kEscapeEnd,
closeSeq: kEscape + closeNum + kEscapeEnd,
keepClose: openNum === kDimCode || openNum === kBoldCode,
};
}

function getStyleCache() {
if (styleCache === undefined) {
styleCache = { __proto__: null };
const colors = inspect.colors;
for (const key of ObjectGetOwnPropertyNames(colors)) {
const codes = colors[key];
if (codes) {
const openNum = codes[0];
const closeNum = codes[1];
styleCache[key] = {
__proto__: null,
openSeq: kEscape + openNum + kEscapeEnd,
closeSeq: kEscape + closeNum + kEscapeEnd,
keepClose: openNum === kDimCode || openNum === kBoldCode,
};
styleCache[key] = buildStyleEntry(codes);
}
}
}
Expand Down Expand Up @@ -388,9 +394,16 @@ function styleText(format, text, options) {
continue;
}

const style = cache[key];
let style = cache[key];
if (style === undefined) {
validateOneOf(key, "format", ObjectGetOwnPropertyNames(inspect.colors));
// inspect.colors is user-mutable; a key added after the cache was
// populated is looked up live and cached on first use.
const codes = inspect.colors[key];
if (codes == null) {
validateOneOf(key, "format", ObjectGetOwnPropertyNames(inspect.colors));
}
style = buildStyleEntry(codes);
Comment thread
robobun marked this conversation as resolved.
cache[key] = style;
}
openCodes += style.openSeq;
closeCodes = style.closeSeq + closeCodes;
Expand Down Expand Up @@ -467,7 +480,7 @@ function getCallSites(frameCount = 10, options) {
try {
Error.stackTraceLimit = frameCount;
} catch {}
Error.captureStackTrace(target, getCallSites);
ErrorCaptureStackTrace(target, getCallSites);
return target.stack;
} finally {
Error.prepareStackTrace = savedPrepareStackTrace;
Expand Down
49 changes: 48 additions & 1 deletion test/js/node/util/util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

import assert from "assert";
import { describe, expect, it } from "bun:test";
import "harness";
import { bunEnv, bunExe } from "harness";
import util from "util";
// const context = require('vm').runInNewContext; // TODO: Use a vm polyfill

Expand Down Expand Up @@ -432,6 +432,32 @@ describe("util", () => {
assert.strictEqual(util.styleText("red", "test", { validateStream: false }), "\u001b[31mtest\u001b[39m");
});

// inspect.colors is user-mutable; styleText must see keys added after its
// internal cache was first populated instead of crashing on the stale cache.
it.concurrent("styleText accepts inspect.colors entries added after first call", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const util = require("node:util");
util.styleText("red", "x", { validateStream: false });
util.inspect.colors.myColor = [95, 39];
const single = util.styleText("myColor", "x", { validateStream: false });
const array = util.styleText(["bold", "myColor"], "x", { validateStream: false });
process.stdout.write(JSON.stringify({ single, array }));`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({
single: "\u001b[95mx\u001b[39m",
array: "\u001b[1m\u001b[95mx\u001b[39m\u001b[22m",
});
expect(exitCode).toBe(0);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

describe("getCallSites", () => {
it("restores Error state when stackTraceLimit is non-writable", () => {
const desc = Object.getOwnPropertyDescriptor(Error, "stackTraceLimit");
Expand All @@ -448,6 +474,27 @@ describe("util", () => {
}
});

it.concurrent("is unaffected by user tampering with the Error global", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const util = require("node:util");
delete Error.captureStackTrace;
globalThis.Error = undefined;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
function outer() { return util.getCallSites(5); }
const sites = outer();
process.stdout.write(JSON.stringify(sites.map(s => s.functionName)));`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toContain("outer");
expect(exitCode).toBe(0);
});

it("each frame has the node v26 shape", () => {
const sites = util.getCallSites(3);
expect(sites.length).toBeGreaterThan(0);
Expand Down
Loading