mirror of
https://github.com/frida/frida-tools.git
synced 2026-11-07 09:35:59 -10:00
repl: Render evaluated values richly
Encode each result into a bounded tagged tree in the agent so serializing cyclic or huge objects can never hang, then pretty-print it host-side with syntax highlighting, depth and breadth caps, and a hexdump for binary data.
This commit is contained in:
1 parent
8911f2da0b
commit
f7384b5498
5 files changed
+705
-25
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import { encodeValue } from "./value.js";
|
||||
|
||||
class REPL {
|
||||
#quickCommands = new Map();
|
||||
|
||||
@@ -100,10 +102,10 @@ function evaluate(func: () => any) {
|
||||
const result = func();
|
||||
if (result instanceof ArrayBuffer) {
|
||||
return result;
|
||||
} else {
|
||||
const type = (result === null) ? "null" : typeof result;
|
||||
return [type, result];
|
||||
}
|
||||
const type = (result === null) ? "null" : typeof result;
|
||||
const [tree, blob] = encodeValue(result, { maxDepth: 5 });
|
||||
return ["inspect", type, tree, (blob !== null) ? Array.from(new Uint8Array(blob)) : null];
|
||||
} catch (exception) {
|
||||
const e = exception as Error;
|
||||
return ["error", {
|
||||
|
||||
@@ -2,6 +2,7 @@ entrypoint = 'agent.ts'
|
||||
|
||||
sources = [
|
||||
entrypoint,
|
||||
'value.ts',
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
'tsconfig.json',
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
// Turn an arbitrary value into a bounded, JSON-safe tagged tree (plus a binary
|
||||
// blob holding any byte buffers), so that serializing it can never hang or blow
|
||||
// up on cyclic or very large objects such as a ControlFlowGraph. The REPL host
|
||||
// decodes and pretty-prints the tree.
|
||||
|
||||
export interface EncodeOptions {
|
||||
maxDepth?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_DEPTH = 3;
|
||||
|
||||
export type PackedValue = [EncodedValueTree, ArrayBuffer | null];
|
||||
|
||||
export type EncodedValueTree = EncodedValue;
|
||||
type EncodedValue = [ValueTag, ...any[]];
|
||||
|
||||
type BytesEncoded = [tag: ValueTag.Bytes, offset: number, length: number, kind: BytesKind];
|
||||
type BytesKind = "ArrayBuffer" | "DataView" | TypedArrayName;
|
||||
|
||||
const enum ValueTag {
|
||||
Number = 0,
|
||||
String = 1,
|
||||
Object = 2,
|
||||
Array = 3,
|
||||
NativePointer = 4,
|
||||
Null = 5,
|
||||
Boolean = 6,
|
||||
Bytes = 7,
|
||||
Function = 8,
|
||||
Error = 9,
|
||||
Undefined = 10,
|
||||
BigInt = 11,
|
||||
Symbol = 12,
|
||||
Date = 13,
|
||||
RegExp = 14,
|
||||
Map = 15,
|
||||
Set = 16,
|
||||
Promise = 17,
|
||||
WeakMap = 18,
|
||||
WeakSet = 19,
|
||||
DepthLimit = 20,
|
||||
Circular = 21,
|
||||
}
|
||||
|
||||
type TypedArrayName =
|
||||
| "Int8Array"
|
||||
| "Uint8Array"
|
||||
| "Uint8ClampedArray"
|
||||
| "Int16Array"
|
||||
| "Uint16Array"
|
||||
| "Int32Array"
|
||||
| "Uint32Array"
|
||||
| "Float32Array"
|
||||
| "Float64Array"
|
||||
| "BigInt64Array"
|
||||
| "BigUint64Array";
|
||||
|
||||
interface EncodeState {
|
||||
seen: Map<unknown, number>;
|
||||
nextId: number;
|
||||
}
|
||||
|
||||
export function encodeValue(value: unknown, options?: EncodeOptions): PackedValue {
|
||||
const chunks: BinaryChunk[] = [];
|
||||
const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
||||
const state = makeEncodeState();
|
||||
const tree = collectTree(value, chunks, maxDepth, 0, state);
|
||||
const { blob, offsets } = packChunks(chunks);
|
||||
const patchedTree = patchTree(tree, offsets);
|
||||
return [patchedTree, blob];
|
||||
}
|
||||
|
||||
function makeEncodeState(): EncodeState {
|
||||
return {
|
||||
seen: new Map(),
|
||||
nextId: 1
|
||||
};
|
||||
}
|
||||
|
||||
function collectTree(
|
||||
value: unknown,
|
||||
chunks: BinaryChunk[],
|
||||
maxDepth: number,
|
||||
depth: number,
|
||||
state: EncodeState
|
||||
): EncodedValueTree {
|
||||
if (value === null) {
|
||||
return [ValueTag.Null];
|
||||
}
|
||||
|
||||
const t = typeof value;
|
||||
|
||||
if (t === "number") {
|
||||
return [ValueTag.Number, value as number];
|
||||
}
|
||||
|
||||
if (t === "string") {
|
||||
return [ValueTag.String, value as string];
|
||||
}
|
||||
|
||||
if (t === "object") {
|
||||
const obj = value as any;
|
||||
|
||||
if (obj instanceof NativePointer) {
|
||||
return [ValueTag.NativePointer, (obj as NativePointer).toString()];
|
||||
}
|
||||
|
||||
if (isBinaryLike(obj)) {
|
||||
const index = chunks.length;
|
||||
const binary = obj as ArrayBufferView | ArrayBuffer;
|
||||
const kind = getBytesKind(binary);
|
||||
chunks.push({ index, buf: binary });
|
||||
return [ValueTag.Bytes, index, kind];
|
||||
}
|
||||
|
||||
if (obj instanceof Error) {
|
||||
const err = obj as Error;
|
||||
const name = err.name ?? "Error";
|
||||
const message = err.message ?? "";
|
||||
const stack = err.stack ?? "";
|
||||
return [ValueTag.Error, name, message, stack];
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return [ValueTag.Date, (obj as Date).toISOString()];
|
||||
}
|
||||
|
||||
if (obj instanceof RegExp) {
|
||||
const r = obj as RegExp;
|
||||
return [ValueTag.RegExp, r.source, r.flags];
|
||||
}
|
||||
|
||||
if (obj instanceof Promise) {
|
||||
return [ValueTag.Promise];
|
||||
}
|
||||
|
||||
if (obj instanceof WeakMap) {
|
||||
return [ValueTag.WeakMap];
|
||||
}
|
||||
|
||||
if (obj instanceof WeakSet) {
|
||||
return [ValueTag.WeakSet];
|
||||
}
|
||||
|
||||
const isArray = Array.isArray(obj);
|
||||
const isMap = obj instanceof Map;
|
||||
const isSet = obj instanceof Set;
|
||||
|
||||
const existingId = state.seen.get(obj);
|
||||
if (existingId !== undefined) {
|
||||
return [ValueTag.Circular, existingId];
|
||||
}
|
||||
|
||||
if (depth >= maxDepth) {
|
||||
if (isArray) {
|
||||
return [ValueTag.DepthLimit, ValueTag.Array];
|
||||
}
|
||||
if (isMap) {
|
||||
return [ValueTag.DepthLimit, ValueTag.Map];
|
||||
}
|
||||
if (isSet) {
|
||||
return [ValueTag.DepthLimit, ValueTag.Set];
|
||||
}
|
||||
return [ValueTag.DepthLimit, ValueTag.Object];
|
||||
}
|
||||
|
||||
const id = state.nextId++;
|
||||
state.seen.set(obj, id);
|
||||
|
||||
if (isMap) {
|
||||
const mapValue = obj as Map<unknown, unknown>;
|
||||
const entries: [EncodedValueTree, EncodedValueTree][] = [];
|
||||
for (const [k, v] of mapValue.entries()) {
|
||||
const keyTree = collectTree(k, chunks, maxDepth, depth + 1, state);
|
||||
const valueTree = collectTree(v, chunks, maxDepth, depth + 1, state);
|
||||
entries.push([keyTree, valueTree]);
|
||||
}
|
||||
return [ValueTag.Map, id, entries];
|
||||
}
|
||||
|
||||
if (isSet) {
|
||||
const setValue = obj as Set<unknown>;
|
||||
const items: EncodedValueTree[] = [];
|
||||
for (const v of setValue.values()) {
|
||||
items.push(collectTree(v, chunks, maxDepth, depth + 1, state));
|
||||
}
|
||||
return [ValueTag.Set, id, items];
|
||||
}
|
||||
|
||||
if (isArray) {
|
||||
const elements = (obj as unknown[]).map(v =>
|
||||
collectTree(v, chunks, maxDepth, depth + 1, state),
|
||||
);
|
||||
return [ValueTag.Array, id, elements];
|
||||
}
|
||||
|
||||
try {
|
||||
const maybeToJSON = obj.toJSON;
|
||||
if (typeof maybeToJSON === "function") {
|
||||
const converted = maybeToJSON.call(obj);
|
||||
return collectTree(converted, chunks, maxDepth, depth, state);
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
|
||||
const entries: [EncodedValueTree, EncodedValueTree][] = [];
|
||||
|
||||
for (const k of Object.keys(obj as Record<string, unknown>)) {
|
||||
const keyTree: EncodedValueTree = [ValueTag.String, k];
|
||||
let v;
|
||||
try {
|
||||
v = obj[k];
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const valueTree = collectTree(v, chunks, maxDepth, depth + 1, state);
|
||||
entries.push([keyTree, valueTree]);
|
||||
}
|
||||
|
||||
const symbols = Object.getOwnPropertySymbols(obj as object);
|
||||
for (const s of symbols) {
|
||||
if (!Object.prototype.propertyIsEnumerable.call(obj, s)) {
|
||||
continue;
|
||||
}
|
||||
const keyTree: EncodedValueTree = [ValueTag.Symbol, String(s)];
|
||||
let v;
|
||||
try {
|
||||
v = obj[s];
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const valueTree = collectTree(v, chunks, maxDepth, depth + 1, state);
|
||||
entries.push([keyTree, valueTree]);
|
||||
}
|
||||
|
||||
return [ValueTag.Object, id, entries];
|
||||
}
|
||||
|
||||
if (t === "boolean") {
|
||||
return [ValueTag.Boolean, value as boolean];
|
||||
}
|
||||
|
||||
if (t === "function") {
|
||||
const fn = value as Function;
|
||||
const name = fn.name ?? "";
|
||||
const sig = name === "" ? "[Function]" : `[Function: ${name}]`;
|
||||
return [ValueTag.Function, sig];
|
||||
}
|
||||
|
||||
if (t === "undefined") {
|
||||
return [ValueTag.Undefined];
|
||||
}
|
||||
|
||||
if (t === "bigint") {
|
||||
const big = value as bigint;
|
||||
return [ValueTag.BigInt, big.toString()];
|
||||
}
|
||||
|
||||
if (t === "symbol") {
|
||||
return [ValueTag.Symbol, String(value)];
|
||||
}
|
||||
|
||||
return [ValueTag.Undefined];
|
||||
}
|
||||
|
||||
interface BinaryChunk {
|
||||
index: number;
|
||||
buf: ArrayBufferView | ArrayBuffer;
|
||||
}
|
||||
|
||||
function packChunks(chunks: BinaryChunk[]): {
|
||||
blob: ArrayBuffer | null;
|
||||
offsets: { offset: number; length: number }[];
|
||||
} {
|
||||
if (chunks.length === 0) {
|
||||
return { blob: null, offsets: [] };
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
const lengths = chunks.map(({ buf }) => {
|
||||
const b = buf instanceof ArrayBuffer ? buf : buf.buffer;
|
||||
return b.byteLength;
|
||||
});
|
||||
for (const len of lengths) {
|
||||
total += len;
|
||||
}
|
||||
|
||||
const blob = new Uint8Array(total);
|
||||
const offsets: { offset: number; length: number }[] = [];
|
||||
|
||||
let offset = 0;
|
||||
chunks.forEach(({ buf }, index) => {
|
||||
const raw = buf instanceof ArrayBuffer
|
||||
? new Uint8Array(buf)
|
||||
: new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
||||
blob.set(raw, offset);
|
||||
offsets[index] = { offset, length: raw.byteLength };
|
||||
offset += raw.byteLength;
|
||||
});
|
||||
|
||||
return { blob: blob.buffer as ArrayBuffer, offsets };
|
||||
}
|
||||
|
||||
function patchTree(
|
||||
tree: EncodedValueTree,
|
||||
offsets: { offset: number; length: number }[]
|
||||
): EncodedValueTree {
|
||||
const [tag, ...rest] = tree;
|
||||
|
||||
switch (tag) {
|
||||
case ValueTag.Object: {
|
||||
const id = rest[0] as number;
|
||||
const entries = rest[1] as [EncodedValueTree, EncodedValueTree][];
|
||||
const patchedEntries = entries.map(([k, v]) => [
|
||||
patchTree(k, offsets),
|
||||
patchTree(v, offsets),
|
||||
] as [EncodedValueTree, EncodedValueTree]);
|
||||
return [ValueTag.Object, id, patchedEntries];
|
||||
}
|
||||
|
||||
case ValueTag.Array: {
|
||||
const id = rest[0] as number;
|
||||
const items = (rest[1] as EncodedValueTree[])
|
||||
.map(child => patchTree(child, offsets));
|
||||
return [ValueTag.Array, id, items];
|
||||
}
|
||||
|
||||
case ValueTag.Bytes: {
|
||||
const index = rest[0] as number;
|
||||
const kind = rest[1] as BytesKind;
|
||||
const { offset, length } = offsets[index];
|
||||
return [ValueTag.Bytes, offset, length, kind] as BytesEncoded;
|
||||
}
|
||||
|
||||
case ValueTag.Map: {
|
||||
const id = rest[0] as number;
|
||||
const entries = rest[1] as [EncodedValueTree, EncodedValueTree][];
|
||||
const patchedEntries = entries.map(([k, v]) => [
|
||||
patchTree(k, offsets),
|
||||
patchTree(v, offsets),
|
||||
] as [EncodedValueTree, EncodedValueTree]);
|
||||
return [ValueTag.Map, id, patchedEntries];
|
||||
}
|
||||
|
||||
case ValueTag.Set: {
|
||||
const id = rest[0] as number;
|
||||
const items = rest[1] as EncodedValueTree[];
|
||||
const patchedItems = items.map(child => patchTree(child, offsets));
|
||||
return [ValueTag.Set, id, patchedItems];
|
||||
}
|
||||
|
||||
default:
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
|
||||
function getBytesKind(v: ArrayBufferView | ArrayBuffer): BytesKind {
|
||||
if (v instanceof ArrayBuffer) {
|
||||
return "ArrayBuffer";
|
||||
}
|
||||
if (v instanceof DataView) {
|
||||
return "DataView";
|
||||
}
|
||||
const ctor = (v as { constructor?: { name?: string } }).constructor;
|
||||
const name = ctor?.name ?? "ArrayBuffer";
|
||||
return name as BytesKind;
|
||||
}
|
||||
|
||||
function isBinaryLike(v: unknown): v is ArrayBufferView | ArrayBuffer {
|
||||
return v instanceof ArrayBuffer || ArrayBuffer.isView(v);
|
||||
}
|
||||
+13
-22
@@ -28,7 +28,7 @@ from prompt_toolkit.styles import Style as PromptToolkitStyle
|
||||
from pygments.lexers.javascript import JavascriptLexer
|
||||
from pygments.token import Token
|
||||
|
||||
from frida_tools import _repl_magic
|
||||
from frida_tools import _repl_magic, repl_inspect
|
||||
from frida_tools.application import ConsoleApplication
|
||||
from frida_tools.cli_formatting import THEME_COLOR, format_compiled, format_compiling, format_diagnostic
|
||||
from frida_tools.reactor import Reactor
|
||||
@@ -482,12 +482,11 @@ class REPLApplication(ConsoleApplication):
|
||||
success = False
|
||||
try:
|
||||
t, value = self._perform_on_reactor_thread(lambda: exec(arg))
|
||||
if t in ("function", "undefined", "null"):
|
||||
output = t
|
||||
elif t == "binary":
|
||||
output = hexdump(value).rstrip("\n")
|
||||
if t == "binary":
|
||||
output = repl_inspect.render_hexdump(value)
|
||||
else:
|
||||
output = json.dumps(value, sort_keys=True, indent=4, separators=(",", ": "))
|
||||
tree, blob = value
|
||||
output = "undefined" if repl_inspect.is_undefined(tree) else repl_inspect.render(tree, blob)
|
||||
success = True
|
||||
except JavaScriptError as e:
|
||||
error = e.error
|
||||
@@ -529,7 +528,8 @@ class REPLApplication(ConsoleApplication):
|
||||
obj_type, obj_value = self._evaluate_expression(obj_to_identify)
|
||||
|
||||
if obj_type == "function":
|
||||
signature = self._evaluate_expression("%s.toString()" % obj_to_identify)[1]
|
||||
_, (signature_tree, _) = self._evaluate_expression("%s.toString()" % obj_to_identify)
|
||||
signature = signature_tree[1]
|
||||
clean_signature = signature.split("{")[0][:-1].split("function ")[-1]
|
||||
|
||||
if "[native code]" in signature:
|
||||
@@ -549,9 +549,9 @@ class REPLApplication(ConsoleApplication):
|
||||
help_text += "Docstring: #TODO :)"
|
||||
|
||||
elif obj_type == "string":
|
||||
bool_text = self._evaluate_expression(obj_to_identify + ".toString()")[1]
|
||||
_, (text_tree, _) = self._evaluate_expression(obj_to_identify + ".toString()")
|
||||
help_text += "Type: Boolean\n"
|
||||
help_text += f"Text: {bool_text.decode()}\n"
|
||||
help_text += f"Text: {text_tree[1]}\n"
|
||||
help_text += "Docstring: #TODO :)"
|
||||
|
||||
self._print(help_text)
|
||||
@@ -681,7 +681,8 @@ class REPLApplication(ConsoleApplication):
|
||||
return ("binary", bytes())
|
||||
elif result[0] == "error":
|
||||
raise JavaScriptError(result[1])
|
||||
return (result[0], result[1])
|
||||
_, value_type, tree, blob = result
|
||||
return (value_type, (tree, blob))
|
||||
|
||||
def _process_message(self, message: Mapping[Any, Any], data: Any) -> None:
|
||||
message_type = message["type"]
|
||||
@@ -1096,7 +1097,8 @@ class FridaCompleter(Completer):
|
||||
if t == "error":
|
||||
return []
|
||||
|
||||
return sorted(filter(self._is_valid_name, set(value)))
|
||||
names = repl_inspect.to_string_list(value[0])
|
||||
return sorted(filter(self._is_valid_name, set(names)))
|
||||
|
||||
def _is_valid_name(self, name) -> bool:
|
||||
tokens = list(self._lexer.get_tokens(name))
|
||||
@@ -1112,17 +1114,6 @@ def script_needs_compilation(path: AnyStr) -> bool:
|
||||
return path.endswith(b".ts")
|
||||
|
||||
|
||||
def hexdump(src, length: int = 16) -> str:
|
||||
FILTER = "".join([(len(repr(chr(x))) == 3) and chr(x) or "." for x in range(256)])
|
||||
lines = []
|
||||
for c in range(0, len(src), length):
|
||||
chars = src[c : c + length]
|
||||
hex = " ".join(["%02x" % x for x in iter(chars)])
|
||||
printable = "".join(["%s" % ((x <= 127 and FILTER[x]) or ".") for x in iter(chars)])
|
||||
lines.append("%04x %-*s %s\n" % (c, length * 3, hex, printable))
|
||||
return "".join(lines)
|
||||
|
||||
|
||||
OS_BINARY_SIGNATURES = {
|
||||
b"\x4d\x5a", # PE
|
||||
b"\xca\xfe\xba\xbe", # Fat Mach-O
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
"""Rendering of JavaScript values inspected by the REPL agent.
|
||||
|
||||
The agent encodes each evaluated value into a bounded tagged tree, so that
|
||||
serializing cyclic or very large objects can never hang. This module
|
||||
pretty-prints that tree with syntax highlighting.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Sequence, Tuple
|
||||
|
||||
from colorama import Fore, Style
|
||||
|
||||
# Tags mirror ValueTag in agents/repl/agent.ts.
|
||||
NUMBER = 0
|
||||
STRING = 1
|
||||
OBJECT = 2
|
||||
ARRAY = 3
|
||||
NATIVE_POINTER = 4
|
||||
NULL = 5
|
||||
BOOLEAN = 6
|
||||
BYTES = 7
|
||||
FUNCTION = 8
|
||||
ERROR = 9
|
||||
UNDEFINED = 10
|
||||
BIGINT = 11
|
||||
SYMBOL = 12
|
||||
DATE = 13
|
||||
REGEXP = 14
|
||||
MAP = 15
|
||||
SET = 16
|
||||
PROMISE = 17
|
||||
WEAKMAP = 18
|
||||
WEAKSET = 19
|
||||
DEPTH_LIMIT = 20
|
||||
CIRCULAR = 21
|
||||
|
||||
_CYAN = Fore.CYAN
|
||||
_MINT = Fore.GREEN
|
||||
_KEY = Fore.LIGHTGREEN_EX
|
||||
_ORANGE = Fore.YELLOW
|
||||
_PURPLE = Fore.MAGENTA
|
||||
_RED = Fore.RED
|
||||
_BLUE = Fore.BLUE
|
||||
_GRAY = Fore.LIGHTBLACK_EX
|
||||
_RESET = Style.RESET_ALL
|
||||
|
||||
_MAX_ITEMS = 100
|
||||
|
||||
_HEX_BYTES_PER_ROW = 16
|
||||
|
||||
_CONTAINER_NAMES = {OBJECT: "Object", ARRAY: "Array", MAP: "Map", SET: "Set"}
|
||||
|
||||
Node = Sequence[Any]
|
||||
|
||||
|
||||
def render(tree: Node, blob: Sequence[int]) -> str:
|
||||
out: List[str] = []
|
||||
_append(tree, 0, out, blob)
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def render_hexdump(data: bytes) -> str:
|
||||
rows = []
|
||||
for offset in range(0, len(data), _HEX_BYTES_PER_ROW):
|
||||
rows.append(_hex_row(offset, data[offset:offset + _HEX_BYTES_PER_ROW]))
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def is_undefined(tree: Node) -> bool:
|
||||
return tree[0] == UNDEFINED
|
||||
|
||||
|
||||
def to_string_list(tree: Node) -> List[str]:
|
||||
return [element[1] for element in tree[2]]
|
||||
|
||||
|
||||
def _append(node: Node, level: int, out: List[str], blob: Sequence[int]) -> None:
|
||||
tag = node[0]
|
||||
|
||||
if tag == NUMBER:
|
||||
out.append(_color(_format_number(node[1]), _CYAN))
|
||||
|
||||
elif tag == STRING:
|
||||
out.append(_color('"%s"' % node[1], _MINT))
|
||||
|
||||
elif tag == OBJECT:
|
||||
_append_object(node, level, out, blob)
|
||||
|
||||
elif tag == ARRAY:
|
||||
_append_array(node, level, out, blob)
|
||||
|
||||
elif tag == NATIVE_POINTER:
|
||||
out.append(_color(node[1], _ORANGE))
|
||||
|
||||
elif tag == NULL:
|
||||
out.append(_color("null", _ORANGE))
|
||||
|
||||
elif tag == BOOLEAN:
|
||||
out.append(_color("true" if node[1] else "false", _ORANGE))
|
||||
|
||||
elif tag == BYTES:
|
||||
_append_bytes(node, level, out, blob)
|
||||
|
||||
elif tag == FUNCTION:
|
||||
out.append(_color(node[1], _PURPLE))
|
||||
|
||||
elif tag == ERROR:
|
||||
_append_error(node, level, out)
|
||||
|
||||
elif tag == UNDEFINED:
|
||||
out.append(_color("undefined", _ORANGE))
|
||||
|
||||
elif tag == BIGINT:
|
||||
out.append(_color(node[1] + "n", _CYAN))
|
||||
|
||||
elif tag == SYMBOL:
|
||||
out.append(_color(node[1], _PURPLE))
|
||||
|
||||
elif tag == DATE:
|
||||
out.append(_color("Date(", _BLUE))
|
||||
out.append(_color(node[1], _MINT))
|
||||
out.append(_color(")", _BLUE))
|
||||
|
||||
elif tag == REGEXP:
|
||||
_append_regexp(node, out)
|
||||
|
||||
elif tag == MAP:
|
||||
_append_map(node, level, out, blob)
|
||||
|
||||
elif tag == SET:
|
||||
_append_set(node, level, out, blob)
|
||||
|
||||
elif tag == PROMISE:
|
||||
out.append(_color("Promise", _PURPLE))
|
||||
|
||||
elif tag == WEAKMAP:
|
||||
out.append(_color("WeakMap", _PURPLE))
|
||||
|
||||
elif tag == WEAKSET:
|
||||
out.append(_color("WeakSet", _PURPLE))
|
||||
|
||||
elif tag == DEPTH_LIMIT:
|
||||
out.append(_color("%s<depth limit reached>" % _CONTAINER_NAMES[node[1]], _ORANGE))
|
||||
|
||||
elif tag == CIRCULAR:
|
||||
out.append(_color("⟳ circular *%d" % node[1], _ORANGE))
|
||||
|
||||
|
||||
def _append_object(node: Node, level: int, out: List[str], blob: Sequence[int]) -> None:
|
||||
properties = node[2]
|
||||
if not properties:
|
||||
out.append(_color("{}", _CYAN))
|
||||
return
|
||||
|
||||
out.append(_color("{", _CYAN))
|
||||
out.append("\n")
|
||||
shown, hidden = _capped(properties)
|
||||
for index, (key, value) in enumerate(shown):
|
||||
_indent(level + 1, out)
|
||||
out.append(_color(key[1], _KEY))
|
||||
out.append(": ")
|
||||
_append(value, level + 1, out, blob)
|
||||
if _has_more(index, shown, hidden):
|
||||
out.append(",")
|
||||
out.append("\n")
|
||||
_append_overflow(hidden, level + 1, out)
|
||||
_indent(level, out)
|
||||
out.append(_color("}", _CYAN))
|
||||
|
||||
|
||||
def _append_array(node: Node, level: int, out: List[str], blob: Sequence[int]) -> None:
|
||||
elements = node[2]
|
||||
if not elements:
|
||||
out.append("[]")
|
||||
return
|
||||
|
||||
out.append("[")
|
||||
out.append("\n")
|
||||
shown, hidden = _capped(elements)
|
||||
for index, element in enumerate(shown):
|
||||
_indent(level + 1, out)
|
||||
_append(element, level + 1, out, blob)
|
||||
if _has_more(index, shown, hidden):
|
||||
out.append(",")
|
||||
out.append("\n")
|
||||
_append_overflow(hidden, level + 1, out)
|
||||
_indent(level, out)
|
||||
out.append("]")
|
||||
|
||||
|
||||
def _append_bytes(node: Node, level: int, out: List[str], blob: Sequence[int]) -> None:
|
||||
offset, length, kind = node[1], node[2], node[3]
|
||||
out.append(_color("Bytes(", _MINT))
|
||||
out.append(_color(kind, _CYAN))
|
||||
out.append(_color("[%d])" % length, _MINT))
|
||||
data = bytes(blob[offset:offset + length])
|
||||
for start in range(0, len(data), _HEX_BYTES_PER_ROW):
|
||||
out.append("\n")
|
||||
_indent(level + 1, out)
|
||||
out.append(_hex_row(start, data[start:start + _HEX_BYTES_PER_ROW]))
|
||||
|
||||
|
||||
def _append_error(node: Node, level: int, out: List[str]) -> None:
|
||||
name, message, stack = node[1], node[2], node[3]
|
||||
out.append(_color(name if not message else "%s: %s" % (name, message), _RED))
|
||||
if not stack:
|
||||
return
|
||||
for line in stack.split("\n"):
|
||||
out.append("\n")
|
||||
_indent(level + 1, out)
|
||||
out.append(Style.DIM + line + _RESET)
|
||||
|
||||
|
||||
def _append_regexp(node: Node, out: List[str]) -> None:
|
||||
pattern, flags = node[1], node[2]
|
||||
out.append(_color("/", _PURPLE))
|
||||
out.append(_color(pattern, _MINT))
|
||||
out.append(_color("/", _PURPLE))
|
||||
if flags:
|
||||
out.append(_color(flags, _PURPLE))
|
||||
|
||||
|
||||
def _append_map(node: Node, level: int, out: List[str], blob: Sequence[int]) -> None:
|
||||
entries = node[2]
|
||||
if not entries:
|
||||
out.append(_color("Map{}", _CYAN))
|
||||
return
|
||||
|
||||
out.append(_color("Map{", _CYAN))
|
||||
out.append("\n")
|
||||
shown, hidden = _capped(entries)
|
||||
for index, (key, value) in enumerate(shown):
|
||||
_indent(level + 1, out)
|
||||
_append(key, level + 1, out, blob)
|
||||
out.append(" => ")
|
||||
_append(value, level + 1, out, blob)
|
||||
if _has_more(index, shown, hidden):
|
||||
out.append(",")
|
||||
out.append("\n")
|
||||
_append_overflow(hidden, level + 1, out)
|
||||
_indent(level, out)
|
||||
out.append(_color("}", _CYAN))
|
||||
|
||||
|
||||
def _append_set(node: Node, level: int, out: List[str], blob: Sequence[int]) -> None:
|
||||
elements = node[2]
|
||||
if not elements:
|
||||
out.append(_color("Set[]", _CYAN))
|
||||
return
|
||||
|
||||
out.append(_color("Set[", _CYAN))
|
||||
out.append("\n")
|
||||
shown, hidden = _capped(elements)
|
||||
for element in shown:
|
||||
_indent(level + 1, out)
|
||||
out.append(_color("• ", _CYAN))
|
||||
_append(element, level + 1, out, blob)
|
||||
out.append("\n")
|
||||
_append_overflow(hidden, level + 1, out)
|
||||
_indent(level, out)
|
||||
out.append(_color("]", _CYAN))
|
||||
|
||||
|
||||
def _capped(items: Sequence[Any]) -> Tuple[Sequence[Any], int]:
|
||||
shown = items[:_MAX_ITEMS]
|
||||
return shown, len(items) - len(shown)
|
||||
|
||||
|
||||
def _has_more(index: int, shown: Sequence[Any], hidden: int) -> bool:
|
||||
return index < len(shown) - 1 or hidden > 0
|
||||
|
||||
|
||||
def _append_overflow(hidden: int, level: int, out: List[str]) -> None:
|
||||
if not hidden:
|
||||
return
|
||||
_indent(level, out)
|
||||
out.append(_color("… %d more" % hidden, _ORANGE))
|
||||
out.append("\n")
|
||||
|
||||
|
||||
def _hex_row(offset: int, chunk: bytes) -> str:
|
||||
cells = " ".join(_color("%02X" % byte, _byte_color(byte)) for byte in chunk)
|
||||
padding = " " * (_HEX_BYTES_PER_ROW - len(chunk))
|
||||
glyphs = "".join(_color(_ascii_glyph(byte), _GRAY) for byte in chunk)
|
||||
return "%s %s%s %s" % (_color("%08X" % offset, _KEY), cells, padding, glyphs)
|
||||
|
||||
|
||||
def _byte_color(byte: int) -> str:
|
||||
if byte == 0x00:
|
||||
return _GRAY
|
||||
if 0x20 <= byte <= 0x7E:
|
||||
return _MINT
|
||||
if byte <= 0x1F or byte == 0x7F:
|
||||
return _ORANGE
|
||||
return _CYAN
|
||||
|
||||
|
||||
def _ascii_glyph(byte: int) -> str:
|
||||
return chr(byte) if 0x20 <= byte <= 0x7E else "."
|
||||
|
||||
|
||||
def _format_number(value: Any) -> str:
|
||||
if value is None:
|
||||
return "null"
|
||||
if isinstance(value, float):
|
||||
return str(int(value)) if value.is_integer() else repr(value)
|
||||
return str(value)
|
||||
|
||||
|
||||
def _color(text: str, color: str) -> str:
|
||||
return color + text + _RESET
|
||||
|
||||
|
||||
def _indent(level: int, out: List[str]) -> None:
|
||||
if level > 0:
|
||||
out.append(" " * level)
|
||||
Reference in new issue
Block a user