@eyeauras/insight-cli 0.3.2 → 0.4.1
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/.cli-package-build.json +1 -1
- package/README.md +34 -62
- package/SKILL.md +4 -1
- package/dist/src/arguments.js +9 -4
- package/dist/src/cli.js +45 -37
- package/dist/src/code-references.d.ts +48 -0
- package/dist/src/code-references.js +154 -0
- package/dist/src/context-diagnostics.d.ts +25 -0
- package/dist/src/context-diagnostics.js +82 -0
- package/dist/src/contexts.d.ts +13 -1
- package/dist/src/contexts.js +38 -9
- package/dist/src/function-boundaries.d.ts +9 -0
- package/dist/src/function-boundaries.js +68 -0
- package/dist/src/function-entries.d.ts +33 -0
- package/dist/src/function-entries.js +84 -0
- package/dist/src/function-inspection.d.ts +4 -2
- package/dist/src/function-inspection.js +94 -59
- package/dist/src/function-results.d.ts +3 -1
- package/dist/src/function-results.js +9 -3
- package/dist/src/inline-script.d.ts +15 -0
- package/dist/src/inline-script.js +62 -0
- package/dist/src/internal-timing.d.ts +3 -0
- package/dist/src/internal-timing.js +52 -0
- package/dist/src/lifecycle.js +14 -4
- package/dist/src/native-inputs.d.ts +1 -1
- package/dist/src/native-inputs.js +1 -1
- package/dist/src/native-queries.js +1 -1
- package/dist/src/persistent-client.d.ts +30 -0
- package/dist/src/persistent-client.js +125 -0
- package/dist/src/queries.js +4 -1
- package/dist/src/query-policy.js +9 -1
- package/dist/src/research-view.d.ts +6 -0
- package/dist/src/research-view.js +9 -0
- package/dist/src/research.d.ts +38 -0
- package/dist/src/research.js +78 -8
- package/dist/src/string-anchors.d.ts +43 -0
- package/dist/src/string-anchors.js +176 -0
- package/dist/src/sync-policy.js +3 -0
- package/dist/src/timed-backend.d.ts +3 -0
- package/dist/src/timed-backend.js +16 -0
- package/dist/src/transport.d.ts +1 -0
- package/dist/src/transport.js +4 -0
- package/dist/src/workspace.js +5 -2
- package/package.json +12 -1
- package/references/native/InsightAnchorScan.java +135 -0
- package/references/native/InsightCodeReferences.java +129 -0
- package/references/native/InsightFunctionBoundaries.java +56 -0
- package/references/native/InsightInlineExecutor.java +143 -0
- package/references/native/InsightInlineScript.java +14 -0
- package/references/native/InsightPeRanges.java +92 -0
- package/references/native/InsightResearchQueries.java +22 -1
- package/references/native/InsightResearchView.java +69 -0
- package/references/native/InsightStringAnchors.java +49 -0
- package/references/persistent-client.mjs +125 -0
- package/references/persistent.md +50 -0
- package/references/query-semantics.md +12 -0
- package/references/recovery.md +36 -0
- package/references/research.md +87 -0
- package/references/setup.md +1 -1
- package/references/sparse-navigation.md +173 -0
- package/runtime/GhidraMCP-7.0.0.jar +0 -0
- package/runtime/GhidraMCP-NOTICE +5 -0
- package/runtime/provenance.json +17 -3
- package/runtime/research-view.provenance.json +28 -0
- package/runtime/toolbox.json +10 -3
- package/skill-bundle/insight-artifacts/SKILL.md +1 -1
- package/skill-bundle/insight-artifacts/references/recovery.md +36 -0
- package/skill-bundle/insight-binary/SKILL.md +1 -1
- package/skill-bundle/insight-capture/SKILL.md +1 -1
- package/skill-bundle/insight-cli/SKILL.md +4 -1
- package/skill-bundle/insight-cli/references/persistent-client.mjs +125 -0
- package/skill-bundle/insight-cli/references/persistent.md +50 -0
- package/skill-bundle/insight-cli/references/recovery.md +36 -0
- package/skill-bundle/insight-cli/references/setup.md +1 -1
- package/skill-bundle/insight-die/SKILL.md +1 -1
- package/skill-bundle/insight-dnspy/SKILL.md +1 -1
- package/skill-bundle/insight-dotnet-dump/SKILL.md +1 -1
- package/skill-bundle/insight-dotnet-stack/SKILL.md +1 -1
- package/skill-bundle/insight-dotnet-trace/SKILL.md +1 -1
- package/skill-bundle/insight-frida/SKILL.md +1 -1
- package/skill-bundle/insight-ghidra/SKILL.md +14 -1
- package/skill-bundle/insight-ghidra/references/persistent-client.mjs +125 -0
- package/skill-bundle/insight-ghidra/references/persistent.md +50 -0
- package/skill-bundle/insight-ghidra/references/query-semantics.md +12 -0
- package/skill-bundle/insight-ghidra/references/recovery.md +36 -0
- package/skill-bundle/insight-ghidra/references/research.md +87 -0
- package/skill-bundle/insight-ghidra/references/setup.md +1 -1
- package/skill-bundle/insight-ghidra/references/sparse-navigation.md +173 -0
- package/skill-bundle/insight-ilspy/SKILL.md +1 -1
- package/skill-bundle/insight-interop/SKILL.md +1 -1
- package/skill-bundle/insight-lldb/SKILL.md +1 -1
- package/skill-bundle/insight-llvm/SKILL.md +1 -1
- package/skill-bundle/insight-managed-heap/SKILL.md +1 -1
- package/skill-bundle/insight-managed-metadata/SKILL.md +1 -1
- package/skill-bundle/insight-mitmproxy/SKILL.md +1 -1
- package/skill-bundle/insight-overview/SKILL.md +1 -1
- package/skill-bundle/insight-procmon/SKILL.md +1 -1
- package/skill-bundle/insight-re/SKILL.md +4 -1
- package/skill-bundle/insight-re/references/persistent-client.mjs +125 -0
- package/skill-bundle/insight-re/references/persistent.md +50 -0
- package/skill-bundle/insight-re/references/recovery.md +36 -0
- package/skill-bundle/insight-sevenzip/SKILL.md +1 -1
- package/skill-bundle/insight-strace/SKILL.md +1 -1
- package/skill-bundle/insight-tshark/SKILL.md +1 -1
- package/skill-bundle/insight-windbg/SKILL.md +1 -1
- package/skill-bundle/manifest.json +1 -1
- package/skills/insight-artifacts/SKILL.md +1 -1
- package/skills/insight-artifacts/references/recovery.md +36 -0
- package/skills/insight-binary/SKILL.md +1 -1
- package/skills/insight-capture/SKILL.md +1 -1
- package/skills/insight-die/SKILL.md +1 -1
- package/skills/insight-dnspy/SKILL.md +1 -1
- package/skills/insight-dotnet-dump/SKILL.md +1 -1
- package/skills/insight-dotnet-stack/SKILL.md +1 -1
- package/skills/insight-dotnet-trace/SKILL.md +1 -1
- package/skills/insight-frida/SKILL.md +1 -1
- package/skills/insight-ghidra/SKILL.md +14 -1
- package/skills/insight-ghidra/references/persistent-client.mjs +125 -0
- package/skills/insight-ghidra/references/persistent.md +50 -0
- package/skills/insight-ghidra/references/query-semantics.md +12 -0
- package/skills/insight-ghidra/references/recovery.md +36 -0
- package/skills/insight-ghidra/references/research.md +87 -0
- package/skills/insight-ghidra/references/setup.md +1 -1
- package/skills/insight-ghidra/references/sparse-navigation.md +173 -0
- package/skills/insight-ilspy/SKILL.md +1 -1
- package/skills/insight-interop/SKILL.md +1 -1
- package/skills/insight-lldb/SKILL.md +1 -1
- package/skills/insight-llvm/SKILL.md +1 -1
- package/skills/insight-managed-heap/SKILL.md +1 -1
- package/skills/insight-managed-metadata/SKILL.md +1 -1
- package/skills/insight-mitmproxy/SKILL.md +1 -1
- package/skills/insight-overview/SKILL.md +1 -1
- package/skills/insight-procmon/SKILL.md +1 -1
- package/skills/insight-re/SKILL.md +4 -1
- package/skills/insight-re/references/persistent-client.mjs +125 -0
- package/skills/insight-re/references/persistent.md +50 -0
- package/skills/insight-re/references/recovery.md +36 -0
- package/skills/insight-sevenzip/SKILL.md +1 -1
- package/skills/insight-strace/SKILL.md +1 -1
- package/skills/insight-tshark/SKILL.md +1 -1
- package/skills/insight-windbg/SKILL.md +1 -1
package/dist/src/research.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { textWindow } from './text-window.js';
|
|
2
2
|
import { functionResult, functionFailure } from './function-results.js';
|
|
3
3
|
import { inspectFunctions } from './function-inspection.js';
|
|
4
|
+
import { entryRequest } from './function-entries.js';
|
|
5
|
+
import { inspectAnchors } from './string-anchors.js';
|
|
4
6
|
import { fail, id, object, text, digest } from './model.js';
|
|
5
7
|
import { fieldKey, scalarMerge, scalarRefresh } from './merge.js';
|
|
6
8
|
import { verifyMutation } from './mutation-effects.js';
|
|
7
9
|
import { transferResult } from './cross-results.js';
|
|
8
10
|
import { CliError } from '@eyeauras/cli-factory';
|
|
11
|
+
import { timed } from './internal-timing.js';
|
|
12
|
+
import { navigationResult } from './context-diagnostics.js';
|
|
13
|
+
import { inspectCodeReferences } from './code-references.js';
|
|
9
14
|
export function address(value) {
|
|
10
15
|
const input = text(value, 128);
|
|
11
16
|
const match = /^(?:([A-Za-z0-9_]+):)?(?:0x)?([a-fA-F0-9]+)$/.exec(input);
|
|
@@ -66,7 +71,7 @@ export class Research {
|
|
|
66
71
|
}
|
|
67
72
|
async ordered(selected, action) {
|
|
68
73
|
const key = this.workspace.context(selected).id, previous = this.tasks.get(key);
|
|
69
|
-
const task = (
|
|
74
|
+
const task = timed('facade.queue', 'research', async () => { await previous?.catch(() => { }); }).then(action);
|
|
70
75
|
this.tasks.set(key, task);
|
|
71
76
|
try {
|
|
72
77
|
return await task;
|
|
@@ -77,7 +82,7 @@ export class Research {
|
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
async boundary(selected, action) {
|
|
80
|
-
const key = 'command:' + this.workspace.context(selected).id, previous = this.tasks.get(key), task = (
|
|
85
|
+
const key = 'command:' + this.workspace.context(selected).id, previous = this.tasks.get(key), task = timed('facade.queue', 'command', async () => { await previous?.catch(() => { }); }).then(action);
|
|
81
86
|
this.tasks.set(key, task);
|
|
82
87
|
try {
|
|
83
88
|
return await task;
|
|
@@ -147,10 +152,14 @@ export class Research {
|
|
|
147
152
|
return opened(request.result);
|
|
148
153
|
if (reserved.replay && request.state !== 'accepted')
|
|
149
154
|
throw new CliError('This open retains an unproved outcome; inspect the retained context before continuing.', { code: 'coordinator.unknown', result: retained(request.state), next });
|
|
150
|
-
|
|
155
|
+
const unresolved = this.workspace.unresolved(reserved.context).filter(other => other.id !== request.id);
|
|
156
|
+
if (unresolved.length) {
|
|
151
157
|
const result = { session: reserved.context.session, target, state: 'blocked' };
|
|
152
158
|
this.workspace.recordRequest(request.id, 'failed', result);
|
|
153
|
-
|
|
159
|
+
const close = unresolved.find(other => other.operation === 'ghidra.close');
|
|
160
|
+
throw new CliError('This context retains an unproved effect; continuation did not start a second operation.', {
|
|
161
|
+
code: 'coordinator.blocked', result, next: close ? [['ghidra', 'close', '--session', reserved.context.session, '--target', target, '--request-id', close.callerId ?? close.id]] : next
|
|
162
|
+
});
|
|
154
163
|
}
|
|
155
164
|
this.workspace.startRequest(request.id);
|
|
156
165
|
try {
|
|
@@ -292,13 +301,48 @@ export class Research {
|
|
|
292
301
|
});
|
|
293
302
|
}
|
|
294
303
|
async inspectFunctions(selected, parameters, options) {
|
|
304
|
+
const lookup = entryRequest(parameters, options);
|
|
295
305
|
return this.ordered(selected, async () => {
|
|
296
306
|
const backend = await this.backend();
|
|
297
307
|
await backend.open(selected);
|
|
298
308
|
const context = this.workspace.context(selected);
|
|
299
|
-
const
|
|
309
|
+
const observe = backend.inspectObservation ? () => backend.inspectObservation(selected) : backend.observation ? () => backend.observation(selected) : undefined;
|
|
310
|
+
if (lookup && !observe)
|
|
311
|
+
fail('unsupported', 'Address resolution requires guarded native observations.');
|
|
312
|
+
const observation = await observe?.();
|
|
300
313
|
const view = { revision: revision(context.baseRevision), identity: digest({ target: selected.target, context: context.id, generation: context.generation, snapshot: context.workingSnapshot, epoch: context.viewEpoch ?? 0, observation }) };
|
|
301
|
-
return inspectFunctions(backend, selected, parameters, options, view, observation);
|
|
314
|
+
return inspectFunctions(backend, selected, parameters, options, view, observation, observe);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
async stringAnchors(selected, request) {
|
|
318
|
+
return this.ordered(selected, async () => {
|
|
319
|
+
const backend = await this.backend();
|
|
320
|
+
await backend.open(selected);
|
|
321
|
+
const observe = backend.inspectObservation ? () => backend.inspectObservation(selected) : backend.observation ? () => backend.observation(selected) : undefined;
|
|
322
|
+
if (!observe)
|
|
323
|
+
fail('unsupported', 'String anchors require guarded native observations.');
|
|
324
|
+
const context = this.workspace.context(selected), observation = await observe();
|
|
325
|
+
const view = digest({ target: selected.target, context: context.id, generation: context.generation, snapshot: context.workingSnapshot, epoch: context.viewEpoch ?? 0, observation });
|
|
326
|
+
return inspectAnchors(backend, selected, request, view, observation, observe);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
async codeReferences(selected, request) {
|
|
330
|
+
return this.ordered(selected, async () => {
|
|
331
|
+
const backend = await this.backend();
|
|
332
|
+
await backend.open(selected);
|
|
333
|
+
if (!backend.observation)
|
|
334
|
+
fail('unsupported', 'Code-reference continuation requires native view observation.');
|
|
335
|
+
const context = this.workspace.context(selected), observation = await backend.observation(selected);
|
|
336
|
+
const view = digest({ target: selected.target, context: context.id, generation: context.generation, snapshot: context.workingSnapshot, epoch: context.viewEpoch ?? 0, observation });
|
|
337
|
+
return inspectCodeReferences(backend, selected, request, view, observation);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
async navigation(selected, maximum) {
|
|
341
|
+
return this.ordered(selected, async () => {
|
|
342
|
+
const backend = await this.backend();
|
|
343
|
+
await backend.open(selected);
|
|
344
|
+
const value = navigationResult(await backend.call(selected, '/navigation_diagnostic', { max_scan: maximum }), maximum);
|
|
345
|
+
return { ...selected, value: { ...value, initialAnalysis: this.workspace.model(selected.target).settings.autoAnalyze ? 'requested' : 'none', priorAnalysis: 'unknown', provenance: 'Initial facade policy only; imported/saved metadata may have an unknown analysis history.' } };
|
|
302
346
|
});
|
|
303
347
|
}
|
|
304
348
|
async xrefs(selected, at, limit, offset) {
|
|
@@ -428,6 +472,9 @@ export class Research {
|
|
|
428
472
|
this.tasks.delete(key);
|
|
429
473
|
}
|
|
430
474
|
}
|
|
475
|
+
async reconcileRequests(selected, backend) {
|
|
476
|
+
return this.sharedOrder(selected.target, () => this.reconcile(selected, backend));
|
|
477
|
+
}
|
|
431
478
|
async reconcile(selected, backend) {
|
|
432
479
|
const active = this.workspace.activePublication(selected.target);
|
|
433
480
|
if (active) {
|
|
@@ -541,7 +588,7 @@ export class Research {
|
|
|
541
588
|
if (!await backend.closed(selected))
|
|
542
589
|
continue;
|
|
543
590
|
const current = this.workspace.context(selected);
|
|
544
|
-
this.workspace.completeSavedRequest(selected, request.id, current.generation, {}, { ...selected, state: 'closed', pendingChanges: count(current) });
|
|
591
|
+
this.workspace.completeSavedRequest(selected, request.id, current.generation, { state: 'stopped' }, { ...selected, state: 'closed', pendingChanges: count(current) });
|
|
545
592
|
}
|
|
546
593
|
catch { /* A stopped metadata flag alone never proves process release. */ }
|
|
547
594
|
continue;
|
|
@@ -794,6 +841,10 @@ export class Research {
|
|
|
794
841
|
if (reserved.replay && request.state !== 'accepted') {
|
|
795
842
|
if (request.state === 'applied')
|
|
796
843
|
return request.result;
|
|
844
|
+
if (operation === '/run_script_inline' && request.state === 'failed' && object(request.result ?? {}).reason === 'script_not_executed') {
|
|
845
|
+
const result = object(request.result), execution = object(result.execution);
|
|
846
|
+
fail('script.' + String(execution.stage), String(object(result.diagnostic).message), result);
|
|
847
|
+
}
|
|
797
848
|
if (operation.startsWith('/sdk/') && request.state === 'failed' && object(request.result ?? {}).state === 'rejected')
|
|
798
849
|
fail('rejected', 'This SDK transaction was rolled back and is not repeated.', request.result);
|
|
799
850
|
fail('unknown', 'This saved-state operation retains an unresolved outcome; it is not re-executed.', request.result);
|
|
@@ -807,7 +858,24 @@ export class Research {
|
|
|
807
858
|
savedVerification: !!verifySaved });
|
|
808
859
|
let savedSnapshot, phase = 'operation';
|
|
809
860
|
try {
|
|
810
|
-
|
|
861
|
+
let raw;
|
|
862
|
+
if (operation === '/run_script_inline' && backend.inlineScript) {
|
|
863
|
+
const attempt = await backend.inlineScript(selected, parameters, request.id);
|
|
864
|
+
raw = attempt.response;
|
|
865
|
+
if (attempt.response.success === false) {
|
|
866
|
+
const result = { ...selected, requestId: request.callerId ?? request.id, state: attempt.safe ? 'rejected' : 'unknown',
|
|
867
|
+
contextState: attempt.safe ? 'ready' : 'needs_recovery', reason: attempt.safe ? 'script_not_executed' : 'script_execution_not_proven_complete',
|
|
868
|
+
execution: attempt.response.execution, diagnostic: attempt.response.diagnostic, diagnostics: attempt.response.diagnostics,
|
|
869
|
+
diagnosticsTruncated: attempt.response.diagnosticsTruncated, compilerOutput: attempt.response.compilerOutput,
|
|
870
|
+
output: attempt.response.console_output, outputTruncated: attempt.response.outputTruncated };
|
|
871
|
+
if (attempt.safe)
|
|
872
|
+
this.workspace.recordRequest(request.id, 'failed', result);
|
|
873
|
+
fail('script.' + String(object(attempt.response.execution).stage), String(object(attempt.response.diagnostic).message), result);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
else
|
|
877
|
+
raw = await backend.call(selected, operation, parameters);
|
|
878
|
+
const response = raw && typeof raw === 'object' && !Array.isArray(raw) ? object(raw) : { output: raw };
|
|
811
879
|
if (['rejected', 'partial', 'failed', 'error'].includes(String(response.status)) || response.success === false)
|
|
812
880
|
fail('partial', 'Program operation reported a rejected or incomplete effect.');
|
|
813
881
|
if (!verifySaved && response.success !== true && response.status !== 'success')
|
|
@@ -825,6 +893,8 @@ export class Research {
|
|
|
825
893
|
return result;
|
|
826
894
|
}
|
|
827
895
|
catch (error) {
|
|
896
|
+
if (operation === '/run_script_inline' && this.workspace.request(request.id).state === 'failed' && object(this.workspace.request(request.id).result ?? {}).reason === 'script_not_executed')
|
|
897
|
+
throw error;
|
|
828
898
|
if (operation.startsWith('/sdk/') && String(error.code).endsWith('.rejected')) {
|
|
829
899
|
this.workspace.recordRequest(request.id, 'failed', { ...selected, state: 'rejected', reason: 'native_transaction_rolled_back' });
|
|
830
900
|
throw error;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type CommandContext } from '@eyeauras/cli-factory';
|
|
2
|
+
import { type Research, type ResearchBackend } from './research.js';
|
|
3
|
+
import type { Scope } from './workspace.js';
|
|
4
|
+
export declare const anchorAlgorithm = "raw-string-anchors-v1";
|
|
5
|
+
export declare const anchorEncodings: readonly ["ascii", "utf16le"];
|
|
6
|
+
type Encoding = typeof anchorEncodings[number];
|
|
7
|
+
interface Frontier {
|
|
8
|
+
address: string;
|
|
9
|
+
encoding: Encoding;
|
|
10
|
+
}
|
|
11
|
+
interface Query {
|
|
12
|
+
text: string;
|
|
13
|
+
encodings: Encoding[];
|
|
14
|
+
start: string;
|
|
15
|
+
end: string;
|
|
16
|
+
cap: number;
|
|
17
|
+
}
|
|
18
|
+
interface Budgets {
|
|
19
|
+
max_bytes: number;
|
|
20
|
+
max_candidates: number;
|
|
21
|
+
timeout_seconds: number;
|
|
22
|
+
limit: number;
|
|
23
|
+
}
|
|
24
|
+
export interface AnchorRequest {
|
|
25
|
+
query: Query;
|
|
26
|
+
from: Frontier;
|
|
27
|
+
budgets: Budgets;
|
|
28
|
+
expectedView?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function anchorRequest(o: Record<string, unknown>, who: Scope): AnchorRequest;
|
|
31
|
+
export declare function anchorResult(raw: unknown, r: AnchorRequest, modification: string): Record<string, unknown>;
|
|
32
|
+
export declare function inspectAnchors(backend: ResearchBackend, who: Scope, r: AnchorRequest, view: string, observation: unknown, observe: () => Promise<unknown>): Promise<{
|
|
33
|
+
value: {
|
|
34
|
+
view: {
|
|
35
|
+
identity: string;
|
|
36
|
+
};
|
|
37
|
+
cursor: string | null;
|
|
38
|
+
};
|
|
39
|
+
session: string;
|
|
40
|
+
target: string;
|
|
41
|
+
}>;
|
|
42
|
+
export declare function stringAnchorCommands(research: (c: CommandContext) => Research): import("@eyeauras/cli-factory").CommandDefinition[];
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { command, Permission } from '@eyeauras/cli-factory';
|
|
2
|
+
import { scope, number, address } from './research.js';
|
|
3
|
+
import { selectedOptions } from './arguments.js';
|
|
4
|
+
import { fail, object, digest, stable } from './model.js';
|
|
5
|
+
import { parseDomainJson } from './json.js';
|
|
6
|
+
export const anchorAlgorithm = 'raw-string-anchors-v1';
|
|
7
|
+
export const anchorEncodings = ['ascii', 'utf16le'];
|
|
8
|
+
const defaults = { max_bytes: 4194304, max_candidates: 10000, timeout_seconds: 5, limit: 100 };
|
|
9
|
+
const big = (s) => BigInt('0x' + s);
|
|
10
|
+
const hex = (v) => { const s = address(v); if (!/^[a-f0-9]{1,16}$/.test(s))
|
|
11
|
+
fail('invalid', 'String anchors require default-space unsigned addresses.'); return s; };
|
|
12
|
+
const bytes = (s, e) => Buffer.from(s, e === 'ascii' ? 'ascii' : 'utf16le');
|
|
13
|
+
function wellFormed(s) { for (let i = 0; i < s.length; i++) {
|
|
14
|
+
const c = s.charCodeAt(i);
|
|
15
|
+
if (c >= 0xd800 && c <= 0xdbff) {
|
|
16
|
+
const next = s.charCodeAt(++i);
|
|
17
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
else if (c >= 0xdc00 && c <= 0xdfff)
|
|
21
|
+
return false;
|
|
22
|
+
} return true; }
|
|
23
|
+
function query(raw) {
|
|
24
|
+
const q = object(raw), text = q.text;
|
|
25
|
+
if (typeof text !== 'string' || !text.length || text.length > 256 || !wellFormed(text) || text.includes('\0'))
|
|
26
|
+
fail('invalid', 'Supply a literal of 1–256 UTF-16 units, without NUL or unpaired surrogates.');
|
|
27
|
+
if (!Array.isArray(q.encodings) || q.encodings.length < 1 || q.encodings.length > 2)
|
|
28
|
+
fail('invalid', 'Select ascii, utf16le, or both.');
|
|
29
|
+
const encodings = anchorEncodings.filter(e => q.encodings.includes(e));
|
|
30
|
+
if (encodings.length !== q.encodings.length)
|
|
31
|
+
fail('invalid', 'Encodings must be distinct supported values.');
|
|
32
|
+
if (encodings.includes('ascii') && /[^\x00-\x7f]/.test(text))
|
|
33
|
+
fail('invalid', 'Text is not representable in ASCII; select utf16le only.');
|
|
34
|
+
const cap = number(q.cap, 8192, 16);
|
|
35
|
+
if (cap % 2 || encodings.some(e => bytes(text, e).length > cap))
|
|
36
|
+
fail('invalid', '--max-string-bytes must be even and fit every encoded needle.');
|
|
37
|
+
const start = hex(q.start), end = hex(q.end);
|
|
38
|
+
if (big(start) > big(end))
|
|
39
|
+
fail('invalid', 'scan-through must not precede scan-from.');
|
|
40
|
+
return { text, encodings, start, end, cap };
|
|
41
|
+
}
|
|
42
|
+
function budgets(o, base = defaults) { return { max_bytes: number(o.maxBytes ?? base.max_bytes, 16777216, 32768), max_candidates: number(o.maxCandidates ?? base.max_candidates, 100000, 1), timeout_seconds: number(o.timeoutSeconds ?? base.timeout_seconds, 30, 1), limit: number(o.limit ?? base.limit, 1000, 1) }; }
|
|
43
|
+
function frontier(raw, q) {
|
|
44
|
+
const f = object(raw), at = hex(f.address);
|
|
45
|
+
if (big(at) < big(q.start) || big(at) > big(q.end) || !q.encodings.includes(f.encoding))
|
|
46
|
+
fail('invalid', 'String-anchor frontier is outside the query.');
|
|
47
|
+
return { address: at, encoding: f.encoding };
|
|
48
|
+
}
|
|
49
|
+
function ordinal(f, q) { return big(f.address) * BigInt(q.encodings.length) + BigInt(q.encodings.indexOf(f.encoding)); }
|
|
50
|
+
export function anchorRequest(o, who) {
|
|
51
|
+
if (o.cursor !== undefined) {
|
|
52
|
+
if (['text', 'encodings', 'scanFrom', 'scanThrough', 'maxStringBytes'].some(k => o[k] !== undefined))
|
|
53
|
+
fail('invalid', 'Cursor excludes text, encodings, range and context-cap selectors.');
|
|
54
|
+
if (typeof o.cursor !== 'string' || o.cursor.length > 16384 || !/^sa1\.[A-Za-z0-9_-]+$/.test(o.cursor))
|
|
55
|
+
fail('invalid', 'Malformed string-anchor cursor.');
|
|
56
|
+
let c;
|
|
57
|
+
try {
|
|
58
|
+
const encoded = o.cursor.slice(4), b = Buffer.from(encoded, 'base64url');
|
|
59
|
+
if (b.toString('base64url') !== encoded)
|
|
60
|
+
throw Error();
|
|
61
|
+
c = object(parseDomainJson(b.toString('utf8')));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
fail('invalid', 'Malformed string-anchor cursor.');
|
|
65
|
+
}
|
|
66
|
+
if (c.version !== 1 || c.algorithm !== anchorAlgorithm || stable(c.scope) !== stable(who) || typeof c.view !== 'string' || !/^[a-f0-9]{64}$/.test(c.view))
|
|
67
|
+
fail('invalid', 'Cursor scope, algorithm or view is incompatible.');
|
|
68
|
+
const q = query(c.query), b = object(c.budgets);
|
|
69
|
+
if (['max_bytes', 'max_candidates', 'timeout_seconds', 'limit'].some(k => !Number.isSafeInteger(b[k])))
|
|
70
|
+
fail('invalid', 'Malformed cursor budgets.');
|
|
71
|
+
return { query: q, from: frontier(c.next, q), expectedView: c.view, budgets: budgets(o, budgets({ maxBytes: b.max_bytes, maxCandidates: b.max_candidates, timeoutSeconds: b.timeout_seconds, limit: b.limit })) };
|
|
72
|
+
}
|
|
73
|
+
if (typeof o.text !== 'string' || typeof o.encodings !== 'string' || typeof o.scanFrom !== 'string' || typeof o.scanThrough !== 'string')
|
|
74
|
+
fail('invalid', 'Supply --text, --encodings, --scan-from and --scan-through, or --cursor.');
|
|
75
|
+
const q = query({ text: o.text, encodings: o.encodings.split(',').map(x => x.trim()), start: o.scanFrom, end: o.scanThrough, cap: o.maxStringBytes ?? 1024 });
|
|
76
|
+
return { query: q, from: { address: q.start, encoding: q.encodings[0] }, budgets: budgets(o) };
|
|
77
|
+
}
|
|
78
|
+
function decoded(raw, e) {
|
|
79
|
+
if (e === 'ascii')
|
|
80
|
+
return raw.some(x => x > 127) ? null : raw.toString('ascii');
|
|
81
|
+
try {
|
|
82
|
+
return new TextDecoder('utf-16le', { fatal: true, ignoreBOM: true }).decode(raw);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function anchorResult(raw, r, modification) {
|
|
89
|
+
const v = object(raw), s = object(v.scan), q = r.query, b = r.budgets;
|
|
90
|
+
if (v.algorithm !== anchorAlgorithm || stable(v.encodings) !== stable(q.encodings) || v.modification !== modification) {
|
|
91
|
+
if (typeof v.modification === 'string' && v.modification !== modification)
|
|
92
|
+
fail('binding', 'Native view changed during string-anchor scan.', { reason: 'changed_view' });
|
|
93
|
+
fail('partial', 'Malformed string-anchor protocol facts.');
|
|
94
|
+
}
|
|
95
|
+
if (!Array.isArray(v.matches) || v.matches.length > b.limit || stable(s.from) !== stable(r.from) || s.through !== q.end || typeof s.complete !== 'boolean' || s.complete !== (s.reason === 'exhausted') || !['exhausted', 'byte_limit', 'candidate_limit', 'timeout', 'result_limit'].includes(String(s.reason)))
|
|
96
|
+
fail('partial', 'Malformed string-anchor coverage.');
|
|
97
|
+
for (const k of ['bytesRead', 'testedPairs', 'candidates'])
|
|
98
|
+
if (!Number.isSafeInteger(s[k]) || Number(s[k]) < 0)
|
|
99
|
+
fail('partial', 'Malformed string-anchor counters.');
|
|
100
|
+
if (Number(s.bytesRead) > b.max_bytes || Number(s.testedPairs) > Number(s.bytesRead) * q.encodings.length || Number(s.candidates) > b.max_candidates || Number(s.candidates) < v.matches.length || Number(s.candidates) > Number(s.testedPairs) + 1 || !Number.isFinite(s.elapsedMs) || Number(s.elapsedMs) < 0)
|
|
101
|
+
fail('partial', 'Inconsistent string-anchor budgets.');
|
|
102
|
+
const next = s.complete ? null : frontier(s.next, q);
|
|
103
|
+
if (s.complete && s.next !== null || next && ordinal(next, q) < ordinal(r.from, q) || next && BigInt(Number(s.testedPairs)) > ordinal(next, q) - ordinal(r.from, q))
|
|
104
|
+
fail('partial', 'String-anchor frontier changed.');
|
|
105
|
+
if (next && ordinal(next, q) === ordinal(r.from, q) && s.reason !== 'timeout')
|
|
106
|
+
fail('partial', 'String-anchor scan made no progress despite its minimum work budget.');
|
|
107
|
+
let previous = ordinal(r.from, q) - 1n;
|
|
108
|
+
for (const item of v.matches) {
|
|
109
|
+
const row = object(item), at = hex(row.matchAddress), encoding = row.encoding;
|
|
110
|
+
if (!q.encodings.includes(encoding))
|
|
111
|
+
fail('partial', 'Unknown string-anchor encoding.');
|
|
112
|
+
const pair = { address: at, encoding }, order = ordinal(pair, q), needle = bytes(q.text, encoding), unit = encoding === 'ascii' ? 1 : 2, l = object(row.literalCandidate);
|
|
113
|
+
if (order <= previous || order < ordinal(r.from, q) || next && order >= ordinal(next, q) || big(at) + BigInt(needle.length - 1) > big(q.end) || row.matchByteLength !== needle.length)
|
|
114
|
+
fail('partial', 'String-anchor pair/order changed.');
|
|
115
|
+
previous = order;
|
|
116
|
+
if (typeof l.textBytes !== 'string' || !/^(?:[a-f0-9]{2})+$/.test(l.textBytes) || l.textBytes.length > q.cap * 2)
|
|
117
|
+
fail('partial', 'String-anchor context bytes are missing or unbounded.');
|
|
118
|
+
const data = Buffer.from(l.textBytes, 'hex'), from = hex(l.textFrom), offset = big(at) - big(from), left = l.start === null ? null : hex(l.start), right = l.terminatorAddress === null ? null : hex(l.terminatorAddress);
|
|
119
|
+
if (offset < 0n || offset > BigInt(q.cap) || Number(offset) % unit || data.length % unit || l.textByteLength !== data.length || big(from) < big(q.start) || big(from) + BigInt(data.length - 1) > big(q.end) || !data.subarray(Number(offset), Number(offset) + needle.length).equals(needle))
|
|
120
|
+
fail('partial', 'String-anchor preview does not contain its byte match.');
|
|
121
|
+
for (let i = 0; i < data.length; i += unit)
|
|
122
|
+
if (data[i] === 0 && (unit === 1 || data[i + 1] === 0))
|
|
123
|
+
fail('partial', 'String-anchor preview crossed a NUL boundary.');
|
|
124
|
+
const boundaries = ['nul', 'range_edge', 'memory_edge', 'string_limit'];
|
|
125
|
+
if (!boundaries.includes(String(l.leftBoundary)) || !boundaries.includes(String(l.rightBoundary)) || (l.leftBoundary === 'nul') !== (left !== null) || (l.rightBoundary === 'nul') !== (right !== null))
|
|
126
|
+
fail('partial', 'String-anchor boundary facts changed.');
|
|
127
|
+
if (left !== null && (big(left) > big(at) || big(left) < big(q.start) + BigInt(unit) || big(left) > big(from) || row.matchOffsetBytes !== Number(big(at) - big(left)) || Number(big(at) - big(left)) % unit || Number(big(at) - big(left)) > q.cap - needle.length) || left === null && row.matchOffsetBytes !== null)
|
|
128
|
+
fail('partial', 'Invalid candidate start/offset.');
|
|
129
|
+
if (right !== null && (big(right) < big(at) + BigInt(needle.length) || big(right) + BigInt(unit - 1) > big(q.end) || big(right) < big(from) + BigInt(data.length) || Number(big(right) - big(at) - BigInt(needle.length)) % unit || Number(big(right) - big(at) - BigInt(needle.length)) > q.cap - needle.length))
|
|
130
|
+
fail('partial', 'Invalid candidate terminator.');
|
|
131
|
+
const bounded = left !== null && right !== null, total = bounded ? Number(big(right) - big(left)) : null, text = decoded(data, encoding), complete = bounded && total <= q.cap && text !== null;
|
|
132
|
+
const reason = complete ? 'complete' : total !== null && total > q.cap || l.leftBoundary === 'string_limit' || l.rightBoundary === 'string_limit' ? 'string_limit' : !bounded ? 'missing_boundary' : 'invalid_encoding';
|
|
133
|
+
if (l.boundariesComplete !== bounded || l.byteLength !== total || l.text !== text || l.decoding !== (text === null ? 'invalid' : 'valid') || l.complete !== complete || l.reason !== reason || complete && (from !== left || data.length !== total))
|
|
134
|
+
fail('partial', 'Inconsistent string-anchor context/decoding facts.');
|
|
135
|
+
}
|
|
136
|
+
if (Buffer.byteLength(JSON.stringify(v)) > 1000000)
|
|
137
|
+
fail('limit', 'String-anchor native response exceeds its envelope reserve.');
|
|
138
|
+
return v;
|
|
139
|
+
}
|
|
140
|
+
export async function inspectAnchors(backend, who, r, view, observation, observe) {
|
|
141
|
+
if (r.expectedView !== undefined && r.expectedView !== view)
|
|
142
|
+
fail('binding', 'String-anchor continuation view changed.', { reason: 'changed_view' });
|
|
143
|
+
const modification = object(observation).modification;
|
|
144
|
+
if (typeof modification !== 'string')
|
|
145
|
+
fail('unsupported', 'String anchors require guarded native observations.');
|
|
146
|
+
const q = r.query;
|
|
147
|
+
let raw;
|
|
148
|
+
try {
|
|
149
|
+
raw = await backend.call(who, '/insight/string-anchors/v1', { text: q.text, encodings: q.encodings.join(','), scan_from: q.start, scan_through: q.end, resume_address: r.from.address, resume_encoding: r.from.encoding, max_string_bytes: q.cap, ...r.budgets });
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (digest(observation) !== digest(await observe()))
|
|
153
|
+
fail('binding', 'Native view changed during string-anchor scan.', { reason: 'changed_view' });
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
if (digest(observation) !== digest(await observe()))
|
|
157
|
+
fail('binding', 'Native view changed during string-anchor scan.', { reason: 'changed_view' });
|
|
158
|
+
const value = anchorResult(raw, r, modification), cursor = value.scan && object(value.scan).complete ? null : 'sa1.' + Buffer.from(stable({ version: 1, algorithm: anchorAlgorithm, scope: who, query: q, next: object(value.scan).next, view, budgets: r.budgets })).toString('base64url');
|
|
159
|
+
const result = { ...who, value: { ...value, view: { identity: view }, cursor } };
|
|
160
|
+
if (Buffer.byteLength(JSON.stringify(result)) > 1048576)
|
|
161
|
+
fail('limit', 'String-anchor response exceeds one MiB.');
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
export function stringAnchorCommands(research) {
|
|
165
|
+
return [command('strings', 'String research', [command('anchors', 'Find raw literal byte hits and separate bounded NUL-delimited candidate context', ({ options: o }, c) => { const who = scope(o); return research(c).stringAnchors(who, anchorRequest(o, who)); }, { permission: Permission.ReadOnly, options: [...selectedOptions,
|
|
166
|
+
{ flags: '--text <literal>', description: 'Case-sensitive literal, 1–256 UTF-16 units, no NUL' },
|
|
167
|
+
{ flags: '--encodings <encodings>', description: 'Explicit ascii, utf16le, or both; no alignment assumption' },
|
|
168
|
+
{ flags: '--scan-from <address>', description: 'Inclusive range start; context stays inside the range' },
|
|
169
|
+
{ flags: '--scan-through <address>', description: 'Inclusive range end; the whole needle must fit' },
|
|
170
|
+
{ flags: '--max-string-bytes <count>', description: 'Even candidate/preview payload cap 16–8192, default 1024' },
|
|
171
|
+
{ flags: '--cursor <token>', description: 'Stateless continuation; excludes text/encodings/range/context selectors' },
|
|
172
|
+
{ flags: '--max-bytes <count>', description: '32768–16777216 actual bytes read including context/rereads; default 4194304' },
|
|
173
|
+
{ flags: '--max-candidates <count>', description: '1–100000 raw matched address/encoding pairs; default 10000' },
|
|
174
|
+
{ flags: '--timeout-seconds <count>', description: '1–30 second cooperative scan deadline; default 5' },
|
|
175
|
+
{ flags: '--limit <count>', description: '1–1000 raw hits per call; default 100' }] })])];
|
|
176
|
+
}
|
package/dist/src/sync-policy.js
CHANGED
|
@@ -40,6 +40,9 @@ export class Synchronization {
|
|
|
40
40
|
async execute(selected, path, input, run) {
|
|
41
41
|
const configured = this.workspace.context(selected).syncPolicy ?? manualPolicy;
|
|
42
42
|
const caller = input.requestId === undefined ? undefined : id(input.requestId);
|
|
43
|
+
const recorded = caller ? this.workspace.scopedRequest(selected, caller) : undefined;
|
|
44
|
+
if (caller && recorded?.id === caller)
|
|
45
|
+
return run(caller);
|
|
43
46
|
const previous = caller ? this.workspace.administrationByCaller(caller) : undefined;
|
|
44
47
|
if (!previous && (configured.publish === 'manual' && configured.refresh === 'manual' || input.requestId !== undefined && this.workspace.requestByCaller(id(input.requestId))))
|
|
45
48
|
return run(input.requestId === undefined ? '' : id(input.requestId));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { timed } from './internal-timing.js';
|
|
2
|
+
/** Observe existing boundaries; retain original method receivers and domain behavior. */
|
|
3
|
+
export function timedBackend(backend) {
|
|
4
|
+
return new Proxy(backend, { get(target, key) {
|
|
5
|
+
const member = Reflect.get(target, key);
|
|
6
|
+
if (typeof member !== 'function')
|
|
7
|
+
return member;
|
|
8
|
+
if (key === 'open')
|
|
9
|
+
return (...args) => timed('context.open', 'open', async () => Reflect.apply(member, target, args));
|
|
10
|
+
if (key === 'saved')
|
|
11
|
+
return (...args) => timed('context.save', 'saved', async () => Reflect.apply(member, target, args));
|
|
12
|
+
if (key === 'call')
|
|
13
|
+
return (...args) => timed('backend.operation', String(args[1]), async () => Reflect.apply(member, target, args));
|
|
14
|
+
return member.bind(target);
|
|
15
|
+
} });
|
|
16
|
+
}
|
package/dist/src/transport.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare class HttpBridge {
|
|
|
17
17
|
private schema?;
|
|
18
18
|
constructor(endpoint: string, fetch?: typeof globalThis.fetch, signal?: AbortSignal | undefined, token?: string | undefined, timeoutMilliseconds?: number | undefined, saveTimeoutMilliseconds?: number);
|
|
19
19
|
private request;
|
|
20
|
+
private requestBody;
|
|
20
21
|
instance(): Promise<Record<string, unknown>>;
|
|
21
22
|
descriptor(path: string): Promise<Descriptor>;
|
|
22
23
|
call(path: string, params?: Record<string, unknown>): Promise<unknown>;
|
package/dist/src/transport.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fail, object } from './model.js';
|
|
2
2
|
import { parseDomainJson } from './json.js';
|
|
3
|
+
import { timed } from './internal-timing.js';
|
|
3
4
|
import { CliError } from '@eyeauras/cli-factory';
|
|
4
5
|
/** The pinned GhidraMCP HTTP side of a dedicated bridge. No discovery by project name. */
|
|
5
6
|
export class HttpBridge {
|
|
@@ -26,6 +27,9 @@ export class HttpBridge {
|
|
|
26
27
|
fail('invalid', 'Bridge timeout is outside the supported bound.');
|
|
27
28
|
}
|
|
28
29
|
async request(path, method, query, body) {
|
|
30
|
+
return timed('native.http', path, () => this.requestBody(path, method, query, body));
|
|
31
|
+
}
|
|
32
|
+
async requestBody(path, method, query, body) {
|
|
29
33
|
const url = new URL(path, this.endpoint);
|
|
30
34
|
// Import includes synchronous auto-analysis. Keep discovery/ordinary requests short,
|
|
31
35
|
// while allowing a real executable to finish its first analysis without losing the reply.
|
package/dist/src/workspace.js
CHANGED
|
@@ -197,9 +197,12 @@ export class Workspace {
|
|
|
197
197
|
existingRequest(callerId, intentHash) {
|
|
198
198
|
if (callerId === undefined)
|
|
199
199
|
return;
|
|
200
|
-
|
|
200
|
+
// A previous build may have reserved a blocked retry under this generated ID.
|
|
201
|
+
// The recorded identity still selects its original intent, not that newer caller ID.
|
|
202
|
+
const recorded = this.db.prepare('SELECT id,hash FROM requests WHERE id=?').get(id(callerId));
|
|
203
|
+
if (!recorded && this.db.prepare('SELECT id FROM administration_requests WHERE caller_id=?').get(id(callerId)))
|
|
201
204
|
fail('idempotency', 'Request ID already identifies a profile administration operation.');
|
|
202
|
-
const row = this.db.prepare('SELECT id,hash FROM requests WHERE caller_id=?').get(id(callerId));
|
|
205
|
+
const row = recorded ?? this.db.prepare('SELECT id,hash FROM requests WHERE caller_id=?').get(id(callerId));
|
|
203
206
|
if (!row)
|
|
204
207
|
return;
|
|
205
208
|
if (row.hash !== intentHash)
|
package/package.json
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eyeauras/insight-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Agent-facing durable sessions and isolated Ghidra research contexts",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
7
7
|
"types": "./dist/src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/src/index.d.ts",
|
|
11
|
+
"default": "./dist/src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./persistent": {
|
|
14
|
+
"types": "./dist/src/persistent-client.d.ts",
|
|
15
|
+
"default": "./dist/src/persistent-client.js"
|
|
16
|
+
},
|
|
17
|
+
"./*": "./*"
|
|
18
|
+
},
|
|
8
19
|
"bin": {
|
|
9
20
|
"insight-cli": "./dist/src/bin.js"
|
|
10
21
|
},
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
package com.eyeauras.insight;
|
|
2
|
+
|
|
3
|
+
import java.math.BigInteger;
|
|
4
|
+
import java.nio.ByteBuffer;
|
|
5
|
+
import java.nio.charset.*;
|
|
6
|
+
import java.util.*;
|
|
7
|
+
import java.util.function.LongSupplier;
|
|
8
|
+
|
|
9
|
+
/** Pure bounded raw-memory algorithm. No Ghidra, filesystem, process or persistent query state. */
|
|
10
|
+
public final class InsightAnchorScan {
|
|
11
|
+
public static final String ALGORITHM="raw-string-anchors-v1";
|
|
12
|
+
public interface Bytes { int read(BigInteger at,byte[] destination)throws Exception; }
|
|
13
|
+
public record Span(BigInteger start,BigInteger end) {}
|
|
14
|
+
public record Config(String text,List<String> encodings,BigInteger start,BigInteger end,
|
|
15
|
+
BigInteger resume,String resumeEncoding,int cap,int maxBytes,int maxCandidates,int seconds,int limit) {
|
|
16
|
+
public Config {
|
|
17
|
+
if(text==null||text.isEmpty()||text.length()>256||text.indexOf(0)>=0)throw new IllegalArgumentException("Expected 1–256 UTF-16 units without NUL");
|
|
18
|
+
for(int i=0;i<text.length();i++)if(Character.isSurrogate(text.charAt(i))){
|
|
19
|
+
if(!Character.isHighSurrogate(text.charAt(i))||i+1>=text.length()||!Character.isLowSurrogate(text.charAt(++i)))throw new IllegalArgumentException("Unpaired surrogate");
|
|
20
|
+
}
|
|
21
|
+
if(!List.of(List.of("ascii"),List.of("utf16le"),List.of("ascii","utf16le")).contains(encodings))throw new IllegalArgumentException("Expected canonical ascii,utf16le encodings");
|
|
22
|
+
if(encodings.contains("ascii")&&text.chars().anyMatch(c->c>127))throw new IllegalArgumentException("Text is not representable in ASCII");
|
|
23
|
+
if(start.signum()<0||end.bitLength()>64||start.compareTo(end)>0||resume.compareTo(start)<0||resume.compareTo(end)>0||!encodings.contains(resumeEncoding))throw new IllegalArgumentException("Invalid range/frontier");
|
|
24
|
+
if(cap<16||cap>8192||(cap&1)!=0||encodings.stream().anyMatch(e->encoded(text,e).length>cap))throw new IllegalArgumentException("Invalid candidate byte cap");
|
|
25
|
+
if(maxBytes<32768||maxBytes>16777216||maxCandidates<1||maxCandidates>100000||seconds<1||seconds>30||limit<1||limit>1000)throw new IllegalArgumentException("Invalid page budgets");
|
|
26
|
+
encodings=List.copyOf(encodings);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
public static byte[] encoded(String text,String encoding){return text.getBytes(encoding.equals("ascii")?StandardCharsets.US_ASCII:StandardCharsets.UTF_16LE);}
|
|
30
|
+
private static final BigInteger ONE=BigInteger.ONE,PAGE=BigInteger.valueOf(4096);
|
|
31
|
+
private static String hex(BigInteger at){return at.toString(16);}
|
|
32
|
+
private static Map<String,Object> frontier(BigInteger at,String encoding){return Map.of("address",hex(at),"encoding",encoding);}
|
|
33
|
+
private static final class Pause extends RuntimeException {final String reason;Pause(String reason){this.reason=reason;}}
|
|
34
|
+
private record Page(BigInteger start,BigInteger end,byte[] bytes) {}
|
|
35
|
+
private record Probe(int bytes,String boundary,BigInteger nul) {}
|
|
36
|
+
private final Config c;private final List<Span> spans;private final Bytes source;private final LongSupplier clock;
|
|
37
|
+
private final long started;private final byte[][] needles;private final LinkedHashMap<BigInteger,Page> cache=new LinkedHashMap<>(16,.75f,true);
|
|
38
|
+
private Page last,positionPage;private int positionIndex,encodingIndex,bytesRead,testedPairs,candidates,estimatedResultBytes=2048;
|
|
39
|
+
private BigInteger position;private final List<Object> matches=new ArrayList<>();
|
|
40
|
+
public InsightAnchorScan(Config config,List<Span> spans,Bytes source,LongSupplier clock){
|
|
41
|
+
c=config;this.spans=List.copyOf(spans);this.source=source;this.clock=clock;started=clock.getAsLong();
|
|
42
|
+
needles=c.encodings.stream().map(e->encoded(c.text,e)).toArray(byte[][]::new);position=c.resume;encodingIndex=c.encodings.indexOf(c.resumeEncoding);
|
|
43
|
+
BigInteger previous=null;
|
|
44
|
+
for(Span s:spans){if(s.start.compareTo(c.start)<0||s.end.compareTo(c.end)>0||s.start.compareTo(s.end)>0||previous!=null&&s.start.compareTo(previous.add(ONE))<=0)throw new IllegalArgumentException("Expected clipped, sorted, merged spans");previous=s.end;}
|
|
45
|
+
}
|
|
46
|
+
private void deadline(){if(clock.getAsLong()-started>=c.seconds*1000000000L)throw new Pause("timeout");}
|
|
47
|
+
private BigInteger at(){return positionPage==null?position:positionPage.start.add(BigInteger.valueOf(positionIndex));}
|
|
48
|
+
private Page page(BigInteger at,Span span)throws Exception{
|
|
49
|
+
if(last!=null&&at.compareTo(last.start)>=0&&at.compareTo(last.end)<=0)return last;
|
|
50
|
+
BigInteger base=at.divide(PAGE).multiply(PAGE),start=base.max(span.start),end=base.add(PAGE).subtract(ONE).min(span.end);
|
|
51
|
+
Page hit=cache.get(start);if(hit!=null)return last=hit;
|
|
52
|
+
deadline();int length=end.subtract(start).intValueExact()+1;
|
|
53
|
+
if(length>c.maxBytes-bytesRead)throw new Pause("byte_limit");
|
|
54
|
+
byte[] data=new byte[length];int read=source.read(start,data);if(read!=length)throw new IllegalStateException("Incomplete initialized-memory read");
|
|
55
|
+
bytesRead+=read;Page value=new Page(start,end,data);cache.put(start,value);if(cache.size()>8)cache.remove(cache.keySet().iterator().next());return last=value;
|
|
56
|
+
}
|
|
57
|
+
private int byteAt(BigInteger at,Span span)throws Exception{Page p=page(at,span);return p.bytes[at.subtract(p.start).intValueExact()]&255;}
|
|
58
|
+
private byte[] copy(BigInteger at,int count,Span span)throws Exception{
|
|
59
|
+
byte[] data=new byte[count];int offset=0;
|
|
60
|
+
while(offset<count){Page p=page(at,span);int index=at.subtract(p.start).intValueExact(),n=Math.min(count-offset,p.bytes.length-index);System.arraycopy(p.bytes,index,data,offset,n);offset+=n;at=at.add(BigInteger.valueOf(n));}
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
private int unit(BigInteger at,int width,Span span)throws Exception{return width==1?byteAt(at,span):byteAt(at,span)|(byteAt(at.add(ONE),span)<<8);}
|
|
64
|
+
private Probe probe(BigInteger match,int length,int width,Span span,boolean left)throws Exception{
|
|
65
|
+
int count=0,allowance=c.cap-length;BigInteger cursor=left?match.subtract(BigInteger.valueOf(width)):match.add(BigInteger.valueOf(length));
|
|
66
|
+
while(true){
|
|
67
|
+
if((count&127)==0)deadline();
|
|
68
|
+
if(cursor.compareTo(span.start)<0||cursor.add(BigInteger.valueOf(width-1)).compareTo(span.end)>0){
|
|
69
|
+
boolean range=left?cursor.compareTo(c.start)<0:cursor.add(BigInteger.valueOf(width-1)).compareTo(c.end)>0;
|
|
70
|
+
return new Probe(count,range?"range_edge":"memory_edge",null);
|
|
71
|
+
}
|
|
72
|
+
// At the payload cap the next code unit may still be the NUL (not payload).
|
|
73
|
+
if(unit(cursor,width,span)==0)return new Probe(count,"nul",cursor);
|
|
74
|
+
if(count==allowance)return new Probe(count,"string_limit",null);
|
|
75
|
+
count+=width;cursor=left?cursor.subtract(BigInteger.valueOf(width)):cursor.add(BigInteger.valueOf(width));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
private Map<String,Object> candidate(BigInteger match,int index,Span span)throws Exception{
|
|
79
|
+
String encoding=c.encodings.get(index);int width=encoding.equals("ascii")?1:2,length=needles[index].length;
|
|
80
|
+
Probe left=probe(match,length,width,span,true),right=probe(match,length,width,span,false);
|
|
81
|
+
int spare=c.cap-length,l=Math.min(left.bytes,(spare/2/width)*width),r=Math.min(right.bytes,spare-l);
|
|
82
|
+
l+=Math.min(left.bytes-l,spare-l-r);r+=Math.min(right.bytes-r,spare-l-r);
|
|
83
|
+
BigInteger textFrom=match.subtract(BigInteger.valueOf(l));byte[] data=copy(textFrom,l+length+r,span);String text=null;
|
|
84
|
+
try{text=(encoding.equals("ascii")?StandardCharsets.US_ASCII:StandardCharsets.UTF_16LE).newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT).decode(ByteBuffer.wrap(data)).toString();}catch(CharacterCodingException ignored){}
|
|
85
|
+
boolean bounded=left.nul!=null&&right.nul!=null;int total=left.bytes+length+right.bytes;
|
|
86
|
+
boolean complete=bounded&&total<=c.cap&&text!=null;
|
|
87
|
+
String reason=complete?"complete":bounded&&total>c.cap||left.boundary.equals("string_limit")||right.boundary.equals("string_limit")?"string_limit":!bounded?"missing_boundary":"invalid_encoding";
|
|
88
|
+
Map<String,Object> literal=new LinkedHashMap<>();literal.put("start",left.nul==null?null:hex(left.nul.add(BigInteger.valueOf(width))));literal.put("terminatorAddress",right.nul==null?null:hex(right.nul));
|
|
89
|
+
literal.put("byteLength",bounded?total:null);literal.put("textFrom",hex(textFrom));literal.put("text",text);literal.put("textBytes",HexFormat.of().formatHex(data));literal.put("textByteLength",data.length);
|
|
90
|
+
literal.put("leftBoundary",left.boundary);literal.put("rightBoundary",right.boundary);literal.put("boundariesComplete",bounded);literal.put("decoding",text==null?"invalid":"valid");literal.put("complete",complete);literal.put("reason",reason);
|
|
91
|
+
Map<String,Object> row=new LinkedHashMap<>();row.put("encoding",encoding);row.put("matchAddress",hex(match));row.put("matchByteLength",length);row.put("matchOffsetBytes",left.nul==null?null:left.bytes);row.put("literalCandidate",literal);return row;
|
|
92
|
+
}
|
|
93
|
+
private boolean match(Page p,int index,int which,Span span)throws Exception{
|
|
94
|
+
byte[] needle=needles[which];if(p.bytes[index]!=needle[0])return false;
|
|
95
|
+
BigInteger at=p.start.add(BigInteger.valueOf(index));if(at.add(BigInteger.valueOf(needle.length-1)).compareTo(span.end)>0)return false;
|
|
96
|
+
if(index+needle.length<=p.bytes.length){for(int i=1;i<needle.length;i++)if(p.bytes[index+i]!=needle[i])return false;return true;}
|
|
97
|
+
for(int i=1;i<needle.length;i++){if((i&63)==0)deadline();if(byteAt(at.add(BigInteger.valueOf(i)),span)!=(needle[i]&255))return false;}return true;
|
|
98
|
+
}
|
|
99
|
+
public Map<String,Object> run()throws Exception{
|
|
100
|
+
String reason="exhausted";boolean complete=true;
|
|
101
|
+
try{
|
|
102
|
+
for(Span span:spans){
|
|
103
|
+
if(span.end.compareTo(c.resume)<0)continue;
|
|
104
|
+
BigInteger current=span.start.max(c.resume);int first=current.equals(c.resume)?c.encodings.indexOf(c.resumeEncoding):0;
|
|
105
|
+
while(current.compareTo(span.end)<=0){
|
|
106
|
+
position=current;positionPage=null;encodingIndex=first;
|
|
107
|
+
// Limits apply only while there remains an eligible unchecked pair.
|
|
108
|
+
if(matches.size()>=c.limit||estimatedResultBytes>=900000)throw new Pause("result_limit");
|
|
109
|
+
if(candidates>=c.maxCandidates)throw new Pause("candidate_limit");
|
|
110
|
+
Page p=page(current,span);positionPage=p;
|
|
111
|
+
for(int i=current.subtract(p.start).intValueExact();i<p.bytes.length;i++){
|
|
112
|
+
positionIndex=i;
|
|
113
|
+
for(int e=first;e<needles.length;e++){
|
|
114
|
+
encodingIndex=e;if((testedPairs&1023)==0)deadline();
|
|
115
|
+
if(matches.size()>=c.limit||estimatedResultBytes>=900000)throw new Pause("result_limit");
|
|
116
|
+
if(candidates>=c.maxCandidates)throw new Pause("candidate_limit");
|
|
117
|
+
if(match(p,i,e,span)){
|
|
118
|
+
candidates++;Map<String,Object> row=candidate(at(),e,span);
|
|
119
|
+
Map<?,?> literal=(Map<?,?>)row.get("literalCandidate");int cost=2048+8*((String)literal.get("textBytes")).length();
|
|
120
|
+
if(estimatedResultBytes+cost>900000)throw new Pause("result_limit");
|
|
121
|
+
matches.add(row);estimatedResultBytes+=cost;
|
|
122
|
+
}
|
|
123
|
+
testedPairs++;
|
|
124
|
+
}
|
|
125
|
+
first=0;
|
|
126
|
+
}
|
|
127
|
+
current=p.end.add(ONE);first=0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}catch(Pause pause){complete=false;reason=pause.reason;}
|
|
131
|
+
Map<String,Object> scan=new LinkedHashMap<>();scan.put("from",frontier(c.resume,c.resumeEncoding));scan.put("through",hex(c.end));scan.put("next",complete?null:frontier(at(),c.encodings.get(encodingIndex)));
|
|
132
|
+
scan.put("complete",complete);scan.put("reason",reason);scan.put("bytesRead",bytesRead);scan.put("testedPairs",testedPairs);scan.put("candidates",candidates);scan.put("elapsedMs",Math.max(0,(clock.getAsLong()-started)/1000000.0));
|
|
133
|
+
return Map.of("algorithm",ALGORITHM,"encodings",c.encodings,"matches",matches,"scan",scan);
|
|
134
|
+
}
|
|
135
|
+
}
|