Skip to content

Commit

Permalink
util: add private fields preview support
Browse files Browse the repository at this point in the history
`util.previewValue` may reveal private fields of an object.
  • Loading branch information
legendecas committed Apr 12, 2020
1 parent 57aba5e commit c4aca9f
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 2 deletions.
7 changes: 7 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1689,6 +1689,13 @@ object.
An attempt was made to `require()` an [ES Module][].

<a id="ERR_PREVIEW_FAILURE"></a>
### `ERR_PREVIEW_FAILURE`

> Stability: 1 - Experimental
An attempt of previewing a JavaScript value was failed.

<a id="ERR_SCRIPT_EXECUTION_INTERRUPTED"></a>
### `ERR_SCRIPT_EXECUTION_INTERRUPTED`

Expand Down
43 changes: 43 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,49 @@ Otherwise, returns `false`.
See [`assert.deepStrictEqual()`][] for more information about deep strict
equality.

### `util.previewValue(value[, opts])`
<!-- YAML
added: REPLACEME
-->

* `object` {any} Any JavaScript primitive or `Object`.
* `options` {Object}
* `breakLength` {integer} The length at which input values are split across
multiple lines. Set to `Infinity` to format the input as a single line
(in combination with `compact` set to `true` or any number >= `1`).
**Default:** `80`.
* `colors` {boolean} If `true`, the output is styled with ANSI color
codes. Colors are customizable. See [Customizing `util.inspect` colors][].
**Default:** `false`.
* `compact` {boolean|integer} Setting this to `false` causes each object key
to be displayed on a new line. It will also add new lines to text that is
longer than `breakLength`. If set to a number, the most `n` inner elements
are united on a single line as long as all properties fit into
`breakLength`. Short array elements are also grouped together. No
text will be reduced below 16 characters, no matter the `breakLength` size.
For more information, see the example below. **Default:** `3`.
* Returns: {Promise<string>} A promise of the representation of `object`.

> Stability: 1 - Experimental
The `util.previewValue` method returns promise of a string representation
of `object` that is intended for debugging. The output of `util.previewValue`
may change at any time and should not be depended upon programmatically.
Additional `options` may be passed that alter the result. `util.previewValue()`
will use the constructor's name make an identifiable tag for an inspected value.

The difference of this method with `util.inspect` is `util.previewValue` can
reveal target own private fields.

```javascript
class Foo { #bar = 'baz' }

util.previewValue(new Foo())
.then((preview) => {
console.log(preview); // 'Foo { #bar: "baz" }'
});
```

