From 9e4f6a8af633de5589abfca6ea62ba4212483000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20S=C3=B8rb=C3=B8?= Date: Sat, 3 Oct 2026 17:42:28 +0200 Subject: [PATCH] repl: Render iterators and generators Evaluating an expression that yields a generator or other iterator object printed an empty object, since such objects have no own enumerable keys. Encode them as a new tagged value by pulling up to 100 items, marking when more remain, and label them by their toStringTag so a generator shows as Generator[...] and a Map iterator as Map Iterator[...]. The iterator is consumed by the display, as it would be by any other means of looking at its items. --- agents/repl/value.ts | 29 +++++++++++++++++++++++++++++ frida_tools/repl_inspect.py | 28 +++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/agents/repl/value.ts b/agents/repl/value.ts index 2712dc5..fcebdff 100644 --- a/agents/repl/value.ts +++ b/agents/repl/value.ts @@ -8,6 +8,7 @@ export interface EncodeOptions { } const DEFAULT_MAX_DEPTH = 3; +const MAX_ITERATOR_ITEMS = 100; export type PackedValue = [EncodedValueTree, ArrayBuffer | null]; @@ -40,6 +41,7 @@ const enum ValueTag { WeakSet = 19, DepthLimit = 20, Circular = 21, + Iterator = 22, } type TypedArrayName = @@ -145,6 +147,7 @@ function collectTree( const isArray = Array.isArray(obj); const isMap = obj instanceof Map; const isSet = obj instanceof Set; + const isIterator = typeof obj.next === "function" && typeof obj[Symbol.iterator] === "function"; const existingId = state.seen.get(obj); if (existingId !== undefined) { @@ -161,6 +164,9 @@ function collectTree( if (isSet) { return [ValueTag.DepthLimit, ValueTag.Set]; } + if (isIterator) { + return [ValueTag.DepthLimit, ValueTag.Iterator]; + } return [ValueTag.DepthLimit, ValueTag.Object]; } @@ -194,6 +200,20 @@ function collectTree( return [ValueTag.Array, id, elements]; } + if (isIterator) { + const label = obj[Symbol.toStringTag] ?? "Iterator"; + const items: EncodedValueTree[] = []; + let truncated = false; + for (const v of obj as Iterable) { + if (items.length === MAX_ITERATOR_ITEMS) { + truncated = true; + break; + } + items.push(collectTree(v, chunks, maxDepth, depth + 1, state)); + } + return [ValueTag.Iterator, id, label, items, truncated]; + } + try { const maybeToJSON = obj.toJSON; if (typeof maybeToJSON === "function") { @@ -349,6 +369,15 @@ function patchTree( return [ValueTag.Set, id, patchedItems]; } + case ValueTag.Iterator: { + const id = rest[0] as number; + const label = rest[1] as string; + const items = rest[2] as EncodedValueTree[]; + const truncated = rest[3] as boolean; + const patchedItems = items.map(child => patchTree(child, offsets)); + return [ValueTag.Iterator, id, label, patchedItems, truncated]; + } + default: return tree; } diff --git a/frida_tools/repl_inspect.py b/frida_tools/repl_inspect.py index 6c1c33e..aa6c4a7 100644 --- a/frida_tools/repl_inspect.py +++ b/frida_tools/repl_inspect.py @@ -33,6 +33,7 @@ WEAKSET = 19 DEPTH_LIMIT = 20 CIRCULAR = 21 +ITERATOR = 22 _CYAN = "fg:#64d2ff" _MINT = "fg:#66d4cf" @@ -47,7 +48,7 @@ _HEX_BYTES_PER_ROW = 16 -_CONTAINER_NAMES = {OBJECT: "Object", ARRAY: "Array", MAP: "Map", SET: "Set"} +_CONTAINER_NAMES = {OBJECT: "Object", ARRAY: "Array", MAP: "Map", SET: "Set", ITERATOR: "Iterator"} Node = Sequence[Any] Fragment = Tuple[str, str] @@ -147,6 +148,9 @@ def _append(node: Node, level: int, out: List[Fragment], blob: Sequence[int]) -> elif tag == CIRCULAR: out.append((_ORANGE, "⟳ circular *%d" % node[1])) + elif tag == ITERATOR: + _append_iterator(node, level, out, blob) + def _append_object(node: Node, level: int, out: List[Fragment], blob: Sequence[int]) -> None: properties = node[2] @@ -263,6 +267,28 @@ def _append_set(node: Node, level: int, out: List[Fragment], blob: Sequence[int] out.append((_CYAN, "]")) +def _append_iterator(node: Node, level: int, out: List[Fragment], blob: Sequence[int]) -> None: + label, elements, truncated = node[2], node[3], node[4] + if not elements: + out.append((_CYAN, "%s[]" % label)) + return + + out.append((_CYAN, "%s[" % label)) + out.append(("", "\n")) + for index, element in enumerate(elements): + _indent(level + 1, out) + _append(element, level + 1, out, blob) + if _has_more(index, elements, truncated): + out.append(("", ",")) + out.append(("", "\n")) + if truncated: + _indent(level + 1, out) + out.append((_ORANGE, "… more")) + out.append(("", "\n")) + _indent(level, out) + out.append((_CYAN, "]")) + + def _capped(items: Sequence[Any]) -> Tuple[Sequence[Any], int]: shown = items[:_MAX_ITEMS] return shown, len(items) - len(shown)