mirror of
https://github.com/radareorg/r2hexpat.git
synced 2026-11-09 04:19:35 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4a5fbfc7f | ||
|
|
d7635ca49e | ||
|
|
f458d64f0b | ||
|
|
ed006e6b4b | ||
|
|
368c353b55 | ||
|
|
ca12911f74 |
No files matched your search
@@ -24,13 +24,13 @@ all: $(OUT)
|
||||
$(OUT): r2/plugin.ts r2/r2.d.ts $(LIB)
|
||||
$(BUNDLE) r2/plugin.ts
|
||||
|
||||
tests/unit.js: tests/unit.ts $(LIB)
|
||||
tests/unit.js: tests/unit.ts tests/search_unit.ts tests/magic_unit.ts tests/tree_unit.ts $(LIB)
|
||||
$(BUNDLE) tests/unit.ts
|
||||
|
||||
tests/testlib.js: tests/testlib.ts tests/upstream_hooks.ts $(LIB)
|
||||
$(BUNDLE) tests/testlib.ts
|
||||
|
||||
tests/upstream.r2.js: tests/upstream_r2.ts tests/upstream_hooks.ts $(LIB)
|
||||
tests/upstream.r2.js: tests/upstream_r2.ts tests/upstream_hooks.ts tests/search_unit.ts tests/magic_unit.ts tests/tree_unit.ts $(LIB)
|
||||
$(BUNDLE) tests/upstream_r2.ts
|
||||
|
||||
user-install: $(OUT)
|
||||
|
||||
@@ -31,9 +31,13 @@ Commands (all output goes through r2's console, so `~grep` and `|` work):
|
||||
| `hexpat- [n]` | unload all loaded files (or the nth) |
|
||||
| `hexpat-v` | show the r2hexpat version |
|
||||
| `hexpatj [file]` | patterns as JSON (`hexpatj~{}` to indent) |
|
||||
| `hexpat* [file]` | patterns as r2 commands: flags in the `hexpat` flagspace, `Cd`/`Cs` data hints and value comments (`.hexpat*` applies them) |
|
||||
| `hexpat* [file]` | patterns as r2 commands: flags in the `hexpat` flagspace, flag colors/comments, `Cd`/`Cs` data hints and value comments (`.hexpat*` applies them) |
|
||||
| `hexpat-*` | r2 commands to remove generated flags (`.hexpat-*` applies them) |
|
||||
| `hexpatl[j]` | list loaded files and their top-level patterns |
|
||||
| `hexpate expr` | evaluate an expression in the context of the last loaded file (`hexpate sizeof(header)`, `hexpate $[0x10]`) |
|
||||
| `hexpate expr` | evaluate an expression or assignment in the last loaded file (`hexpate sizeof(header)`, `hexpate header.value = 3`) |
|
||||
| `hexpatp[?+=-ijl] ...` | build partial instances and masked search patterns (`hexpatp?` for help) |
|
||||
| `hexpatm [options] Type` | print a libmagic header decoder (`hexpatm?` for help) |
|
||||
| `hexpatt[j*] [options] [Type [address]\|pattern]` | view a type at an address or a loaded pattern as a tree, JSON or r2 commands (`hexpatt?` for help) |
|
||||
| `hexpats [file]` | one-line summary (types with their field count, enums, functions, patterns, size) or the error, meant for status bars |
|
||||
| `hexpat?` | help |
|
||||
|
||||
@@ -44,6 +48,165 @@ change and show `hexpats base64:...` in a status bar for live feedback.
|
||||
Addresses are r2 addresses: pattern offsets are translated by the binary's
|
||||
base address (`ij.bin.baddr`), or `#pragma base_address 0x...`.
|
||||
|
||||
`hexpate header.value = 3` writes the field's bytes through r2 I/O, using its
|
||||
declared size and byte order. Compound assignments (`+=`, `|=`, etc.), array
|
||||
entries and bitfield members also work. Open r2 with `-w`, or enable
|
||||
`e io.cache=true` to stage edits in r2's write cache. Assignments print nothing;
|
||||
query the field again or use `p8` to inspect the result. Editing a count or
|
||||
pointer does not rebuild the pattern layout; reload the pattern file when
|
||||
its layout changes. Fields with `[[transform(...)]]` cannot be written because
|
||||
the transform has no inverse.
|
||||
Quote the whole r2 command when it contains operators that r2 handles itself,
|
||||
for example `"hexpate header.value |= 8"` or `"hexpate header.value >= 3"`.
|
||||
|
||||
## Address-bound trees and flag colors
|
||||
|
||||
`hexpatt` displays a type's fields as a tree with addresses, values, sizes
|
||||
and evaluated attributes, including `color`, `comment` and `name`:
|
||||
|
||||
```text
|
||||
hexpatt -f types.hexpat Header @ 0x100
|
||||
hexpat -q types.hexpat
|
||||
hexpatt Header 0x100
|
||||
hexpatt header
|
||||
hexpatt header.inner
|
||||
hexpatt
|
||||
```
|
||||
|
||||
A type uses the current r2 seek when no address is given; an explicit address
|
||||
can be an r2 expression or flag name. `-f file` supplies types without loading
|
||||
the file's placements, executing its top-level statements or calling `main()`.
|
||||
It accepts `base64:` sources and `-I dir` for includes. Without `-f`, types come
|
||||
from the last loaded file. Type decoding follows ordinary hexpat declarations,
|
||||
including data-dependent arrays and pointers, and honors the binary base
|
||||
address or `#pragma base_address`. These one-shot views preserve loaded files
|
||||
and search instances. A loaded pattern name or nested path shows its existing
|
||||
tree; no arguments shows all loaded patterns.
|
||||
|
||||
Rows display their color attributes even with `e scr.color=0`; enabling colors
|
||||
also colors the rows. Enclosing aggregate colors apply to members that have no
|
||||
explicit color of their own. `hexpattj` and `hexpatj` expose `color`, `comment`,
|
||||
`displayName` and evaluated `attributes` in JSON.
|
||||
|
||||
`hexpat*` and `hexpatt*` emit `fc flag=rgb:RRGGBB` for colored flags and `fC`
|
||||
for pattern comments. The runtime's `std::core::set_pattern_color` is supported
|
||||
too. Apply a type's flags directly with `.hexpatt* -f types.hexpat Header @ 0x100`.
|
||||
|
||||
`hexpat-*` prints a removal script; `.hexpat-*` applies it. This removes flags
|
||||
matching `hexpat.*` in the `hexpat` flagspace, including their flag metadata,
|
||||
and restores the previous flagspace. It works after the patterns are unloaded
|
||||
and preserves other flags, loaded files and search instances. Address comments
|
||||
and data hints remain. `hexpat-` (or `hexpat- *`, with a space) still unloads
|
||||
the pattern files.
|
||||
|
||||
## Search patterns
|
||||
|
||||
`hexpatp` instantiates a type without a backing address. All fields start
|
||||
**unset**. Set only the fields you know; the rest, including padding, become
|
||||
wildcards. Its normal output is a single `bytes:mask` argument for r2's `/x`,
|
||||
including bit masks for individual bitfield members:
|
||||
|
||||
```sh
|
||||
r2 -q -i hexpat.r2.js \
|
||||
-c '/x `hexpatp -f demo/player.hexpat -a 8 Player health=100 lives=3`' target.bin
|
||||
```
|
||||
|
||||
`-f` compiles types and constant globals from a file, resolving includes with
|
||||
`-I dir`. It does not decode placements or execute the file's top-level code
|
||||
or `main()`. Without `-f`, the last file loaded with `hexpat` supplies the types.
|
||||
The file can also be `base64:<data>`. One-shot patterns do not create a named
|
||||
instance or change r2 I/O.
|
||||
|
||||
For interactive use, create a named instance and refine it:
|
||||
|
||||
```text
|
||||
hexpatp+ -f demo/player.hexpat -a 8 wanted Player
|
||||
hexpatpi wanted
|
||||
hexpatp= wanted health=100 lives=3 role=Role::Rogue position.z=3
|
||||
hexpatp wanted
|
||||
/x `hexpatp wanted`
|
||||
hexpatp- wanted role position
|
||||
hexpatpi wanted
|
||||
hexpatpj wanted
|
||||
hexpatpl
|
||||
hexpatp- wanted
|
||||
```
|
||||
|
||||
| command | result |
|
||||
| --- | --- |
|
||||
| `hexpatp [options] Type [field=expr ...]` | one-shot `/x` pattern |
|
||||
| `hexpatp name` | named instance's `/x` pattern |
|
||||
| `hexpatp+ [options] name Type [field=expr ...]` | create a named instance |
|
||||
| `hexpatp= name field=expr ...` | set fields atomically |
|
||||
| `hexpatp- name field ...` | unset fields or entire nested aggregates |
|
||||
| `hexpatp- name` / `hexpatp- *` | delete one / all named instances |
|
||||
| `hexpatpi name` / `hexpatpj name` | inspect fields as text / JSON |
|
||||
| `hexpatpl` / `hexpatplj` | list named instances as text / JSON |
|
||||
|
||||
Options go before the type or instance name: `-f file`, `-I dir`, and
|
||||
`-a N` (also `--align N`). Nested paths and constant array indices work, e.g.
|
||||
`position.z=3` and `values[2]=0x1234`. Values are hexpat expressions, including
|
||||
enum names, arithmetic, booleans, and quoted strings with spaces. Multiple
|
||||
assignments can be separated by whitespace or commas. A zero value is **set**;
|
||||
it constrains zero bytes with `ff` mask bytes. An unset field contributes no
|
||||
mask bits. JSON reports `defined` for each leaf and `unset`, `partial` or `set`
|
||||
for the instance. Overlapping union fields are independent constraints;
|
||||
conflicting values are rejected, and unsetting one preserves the others.
|
||||
|
||||
Quote the whole r2 command when it contains r2 operators or template brackets,
|
||||
for example `"hexpatp -f types.hexpat Array<u16,2> values[1]=1|2"`.
|
||||
|
||||
The default layout is packed (`-a 1`). `-a 2`, `4`, `8` or `16` aligns each
|
||||
member to its natural alignment, capped at that number, and includes struct
|
||||
tail padding in array strides. Power-of-two scalar widths use their width
|
||||
as their natural alignment; other widths and ImHex bitfields align to one
|
||||
byte. `[[packed]]` on a type and `[[align(N)]]` on a member override this policy.
|
||||
Explicit `padding[N]` and aggregate `[[fixed_size(N)]]` remain wildcarded.
|
||||
Pointer widths stay explicit (`Player *next : u64;` in the example).
|
||||
|
||||
This is an explicit layout policy: it does not infer a C ABI or implement C
|
||||
bitfield packing. Files with a non-packed `abi` pragma require an explicit `-a` selection.
|
||||
Choose the layout that matches the target; r2's `search.align` separately
|
||||
controls candidate addresses (e.g. `e search.align=8`). For background, see
|
||||
[Frida's packed and native layout example](https://frida.re/news/2026/10/02/frida-17-20-0-released/).
|
||||
|
||||
Search types must have fixed, contiguous layouts. Arrays sized by an unset
|
||||
field, variable-length arrays, conditionals, address-dependent layouts, and
|
||||
transformed values are rejected instead of guessing their layout. Setting a
|
||||
length field does not make a dynamic type static. `hexpat` and `hexpate` still
|
||||
decode and modify ordinary placed patterns as before.
|
||||
|
||||
## Magic header decoders
|
||||
|
||||
`hexpatm` exports a fixed type as a text magic file for r2's `pm` or `file -m`:
|
||||
|
||||
```text
|
||||
hexpatm -f demo/test.hexpat MachHeader > /tmp/macho.magic
|
||||
pm /tmp/macho.magic @ 0
|
||||
```
|
||||
|
||||
Without `-f`, it uses the types from the last file loaded with `hexpat`.
|
||||
It accepts the same `-f file`, `-I dir`, `-a N` / `--align N` options as
|
||||
`hexpatp`, including `base64:` sources, and compiles types without decoding
|
||||
placements or running top-level statements or `main()`. Exporting leaves
|
||||
the loaded files and search instances intact.
|
||||
|
||||
The decoder prints nested field paths, fixed array entries and all union
|
||||
views at their declared offsets and byte order. Integers, enums, booleans,
|
||||
`char16` scalars and pointer addresses print their raw hexadecimal bits;
|
||||
`float` and `double` print numeric values. Fixed `char` arrays and `char`
|
||||
scalars print bounded strings; `char16` arrays use libmagic's string16 types.
|
||||
Padding is omitted. Each field uses an unconditional `x` test: this decodes
|
||||
the selected header type rather than identifying its file format by a
|
||||
signature. The first rule checks that the whole fixed header is available.
|
||||
Increase r2's block size (`b N`) when the header exceeds the current block.
|
||||
|
||||
Dynamic layouts, transformed fields, bitfields, integer/pointer widths other
|
||||
than 1, 2, 4 or 8 bytes, and `float16` are rejected. Use ordinary `hexpat`
|
||||
evaluation for those types. Library callers can export the same decoder
|
||||
with `new SearchPattern(program, "MachHeader").toMagic()`; any assigned
|
||||
search values are ignored.
|
||||
|
||||
## Library
|
||||
|
||||
The library in `lib/` never talks to radare2. Embedders implement
|
||||
@@ -63,11 +226,50 @@ inst.dump();
|
||||
```
|
||||
|
||||
`toJson(inst)` serializes the placed patterns, `inst.evaluateExpression(src)`
|
||||
evaluates an expression in the global scope of an evaluated pattern and
|
||||
evaluates an expression or assignment in the global scope of an evaluated pattern and
|
||||
`BufferHost` is a ready-made host over a `Uint8Array`. `HostHooks` adds
|
||||
functions, types, pragma handlers and `in` variables, like the C++ runtime's
|
||||
`addFunction`/`addType`/`addPragma`.
|
||||
|
||||
`decodeType(program, "Header", host, offset)` decodes one type at a host-relative
|
||||
offset after preparing types and constants only. Its root pattern is named
|
||||
`value` (with underscores added if a constant already uses that name).
|
||||
`dumpTree(inst, print, { base, color, maxEntries })` renders a tree
|
||||
with metadata; `patterns` can select specific roots.
|
||||
|
||||
Hosts can implement `write(addr, bytes)` for interactive field assignments;
|
||||
read-only hosts reject those writes. Assignments while evaluating a pattern
|
||||
file retain ImHex's value overrides and do not write host data.
|
||||
|
||||
`SearchPattern` exposes the same partial instances without r2:
|
||||
|
||||
```ts
|
||||
import { compile, SearchPattern } from "./lib/index";
|
||||
|
||||
const program = compile(source); // optional host resolves includes
|
||||
const wanted = new SearchPattern(program, "Player", { alignment: 8 });
|
||||
wanted.set({ health: 100, lives: 3, position: { z: 3 } });
|
||||
wanted.isDefined("role"); // false
|
||||
wanted.get("role"); // undefined
|
||||
wanted.set("health", 0); // a defined zero
|
||||
wanted.assign("role=Role::Rogue");
|
||||
wanted.unset("position"); // clear all position fields
|
||||
wanted.state(); // "partial"
|
||||
const { bytes, mask } = wanted.encode(); // independent Uint8Array copies
|
||||
console.log(wanted.toR2());
|
||||
console.log(JSON.stringify(wanted));
|
||||
```
|
||||
|
||||
`set()` merges partial objects and arrays, or sets a single field path;
|
||||
`unset()` with no argument clears the whole instance. `state(path)` reports
|
||||
aggregate state, `fields()` returns field offsets and definition status, and
|
||||
`get(path)` returns `undefined` for unset leaves. Integer values are decoded
|
||||
as `bigint`; supply `bigint` for values beyond JavaScript's safe integer range.
|
||||
A pointer set to `null` is a defined zero pointer. Strings fill their fixed
|
||||
field and pad with zero bytes. `SearchOptions` also accepts `maxSize` (default
|
||||
1 MiB) and `maxFields` (default 65536). The library reads no target bytes while
|
||||
creating or editing a search instance.
|
||||
|
||||
## Test
|
||||
|
||||
make check # tsc type-check (lint)
|
||||
|
||||
+2
-1
@@ -23,7 +23,8 @@ tests and the ImHex-Patterns corpus (`make test-corpus`: 358/358 files parse,
|
||||
- [x] Built-in std: `assert`, `print`, `format` (fmt specs), `std::mem` (sections, reads), `std::core`, `std::string`, `std::math`
|
||||
- [x] Semantic errors: redeclarations, const assignment, division by zero, bad string ops, arity, undefined functions
|
||||
- [x] Host-independent library (`HexpatHost`), r2 plugin in `r2/`, paged reads (4K `p8`)
|
||||
- [x] r2 commands: `hexpatj` (JSON), `hexpat*` (flags, data hints, comments), `hexpatl`, `hexpate` (expressions), `hexpat+`/`hexpat-` (several loaded files), `hexpat?`
|
||||
- [x] r2 commands: `hexpatj` (JSON), `hexpat*` (flags, data hints, comments), `hexpatl`, `hexpate` (expressions and assignments through r2 I/O), `hexpat+`/`hexpat-` (several loaded files), `hexpat?`
|
||||
- [x] Partial type instances: `hexpatp` CLI and `SearchPattern` API, set/unset fields, nested and array paths, masked `/x` searches, explicit alignment and wildcard padding
|
||||
|
||||
## 🚀 Next
|
||||
- [x] The real ImHex std/type libraries load from an ImHex-Patterns checkout (`hexpat -I .../includes`); all `builtin::std` functions they use exist except `std::file` (no filesystem access)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Packed by default; hexpatp --align 8 selects natural alignment capped at 8.
|
||||
enum Role : u8 { Warrior, Mage, Rogue };
|
||||
struct Vec3 { float x; float y; float z; };
|
||||
struct Player {
|
||||
u32 health;
|
||||
u32 lives;
|
||||
Role role;
|
||||
Vec3 position;
|
||||
Player *next : u64;
|
||||
};
|
||||
+235
-35
@@ -11,7 +11,7 @@ import * as A from "./ast";
|
||||
import { uintN, intN } from "./bigint";
|
||||
import { Chr } from "./ast";
|
||||
import { PatternError } from "./lexer";
|
||||
import { parseExpression } from "./parser";
|
||||
import { parseEvaluation, parseType } from "./parser";
|
||||
import { Pattern, PKind, EnumInfo, BitMode, MAIN_SECTION, HEAP_SECTION } from "./patterns";
|
||||
import { Memory, BufferMemory, SparseMemory } from "./memory";
|
||||
import { callBuiltin, formatString } from "./stdlib";
|
||||
@@ -61,6 +61,13 @@ export interface HostHooks {
|
||||
inVariables?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface LayoutOptions {
|
||||
/** Maximum natural member alignment: 1 (packed), 2, 4, 8 or 16. */
|
||||
alignment?: number;
|
||||
/** Maximum size of an independently instantiated layout (default: 1 MiB). */
|
||||
maxSize?: number;
|
||||
}
|
||||
|
||||
const INT_TYPES: Record<string, number> = {
|
||||
u8: 1, u16: 2, u24: 3, u32: 4, u48: 6, u64: 8, u96: 12, u128: 16,
|
||||
s8: 1, s16: 2, s24: 3, s32: 4, s48: 6, s64: 8, s96: 12, s128: 16,
|
||||
@@ -101,6 +108,10 @@ export class PatternInstance {
|
||||
private importsRegistered = new Set<A.ImportedDecl>();
|
||||
private patternCount = 0;
|
||||
private printFn: (s: string) => void;
|
||||
private writeMain = false; // interactive assignments write through; pattern evaluation keeps ImHex overrides
|
||||
private layoutOnly = false;
|
||||
private layoutFields = new WeakSet<Pattern>();
|
||||
private alignmentStack = [1];
|
||||
|
||||
constructor(private program: A.Program, private data: Memory, private host: HostHooks = {}, print?: (s: string) => void) {
|
||||
this.sections.set(MAIN_SECTION, data);
|
||||
@@ -113,11 +124,7 @@ export class PatternInstance {
|
||||
|
||||
// ------------------------------------------------------------------ setup
|
||||
eval(): void {
|
||||
for (const [k, v] of this.program.pragmas) this.pragma(k, v);
|
||||
this.register(this.program.body);
|
||||
const root = new Scope();
|
||||
this.global = { scope: root, root, kind: "global", ns: [], binds: new Map() };
|
||||
this.frames = [this.global];
|
||||
this.initialize();
|
||||
try {
|
||||
this.execBlock(this.program.body, this.global);
|
||||
} catch (e) {
|
||||
@@ -131,6 +138,66 @@ export class PatternInstance {
|
||||
if (main) this.callFn(main, [], main.loc);
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
for (const [k, v] of this.program.pragmas) this.pragma(k, v);
|
||||
this.register(this.program.body);
|
||||
const root = new Scope();
|
||||
this.global = { scope: root, root, kind: "global", ns: [], binds: new Map() };
|
||||
this.frames = [this.global];
|
||||
}
|
||||
|
||||
/** Load types and constant globals without decoding placements or running main(). */
|
||||
prepareTypes(): void {
|
||||
this.initialize();
|
||||
const constants = (body: A.Stmt[]): A.Stmt[] => body.flatMap((s): A.Stmt[] => {
|
||||
if (s.s === "decl") return s.isConst && !s.placement && s.init ? [s] : [];
|
||||
if (s.s === "multi") return constants(s.decls);
|
||||
if (s.s === "nsctx") return [{ ...s, body: constants(s.body) }];
|
||||
return [];
|
||||
});
|
||||
this.execBlock(constants(this.program.body), this.global);
|
||||
}
|
||||
|
||||
/** Decode a type against the input at an offset, without running top-level code. */
|
||||
placeType(type: string, offset = 0, name?: string): Pattern {
|
||||
if (!this.global) this.prepareTypes();
|
||||
if (!Number.isSafeInteger(offset) || offset < 0 || offset > this.data.size())
|
||||
this.error("type offset is outside the data");
|
||||
this.frames = [this.global];
|
||||
this.cursor = offset;
|
||||
if (name === undefined) {
|
||||
name = "value";
|
||||
while (this.global.scope.vars.has(name)) name += "_";
|
||||
}
|
||||
const p = this.create(this.resolveType(parseType(type, this.types.keys())), name);
|
||||
this.declare(name, { v: p }, this.global.scope, this.loc);
|
||||
this.patterns.push(p);
|
||||
this.variables.set(name, p);
|
||||
return p;
|
||||
}
|
||||
|
||||
/** Build a fixed layout at offset zero, without reading any instance data. */
|
||||
instantiateType(type: string, options: LayoutOptions = {}): Pattern {
|
||||
const alignment = options.alignment === undefined ? 1 : options.alignment;
|
||||
if (![1, 2, 4, 8, 16].includes(alignment)) this.error("alignment must be 1, 2, 4, 8 or 16");
|
||||
this.layoutOnly = true;
|
||||
this.alignmentStack = [alignment];
|
||||
this.cursor = 0;
|
||||
try {
|
||||
const p = this.create(this.resolveType(parseType(type, this.types.keys())), "");
|
||||
const max = options.maxSize === undefined ? 0x100000 : options.maxSize;
|
||||
if (!Number.isSafeInteger(p.size) || p.size < 0 || p.size > max) this.error("layout exceeds maximum size " + max);
|
||||
return p;
|
||||
} finally { this.layoutOnly = false; this.alignmentStack = [1]; }
|
||||
}
|
||||
|
||||
/** Encode a field using the same conversions and byte order as interactive writes. */
|
||||
writePattern(p: Pattern, value: any): void {
|
||||
const save = this.writeMain;
|
||||
this.writeMain = true;
|
||||
try { this.assignPattern(p, value); } finally { this.writeMain = save; }
|
||||
}
|
||||
|
||||
private pragma(key: string, value: string): void {
|
||||
const h = this.host.pragmas && this.host.pragmas[key];
|
||||
if (h && !h(value)) this.error("invalid value for pragma '" + key + "': '" + value + "'", { line: 0, col: 0, src: "" });
|
||||
@@ -342,6 +409,10 @@ export class PatternInstance {
|
||||
p.size = size;
|
||||
p.be = be;
|
||||
p.local = this.section === HEAP_SECTION;
|
||||
if (this.layoutOnly) {
|
||||
this.layoutFields.add(p);
|
||||
p.alignment = size > 0 && (size & (size - 1)) === 0 ? Math.min(size, this.alignmentStack[this.alignmentStack.length - 1]) : 1;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -360,6 +431,7 @@ export class PatternInstance {
|
||||
const d = c.decl;
|
||||
if (d.s === "enum") {
|
||||
const ut = this.resolveInFrame(d.underlying, d.ns);
|
||||
if (this.layoutOnly && (ut.t !== "builtin" || !(ut.name in INT_TYPES))) this.error("search enums require integer storage");
|
||||
if (ut.t === "builtin") {
|
||||
p = this.newPattern("enum", c.display, ut.size, this.endian(ut));
|
||||
if (ut.name[0] === "s") p.fieldKind = "signed";
|
||||
@@ -437,6 +509,7 @@ export class PatternInstance {
|
||||
p.parent = this.curFrame.pattern;
|
||||
const start = this.cursor;
|
||||
const fr = this.pushFrame("struct", p, d.ns, c.binds);
|
||||
if (this.layoutOnly) this.alignmentStack.push(this.attrOf(d.attrs, "packed") || this.attrOf(c.attrs, "packed") ? 1 : this.alignmentStack[this.alignmentStack.length - 1]);
|
||||
fr.placeBase = placeBase;
|
||||
try {
|
||||
const run = (decl: A.StructDecl, binds: Map<string, any>) => {
|
||||
@@ -466,7 +539,8 @@ export class PatternInstance {
|
||||
if (!p.children) p.children = [];
|
||||
// type attributes see the struct's members (`[[name(name), color(...)]]`)
|
||||
this.applyTypeAttrs(p, c);
|
||||
} finally { this.frames.pop(); }
|
||||
if (this.layoutOnly) p.size = Math.ceil(p.size / p.alignment) * p.alignment;
|
||||
} finally { this.frames.pop(); if (this.layoutOnly) this.alignmentStack.pop(); }
|
||||
this.cursor = start + p.size;
|
||||
return p;
|
||||
}
|
||||
@@ -535,6 +609,10 @@ export class PatternInstance {
|
||||
/** Apply `[[attributes]]` to a created pattern. */
|
||||
applyAttrs(p: Pattern, attrs: A.Attr[], typeLevel = false): void {
|
||||
for (const a of attrs) {
|
||||
if (this.layoutOnly) {
|
||||
if (a.name === "transform") this.error("search layouts require raw fields; transforms are not supported");
|
||||
if (!["fixed_size", "align", "packed", "no_unique_address", "bitfield_order", "left_to_right", "right_to_left"].includes(a.name)) continue;
|
||||
}
|
||||
let args: any[];
|
||||
if (a.name === "fixed_size" || a.name === "transform" || a.name === "format" || a.name === "format_read") {
|
||||
args = a.args.map((e) => this.evalExpr(e));
|
||||
@@ -545,6 +623,13 @@ export class PatternInstance {
|
||||
if (!p.attrs) p.attrs = [];
|
||||
p.attrs.push({ name: a.name, args });
|
||||
switch (a.name) {
|
||||
case "align":
|
||||
if (this.layoutOnly) {
|
||||
const n = Number(this.toInt(args[0]));
|
||||
if (![1, 2, 4, 8, 16].includes(n)) this.error("field alignment must be 1, 2, 4, 8 or 16");
|
||||
p.alignment = n;
|
||||
}
|
||||
break;
|
||||
case "hidden": p.hidden = true; break;
|
||||
case "sealed": p.sealed = true; break;
|
||||
case "format": case "format_read": p.formatFn = this.toStr(args[0]); break;
|
||||
@@ -554,7 +639,8 @@ export class PatternInstance {
|
||||
case "name": if (args[0] !== undefined) p.displayName = this.toStr(this.decay(args[0])); break;
|
||||
case "fixed_size": {
|
||||
const n = Number(this.toInt(args[0]));
|
||||
if (!typeLevel && p.size > n) this.error("pattern of size " + p.size + " exceeds fixed_size(" + n + ")");
|
||||
if ((!typeLevel || this.layoutOnly) && p.size > n) this.error("pattern of size " + p.size + " exceeds fixed_size(" + n + ")");
|
||||
if (this.layoutOnly && !p.isComposite() && p.size !== n) this.error("use explicit padding after scalar fields instead of fixed_size");
|
||||
p.size = n;
|
||||
this.cursor = p.offset + n;
|
||||
break;
|
||||
@@ -593,10 +679,24 @@ export class PatternInstance {
|
||||
} else {
|
||||
p = this.createDecl(d);
|
||||
}
|
||||
if (this.layoutOnly && fr.kind === "struct") {
|
||||
const offset = parent.offset + Math.ceil((start - parent.offset) / p.alignment) * p.alignment;
|
||||
this.relocateLayout(p, offset - start);
|
||||
this.cursor = offset + p.size;
|
||||
parent.alignment = Math.max(parent.alignment, p.alignment);
|
||||
}
|
||||
if (this.attrOf(d.attrs, "no_unique_address")) this.cursor = start;
|
||||
this.addMember(fr, p, d);
|
||||
}
|
||||
|
||||
private relocateLayout(p: Pattern, delta: number, modes = new Set<BitMode>()): void {
|
||||
p.offset += delta;
|
||||
if (p.bitMode && !modes.has(p.bitMode)) { modes.add(p.bitMode); p.bitMode.root += delta; }
|
||||
for (const c of p.children || []) this.relocateLayout(c, delta, modes);
|
||||
if (p.proto) this.relocateLayout(p.proto, delta, modes);
|
||||
if (p.entries) for (const e of p.entries.values()) this.relocateLayout(e, delta, modes);
|
||||
}
|
||||
|
||||
private addMember(fr: Frame, p: Pattern, d: A.DeclStmt): void {
|
||||
const parent = fr.pattern!;
|
||||
p.parent = parent;
|
||||
@@ -744,6 +844,7 @@ export class PatternInstance {
|
||||
const ptr = this.newPattern("pointer", "", sizeType.size, this.endian(sizeType));
|
||||
ptr.fieldKind = sizeType.name[0] === "s" ? "signed" : "unsigned";
|
||||
this.cursor += sizeType.size;
|
||||
if (this.layoutOnly) { ptr.typeName = this.typeDisplay(c) + "*"; return ptr; }
|
||||
const end = this.cursor;
|
||||
let addr = this.toInt(this.readScalar(ptr, ptr.fieldKind === "signed" ? "signed" : "unsigned"));
|
||||
const pb = this.attrOf(d.attrs, "pointer_base");
|
||||
@@ -781,6 +882,7 @@ export class PatternInstance {
|
||||
const kindFor = (): PKind => (elemName === "char" ? "string" : elemName === "char16" ? "wstring" : elemName === "padding" ? "padding" : "array");
|
||||
const arr = this.newPattern(kindFor(), "", 0, proto ? proto.be : this.endian(c));
|
||||
arr.parent = this.curFrame.pattern;
|
||||
if (this.layoutOnly && proto) arr.alignment = proto.alignment;
|
||||
arr.children = undefined;
|
||||
const elemType = this.typeDisplay(c);
|
||||
let count = 0;
|
||||
@@ -828,6 +930,7 @@ export class PatternInstance {
|
||||
let e: Pattern;
|
||||
try { e = this.create(c, "[" + arr.children!.length + "]"); } finally { this.arrayIndex = saveIdx; }
|
||||
e.parent = arr;
|
||||
if (this.layoutOnly) arr.alignment = Math.max(arr.alignment, e.alignment);
|
||||
arr.children!.push(e);
|
||||
// `break` in an entry keeps it and ends the array, `continue` drops it
|
||||
const ctrl = this.pendingCtrl;
|
||||
@@ -879,6 +982,7 @@ export class PatternInstance {
|
||||
if (!d.name) { bit.pos += width; return; } // padding
|
||||
const c = this.resolveType(d.type);
|
||||
const p = new Pattern("bitfield_field", "");
|
||||
if (this.layoutOnly) this.layoutFields.add(p);
|
||||
p.section = this.section;
|
||||
p.local = this.section === HEAP_SECTION;
|
||||
p.bitMode = bit.mode;
|
||||
@@ -965,6 +1069,11 @@ export class PatternInstance {
|
||||
|
||||
/** The value of a pattern (decays scalars; composites stay patterns). */
|
||||
value(p: Pattern, raw = false): any {
|
||||
if (this.layoutOnly) {
|
||||
for (let q: Pattern | undefined = p; q; q = q.parent) {
|
||||
if (this.layoutFields.has(q)) this.error("layout depends on unset field '" + (p.name || p.typeName) + "'");
|
||||
}
|
||||
}
|
||||
if (!raw && p.transformFn) {
|
||||
const fn = this.findFn(p.transformFn);
|
||||
if (!fn) this.error("transform function '" + p.transformFn + "' not found");
|
||||
@@ -1014,9 +1123,63 @@ export class PatternInstance {
|
||||
return b;
|
||||
}
|
||||
|
||||
/** Patch a bitfield member without changing its neighbors. */
|
||||
private writeBits(p: Pattern, v: bigint): void {
|
||||
const m = p.bitMode!, mem = this.mem(p.section);
|
||||
if (m.order && m.order.size) {
|
||||
const nbytes = Math.ceil(m.order.size / 8);
|
||||
const bytes = mem.read(m.root, nbytes);
|
||||
let raw = 0n;
|
||||
for (let i = 0; i < nbytes; i++) raw |= BigInt(bytes[i]) << BigInt(8 * (m.be ? nbytes - 1 - i : i));
|
||||
const shift = m.order.lsbFirst ? p.bitPos : m.order.size - p.bitPos - p.bits;
|
||||
if (shift < 0 || shift + p.bits > m.order.size) this.error("bitfield member outside its storage");
|
||||
const mask = ((1n << BigInt(p.bits)) - 1n) << BigInt(shift);
|
||||
raw = (raw & ~mask) | (uintN(p.bits, v) << BigInt(shift));
|
||||
mem.write!(m.root, this.encodeInt(raw, nbytes, m.be));
|
||||
return;
|
||||
}
|
||||
const first = Math.floor(p.bitPos / 8);
|
||||
const bytes = mem.read(m.root + first, Math.ceil(((p.bitPos % 8) + p.bits) / 8));
|
||||
const lsbFirst = m.order ? m.order.lsbFirst : !m.be;
|
||||
for (let i = 0; i < p.bits; i++) {
|
||||
const k = (p.bitPos % 8) + i;
|
||||
const bit = lsbFirst ? k & 7 : 7 - (k & 7);
|
||||
const value = Number((v >> BigInt(lsbFirst ? i : p.bits - 1 - i)) & 1n);
|
||||
bytes[k >> 3] = (bytes[k >> 3] & ~(1 << bit)) | (value << bit);
|
||||
}
|
||||
mem.write!(m.root + first, bytes);
|
||||
}
|
||||
|
||||
private encodeFloat(v: number, size: number, be: boolean): Uint8Array {
|
||||
if (size === 2) {
|
||||
const sign = v < 0 || Object.is(v, -0) ? 0x8000 : 0;
|
||||
const a = Math.abs(v);
|
||||
let h: number;
|
||||
const roundEven = (n: number): number => {
|
||||
const i = Math.floor(n), f = n - i;
|
||||
return i + (f > 0.5 || (f === 0.5 && (i & 1)) ? 1 : 0);
|
||||
};
|
||||
let e = Math.floor(Math.log2(a));
|
||||
if (Number.isNaN(a)) h = 0x7e00;
|
||||
else if (a === Infinity || e > 15) h = 0x7c00;
|
||||
else if (e < -14) h = roundEven(a * Math.pow(2, 24));
|
||||
else {
|
||||
let n = roundEven(a / Math.pow(2, e - 10));
|
||||
if (n === 2048) { n = 1024; e++; }
|
||||
h = e > 15 ? 0x7c00 : ((e + 15) << 10) | (n - 1024);
|
||||
}
|
||||
return this.encodeInt(BigInt(sign | h), 2, be);
|
||||
}
|
||||
const dv = new DataView(new ArrayBuffer(size));
|
||||
if (size === 4) dv.setFloat32(0, v, !be); else dv.setFloat64(0, v, !be);
|
||||
return new Uint8Array(dv.buffer);
|
||||
}
|
||||
|
||||
/** Write a value into a pattern (local variables, members, array entries). */
|
||||
assignPattern(p: Pattern, v: any): void {
|
||||
if (v === null || v === undefined) return;
|
||||
const mainWrite = this.writeMain && p.section === MAIN_SECTION && !p.loose;
|
||||
if (mainWrite && (p.transformFn || p.inner)) this.error("cannot write a transformed field");
|
||||
if (v instanceof Array) {
|
||||
if (p.kind !== "array" && p.kind !== "string") this.error("cannot assign a list to a non-array variable");
|
||||
if (p.kind === "string") { this.assignPattern(p, v.map((x) => this.toStr(x)).join("")); return; }
|
||||
@@ -1039,27 +1202,36 @@ export class PatternInstance {
|
||||
if (v instanceof Pattern) return;
|
||||
this.error("cannot assign a " + this.typeOfValue(v) + " to a " + p.typeName);
|
||||
}
|
||||
const writable = p.section !== MAIN_SECTION && !p.loose && (this.mem(p.section) as any).write;
|
||||
const mem = this.mem(p.section);
|
||||
if (mainWrite && !mem.write) this.error("host data is read-only");
|
||||
const writable = (p.section !== MAIN_SECTION || mainWrite) && !p.loose && mem.write;
|
||||
if (p.kind === "string" || p.kind === "wstring") {
|
||||
if (writable && !p.hasOverride) {
|
||||
if (writable && (!p.hasOverride || mainWrite)) {
|
||||
const s = this.toStr(v);
|
||||
const b = new Uint8Array(p.size);
|
||||
for (let i = 0; i < s.length && i < p.size; i++) b[i] = s.charCodeAt(i) & 0xff;
|
||||
this.mem(p.section).write!(p.offset, b);
|
||||
const width = p.kind === "wstring" ? 2 : 1;
|
||||
for (let i = 0; i < s.length && (i + 1) * width <= p.size; i++) {
|
||||
const c = s.charCodeAt(i);
|
||||
if (width === 1) b[i] = c & 0xff;
|
||||
else { b[2 * i] = p.be ? c >> 8 : c & 0xff; b[2 * i + 1] = p.be ? c & 0xff : c >> 8; }
|
||||
}
|
||||
mem.write!(p.offset, b);
|
||||
p.hasOverride = false;
|
||||
} else { p.hasOverride = true; p.override = this.toStr(v); }
|
||||
return;
|
||||
}
|
||||
const conv = this.convertFor(p, v);
|
||||
if (p.loose) { p.hasOverride = true; p.override = v instanceof Chr || typeof v !== "bigint" ? v : v; return; }
|
||||
if (mainWrite && p.kind === "bitfield_field") {
|
||||
this.writeBits(p, this.toInt(conv));
|
||||
p.hasOverride = false;
|
||||
return;
|
||||
}
|
||||
if (!writable || p.kind === "bitfield_field") { p.hasOverride = true; p.override = conv; return; }
|
||||
p.hasOverride = false;
|
||||
let bytes: Uint8Array;
|
||||
switch (p.kind) {
|
||||
case "float": {
|
||||
const dv = new DataView(new ArrayBuffer(8));
|
||||
if (p.size === 4) dv.setFloat32(0, Number(conv), false); else dv.setFloat64(0, Number(conv), false);
|
||||
bytes = new Uint8Array(p.size);
|
||||
for (let i = 0; i < p.size; i++) bytes[p.be ? i : p.size - 1 - i] = dv.getUint8(i);
|
||||
bytes = this.encodeFloat(Number(conv), p.size, p.be);
|
||||
break;
|
||||
}
|
||||
case "bool": bytes = this.encodeInt(conv ? 1n : 0n, p.size, p.be); break;
|
||||
@@ -1067,7 +1239,8 @@ export class PatternInstance {
|
||||
case "padding": return;
|
||||
default: bytes = this.encodeInt(this.toInt(conv), p.size, p.be);
|
||||
}
|
||||
this.mem(p.section).write!(p.offset, bytes);
|
||||
mem.write!(p.offset, bytes);
|
||||
p.hasOverride = false;
|
||||
}
|
||||
|
||||
/** Convert a value to the representation of a scalar pattern kind. */
|
||||
@@ -1110,6 +1283,14 @@ export class PatternInstance {
|
||||
|
||||
exec(s: A.Stmt, fr: Frame): void {
|
||||
this.loc = s.loc;
|
||||
if (this.layoutOnly && (fr.kind === "struct" || fr.kind === "bitfield")) {
|
||||
if (s.s !== "decl" && s.s !== "multi") this.error("search layouts require fixed declarations, not '" + s.s + "'", s.loc);
|
||||
const decls = s.s === "decl" ? [s] : s.s === "multi" ? s.decls : [];
|
||||
for (const d of decls) {
|
||||
if (d.init || d.placement || d.section || d.isIn || d.isOut || (d.array && d.array.k !== "fixed"))
|
||||
this.error("search layouts require fixed, contiguous fields", d.loc);
|
||||
}
|
||||
}
|
||||
switch (s.s) {
|
||||
case "decl": this.execDecl(s, fr); return;
|
||||
case "multi": for (const d of s.decls) this.execDecl(d, fr); return;
|
||||
@@ -1229,7 +1410,7 @@ export class PatternInstance {
|
||||
else vr.v = v instanceof Pattern && !v.isComposite() ? this.value(v) : v;
|
||||
return;
|
||||
}
|
||||
const p = this.evalExpr(target);
|
||||
const p = this.evalExpr(target, this.writeMain);
|
||||
if (!(p instanceof Pattern)) this.error("cannot assign to a non-variable expression", target.loc);
|
||||
if (op !== "=") v = this.binop(op.slice(0, -1), this.decay(p), this.decay(v), target.loc);
|
||||
this.assignPattern(p, v);
|
||||
@@ -1307,7 +1488,9 @@ export class PatternInstance {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
evalExpr(e: A.Expr): any {
|
||||
evalExpr(e: A.Expr, lvalue = false): any {
|
||||
if (this.layoutOnly && (e.k === "dollar" || (e.k === "typeop" && e.op === "addressof")))
|
||||
this.error("search layouts cannot depend on instance addresses", e.loc);
|
||||
switch (e.k) {
|
||||
case "lit": return e.v;
|
||||
case "id": {
|
||||
@@ -1348,7 +1531,7 @@ export class PatternInstance {
|
||||
return m;
|
||||
}
|
||||
if (!m) this.error("no member named '" + e.name + "' in " + o.typeName, e.loc);
|
||||
return m.kind === "pointer" ? m.pointee : m;
|
||||
return m.kind === "pointer" && !lvalue ? m.pointee : m;
|
||||
}
|
||||
case "index": {
|
||||
if (e.obj.k === "dollar") {
|
||||
@@ -1363,12 +1546,18 @@ export class PatternInstance {
|
||||
if (o.kind === "string" || o.kind === "wstring") {
|
||||
const s = this.value(o) as string;
|
||||
if (i < 0 || i >= (o.hasOverride ? s.length : o.size / (o.kind === "wstring" ? 2 : 1))) this.error("index " + i + " out of bounds", e.loc);
|
||||
if (lvalue && !o.hasOverride) {
|
||||
const width = o.kind === "wstring" ? 2 : 1;
|
||||
const p = new Pattern(width === 2 ? "char16" : "char", width === 2 ? "char16" : "char");
|
||||
p.section = o.section; p.offset = o.offset + i * width; p.size = width; p.be = o.be;
|
||||
return p;
|
||||
}
|
||||
return new Chr(s.charCodeAt(i) || 0, o.kind === "wstring");
|
||||
}
|
||||
if (o.kind !== "array") this.error("cannot index a non-array pattern", e.loc);
|
||||
const en = o.entry(i);
|
||||
if (!en) this.error("array index " + i + " out of bounds (size " + o.entryCount() + ")", e.loc);
|
||||
return en.kind === "pointer" ? en.pointee : en;
|
||||
return en.kind === "pointer" && !lvalue ? en.pointee : en;
|
||||
}
|
||||
if (typeof o === "string") {
|
||||
if (i < 0 || i >= o.length) this.error("string index " + i + " out of bounds", e.loc);
|
||||
@@ -1665,19 +1854,14 @@ export class PatternInstance {
|
||||
|
||||
private dumpPattern(p: Pattern, indent: number, print: (s: string) => void, name: string): void {
|
||||
if (p.hidden) return;
|
||||
if (p.displayName) name = p.displayName;
|
||||
const pad = "".padStart(indent * 2, " ");
|
||||
const addr = p.local ? "local" : "0x" + (this.dumpBase + p.offset).toString(16).padStart(8, "0");
|
||||
const type = p.kind === "bitfield_field" ? p.typeName + ":" + p.bits : p.typeName;
|
||||
let line = pad + name + " (" + type + ") @ " + addr;
|
||||
print(pad + this.describePattern(p, this.dumpBase, name));
|
||||
if (p.displayName) name = p.displayName;
|
||||
if (p.kind === "pointer") {
|
||||
const v = this.value(p) as bigint;
|
||||
print(line + " = " + fmtInt(v));
|
||||
if (p.pointee) this.dumpPattern(p.pointee, indent + 1, print, p.pointee.name || "*" + name);
|
||||
return;
|
||||
}
|
||||
if (p.isComposite() && !p.formatFn && !p.transformFn) {
|
||||
print(line + " size=" + p.size);
|
||||
if (p.sealed) return;
|
||||
if (p.kind === "array") {
|
||||
const n = p.entryCount();
|
||||
@@ -1685,9 +1869,16 @@ export class PatternInstance {
|
||||
} else for (const c of p.children || []) this.dumpPattern(c, indent + 1, print, c.name);
|
||||
return;
|
||||
}
|
||||
if (p.kind === "padding") { print(line + " size=" + p.size); return; }
|
||||
line += " = " + this.dumpValue(p);
|
||||
print(line);
|
||||
}
|
||||
|
||||
/** One pattern row, shared by the indented dump and the metadata tree. */
|
||||
describePattern(p: Pattern, base = 0, name = p.name): string {
|
||||
const addr = p.local ? "local" : "0x" + (base + p.offset).toString(16).padStart(8, "0");
|
||||
const type = p.kind === "bitfield_field" ? p.typeName + ":" + p.bits : p.typeName;
|
||||
const line = (p.displayName || name) + " (" + type + ") @ " + addr;
|
||||
if (p.kind === "pointer") return line + " = " + fmtInt(this.value(p) as bigint);
|
||||
if ((p.isComposite() && !p.formatFn && !p.transformFn) || p.kind === "padding") return line + " size=" + p.size;
|
||||
return line + " = " + this.dumpValue(p);
|
||||
}
|
||||
|
||||
private dumpValue(p: Pattern): string {
|
||||
@@ -1703,17 +1894,26 @@ export class PatternInstance {
|
||||
return this.formatValue(v);
|
||||
}
|
||||
|
||||
/** Evaluate an expression in the global scope of this (already evaluated) instance. */
|
||||
/** Evaluate an expression or assignment in the global scope of this instance. */
|
||||
evaluateExpression(src: string): any {
|
||||
const e = parseExpression(src, this.types.keys());
|
||||
const s = parseEvaluation(src, this.types.keys());
|
||||
if (!this.global) {
|
||||
const root = new Scope();
|
||||
this.global = { scope: root, root, kind: "global", ns: [], binds: new Map() };
|
||||
}
|
||||
this.frames = [this.global];
|
||||
return this.evalExpr(e);
|
||||
this.invalidateData();
|
||||
const save = this.writeMain;
|
||||
this.writeMain = true;
|
||||
try {
|
||||
if (s.s === "expr") return this.evalExpr(s.e);
|
||||
this.exec(s, this.global);
|
||||
} finally { this.writeMain = save; }
|
||||
}
|
||||
|
||||
/** Refresh host reads after external writes or writes from another instance. */
|
||||
invalidateData(): void { if (this.data.invalidate) this.data.invalidate(); }
|
||||
|
||||
/** Human readable rendering of any runtime value (dump style). */
|
||||
describe(v: any, base = 0): string {
|
||||
if (v instanceof Pattern) {
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface HexpatHost {
|
||||
size(): number;
|
||||
/** Read `size` bytes at pattern address `addr` (short reads are zero-padded). */
|
||||
read(addr: number, size: number): Uint8Array;
|
||||
/** Write bytes at a pattern address, or throw on failure (optional for read-only hosts). */
|
||||
write?(addr: number, bytes: Uint8Array): void;
|
||||
/** Output of std::print and friends (defaults to console.log). */
|
||||
print?(msg: string): void;
|
||||
/** Resolve an `#include` / `import` path relative to the including source. */
|
||||
@@ -27,6 +29,11 @@ export class BufferHost implements HexpatHost {
|
||||
if (addr < this.data.length) out.set(this.data.subarray(addr, Math.min(this.data.length, addr + size)));
|
||||
return out;
|
||||
}
|
||||
write(addr: number, bytes: Uint8Array): void {
|
||||
if (!Number.isSafeInteger(addr) || addr < 0 || addr + bytes.length > this.data.length)
|
||||
throw new Error("write outside the data");
|
||||
this.data.set(bytes, addr);
|
||||
}
|
||||
resolve(path: string): HexpatSource | undefined {
|
||||
for (const n of [path, path + ".hexpat", path + ".pat"]) if (n in this.sources) return { name: n, src: this.sources[n] };
|
||||
return undefined;
|
||||
|
||||
+11
-2
@@ -12,8 +12,10 @@ import * as A from "./ast";
|
||||
export { parse, PatternInstance, PatternError, BufferHost };
|
||||
export type { HexpatHost, HostHooks };
|
||||
export { Pattern } from "./patterns";
|
||||
export { toJson, patternToJson, walkPatterns, jsonValue } from "./output";
|
||||
export type { PatternJson, JsonOptions } from "./output";
|
||||
export { toJson, patternToJson, walkPatterns, jsonValue, dumpTree, patternColor } from "./output";
|
||||
export type { PatternJson, JsonOptions, TreeOptions } from "./output";
|
||||
export { SearchPattern } from "./search";
|
||||
export type { SearchOptions, SearchField, SearchEncoding, FieldState } from "./search";
|
||||
|
||||
export interface RunOptions {
|
||||
name?: string; // source name for error messages and relative includes
|
||||
@@ -33,6 +35,13 @@ export function evaluate(program: A.Program, host: HexpatHost, hooks?: HostHooks
|
||||
return inst;
|
||||
}
|
||||
|
||||
/** Decode one type at a host offset, preparing types and constants only. */
|
||||
export function decodeType(program: A.Program, type: string, host: HexpatHost, offset = 0, hooks?: HostHooks): PatternInstance {
|
||||
const inst = new PatternInstance(program, new HostMemory(host), hooks, host.print ? (s) => host.print!(s) : undefined);
|
||||
inst.placeType(type, offset);
|
||||
return inst;
|
||||
}
|
||||
|
||||
/** Parse and evaluate a hexpat source. Synchronous. */
|
||||
export function runHexpat(src: string, host: HexpatHost, opts: RunOptions = {}): PatternInstance {
|
||||
return evaluate(compile(src, host, opts.name, opts.defines), host, opts.hooks);
|
||||
|
||||
+11
-2
@@ -7,16 +7,25 @@ import { HexpatHost } from "./host";
|
||||
export interface Memory {
|
||||
read(addr: number, size: number): Uint8Array;
|
||||
write?(addr: number, bytes: Uint8Array): void;
|
||||
invalidate?(): void;
|
||||
size(): number;
|
||||
}
|
||||
|
||||
const PAGE = 4096;
|
||||
|
||||
/** Read-only view of the host data with a page cache. */
|
||||
/** View of the host data with a page cache and optional write-through support. */
|
||||
export class HostMemory implements Memory {
|
||||
private pages = new Map<number, Uint8Array>();
|
||||
private _size = -1;
|
||||
constructor(private host: HexpatHost) {}
|
||||
write?: (addr: number, bytes: Uint8Array) => void;
|
||||
constructor(private host: HexpatHost) {
|
||||
if (host.write) this.write = (addr, bytes) => {
|
||||
if (!Number.isSafeInteger(addr) || addr < 0 || addr + bytes.length > this.size())
|
||||
throw new Error("write outside the data");
|
||||
try { host.write!(addr, bytes); } finally { this.invalidate(); }
|
||||
};
|
||||
}
|
||||
invalidate(): void { this.pages.clear(); this._size = -1; }
|
||||
size(): number {
|
||||
if (this._size < 0) this._size = Math.max(0, this.host.size());
|
||||
return this._size;
|
||||
|
||||
+60
-2
@@ -8,6 +8,11 @@ export interface JsonOptions {
|
||||
maxEntries?: number; // cap array entries (default: all)
|
||||
}
|
||||
|
||||
export interface TreeOptions extends JsonOptions {
|
||||
color?: boolean; // color rows with ANSI truecolor escapes (default: off)
|
||||
patterns?: Pattern[]; // selected roots (default: all placed patterns)
|
||||
}
|
||||
|
||||
export interface PatternJson {
|
||||
name: string;
|
||||
type: string;
|
||||
@@ -15,6 +20,10 @@ export interface PatternJson {
|
||||
addr: number;
|
||||
offset: number;
|
||||
size: number;
|
||||
displayName?: string;
|
||||
color?: string;
|
||||
comment?: string;
|
||||
attributes?: { name: string; args: any[] }[];
|
||||
local?: boolean;
|
||||
section?: number;
|
||||
bits?: number;
|
||||
@@ -46,6 +55,10 @@ export function patternToJson(inst: PatternInstance, p: Pattern, opts: JsonOptio
|
||||
size: p.size,
|
||||
};
|
||||
if (p.local) j.local = true;
|
||||
if (p.displayName !== undefined) j.displayName = p.displayName;
|
||||
if (p.getColor() !== undefined) j.color = p.getColor();
|
||||
if (p.comment !== undefined) j.comment = p.comment;
|
||||
if (p.attrs && p.attrs.length) j.attributes = attributes(inst, p);
|
||||
if (p.section !== MAIN_SECTION && !p.local) j.section = p.section;
|
||||
if (p.kind === "bitfield_field") { j.bits = p.bits; j.bitOffset = p.bitPos % 8; }
|
||||
if (p.kind === "array") {
|
||||
@@ -74,12 +87,57 @@ export function toJson(inst: PatternInstance, opts: JsonOptions = {}): PatternJs
|
||||
return inst.patterns.filter((p) => !p.hidden).map((p) => patternToJson(inst, p, opts));
|
||||
}
|
||||
|
||||
/** Evaluated, JSON-safe attribute arguments, including pattern-valued arguments. */
|
||||
function attributes(inst: PatternInstance, p: Pattern): { name: string; args: any[] }[] {
|
||||
return (p.attrs || []).map((a) => ({ name: a.name, args: a.args.map((v) => {
|
||||
const value = inst.decay(v);
|
||||
return value instanceof Pattern ? inst.toStr(value) : jsonValue(value);
|
||||
}) }));
|
||||
}
|
||||
|
||||
/** Hexpat's RRGGBB colors in a form usable by ANSI and r2's rgb: syntax. */
|
||||
export function patternColor(p: Pattern): string | undefined {
|
||||
const m = (p.getColor() || "").match(/^(?:#|0x|rgb:)?([0-9a-f]{6})$/i);
|
||||
return m ? m[1].toLowerCase() : undefined;
|
||||
}
|
||||
|
||||
/** Draw a tree with values, addresses and evaluated presentation attributes. */
|
||||
export function dumpTree(inst: PatternInstance, print: (s: string) => void, opts: TreeOptions = {}): void {
|
||||
const walk = (p: Pattern, prefix: string, branch: string): void => {
|
||||
const attrs = attributes(inst, p).filter((a) => !["color", "comment", "name"].includes(a.name));
|
||||
const presentation: [string, string | undefined][] = [["color", p.getColor()], ["comment", p.comment], ["name", p.displayName]];
|
||||
for (const [name, value] of presentation) {
|
||||
if (value !== undefined) attrs.push({ name, args: [value] });
|
||||
}
|
||||
let line = inst.describePattern(p, opts.base || 0);
|
||||
if (attrs.length) line += " [[" + attrs.map((a) => a.name + (a.args.length ? "(" +
|
||||
a.args.map((v) => JSON.stringify(v) || "null").join(", ") + ")" : "")).join(", ") + "]]";
|
||||
const rgb = opts.color ? patternColor(p) : undefined;
|
||||
if (rgb) line = "\x1b[38;2;" + [0, 2, 4].map((i) => parseInt(rgb.substring(i, i + 2), 16)).join(";") + "m" + line + "\x1b[0m";
|
||||
print(prefix + branch + line);
|
||||
if (p.sealed) return;
|
||||
let children: Pattern[] = [];
|
||||
let omitted = 0;
|
||||
if (p.kind === "pointer") children = p.pointee ? [p.pointee] : [];
|
||||
else if (p.kind === "array") {
|
||||
const n = p.entryCount(), max = opts.maxEntries === undefined ? n : Math.min(n, opts.maxEntries);
|
||||
for (let i = 0; i < max; i++) children.push(p.entry(i)!);
|
||||
omitted = n - max;
|
||||
} else if (p.isComposite()) children = p.children || [];
|
||||
children = children.filter((c) => !c.hidden);
|
||||
const next = prefix + (branch ? branch === "`- " ? " " : "| " : "");
|
||||
children.forEach((c, i) => walk(c, next, i === children.length - 1 && !omitted ? "`- " : "|- "));
|
||||
if (omitted) print(next + "`- ... (" + omitted + " more entries)");
|
||||
};
|
||||
for (const p of opts.patterns || inst.patterns) if (!p.hidden) walk(p, "", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the placed patterns depth first. `path` holds the member names from
|
||||
* the top-level pattern down (array entries use their index).
|
||||
* Return false from `visit` to skip the children of a pattern.
|
||||
*/
|
||||
export function walkPatterns(inst: PatternInstance, visit: (p: Pattern, path: string[]) => boolean | void): void {
|
||||
export function walkPatterns(inst: PatternInstance, visit: (p: Pattern, path: string[]) => boolean | void, patterns = inst.patterns): void {
|
||||
const walk = (p: Pattern, path: string[]) => {
|
||||
if (p.hidden) return;
|
||||
if (visit(p, path) === false) return;
|
||||
@@ -92,5 +150,5 @@ export function walkPatterns(inst: PatternInstance, visit: (p: Pattern, path: st
|
||||
for (const c of p.children || []) walk(c, path.concat(c.name));
|
||||
}
|
||||
};
|
||||
for (const p of inst.patterns) walk(p, [p.name]);
|
||||
for (const p of patterns) walk(p, [p.name]);
|
||||
}
|
||||
+46
-3
@@ -308,7 +308,7 @@ class Parser {
|
||||
}
|
||||
}
|
||||
|
||||
private parseAssignment(): A.Stmt {
|
||||
private parseAssignment(): Extract<A.Stmt, { s: "assign" }> {
|
||||
const loc = this.loc();
|
||||
let target: A.Expr;
|
||||
if (this.acceptOp("$")) target = { k: "dollar", loc };
|
||||
@@ -318,6 +318,26 @@ class Parser {
|
||||
return { s: "assign", loc, target, op, value };
|
||||
}
|
||||
|
||||
/** One interactive expression or assignment, with an optional semicolon. */
|
||||
parseEvaluation(): A.Stmt {
|
||||
const loc = this.loc();
|
||||
const s: A.Stmt = this.atAssignment() ? this.parseAssignment() : { s: "expr", loc, e: this.parseExpr() };
|
||||
this.semis();
|
||||
this.expectEnd();
|
||||
return s;
|
||||
}
|
||||
|
||||
parseAssignments(): Extract<A.Stmt, { s: "assign" }>[] {
|
||||
const out: Extract<A.Stmt, { s: "assign" }>[] = [];
|
||||
while (this.peek().k !== "eof") {
|
||||
if (!this.atAssignment()) this.err("expected a field assignment");
|
||||
out.push(this.parseAssignment());
|
||||
this.semis();
|
||||
this.acceptOp(",");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Parse one statement (or declaration) and append it to `out`. */
|
||||
statement(out: A.Stmt[], inBitfield = false): void {
|
||||
const p = this.peek();
|
||||
@@ -761,13 +781,36 @@ export function parse(src: string, name = "", resolve?: SourceResolver, defines:
|
||||
return parseProgram(src, name, resolve, defines);
|
||||
}
|
||||
|
||||
/** Parse a single expression (used to evaluate expressions against a loaded pattern). */
|
||||
export function parseExpression(src: string, knownTypes: Iterable<string> = []): A.Expr {
|
||||
function expressionParser(src: string, knownTypes: Iterable<string>): Parser {
|
||||
const pre = preprocess(src, "<expr>");
|
||||
const st: ImportState = { seen: new Set(), pragmas: pre.pragmas, defines: new Map(), once: new Set() };
|
||||
const p = new Parser(pre.tokens, st);
|
||||
for (const t of knownTypes) p.types.add(t);
|
||||
return p;
|
||||
}
|
||||
|
||||
/** Parse a single expression. */
|
||||
export function parseExpression(src: string, knownTypes: Iterable<string> = []): A.Expr {
|
||||
const p = expressionParser(src, knownTypes);
|
||||
const e = p.parseExpr();
|
||||
p.expectEnd();
|
||||
return e;
|
||||
}
|
||||
|
||||
/** Parse an interactive expression or assignment against a loaded pattern. */
|
||||
export function parseEvaluation(src: string, knownTypes: Iterable<string> = []): A.Stmt {
|
||||
return expressionParser(src, knownTypes).parseEvaluation();
|
||||
}
|
||||
|
||||
/** Parse a type name, including endian qualifiers and template arguments. */
|
||||
export function parseType(src: string, knownTypes: Iterable<string> = []): A.TypeApp {
|
||||
const p = expressionParser(src, knownTypes);
|
||||
const t = p.parseType();
|
||||
p.expectEnd();
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Field assignments separated by whitespace, commas or semicolons. */
|
||||
export function parseAssignments(src: string, knownTypes: Iterable<string> = []): Extract<A.Stmt, { s: "assign" }>[] {
|
||||
return expressionParser(src, knownTypes).parseAssignments();
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export class Pattern {
|
||||
section = MAIN_SECTION;
|
||||
offset = 0;
|
||||
size = 0;
|
||||
alignment = 1; // alignment of an independently instantiated layout
|
||||
be = false;
|
||||
parent?: Pattern;
|
||||
attrs?: { name: string; args: any[] }[];
|
||||
@@ -66,6 +67,11 @@ export class Pattern {
|
||||
return this.kind === "struct" || this.kind === "union" || this.kind === "array" || this.kind === "bitfield";
|
||||
}
|
||||
|
||||
/** Explicit member colors override the enclosing aggregate's color. */
|
||||
getColor(): string | undefined {
|
||||
return this.color === undefined ? this.parent?.getColor() : this.color;
|
||||
}
|
||||
|
||||
attr(name: string): { name: string; args: any[] } | undefined {
|
||||
if (this.attrs) for (const a of this.attrs) if (a.name === name) return a;
|
||||
return undefined;
|
||||
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
/** search.ts - independent, partially defined instances for masked binary searches. */
|
||||
import * as A from "./ast";
|
||||
import { PatternInstance, LayoutOptions } from "./evaluator";
|
||||
import { Pattern } from "./patterns";
|
||||
import { Memory, SparseMemory } from "./memory";
|
||||
import { parseAssignments, parseExpression } from "./parser";
|
||||
import { jsonValue } from "./output";
|
||||
|
||||
export interface SearchOptions extends LayoutOptions {
|
||||
/** Maximum number of scalar fields (default: 65536). */
|
||||
maxFields?: number;
|
||||
}
|
||||
export type FieldState = "unset" | "partial" | "set";
|
||||
export interface SearchField {
|
||||
path: string;
|
||||
type: string;
|
||||
offset: number;
|
||||
size: number;
|
||||
bits?: number;
|
||||
defined: boolean;
|
||||
value?: any;
|
||||
}
|
||||
export interface SearchEncoding { bytes: Uint8Array; mask: Uint8Array; }
|
||||
interface Value extends SearchEncoding { offset: number; value: any; }
|
||||
|
||||
/** Scratch storage never accesses the host; layout and value expressions cannot read it. */
|
||||
class ScratchMemory implements Memory {
|
||||
private data = new SparseMemory();
|
||||
readable = false;
|
||||
constructor(private limit: number) {}
|
||||
size(): number { return this.limit; }
|
||||
read(addr: number, size: number): Uint8Array {
|
||||
if (!this.readable) throw new Error("search layouts and expressions cannot read input data");
|
||||
return this.data.read(addr, size);
|
||||
}
|
||||
write(addr: number, bytes: Uint8Array): void {
|
||||
if (addr < 0 || addr + bytes.length > this.limit) throw new Error("field exceeds search layout");
|
||||
this.data.write(addr, bytes);
|
||||
}
|
||||
reset(): void { this.data = new SparseMemory(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* A fixed type instantiated without a backing address. Fields start unset,
|
||||
* including zero-valued fields. Only explicit assignments contribute mask bits.
|
||||
*/
|
||||
export class SearchPattern {
|
||||
readonly type: string;
|
||||
readonly size: number;
|
||||
readonly alignment: number;
|
||||
private readonly context: PatternInstance;
|
||||
private readonly memory: ScratchMemory;
|
||||
private readonly nodes = new Map<string, Pattern>();
|
||||
private readonly leaves = new Map<string, Pattern>();
|
||||
private values = new Map<string, Value>();
|
||||
|
||||
constructor(program: A.Program, type: string, options: SearchOptions = {}) {
|
||||
const maxSize = options.maxSize === undefined ? 0x100000 : options.maxSize;
|
||||
const maxFields = options.maxFields === undefined ? 0x10000 : options.maxFields;
|
||||
if (!Number.isSafeInteger(maxSize) || maxSize <= 0 || !Number.isSafeInteger(maxFields) || maxFields <= 0)
|
||||
throw new Error("search limits must be positive integers");
|
||||
const abi = program.pragmas.filter(([k]) => k === "abi").map(([, v]) => v.trim()).pop();
|
||||
if (abi && abi !== "packed" && options.alignment === undefined)
|
||||
throw new Error("specify --align (API: alignment) for this layout; target ABI is not inferred");
|
||||
this.memory = new ScratchMemory(maxSize);
|
||||
this.context = new PatternInstance(program, this.memory, {}, () => undefined);
|
||||
this.context.prepareTypes();
|
||||
// Bound construction too, including arrays of zero-sized aggregate entries.
|
||||
this.context.arrayLimit = this.context.arrayLimit > 0 ? Math.min(this.context.arrayLimit, maxFields) : maxFields;
|
||||
if (!(this.context.patternLimit > 0)) this.context.patternLimit = 0x100000;
|
||||
const root = this.context.instantiateType(type, options);
|
||||
if (!root.isComposite() || !root.size) throw new Error("search type must be a nonempty struct, union or bitfield");
|
||||
this.type = root.typeName;
|
||||
this.size = root.size;
|
||||
this.alignment = root.alignment;
|
||||
const collect = (p: Pattern, path: string): void => {
|
||||
if (p.kind === "padding") return;
|
||||
if (p.offset < 0 || p.offset + p.size > this.size) throw new Error("field outside its search layout: " + path);
|
||||
this.nodes.set(path, p);
|
||||
if (p.kind === "array") {
|
||||
if (p.entryCount() > maxFields) throw new Error("search layout exceeds maximum field count " + maxFields);
|
||||
for (let i = 0; i < p.entryCount(); i++) collect(p.entry(i)!, path + "[" + i + "]");
|
||||
} else if (p.isComposite()) {
|
||||
for (const c of p.children || []) if (c.name) collect(c, path ? path + "." + c.name : c.name);
|
||||
} else {
|
||||
this.leaves.set(path, p);
|
||||
if (this.leaves.size > maxFields) throw new Error("search layout exceeds maximum field count " + maxFields);
|
||||
}
|
||||
};
|
||||
collect(root, "");
|
||||
this.memory.readable = true;
|
||||
}
|
||||
|
||||
private node(path: string): Pattern {
|
||||
const p = this.nodes.get(path);
|
||||
if (!p) throw new Error("unknown search field '" + path + "'");
|
||||
return p;
|
||||
}
|
||||
private inside(path: string, parent: string): boolean {
|
||||
return !parent || path === parent || path.startsWith(parent + ".") || path.startsWith(parent + "[");
|
||||
}
|
||||
|
||||
/** Assign a leaf or merge a partial object/array. Updates are atomic. */
|
||||
set(fields: Record<string, any>): this;
|
||||
set(path: string, value: any): this;
|
||||
set(path: string | Record<string, any>, value?: any): this {
|
||||
const next = new Map(this.values);
|
||||
const assign = (key: string, v: any): void => {
|
||||
const p = this.node(key);
|
||||
if (v === null && p.kind === "pointer") v = 0n;
|
||||
if (v === undefined || v === null) throw new Error("use unset() to leave a search field undefined");
|
||||
if (p.isComposite()) {
|
||||
if (typeof v !== "object" || v instanceof A.Chr) throw new Error("field '" + key + "' needs an object or array");
|
||||
if (p.kind === "array" && !Array.isArray(v)) throw new Error("field '" + key + "' needs an array");
|
||||
for (const child of Object.keys(v)) {
|
||||
const target = p.kind === "array" ? key + "[" + child + "]" : key ? key + "." + child : child;
|
||||
assign(target, v[child]);
|
||||
}
|
||||
} else {
|
||||
if (typeof v === "object" && !(v instanceof A.Chr)) throw new Error("field '" + key + "' needs a scalar value");
|
||||
if (typeof v === "number" && ["unsigned", "signed", "enum", "pointer", "bitfield_field"].includes(p.kind) && !Number.isSafeInteger(v))
|
||||
throw new Error("integer field '" + key + "' requires a safe integer number or bigint");
|
||||
if ((p.kind === "char" || p.kind === "char16") && typeof v === "string") {
|
||||
if (v.length !== 1) throw new Error("field '" + key + "' needs one character");
|
||||
v = new A.Chr(v.charCodeAt(0), p.kind === "char16");
|
||||
}
|
||||
if ((p.kind === "string" || p.kind === "wstring") && typeof v === "string" && v.length * (p.kind === "wstring" ? 2 : 1) > p.size)
|
||||
throw new Error("string exceeds field '" + key + "'");
|
||||
next.set(key, this.encodeField(p, v));
|
||||
}
|
||||
};
|
||||
if (typeof path === "string") assign(path, value);
|
||||
else assign("", path);
|
||||
this.merge(next); // reject conflicting overlapping constraints before committing
|
||||
this.values = next;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Remove constraints from a field, aggregate, or the whole instance. */
|
||||
unset(path = ""): this {
|
||||
this.node(path);
|
||||
for (const key of this.values.keys()) if (this.inside(key, path)) this.values.delete(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
state(path = ""): FieldState {
|
||||
this.node(path);
|
||||
let total = 0, defined = 0;
|
||||
for (const key of this.leaves.keys()) if (this.inside(key, path)) { total++; if (this.values.has(key)) defined++; }
|
||||
return !defined ? "unset" : defined === total ? "set" : "partial";
|
||||
}
|
||||
isDefined(path: string): boolean { return this.state(path) === "set"; }
|
||||
|
||||
/** Unset leaves return undefined; aggregates return only their defined values. */
|
||||
get(path = ""): any {
|
||||
const p = this.node(path);
|
||||
if (!p.isComposite()) return this.values.get(path)?.value;
|
||||
if (this.state(path) === "unset") return undefined;
|
||||
const out: any = p.kind === "array" ? [] : Object.create(null);
|
||||
if (p.kind === "array") {
|
||||
for (let i = 0; i < p.entryCount(); i++) { const v = this.get(path + "[" + i + "]"); if (v !== undefined) out[i] = v; }
|
||||
} else {
|
||||
for (const c of p.children || []) if (c.name && c.kind !== "padding") {
|
||||
const v = this.get(path ? path + "." + c.name : c.name);
|
||||
if (v !== undefined) out[c.name] = v;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Evaluate constants, enum names and arithmetic without reading input data. */
|
||||
evaluateExpression(src: string): any {
|
||||
this.memory.readable = false;
|
||||
try { return this.context.decay(this.context.evalExpr(parseExpression(src, this.context.typeNames()))); }
|
||||
finally { this.memory.readable = true; }
|
||||
}
|
||||
|
||||
/** Apply CLI-style field=expression assignments as one transaction. */
|
||||
assign(src: string): this {
|
||||
const assignments = parseAssignments(src, this.context.typeNames());
|
||||
if (!assignments.length) throw new Error("expected at least one field=expression assignment");
|
||||
const path = (e: A.Expr): string => {
|
||||
if (e.k === "id") return e.name;
|
||||
if (e.k === "member") return path(e.obj) + "." + e.name;
|
||||
if (e.k === "index") {
|
||||
this.memory.readable = false;
|
||||
let i: number;
|
||||
try { i = Number(this.context.toInt(this.context.decay(this.context.evalExpr(e.idx)))); }
|
||||
finally { this.memory.readable = true; }
|
||||
if (!Number.isSafeInteger(i) || i < 0) throw new Error("field index must be a nonnegative integer");
|
||||
return path(e.obj) + "[" + i + "]";
|
||||
}
|
||||
throw new Error("expected a field path");
|
||||
};
|
||||
const fields: Record<string, any> = Object.create(null);
|
||||
for (const s of assignments) {
|
||||
if (s.op !== "=") throw new Error("search fields use field=expression; unset fields have no prior value");
|
||||
this.memory.readable = false;
|
||||
let value: any;
|
||||
try { value = this.context.decay(this.context.evalExpr(s.value)); }
|
||||
finally { this.memory.readable = true; }
|
||||
fields[path(s.target)] = value;
|
||||
}
|
||||
return this.set(fields);
|
||||
}
|
||||
|
||||
private encodeField(p: Pattern, v: any): Value {
|
||||
const ordered = p.kind === "bitfield_field" && p.bitMode?.order?.size;
|
||||
const offset = ordered ? p.bitMode!.root : p.offset;
|
||||
const size = ordered ? Math.ceil(Number(ordered) / 8) : p.size;
|
||||
this.memory.reset();
|
||||
this.context.writePattern(p, v);
|
||||
const raw = this.context.value(p);
|
||||
const value = raw instanceof A.Chr ? String.fromCharCode(raw.c) : raw;
|
||||
const bytes = this.memory.read(offset, size);
|
||||
let mask: Uint8Array;
|
||||
if (p.kind === "bitfield_field") {
|
||||
this.memory.reset();
|
||||
this.context.writePattern(p, (1n << BigInt(p.bits)) - 1n);
|
||||
mask = this.memory.read(offset, size);
|
||||
} else mask = new Uint8Array(size).fill(0xff);
|
||||
return { offset, bytes, mask, value };
|
||||
}
|
||||
|
||||
private merge(values: Map<string, Value>): SearchEncoding {
|
||||
const bytes = new Uint8Array(this.size), mask = new Uint8Array(this.size);
|
||||
for (const [path, v] of values) {
|
||||
for (let i = 0; i < v.bytes.length; i++) {
|
||||
const at = v.offset + i, m = v.mask[i];
|
||||
if (((bytes[at] ^ v.bytes[i]) & mask[at] & m) !== 0)
|
||||
throw new Error("conflicting search field '" + path + "' at byte " + at + "; unset overlapping fields first");
|
||||
bytes[at] = (bytes[at] & ~m) | (v.bytes[i] & m);
|
||||
mask[at] |= m;
|
||||
}
|
||||
}
|
||||
return { bytes, mask };
|
||||
}
|
||||
|
||||
encode(): SearchEncoding { return this.merge(this.values); }
|
||||
/** r2 /x accepts arbitrary bit masks, including partially defined bitfield bytes. */
|
||||
toR2(): string { const { bytes, mask } = this.encode(); return hex(bytes) + ":" + hex(mask); }
|
||||
|
||||
/** A libmagic decoder for this fixed layout; assignments do not constrain its matches. */
|
||||
toMagic(): string {
|
||||
// Labels are printf descriptions, not expressions or magic-file directives.
|
||||
const label = (s: string): string => s.replace(/[^\x20-\x7e]|[\\%]/g, "_");
|
||||
const lines = ["# " + label(this.type) + " size=" + this.size + " align=" + this.alignment,
|
||||
(this.size - 1) + "\tubyte\tx\t" + label(this.type)];
|
||||
for (const [path, p] of this.leaves) {
|
||||
if (!p.size) continue;
|
||||
const endian = p.be ? "be" : "le";
|
||||
let type: string, format: string;
|
||||
if (p.kind === "string" || p.kind === "wstring" || p.kind === "char") {
|
||||
type = p.kind === "wstring" ? endian + "string16" : "string";
|
||||
format = "%." + (p.kind === "wstring" ? p.size / 2 : p.size) + "s";
|
||||
} else if (p.kind === "float" && (p.size === 4 || p.size === 8)) {
|
||||
type = endian + (p.size === 4 ? "float" : "double");
|
||||
format = "%g";
|
||||
} else if (["unsigned", "signed", "bool", "char16", "enum", "pointer"].includes(p.kind) && [1, 2, 4, 8].includes(p.size)) {
|
||||
const width = { 1: "byte", 2: "short", 4: "long", 8: "quad" }[p.size];
|
||||
type = "u" + (p.size === 1 ? "" : endian) + width;
|
||||
// Raw hexadecimal also preserves signed fields' exact bit representation.
|
||||
format = p.size === 8 ? "0x%llx" : "0x%x";
|
||||
} else {
|
||||
throw new Error("libmagic cannot decode field '" + path + "' (" + (p.typeName || p.kind) +
|
||||
", " + p.size + " bytes); use hexpat to decode this type");
|
||||
}
|
||||
lines.push(">" + p.offset + "\t" + type + "\tx\t" + label(path) + "=" + format);
|
||||
}
|
||||
// Some bundled r2 magic readers skip the final rule; keep it message-free.
|
||||
lines.push(">0\tubyte\tx");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
fields(): SearchField[] {
|
||||
return Array.from(this.leaves, ([path, p]) => {
|
||||
const v = this.values.get(path);
|
||||
const field: SearchField = { path, type: p.typeName, offset: p.offset, size: p.size, defined: !!v };
|
||||
if (p.kind === "bitfield_field") {
|
||||
field.bits = p.bits;
|
||||
const encoded = v || this.encodeField(p, 0n);
|
||||
const first = encoded.mask.findIndex((m) => m !== 0);
|
||||
let last = encoded.mask.length - 1;
|
||||
while (last >= 0 && !encoded.mask[last]) last--;
|
||||
if (first >= 0) { field.offset = encoded.offset + first; field.size = last - first + 1; }
|
||||
}
|
||||
if (v) field.value = jsonValue(v.value);
|
||||
return field;
|
||||
});
|
||||
}
|
||||
toJSON(): any {
|
||||
const { bytes, mask } = this.encode();
|
||||
return { type: this.type, size: this.size, alignment: this.alignment, state: this.state(),
|
||||
bytes: hex(bytes), mask: hex(mask), pattern: this.toR2(), fields: this.fields() };
|
||||
}
|
||||
}
|
||||
|
||||
function hex(bytes: Uint8Array): string { return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); }
|
||||
+7
-1
@@ -230,7 +230,13 @@ const BUILTINS: Record<string, Fn> = {
|
||||
"std::core::get_attribute_value": (ev, a) => BUILTINS["std::core::get_attribute_argument"](ev, a, ev.loc),
|
||||
"std::core::set_display_name": (ev, a) => { if (a[0] instanceof Pattern) a[0].name = ev.toStr(ev.decay(a[1])); },
|
||||
"std::core::set_pattern_comment": (ev, a) => { if (a[0] instanceof Pattern) a[0].comment = ev.toStr(ev.decay(a[1])); },
|
||||
"std::core::set_pattern_color": () => undefined,
|
||||
"std::core::set_pattern_color": (ev, a) => {
|
||||
if (a[0] instanceof Pattern) {
|
||||
// The runtime's integer colors store red in the least significant byte.
|
||||
const color = uintN(32, ev.toInt(a[1]));
|
||||
a[0].color = [0n, 8n, 16n].map((shift) => Number((color >> shift) & 0xffn).toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
},
|
||||
"std::core::set_endian": (ev, a) => { const e = ev.toInt(a[0]); ev.bigEndian = e === 1n; },
|
||||
"std::core::get_endian": (ev) => (ev.bigEndian ? 1n : 2n),
|
||||
"std::core::array_index": (ev) => ev.arrayIndex,
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "r2hexpat",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "r2hexpat",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "r2hexpat",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"description": "ImHex pattern language (.hexpat) evaluator for radare2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,861 @@
|
||||
// Game Boy / Game Boy Color cartridge and CPU memory layouts.
|
||||
// Reference: https://gbdev.io/pandocs/ (CC0 hardware documentation).
|
||||
// Header: https://gbdev.io/pandocs/The_Cartridge_Header.html
|
||||
// I/O: https://gbdev.io/pandocs/Hardware_Reg_List.html
|
||||
// Memory: https://gbdev.io/pandocs/Memory_Map.html
|
||||
// Mappers: https://gbdev.io/pandocs/MBCs.html
|
||||
// Plugin: https://github.com/radareorg/r2hexpat
|
||||
// ROM: r2 -q -c 'hexpat gameboy.hexpat' game.gb
|
||||
// Apply flags, data hints and comments: .hexpat* gameboy.hexpat
|
||||
// JSON: hexpatj gameboy.hexpat
|
||||
// Set GB_CPU_MEMORY to 1 ONLY for a flat 64 KiB CPU address-space dump.
|
||||
// A ROM file's offsets above 0x7FFF are more ROM banks, not CPU RAM or I/O.
|
||||
// CPU dumps expose only the currently selected ROM/VRAM/WRAM/cartridge bank.
|
||||
// This maps data; it does not emulate devices or reconstruct bank state.
|
||||
|
||||
#pragma endian little
|
||||
#pragma base_address 0
|
||||
|
||||
#define GB_CPU_MEMORY 0
|
||||
|
||||
// Bitfields below run from least significant bit to most significant bit.
|
||||
// CGB flag bit 7 enables color mode; 0xC0 conventionally means CGB-only.
|
||||
// 0x00 requests DMG compatibility. 0xCE and 0xC3 are NOT DMG/CGB markers.
|
||||
enum CgbFlag : u8 {
|
||||
DmgCompatible = 0x00 ... 0x7F,
|
||||
CgbEnhanced = 0x80,
|
||||
CgbOnly = 0xC0
|
||||
};
|
||||
|
||||
enum SgbFlag : u8 {
|
||||
NoSgb = 0x00,
|
||||
SgbSupported = 0x03
|
||||
};
|
||||
|
||||
enum CartridgeType : u8 {
|
||||
RomOnly = 0x00,
|
||||
Mbc1 = 0x01,
|
||||
Mbc1Ram = 0x02,
|
||||
Mbc1RamBattery = 0x03,
|
||||
Mbc2 = 0x05,
|
||||
Mbc2Battery = 0x06,
|
||||
RomRam = 0x08,
|
||||
RomRamBattery = 0x09,
|
||||
Mmm01 = 0x0B,
|
||||
Mmm01Ram = 0x0C,
|
||||
Mmm01RamBattery = 0x0D,
|
||||
Mbc3TimerBattery = 0x0F,
|
||||
Mbc3TimerRamBattery = 0x10,
|
||||
Mbc3 = 0x11,
|
||||
Mbc3Ram = 0x12,
|
||||
Mbc3RamBattery = 0x13,
|
||||
Mbc5 = 0x19,
|
||||
Mbc5Ram = 0x1A,
|
||||
Mbc5RamBattery = 0x1B,
|
||||
Mbc5Rumble = 0x1C,
|
||||
Mbc5RumbleRam = 0x1D,
|
||||
Mbc5RumbleRamBattery = 0x1E,
|
||||
Mbc6 = 0x20,
|
||||
Mbc7SensorRumbleRamBattery = 0x22,
|
||||
PocketCamera = 0xFC,
|
||||
BandaiTama5 = 0xFD,
|
||||
HuC3 = 0xFE,
|
||||
HuC1RamBattery = 0xFF
|
||||
};
|
||||
|
||||
enum RomSize : u8 {
|
||||
KiB32_Banks2 = 0x00,
|
||||
KiB64_Banks4 = 0x01,
|
||||
KiB128_Banks8 = 0x02,
|
||||
KiB256_Banks16 = 0x03,
|
||||
KiB512_Banks32 = 0x04,
|
||||
MiB1_Banks64 = 0x05,
|
||||
MiB2_Banks128 = 0x06,
|
||||
MiB4_Banks256 = 0x07,
|
||||
MiB8_Banks512 = 0x08,
|
||||
Unofficial_Banks72 = 0x52,
|
||||
Unofficial_Banks80 = 0x53,
|
||||
Unofficial_Banks96 = 0x54
|
||||
};
|
||||
|
||||
// 0x01 is a legacy unofficial 2 KiB code, unused by licensed cartridges.
|
||||
// MBC2 has 512 four-bit RAM cells despite its header RAM size being zero.
|
||||
enum RamSize : u8 {
|
||||
None = 0x00,
|
||||
Unused_LegacyKiB2 = 0x01,
|
||||
KiB8_Banks1 = 0x02,
|
||||
KiB32_Banks4 = 0x03,
|
||||
KiB128_Banks16 = 0x04,
|
||||
KiB64_Banks8 = 0x05
|
||||
};
|
||||
|
||||
enum Destination : u8 {
|
||||
Japan = 0x00,
|
||||
Overseas = 0x01
|
||||
};
|
||||
|
||||
// New codes are two ASCII bytes; read this enum big-endian.
|
||||
enum NewLicenseeCode : u16 {
|
||||
None_00 = 0x3030,
|
||||
Nintendo_Research_Development_1_01 = 0x3031,
|
||||
Capcom_08 = 0x3038,
|
||||
EA_Electronic_Arts_13 = 0x3133,
|
||||
Hudson_Soft_18 = 0x3138,
|
||||
B_AI_19 = 0x3139,
|
||||
KSS_20 = 0x3230,
|
||||
Planning_Office_WADA_22 = 0x3232,
|
||||
PCM_Complete_24 = 0x3234,
|
||||
San_X_25 = 0x3235,
|
||||
Kemco_28 = 0x3238,
|
||||
SETA_Corporation_29 = 0x3239,
|
||||
Viacom_30 = 0x3330,
|
||||
Nintendo_31 = 0x3331,
|
||||
Bandai_32 = 0x3332,
|
||||
Ocean_Software_Acclaim_Entertainment_33 = 0x3333,
|
||||
Konami_34 = 0x3334,
|
||||
HectorSoft_35 = 0x3335,
|
||||
Taito_37 = 0x3337,
|
||||
Hudson_Soft_38 = 0x3338,
|
||||
Banpresto_39 = 0x3339,
|
||||
Ubi_Soft_41 = 0x3431,
|
||||
Atlus_42 = 0x3432,
|
||||
Malibu_Interactive_44 = 0x3434,
|
||||
Angel_46 = 0x3436,
|
||||
Bullet_Proof_Software_47 = 0x3437,
|
||||
Irem_49 = 0x3439,
|
||||
Absolute_50 = 0x3530,
|
||||
Acclaim_Entertainment_51 = 0x3531,
|
||||
Activision_52 = 0x3532,
|
||||
Sammy_USA_Corporation_53 = 0x3533,
|
||||
Konami_54 = 0x3534,
|
||||
Hi_Tech_Expressions_55 = 0x3535,
|
||||
LJN_56 = 0x3536,
|
||||
Matchbox_57 = 0x3537,
|
||||
Mattel_58 = 0x3538,
|
||||
Milton_Bradley_Company_59 = 0x3539,
|
||||
Titus_Interactive_60 = 0x3630,
|
||||
Virgin_Games_Ltd_61 = 0x3631,
|
||||
Lucasfilm_Games_64 = 0x3634,
|
||||
Ocean_Software_67 = 0x3637,
|
||||
EA_Electronic_Arts_69 = 0x3639,
|
||||
Infogrames_70 = 0x3730,
|
||||
Interplay_Entertainment_71 = 0x3731,
|
||||
Broderbund_72 = 0x3732,
|
||||
Sculptured_Software_73 = 0x3733,
|
||||
The_Sales_Curve_Limited_75 = 0x3735,
|
||||
THQ_78 = 0x3738,
|
||||
Accolade_79 = 0x3739,
|
||||
Misawa_Entertainment_80 = 0x3830,
|
||||
LOZC_G_83 = 0x3833,
|
||||
Tokuma_Shoten_86 = 0x3836,
|
||||
Tsukuda_Original_87 = 0x3837,
|
||||
Chunsoft_Co_91 = 0x3931,
|
||||
Video_System_92 = 0x3932,
|
||||
Ocean_Software_Acclaim_Entertainment_93 = 0x3933,
|
||||
Varie_95 = 0x3935,
|
||||
Yonezawa_S_Pal_96 = 0x3936,
|
||||
Kaneko_97 = 0x3937,
|
||||
Pack_In_Video_99 = 0x3939,
|
||||
Bottom_Up_9H = 0x3948,
|
||||
Konami_Yu_Gi_Oh_A4 = 0x4134,
|
||||
MTO_BL = 0x424C,
|
||||
Kodansha_DK = 0x444B
|
||||
};
|
||||
|
||||
enum OldLicenseeCode : u8 {
|
||||
None_00 = 0x00,
|
||||
Nintendo_01 = 0x01,
|
||||
Capcom_08 = 0x08,
|
||||
HOT_B_09 = 0x09,
|
||||
Jaleco_0A = 0x0A,
|
||||
Coconuts_Japan_0B = 0x0B,
|
||||
Elite_Systems_0C = 0x0C,
|
||||
EA_Electronic_Arts_13 = 0x13,
|
||||
Hudson_Soft_18 = 0x18,
|
||||
ITC_Entertainment_19 = 0x19,
|
||||
Yanoman_1A = 0x1A,
|
||||
Japan_Clary_1D = 0x1D,
|
||||
Virgin_Games_Ltd_1F = 0x1F,
|
||||
PCM_Complete_24 = 0x24,
|
||||
San_X_25 = 0x25,
|
||||
Kemco_28 = 0x28,
|
||||
SETA_Corporation_29 = 0x29,
|
||||
Infogrames_30 = 0x30,
|
||||
Nintendo_31 = 0x31,
|
||||
Bandai_32 = 0x32,
|
||||
UseNewLicensee_33 = 0x33,
|
||||
Konami_34 = 0x34,
|
||||
HectorSoft_35 = 0x35,
|
||||
Capcom_38 = 0x38,
|
||||
Banpresto_39 = 0x39,
|
||||
Entertainment_Interactive_3C = 0x3C,
|
||||
Gremlin_3E = 0x3E,
|
||||
Ubi_Soft_41 = 0x41,
|
||||
Atlus_42 = 0x42,
|
||||
Malibu_Interactive_44 = 0x44,
|
||||
Angel_46 = 0x46,
|
||||
Spectrum_HoloByte_47 = 0x47,
|
||||
Irem_49 = 0x49,
|
||||
Virgin_Games_Ltd_4A = 0x4A,
|
||||
Malibu_Interactive_4D = 0x4D,
|
||||
U_S_Gold_4F = 0x4F,
|
||||
Absolute_50 = 0x50,
|
||||
Acclaim_Entertainment_51 = 0x51,
|
||||
Activision_52 = 0x52,
|
||||
Sammy_USA_Corporation_53 = 0x53,
|
||||
GameTek_54 = 0x54,
|
||||
Park_Place_55 = 0x55,
|
||||
LJN_56 = 0x56,
|
||||
Matchbox_57 = 0x57,
|
||||
Milton_Bradley_Company_59 = 0x59,
|
||||
Mindscape_5A = 0x5A,
|
||||
Romstar_5B = 0x5B,
|
||||
Naxat_Soft_5C = 0x5C,
|
||||
Tradewest_5D = 0x5D,
|
||||
Titus_Interactive_60 = 0x60,
|
||||
Virgin_Games_Ltd_61 = 0x61,
|
||||
Ocean_Software_67 = 0x67,
|
||||
EA_Electronic_Arts_69 = 0x69,
|
||||
Elite_Systems_6E = 0x6E,
|
||||
Electro_Brain_6F = 0x6F,
|
||||
Infogrames_70 = 0x70,
|
||||
Interplay_Entertainment_71 = 0x71,
|
||||
Broderbund_72 = 0x72,
|
||||
Sculptured_Software_73 = 0x73,
|
||||
The_Sales_Curve_Limited_75 = 0x75,
|
||||
THQ_78 = 0x78,
|
||||
Accolade_79 = 0x79,
|
||||
Triffix_Entertainment_7A = 0x7A,
|
||||
MicroProse_7C = 0x7C,
|
||||
Kemco_7F = 0x7F,
|
||||
Misawa_Entertainment_80 = 0x80,
|
||||
LOZC_G_83 = 0x83,
|
||||
Tokuma_Shoten_86 = 0x86,
|
||||
Bullet_Proof_Software_8B = 0x8B,
|
||||
Vic_Tokai_Corp_8C = 0x8C,
|
||||
Ape_Inc_8E = 0x8E,
|
||||
I_Max_8F = 0x8F,
|
||||
Chunsoft_Co_91 = 0x91,
|
||||
Video_System_92 = 0x92,
|
||||
Tsubaraya_Productions_93 = 0x93,
|
||||
Varie_95 = 0x95,
|
||||
Yonezawa_S_Pal_96 = 0x96,
|
||||
Kemco_97 = 0x97,
|
||||
Arc_99 = 0x99,
|
||||
Nihon_Bussan_9A = 0x9A,
|
||||
Tecmo_9B = 0x9B,
|
||||
Imagineer_9C = 0x9C,
|
||||
Banpresto_9D = 0x9D,
|
||||
Nova_9F = 0x9F,
|
||||
Hori_Electric_A1 = 0xA1,
|
||||
Bandai_A2 = 0xA2,
|
||||
Konami_A4 = 0xA4,
|
||||
Kawada_A6 = 0xA6,
|
||||
Takara_A7 = 0xA7,
|
||||
Technos_Japan_A9 = 0xA9,
|
||||
Broderbund_AA = 0xAA,
|
||||
Toei_Animation_AC = 0xAC,
|
||||
Toho_AD = 0xAD,
|
||||
Namco_AF = 0xAF,
|
||||
Acclaim_Entertainment_B0 = 0xB0,
|
||||
ASCII_Corporation_or_Nexsoft_B1 = 0xB1,
|
||||
Bandai_B2 = 0xB2,
|
||||
Square_Enix_B4 = 0xB4,
|
||||
HAL_Laboratory_B6 = 0xB6,
|
||||
SNK_B7 = 0xB7,
|
||||
Pony_Canyon_B9 = 0xB9,
|
||||
Culture_Brain_BA = 0xBA,
|
||||
Sunsoft_BB = 0xBB,
|
||||
Sony_Imagesoft_BD = 0xBD,
|
||||
Sammy_Corporation_BF = 0xBF,
|
||||
Taito_C0 = 0xC0,
|
||||
Kemco_C2 = 0xC2,
|
||||
Square_C3 = 0xC3,
|
||||
Tokuma_Shoten_C4 = 0xC4,
|
||||
Data_East_C5 = 0xC5,
|
||||
Tonkin_House_C6 = 0xC6,
|
||||
Koei_C8 = 0xC8,
|
||||
UFL_C9 = 0xC9,
|
||||
Ultra_Games_CA = 0xCA,
|
||||
VAP_Inc_CB = 0xCB,
|
||||
Use_Corporation_CC = 0xCC,
|
||||
Meldac_CD = 0xCD,
|
||||
Pony_Canyon_CE = 0xCE,
|
||||
Angel_CF = 0xCF,
|
||||
Taito_D0 = 0xD0,
|
||||
SOFEL_Software_Engineering_Lab_D1 = 0xD1,
|
||||
Quest_D2 = 0xD2,
|
||||
Sigma_Enterprises_D3 = 0xD3,
|
||||
ASK_Kodansha_Co_D4 = 0xD4,
|
||||
Naxat_Soft_D6 = 0xD6,
|
||||
Copya_System_D7 = 0xD7,
|
||||
Banpresto_D9 = 0xD9,
|
||||
Tomy_DA = 0xDA,
|
||||
LJN_DB = 0xDB,
|
||||
Nippon_Computer_Systems_DD = 0xDD,
|
||||
Human_Ent_DE = 0xDE,
|
||||
Altron_DF = 0xDF,
|
||||
Jaleco_E0 = 0xE0,
|
||||
Towa_Chiki_E1 = 0xE1,
|
||||
Yutaka_E2 = 0xE2,
|
||||
Varie_E3 = 0xE3,
|
||||
Epoch_E5 = 0xE5,
|
||||
Athena_E7 = 0xE7,
|
||||
Asmik_Ace_Entertainment_E8 = 0xE8,
|
||||
Natsume_E9 = 0xE9,
|
||||
King_Records_EA = 0xEA,
|
||||
Atlus_EB = 0xEB,
|
||||
Epic_Sony_Records_EC = 0xEC,
|
||||
IGS_EE = 0xEE,
|
||||
A_Wave_F0 = 0xF0,
|
||||
Extreme_Entertainment_F3 = 0xF3
|
||||
};
|
||||
|
||||
// All three title layouts occupy the same 16 bytes; no reliable auto-detection.
|
||||
struct CgbTitle {
|
||||
char text[15];
|
||||
CgbFlag cgb_flag;
|
||||
};
|
||||
|
||||
struct ManufacturerTitle {
|
||||
char text[11];
|
||||
char manufacturer_code[4];
|
||||
CgbFlag cgb_flag;
|
||||
};
|
||||
|
||||
union CartridgeTitle {
|
||||
char dmg_text[16];
|
||||
CgbTitle cgb;
|
||||
ManufacturerTitle manufacturer;
|
||||
};
|
||||
|
||||
union NewLicensee {
|
||||
char ascii[2];
|
||||
be NewLicenseeCode publisher;
|
||||
};
|
||||
|
||||
struct GameBoyHeader {
|
||||
u8 entry_point_code[4]; // 0100: instructions, not an integer pointer.
|
||||
u8 nintendo_logo[48]; // 0104: boot logo, ends at 0133.
|
||||
CartridgeTitle title; // 0134: overlapping DMG/CGB title layouts.
|
||||
NewLicensee new_licensee; // 0144: used only when old_licensee == 0x33.
|
||||
SgbFlag sgb_flag; // 0146: SGB also requires old_licensee 0x33.
|
||||
CartridgeType cartridge_type; // 0147.
|
||||
RomSize rom_size; // 0148.
|
||||
RamSize ram_size; // 0149.
|
||||
Destination destination; // 014A.
|
||||
OldLicenseeCode old_licensee; // 014B.
|
||||
u8 rom_version; // 014C.
|
||||
u8 header_checksum; // 014D: subtract each byte 0134..014C plus 1.
|
||||
be u16 global_checksum; // 014E: sum ROM bytes excluding 014E..014F.
|
||||
};
|
||||
|
||||
// These slots contain code, not address pointers; unused vectors may hold data.
|
||||
struct GameBoyVectors {
|
||||
u8 rst_00[8];
|
||||
u8 rst_08[8];
|
||||
u8 rst_10[8];
|
||||
u8 rst_18[8];
|
||||
u8 rst_20[8];
|
||||
u8 rst_28[8];
|
||||
u8 rst_30[8];
|
||||
u8 rst_38[8];
|
||||
u8 interrupt_vblank_40[8];
|
||||
u8 interrupt_lcd_stat_48[8];
|
||||
u8 interrupt_timer_50[8];
|
||||
u8 interrupt_serial_58[8];
|
||||
u8 interrupt_joypad_60[8];
|
||||
};
|
||||
|
||||
// Register layouts describe bit positions, not read/write side effects.
|
||||
// Unused bits often read as 1; write-only bits cannot be recovered from a dump.
|
||||
enum TimerClock : u8 {
|
||||
Hz4096 = 0,
|
||||
Hz262144 = 1,
|
||||
Hz65536 = 2,
|
||||
Hz16384 = 3
|
||||
};
|
||||
|
||||
enum PpuMode : u8 {
|
||||
HBlank = 0,
|
||||
VBlank = 1,
|
||||
OamScan = 2,
|
||||
Drawing = 3
|
||||
};
|
||||
|
||||
enum DmgShade : u8 {
|
||||
White = 0,
|
||||
LightGray = 1,
|
||||
DarkGray = 2,
|
||||
Black = 3
|
||||
};
|
||||
|
||||
enum DutyCycle : u8 {
|
||||
Percent12_5 = 0,
|
||||
Percent25 = 1,
|
||||
Percent50 = 2,
|
||||
Percent75 = 3
|
||||
};
|
||||
|
||||
enum WaveLevel : u8 {
|
||||
Mute = 0,
|
||||
Full = 1,
|
||||
Half = 2,
|
||||
Quarter = 3
|
||||
};
|
||||
|
||||
bitfield Joypad {
|
||||
a_or_right_released : 1;
|
||||
b_or_left_released : 1;
|
||||
select_or_up_released : 1;
|
||||
start_or_down_released : 1;
|
||||
dpad_deselected : 1; // 0 selects directions.
|
||||
buttons_deselected : 1; // 0 selects buttons; all inputs active-low.
|
||||
padding : 2;
|
||||
};
|
||||
|
||||
bitfield SerialControl {
|
||||
internal_clock : 1;
|
||||
cgb_fast_clock : 1;
|
||||
padding : 5;
|
||||
transfer_requested : 1;
|
||||
};
|
||||
|
||||
bitfield TimerControl {
|
||||
TimerClock clock : 2; // Normal-speed rates; double on CGB double speed.
|
||||
enabled : 1;
|
||||
padding : 5;
|
||||
};
|
||||
|
||||
bitfield Interrupts {
|
||||
vblank : 1;
|
||||
lcd_stat : 1;
|
||||
timer : 1;
|
||||
serial : 1;
|
||||
joypad : 1;
|
||||
padding : 3;
|
||||
};
|
||||
|
||||
bitfield AudioSweep {
|
||||
shift : 3;
|
||||
subtract : 1;
|
||||
pace : 3;
|
||||
padding : 1;
|
||||
};
|
||||
|
||||
bitfield AudioDutyLength {
|
||||
initial_length_timer : 6; // Length = 64 - value; write-only portion.
|
||||
DutyCycle duty : 2;
|
||||
};
|
||||
|
||||
bitfield AudioEnvelope {
|
||||
pace : 3;
|
||||
increase : 1;
|
||||
initial_volume : 4;
|
||||
};
|
||||
|
||||
bitfield AudioPeriodHigh {
|
||||
period_high : 3;
|
||||
padding : 3;
|
||||
length_enabled : 1;
|
||||
trigger : 1; // Write 1 to trigger; not a readable state bit.
|
||||
};
|
||||
|
||||
bitfield WaveDac {
|
||||
padding : 7;
|
||||
enabled : 1;
|
||||
};
|
||||
|
||||
bitfield WaveOutputLevel {
|
||||
padding : 5;
|
||||
WaveLevel level : 2;
|
||||
padding : 1;
|
||||
};
|
||||
|
||||
bitfield NoiseLength {
|
||||
initial_length_timer : 6;
|
||||
padding : 2;
|
||||
};
|
||||
|
||||
bitfield NoisePolynomial {
|
||||
clock_divider : 3; // Divisors: 8,16,32,48,64,80,96,112.
|
||||
short_lfsr : 1; // 1: 7-bit; 0: 15-bit.
|
||||
clock_shift : 4;
|
||||
};
|
||||
|
||||
bitfield NoiseControl {
|
||||
padding : 6;
|
||||
length_enabled : 1;
|
||||
trigger : 1;
|
||||
};
|
||||
|
||||
bitfield AudioVolume {
|
||||
right_volume : 3;
|
||||
vin_right : 1;
|
||||
left_volume : 3;
|
||||
vin_left : 1;
|
||||
};
|
||||
|
||||
bitfield AudioPanning {
|
||||
channel1_right : 1;
|
||||
channel2_right : 1;
|
||||
channel3_right : 1;
|
||||
channel4_right : 1;
|
||||
channel1_left : 1;
|
||||
channel2_left : 1;
|
||||
channel3_left : 1;
|
||||
channel4_left : 1;
|
||||
};
|
||||
|
||||
bitfield AudioMaster {
|
||||
channel1_active : 1; // Bits 0..3 read-only.
|
||||
channel2_active : 1;
|
||||
channel3_active : 1;
|
||||
channel4_active : 1;
|
||||
padding : 3;
|
||||
audio_enabled : 1;
|
||||
};
|
||||
|
||||
bitfield WaveSamples {
|
||||
second_sample : 4;
|
||||
first_sample : 4;
|
||||
};
|
||||
|
||||
bitfield LcdControl {
|
||||
bg_window_enable_or_cgb_priority : 1;
|
||||
objects_enabled : 1;
|
||||
objects_8x16 : 1;
|
||||
bg_map_9c00 : 1;
|
||||
tile_data_8000_unsigned : 1; // 0: signed IDs relative to 9000.
|
||||
window_enabled : 1;
|
||||
window_map_9c00 : 1;
|
||||
lcd_enabled : 1;
|
||||
};
|
||||
|
||||
bitfield LcdStatus {
|
||||
PpuMode mode : 2; // Read-only.
|
||||
ly_equals_lyc : 1; // Read-only.
|
||||
hblank_interrupt : 1;
|
||||
vblank_interrupt : 1;
|
||||
oam_interrupt : 1;
|
||||
lyc_interrupt : 1;
|
||||
padding : 1;
|
||||
};
|
||||
|
||||
bitfield DmgPalette {
|
||||
DmgShade color0 : 2; // Ignored for OBP0/OBP1: OBJ index 0 is transparent.
|
||||
DmgShade color1 : 2;
|
||||
DmgShade color2 : 2;
|
||||
DmgShade color3 : 2;
|
||||
};
|
||||
|
||||
bitfield CpuMode {
|
||||
padding : 2;
|
||||
dmg_compatibility : 1; // Boot-ROM-only; locked when boot ROM is unmapped.
|
||||
unknown : 5;
|
||||
};
|
||||
|
||||
bitfield SpeedSwitch {
|
||||
switch_armed : 1; // Arm, then execute STOP.
|
||||
padding : 6;
|
||||
double_speed : 1; // Read-only.
|
||||
};
|
||||
|
||||
bitfield VramBank {
|
||||
bank : 1;
|
||||
padding : 7;
|
||||
};
|
||||
|
||||
bitfield VramDmaControl {
|
||||
blocks_minus_one : 7; // Transfer length = (value + 1) * 16 bytes.
|
||||
hblank_on_write_inactive_on_read : 1;
|
||||
};
|
||||
|
||||
bitfield Infrared {
|
||||
led_on : 1;
|
||||
no_signal : 1; // Read-only, 0 means receiving.
|
||||
padding : 4;
|
||||
read_enable : 2; // 3 enables receiver; 0 disables it.
|
||||
};
|
||||
|
||||
bitfield PaletteIndex {
|
||||
byte_index : 6;
|
||||
padding : 1;
|
||||
auto_increment_on_write : 1;
|
||||
};
|
||||
|
||||
bitfield ObjectPriority {
|
||||
dmg_x_priority : 1; // 0: CGB OAM order; 1: DMG x-coordinate order.
|
||||
padding : 7;
|
||||
};
|
||||
|
||||
bitfield WramBank {
|
||||
bank : 3; // 0 aliases bank 1.
|
||||
padding : 5;
|
||||
};
|
||||
|
||||
bitfield Undocumented75 {
|
||||
padding : 4;
|
||||
storage : 3;
|
||||
padding : 1;
|
||||
};
|
||||
|
||||
bitfield Pcm12 {
|
||||
channel1 : 4;
|
||||
channel2 : 4;
|
||||
};
|
||||
|
||||
bitfield Pcm34 {
|
||||
channel3 : 4;
|
||||
channel4 : 4;
|
||||
};
|
||||
|
||||
// Offsets below are relative to FF00; gaps preserve exact register addresses.
|
||||
// CGB-only fields are retained in DMG dumps for address consistency.
|
||||
struct GameBoyIo {
|
||||
Joypad JOYP; // FF00: joypad / SGB packet transport.
|
||||
u8 SB; // FF01: serial data.
|
||||
SerialControl SC; // FF02.
|
||||
u8 unused_ff03;
|
||||
u8 DIV; // FF04: any write resets divider.
|
||||
u8 TIMA; // FF05: timer counter.
|
||||
u8 TMA; // FF06: timer reload value.
|
||||
TimerControl TAC; // FF07.
|
||||
u8 unused_ff08_ff0e[7];
|
||||
Interrupts IF; // FF0F: requested interrupts.
|
||||
AudioSweep NR10; // FF10: channel 1.
|
||||
AudioDutyLength NR11; // FF11.
|
||||
AudioEnvelope NR12; // FF12.
|
||||
u8 NR13; // FF13: period low, write-only.
|
||||
AudioPeriodHigh NR14; // FF14.
|
||||
u8 unused_ff15;
|
||||
AudioDutyLength NR21; // FF16: channel 2.
|
||||
AudioEnvelope NR22; // FF17.
|
||||
u8 NR23; // FF18: period low, write-only.
|
||||
AudioPeriodHigh NR24; // FF19.
|
||||
WaveDac NR30; // FF1A: channel 3.
|
||||
u8 NR31; // FF1B: length = 256 - value, write-only.
|
||||
WaveOutputLevel NR32; // FF1C.
|
||||
u8 NR33; // FF1D: period low, write-only.
|
||||
AudioPeriodHigh NR34; // FF1E.
|
||||
u8 unused_ff1f;
|
||||
NoiseLength NR41; // FF20: channel 4.
|
||||
AudioEnvelope NR42; // FF21.
|
||||
NoisePolynomial NR43; // FF22.
|
||||
NoiseControl NR44; // FF23.
|
||||
AudioVolume NR50; // FF24.
|
||||
AudioPanning NR51; // FF25.
|
||||
AudioMaster NR52; // FF26.
|
||||
u8 unused_ff27_ff2f[9];
|
||||
WaveSamples wave_ram[16]; // FF30..FF3F: 32 four-bit samples.
|
||||
LcdControl LCDC; // FF40.
|
||||
LcdStatus STAT; // FF41.
|
||||
u8 SCY; // FF42: scroll y.
|
||||
u8 SCX; // FF43: scroll x.
|
||||
u8 LY; // FF44: scanline, read-only.
|
||||
u8 LYC; // FF45: scanline compare.
|
||||
u8 DMA; // FF46: write source high byte; copies 160 bytes to OAM.
|
||||
DmgPalette BGP; // FF47: non-CGB background palette.
|
||||
DmgPalette OBP0; // FF48: non-CGB object palette 0.
|
||||
DmgPalette OBP1; // FF49: non-CGB object palette 1.
|
||||
u8 WY; // FF4A: window y.
|
||||
u8 WX; // FF4B: window x + 7.
|
||||
CpuMode KEY0; // FF4C: CGB.
|
||||
SpeedSwitch KEY1; // FF4D: CGB.
|
||||
u8 unused_ff4e;
|
||||
VramBank VBK; // FF4F: CGB.
|
||||
u8 BOOT; // FF50: boot ROM unmap, write-only.
|
||||
u8 HDMA1; // FF51: CGB DMA source high, write-only.
|
||||
u8 HDMA2; // FF52: source low, low nibble ignored, write-only.
|
||||
u8 HDMA3; // FF53: destination bits 12..8, write-only.
|
||||
u8 HDMA4; // FF54: destination bits 7..4, write-only.
|
||||
VramDmaControl HDMA5; // FF55: CGB.
|
||||
Infrared RP; // FF56: CGB infrared.
|
||||
u8 unused_ff57_ff67[17];
|
||||
PaletteIndex BCPS; // FF68: CGB BG palette index (BGPI).
|
||||
u8 BCPD; // FF69: CGB BG palette byte (BGPD).
|
||||
PaletteIndex OCPS; // FF6A: CGB OBJ palette index (OBPI).
|
||||
u8 OCPD; // FF6B: CGB OBJ palette byte (OBPD).
|
||||
ObjectPriority OPRI; // FF6C: CGB.
|
||||
u8 unused_ff6d_ff6f[3];
|
||||
WramBank SVBK; // FF70: CGB.
|
||||
u8 unused_ff71;
|
||||
u8 undocumented_ff72; // CGB storage, R/W.
|
||||
u8 undocumented_ff73; // CGB storage, R/W.
|
||||
u8 undocumented_ff74; // CGB-mode storage, R/W.
|
||||
Undocumented75 undocumented_ff75;
|
||||
Pcm12 PCM12; // FF76: CGB channel 1/2 output, read-only.
|
||||
Pcm34 PCM34; // FF77: CGB channel 3/4 output, read-only.
|
||||
u8 unused_ff78_ff7f[8];
|
||||
};
|
||||
|
||||
// Graphics structures can also be placed manually over known ROM asset offsets.
|
||||
struct TileRow2bpp {
|
||||
u8 low_plane; // Bit 7 is the leftmost pixel.
|
||||
u8 high_plane; // Pixel index = low bit | (high bit << 1).
|
||||
};
|
||||
|
||||
struct Tile2bpp {
|
||||
TileRow2bpp rows[8];
|
||||
};
|
||||
|
||||
bitfield BgMapAttributes {
|
||||
cgb_palette : 3;
|
||||
tile_vram_bank : 1;
|
||||
padding : 1;
|
||||
x_flip : 1;
|
||||
y_flip : 1;
|
||||
bg_priority : 1;
|
||||
};
|
||||
|
||||
union TileMap {
|
||||
u8 tile_ids[1024]; // VRAM bank 0: 32 rows of 32 tile IDs.
|
||||
BgMapAttributes attributes[1024]; // VRAM bank 1: corresponding CGB attributes.
|
||||
};
|
||||
|
||||
struct GameBoyVram {
|
||||
Tile2bpp tiles[384]; // 8000..97FF.
|
||||
TileMap map_9800;
|
||||
TileMap map_9c00;
|
||||
};
|
||||
|
||||
bitfield ObjectAttributes {
|
||||
cgb_palette : 3;
|
||||
cgb_tile_bank : 1;
|
||||
dmg_palette_1 : 1;
|
||||
x_flip : 1;
|
||||
y_flip : 1;
|
||||
behind_bg_colors_1_to_3 : 1;
|
||||
};
|
||||
|
||||
struct OamObject {
|
||||
u8 y_plus_16;
|
||||
u8 x_plus_8;
|
||||
u8 tile_index; // In 8x16 mode, bit 0 is ignored.
|
||||
ObjectAttributes attributes;
|
||||
};
|
||||
|
||||
bitfield Rgb555 {
|
||||
red : 5;
|
||||
green : 5;
|
||||
blue : 5;
|
||||
padding : 1;
|
||||
};
|
||||
|
||||
struct CgbPalette {
|
||||
Rgb555 colors[4];
|
||||
};
|
||||
|
||||
// Not directly CPU-mapped: dump separately through BGPI/BGPD or OBPI/OBPD.
|
||||
// Each palette RAM bank contains eight palettes (64 bytes).
|
||||
struct CgbPaletteRam {
|
||||
CgbPalette palettes[8];
|
||||
};
|
||||
|
||||
// MBC control registers are WRITE destinations, not readable ROM structures.
|
||||
// These enums are reference constants; no control state is overlaid onto ROM.
|
||||
// Other cartridge types above need their own mapper-specific device protocols.
|
||||
enum Mbc1WriteAddress : u16 {
|
||||
RamEnable_0000_1fff = 0x0000, // Low nibble 0xA enables; anything else disables.
|
||||
RomBankLow5_2000_3fff = 0x2000, // 0 aliases 1 before masking to ROM size.
|
||||
BankHigh2_4000_5fff = 0x4000, // Upper ROM bits or RAM bank, depends on wiring.
|
||||
BankingMode_6000_7fff = 0x6000 // 0: simple; 1: advanced RAM/ROM banking.
|
||||
};
|
||||
|
||||
enum Mbc1Mode : u8 {
|
||||
Simple = 0,
|
||||
Advanced = 1
|
||||
};
|
||||
|
||||
enum RamEnableValue : u8 {
|
||||
Disable = 0x00,
|
||||
Enable = 0x0A
|
||||
};
|
||||
|
||||
// MBC2 decodes address bit 8 throughout 0000..3FFF, not two contiguous ranges.
|
||||
enum Mbc2WriteAddress : u16 {
|
||||
RamEnable_AddressBit8Clear = 0x0000,
|
||||
RomBankLow4_AddressBit8Set = 0x2100 // 0 aliases 1; RAM cells at A000..A1FF, mirrored.
|
||||
};
|
||||
|
||||
enum Mbc3WriteAddress : u16 {
|
||||
RamRtcEnable_0000_1fff = 0x0000,
|
||||
RomBank7_2000_3fff = 0x2000, // 0 aliases 1 (MBC30 uses 8 bits).
|
||||
RamRtcSelect_4000_5fff = 0x4000,
|
||||
LatchClock_6000_7fff = 0x6000 // Write 0, then 1, to latch RTC.
|
||||
};
|
||||
|
||||
enum Mbc3RamRtcSelect : u8 {
|
||||
RamBank0 = 0x00,
|
||||
RamBank1 = 0x01,
|
||||
RamBank2 = 0x02,
|
||||
RamBank3 = 0x03,
|
||||
Mbc30RamBank4 = 0x04,
|
||||
Mbc30RamBank5 = 0x05,
|
||||
Mbc30RamBank6 = 0x06,
|
||||
Mbc30RamBank7 = 0x07,
|
||||
RtcSeconds = 0x08,
|
||||
RtcMinutes = 0x09,
|
||||
RtcHours = 0x0A,
|
||||
RtcDayLow = 0x0B,
|
||||
RtcDayHigh = 0x0C
|
||||
};
|
||||
|
||||
bitfield RtcDayHigh {
|
||||
day_bit8 : 1;
|
||||
padding : 5;
|
||||
halted : 1;
|
||||
day_overflow : 1;
|
||||
};
|
||||
|
||||
// A convenient five-byte export layout, NOT five adjacent CPU registers.
|
||||
// Only the selected RTC register appears at A000..BFFF, repeated at every byte.
|
||||
struct Mbc3RtcSnapshot {
|
||||
u8 seconds;
|
||||
u8 minutes;
|
||||
u8 hours;
|
||||
u8 day_low;
|
||||
RtcDayHigh day_high;
|
||||
};
|
||||
|
||||
enum Mbc5WriteAddress : u16 {
|
||||
RamEnable_0000_1fff = 0x0000,
|
||||
RomBankLow8_2000_2fff = 0x2000, // Bank 0 is allowed.
|
||||
RomBankBit8_3000_3fff = 0x3000,
|
||||
RamBank4_4000_5fff = 0x4000 // Rumble carts: bit 3 motor, bits 0..2 RAM bank.
|
||||
};
|
||||
|
||||
bitfield Mbc5RumbleRamBank {
|
||||
ram_bank : 3;
|
||||
rumble_on : 1;
|
||||
padding : 4;
|
||||
};
|
||||
|
||||
// A raw ROM bank N starts at file offset N * 0x4000.
|
||||
// Its usual CPU address is 0x4000 + (file_offset % 0x4000) for N > 0.
|
||||
struct RomBank {
|
||||
u8 bytes[0x4000];
|
||||
};
|
||||
|
||||
if (GB_CPU_MEMORY) {
|
||||
std::assert (std::mem::size () >= 0x10000, "CPU memory mode requires a 64 KiB dump");
|
||||
GameBoyVectors vectors @ 0x0000;
|
||||
GameBoyHeader header @ 0x0100;
|
||||
u8 rom0_before_header[0x98] @ 0x0068;
|
||||
u8 rom0_after_header[0x3EB0] @ 0x0150;
|
||||
RomBank selected_rom_bank @ 0x4000;
|
||||
GameBoyVram vram @ 0x8000;
|
||||
u8 cartridge_ram_or_device[0x2000] @ 0xA000;
|
||||
u8 wram_bank0[0x1000] @ 0xC000;
|
||||
u8 selected_wram_bank[0x1000] @ 0xD000;
|
||||
u8 echo_c000_ddff[0x1E00] @ 0xE000;
|
||||
OamObject oam[40] @ 0xFE00;
|
||||
u8 unusable_fea0_feff[0x60] @ 0xFEA0;
|
||||
GameBoyIo io @ 0xFF00;
|
||||
u8 hram[0x7F] @ 0xFF80;
|
||||
Interrupts IE @ 0xFFFF;
|
||||
} else {
|
||||
std::assert (std::mem::size () >= 0x150, "ROM is too short for a Game Boy header");
|
||||
GameBoyVectors vectors @ 0x0000;
|
||||
GameBoyHeader header @ 0x0100;
|
||||
}
|
||||
+256
-13
@@ -3,10 +3,11 @@
|
||||
* plugin.ts - radare2 core plugin (r2js) running the hexpat library.
|
||||
* R2Host implements the library's HexpatHost interface on top of r2 commands.
|
||||
*/
|
||||
import { compile, evaluate, pragma, toJson, walkPatterns, HexpatHost, PatternError, PatternInstance, Pattern } from "../lib/index";
|
||||
import { compile, evaluate, decodeType, pragma, toJson, patternToJson, dumpTree, patternColor, walkPatterns,
|
||||
HexpatHost, PatternError, PatternInstance, Pattern, SearchPattern } from "../lib/index";
|
||||
import * as A from "../lib/ast";
|
||||
|
||||
/** Host reading the data loaded in r2 (`p8`), translated by a base address. */
|
||||
/** Host accessing r2 I/O (`p8`/`wx`), translated by a base address. */
|
||||
export class R2Host implements HexpatHost {
|
||||
includeDirs: string[] = [];
|
||||
constructor(private r2: R2JsBridge, public baseAddress = 0) {}
|
||||
@@ -25,6 +26,17 @@ export class R2Host implements HexpatHost {
|
||||
return out;
|
||||
}
|
||||
|
||||
write(addr: number, bytes: Uint8Array): void {
|
||||
if (!bytes.length) return;
|
||||
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
const address = "0x" + (this.baseAddress + addr).toString(16);
|
||||
const status = this.r2.cmd("wx " + hex + " @ " + address + ";?v $?").trim();
|
||||
if (status !== "0x0" && status !== "0")
|
||||
throw new Error("cannot write at " + address + ": open with -w or enable io.cache");
|
||||
const actual = this.read(addr, bytes.length);
|
||||
if (actual.some((b, i) => b !== bytes[i])) throw new Error("write failed at " + address);
|
||||
}
|
||||
|
||||
quiet = false; // drop std::print output (JSON and r2 script output must stay parseable)
|
||||
print(msg: string): void { if (!this.quiet) r2.log(msg); }
|
||||
|
||||
@@ -59,10 +71,10 @@ export class R2Host implements HexpatHost {
|
||||
}
|
||||
}
|
||||
|
||||
const VERSION = "0.2.0";
|
||||
const VERSION = "0.3.0";
|
||||
|
||||
const HELP = [
|
||||
"Usage: hexpat[?+-jl*es] [args] # ImHex pattern language",
|
||||
"Usage: hexpat[?+-jl*espmt] [args] # ImHex pattern language",
|
||||
"| hexpat [-q] [-I dir] [file] evaluate file (replacing the loaded ones) and show its patterns",
|
||||
"| hexpat show the patterns of the loaded files",
|
||||
"| hexpat+ [-I dir] file evaluate file and add it to the loaded ones",
|
||||
@@ -70,9 +82,13 @@ const HELP = [
|
||||
"| hexpat-v show the r2hexpat version",
|
||||
"| hexpatj [file] show the patterns as JSON",
|
||||
"| hexpat* [file] show the patterns as r2 commands (flags, comments, data hints)",
|
||||
"| hexpat-* show r2 commands to remove generated flags (.hexpat-* applies them)",
|
||||
"| hexpatl[j] list the loaded files and their top-level patterns",
|
||||
"| hexpats [file] one-line summary (types, fields, patterns) or the error, for status bars",
|
||||
"| hexpate expr evaluate an expression in the context of the last loaded file",
|
||||
"| hexpate expr evaluate an expression or assignment in the last loaded file (writes r2 I/O)",
|
||||
"| hexpatp[?+=-ijl] ... create partial instances and masked search patterns (hexpatp? for help)",
|
||||
"| hexpatm [options] Type print a libmagic decoder for a fixed type (hexpatm? for help)",
|
||||
"| hexpatt[j*] [options] [Type [address]|pattern] view a type at an address or a loaded pattern as a tree",
|
||||
"| hexpat? show this help",
|
||||
"| options: -q (evaluate only), -I dir (add #include / import search path)",
|
||||
"| file can be 'base64:<data>' to evaluate the encoded source instead of reading a file",
|
||||
@@ -82,6 +98,49 @@ interface Session { file: string; program: A.Program; instance: PatternInstance;
|
||||
|
||||
const B64 = "base64:";
|
||||
const sessions: Session[] = [];
|
||||
const searchPatterns = new Map<string, SearchPattern>();
|
||||
|
||||
const PATTERN_HELP = [
|
||||
"Usage: hexpatp[?+=-ijl] [options] [args] # partial instances for /x searches",
|
||||
"| hexpatp [options] Type [field=expr ...] one-shot /x bytes:mask; omitted fields are wildcards",
|
||||
"| hexpatp name /x bytes:mask of a named instance",
|
||||
"| hexpatp+ [options] name Type [field=expr ...] create a named instance (all fields start unset)",
|
||||
"| hexpatp= name field=expr ... set fields; zero is a defined value",
|
||||
"| hexpatp- name field ... unset fields (or aggregates)",
|
||||
"| hexpatp- name delete the named instance; '*' deletes all",
|
||||
"| hexpatpi [options] name|Type [fields ...] inspect offsets, sizes and set/unset fields",
|
||||
"| hexpatpj [options] name|Type [fields ...] inspect as JSON, including bytes and bit mask",
|
||||
"| hexpatpl[j] list named instances (optionally JSON)",
|
||||
"| options: -f file (compile types only), -I dir (includes), -a/--align 1|2|4|8|16",
|
||||
"| defaults: last loaded file, packed layout (-a 1); pointers use explicit hexpat widths",
|
||||
"| fields: nested.member=expr, array[index]=expr; enum names, arithmetic and quoted strings work",
|
||||
"| padding stays wildcarded; dynamic layouts and conflicting union fields are rejected",
|
||||
"| search: /x `hexpatp -f game.hexpat Player health=100 lives=3`",
|
||||
];
|
||||
|
||||
const MAGIC_HELP = [
|
||||
"Usage: hexpatm [options] Type # libmagic header decoder for pm",
|
||||
"| hexpatm [-f file] [-I dir] [-a N] Type print a magic file for the type's fixed layout",
|
||||
"| defaults: types from the last loaded file, packed layout (-a 1)",
|
||||
"| options: -f file (compile types only), -I dir (includes), -a/--align 1|2|4|8|16",
|
||||
"| fields: nested structs, unions, arrays, strings, 1/2/4/8-byte integers and pointers, float/double",
|
||||
"| integers/enums print raw hex; padding is omitted; dynamic layouts and unsupported fields are rejected",
|
||||
"| export: hexpatm -f demo/test.hexpat MachHeader > /tmp/macho.magic",
|
||||
"| decode: pm /tmp/macho.magic @ 0 (increase block size with b if the header is larger)",
|
||||
];
|
||||
|
||||
const TREE_HELP = [
|
||||
"Usage: hexpatt[j*] [options] [Type [address]|pattern] # address-bound pattern tree",
|
||||
"| hexpatt tree of loaded patterns, with values, colors and attributes",
|
||||
"| hexpatt pattern tree of a loaded pattern (nested paths also work)",
|
||||
"| hexpatt [-f file] [-I dir] Type [address] decode a type at an r2 address (default: current seek)",
|
||||
"| hexpattj ... JSON including color, comment, displayName and attributes",
|
||||
"| hexpatt* ... r2 commands for flags, flag colors/comments and data hints",
|
||||
"| types: last loaded file or -f file; only types and constants are loaded, not placements or main()",
|
||||
"| layouts: hexpat declarations, including data-dependent arrays; addresses honor base_address",
|
||||
"| examples: hexpatt -f demo/player.hexpat Player @ 0x100; hexpatt header",
|
||||
"| apply/remove flags: .hexpatt* Player @ 0x100; .hexpat-*",
|
||||
];
|
||||
|
||||
/** Collect output lines and print them through r_cons at once (so ~grep and | work). */
|
||||
class Out {
|
||||
@@ -90,6 +149,174 @@ class Out {
|
||||
flush(): void { if (this.lines.length) r2.log(this.lines.join("\n")); this.lines = []; }
|
||||
}
|
||||
|
||||
/** Read one CLI argument while retaining the remainder as pattern-language expressions. */
|
||||
function word(src: string): { value: string; rest: string } {
|
||||
const s = src.trimStart();
|
||||
if (!s) return { value: "", rest: "" };
|
||||
const quote = s[0] === '"' || s[0] === "'" ? s[0] : "";
|
||||
let value = "", i = quote ? 1 : 0;
|
||||
for (; i < s.length; i++) {
|
||||
if (quote ? s[i] === quote : /\s/.test(s[i])) return { value, rest: s.substring(i + 1).trimStart() };
|
||||
if (quote && s[i] === "\\" && i + 1 < s.length && (s[i + 1] === quote || s[i + 1] === "\\")) i++;
|
||||
value += s[i];
|
||||
}
|
||||
if (quote) throw new Error("unterminated quoted argument");
|
||||
return { value, rest: "" };
|
||||
}
|
||||
|
||||
function patternOptions(src: string, allowAlignment = true): { file: string; includeDirs: string[]; alignment?: number; rest: string } {
|
||||
const opts = { file: "", includeDirs: [] as string[], alignment: undefined as number | undefined, rest: src.trim() };
|
||||
while (opts.rest.startsWith("-")) {
|
||||
const flag = word(opts.rest), arg = word(flag.rest);
|
||||
const allowed = allowAlignment ? ["-f", "--file", "-I", "-a", "--align"] : ["-f", "--file", "-I"];
|
||||
if (!allowed.includes(flag.value)) throw new Error("unknown layout option '" + flag.value + "'");
|
||||
if (!arg.value) throw new Error("missing value for " + flag.value);
|
||||
if (flag.value === "-f" || flag.value === "--file") opts.file = arg.value;
|
||||
else if (flag.value === "-I") opts.includeDirs.push(arg.value);
|
||||
else opts.alignment = Number(arg.value);
|
||||
opts.rest = arg.rest;
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
function searchProgram(file: string, includeDirs: string[]): A.Program {
|
||||
if (!file) {
|
||||
const s = sessions[sessions.length - 1];
|
||||
if (!s) throw new Error("no types loaded: use -f file or load a pattern with hexpat");
|
||||
if (includeDirs.length) throw new Error("-I requires -f when compiling types");
|
||||
return s.program;
|
||||
}
|
||||
const host = new R2Host(r2);
|
||||
host.includeDirs = includeDirs;
|
||||
const inline = file.startsWith(B64);
|
||||
const src = inline ? b64(file.substring(B64.length), true) : host.readFile(file);
|
||||
if (!src) throw new Error("cannot read " + file);
|
||||
return compile(src, host, inline ? "base64" : file);
|
||||
}
|
||||
|
||||
function patternCommand(tail: string, out: Out): void {
|
||||
const mode = tail[0] && "+=-ijl?".includes(tail[0]) ? tail[0] : "";
|
||||
let rest = (mode ? tail.substring(1) : tail).trim();
|
||||
if (mode === "?" || (!mode && (!rest || rest === "-h" || rest === "--help"))) {
|
||||
for (const line of PATTERN_HELP) out.push(line);
|
||||
return;
|
||||
}
|
||||
const named = (name: string): SearchPattern => {
|
||||
const p = searchPatterns.get(name);
|
||||
if (!p) throw new Error("no search instance '" + name + "'; use hexpatpl or hexpatp+");
|
||||
return p;
|
||||
};
|
||||
if (mode === "l") {
|
||||
const items = Array.from(searchPatterns, ([name, p]) => ({ name, type: p.type, size: p.size, alignment: p.alignment,
|
||||
state: p.state(), defined: p.fields().filter((f) => f.defined).length, fields: p.fields().length }));
|
||||
if (rest === "j") out.push(JSON.stringify(items));
|
||||
else if (rest) throw new Error("Usage: hexpatpl[j]");
|
||||
else for (const i of items) out.push(i.name + " " + i.type + " size=" + i.size + " align=" + i.alignment + " fields=" + i.defined + "/" + i.fields);
|
||||
return;
|
||||
}
|
||||
if (mode === "=" || mode === "-") {
|
||||
const arg = word(rest);
|
||||
if (!arg.value) throw new Error(mode === "=" ? "Usage: hexpatp= name field=expr ..." : "Usage: hexpatp- name [field ...]");
|
||||
if (mode === "-") {
|
||||
if (!arg.rest) {
|
||||
if (arg.value === "*") searchPatterns.clear();
|
||||
else { named(arg.value); searchPatterns.delete(arg.value); }
|
||||
} else {
|
||||
const p = named(arg.value), fields = arg.rest.split(/\s+/);
|
||||
for (const f of fields) p.state(f); // validate all paths before changing the instance
|
||||
for (const f of fields) p.unset(f);
|
||||
}
|
||||
} else named(arg.value).assign(arg.rest);
|
||||
return;
|
||||
}
|
||||
const opts = patternOptions(rest);
|
||||
let arg = word(opts.rest), name = "";
|
||||
if (mode === "+") {
|
||||
name = arg.value;
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) throw new Error("search instance name must be an identifier");
|
||||
if (searchPatterns.has(name)) throw new Error("search instance '" + name + "' already exists; delete it with hexpatp-");
|
||||
arg = word(arg.rest);
|
||||
}
|
||||
if (!arg.value) throw new Error("missing type or instance name; see hexpatp?");
|
||||
let p = mode !== "+" && !opts.file ? searchPatterns.get(arg.value) : undefined;
|
||||
if (p) {
|
||||
name = arg.value;
|
||||
if (opts.alignment !== undefined || opts.includeDirs.length || arg.rest) throw new Error("use hexpatp= to update a named instance");
|
||||
} else {
|
||||
p = new SearchPattern(searchProgram(opts.file, opts.includeDirs), arg.value, { alignment: opts.alignment });
|
||||
if (arg.rest) p.assign(arg.rest);
|
||||
}
|
||||
if (mode === "+") { searchPatterns.set(name, p); return; }
|
||||
if (mode === "j") out.push(JSON.stringify({ ...(name ? { name } : {}), ...p.toJSON() }));
|
||||
else if (mode === "i") {
|
||||
out.push((name ? name + " " : "") + p.type + " size=" + p.size + " align=" + p.alignment + " (" + p.state() + ")");
|
||||
for (const f of p.fields()) out.push(" 0x" + f.offset.toString(16).padStart(4, "0") + " " + String(f.size).padStart(4) +
|
||||
" " + f.path + " (" + f.type + (f.bits === undefined ? "" : ":" + f.bits) + ") = " + (f.defined ? JSON.stringify(f.value) : "unset"));
|
||||
} else out.push(p.toR2());
|
||||
}
|
||||
|
||||
function magicCommand(rest: string, out: Out): void {
|
||||
if (!rest || ["?", "-h", "--help"].includes(rest)) {
|
||||
for (const line of MAGIC_HELP) out.push(line);
|
||||
return;
|
||||
}
|
||||
const opts = patternOptions(rest), arg = word(opts.rest);
|
||||
if (!arg.value || arg.rest) throw new Error("Usage: hexpatm [-f file] [-I dir] [-a N] Type");
|
||||
const p = new SearchPattern(searchProgram(opts.file, opts.includeDirs), arg.value, { alignment: opts.alignment });
|
||||
out.push(p.toMagic());
|
||||
}
|
||||
|
||||
function treeCommand(tail: string, out: Out): void {
|
||||
const mode = tail[0] && "j*?".includes(tail[0]) ? tail[0] : "";
|
||||
const rest = (mode ? tail.substring(1) : tail).trim();
|
||||
if (mode === "?" || rest === "-h" || rest === "--help") {
|
||||
for (const line of TREE_HELP) out.push(line);
|
||||
return;
|
||||
}
|
||||
const opts = patternOptions(rest, false), arg = word(opts.rest);
|
||||
let targets: { session: Session; patterns: Pattern[] }[];
|
||||
if (!arg.value) {
|
||||
if (opts.file || opts.includeDirs.length) throw new Error("missing type; see hexpatt?");
|
||||
targets = sessions.map((session) => ({ session, patterns: session.instance.patterns }));
|
||||
} else {
|
||||
const last = sessions[sessions.length - 1];
|
||||
let p: Pattern | undefined;
|
||||
if (!opts.file && !arg.rest && last) {
|
||||
p = last.instance.get(arg.value);
|
||||
if (!p && /^[a-zA-Z_]\w*[.\[]/.test(arg.value)) {
|
||||
const v = last.instance.evaluateExpression(arg.value);
|
||||
if (!(v instanceof Pattern)) throw new Error("tree requires a pattern");
|
||||
p = v;
|
||||
}
|
||||
}
|
||||
if (p) {
|
||||
if (opts.includeDirs.length) throw new Error("-I requires -f when compiling types");
|
||||
targets = [{ session: last, patterns: [p] }];
|
||||
} else {
|
||||
const program = searchProgram(opts.file, opts.includeDirs);
|
||||
const host = new R2Host(r2, programBase(program));
|
||||
host.quiet = true;
|
||||
const number = r2.cmdj("?j " + (arg.rest || "$$"));
|
||||
const address = Number(number ? number.uint64 : undefined);
|
||||
if (!Number.isSafeInteger(address) || address < 0) throw new Error("invalid address");
|
||||
const instance = decodeType(program, arg.value, host, address - host.baseAddress);
|
||||
targets = [{ session: { file: opts.file || last.file, program, instance, host }, patterns: instance.patterns }];
|
||||
}
|
||||
}
|
||||
if (mode === "j") {
|
||||
out.push(JSON.stringify(targets.flatMap(({ session: s, patterns }) =>
|
||||
patterns.filter((p) => !p.hidden).map((p) => patternToJson(s.instance, p, { base: s.host.baseAddress })))));
|
||||
} else if (mode === "*") {
|
||||
out.push("fs+hexpat");
|
||||
for (const { session, patterns } of targets) r2script(session, out, patterns);
|
||||
out.push("fs-");
|
||||
} else {
|
||||
const color = Number(r2.cmd("e scr.color").trim()) > 0;
|
||||
for (const { session: s, patterns } of targets) dumpTree(s.instance, out.push, { base: s.host.baseAddress, color, patterns });
|
||||
if (!targets.length) out.push("no patterns loaded, run 'hexpatt?' for help");
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(args: string[]): { quiet: boolean; includeDirs: string[]; file: string } {
|
||||
const r = { quiet: false, includeDirs: [] as string[], file: "" };
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
@@ -101,6 +328,11 @@ function parseArgs(args: string[]): { quiet: boolean; includeDirs: string[]; fil
|
||||
return r;
|
||||
}
|
||||
|
||||
function programBase(program: A.Program): number {
|
||||
const ba = (pragma(program, "base_address") || "").match(/0x[0-9a-fA-F]+|\d+/);
|
||||
return ba ? parseInt(ba[0]) : R2Host.binBaseAddress(r2);
|
||||
}
|
||||
|
||||
function load(file: string, includeDirs: string[], quiet = false): Session {
|
||||
const host = new R2Host(r2);
|
||||
const inline = file.startsWith(B64);
|
||||
@@ -110,8 +342,7 @@ function load(file: string, includeDirs: string[], quiet = false): Session {
|
||||
host.includeDirs = includeDirs;
|
||||
host.quiet = quiet;
|
||||
const program = compile(src || "", host, file);
|
||||
const ba = (pragma(program, "base_address") || "").match(/0x[0-9a-fA-F]+|\d+/);
|
||||
host.baseAddress = ba ? parseInt(ba[0]) : R2Host.binBaseAddress(r2);
|
||||
host.baseAddress = programBase(program);
|
||||
return { file, program, instance: evaluate(program, host), host };
|
||||
}
|
||||
|
||||
@@ -147,13 +378,16 @@ function commentValue(inst: PatternInstance, p: Pattern): string {
|
||||
return inst.formatPattern(p, true);
|
||||
}
|
||||
|
||||
function r2script(s: Session, out: Out): void {
|
||||
function r2script(s: Session, out: Out, patterns = s.instance.patterns): void {
|
||||
const inst = s.instance;
|
||||
const base = s.host.baseAddress;
|
||||
walkPatterns(inst, (p: Pattern, path: string[]) => {
|
||||
if (p.local || p.section !== 0) return false;
|
||||
const addr = "0x" + (base + p.offset).toString(16);
|
||||
out.push("f " + flagName(path) + " " + Math.max(1, p.size) + " @ " + addr);
|
||||
const name = flagName(path), color = patternColor(p);
|
||||
out.push("f " + name + " " + Math.max(1, p.size) + " @ " + addr);
|
||||
if (color) out.push("fc " + name + "=rgb:" + color);
|
||||
if (p.comment) out.push("fC " + name + " " + sanitize(printable(p.comment, 256)));
|
||||
if (p.kind === "string" || p.kind === "wstring") {
|
||||
out.push("Cs " + p.size + " @ " + addr);
|
||||
out.push("CCu " + sanitize(p.typeName + " " + p.name + " = " + printable(String(inst.value(p)))) + " @ " + addr);
|
||||
@@ -173,7 +407,7 @@ function r2script(s: Session, out: Out): void {
|
||||
out.push("CCu " + sanitize((t ? t + " " : "") + p.name + " = " + commentValue(inst, p)) + " @ " + addr);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, patterns);
|
||||
}
|
||||
|
||||
function list(out: Out, json: boolean): void {
|
||||
@@ -230,6 +464,7 @@ function hexpatCommand(cmd: string): void {
|
||||
const rest = cmd.substr(sub === " " || sub === "" ? 6 : 7).trim();
|
||||
const args = rest.split(/\s+/).filter((x) => x);
|
||||
try {
|
||||
for (const s of sessions) s.instance.invalidateData();
|
||||
switch (sub) {
|
||||
case "?": case "h": for (const l of HELP) out.push(l); break;
|
||||
case "": case " ":
|
||||
@@ -250,7 +485,12 @@ function hexpatCommand(cmd: string): void {
|
||||
break;
|
||||
}
|
||||
case "-": {
|
||||
if (rest === "v") out.push(VERSION);
|
||||
if (cmd.charAt(7) === "*") {
|
||||
if (rest !== "*") throw new Error("Usage: hexpat-*");
|
||||
out.push("fs+hexpat");
|
||||
out.push("f-hexpat.*");
|
||||
out.push("fs-");
|
||||
} else if (rest === "v") out.push(VERSION);
|
||||
else if (rest === "" || rest === "*") sessions.length = 0;
|
||||
else {
|
||||
const n = parseInt(rest, 10);
|
||||
@@ -283,7 +523,7 @@ function hexpatCommand(cmd: string): void {
|
||||
break;
|
||||
}
|
||||
case "e": {
|
||||
if (!rest) { out.push("Usage: hexpate expr"); break; }
|
||||
if (!rest) { out.push("Usage: hexpate expr (e.g. header.value = 3)"); break; }
|
||||
let s = sessions[sessions.length - 1];
|
||||
if (!s) {
|
||||
// no pattern loaded: evaluate against the current data with an empty program
|
||||
@@ -294,6 +534,9 @@ function hexpatCommand(cmd: string): void {
|
||||
if (d !== "") out.push(d);
|
||||
break;
|
||||
}
|
||||
case "p": patternCommand(cmd.substring(7), out); break;
|
||||
case "m": magicCommand(rest, out); break;
|
||||
case "t": treeCommand(cmd.substring(7), out); break;
|
||||
default:
|
||||
for (const l of HELP) out.push(l);
|
||||
}
|
||||
@@ -317,7 +560,7 @@ function hexpatCommand(cmd: string): void {
|
||||
call: function (cmd: string) {
|
||||
if (!cmd.startsWith("hexpat")) return false;
|
||||
const c = cmd.charAt(6);
|
||||
if (c !== "" && " ?h+-j*les".indexOf(c) < 0) return false;
|
||||
if (c !== "" && " ?h+-j*lespmt".indexOf(c) < 0) return false;
|
||||
hexpatCommand(cmd);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
?e -- assignments write r2 I/O
|
||||
e io.cache=true
|
||||
hexpat -q tests/edit_fields.pat
|
||||
hexpate alias + 0
|
||||
hexpate header.value= 3
|
||||
p8 4 @ 0
|
||||
hexpate header.value
|
||||
hexpate alias + 0
|
||||
hexpate header.value += 2
|
||||
hexpate header.value == 5
|
||||
"hexpate header.value >= 5"
|
||||
"hexpate header.value |= 8"
|
||||
hexpate header.value + 0
|
||||
hexpate "a=b"
|
||||
hexpate header.big = 0x1234
|
||||
hexpate header.signedValue = -2
|
||||
hexpate header.enabled = false
|
||||
p8 8 @ 0
|
||||
hexpate values[1] = 0x5678
|
||||
hexpate values[0] = values[1] + 1
|
||||
p8 4 @ 8
|
||||
?e -- bitfields preserve neighboring bits
|
||||
wx ffffffff @ 12
|
||||
hexpate bits.low = 3
|
||||
hexpate bits.enabled = false
|
||||
hexpate bits.high = -2
|
||||
hexpate big_bits.low = 3
|
||||
hexpate big_bits.enabled = false
|
||||
hexpate ordered.low = 3
|
||||
p8 4 @ 12
|
||||
?e -- strings and floats use the declared byte order
|
||||
hexpate text = "ab"
|
||||
hexpate wide = "A"
|
||||
hexpate text[1] = 'c'
|
||||
hexpate wide[1] = 'B'
|
||||
p8 8 @ 16
|
||||
hexpate number = 1.5
|
||||
hexpate big_number = -2.0
|
||||
hexpate half = 1.5
|
||||
p8 14 @ 24
|
||||
?e -- function assignments and other loaded sessions share I/O
|
||||
hexpat+ -q tests/edit_fields.pat
|
||||
hexpate set_value(9)
|
||||
hexpat~value (u32)
|
||||
?e -- reads refresh after an external write
|
||||
wx 0a000000 @ 0
|
||||
hexpate header.value + 0
|
||||
hexpat~value (u32)
|
||||
?e -- read-only I/O rejects writes without changing the field
|
||||
wcr
|
||||
e io.cache=false
|
||||
hexpate header.value= 3
|
||||
p8 4 @ 0
|
||||
hexpate header.value + 0
|
||||
?e -- base addresses also apply to writes
|
||||
e io.cache=true
|
||||
omv 1 0x1000
|
||||
hexpat -q base64:I3ByYWdtYSBiYXNlX2FkZHJlc3MgMHgxMDAwCnN0cnVjdCBIZWFkZXIgeyB1MzIgdmFsdWU7IH07IEhlYWRlciBoZWFkZXIgQCAwOw==
|
||||
hexpate header.value = 3
|
||||
p8 4 @ 0x1000
|
||||
hexpate header.value
|
||||
@@ -0,0 +1,31 @@
|
||||
-- assignments write r2 I/O
|
||||
421991804 (0x1927157c)
|
||||
03000000
|
||||
value (u32) @ 0x00000000 = 3 (0x3)
|
||||
3 (0x3)
|
||||
true
|
||||
true
|
||||
13 (0xd)
|
||||
"a=b"
|
||||
0d0000001234fe00
|
||||
79567856
|
||||
-- bitfields preserve neighboring bits
|
||||
e36ffffb
|
||||
-- strings and floats use the declared byte order
|
||||
6163000000410042
|
||||
0000c03fc000000000000000003e
|
||||
-- function assignments and other loaded sessions share I/O
|
||||
value (u32) @ 0x00000000 = 9 (0x9)
|
||||
value (u32) @ 0x00000000 = 9 (0x9)
|
||||
-- reads refresh after an external write
|
||||
10 (0xa)
|
||||
value (u32) @ 0x00000000 = 10 (0xa)
|
||||
value (u32) @ 0x00000000 = 10 (0xa)
|
||||
-- read-only I/O rejects writes without changing the field
|
||||
ERROR: Cannot write. Use `omp`, `io.cache` or reopen the file in rw with `oo+`
|
||||
7c152719
|
||||
421991804 (0x1927157c)
|
||||
-- base addresses also apply to writes
|
||||
03000000
|
||||
value (u32) @ 0x00001000 = 3 (0x3)
|
||||
hexpat: cannot write at 0x0: open with -w or enable io.cache
|
||||
@@ -0,0 +1,23 @@
|
||||
?e -- magic help
|
||||
hexpatm?
|
||||
?e -- types from a loaded file
|
||||
hexpat -q demo/test.hexpat
|
||||
hexpatm MachHeader
|
||||
?e -- type-only export, includes and nested fixed layouts
|
||||
hexpatm -f tests/magic_types.pat -I demo Header
|
||||
hexpatm -f tests/magic_types.pat -I demo Header > /tmp/r2hexpat-header.magic
|
||||
?e -- decode header at another address with pm
|
||||
e io.cache=true
|
||||
wx 4845585001001234887766554433221112345678785634120000c03f01feff00efbeadde00410042 @ 64
|
||||
pm /tmp/r2hexpat-header.magic @ 64
|
||||
?e -- exporting preserves the loaded file and supports alignment
|
||||
hexpatl
|
||||
hexpatm -f demo/player.hexpat -a 8 Player
|
||||
?e -- unsupported fields and bad arguments
|
||||
hexpatm -f tests/magic_types.pat -I demo Wide
|
||||
hexpatm -f tests/magic_types.pat -I demo Dynamic
|
||||
hexpatm MachHeader extra
|
||||
hexpatm -a 3 MachHeader
|
||||
?e -- discard staged test bytes
|
||||
wcr
|
||||
e io.cache=false
|
||||
@@ -0,0 +1,63 @@
|
||||
-- magic help
|
||||
Usage: hexpatm [options] Type # libmagic header decoder for pm
|
||||
| hexpatm [-f file] [-I dir] [-a N] Type print a magic file for the type's fixed layout
|
||||
| defaults: types from the last loaded file, packed layout (-a 1)
|
||||
| options: -f file (compile types only), -I dir (includes), -a/--align 1|2|4|8|16
|
||||
| fields: nested structs, unions, arrays, strings, 1/2/4/8-byte integers and pointers, float/double
|
||||
| integers/enums print raw hex; padding is omitted; dynamic layouts and unsupported fields are rejected
|
||||
| export: hexpatm -f demo/test.hexpat MachHeader > /tmp/macho.magic
|
||||
| decode: pm /tmp/macho.magic @ 0 (increase block size with b if the header is larger)
|
||||
-- types from a loaded file
|
||||
# MachHeader size=32 align=1
|
||||
31 ubyte x MachHeader
|
||||
>0 ulelong x magic=0x%x
|
||||
>4 ulelong x cputype=0x%x
|
||||
>8 ulelong x cpusubtype=0x%x
|
||||
>12 ulelong x filetype=0x%x
|
||||
>16 ulelong x ncmds=0x%x
|
||||
>20 ulelong x sizeofcmds=0x%x
|
||||
>24 ulelong x flags=0x%x
|
||||
>28 ulelong x reserved=0x%x
|
||||
>0 ubyte x
|
||||
-- type-only export, includes and nested fixed layouts
|
||||
# Header size=40 align=1
|
||||
39 ubyte x Header
|
||||
>0 string x magic=%.4s
|
||||
>4 uleshort x version=0x%x
|
||||
>6 ubeshort x flags=0x%x
|
||||
>8 ulequad x timestamp=0x%llx
|
||||
>16 ubeshort x pairs[0].value=0x%x
|
||||
>18 ubeshort x pairs[1].value=0x%x
|
||||
>20 ulelong x value.word=0x%x
|
||||
>20 ubyte x value.bytes[0]=0x%x
|
||||
>21 ubyte x value.bytes[1]=0x%x
|
||||
>22 ubyte x value.bytes[2]=0x%x
|
||||
>23 ubyte x value.bytes[3]=0x%x
|
||||
>24 lefloat x ratio=%g
|
||||
>28 ubyte x mode=0x%x
|
||||
>29 uleshort x delta=0x%x
|
||||
>32 ulelong x next=0x%x
|
||||
>36 bestring16 x name=%.2s
|
||||
>0 ubyte x
|
||||
-- decode header at another address with pm
|
||||
0x00000040 0 Header magic=HEXP version=0x1 flags=0x1234 timestamp=0x1122334455667788 pairs[0].value=0x1234 pairs[1].value=0x5678 value.word=0x12345678 value.bytes[0]=0x78 value.bytes[1]=0x56 value.bytes[2]=0x34 value.bytes[3]=0x12 ratio=1.5 mode=0x1 delta=0xfffe next=0xdeadbeef name=AB
|
||||
-- exporting preserves the loaded file and supports alignment
|
||||
0 demo/test.hexpat (2 patterns)
|
||||
0x00000000 32 macho (MachHeader)
|
||||
0x00000020 8 someflags (u32[2])
|
||||
# Player size=32 align=8
|
||||
31 ubyte x Player
|
||||
>0 ulelong x health=0x%x
|
||||
>4 ulelong x lives=0x%x
|
||||
>8 ubyte x role=0x%x
|
||||
>12 lefloat x position.x=%g
|
||||
>16 lefloat x position.y=%g
|
||||
>20 lefloat x position.z=%g
|
||||
>24 ulequad x next=0x%llx
|
||||
>0 ubyte x
|
||||
-- unsupported fields and bad arguments
|
||||
-- discard staged test bytes
|
||||
hexpat: libmagic cannot decode field 'value' (u24, 3 bytes); use hexpat to decode this type
|
||||
hexpat: error: layout depends on unset field 'count' (tests/magic_types.pat:20:31)
|
||||
hexpat: Usage: hexpatm [-f file] [-I dir] [-a N] Type
|
||||
hexpat: error: alignment must be 1, 2, 4, 8 or 16 (line 0:0)
|
||||
@@ -0,0 +1,50 @@
|
||||
?e -- search help
|
||||
hexpatp?
|
||||
?e -- one-shot patterns from a file, independent of current data
|
||||
hexpatp -f demo/player.hexpat Player health=100 lives=3
|
||||
hexpatp -f demo/player.hexpat -a 8 Player health=100 lives=3
|
||||
hexpatp -f tests/search_types.pat Text name="a=b c" code=0x1234
|
||||
"hexpatp -f tests/search_types.pat Array<u16,2> values[1]=1|2"
|
||||
?e -- named instances start unset, including zero-valued fields
|
||||
hexpatp+ -f demo/player.hexpat -a 8 wanted Player
|
||||
hexpatpi wanted
|
||||
hexpatp= wanted health=0 lives=3 position.z=3
|
||||
hexpatpi wanted
|
||||
hexpatp- wanted health position
|
||||
hexpatpl
|
||||
hexpatplj
|
||||
hexpatpj wanted
|
||||
?e -- use types from the last loaded file
|
||||
hexpat -q demo/player.hexpat
|
||||
hexpatp Player health=100 lives=3
|
||||
?e -- real /x searches with wildcard padding and unknown fields
|
||||
e io.cache=true
|
||||
wx 640000000300000001ffffff112233445566778899aabbccddeeff0011223344 @ 0
|
||||
wx 640000000300000002ffffff112233445566778899aabbccddeeff0011223344 @ 64
|
||||
e search.in=io.maps
|
||||
e search.from=0
|
||||
e search.to=96
|
||||
e search.align=8
|
||||
e search.flags=false
|
||||
hexpatp= wanted health=100
|
||||
/x `hexpatp wanted`
|
||||
hexpatp= wanted role=Role::Rogue
|
||||
/x `hexpatp wanted`
|
||||
?e -- arbitrary bit masks match only the selected bitfield
|
||||
wx 030bf3ff @ 96
|
||||
e search.from=96
|
||||
e search.to=100
|
||||
e search.align=1
|
||||
/x `hexpatp -f tests/search_types.pat Bits low=3`
|
||||
?e -- errors preserve the named instance
|
||||
hexpatp+ wanted Player
|
||||
hexpatp= wanted health=2 nope=3
|
||||
hexpatp wanted
|
||||
hexpatp -f tests/search_types.pat Dynamic length=3
|
||||
hexpatp- wanted missing
|
||||
?e -- delete named instances and discard staged test bytes
|
||||
hexpatp- wanted
|
||||
hexpatplj
|
||||
wcr
|
||||
e io.cache=false
|
||||
p8 8 @ 0
|
||||
@@ -0,0 +1,63 @@
|
||||
-- search help
|
||||
Usage: hexpatp[?+=-ijl] [options] [args] # partial instances for /x searches
|
||||
| hexpatp [options] Type [field=expr ...] one-shot /x bytes:mask; omitted fields are wildcards
|
||||
| hexpatp name /x bytes:mask of a named instance
|
||||
| hexpatp+ [options] name Type [field=expr ...] create a named instance (all fields start unset)
|
||||
| hexpatp= name field=expr ... set fields; zero is a defined value
|
||||
| hexpatp- name field ... unset fields (or aggregates)
|
||||
| hexpatp- name delete the named instance; '*' deletes all
|
||||
| hexpatpi [options] name|Type [fields ...] inspect offsets, sizes and set/unset fields
|
||||
| hexpatpj [options] name|Type [fields ...] inspect as JSON, including bytes and bit mask
|
||||
| hexpatpl[j] list named instances (optionally JSON)
|
||||
| options: -f file (compile types only), -I dir (includes), -a/--align 1|2|4|8|16
|
||||
| defaults: last loaded file, packed layout (-a 1); pointers use explicit hexpat widths
|
||||
| fields: nested.member=expr, array[index]=expr; enum names, arithmetic and quoted strings work
|
||||
| padding stays wildcarded; dynamic layouts and conflicting union fields are rejected
|
||||
| search: /x `hexpatp -f game.hexpat Player health=100 lives=3`
|
||||
-- one-shot patterns from a file, independent of current data
|
||||
6400000003000000000000000000000000000000000000000000000000:ffffffffffffffff000000000000000000000000000000000000000000
|
||||
6400000003000000000000000000000000000000000000000000000000000000:ffffffffffffffff000000000000000000000000000000000000000000000000
|
||||
613d6220630000003412:ffffffffffffffffffff
|
||||
00000300:0000ffff
|
||||
-- named instances start unset, including zero-valued fields
|
||||
wanted Player size=32 align=8 (unset)
|
||||
0x0000 4 health (u32) = unset
|
||||
0x0004 4 lives (u32) = unset
|
||||
0x0008 1 role (Role) = unset
|
||||
0x000c 4 position.x (float) = unset
|
||||
0x0010 4 position.y (float) = unset
|
||||
0x0014 4 position.z (float) = unset
|
||||
0x0018 8 next (Player*) = unset
|
||||
wanted Player size=32 align=8 (partial)
|
||||
0x0000 4 health (u32) = 0
|
||||
0x0004 4 lives (u32) = 3
|
||||
0x0008 1 role (Role) = unset
|
||||
0x000c 4 position.x (float) = unset
|
||||
0x0010 4 position.y (float) = unset
|
||||
0x0014 4 position.z (float) = 3
|
||||
0x0018 8 next (Player*) = unset
|
||||
wanted Player size=32 align=8 fields=1/7
|
||||
[{"name":"wanted","type":"Player","size":32,"alignment":8,"state":"partial","defined":1,"fields":7}]
|
||||
{"name":"wanted","type":"Player","size":32,"alignment":8,"state":"partial","bytes":"0000000003000000000000000000000000000000000000000000000000000000","mask":"00000000ffffffff000000000000000000000000000000000000000000000000","pattern":"0000000003000000000000000000000000000000000000000000000000000000:00000000ffffffff000000000000000000000000000000000000000000000000","fields":[{"path":"health","type":"u32","offset":0,"size":4,"defined":false},{"path":"lives","type":"u32","offset":4,"size":4,"defined":true,"value":3},{"path":"role","type":"Role","offset":8,"size":1,"defined":false},{"path":"position.x","type":"float","offset":12,"size":4,"defined":false},{"path":"position.y","type":"float","offset":16,"size":4,"defined":false},{"path":"position.z","type":"float","offset":20,"size":4,"defined":false},{"path":"next","type":"Player*","offset":24,"size":8,"defined":false}]}
|
||||
-- use types from the last loaded file
|
||||
6400000003000000000000000000000000000000000000000000000000:ffffffffffffffff000000000000000000000000000000000000000000
|
||||
-- real /x searches with wildcard padding and unknown fields
|
||||
'fs hits
|
||||
0x00000000 hit0_0 640000000300000001ffffff112233445566778899aabbccddeeff0011223344
|
||||
0x00000040 hit0_1 640000000300000002ffffff112233445566778899aabbccddeeff0011223344
|
||||
'fs hits
|
||||
0x00000040 hit1_0 640000000300000002ffffff112233445566778899aabbccddeeff0011223344
|
||||
-- arbitrary bit masks match only the selected bitfield
|
||||
'fs hits
|
||||
0x00000060 hit2_0 03
|
||||
0x00000061 hit2_1 0b
|
||||
0x00000062 hit2_2 f3
|
||||
-- errors preserve the named instance
|
||||
6400000003000000020000000000000000000000000000000000000000000000:ffffffffffffffffff0000000000000000000000000000000000000000000000
|
||||
-- delete named instances and discard staged test bytes
|
||||
[]
|
||||
7c152719d7f552c8
|
||||
hexpat: search instance 'wanted' already exists; delete it with hexpatp-
|
||||
hexpat: unknown search field 'nope'
|
||||
hexpat: error: layout depends on unset field 'length' (tests/search_types.pat:3:32)
|
||||
hexpat: unknown search field 'missing'
|
||||
@@ -1,5 +1,5 @@
|
||||
-- help
|
||||
Usage: hexpat[?+-jl*es] [args] # ImHex pattern language
|
||||
Usage: hexpat[?+-jl*espmt] [args] # ImHex pattern language
|
||||
| hexpat [-q] [-I dir] [file] evaluate file (replacing the loaded ones) and show its patterns
|
||||
| hexpat show the patterns of the loaded files
|
||||
| hexpat+ [-I dir] file evaluate file and add it to the loaded ones
|
||||
@@ -7,9 +7,13 @@ Usage: hexpat[?+-jl*es] [args] # ImHex pattern language
|
||||
| hexpat-v show the r2hexpat version
|
||||
| hexpatj [file] show the patterns as JSON
|
||||
| hexpat* [file] show the patterns as r2 commands (flags, comments, data hints)
|
||||
| hexpat-* show r2 commands to remove generated flags (.hexpat-* applies them)
|
||||
| hexpatl[j] list the loaded files and their top-level patterns
|
||||
| hexpats [file] one-line summary (types, fields, patterns) or the error, for status bars
|
||||
| hexpate expr evaluate an expression in the context of the last loaded file
|
||||
| hexpate expr evaluate an expression or assignment in the last loaded file (writes r2 I/O)
|
||||
| hexpatp[?+=-ijl] ... create partial instances and masked search patterns (hexpatp? for help)
|
||||
| hexpatm [options] Type print a libmagic decoder for a fixed type (hexpatm? for help)
|
||||
| hexpatt[j*] [options] [Type [address]|pattern] view a type at an address or a loaded pattern as a tree
|
||||
| hexpat? show this help
|
||||
| options: -q (evaluate only), -I dir (add #include / import search path)
|
||||
| file can be 'base64:<data>' to evaluate the encoded source instead of reading a file
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
?e -- decode types without loading a session
|
||||
hexpatt -f tests/tree_types.pat Header @ 0
|
||||
hexpattj -f tests/tree_types.pat Header 8
|
||||
hexpatlj
|
||||
?e -- current seek and address expressions
|
||||
s 8
|
||||
hexpatt -f tests/tree_types.pat Header
|
||||
f tree.address 1 @ 8
|
||||
hexpatt -f tests/tree_types.pat Header tree.address
|
||||
?e -- base address, inline sources and include directories
|
||||
omv 1 0x1000
|
||||
hexpattj -f base64:I3ByYWdtYSBiYXNlX2FkZHJlc3MgMHgxMDAwCiNpbmNsdWRlIDx0cmVlX3R5cGVzLnBhdD4K -I tests Header 0x1008
|
||||
omv 1 0
|
||||
?e -- loaded pattern trees and one-shot types preserve the session
|
||||
hexpat -q tests/syntax_structs.hexpat
|
||||
hexpatt file.head
|
||||
hexpatt file.data[0]
|
||||
hexpattj file.head.version
|
||||
hexpatt Header 4
|
||||
hexpatl
|
||||
?e -- generated flag colors and comments
|
||||
hexpatt* -f tests/tree_types.pat Header 0
|
||||
fs other
|
||||
f other.keep 1 @ 0
|
||||
.hexpatt* -f tests/tree_types.pat Header 0
|
||||
fs
|
||||
fs hexpat
|
||||
fc*
|
||||
fC hexpat.value.count
|
||||
?e -- regular flag export carries colors too
|
||||
hexpat* base64:c3RydWN0IENvbG9yIHsgdTggdGFnIFtbY29tbWVudCgiY29sb3JlZCBmaWVsZCIpXV07IH0gW1tjb2xvcigiQUJDREVGIildXTsgQ29sb3IgY29sb3JlZCBAIDA7
|
||||
.hexpat*
|
||||
fc*
|
||||
hexpate std::core::set_pattern_color(colored, 0x00332211)
|
||||
hexpatt colored
|
||||
.hexpat*
|
||||
fc*
|
||||
?e -- cleanup preview and application preserve sessions and unrelated flags
|
||||
hexpat-*
|
||||
f keep 1 @ 8
|
||||
fs other
|
||||
f hexpat.outside 1 @ 0
|
||||
.hexpat-*
|
||||
f
|
||||
fs hexpat
|
||||
f
|
||||
hexpatl
|
||||
?e -- cleanup also works after unloading
|
||||
hexpat-
|
||||
.hexpatt* -f tests/tree_types.pat Header 8
|
||||
hexpat-
|
||||
.hexpat-*
|
||||
f
|
||||
?e -- errors
|
||||
hexpatt -f tests/tree_types.pat
|
||||
hexpatt -f tests/tree_types.pat Missing 0
|
||||
hexpatt -f tests/tree_types.pat Header -1
|
||||
hexpatt -f tests/tree_types.pat Header 0x100000
|
||||
hexpatt -f tests/tree_types.pat Header no.such.address
|
||||
hexpatt Header
|
||||
@@ -0,0 +1,134 @@
|
||||
-- decode types without loading a session
|
||||
value (Header) @ 0x00000000 size=4 [[color("1144AA")]]
|
||||
|- count (u8) @ 0x00000000 = 124 (0x7c) [[color("FF0000"), comment("entry count")]]
|
||||
|- Details (Inner) @ 0x00000001 size=2 [[color("00FF00"), name("Details")]]
|
||||
| `- word (u16) @ 0x00000001 = 5415 (0x1527) [[color("00FF00")]]
|
||||
`- data (u8[1]) @ 0x00000003 size=1 [[color("1144AA")]]
|
||||
`- [0] (u8) @ 0x00000003 = 25 (0x19) [[color("1144AA")]]
|
||||
[{"name":"value","type":"Header","kind":"struct","addr":8,"offset":8,"size":5,"color":"1144AA","attributes":[{"name":"color","args":["1144AA"]}],"children":[{"name":"count","type":"u8","kind":"unsigned","addr":8,"offset":8,"size":1,"color":"FF0000","comment":"entry count","attributes":[{"name":"color","args":["FF0000"]},{"name":"comment","args":["entry count"]}],"value":131},{"name":"inner","type":"Inner","kind":"struct","addr":9,"offset":9,"size":2,"displayName":"Details","color":"00FF00","attributes":[{"name":"color","args":["00FF00"]},{"name":"name","args":["Details"]}],"children":[{"name":"word","type":"u16","kind":"unsigned","addr":9,"offset":9,"size":2,"color":"00FF00","value":48272}]},{"name":"data","type":"u8[2]","kind":"array","addr":11,"offset":11,"size":2,"color":"1144AA","count":2,"entries":[{"name":"[0]","type":"u8","kind":"unsigned","addr":11,"offset":11,"size":1,"color":"1144AA","value":164},{"name":"[1]","type":"u8","kind":"unsigned","addr":12,"offset":12,"size":1,"color":"1144AA","value":66}]}]}]
|
||||
[]
|
||||
-- current seek and address expressions
|
||||
value (Header) @ 0x00000008 size=5 [[color("1144AA")]]
|
||||
|- count (u8) @ 0x00000008 = 131 (0x83) [[color("FF0000"), comment("entry count")]]
|
||||
|- Details (Inner) @ 0x00000009 size=2 [[color("00FF00"), name("Details")]]
|
||||
| `- word (u16) @ 0x00000009 = 48272 (0xbc90) [[color("00FF00")]]
|
||||
`- data (u8[2]) @ 0x0000000b size=2 [[color("1144AA")]]
|
||||
|- [0] (u8) @ 0x0000000b = 164 (0xa4) [[color("1144AA")]]
|
||||
`- [1] (u8) @ 0x0000000c = 66 (0x42) [[color("1144AA")]]
|
||||
value (Header) @ 0x00000008 size=5 [[color("1144AA")]]
|
||||
|- count (u8) @ 0x00000008 = 131 (0x83) [[color("FF0000"), comment("entry count")]]
|
||||
|- Details (Inner) @ 0x00000009 size=2 [[color("00FF00"), name("Details")]]
|
||||
| `- word (u16) @ 0x00000009 = 48272 (0xbc90) [[color("00FF00")]]
|
||||
`- data (u8[2]) @ 0x0000000b size=2 [[color("1144AA")]]
|
||||
|- [0] (u8) @ 0x0000000b = 164 (0xa4) [[color("1144AA")]]
|
||||
`- [1] (u8) @ 0x0000000c = 66 (0x42) [[color("1144AA")]]
|
||||
-- base address, inline sources and include directories
|
||||
[{"name":"value","type":"Header","kind":"struct","addr":4104,"offset":8,"size":5,"color":"1144AA","attributes":[{"name":"color","args":["1144AA"]}],"children":[{"name":"count","type":"u8","kind":"unsigned","addr":4104,"offset":8,"size":1,"color":"FF0000","comment":"entry count","attributes":[{"name":"color","args":["FF0000"]},{"name":"comment","args":["entry count"]}],"value":131},{"name":"inner","type":"Inner","kind":"struct","addr":4105,"offset":9,"size":2,"displayName":"Details","color":"00FF00","attributes":[{"name":"color","args":["00FF00"]},{"name":"name","args":["Details"]}],"children":[{"name":"word","type":"u16","kind":"unsigned","addr":4105,"offset":9,"size":2,"color":"00FF00","value":48272}]},{"name":"data","type":"u8[2]","kind":"array","addr":4107,"offset":11,"size":2,"color":"1144AA","count":2,"entries":[{"name":"[0]","type":"u8","kind":"unsigned","addr":4107,"offset":11,"size":1,"color":"1144AA","value":164},{"name":"[1]","type":"u8","kind":"unsigned","addr":4108,"offset":12,"size":1,"color":"1144AA","value":66}]}]}]
|
||||
-- loaded pattern trees and one-shot types preserve the session
|
||||
head (Header) @ 0x00000000 size=16
|
||||
|- magic (u32) @ 0x00000000 = 421991804 (0x1927157c)
|
||||
|- version (u16) @ 0x00000004 = 62935 (0xf5d7)
|
||||
|- flags (u16) @ 0x00000006 = 51282 (0xc852)
|
||||
`- timestamp (u64) @ 0x00000008 = 13032463231746096259 (0xb4dc9c42a490bc83)
|
||||
[0] (u8) @ 0x00000015 = 126 (0x7e)
|
||||
[{"name":"version","type":"u16","kind":"unsigned","addr":4,"offset":4,"size":2,"value":62935}]
|
||||
value (Header) @ 0x00000004 size=16
|
||||
|- magic (u32) @ 0x00000004 = 3360880087 (0xc852f5d7)
|
||||
|- version (u16) @ 0x00000008 = 48259 (0xbc83)
|
||||
|- flags (u16) @ 0x0000000a = 42128 (0xa490)
|
||||
`- timestamp (u64) @ 0x0000000c = 1165859946296548418 (0x102df771b4dc9c42)
|
||||
0 tests/syntax_structs.hexpat (2 patterns)
|
||||
0x00000000 22 file (File)
|
||||
0x00000016 4 footer (u32)
|
||||
-- generated flag colors and comments
|
||||
fs+hexpat
|
||||
f hexpat.value 4 @ 0x0
|
||||
fc hexpat.value=rgb:1144aa
|
||||
f hexpat.value.count 1 @ 0x0
|
||||
fc hexpat.value.count=rgb:ff0000
|
||||
fC hexpat.value.count entry count
|
||||
Cd 1 @ 0x0
|
||||
CCu u8 count = 0x7c @ 0x0
|
||||
f hexpat.value.inner 2 @ 0x1
|
||||
fc hexpat.value.inner=rgb:00ff00
|
||||
f hexpat.value.inner.word 2 @ 0x1
|
||||
fc hexpat.value.inner.word=rgb:00ff00
|
||||
Cd 2 @ 0x1
|
||||
CCu u16 word = 0x1527 @ 0x1
|
||||
f hexpat.value.data 1 @ 0x3
|
||||
fc hexpat.value.data=rgb:1144aa
|
||||
Cd 1 1 @ 0x3
|
||||
fs-
|
||||
rgb:1144aa
|
||||
rgb:ff0000
|
||||
rgb:00ff00
|
||||
rgb:00ff00
|
||||
rgb:1144aa
|
||||
0 . classes
|
||||
0 . format
|
||||
5 . hexpat
|
||||
1 * other
|
||||
0 . sections
|
||||
0 . segments
|
||||
0 . symbols
|
||||
0 . trycatch
|
||||
'fc hexpat.value=rgb:1144aa
|
||||
'fc hexpat.value.count=rgb:ff0000
|
||||
'fc hexpat.value.inner=rgb:00ff00
|
||||
'fc hexpat.value.inner.word=rgb:00ff00
|
||||
'fc hexpat.value.data=rgb:1144aa
|
||||
entry count
|
||||
-- regular flag export carries colors too
|
||||
fs+hexpat
|
||||
f hexpat.colored 1 @ 0x0
|
||||
fc hexpat.colored=rgb:abcdef
|
||||
f hexpat.colored.tag 1 @ 0x0
|
||||
fc hexpat.colored.tag=rgb:abcdef
|
||||
fC hexpat.colored.tag colored field
|
||||
Cd 1 @ 0x0
|
||||
CCu u8 tag = 0x7c @ 0x0
|
||||
fs-
|
||||
rgb:abcdef
|
||||
rgb:abcdef
|
||||
'fc hexpat.value=rgb:1144aa
|
||||
'fc hexpat.value.count=rgb:ff0000
|
||||
'fc hexpat.colored=rgb:abcdef
|
||||
'fc hexpat.colored.tag=rgb:abcdef
|
||||
'fc hexpat.value.inner=rgb:00ff00
|
||||
'fc hexpat.value.inner.word=rgb:00ff00
|
||||
'fc hexpat.value.data=rgb:1144aa
|
||||
colored (Color) @ 0x00000000 size=1 [[color("112233")]]
|
||||
`- tag (u8) @ 0x00000000 = 124 (0x7c) [[color("112233"), comment("colored field")]]
|
||||
rgb:112233
|
||||
rgb:112233
|
||||
'fc hexpat.value=rgb:1144aa
|
||||
'fc hexpat.value.count=rgb:ff0000
|
||||
'fc hexpat.colored=rgb:112233
|
||||
'fc hexpat.colored.tag=rgb:112233
|
||||
'fc hexpat.value.inner=rgb:00ff00
|
||||
'fc hexpat.value.inner.word=rgb:00ff00
|
||||
'fc hexpat.value.data=rgb:1144aa
|
||||
-- cleanup preview and application preserve sessions and unrelated flags
|
||||
fs+hexpat
|
||||
f-hexpat.*
|
||||
fs-
|
||||
0x00000000 1 other.keep
|
||||
0x00000000 1 hexpat.outside
|
||||
0x00000008 1 keep
|
||||
0 base64 (1 patterns)
|
||||
0x00000000 1 colored (Color)
|
||||
-- cleanup also works after unloading
|
||||
rgb:1144aa
|
||||
rgb:ff0000
|
||||
rgb:00ff00
|
||||
rgb:00ff00
|
||||
rgb:1144aa
|
||||
0x00000008 1 keep
|
||||
-- errors
|
||||
ERROR: unknown symbol
|
||||
hexpat: missing type; see hexpatt?
|
||||
hexpat: error: unknown type 'Missing' (<expr>:1:1)
|
||||
hexpat: invalid address
|
||||
hexpat: error: type offset is outside the data (line 0:0)
|
||||
hexpat: invalid address
|
||||
hexpat: no types loaded: use -f file or load a pattern with hexpat
|
||||
@@ -0,0 +1,29 @@
|
||||
// Fields for the interactive assignment tests (writes stay in r2's I/O cache).
|
||||
struct Header {
|
||||
u32 value;
|
||||
be u16 big;
|
||||
s8 signedValue;
|
||||
bool enabled;
|
||||
};
|
||||
Header header @ 0;
|
||||
u32 alias @ 0;
|
||||
u16 values[2] @ 8;
|
||||
bitfield Bits {
|
||||
low : 3;
|
||||
bool enabled : 1;
|
||||
signed high : 4;
|
||||
};
|
||||
Bits bits @ 12;
|
||||
be Bits big_bits @ 13;
|
||||
bitfield Ordered {
|
||||
low : 3;
|
||||
high : 13;
|
||||
} [[bitfield_order(1, 16)]];
|
||||
be Ordered ordered @ 14;
|
||||
char text[4] @ 16;
|
||||
be char16 wide[2] @ 20;
|
||||
float number @ 24;
|
||||
be double big_number @ 28;
|
||||
float16 half @ 36;
|
||||
const u8 fixed = 7;
|
||||
fn set_value(auto v) { header.value = v; };
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "test.hexpat"
|
||||
enum Mode : u8 { Off, On };
|
||||
struct Pair { be u16 value; };
|
||||
union Value { u32 word; u8 bytes[4]; };
|
||||
struct Header {
|
||||
char magic[4];
|
||||
u16 version;
|
||||
be u16 flags;
|
||||
u64 timestamp;
|
||||
Pair pairs[2];
|
||||
Value value;
|
||||
float ratio;
|
||||
Mode mode;
|
||||
s16 delta;
|
||||
padding[1];
|
||||
Header *next : u32;
|
||||
be char16 name[2];
|
||||
};
|
||||
struct Wide { u24 value; };
|
||||
struct Dynamic { u8 count; u8 values[count]; };
|
||||
// Exporting types must not decode the included MachHeader or execute assertions.
|
||||
std::assert(false, "magic commands must not execute top-level statements");
|
||||
@@ -0,0 +1,73 @@
|
||||
import { compile, SearchPattern } from "../lib/index";
|
||||
|
||||
export function testMagic(): void {
|
||||
const program = compile(`
|
||||
#pragma endian big
|
||||
enum Kind : u8 { One = 1 };
|
||||
using Word = le u32;
|
||||
struct Pair { Word value; };
|
||||
union Overlay { le u32 word; u8 bytes[4]; };
|
||||
struct Header {
|
||||
char magic[4];
|
||||
u16 version;
|
||||
Kind kind;
|
||||
padding[1];
|
||||
Pair pairs[2];
|
||||
Overlay overlay;
|
||||
s64 timestamp;
|
||||
le float ratio;
|
||||
double scale;
|
||||
le char16 name[2];
|
||||
Header *next : le u64;
|
||||
};
|
||||
struct Aligned { u8 tag; u32 value; };
|
||||
struct BadWidth { u24 value; };
|
||||
struct Half { float16 value; };
|
||||
bitfield Bits { value : 3; };
|
||||
struct Dynamic { u8 count; u8 data[count]; };
|
||||
const u32 COUNT = 2;
|
||||
struct Array<T, auto N> { T values[N]; };
|
||||
namespace ns { struct Numbers { le u16 values[COUNT]; }; }
|
||||
std::assert(false, "magic export must not execute the program");
|
||||
fn main() { std::assert(false, "magic export must not run main"); };
|
||||
`);
|
||||
const check = (ok: boolean, msg: string): void => { if (!ok) throw new Error("magic: " + msg); };
|
||||
const fails = (type: string, msg: string): void => {
|
||||
try { new SearchPattern(program, type).toMagic(); } catch (e) {
|
||||
check(e instanceof Error && e.message.includes(msg), "wrong error: " + String(e));
|
||||
return;
|
||||
}
|
||||
throw new Error("magic: expected error containing " + msg);
|
||||
};
|
||||
const header = new SearchPattern(program, "Header");
|
||||
const magic = header.toMagic();
|
||||
check(magic.startsWith("# Header size=52 align=1\n51\tubyte\tx\tHeader\n"), "header size guard");
|
||||
const field = (offset: number, type: string, path: string, format: string): void => {
|
||||
check(magic.split("\n").includes(">" + offset + "\t" + type + "\tx\t" + path + "=" + format), path);
|
||||
};
|
||||
field(0, "string", "magic", "%.4s");
|
||||
field(4, "ubeshort", "version", "0x%x");
|
||||
field(6, "ubyte", "kind", "0x%x");
|
||||
field(8, "ulelong", "pairs[0].value", "0x%x");
|
||||
field(12, "ulelong", "pairs[1].value", "0x%x");
|
||||
field(16, "ulelong", "overlay.word", "0x%x");
|
||||
field(19, "ubyte", "overlay.bytes[3]", "0x%x");
|
||||
field(20, "ubequad", "timestamp", "0x%llx");
|
||||
field(28, "lefloat", "ratio", "%g");
|
||||
field(32, "bedouble", "scale", "%g");
|
||||
field(40, "lestring16", "name", "%.2s");
|
||||
field(44, "ulequad", "next", "0x%llx");
|
||||
check(!magic.includes(">7\t"), "padding was emitted");
|
||||
check(magic.split("\n").length === 18 && magic.endsWith(">0\tubyte\tx"), "missing leaves or final empty rule");
|
||||
header.set("version", 1);
|
||||
check(header.toMagic() === magic, "assigned search values constrained the decoder");
|
||||
const aligned = new SearchPattern(program, "Aligned", { alignment: 4 }).toMagic();
|
||||
check(aligned.includes("7\tubyte\tx\tAligned") && aligned.includes(">4\tubelong\tx\tvalue=0x%x"), "alignment policy");
|
||||
check(new SearchPattern(program, "Array<u16, COUNT>").toMagic().includes(">2\tubeshort\tx\tvalues[1]=0x%x"), "template constants");
|
||||
check(new SearchPattern(program, "ns::Numbers").toMagic().includes(">2\tuleshort\tx\tvalues[1]=0x%x"), "namespace types");
|
||||
fails("BadWidth", "libmagic cannot decode field 'value'");
|
||||
fails("Half", "libmagic cannot decode field 'value'");
|
||||
fails("Bits", "libmagic cannot decode field 'value'");
|
||||
fails("Dynamic", "unset field");
|
||||
console.log("[test] Magic export OK");
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#include "../demo/player.hexpat"
|
||||
bitfield Bits { low : 3; bool enabled : 1; signed high : 4; };
|
||||
struct Dynamic { u8 length; u8 payload[length]; };
|
||||
struct Text { char name[8]; u16 code; };
|
||||
struct Array<T, auto N> { T values[N]; };
|
||||
// Type-only loading must neither read this placement nor run the assertion.
|
||||
Player impossible @ 0x100000;
|
||||
std::assert(false, "search commands must not execute top-level statements");
|
||||
@@ -0,0 +1,127 @@
|
||||
import { compile, SearchPattern } from "../lib/index";
|
||||
|
||||
export function testSearchPatterns(): void {
|
||||
const src = `
|
||||
enum Role : u8 { Warrior, Mage, Rogue };
|
||||
struct Vec3 { float x; float y; float z; };
|
||||
struct Player { u32 health; u32 lives; Role role; Vec3 position; Player *next : u64; };
|
||||
struct Container { u8 tag; Player players[2]; u16 tail; };
|
||||
struct Packed { u8 tag; u32 value; } [[packed]];
|
||||
struct Explicit { u8 tag; u32 value [[align(8)]]; };
|
||||
struct Padded { u8 tag; padding[3]; u32 value; };
|
||||
struct TailPadded { u8 tag; } [[fixed_size(4)]];
|
||||
union Word { u32 value; u8 bytes[4]; };
|
||||
bitfield Bits { low : 3; bool enabled : 1; signed high : 4; };
|
||||
bitfield Ordered { low : 3; high : 13; } [[bitfield_order(1, 16)]];
|
||||
struct Flags { u8 tag; be Ordered ordered [[align(2)]]; Bits bits; };
|
||||
struct Strings { char text[4]; be char16 wide[2]; float number; float16 half; };
|
||||
template_unused(); // must not execute top-level code when instantiating types
|
||||
fn main() { std::assert(false, "main must not run"); };
|
||||
const u32 COUNT = 2;
|
||||
struct Array<T, auto N> { T values[N]; };
|
||||
namespace ns { const u32 N = 3; struct Numbers { be u16 values[N]; }; }
|
||||
struct Dynamic { u8 length; u8 payload[length]; };
|
||||
struct Unsized { char text[]; };
|
||||
struct Conditional { u8 tag; if (tag == 1) { u32 value; } };
|
||||
struct Transform { u32 value [[transform("decode")]]; };
|
||||
`;
|
||||
const program = compile(src);
|
||||
const check = (ok: boolean, msg: string): void => { if (!ok) throw new Error("search: " + msg); };
|
||||
const fails = (run: () => any, msg: string): void => {
|
||||
try { run(); } catch (e) {
|
||||
check(e instanceof Error && e.message.includes(msg), "wrong error: " + String(e));
|
||||
return;
|
||||
}
|
||||
throw new Error("search: expected error containing " + msg);
|
||||
};
|
||||
const packed = new SearchPattern(program, "Player");
|
||||
check(packed.size === 29 && packed.alignment === 1, "packed size");
|
||||
check(packed.get("health") === undefined && !packed.isDefined("health"), "unset health read as zero");
|
||||
check(packed.state() === "unset", "initial state");
|
||||
packed.set({ health: 0, position: { z: 3 } });
|
||||
check(packed.get("health") === 0n && packed.isDefined("health"), "zero is not defined");
|
||||
check(packed.state("position") === "partial" && packed.state() === "partial", "nested partial state");
|
||||
check(packed.get("position").z === 3 && packed.get("position").x === undefined, "nested get");
|
||||
packed.unset("health").unset("position");
|
||||
check(packed.state() === "unset" && packed.encode().mask.every((b) => b === 0), "unset did not clear mask");
|
||||
|
||||
const p = new SearchPattern(program, "Player", { alignment: 8 });
|
||||
check(p.size === 32 && p.alignment === 8, "aligned size");
|
||||
p.assign("health = 100 lives=1+2 role=Role::Rogue position.z=3 next=0x1122334455667788");
|
||||
const enc = p.encode();
|
||||
check(Array.from(enc.bytes.slice(0, 8)).join() === "100,0,0,0,3,0,0,0", "scalar encoding");
|
||||
check(enc.bytes[8] === 2 && enc.mask[8] === 255, "enum encoding");
|
||||
check(enc.mask.slice(9, 20).every((b) => b === 0), "padding/unknown floats constrained");
|
||||
check(Array.from(enc.bytes.slice(20, 24)).join() === "0,0,64,64", "nested float offset");
|
||||
check(Array.from(enc.bytes.slice(24)).join() === "136,119,102,85,68,51,34,17", "pointer encoding");
|
||||
check(p.fields().find((f) => f.path === "next")!.value === "1234605616436508552", "large integer JSON");
|
||||
enc.bytes.fill(0); check(p.encode().bytes[0] === 100, "encode exposed mutable state");
|
||||
const before = p.toR2();
|
||||
fails(() => p.assign("health=2 nope=3"), "unknown search field");
|
||||
fails(() => p.set({ health: 2, nope: 3 }), "unknown search field");
|
||||
check(p.toR2() === before, "failed updates changed instance");
|
||||
fails(() => p.assign("health=2 lives="), "expression");
|
||||
fails(() => p.assign("health += 1"), "field=expression");
|
||||
fails(() => p.assign("health=$[0]"), "cannot read input data");
|
||||
fails(() => p.set("health", undefined), "unset()");
|
||||
fails(() => p.set("next", 9007199254740992), "safe integer number or bigint");
|
||||
fails(() => p.state("nope"), "unknown search field");
|
||||
p.unset("position"); check(!p.isDefined("position.z"), "aggregate unset");
|
||||
p.set("next", null); check(p.isDefined("next") && p.get("next") === 0n, "null pointer was unset");
|
||||
|
||||
const c = new SearchPattern(program, "Container", { alignment: 8 });
|
||||
check(c.size === 80 && c.alignment === 8, "aligned struct array size");
|
||||
c.set("players", [{ health: 100 }, { position: { z: 3 } }]);
|
||||
check(c.fields().find((f) => f.path === "players[0].health")!.offset === 8, "first array element offset");
|
||||
check(c.fields().find((f) => f.path === "players[1].position.z")!.offset === 60, "second array element offset");
|
||||
check(c.encode().mask.slice(74).every((b) => b === 0), "tail padding constrained");
|
||||
fails(() => c.set("players[2].health", 100), "unknown search field");
|
||||
c.unset("players[0]"); check(!c.isDefined("players[0].health") && c.isDefined("players[1].position.z"), "indexed unset");
|
||||
const custom = new SearchPattern(program, "Packed", { alignment: 8 });
|
||||
check(custom.size === 5 && custom.alignment === 1, "packed type override");
|
||||
const explicit = new SearchPattern(program, "Explicit");
|
||||
explicit.set("value", 3);
|
||||
check(explicit.size === 16 && explicit.alignment === 8 && explicit.encode().bytes[8] === 3, "explicit member alignment");
|
||||
const pad = new SearchPattern(program, "Padded").set("value", 3);
|
||||
check(pad.size === 8 && pad.encode().mask.slice(0, 4).every((b) => b === 0), "explicit padding constrained");
|
||||
const tail = new SearchPattern(program, "TailPadded").set("tag", 3);
|
||||
check(tail.size === 4 && tail.encode().mask[0] === 255 && tail.encode().mask.slice(1).every((b) => b === 0), "fixed tail padding constrained");
|
||||
|
||||
const word = new SearchPattern(program, "Word").set("value", 0x12345678);
|
||||
word.set("bytes[0]", 0x78);
|
||||
const oldWord = word.toR2();
|
||||
fails(() => word.set("bytes[0]", 0x79), "conflicting search field");
|
||||
check(word.toR2() === oldWord, "conflict did not preserve instance");
|
||||
word.assign("value=0x12345679 bytes[0]=0x79"); // simultaneous overlapping update
|
||||
word.unset("value");
|
||||
check(word.encode().mask[0] === 255 && word.encode().mask.slice(1).every((b) => b === 0), "union unset removed another constraint");
|
||||
const bits = new SearchPattern(program, "Bits").set("low", 3);
|
||||
check(bits.toR2() === "03:07", "bit mask constrained neighboring fields");
|
||||
bits.set("enabled", false).set("high", -2);
|
||||
check(bits.toR2() === "e3:ff" && bits.state() === "set", "signed/bool bitfield encoding");
|
||||
bits.unset("low"); check(bits.toR2() === "e0:f8", "bitfield unset");
|
||||
const flags = new SearchPattern(program, "Flags").set("ordered.low", 3);
|
||||
check(flags.size === 6 && flags.toR2() === "000000030000:000000070000", "aligned/reversed bitfield offset");
|
||||
check(flags.fields().find((f) => f.path === "ordered.low")!.offset === 3, "physical bitfield offset");
|
||||
const str = new SearchPattern(program, "Strings").assign('text="a=b" wide="AB" number=1.5 half=1.5');
|
||||
check(str.toR2() === "613d6200004100420000c03f003e:" + "ff".repeat(14), "strings and floats");
|
||||
fails(() => str.set("text", "too long"), "string exceeds field");
|
||||
const arr = new SearchPattern(program, "Array<u16, COUNT>").assign("values[COUNT-1] = 0x1234");
|
||||
check(arr.toR2() === "00003412:0000ffff", "template/constant indices");
|
||||
const ns = new SearchPattern(program, "ns::Numbers").assign("values[1]=0x1234");
|
||||
check(ns.toR2() === "000012340000:0000ffff0000", "namespace constant/endianness");
|
||||
fails(() => new SearchPattern(program, "Dynamic"), "unset field");
|
||||
fails(() => new SearchPattern(program, "Unsized"), "fixed, contiguous");
|
||||
fails(() => new SearchPattern(program, "Conditional"), "fixed declarations");
|
||||
fails(() => new SearchPattern(program, "Transform"), "raw fields");
|
||||
fails(() => new SearchPattern(program, "Player", { alignment: 3 }), "alignment must");
|
||||
fails(() => new SearchPattern(program, "Player", { maxSize: 4 }), "maximum size");
|
||||
fails(() => new SearchPattern(program, "Player", { maxFields: 2 }), "field count");
|
||||
const abi = compile("#pragma abi native\nstruct S { u8 tag; u32 value; };");
|
||||
fails(() => new SearchPattern(abi, "S"), "target ABI is not inferred");
|
||||
check(new SearchPattern(abi, "S", { alignment: 4 }).size === 8, "explicit ABI layout policy");
|
||||
check(new SearchPattern(compile("#pragma abi packed\nstruct S { u8 tag; u32 value; };"), "S").size === 5, "packed ABI default");
|
||||
const bounded = compile("#pragma array_limit 0\nstruct Empty {}; struct Huge { Empty entries[4]; u8 tag; };");
|
||||
fails(() => new SearchPattern(bounded, "Huge", { maxFields: 2 }), "array grew past set limit");
|
||||
console.log("[test] Partial search patterns OK");
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
struct Inner { be u16 word; } [[color("00FF00")]];
|
||||
struct Header {
|
||||
u8 count [[color("FF0000"), comment("entry count")]];
|
||||
Inner inner [[name("Details")]];
|
||||
u8 data[count % 3];
|
||||
} [[color("1144AA")]];
|
||||
std::assert(false, "tree commands must not execute top-level statements");
|
||||
fn main() { std::assert(false, "tree commands must not run main()"); };
|
||||
@@ -0,0 +1,59 @@
|
||||
import { compile, decodeType, BufferHost, dumpTree, toJson, patternColor } from "../lib/index";
|
||||
|
||||
export function testTree(): void {
|
||||
const check = (ok: boolean, msg: string): void => { if (!ok) throw new Error("tree: " + msg); };
|
||||
const program = compile(`
|
||||
const u8 N = 2;
|
||||
struct Inner { u16 word; } [[color("00FF00")]];
|
||||
struct Header {
|
||||
u8 count [[color("FF0000"), comment("entry count")]];
|
||||
Inner inner [[name("Details"), tag(count)]];
|
||||
u8 data[count];
|
||||
padding[1];
|
||||
u8 secret [[hidden]];
|
||||
Inner closed [[sealed]];
|
||||
} [[color("1144AA")]];
|
||||
struct Array<T, auto Count> { T entries[Count]; };
|
||||
Header unrelated @ 10000;
|
||||
std::assert(false, "type decoder must not execute top-level statements");
|
||||
fn main() { std::assert(false, "type decoder must not call main"); };
|
||||
`);
|
||||
const data = new Uint8Array([99, 99, 2, 0x34, 0x12, 1, 2, 0, 0x78, 0x56, 3, 0]);
|
||||
const inst = decodeType(program, "Header", new BufferHost(data), 2);
|
||||
const root = inst.get("value")!;
|
||||
check(inst.patterns.length === 1 && root.offset === 2 && root.size === 9, "placed layout");
|
||||
check(inst.evaluateExpression("value.inner.word + 0") === 0x1234n, "decoded value");
|
||||
check(root.member("data")!.entryCount() === 2, "data-dependent array length");
|
||||
check(patternColor(root.member("data")!.entry(0)!) === "1144aa", "array color inheritance");
|
||||
check(patternColor(root.member("inner")!.member("word")!) === "00ff00", "member type color override");
|
||||
const json = toJson(inst, { base: 0x1000 })[0];
|
||||
check(json.addr === 0x1002 && json.color === "1144AA", "JSON address/color");
|
||||
check(json.children![0].comment === "entry count" && json.children![1].displayName === "Details", "JSON presentation metadata");
|
||||
check(json.children![1].attributes!.find((a) => a.name === "tag")!.args[0] === 2, "pattern-valued attribute serialization");
|
||||
check(json.children![2].entries![1].color === "1144AA", "JSON array entry color");
|
||||
const lines: string[] = [];
|
||||
dumpTree(inst, (line) => lines.push(line), { base: 0x1000 });
|
||||
const tree = lines.join("\n");
|
||||
check(tree.includes('value (Header) @ 0x00001002 size=9 [[color("1144AA")]]'), "root row");
|
||||
check(tree.includes('|- Details (Inner)') && tree.includes('| `- word (u16) @ 0x00001003 = 4660 (0x1234)'), "nested tree");
|
||||
check(tree.includes('tag(2)') && tree.includes('comment("entry count")'), "tree attributes");
|
||||
check(!tree.includes("secret") && !tree.includes("word (u16) @ 0x00001009"), "hidden/sealed patterns");
|
||||
check(!tree.includes("\x1b"), "plain output contains ANSI escapes");
|
||||
const limited: string[] = [];
|
||||
dumpTree(inst, (line) => limited.push(line), { patterns: [root.member("data")!], maxEntries: 1, color: true });
|
||||
check(limited[0].includes("\x1b[38;2;17;68;170m") && limited[2] === "`- ... (1 more entries)", "colored/limited tree");
|
||||
inst.evaluateExpression("std::core::set_pattern_color(value.data, 0x00efcdab)");
|
||||
check(patternColor(root.member("data")!.entry(0)!) === "abcdef", "runtime ABGR color");
|
||||
check(toJson(inst)[0].children![2].color === "abcdef", "runtime color serialization");
|
||||
check(decodeType(program, "Array<u16, N>", new BufferHost(data)).patterns[0].size === 4, "template constants");
|
||||
const conflict = decodeType(compile("const u8 value = 2; struct H { u8 bytes[value]; };"), "H", new BufferHost(data));
|
||||
check(conflict.patterns[0].name === "value_" && conflict.patterns[0].size === 2, "root collides with a constant");
|
||||
for (const offset of [-1, 1.5, data.length + 1, Number.MAX_SAFE_INTEGER + 1]) {
|
||||
let failed = false;
|
||||
try { decodeType(program, "Header", new BufferHost(data), offset); } catch (e) {
|
||||
failed = e instanceof Error && e.message.includes("outside the data");
|
||||
}
|
||||
check(failed, "invalid offset " + offset);
|
||||
}
|
||||
console.log("[test] Address-bound trees OK");
|
||||
}
|
||||
+128
@@ -1,5 +1,8 @@
|
||||
/// <reference path="../lib/globals.d.ts" />
|
||||
import { runHexpat, BufferHost } from "../lib/index";
|
||||
import { testSearchPatterns } from "./search_unit";
|
||||
import { testMagic } from "./magic_unit";
|
||||
import { testTree } from "./tree_unit";
|
||||
|
||||
const src = `
|
||||
struct Header {
|
||||
@@ -32,3 +35,128 @@ const parsed = JSON.parse(js);
|
||||
if (parsed[0].name !== "header" || parsed[0].addr !== 0x1000 || parsed[0].children[0].value !== 0x464c457f) throw new Error("json mismatch!");
|
||||
if (parsed[1].name !== "checksum" || parsed[1].value !== 0xddccbbaa) throw new Error("json checksum mismatch!");
|
||||
console.log("[test] JSON OK");
|
||||
|
||||
// Interactive assignments must reach the host bytes, including overlapping views.
|
||||
const editData = new Uint8Array(64).fill(0xff);
|
||||
const edit = runHexpat(`
|
||||
struct Header { u32 value; be u16 big; s8 signedValue; bool enabled; };
|
||||
Header header @ 0;
|
||||
u32 alias @ 0;
|
||||
u16 values[2] @ 8;
|
||||
bitfield Bits { low : 3; bool enabled : 1; signed high : 4; };
|
||||
Bits bits @ 12;
|
||||
be Bits big_bits @ 13;
|
||||
bitfield Ordered { low : 3; high : 13; } [[bitfield_order(1, 16)]];
|
||||
be Ordered ordered @ 14;
|
||||
char text[4] @ 16;
|
||||
be char16 wide[2] @ 20;
|
||||
float number @ 24;
|
||||
be double big_number @ 28;
|
||||
float16 half @ 36;
|
||||
struct PointerHeader { u8 *ptr : u8; };
|
||||
PointerHeader pointers @ 40;
|
||||
const u8 fixed = 7;
|
||||
fn set_value(auto v) {
|
||||
auto old = alias + 0;
|
||||
header.value = v;
|
||||
std::assert(alias == v, "host cache retained bytes after a write");
|
||||
};
|
||||
`, new BufferHost(editData));
|
||||
|
||||
function check(ok: boolean, msg: string): void { if (!ok) throw new Error(msg); }
|
||||
function fails(ev: typeof edit, src: string, message: string): void {
|
||||
try { ev.evaluateExpression(src); }
|
||||
catch (e) {
|
||||
check(e instanceof Error && e.message.indexOf(message) >= 0, "wrong error for " + src + ": " + String(e));
|
||||
return;
|
||||
}
|
||||
throw new Error("expected an error for " + src);
|
||||
}
|
||||
function bytes(addr: number, expected: number[]): void {
|
||||
check(expected.every((b, i) => editData[addr + i] === b), "wrong bytes at " + addr);
|
||||
}
|
||||
edit.evaluateExpression("alias + 0"); // cache the old value before the write
|
||||
check(edit.evaluateExpression("header.value= 3") === undefined, "assignment should return no value");
|
||||
bytes(0, [3, 0, 0, 0]);
|
||||
check(edit.evaluateExpression("alias + 0") === 3n, "overlapping view is stale");
|
||||
edit.evaluateExpression("header.value += 2;");
|
||||
check(edit.evaluateExpression("header.value == 5") === true, "equality parsed as assignment");
|
||||
check(edit.evaluateExpression("header.value >= 5") === true, "comparison parsed as assignment");
|
||||
check(edit.evaluateExpression("header.value <= 5") === true, "comparison parsed as assignment");
|
||||
check(edit.evaluateExpression("header.value != 3") === true, "inequality parsed as assignment");
|
||||
check(edit.evaluateExpression('"a=b"') === "a=b", "string parsed as assignment");
|
||||
fails(edit, "header.value = 9; header.value = 10", "trailing input");
|
||||
fails(edit, "header.value =", "expression");
|
||||
fails(edit, "header.value /= 0", "division by zero");
|
||||
bytes(0, [5, 0, 0, 0]);
|
||||
edit.evaluateExpression("header.big = 0x1234");
|
||||
edit.evaluateExpression("header.signedValue = -2");
|
||||
edit.evaluateExpression("header.enabled = false");
|
||||
bytes(4, [0x12, 0x34, 0xfe, 0]);
|
||||
edit.evaluateExpression("values[1] = 0x5678");
|
||||
edit.evaluateExpression("values[0] = values[1] + 1");
|
||||
bytes(8, [0x79, 0x56, 0x78, 0x56]);
|
||||
fails(edit, "values[2] = 1", "out of bounds");
|
||||
fails(edit, "fixed = 3", "constant variable");
|
||||
edit.evaluateExpression("bits.low = 3");
|
||||
edit.evaluateExpression("bits.enabled = false");
|
||||
bytes(12, [0xf3]);
|
||||
edit.evaluateExpression("bits.high = -2");
|
||||
bytes(12, [0xe3]);
|
||||
edit.evaluateExpression("big_bits.low = 3");
|
||||
edit.evaluateExpression("big_bits.enabled = false");
|
||||
bytes(13, [0x6f]);
|
||||
edit.evaluateExpression("ordered.low = 3");
|
||||
bytes(14, [0xff, 0xfb]);
|
||||
check(edit.evaluateExpression("ordered.high + 0") === 8191n, "bitfield write changed neighbor");
|
||||
edit.evaluateExpression('text = "ab"');
|
||||
bytes(16, [0x61, 0x62, 0, 0]);
|
||||
edit.evaluateExpression('wide = "A"');
|
||||
bytes(20, [0, 0x41, 0, 0]);
|
||||
edit.evaluateExpression("text[1] = 'c'");
|
||||
bytes(16, [0x61, 0x63, 0, 0]);
|
||||
edit.evaluateExpression("wide[1] = 'B'");
|
||||
bytes(20, [0, 0x41, 0, 0x42]);
|
||||
edit.evaluateExpression("number = 1.5");
|
||||
bytes(24, [0, 0, 0xc0, 0x3f]);
|
||||
edit.evaluateExpression("big_number = -2.0");
|
||||
bytes(28, [0xc0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
edit.evaluateExpression("half = 1.5");
|
||||
bytes(36, [0, 0x3e]);
|
||||
check(edit.evaluateExpression("half + 0") === 1.5, "float16 write failed");
|
||||
edit.evaluateExpression("half = 1.00048828125"); // ties round to an even significand
|
||||
bytes(36, [0, 0x3c]);
|
||||
edit.evaluateExpression("half = 0.000000059604644775390625");
|
||||
bytes(36, [1, 0]);
|
||||
edit.evaluateExpression("set_value(9)");
|
||||
bytes(0, [9, 0, 0, 0]);
|
||||
edit.evaluateExpression("pointers.ptr = 48");
|
||||
bytes(40, [48]);
|
||||
check(edit.value(edit.get("pointers")!.member("ptr")!) === 48n, "pointer assignment wrote pointee instead of pointer");
|
||||
editData[0] = 10;
|
||||
check(edit.evaluateExpression("header.value + 0") === 10n, "external host write is stale");
|
||||
|
||||
// Pattern-file assignments retain the reference runtime's read-only semantics.
|
||||
const initial = runHexpat("u8 value @ 0; value = 7;", new BufferHost(editData));
|
||||
check(editData[0] === 10 && initial.value(initial.get("value")!) === 7n, "pattern evaluation wrote host data");
|
||||
initial.evaluateExpression("value = 11");
|
||||
check(editData[0] === 11 && initial.value(initial.get("value")!) === 11n, "interactive assignment kept an override");
|
||||
const readonly = runHexpat("u8 value @ 0; u8 local = 1;", { size: () => 1, read: () => new Uint8Array([2]) });
|
||||
fails(readonly, "value = 3", "read-only");
|
||||
check(readonly.evaluateExpression("value + 0") === 2n, "failed write changed visible value");
|
||||
readonly.evaluateExpression("local = 3");
|
||||
check(readonly.evaluateExpression("local + 0") === 3n, "local assignment needs writable host data");
|
||||
const rejecting = runHexpat("u8 value @ 0; value = 7;", {
|
||||
size: () => 1, read: () => new Uint8Array([2]), write: () => { throw new Error("write rejected"); },
|
||||
});
|
||||
fails(rejecting, "value = 3", "write rejected");
|
||||
check(rejecting.evaluateExpression("value + 0") === 7n, "failed write lost previous value");
|
||||
const outside = runHexpat("u32 value @ 63;", new BufferHost(editData));
|
||||
fails(outside, "value = 3", "outside the data");
|
||||
const transformed = runHexpat('fn decode(auto v) { return v + 1; }; u8 value @ 0 [[transform("decode")]];', new BufferHost(editData));
|
||||
fails(transformed, "value = 3", "transformed field");
|
||||
check(editData[0] === 11, "rejected field write modified host bytes");
|
||||
console.log("[test] Interactive writes OK");
|
||||
testSearchPatterns();
|
||||
testMagic();
|
||||
testTree();
|
||||
@@ -6,6 +6,9 @@
|
||||
*/
|
||||
import { runHexpat, BufferHost } from "../lib/index";
|
||||
import { VIRTUAL_SOURCES, HOOKS } from "./upstream_hooks";
|
||||
import { testSearchPatterns } from "./search_unit";
|
||||
import { testMagic } from "./magic_unit";
|
||||
import { testTree } from "./tree_unit";
|
||||
|
||||
const size = JSON.parse(r2.cmd("ij")).core.size;
|
||||
const hex = r2.cmd("p8 " + size + " @ 0").replace(/[^0-9a-fA-F]/g, "");
|
||||
@@ -35,3 +38,6 @@ for (const f of files) {
|
||||
}
|
||||
console.log("==================================");
|
||||
console.log("r2: " + passed + "/" + files.length + " passed" + (unexpected.length ? ", unexpected failures: " + unexpected.join(" ") : ""));
|
||||
try { testSearchPatterns(); } catch (e) { console.log("FAIL partial search patterns: " + String(e)); }
|
||||
try { testMagic(); } catch (e) { console.log("FAIL magic export: " + String(e)); }
|
||||
try { testTree(); } catch (e) { console.log("FAIL address-bound trees: " + String(e)); }
|
||||
Reference in new issue
Block a user