recon-generate 0.0.17 → 0.0.19
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.
- package/dist/cfg/builder.d.ts +89 -0
- package/dist/cfg/builder.js +1295 -0
- package/dist/cfg/edge-mapper.d.ts +103 -0
- package/dist/cfg/edge-mapper.js +297 -0
- package/dist/cfg/emitter.d.ts +11 -0
- package/dist/cfg/emitter.js +492 -0
- package/dist/cfg/index.d.ts +35 -0
- package/dist/cfg/index.js +77 -0
- package/dist/cfg/sourcemap.d.ts +169 -0
- package/dist/cfg/sourcemap.js +605 -0
- package/dist/cfg/types.d.ts +270 -0
- package/dist/cfg/types.js +41 -0
- package/dist/cfg/unified-coverage.d.ts +87 -0
- package/dist/cfg/unified-coverage.js +248 -0
- package/dist/generator.js +1 -1
- package/dist/index.js +19 -0
- package/dist/info.js +0 -13
- package/dist/sourcemap.d.ts +14 -0
- package/dist/sourcemap.js +317 -0
- package/dist/types.d.ts +5 -0
- package/dist/wakeGenerator.js +2 -1
- package/package.json +2 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
export type SExpr = {
|
|
2
|
+
atom: string;
|
|
3
|
+
} | {
|
|
4
|
+
list: SExpr[];
|
|
5
|
+
} | {
|
|
6
|
+
comment: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const atom: (s: string) => SExpr;
|
|
9
|
+
export declare const list: (...args: SExpr[]) => SExpr;
|
|
10
|
+
export declare const comment: (s: string) => SExpr;
|
|
11
|
+
export type SymExpr = {
|
|
12
|
+
kind: 'var';
|
|
13
|
+
name: string;
|
|
14
|
+
type: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'lit';
|
|
17
|
+
value: string;
|
|
18
|
+
type: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'bool';
|
|
21
|
+
value: boolean;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'binop';
|
|
24
|
+
op: BinOp;
|
|
25
|
+
left: SymExpr;
|
|
26
|
+
right: SymExpr;
|
|
27
|
+
checked: boolean;
|
|
28
|
+
signed: boolean;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'unop';
|
|
31
|
+
op: UnOp;
|
|
32
|
+
arg: SymExpr;
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'select';
|
|
35
|
+
array: SymExpr;
|
|
36
|
+
index: SymExpr;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'ite';
|
|
39
|
+
cond: SymExpr;
|
|
40
|
+
then: SymExpr;
|
|
41
|
+
else: SymExpr;
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'call';
|
|
44
|
+
target: string;
|
|
45
|
+
method: string;
|
|
46
|
+
args: SymExpr[];
|
|
47
|
+
callType: CallType;
|
|
48
|
+
contract?: string;
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'field';
|
|
51
|
+
base: SymExpr;
|
|
52
|
+
field: string;
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'cast';
|
|
55
|
+
toType: string;
|
|
56
|
+
arg: SymExpr;
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'keccak';
|
|
59
|
+
arg: SymExpr;
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'external_return';
|
|
62
|
+
target: string;
|
|
63
|
+
method: string;
|
|
64
|
+
args: SymExpr[];
|
|
65
|
+
ssaId: string;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'loop';
|
|
68
|
+
var: string;
|
|
69
|
+
start: SymExpr;
|
|
70
|
+
bound: SymExpr;
|
|
71
|
+
body: string;
|
|
72
|
+
};
|
|
73
|
+
export type BinOp = 'add' | 'sub' | 'mul' | 'div' | 'mod' | 'exp' | 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge' | 'and' | 'or' | 'xor' | 'shl' | 'shr' | 'land' | 'lor';
|
|
74
|
+
export type UnOp = 'not' | 'neg' | 'bnot';
|
|
75
|
+
export type CallType = 'internal' | 'library' | 'external' | 'constructor';
|
|
76
|
+
export interface BasicBlock {
|
|
77
|
+
id: string;
|
|
78
|
+
statements: CFGStatement[];
|
|
79
|
+
terminator: Terminator;
|
|
80
|
+
astIds: number[];
|
|
81
|
+
}
|
|
82
|
+
export type CFGStatement = {
|
|
83
|
+
kind: 'let';
|
|
84
|
+
name: string;
|
|
85
|
+
type: string;
|
|
86
|
+
expr: SymExpr;
|
|
87
|
+
} | {
|
|
88
|
+
kind: 'store';
|
|
89
|
+
target: SymExpr;
|
|
90
|
+
index: SymExpr;
|
|
91
|
+
value: SymExpr;
|
|
92
|
+
} | {
|
|
93
|
+
kind: 'assign';
|
|
94
|
+
target: string;
|
|
95
|
+
value: SymExpr;
|
|
96
|
+
} | {
|
|
97
|
+
kind: 'call';
|
|
98
|
+
expr: SymExpr;
|
|
99
|
+
} | {
|
|
100
|
+
kind: 'assume';
|
|
101
|
+
cond: SymExpr;
|
|
102
|
+
reason?: string;
|
|
103
|
+
revertEdge?: string;
|
|
104
|
+
};
|
|
105
|
+
export type Terminator = {
|
|
106
|
+
kind: 'branch';
|
|
107
|
+
cond: SymExpr;
|
|
108
|
+
trueBlock: string;
|
|
109
|
+
falseBlock: string;
|
|
110
|
+
} | {
|
|
111
|
+
kind: 'jump';
|
|
112
|
+
target: string;
|
|
113
|
+
} | {
|
|
114
|
+
kind: 'return';
|
|
115
|
+
values: SymExpr[];
|
|
116
|
+
} | {
|
|
117
|
+
kind: 'revert';
|
|
118
|
+
reason?: string;
|
|
119
|
+
} | {
|
|
120
|
+
kind: 'placeholder';
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* A CoverageEdge represents one branch outcome.
|
|
124
|
+
* Covering an edge means executing that branch direction.
|
|
125
|
+
*/
|
|
126
|
+
export interface CoverageEdge {
|
|
127
|
+
id: string;
|
|
128
|
+
from: string;
|
|
129
|
+
to: string;
|
|
130
|
+
condition: SymExpr;
|
|
131
|
+
functionName: string;
|
|
132
|
+
astId?: number;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A CompletePath is a sequence of edges from entry to exit.
|
|
136
|
+
* This is what we enumerate for "n paths total".
|
|
137
|
+
*/
|
|
138
|
+
export interface CompletePath {
|
|
139
|
+
id: string;
|
|
140
|
+
edges: string[];
|
|
141
|
+
entryBlock: string;
|
|
142
|
+
exitBlock: string;
|
|
143
|
+
pathCondition: SymExpr;
|
|
144
|
+
outcome: 'success' | 'revert';
|
|
145
|
+
solvable: boolean;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Snapshot of state at a block entry - for mid-point solving.
|
|
149
|
+
*/
|
|
150
|
+
export interface StateSnapshot {
|
|
151
|
+
atBlock: string;
|
|
152
|
+
variables: Map<string, SymExpr>;
|
|
153
|
+
pathCondition: SymExpr;
|
|
154
|
+
coveredBy: string[];
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Observed return values from external calls (from tracing).
|
|
158
|
+
*/
|
|
159
|
+
export interface ExternalCallOracle {
|
|
160
|
+
target: string;
|
|
161
|
+
method: string;
|
|
162
|
+
argPatterns: string[];
|
|
163
|
+
observedReturns: string[];
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Loop summary for symbolic reasoning
|
|
167
|
+
*/
|
|
168
|
+
export interface LoopInfo {
|
|
169
|
+
id: string;
|
|
170
|
+
headerBlock: string;
|
|
171
|
+
exitBlock: string;
|
|
172
|
+
condition: SymExpr;
|
|
173
|
+
inductionVar?: string;
|
|
174
|
+
bound?: SymExpr;
|
|
175
|
+
bodyBlocks: string[];
|
|
176
|
+
}
|
|
177
|
+
export interface FunctionCFG {
|
|
178
|
+
contract: string;
|
|
179
|
+
name: string;
|
|
180
|
+
selector: string;
|
|
181
|
+
visibility: 'public' | 'external' | 'internal' | 'private';
|
|
182
|
+
params: {
|
|
183
|
+
name: string;
|
|
184
|
+
type: string;
|
|
185
|
+
}[];
|
|
186
|
+
returns: {
|
|
187
|
+
name?: string;
|
|
188
|
+
type: string;
|
|
189
|
+
}[];
|
|
190
|
+
blocks: Map<string, BasicBlock>;
|
|
191
|
+
entryBlock: string;
|
|
192
|
+
edges: CoverageEdge[];
|
|
193
|
+
paths: CompletePath[];
|
|
194
|
+
externalCalls: {
|
|
195
|
+
target: string;
|
|
196
|
+
method: string;
|
|
197
|
+
ssaVar: string;
|
|
198
|
+
}[];
|
|
199
|
+
hasKeccak: boolean;
|
|
200
|
+
internalCalls: {
|
|
201
|
+
target: string;
|
|
202
|
+
contract: string;
|
|
203
|
+
callType: 'internal' | 'super' | 'library' | 'modifier' | 'constructor';
|
|
204
|
+
args: SymExpr[];
|
|
205
|
+
}[];
|
|
206
|
+
modifiers: string[];
|
|
207
|
+
ternaryEdges: CoverageEdge[];
|
|
208
|
+
loops: LoopInfo[];
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Variable role classification for solver
|
|
212
|
+
*/
|
|
213
|
+
export type VarRole = 'input' | 'local' | 'state' | 'global' | 'external';
|
|
214
|
+
/**
|
|
215
|
+
* Variable metadata for solver
|
|
216
|
+
*/
|
|
217
|
+
export interface VarInfo {
|
|
218
|
+
name: string;
|
|
219
|
+
baseName: string;
|
|
220
|
+
role: VarRole;
|
|
221
|
+
type: string;
|
|
222
|
+
slot?: number;
|
|
223
|
+
isMapping?: boolean;
|
|
224
|
+
keyTypes?: string[];
|
|
225
|
+
}
|
|
226
|
+
export interface ContractModule {
|
|
227
|
+
name: string;
|
|
228
|
+
solidityVersion: string;
|
|
229
|
+
stateVars: {
|
|
230
|
+
name: string;
|
|
231
|
+
type: string;
|
|
232
|
+
slot?: number;
|
|
233
|
+
contract?: string;
|
|
234
|
+
}[];
|
|
235
|
+
mappings: {
|
|
236
|
+
name: string;
|
|
237
|
+
keyType: string;
|
|
238
|
+
valueType: string;
|
|
239
|
+
slot?: number;
|
|
240
|
+
contract?: string;
|
|
241
|
+
}[];
|
|
242
|
+
constants?: {
|
|
243
|
+
name: string;
|
|
244
|
+
type: string;
|
|
245
|
+
value?: string;
|
|
246
|
+
contract?: string;
|
|
247
|
+
}[];
|
|
248
|
+
immutables?: {
|
|
249
|
+
name: string;
|
|
250
|
+
type: string;
|
|
251
|
+
contract?: string;
|
|
252
|
+
}[];
|
|
253
|
+
functions: FunctionCFG[];
|
|
254
|
+
totalPaths: number;
|
|
255
|
+
totalEdges: number;
|
|
256
|
+
oracle: ExternalCallOracle[];
|
|
257
|
+
varRoles?: Map<string, VarInfo>;
|
|
258
|
+
}
|
|
259
|
+
export interface SSAContext {
|
|
260
|
+
counter: Map<string, number>;
|
|
261
|
+
types: Map<string, string>;
|
|
262
|
+
blockCounter: number;
|
|
263
|
+
pathCounter: number;
|
|
264
|
+
edgeCounter: number;
|
|
265
|
+
}
|
|
266
|
+
export declare function freshSSA(ctx: SSAContext, base: string, type: string): string;
|
|
267
|
+
export declare function currentSSA(ctx: SSAContext, base: string): string;
|
|
268
|
+
export declare function newBlock(ctx: SSAContext): string;
|
|
269
|
+
export declare function newPath(ctx: SSAContext): string;
|
|
270
|
+
export declare function newEdge(ctx: SSAContext, funcName: string, from: string, dir: 'T' | 'F' | 'J'): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// S-Expression AST (for output)
|
|
4
|
+
// ============================================================================
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.comment = exports.list = exports.atom = void 0;
|
|
7
|
+
exports.freshSSA = freshSSA;
|
|
8
|
+
exports.currentSSA = currentSSA;
|
|
9
|
+
exports.newBlock = newBlock;
|
|
10
|
+
exports.newPath = newPath;
|
|
11
|
+
exports.newEdge = newEdge;
|
|
12
|
+
// Helper constructors
|
|
13
|
+
const atom = (s) => ({ atom: s });
|
|
14
|
+
exports.atom = atom;
|
|
15
|
+
const list = (...args) => ({ list: args });
|
|
16
|
+
exports.list = list;
|
|
17
|
+
const comment = (s) => ({ comment: s });
|
|
18
|
+
exports.comment = comment;
|
|
19
|
+
function freshSSA(ctx, base, type) {
|
|
20
|
+
const count = (ctx.counter.get(base) || 0) + 1;
|
|
21
|
+
ctx.counter.set(base, count);
|
|
22
|
+
const name = `${base}_${count}`;
|
|
23
|
+
ctx.types.set(name, type);
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
function currentSSA(ctx, base) {
|
|
27
|
+
var _a;
|
|
28
|
+
// If variable hasn't been assigned yet, return _0 (initial/undefined state)
|
|
29
|
+
// This ensures the first freshSSA returns _1, which will be different
|
|
30
|
+
const count = (_a = ctx.counter.get(base)) !== null && _a !== void 0 ? _a : 0;
|
|
31
|
+
return `${base}_${count}`;
|
|
32
|
+
}
|
|
33
|
+
function newBlock(ctx) {
|
|
34
|
+
return `B${ctx.blockCounter++}`;
|
|
35
|
+
}
|
|
36
|
+
function newPath(ctx) {
|
|
37
|
+
return `P${ctx.pathCounter++}`;
|
|
38
|
+
}
|
|
39
|
+
function newEdge(ctx, funcName, from, dir) {
|
|
40
|
+
return `${funcName}_${from}_${dir}`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Coverage Map Generator
|
|
3
|
+
*
|
|
4
|
+
* Creates a single coverage-map.json that combines:
|
|
5
|
+
* - branches.json (PC → AST mapping, line numbers)
|
|
6
|
+
* - edge-mappings.json (PC → Edge ID mapping from CFG)
|
|
7
|
+
*
|
|
8
|
+
* This eliminates the need for two separate JSON files and ensures
|
|
9
|
+
* the Rust fuzzer has all coverage information in one place.
|
|
10
|
+
*/
|
|
11
|
+
/** Unified branch info combining branches.json and edge-mappings.json data */
|
|
12
|
+
export interface UnifiedBranch {
|
|
13
|
+
/** PC of the JUMPI instruction */
|
|
14
|
+
pc: number;
|
|
15
|
+
/** AST node ID */
|
|
16
|
+
astId: number | null;
|
|
17
|
+
/** All AST IDs at this source location */
|
|
18
|
+
allAstIds: number[];
|
|
19
|
+
/** Source byte offset */
|
|
20
|
+
offset: number;
|
|
21
|
+
/** Source byte length */
|
|
22
|
+
length: number;
|
|
23
|
+
/** Source file index */
|
|
24
|
+
fileIndex: number;
|
|
25
|
+
/** Line number of the branch condition (1-based) */
|
|
26
|
+
conditionLine?: number;
|
|
27
|
+
/** Edge ID when branch is taken (true) */
|
|
28
|
+
trueEdge?: string;
|
|
29
|
+
/** Edge ID when branch is not taken (false) */
|
|
30
|
+
falseEdge?: string;
|
|
31
|
+
/** Function name containing this branch */
|
|
32
|
+
function?: string;
|
|
33
|
+
/** Block ID in CFG */
|
|
34
|
+
block?: string;
|
|
35
|
+
/** Line number of the first statement in true branch */
|
|
36
|
+
trueBodyLine?: number;
|
|
37
|
+
/** Line number of the first statement in false branch */
|
|
38
|
+
falseBodyLine?: number;
|
|
39
|
+
/** First PC of the true branch body */
|
|
40
|
+
trueBodyPc?: number;
|
|
41
|
+
/** First PC of the false branch body */
|
|
42
|
+
falseBodyPc?: number;
|
|
43
|
+
/** Whether this is an assertion (require/assert) - FALSE branch is the bug-finding path */
|
|
44
|
+
isAssertion?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Per-contract unified coverage data */
|
|
47
|
+
export interface UnifiedContractCoverage {
|
|
48
|
+
/** Contract name */
|
|
49
|
+
name: string;
|
|
50
|
+
/** Source file path */
|
|
51
|
+
sourceFile: string;
|
|
52
|
+
/** Total edges (2 per branch: true + false) */
|
|
53
|
+
totalEdges: number;
|
|
54
|
+
/** All branches with unified info */
|
|
55
|
+
branches: UnifiedBranch[];
|
|
56
|
+
}
|
|
57
|
+
/** Unified coverage manifest */
|
|
58
|
+
export interface UnifiedCoverageManifest {
|
|
59
|
+
/** Generation timestamp */
|
|
60
|
+
generated: string;
|
|
61
|
+
/** Schema version for forward compatibility */
|
|
62
|
+
version: string;
|
|
63
|
+
/** Total contracts */
|
|
64
|
+
totalContracts: number;
|
|
65
|
+
/** Total branch points */
|
|
66
|
+
totalBranches: number;
|
|
67
|
+
/** Total edges (branch directions) */
|
|
68
|
+
totalEdges: number;
|
|
69
|
+
/** Per-contract coverage data */
|
|
70
|
+
contracts: {
|
|
71
|
+
[name: string]: UnifiedContractCoverage;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Generate unified coverage map by merging branches.json and edge-mappings.json
|
|
76
|
+
* Can be called after both files exist, or during the "all" command
|
|
77
|
+
*/
|
|
78
|
+
export declare function generateUnifiedCoverageMap(reconDir: string, branchesData?: any, edgeMappingsData?: any): Promise<UnifiedCoverageManifest>;
|
|
79
|
+
/**
|
|
80
|
+
* Write unified coverage map to file
|
|
81
|
+
*/
|
|
82
|
+
export declare function writeUnifiedCoverageMap(reconDir: string, manifest: UnifiedCoverageManifest): void;
|
|
83
|
+
/**
|
|
84
|
+
* Generate unified coverage map directly from branch and edge data
|
|
85
|
+
* Called during the "all" command to avoid re-reading JSON files
|
|
86
|
+
*/
|
|
87
|
+
export declare function createUnifiedFromMemory(branchManifest: any, edgeMappings: any | null): UnifiedCoverageManifest;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Unified Coverage Map Generator
|
|
4
|
+
*
|
|
5
|
+
* Creates a single coverage-map.json that combines:
|
|
6
|
+
* - branches.json (PC → AST mapping, line numbers)
|
|
7
|
+
* - edge-mappings.json (PC → Edge ID mapping from CFG)
|
|
8
|
+
*
|
|
9
|
+
* This eliminates the need for two separate JSON files and ensures
|
|
10
|
+
* the Rust fuzzer has all coverage information in one place.
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.generateUnifiedCoverageMap = generateUnifiedCoverageMap;
|
|
47
|
+
exports.writeUnifiedCoverageMap = writeUnifiedCoverageMap;
|
|
48
|
+
exports.createUnifiedFromMemory = createUnifiedFromMemory;
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Generation
|
|
53
|
+
// ============================================================================
|
|
54
|
+
/**
|
|
55
|
+
* Generate unified coverage map by merging branches.json and edge-mappings.json
|
|
56
|
+
* Can be called after both files exist, or during the "all" command
|
|
57
|
+
*/
|
|
58
|
+
async function generateUnifiedCoverageMap(reconDir, branchesData, edgeMappingsData) {
|
|
59
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
60
|
+
// Load branches.json if not provided
|
|
61
|
+
if (!branchesData) {
|
|
62
|
+
const branchesPath = path.join(reconDir, 'branches.json');
|
|
63
|
+
if (!fs.existsSync(branchesPath)) {
|
|
64
|
+
throw new Error(`branches.json not found in ${reconDir}`);
|
|
65
|
+
}
|
|
66
|
+
branchesData = JSON.parse(fs.readFileSync(branchesPath, 'utf-8'));
|
|
67
|
+
}
|
|
68
|
+
// Load edge-mappings.json if not provided (optional - might not exist)
|
|
69
|
+
if (!edgeMappingsData) {
|
|
70
|
+
const edgeMappingsPath = path.join(reconDir, 'edge-mappings.json');
|
|
71
|
+
if (fs.existsSync(edgeMappingsPath)) {
|
|
72
|
+
edgeMappingsData = JSON.parse(fs.readFileSync(edgeMappingsPath, 'utf-8'));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Build lookup maps from edge-mappings
|
|
76
|
+
// Primary: PC → edge info
|
|
77
|
+
// Fallback: AST ID → edge info (handles PC mismatches between branches.json and edge-mappings.json)
|
|
78
|
+
const pcToEdgeInfo = new Map(); // key: "contractName:pc"
|
|
79
|
+
const astIdToEdgeInfo = new Map(); // key: "contractName:astId"
|
|
80
|
+
if (edgeMappingsData === null || edgeMappingsData === void 0 ? void 0 : edgeMappingsData.contracts) {
|
|
81
|
+
for (const [contractName, contractData] of Object.entries(edgeMappingsData.contracts)) {
|
|
82
|
+
const branches = contractData.branches || [];
|
|
83
|
+
for (const branch of branches) {
|
|
84
|
+
const pcKey = `${contractName}:${branch.pc}`;
|
|
85
|
+
pcToEdgeInfo.set(pcKey, branch);
|
|
86
|
+
// Also index by AST ID for fallback lookup
|
|
87
|
+
if (branch.astId !== undefined && branch.astId !== null) {
|
|
88
|
+
const astKey = `${contractName}:${branch.astId}`;
|
|
89
|
+
astIdToEdgeInfo.set(astKey, branch);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Merge data
|
|
95
|
+
const unifiedContracts = {};
|
|
96
|
+
let totalBranches = 0;
|
|
97
|
+
let totalEdges = 0;
|
|
98
|
+
for (const contract of branchesData.contracts || []) {
|
|
99
|
+
const contractName = contract.contract;
|
|
100
|
+
const unifiedBranches = [];
|
|
101
|
+
for (const branch of contract.branches || []) {
|
|
102
|
+
// Look up edge info - try PC first, then fallback to AST ID
|
|
103
|
+
const pcKey = `${contractName}:${branch.pc}`;
|
|
104
|
+
let edgeInfo = pcToEdgeInfo.get(pcKey);
|
|
105
|
+
// If PC lookup fails, try AST ID lookup (handles PC mismatches)
|
|
106
|
+
if (!edgeInfo && branch.astId !== undefined && branch.astId !== null) {
|
|
107
|
+
const astKey = `${contractName}:${branch.astId}`;
|
|
108
|
+
edgeInfo = astIdToEdgeInfo.get(astKey);
|
|
109
|
+
}
|
|
110
|
+
// Also try allAstIds if primary astId didn't match
|
|
111
|
+
if (!edgeInfo && ((_a = branch.allAstIds) === null || _a === void 0 ? void 0 : _a.length) > 0) {
|
|
112
|
+
for (const astId of branch.allAstIds) {
|
|
113
|
+
const astKey = `${contractName}:${astId}`;
|
|
114
|
+
edgeInfo = astIdToEdgeInfo.get(astKey);
|
|
115
|
+
if (edgeInfo)
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Determine if this is an assertion branch
|
|
120
|
+
// Assertions have edge IDs containing "assert" - the FALSE branch is the bug path
|
|
121
|
+
const isAssertion = ((_b = edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.trueEdge) === null || _b === void 0 ? void 0 : _b.includes('assert')) ||
|
|
122
|
+
((_c = edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.falseEdge) === null || _c === void 0 ? void 0 : _c.includes('assert')) ||
|
|
123
|
+
false;
|
|
124
|
+
const unified = {
|
|
125
|
+
pc: branch.pc,
|
|
126
|
+
astId: branch.astId,
|
|
127
|
+
allAstIds: branch.allAstIds || [],
|
|
128
|
+
offset: branch.offset,
|
|
129
|
+
length: branch.length,
|
|
130
|
+
fileIndex: branch.fileIndex,
|
|
131
|
+
conditionLine: branch.conditionLine,
|
|
132
|
+
// Edge info (from edge-mappings.json)
|
|
133
|
+
trueEdge: edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.trueEdge,
|
|
134
|
+
falseEdge: edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.falseEdge,
|
|
135
|
+
function: edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.function,
|
|
136
|
+
block: edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.block,
|
|
137
|
+
// Body info (can come from either source)
|
|
138
|
+
trueBodyLine: (_d = branch.trueBodyLine) !== null && _d !== void 0 ? _d : edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.trueBodyLine,
|
|
139
|
+
falseBodyLine: (_e = branch.falseBodyLine) !== null && _e !== void 0 ? _e : edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.falseBodyLine,
|
|
140
|
+
trueBodyPc: (_f = branch.trueBodyPc) !== null && _f !== void 0 ? _f : edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.trueBodyPc,
|
|
141
|
+
falseBodyPc: (_g = branch.falseBodyPc) !== null && _g !== void 0 ? _g : edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.falseBodyPc,
|
|
142
|
+
// Metadata
|
|
143
|
+
isAssertion,
|
|
144
|
+
};
|
|
145
|
+
unifiedBranches.push(unified);
|
|
146
|
+
}
|
|
147
|
+
// Count edges: each branch with edge info has 2 edges, others have 2 potential directions
|
|
148
|
+
const edgesForContract = unifiedBranches.length * 2;
|
|
149
|
+
unifiedContracts[contractName] = {
|
|
150
|
+
name: contractName,
|
|
151
|
+
sourceFile: contract.sourceFile,
|
|
152
|
+
totalEdges: edgesForContract,
|
|
153
|
+
branches: unifiedBranches,
|
|
154
|
+
};
|
|
155
|
+
totalBranches += unifiedBranches.length;
|
|
156
|
+
totalEdges += edgesForContract;
|
|
157
|
+
}
|
|
158
|
+
const manifest = {
|
|
159
|
+
generated: new Date().toISOString(),
|
|
160
|
+
version: '1.0',
|
|
161
|
+
totalContracts: Object.keys(unifiedContracts).length,
|
|
162
|
+
totalBranches,
|
|
163
|
+
totalEdges,
|
|
164
|
+
contracts: unifiedContracts,
|
|
165
|
+
};
|
|
166
|
+
return manifest;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Write unified coverage map to file
|
|
170
|
+
*/
|
|
171
|
+
function writeUnifiedCoverageMap(reconDir, manifest) {
|
|
172
|
+
const outputPath = path.join(reconDir, 'coverage-map.json');
|
|
173
|
+
fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
|
|
174
|
+
console.log(` ✅ Unified coverage map written to ${outputPath}`);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Generate unified coverage map directly from branch and edge data
|
|
178
|
+
* Called during the "all" command to avoid re-reading JSON files
|
|
179
|
+
*/
|
|
180
|
+
function createUnifiedFromMemory(branchManifest, edgeMappings) {
|
|
181
|
+
return generateUnifiedCoverageMapSync(branchManifest, edgeMappings);
|
|
182
|
+
}
|
|
183
|
+
/** Synchronous version for in-memory data */
|
|
184
|
+
function generateUnifiedCoverageMapSync(branchesData, edgeMappingsData) {
|
|
185
|
+
var _a, _b, _c, _d, _e, _f;
|
|
186
|
+
// Build PC → edge info lookup
|
|
187
|
+
const pcToEdgeInfo = new Map();
|
|
188
|
+
if (edgeMappingsData === null || edgeMappingsData === void 0 ? void 0 : edgeMappingsData.contracts) {
|
|
189
|
+
for (const [contractName, contractData] of Object.entries(edgeMappingsData.contracts)) {
|
|
190
|
+
const branches = contractData.branches || [];
|
|
191
|
+
for (const branch of branches) {
|
|
192
|
+
const key = `${contractName}:${branch.pc}`;
|
|
193
|
+
pcToEdgeInfo.set(key, branch);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Merge data
|
|
198
|
+
const unifiedContracts = {};
|
|
199
|
+
let totalBranches = 0;
|
|
200
|
+
let totalEdges = 0;
|
|
201
|
+
for (const contract of branchesData.contracts || []) {
|
|
202
|
+
const contractName = contract.contract;
|
|
203
|
+
const unifiedBranches = [];
|
|
204
|
+
for (const branch of contract.branches || []) {
|
|
205
|
+
const edgeKey = `${contractName}:${branch.pc}`;
|
|
206
|
+
const edgeInfo = pcToEdgeInfo.get(edgeKey);
|
|
207
|
+
const isAssertion = ((_a = edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.trueEdge) === null || _a === void 0 ? void 0 : _a.includes('assert')) ||
|
|
208
|
+
((_b = edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.falseEdge) === null || _b === void 0 ? void 0 : _b.includes('assert')) ||
|
|
209
|
+
false;
|
|
210
|
+
const unified = {
|
|
211
|
+
pc: branch.pc,
|
|
212
|
+
astId: branch.astId,
|
|
213
|
+
allAstIds: branch.allAstIds || [],
|
|
214
|
+
offset: branch.offset,
|
|
215
|
+
length: branch.length,
|
|
216
|
+
fileIndex: branch.fileIndex,
|
|
217
|
+
conditionLine: branch.conditionLine,
|
|
218
|
+
trueEdge: edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.trueEdge,
|
|
219
|
+
falseEdge: edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.falseEdge,
|
|
220
|
+
function: edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.function,
|
|
221
|
+
block: edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.block,
|
|
222
|
+
trueBodyLine: (_c = branch.trueBodyLine) !== null && _c !== void 0 ? _c : edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.trueBodyLine,
|
|
223
|
+
falseBodyLine: (_d = branch.falseBodyLine) !== null && _d !== void 0 ? _d : edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.falseBodyLine,
|
|
224
|
+
trueBodyPc: (_e = branch.trueBodyPc) !== null && _e !== void 0 ? _e : edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.trueBodyPc,
|
|
225
|
+
falseBodyPc: (_f = branch.falseBodyPc) !== null && _f !== void 0 ? _f : edgeInfo === null || edgeInfo === void 0 ? void 0 : edgeInfo.falseBodyPc,
|
|
226
|
+
isAssertion,
|
|
227
|
+
};
|
|
228
|
+
unifiedBranches.push(unified);
|
|
229
|
+
}
|
|
230
|
+
const edgesForContract = unifiedBranches.length * 2;
|
|
231
|
+
unifiedContracts[contractName] = {
|
|
232
|
+
name: contractName,
|
|
233
|
+
sourceFile: contract.sourceFile,
|
|
234
|
+
totalEdges: edgesForContract,
|
|
235
|
+
branches: unifiedBranches,
|
|
236
|
+
};
|
|
237
|
+
totalBranches += unifiedBranches.length;
|
|
238
|
+
totalEdges += edgesForContract;
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
generated: new Date().toISOString(),
|
|
242
|
+
version: '1.0',
|
|
243
|
+
totalContracts: Object.keys(unifiedContracts).length,
|
|
244
|
+
totalBranches,
|
|
245
|
+
totalEdges,
|
|
246
|
+
contracts: unifiedContracts,
|
|
247
|
+
};
|
|
248
|
+
}
|
package/dist/generator.js
CHANGED
|
@@ -298,7 +298,7 @@ class ReconGenerator {
|
|
|
298
298
|
this.logDebug('Skipping artifact from non-source directory', { contractName, sourcePath });
|
|
299
299
|
continue;
|
|
300
300
|
}
|
|
301
|
-
if (isAbstract
|
|
301
|
+
if (isAbstract) {
|
|
302
302
|
this.logDebug('Skipping abstract contract', { contractName, sourcePath });
|
|
303
303
|
continue;
|
|
304
304
|
}
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ const pathsGenerator_1 = require("./pathsGenerator");
|
|
|
44
44
|
const info_1 = require("./info");
|
|
45
45
|
const utils_1 = require("./utils");
|
|
46
46
|
const link_1 = require("./link");
|
|
47
|
+
const sourcemap_1 = require("./sourcemap");
|
|
47
48
|
function parseFilter(input) {
|
|
48
49
|
if (!input)
|
|
49
50
|
return undefined;
|
|
@@ -193,6 +194,24 @@ async function main() {
|
|
|
193
194
|
: path.join(foundryRoot, medusaConfigOpt);
|
|
194
195
|
await (0, link_1.runLink)(foundryRoot, echidnaConfigPath, medusaConfigPath, !!opts.verbose);
|
|
195
196
|
});
|
|
197
|
+
program
|
|
198
|
+
.command('sourcemap')
|
|
199
|
+
.description('Generate CFGs and coverage-map.json for coverage-directed fuzzing')
|
|
200
|
+
.option('-o, --output <path>', 'Custom output directory for coverage artifacts (defaults to .recon)')
|
|
201
|
+
.option('--verbose', 'Enable verbose output for debugging')
|
|
202
|
+
.option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
|
|
203
|
+
.action(async (opts, cmd) => {
|
|
204
|
+
const workspaceRoot = process.cwd();
|
|
205
|
+
const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
|
|
206
|
+
const foundryRoot = path.dirname(foundryConfig);
|
|
207
|
+
const outputDir = opts.output
|
|
208
|
+
? (path.isAbsolute(opts.output) ? opts.output : path.join(foundryRoot, opts.output))
|
|
209
|
+
: undefined;
|
|
210
|
+
await (0, sourcemap_1.runSourcemap)(foundryRoot, {
|
|
211
|
+
outputDir,
|
|
212
|
+
verbose: !!opts.verbose,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
196
215
|
program
|
|
197
216
|
.command('info <contractName>')
|
|
198
217
|
.description('Generate contract info JSON (payable, constants, function relations, etc.)')
|