## `util.promisify(original)`
<!-- YAML
added: v8.0.0
Expand Down
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,7 @@ E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath, base = undefined) => {
pkgPath} imported from ${base}`;
}
}, Error);
E('ERR_PREVIEW_FAILURE', 'Preview value failed for reason: "%s"', Error);
E('ERR_REQUIRE_ESM',
(filename, parentPath = null, packageJsonPath = null) => {
let msg = `Must use import to load ES Module: ${filename}`;
Expand Down
201 changes: 200 additions & 1 deletion lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const {
ObjectPrototypeHasOwnProperty,
ObjectPrototypePropertyIsEnumerable,
ObjectSeal,
Promise,
RegExp,
RegExpPrototypeToString,
Set,
Expand Down Expand Up @@ -73,7 +74,8 @@ const {

const {
codes: {
ERR_INVALID_ARG_TYPE
ERR_INVALID_ARG_TYPE,
ERR_PREVIEW_FAILURE
},
isStackOverflowError
} = require('internal/errors');
Expand Down Expand Up @@ -120,6 +122,12 @@ const assert = require('internal/assert');

const { NativeModule } = require('internal/bootstrap/loaders');

const { hasInspector } = internalBinding('config');

let inspector;
let privateInspectionSession;
let privatePreviewSessionCount = 0;

const setSizeGetter = uncurryThis(
ObjectGetOwnPropertyDescriptor(SetPrototype, 'size').get);
const mapSizeGetter = uncurryThis(
Expand Down Expand Up @@ -2027,11 +2035,202 @@ function stripVTControlCharacters(str) {
return str.replace(ansi, '');
}

async function previewValue(target, opts) {
const ctx = {
budget: {},
indentationLvl: 0,
currentDepth: 0,
stylize: stylizeNoColor,
compact: inspectDefaultOptions.compact,
breakLength: inspectDefaultOptions.breakLength,
};
if (opts) {
const optKeys = ObjectKeys(opts);
for (const key of optKeys) {
if (ObjectPrototypeHasOwnProperty(inspectDefaultOptions, key)) {
ctx[key] = opts[key];
} else if (ctx.userOptions === undefined) {
// This is required to pass through the actual user input.
ctx.userOptions = opts;
}
}
}
if (ctx.colors) {
ctx.stylize = stylizeWithColor;
}

return previewRaw(ctx, target);
}

async function previewRaw(ctx, target) {
if (typeof target !== 'object' &&
typeof target !== 'function' &&
!isUndetectableObject(target)) {
return formatPrimitive(ctx.stylize, target, ctx);
}
if (target === null) {
return ctx.stylize('null', 'null');
}
if (typeof target === 'function') {
return formatRaw(ctx, target);
}

const preview = await getValuePreviewAsync(target);
if (preview == null) {
return undefined;
}
return formatInspectorPropertyPreview(ctx, target, preview);
}

function previewPrelude() {
if (!hasInspector) {
return false;
}
if (privateInspectionSession == null) {
inspector = require('inspector');
privateInspectionSession = new inspector.Session();
privateInspectionSession.connect();
}
return true;
}

function getValuePreviewAsync(target) {
if (!previewPrelude()) {
return undefined;
}
if (privatePreviewSessionCount === 0) {
privateInspectionSession.post('Runtime.enable');
privateInspectionSession.post('Debugger.enable');
}
privatePreviewSessionCount++;

return new Promise((resolve, reject) => {
privateInspectionSession.once('Debugger.paused', (pausedContext) => {
const callFrames = pausedContext.params.callFrames;
/** USED */target;
privateInspectionSession.post(
'Debugger.evaluateOnCallFrame', {
/**
* 0. inspector.dispatch,
* 1. inspector.post,
* 2. new Promise,
* 3. previewValueAsync
*/
callFrameId: callFrames[3].callFrameId,
expression: 'target',
}, (err, result) => {
if (err) {
return reject(err);
}
const { result: evalResult } = result;

if (evalResult.type !== 'object') {
return resolve();
}
if (evalResult.subtype === 'error') {
return reject(ERR_PREVIEW_FAILURE(evalResult.description));
}
resolve(getValuePreviewByObjectIdAsync(evalResult.objectId));
});
});
privateInspectionSession.post('Debugger.pause', () => {
/* pause doesn't return anything. */
});
}).finally(() => {
privatePreviewSessionCount--;
if (privatePreviewSessionCount === 0) {
privateInspectionSession.post('Runtime.disable');
privateInspectionSession.post('Debugger.disable');
}
});
}

function getValuePreviewByObjectIdAsync(objectId, options = {}) {
return new Promise((resolve, reject) => {
privateInspectionSession.post('Runtime.getProperties', {
objectId: objectId,
ownProperties: true,
...options
}, (err, result) => {
if (err) {
return reject(err);
}
resolve(result);
});
});
}

async function formatInspectorPropertyPreview(
ctx, target, preview, isJsValue = true
) {
const { result, privateProperties } = preview;

const output = [];
for (var item of [
// __proto__ is an own property
...result.filter((it) => it.name !== '__proto__'),
...(privateProperties ?? [])
]) {
const valueDescriptor = item.value;
const valueType = valueDescriptor.type;
const subtype = valueDescriptor.subtype;
const stylize = ctx.stylize;
let str;
if (valueType === 'string') {
str = stylize(strEscape(valueDescriptor.value), valueType);
} else if (valueType === 'number') {
str = stylize(
// -0 is unserializable to JSON representation
valueDescriptor.value ?? valueDescriptor.unserializableValue,
valueType);
} else if (valueType === 'boolean') {
str = stylize(valueDescriptor.value, valueType);
} else if (valueType === 'symbol') {
str = stylize(valueDescriptor.description, valueType);
} else if (valueType === 'undefined') {
str = stylize(valueType, valueType);
} else if (subtype === 'null') {
str = stylize(subtype, 'null');
} else if (valueType === 'bigint') {
str = stylize(valueDescriptor.unserializableValue, valueType);
} else if (valueType === 'function') {
str = await previewFunction(ctx, valueDescriptor.objectId);
} else if (valueType === 'object') {
// TODO(legendecas): Inspect the whole object, not only the class name.
str = `#[${valueDescriptor.className}]`;
} else {
/* c8 ignore next */
assert.fail(`Unknown value type '${valueType}'`);
}
output.push(`${item.name}: ${str}`);
}

const constructor = isJsValue ?
getConstructorName(target, ctx, 0) :
target.className;
const braces = [`${getPrefix(constructor, '', 'Object')}{`, '}'];
const base = '';
const res = reduceToSingleString(
ctx, output, base, braces, kObjectType, ctx.currentDepth);
return res;
}

async function previewFunction(ctx, objectId) {
const { result } = await getValuePreviewByObjectIdAsync(
objectId, { ownProperties: false });
const nameDesc = result.find((it) => it.name === 'name');
if (nameDesc == null) {
return ctx.stylize('[Function]', 'function');
}
return ctx.stylize(`[Function: ${nameDesc.value.value}]`, 'function');
}

module.exports = {
inspect,
format,
formatWithOptions,
getStringWidth,
inspectDefaultOptions,
previewValue,
stripVTControlCharacters
};
4 changes: 3 additions & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ const {
const {
format,
formatWithOptions,
inspect
inspect,
previewValue
} = require('internal/util/inspect');
const { debuglog } = require('internal/util/debuglog');
const { validateNumber } = require('internal/validators');
Expand Down Expand Up @@ -269,6 +270,7 @@ module.exports = {
isFunction,
isPrimitive,
log,
previewValue,
promisify,
TextDecoder,
TextEncoder,
Expand Down
Loading

0 comments on commit c4aca9f

Please sign in to comment.