Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: harden util.inspect #21869

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
204 changes: 135 additions & 69 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const types = internalBinding('types');
Object.assign(types, require('internal/util/types'));
const {
isAnyArrayBuffer,
isArrayBuffer,
isArgumentsObject,
isDataView,
isExternal,
Expand All @@ -55,7 +56,23 @@ const {
isWeakSet,
isRegExp,
isDate,
isTypedArray
isTypedArray,
isStringObject,
isNumberObject,
isBooleanObject,
isSymbolObject,
isBigIntObject,
isUint8Array,
isUint8ClampedArray,
isUint16Array,
isUint32Array,
isInt8Array,
isInt16Array,
isInt32Array,
isFloat32Array,
isFloat64Array,
isBigInt64Array,
isBigUint64Array
} = types;

const {
Expand Down Expand Up @@ -84,6 +101,16 @@ const regExpToString = RegExp.prototype.toString;
const dateToISOString = Date.prototype.toISOString;
const errorToString = Error.prototype.toString;

const bigIntValueOf = BigInt.prototype.valueOf;
const booleanValueOf = Boolean.prototype.valueOf;
const numberValueOf = Number.prototype.valueOf;
const symbolValueOf = Symbol.prototype.valueOf;
const stringValueOf = String.prototype.valueOf;

const setValues = Set.prototype.values;
const mapEntries = Map.prototype.entries;
const dateGetTime = Date.prototype.getTime;

let CIRCULAR_ERROR_MESSAGE;
let internalDeepEqual;

Expand Down Expand Up @@ -455,6 +482,36 @@ function getPrefix(constructor, tag) {
return '';
}

function addExtraKeys(source, target, keys) {
for (const key of keys) {
target[key] = source[key];
}
return target;
}

function findTypedConstructor(value) {
for (const [check, clazz] of [
[isUint8Array, Uint8Array],
[isUint8ClampedArray, Uint8ClampedArray],
[isUint16Array, Uint16Array],
[isUint32Array, Uint32Array],
[isInt8Array, Int8Array],
[isInt16Array, Int16Array],
[isInt32Array, Int32Array],
[isFloat32Array, Float32Array],
[isFloat64Array, Float64Array],
[isBigInt64Array, BigInt64Array],
[isBigUint64Array, BigUint64Array]
]) {
if (check(value)) {
return new clazz(value);
}
}
return value;
}

const getBoxedValue = formatPrimitive.bind(null, stylizeNoColor);

function formatValue(ctx, value, recurseTimes) {
// Primitive types cannot have properties
if (typeof value !== 'object' && typeof value !== 'function') {
Expand Down Expand Up @@ -548,8 +605,8 @@ function formatValue(ctx, value, recurseTimes) {
let formatter = formatObject;
let braces;
let noIterator = true;
let raw;
let extra;
let i = 0;

// Iterators and the rest are split to reduce checks
if (value[Symbol.iterator]) {
Expand Down Expand Up @@ -583,34 +640,16 @@ function formatValue(ctx, value, recurseTimes) {
braces = [`[${tag}] {`, '}'];
formatter = formatSetIterator;
} else {
// Check for boxed strings with valueOf()
// The .valueOf() call can fail for a multitude of reasons
try {
raw = value.valueOf();
} catch (e) { /* ignore */ }

if (typeof raw === 'string') {
const formatted = formatPrimitive(stylizeNoColor, raw, ctx);
if (keyLength === raw.length)
return ctx.stylize(`[String: ${formatted}]`, 'string');
base = `[String: ${formatted}]`;
// For boxed Strings, we have to remove the 0-n indexed entries,
// since they just noisy up the output and are redundant
// Make boxed primitive Strings look like such
keys = keys.slice(value.length);
braces = ['{', '}'];
} else {
noIterator = true;
}
noIterator = true;
}
}
if (noIterator) {
braces = ['{', '}'];
if (constructor === 'Object') {
if (isArgumentsObject(value)) {
braces[0] = '[Arguments] {';
if (keyLength === 0)
return '[Arguments] {}';
braces[0] = '[Arguments] {';
} else if (tag !== '') {
braces[0] = `${getPrefix(constructor, tag)}{`;
if (keyLength === 0) {
Expand All @@ -620,8 +659,8 @@ function formatValue(ctx, value, recurseTimes) {
return '{}';
}
} else if (typeof value === 'function') {
const name =
`${constructor || tag}${value.name ? `: ${value.name}` : ''}`;
const type = constructor || tag || 'Function';
const name = `${type}${value.name ? `: ${value.name}` : ''}`;
if (keyLength === 0)
return ctx.stylize(`[${name}]`, 'special');
base = `[${name}]`;
Expand All @@ -631,12 +670,12 @@ function formatValue(ctx, value, recurseTimes) {
return ctx.stylize(regExpToString.call(value), 'regexp');
base = `${regExpToString.call(value)}`;
} else if (isDate(value)) {
// Make dates with properties first say the date
if (keyLength === 0) {
if (Number.isNaN(value.getTime()))
return ctx.stylize(value.toString(), 'date');
if (Number.isNaN(dateGetTime.call(value)))
return ctx.stylize(String(value), 'date');
return ctx.stylize(dateToISOString.call(value), 'date');
}
// Make dates with properties first say the date
base = dateToISOString.call(value);
} else if (isError(value)) {
// Make error with message first say the error
Expand All @@ -662,28 +701,31 @@ function formatValue(ctx, value, recurseTimes) {
// Fast path for ArrayBuffer and SharedArrayBuffer.
// Can't do the same for DataView because it has a non-primitive
// .buffer property that we need to recurse for.
const prefix = getPrefix(constructor, tag);
let prefix = getPrefix(constructor, tag);
if (prefix === '') {
prefix = isArrayBuffer(value) ? 'ArrayBuffer ' : 'SharedArrayBuffer ';
}
if (keyLength === 0)
return prefix +
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
braces[0] = `${prefix}{`;
keys.unshift('byteLength');
} else if (isDataView(value)) {
braces[0] = `${getPrefix(constructor, tag)}{`;
braces[0] = `${getPrefix(constructor, tag) || 'DataView '}{`;
// .buffer goes last, it's not a primitive like the others.
keys.unshift('byteLength', 'byteOffset', 'buffer');
} else if (isPromise(value)) {
braces[0] = `${getPrefix(constructor, tag)}{`;
braces[0] = `${getPrefix(constructor, tag) || 'Promise '}{`;
formatter = formatPromise;
} else if (isWeakSet(value)) {
braces[0] = `${getPrefix(constructor, tag)}{`;
braces[0] = `${getPrefix(constructor, tag) || 'WeakSet '}{`;
if (ctx.showHidden) {
formatter = formatWeakSet;
} else {
extra = '<items unknown>';
}
} else if (isWeakMap(value)) {
braces[0] = `${getPrefix(constructor, tag)}{`;
braces[0] = `${getPrefix(constructor, tag) || 'WeakMap '}{`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to make the || 'thing ' be a param of getPrefix to add the trailing space then?

if (ctx.showHidden) {
formatter = formatWeakMap;
} else {
Expand All @@ -692,43 +734,67 @@ function formatValue(ctx, value, recurseTimes) {
} else if (types.isModuleNamespaceObject(value)) {
braces[0] = `[${tag}] {`;
formatter = formatNamespaceObject;
} else if (isNumberObject(value)) {
base = `[Number: ${getBoxedValue(numberValueOf.call(value))}]`;
if (keyLength === 0)
return ctx.stylize(base, 'number');
} else if (isBooleanObject(value)) {
base = `[Boolean: ${getBoxedValue(booleanValueOf.call(value))}]`;
if (keyLength === 0)
return ctx.stylize(base, 'boolean');
} else if (isBigIntObject(value)) {
base = `[BigInt: ${getBoxedValue(bigIntValueOf.call(value))}]`;
if (keyLength === 0)
return ctx.stylize(base, 'bigint');
} else if (isSymbolObject(value)) {
base = `[Symbol: ${getBoxedValue(symbolValueOf.call(value))}]`;
if (keyLength === 0)
return ctx.stylize(base, 'symbol');
} else if (isStringObject(value)) {
const raw = stringValueOf.call(value);
base = `[String: ${getBoxedValue(raw, ctx)}]`;
if (keyLength === raw.length)
return ctx.stylize(base, 'string');
// For boxed Strings, we have to remove the 0-n indexed entries,
// since they just noisy up the output and are redundant
// Make boxed primitive Strings look like such
keys = keys.slice(value.length);
braces = ['{', '}'];
// The input prototype got manipulated. Special handle these.
// We have to rebuild the information so we are able to display everything.
} else if (isSet(value)) {
const newVal = addExtraKeys(value, new Set(setValues.call(value)), keys);
return formatValue(ctx, newVal, recurseTimes);
} else if (isMap(value)) {
const newVal = addExtraKeys(value, new Map(mapEntries.call(value)), keys);
return formatValue(ctx, newVal, recurseTimes);
} else if (Array.isArray(value)) {
// The prefix is not always possible to fully reconstruct.
const prefix = getPrefix(constructor, tag);
braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
formatter = formatArray;
const newValue = [];
newValue.length = value.length;
value = addExtraKeys(value, newValue, keys);
} else if (isTypedArray(value)) {
const newValue = findTypedConstructor(value);
value = addExtraKeys(value, newValue, keys.slice(newValue.length));
// The prefix is not always possible to fully reconstruct.
braces = [`${getPrefix(getConstructorName(value), tag)}[`, ']'];
formatter = formatTypedArray;
} else if (isMapIterator(value)) {
braces = [`[${tag || 'Map Iterator'}] {`, '}'];
formatter = formatMapIterator;
} else if (isSetIterator(value)) {
braces = [`[${tag || 'Set Iterator'}] {`, '}'];
formatter = formatSetIterator;
// Handle other regular objects again.
} else if (keyLength === 0) {
if (isExternal(value))
return ctx.stylize('[External]', 'special');
return `${getPrefix(constructor, tag)}{}`;
} else {
// Check boxed primitives other than string with valueOf()
// NOTE: `Date` has to be checked first!
// The .valueOf() call can fail for a multitude of reasons
try {
raw = value.valueOf();
} catch (e) { /* ignore */ }

if (typeof raw === 'number') {
// Make boxed primitive Numbers look like such
const formatted = formatPrimitive(stylizeNoColor, raw);
if (keyLength === 0)
return ctx.stylize(`[Number: ${formatted}]`, 'number');
base = `[Number: ${formatted}]`;
} else if (typeof raw === 'boolean') {
// Make boxed primitive Booleans look like such
const formatted = formatPrimitive(stylizeNoColor, raw);
if (keyLength === 0)
return ctx.stylize(`[Boolean: ${formatted}]`, 'boolean');
base = `[Boolean: ${formatted}]`;
// eslint-disable-next-line valid-typeof
} else if (typeof raw === 'bigint') {
// Make boxed primitive BigInts look like such
const formatted = formatPrimitive(stylizeNoColor, raw);
if (keyLength === 0)
return ctx.stylize(`[BigInt: ${formatted}]`, 'bigint');
base = `[BigInt: ${formatted}]`;
} else if (typeof raw === 'symbol') {
const formatted = formatPrimitive(stylizeNoColor, raw);
return ctx.stylize(`[Symbol: ${formatted}]`, 'symbol');
} else if (keyLength === 0) {
if (isExternal(value))
return ctx.stylize('[External]', 'special');
return `${getPrefix(constructor, tag)}{}`;
} else {
braces[0] = `${getPrefix(constructor, tag)}{`;
}
braces[0] = `${getPrefix(constructor, tag)}{`;
}
}

Expand Down Expand Up @@ -761,7 +827,7 @@ function formatValue(ctx, value, recurseTimes) {
if (extra !== undefined)
output.unshift(extra);

for (var i = 0; i < symbols.length; i++) {
for (i = 0; i < symbols.length; i++) {
output.push(formatProperty(ctx, value, recurseTimes, symbols[i], 0));
}

Expand Down
Loading