@thru/abi 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ import type { BinaryOperator, Expression, UnaryOperator } from "./abiSchema";
2
+ import type { DecodedStructValue, DecodedValue } from "./decodedValue";
3
+ import { AbiDecodeError } from "./errors";
4
+ import type { TypeRegistry } from "./typeRegistry";
5
+ import { getConstSize, getTypeAlignment } from "./decoder";
6
+
7
+ export interface FieldScope {
8
+ fields: Map<string, DecodedValue>;
9
+ parent?: FieldScope;
10
+ }
11
+
12
+ export function evaluateExpression(expression: Expression, scope: FieldScope | undefined, context: string, registry?: TypeRegistry): bigint {
13
+ switch (expression.type) {
14
+ case "literal":
15
+ return expression.value;
16
+ case "field-ref":
17
+ return toBigIntValue(resolveFieldPath(expression.path, scope, context), context);
18
+ case "binary":
19
+ return applyBinaryOperator(
20
+ expression.op,
21
+ evaluateExpression(expression.left, scope, `${context} (left)`, registry),
22
+ evaluateExpression(expression.right, scope, `${context} (right)`, registry),
23
+ context,
24
+ );
25
+ case "unary":
26
+ return applyUnaryOperator(
27
+ expression.op,
28
+ evaluateExpression(expression.operand, scope, `${context} (operand)`, registry),
29
+ context,
30
+ );
31
+ case "sizeof":
32
+ if (!registry) {
33
+ throw new AbiDecodeError(`Cannot evaluate sizeof(${expression.typeName}) without a TypeRegistry`);
34
+ }
35
+ const type = registry.get(expression.typeName);
36
+ // We need access to getConstSize from decoder.ts or similar logic
37
+ // Since getConstSize is currently internal to decoder.ts and not exported/accessible easily without refactoring
38
+ // We will assume getConstSize is made available or we implement a simple lookup if possible.
39
+ // However, decoder.ts imports expression.ts, so we have a circular dependency if we import getConstSize directly from decoder.ts
40
+ // ideally getConstSize should be in a separate file.
41
+ // For now, let's use the imported one if we can move it, or throw/stub it.
42
+ const size = getConstSize(type.kind, registry, new Map(), new Set());
43
+ if (size === null) {
44
+ throw new AbiDecodeError(`sizeof(${expression.typeName}) is not constant`);
45
+ }
46
+ return BigInt(size);
47
+ case "alignof":
48
+ if (!registry) {
49
+ throw new AbiDecodeError(`Cannot evaluate alignof(${expression.typeName}) without a TypeRegistry`);
50
+ }
51
+ const targetType = registry.get(expression.typeName);
52
+ const alignment = Math.max(getTypeAlignment(targetType.kind, registry), 1);
53
+ return BigInt(alignment);
54
+ default:
55
+ expression satisfies never;
56
+ throw new AbiDecodeError(`Unsupported expression encountered while decoding ${context}`);
57
+ }
58
+ }
59
+
60
+ export function resolveFieldPath(path: string[], scope: FieldScope | undefined, context: string): DecodedValue {
61
+ if (path.length === 0) {
62
+ throw new AbiDecodeError(`Invalid field-ref in ${context}: path cannot be empty`);
63
+ }
64
+ if (!scope) {
65
+ throw new AbiDecodeError(`Unable to resolve field '${path.join(".")}' in ${context}`);
66
+ }
67
+
68
+ const [head, ...tail] = path;
69
+
70
+ if (head === "..") {
71
+ if (!scope.parent) {
72
+ throw new AbiDecodeError(`Field reference in ${context} attempted to access parent scope, but none exists`);
73
+ }
74
+ return resolveFieldPath(tail, scope.parent, context);
75
+ }
76
+
77
+ if (scope.fields.has(head)) {
78
+ const value = scope.fields.get(head)!;
79
+ if (tail.length === 0) {
80
+ return value;
81
+ }
82
+ return resolveNestedStructValue(value, tail, context);
83
+ }
84
+
85
+ return resolveFieldPath(path, scope.parent, context);
86
+ }
87
+
88
+ function resolveNestedStructValue(value: DecodedValue, path: string[], context: string): DecodedValue {
89
+ if (value.kind !== "struct") {
90
+ throw new AbiDecodeError(
91
+ `Field reference '${path.join(".")}' in ${context} traversed through non-struct value of kind '${value.kind}'`,
92
+ );
93
+ }
94
+ const [head, ...tail] = path;
95
+ const nested = value.fields[head];
96
+ if (!nested) {
97
+ throw new AbiDecodeError(`Struct field '${head}' referenced in ${context} does not exist`);
98
+ }
99
+ if (tail.length === 0) {
100
+ return nested;
101
+ }
102
+ return resolveNestedStructValue(nested, tail, context);
103
+ }
104
+
105
+ function toBigIntValue(value: DecodedValue, context: string): bigint {
106
+ if (value.kind !== "primitive") {
107
+ throw new AbiDecodeError(`Expression in ${context} referenced non-primitive value of kind '${value.kind}'`);
108
+ }
109
+ return typeof value.value === "bigint" ? value.value : BigInt(value.value);
110
+ }
111
+
112
+ function applyBinaryOperator(op: BinaryOperator, left: bigint, right: bigint, context: string): bigint {
113
+ switch (op) {
114
+ case "add":
115
+ return left + right;
116
+ case "sub":
117
+ return left - right;
118
+ case "mul":
119
+ return left * right;
120
+ case "div":
121
+ if (right === 0n) {
122
+ throw new AbiDecodeError(`Division by zero while evaluating expression for ${context}`);
123
+ }
124
+ return left / right;
125
+ case "mod":
126
+ if (right === 0n) {
127
+ throw new AbiDecodeError(`Modulo by zero while evaluating expression for ${context}`);
128
+ }
129
+ return left % right;
130
+ case "bit-and":
131
+ return left & right;
132
+ case "bit-or":
133
+ return left | right;
134
+ case "bit-xor":
135
+ return left ^ right;
136
+ case "left-shift":
137
+ return left << right;
138
+ case "right-shift":
139
+ return left >> right;
140
+ default:
141
+ op satisfies never;
142
+ throw new AbiDecodeError(`Binary operator '${op}' is not supported in ${context}`);
143
+ }
144
+ }
145
+
146
+ function applyUnaryOperator(op: UnaryOperator, operand: bigint, context: string): bigint {
147
+ switch (op) {
148
+ case "bit-not":
149
+ return ~operand;
150
+ default:
151
+ op satisfies never;
152
+ throw new AbiDecodeError(`Unary operator '${op}' is not supported in ${context}`);
153
+ }
154
+ }
155
+
156
+ export function createScope(parent?: FieldScope): FieldScope {
157
+ return { fields: new Map(), parent };
158
+ }
159
+
160
+ export function addFieldToScope(scope: FieldScope | undefined, name: string, value: DecodedValue) {
161
+ if (scope) {
162
+ scope.fields.set(name, value);
163
+ }
164
+ }
165
+
@@ -0,0 +1,249 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+ import YAML from "yaml";
6
+ import {
7
+ AbiDecodeError,
8
+ AbiValidationError,
9
+ DecodedArrayValue,
10
+ DecodedPrimitiveValue,
11
+ DecodedStructValue,
12
+ DecodedValue,
13
+ decodeData,
14
+ } from "./index";
15
+
16
+ const here = fileURLToPath(new URL(".", import.meta.url));
17
+ const repoRoot = path.resolve(here, "../../../..");
18
+ const complianceRoot = path.join(repoRoot, "abi/abi_gen/tests/compliance_tests");
19
+
20
+ const abiDefinitions = (relativePath: string) => fs.readFileSync(path.join(complianceRoot, "abi_definitions", relativePath), "utf8");
21
+ const testCasePath = (relativePath: string) => path.join(complianceRoot, "test_cases", relativePath);
22
+
23
+ const loadBinaryFromTestCase = (relativePath: string): Uint8Array => {
24
+ const raw = fs.readFileSync(testCasePath(relativePath), "utf8");
25
+ const parsed = YAML.parse(raw);
26
+ const hex = parsed?.["test-case"]?.["binary-hex"];
27
+ if (typeof hex !== "string") {
28
+ throw new Error(`Test case '${relativePath}' is missing binary-hex data`);
29
+ }
30
+ return hexToBytes(hex);
31
+ };
32
+
33
+ describe("decodeData happy path", () => {
34
+ it("decodes primitives with accurate numeric types", () => {
35
+ const abiText = abiDefinitions("primitives.abi.yaml");
36
+ const binary = loadBinaryFromTestCase("primitives/common_values.yaml");
37
+
38
+ const result = decodeData(abiText, "AllPrimitives", binary) as DecodedStructValue;
39
+
40
+ const fields = result.fields;
41
+ expect(asPrimitive(fields.u8_val).value).toBe(42);
42
+ expect(asPrimitive(fields.u16_val).value).toBe(1000);
43
+ expect(asPrimitive(fields.u32_val).value).toBe(0x12345678);
44
+ expect(asPrimitive(fields.u64_val).value).toBe(0x123456789abcdef0n);
45
+ expect(asPrimitive(fields.i8_val).value).toBe(-42);
46
+ expect(asPrimitive(fields.i16_val).value).toBe(-1234);
47
+ expect(asPrimitive(fields.i32_val).value).toBe(-123456);
48
+ expect(asPrimitive(fields.i64_val).value).toBe(-123_456_789n);
49
+ expect(asPrimitive(fields.f32_val).value).toBeCloseTo(3.14159, 5);
50
+ expect(asPrimitive(fields.f64_val).value).toBeCloseTo(2.718281828459045, 12);
51
+ });
52
+
53
+ it("handles interleaved variable-length arrays", () => {
54
+ const abiText = abiDefinitions("array_structs.abi.yaml");
55
+ const binary = loadBinaryFromTestCase("array_structs/dual_arrays.yaml");
56
+
57
+ const result = decodeData(abiText, "DualArrays", binary) as DecodedStructValue;
58
+ const arr1 = result.fields.arr1 as DecodedArrayValue;
59
+ const arr2 = result.fields.arr2 as DecodedArrayValue;
60
+
61
+ expect(arr1.length).toBe(3);
62
+ expect(arr1.elements.map((el) => asPrimitive(el).value)).toEqual([0x11, 0x22, 0x33]);
63
+
64
+ expect(arr2.length).toBe(2);
65
+ expect(arr2.elements.map((el) => asPrimitive(el).value)).toEqual([0x4444, 0x5555]);
66
+ });
67
+
68
+ it("evaluates array size expressions using field references", () => {
69
+ const abiText = abiDefinitions("array_structs.abi.yaml");
70
+ const binary = loadBinaryFromTestCase("array_structs/matrix.yaml");
71
+
72
+ const result = decodeData(abiText, "Matrix", binary) as DecodedStructValue;
73
+
74
+ const rows = Number(asPrimitive(result.fields.rows).value);
75
+ const cols = Number(asPrimitive(result.fields.cols).value);
76
+ const dataField = result.fields.data as DecodedArrayValue;
77
+
78
+ expect(dataField.length).toBe(rows * cols + 1);
79
+ expect(dataField.elements.map((el) => asPrimitive(el).value)).toEqual([1, 2, 3, 4, 5, 6, 255]);
80
+ });
81
+
82
+ it("selects size-discriminated union variants based on runtime size", () => {
83
+ const abiText = sizeDiscriminatedUnionAbi;
84
+ const shortData = hexToBytes("04 00 00 00");
85
+ const longData = hexToBytes("01 00 00 00 02 00 00 00");
86
+
87
+ const short = decodeData(abiText, "Payload", shortData);
88
+ expect(short.kind).toBe("size-discriminated-union");
89
+ if (short.kind === "size-discriminated-union") {
90
+ expect(short.variantName).toBe("Short");
91
+ const value = short.value as DecodedStructValue;
92
+ expect(asPrimitive(value.fields.value).value).toBe(4);
93
+ }
94
+
95
+ const long = decodeData(abiText, "Payload", longData);
96
+ expect(long.kind).toBe("size-discriminated-union");
97
+ if (long.kind === "size-discriminated-union") {
98
+ expect(long.variantName).toBe("Long");
99
+ const value = long.value as DecodedStructValue;
100
+ expect(asPrimitive(value.fields.head).value).toBe(1);
101
+ expect(asPrimitive(value.fields.tail).value).toBe(2);
102
+ }
103
+ });
104
+ });
105
+
106
+ describe("decodeData error cases", () => {
107
+ it("throws when buffer is too short", () => {
108
+ const abiText = abiDefinitions("primitives.abi.yaml");
109
+ const binary = loadBinaryFromTestCase("primitives/common_values.yaml").slice(0, 10);
110
+ expect(() => decodeData(abiText, "AllPrimitives", binary)).toThrowError(AbiDecodeError);
111
+ });
112
+
113
+ it("rejects references to unknown types", () => {
114
+ const abiText = `
115
+ abi:
116
+ package: "test"
117
+ abi-version: 1
118
+ types:
119
+ - name: "HasRef"
120
+ kind:
121
+ struct:
122
+ packed: true
123
+ fields:
124
+ - name: "missing"
125
+ field-type:
126
+ type-ref:
127
+ name: "Nope"
128
+ `;
129
+ expect(() => decodeData(abiText, "HasRef", new Uint8Array()))
130
+ .toThrowError(AbiValidationError);
131
+ });
132
+
133
+ it("detects simple reference cycles", () => {
134
+ const abiText = `
135
+ abi:
136
+ package: "test"
137
+ abi-version: 1
138
+ types:
139
+ - name: "Loop"
140
+ kind:
141
+ struct:
142
+ packed: true
143
+ fields:
144
+ - name: "next"
145
+ field-type:
146
+ type-ref:
147
+ name: "Loop"
148
+ `;
149
+ expect(() => decodeData(abiText, "Loop", new Uint8Array()))
150
+ .toThrowError(AbiValidationError);
151
+ });
152
+
153
+ it("reports unsupported expression operators", () => {
154
+ const abiText = `
155
+ abi:
156
+ package: "test"
157
+ abi-version: 1
158
+ types:
159
+ - name: "BadArray"
160
+ kind:
161
+ struct:
162
+ packed: true
163
+ fields:
164
+ - name: "length"
165
+ field-type:
166
+ primitive: u32
167
+ - name: "data"
168
+ field-type:
169
+ array:
170
+ size:
171
+ pow:
172
+ left:
173
+ field-ref:
174
+ path: ["length"]
175
+ right:
176
+ literal:
177
+ u32: 2
178
+ element-type:
179
+ primitive: u8
180
+ `;
181
+ expect(() => decodeData(abiText, "BadArray", new Uint8Array()))
182
+ .toThrowError(AbiValidationError);
183
+ });
184
+
185
+ it("errors when no size-discriminated union variant matches", () => {
186
+ const data = hexToBytes("01 02 03");
187
+ expect(() => decodeData(sizeDiscriminatedUnionAbi, "Payload", data))
188
+ .toThrowError(AbiDecodeError);
189
+ });
190
+
191
+ it("throws when requested type is absent", () => {
192
+ const abiText = abiDefinitions("primitives.abi.yaml");
193
+ expect(() => decodeData(abiText, "MissingType", new Uint8Array()))
194
+ .toThrowError(AbiValidationError);
195
+ });
196
+ });
197
+
198
+ function hexToBytes(hex: string): Uint8Array {
199
+ const normalized = hex.replace(/[^a-fA-F0-9]/g, "");
200
+ if (normalized.length % 2 !== 0) {
201
+ throw new Error("Hex string must contain an even number of characters");
202
+ }
203
+ const bytes = new Uint8Array(normalized.length / 2);
204
+ for (let i = 0; i < bytes.length; i++) {
205
+ bytes[i] = Number.parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
206
+ }
207
+ return bytes;
208
+ }
209
+
210
+ function asPrimitive(value: DecodedValue): DecodedPrimitiveValue {
211
+ if (value.kind !== "primitive") {
212
+ throw new Error(`Expected primitive value, received ${value.kind}`);
213
+ }
214
+ return value;
215
+ }
216
+
217
+ const sizeDiscriminatedUnionAbi = `
218
+ abi:
219
+ package: "test.union"
220
+ abi-version: 1
221
+ types:
222
+ - name: "Payload"
223
+ kind:
224
+ size-discriminated-union:
225
+ packed: true
226
+ variants:
227
+ - name: "Short"
228
+ expected-size: 4
229
+ variant-type:
230
+ struct:
231
+ packed: true
232
+ fields:
233
+ - name: "value"
234
+ field-type:
235
+ primitive: u32
236
+ - name: "Long"
237
+ expected-size: 8
238
+ variant-type:
239
+ struct:
240
+ packed: true
241
+ fields:
242
+ - name: "head"
243
+ field-type:
244
+ primitive: u32
245
+ - name: "tail"
246
+ field-type:
247
+ primitive: u32
248
+ `;
249
+
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export { decodeData } from "./decoder";
2
+ export { parseAbiDocument } from "./abiSchema";
3
+ export { buildTypeRegistry, TypeRegistry } from "./typeRegistry";
4
+ export type {
5
+ DecodedArrayValue,
6
+ DecodedEnumValue,
7
+ DecodedField,
8
+ DecodedPrimitiveValue,
9
+ DecodedSizeDiscriminatedUnionValue,
10
+ DecodedStructValue,
11
+ DecodedUnionValue,
12
+ DecodedValue,
13
+ } from "./decodedValue";
14
+ export { AbiError, AbiDecodeError, AbiParseError, AbiValidationError } from "./errors";
15
+
@@ -0,0 +1,172 @@
1
+ import type {
2
+ AbiDocument,
3
+ ArrayType,
4
+ EnumType,
5
+ SizeDiscriminatedUnionType,
6
+ StructType,
7
+ TypeDefinition,
8
+ TypeKind,
9
+ TypeRefType,
10
+ UnionType,
11
+ } from "./abiSchema";
12
+ import { AbiValidationError } from "./errors";
13
+
14
+ export class TypeRegistry {
15
+ private readonly types = new Map<string, TypeDefinition>();
16
+
17
+ constructor(definitions: Iterable<TypeDefinition>) {
18
+ for (const def of definitions) {
19
+ this.types.set(def.name, def);
20
+ }
21
+ }
22
+
23
+ get(typeName: string): TypeDefinition {
24
+ const definition = this.types.get(typeName);
25
+ if (!definition) {
26
+ throw new AbiValidationError(`Type '${typeName}' is not defined in this ABI file`, { typeName });
27
+ }
28
+ return definition;
29
+ }
30
+
31
+ has(typeName: string): boolean {
32
+ return this.types.has(typeName);
33
+ }
34
+
35
+ entries(): IterableIterator<[string, TypeDefinition]> {
36
+ return this.types.entries();
37
+ }
38
+ }
39
+
40
+ export function buildTypeRegistry(document: AbiDocument): TypeRegistry {
41
+ const registry = new TypeRegistry(document.types);
42
+ validateTypeReferences(registry);
43
+ detectReferenceCycles(registry);
44
+ return registry;
45
+ }
46
+
47
+ function validateTypeReferences(registry: TypeRegistry) {
48
+ for (const [, type] of registry.entries()) {
49
+ validateTypeKindReferences(type.kind, registry, type.name);
50
+ }
51
+ }
52
+
53
+ function validateTypeKindReferences(kind: TypeKind, registry: TypeRegistry, context: string) {
54
+ switch (kind.kind) {
55
+ case "type-ref":
56
+ ensureTypeExists(kind, registry, context);
57
+ break;
58
+ case "struct":
59
+ kind.fields.forEach((field) => validateTypeKindReferences(field.type, registry, `${context}.${field.name}`));
60
+ break;
61
+ case "array":
62
+ validateTypeKindReferences(kind.elementType, registry, `${context}[]`);
63
+ break;
64
+ case "enum":
65
+ kind.variants.forEach((variant) => validateTypeKindReferences(variant.type, registry, `${context}.${variant.name}`));
66
+ break;
67
+ case "union":
68
+ kind.variants.forEach((variant) => validateTypeKindReferences(variant.type, registry, `${context}.${variant.name}`));
69
+ break;
70
+ case "size-discriminated-union":
71
+ kind.variants.forEach((variant) => validateTypeKindReferences(variant.type, registry, `${context}.${variant.name}`));
72
+ break;
73
+ case "primitive":
74
+ break;
75
+ default:
76
+ kind satisfies never;
77
+ }
78
+ }
79
+
80
+ function ensureTypeExists(typeRef: TypeRefType, registry: TypeRegistry, context: string) {
81
+ if (!registry.has(typeRef.name)) {
82
+ throw new AbiValidationError(`Type '${context}' references unknown type '${typeRef.name}'`, {
83
+ typeName: context,
84
+ referencedType: typeRef.name,
85
+ });
86
+ }
87
+ }
88
+
89
+ function detectReferenceCycles(registry: TypeRegistry) {
90
+ const visiting = new Set<string>();
91
+ const visited = new Set<string>();
92
+
93
+ const visit = (typeName: string, stack: string[]) => {
94
+ if (visited.has(typeName)) {
95
+ return;
96
+ }
97
+ if (visiting.has(typeName)) {
98
+ const cyclePath = [...stack, typeName];
99
+ throw new AbiValidationError(`Cyclic type reference detected: ${cyclePath.join(" -> ")}`, { cycle: cyclePath });
100
+ }
101
+
102
+ visiting.add(typeName);
103
+ const type = registry.get(typeName);
104
+ const referencedTypes = collectTypeReferences(type.kind);
105
+ for (const referenced of referencedTypes) {
106
+ if (registry.has(referenced)) {
107
+ visit(referenced, [...stack, typeName]);
108
+ }
109
+ }
110
+ visiting.delete(typeName);
111
+ visited.add(typeName);
112
+ };
113
+
114
+ for (const [typeName] of registry.entries()) {
115
+ if (!visited.has(typeName)) {
116
+ visit(typeName, []);
117
+ }
118
+ }
119
+ }
120
+
121
+ function collectTypeReferences(kind: TypeKind, refs: Set<string> = new Set()): Set<string> {
122
+ switch (kind.kind) {
123
+ case "type-ref":
124
+ refs.add(kind.name);
125
+ break;
126
+ case "struct":
127
+ collectStructRefs(kind, refs);
128
+ break;
129
+ case "array":
130
+ collectTypeReferences(kind.elementType, refs);
131
+ break;
132
+ case "enum":
133
+ collectEnumRefs(kind, refs);
134
+ break;
135
+ case "union":
136
+ collectUnionRefs(kind, refs);
137
+ break;
138
+ case "size-discriminated-union":
139
+ collectSizeUnionRefs(kind, refs);
140
+ break;
141
+ case "primitive":
142
+ break;
143
+ default:
144
+ kind satisfies never;
145
+ }
146
+ return refs;
147
+ }
148
+
149
+ function collectStructRefs(struct: StructType, refs: Set<string>) {
150
+ for (const field of struct.fields) {
151
+ collectTypeReferences(field.type, refs);
152
+ }
153
+ }
154
+
155
+ function collectEnumRefs(enumType: EnumType, refs: Set<string>) {
156
+ for (const variant of enumType.variants) {
157
+ collectTypeReferences(variant.type, refs);
158
+ }
159
+ }
160
+
161
+ function collectUnionRefs(union: UnionType, refs: Set<string>) {
162
+ for (const variant of union.variants) {
163
+ collectTypeReferences(variant.type, refs);
164
+ }
165
+ }
166
+
167
+ function collectSizeUnionRefs(union: SizeDiscriminatedUnionType, refs: Set<string>) {
168
+ for (const variant of union.variants) {
169
+ collectTypeReferences(variant.type, refs);
170
+ }
171
+ }
172
+
@@ -0,0 +1,7 @@
1
+ export function bytesToHex(bytes: Uint8Array): string {
2
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
3
+ }
4
+
5
+ export function alignUp(offset: number, alignment: number): number {
6
+ return (offset + alignment - 1) & ~(alignment - 1);
7
+ }