10 Commits
41 changed files with 3123 additions and 80 deletions

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "##build "
prefix-development: "##build "
labels:
- buildsystem
+2 -2
View File
@@ -16,8 +16,8 @@ jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
+3 -3
View File
@@ -14,8 +14,8 @@ jobs:
release:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
@@ -29,7 +29,7 @@ jobs:
- name: Test
run: make BUNDLER=esbuild test
- name: Create GitHub release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
name: ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
+2 -2
View File
@@ -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)
+211 -3
View File
@@ -29,15 +29,184 @@ Commands (all output goes through r2's console, so `~grep` and `|` work):
| `hexpat [-q] [-I dir] [file]` | evaluate file (replacing the loaded ones) and show its patterns; without file show the loaded ones |
| `hexpat+ [-I dir] file` | evaluate file and add it to the loaded ones |
| `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 |
Any `file` argument can be `base64:<data>` to evaluate the encoded source
directly, so frontends (like iaito) can feed the contents of an editor on every
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
@@ -57,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
View File
@@ -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)
+10
View File
@@ -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
View File
@@ -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) {
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
}
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "r2hexpat",
"version": "0.1.0",
"version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "r2hexpat",
"version": "0.1.0",
"version": "0.3.0",
"license": "MIT",
"devDependencies": {
"esbuild": "^0.25.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "r2hexpat",
"version": "0.1.0",
"version": "0.3.0",
"description": "ImHex pattern language (.hexpat) evaluator for radare2",
"private": true,
"license": "MIT",
+861
View File
@@ -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;
}
+319 -19
View File
@@ -3,9 +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) {}
@@ -24,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); }
@@ -58,22 +71,76 @@ export class R2Host implements HexpatHost {
}
}
const VERSION = "0.3.0";
const HELP = [
"Usage: hexpat[?+-jl*e] [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",
"| hexpat- [n] unload all loaded files (or the nth)",
"| 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",
"| hexpate expr evaluate an expression in the context of the last loaded file",
"| hexpats [file] one-line summary (types, fields, patterns) or the error, for status bars",
"| 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",
];
interface Session { file: string; instance: PatternInstance; host: R2Host; }
interface Session { file: string; program: A.Program; instance: PatternInstance; host: R2Host; }
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 {
@@ -82,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++) {
@@ -93,16 +328,22 @@ 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 src = host.readFile(file);
if (!src || src.trim() === "") throw new Error("cannot read " + file);
const inline = file.startsWith(B64);
const src = inline ? b64(file.substring(B64.length), true) : host.readFile(file);
if (inline) file = "base64";
else if (!src || src.trim() === "") throw new Error("cannot read " + file);
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);
return { file, instance: evaluate(program, host), host };
const program = compile(src || "", host, file);
host.baseAddress = programBase(program);
return { file, program, instance: evaluate(program, host), host };
}
/** Sessions to show: the given file (replacing the loaded ones) or all loaded. */
@@ -137,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);
@@ -163,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 {
@@ -182,17 +426,55 @@ function list(out: Out, json: boolean): void {
});
}
/** Number of member declarations in a struct/union/bitfield body (including conditional ones). */
function countFields(body: A.Stmt[]): number {
let n = 0;
for (const st of body) {
switch (st.s) {
case "decl": n++; break;
case "multi": n += st.decls.length; break;
case "if": n += countFields(st.then) + countFields(st.else || []); break;
case "while": case "for": case "try": case "block": case "nsctx": n += countFields(st.body); break;
case "match": for (const c of st.cases) n += countFields(c.body); break;
}
}
return n;
}
/** One line summary of the loaded sessions: types with their field counts, functions and placed patterns. */
function stats(ss: Session[]): string {
const comp: string[] = [];
let enums = 0, fns = 0, patterns = 0, size = 0;
for (const s of ss) {
for (const st of s.program.body) {
if (st.s === "struct" || st.s === "union" || st.s === "bitfield") comp.push(st.ns.concat(st.name).join("::") + ":" + countFields(st.body));
else if (st.s === "enum") enums++;
else if (st.s === "fn") fns++;
}
patterns += s.instance.patterns.length;
for (const p of s.instance.patterns) size += p.size;
}
return "types " + comp.length + (comp.length ? " (" + comp.join(" ") + ")" : "") +
" enums " + enums + " fns " + fns + " patterns " + patterns + " size " + size;
}
function hexpatCommand(cmd: string): void {
const out = new Out();
const sub = cmd.substr(6, 1);
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 " ":
if (args.indexOf("-h") >= 0 || args.indexOf("--help") >= 0) { for (const l of HELP) out.push(l); break; }
for (const s of target(args)) s.instance.dump(out.push, s.host.baseAddress);
const targets = target(args);
if (targets.length === 0 && args.length === 0) {
out.push("no patterns loaded, run 'hexpat?' for help");
} else {
for (const s of targets) s.instance.dump(out.push, s.host.baseAddress);
}
break;
case "+": {
const a = parseArgs(args);
@@ -203,7 +485,13 @@ function hexpatCommand(cmd: string): void {
break;
}
case "-": {
if (rest === "" || rest === "*") sessions.length = 0;
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);
if (isNaN(n) || n < 0 || n >= sessions.length) throw new Error("no loaded pattern file #" + rest);
@@ -225,18 +513,30 @@ function hexpatCommand(cmd: string): void {
break;
}
case "l": list(out, rest === "j" || args[0] === "j"); break;
case "s": {
// errors go to stdout too, so frontends get a single line in any case
try {
out.push(stats(target(args, true)));
} catch (e) {
out.push("error: " + (e instanceof Error ? e.message : String(e)));
}
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
s = { file: "", instance: evaluate(compile("", undefined), new R2Host(r2, R2Host.binBaseAddress(r2))), host: new R2Host(r2) };
s = { file: "", program: { pragmas: [], body: [] }, instance: evaluate(compile("", undefined), new R2Host(r2, R2Host.binBaseAddress(r2))), host: new R2Host(r2) };
}
const v = s.instance.evaluateExpression(rest);
const d = s.instance.describe(v, s.host.baseAddress);
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);
}
@@ -260,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*le".indexOf(c) < 0) return false;
if (c !== "" && " ?h+-j*lespmt".indexOf(c) < 0) return false;
hexpatCommand(cmd);
return true;
},
Vendored
+1
View File
@@ -9,5 +9,6 @@ declare global {
unload(type: string, name: string): void;
}
var r2: R2JsBridge;
function b64(data: string, decode?: boolean): string;
}
export {};
+61
View File
@@ -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
+31
View File
@@ -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
+23
View File
@@ -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
+63
View File
@@ -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)
+50
View File
@@ -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
+63
View File
@@ -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'
+7
View File
@@ -26,3 +26,10 @@ hexpate u8(-1)
?e -- errors
hexpate nope
hexpat tests/does_not_exist.hexpat
hexpats tests/does_not_exist.hexpat
?e -- one line stats, inline base64 sources
hexpats tests/syntax_structs.hexpat
hexpats
hexpats base64:Ly8gU3RydWN0cywgbmVzdGVkIHN0cnVjdHMsIHVuaW9ucwpzdHJ1Y3QgSGVhZGVyIHsKICAgIHUzMiBtYWdpYzsKICAgIHUxNiB2ZXJzaW9uOwogICAgdTE2IGZsYWdzOwogICAgdTY0IHRpbWVzdGFtcDsKfTsKdW5pb24gV29yZCB7CiAgICB1MzIgdmFsdWU7CiAgICB1OCBieXRlc1s0XTsKfTsKc3RydWN0IEZpbGUgewogICAgSGVhZGVyIGhlYWQ7CiAgICBXb3JkIHdvcmQ7CiAgICB1OCBsZW47CiAgICB1OCBkYXRhW2xlbiAlIDRdOwp9OwpGaWxlIGZpbGUgQCAweDAwOwp1MzIgZm9vdGVyIEAgJDsK
hexpatl
hexpats base64:c3RydWN0IEEgeyB1OCB4OyA=
+18 -2
View File
@@ -1,15 +1,22 @@
-- help
Usage: hexpat[?+-jl*e] [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
| hexpat- [n] unload all loaded files (or the nth)
| 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
| hexpate expr evaluate an expression in the context of the last loaded file
| hexpats [file] one-line summary (types, fields, patterns) or the error, for status bars
| 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
-- load one file, list, evaluate expressions in its context
fib=55 kind=low high sizeof=6 type=Pair<u16, 2>
0 tests/syntax_functions.hexpat (2 patterns)
@@ -45,5 +52,14 @@ probe2 (u8) @ 0x0000000b = 164 (0xa4)
145 (0x91)
255 (0xff)
-- errors
error: cannot read tests/does_not_exist.hexpat
-- one line stats, inline base64 sources
types 3 (Header:4 Word:2 File:4) enums 0 fns 0 patterns 2 size 26
types 3 (Header:4 Word:2 File:4) enums 0 fns 0 patterns 2 size 26
types 3 (Header:4 Word:2 File:4) enums 0 fns 0 patterns 2 size 26
0 base64 (2 patterns)
0x00000000 22 file (File)
0x00000016 4 footer (u32)
error: expected '}' at end of input (base64:0:0)
hexpat: error: variable 'nope' does not exist (<expr>:1:1)
hexpat: cannot read tests/does_not_exist.hexpat
+60
View 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
+134
View File
@@ -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
+29
View File
@@ -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; };
+22
View File
@@ -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");
+73
View File
@@ -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");
}
+8
View File
@@ -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");
+127
View File
@@ -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");
}
+8
View File
@@ -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()"); };
+59
View File
@@ -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
View File
@@ -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
View File
@@ -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)); }
Executable
+47
View File
@@ -0,0 +1,47 @@
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
PKG="$SCRIPT_DIR/package.json"
LOCK="$SCRIPT_DIR/package-lock.json"
PLUGIN="$SCRIPT_DIR/r2/plugin.ts"
for f in "$PKG" "$LOCK" "$PLUGIN"; do
if [ ! -f "$f" ]; then
printf '%s\n' "version.sh: missing file: $f" >&2
exit 1
fi
done
current_version=$(awk -F'"' '/"version": "/ { print $4; exit }' "$PKG")
if [ "$#" -eq 0 ]; then
printf '%s\n' "$current_version"
exit 0
fi
new_version=$1
if ! printf '%s\n' "$new_version" | grep -Eq '^[0-9]+(\.[0-9]+){2}$'; then
printf '%s\n' "version.sh: expected version in the form X.Y.Z, got: $new_version" >&2
exit 1
fi
plugin_matches=$(grep -c '^const VERSION = "' "$PLUGIN" || true)
if [ "$plugin_matches" -ne 1 ]; then
printf '%s\n' "version.sh: expected exactly one version string in $PLUGIN, found $plugin_matches" >&2
exit 1
fi
plugin_version=$(awk -F'"' '/^const VERSION = "/ { print $2; exit }' "$PLUGIN")
perl -pi -e "s/^const VERSION = \"[^\"]*\"/const VERSION = \"$new_version\"/" "$PLUGIN"
# only the first "version" key in package.json (the package itself)
perl -pi -e "s/\"version\": \"[^\"]*\"/\"version\": \"$new_version\"/ if !\$done && (\$done = /\"version\": /)" "$PKG"
# package-lock.json: the top-level entry and the root package ("") entry
perl -0pi -e "s/\A(\{\s*\"name\": \"r2hexpat\",\s*\"version\": \")[^\"]*\"/\${1}$new_version\"/; s/(\"\": \{\s*\"name\": \"r2hexpat\",\s*\"version\": \")[^\"]*\"/\${1}$new_version\"/" "$LOCK"
printf 'package: %s -> %s\n' "$current_version" "$new_version"
printf 'plugin: %s -> %s\n' "$plugin_version" "$new_version"