@eyeauras/insight-cli 0.3.2 → 0.4.0

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.
Files changed (140) hide show
  1. package/.cli-package-build.json +1 -1
  2. package/SKILL.md +4 -1
  3. package/dist/src/arguments.js +9 -4
  4. package/dist/src/cli.js +45 -37
  5. package/dist/src/code-references.d.ts +48 -0
  6. package/dist/src/code-references.js +154 -0
  7. package/dist/src/context-diagnostics.d.ts +25 -0
  8. package/dist/src/context-diagnostics.js +82 -0
  9. package/dist/src/contexts.d.ts +13 -1
  10. package/dist/src/contexts.js +38 -9
  11. package/dist/src/function-boundaries.d.ts +9 -0
  12. package/dist/src/function-boundaries.js +68 -0
  13. package/dist/src/function-entries.d.ts +33 -0
  14. package/dist/src/function-entries.js +84 -0
  15. package/dist/src/function-inspection.d.ts +4 -2
  16. package/dist/src/function-inspection.js +94 -59
  17. package/dist/src/function-results.d.ts +3 -1
  18. package/dist/src/function-results.js +9 -3
  19. package/dist/src/inline-script.d.ts +15 -0
  20. package/dist/src/inline-script.js +62 -0
  21. package/dist/src/internal-timing.d.ts +3 -0
  22. package/dist/src/internal-timing.js +52 -0
  23. package/dist/src/lifecycle.js +14 -4
  24. package/dist/src/native-inputs.d.ts +1 -1
  25. package/dist/src/native-inputs.js +1 -1
  26. package/dist/src/native-queries.js +1 -1
  27. package/dist/src/persistent-client.d.ts +30 -0
  28. package/dist/src/persistent-client.js +125 -0
  29. package/dist/src/queries.js +4 -1
  30. package/dist/src/query-policy.js +9 -1
  31. package/dist/src/research-view.d.ts +6 -0
  32. package/dist/src/research-view.js +9 -0
  33. package/dist/src/research.d.ts +38 -0
  34. package/dist/src/research.js +78 -8
  35. package/dist/src/string-anchors.d.ts +43 -0
  36. package/dist/src/string-anchors.js +176 -0
  37. package/dist/src/sync-policy.js +3 -0
  38. package/dist/src/timed-backend.d.ts +3 -0
  39. package/dist/src/timed-backend.js +16 -0
  40. package/dist/src/transport.d.ts +1 -0
  41. package/dist/src/transport.js +4 -0
  42. package/dist/src/workspace.js +5 -2
  43. package/package.json +12 -1
  44. package/references/native/InsightAnchorScan.java +135 -0
  45. package/references/native/InsightCodeReferences.java +129 -0
  46. package/references/native/InsightFunctionBoundaries.java +56 -0
  47. package/references/native/InsightInlineExecutor.java +143 -0
  48. package/references/native/InsightInlineScript.java +14 -0
  49. package/references/native/InsightPeRanges.java +92 -0
  50. package/references/native/InsightResearchQueries.java +22 -1
  51. package/references/native/InsightResearchView.java +69 -0
  52. package/references/native/InsightStringAnchors.java +49 -0
  53. package/references/persistent-client.mjs +125 -0
  54. package/references/persistent.md +50 -0
  55. package/references/query-semantics.md +12 -0
  56. package/references/recovery.md +36 -0
  57. package/references/research.md +87 -0
  58. package/references/setup.md +1 -1
  59. package/references/sparse-navigation.md +173 -0
  60. package/runtime/GhidraMCP-7.0.0.jar +0 -0
  61. package/runtime/GhidraMCP-NOTICE +5 -0
  62. package/runtime/provenance.json +17 -3
  63. package/runtime/research-view.provenance.json +28 -0
  64. package/runtime/toolbox.json +10 -3
  65. package/skill-bundle/insight-artifacts/SKILL.md +1 -1
  66. package/skill-bundle/insight-artifacts/references/recovery.md +36 -0
  67. package/skill-bundle/insight-binary/SKILL.md +1 -1
  68. package/skill-bundle/insight-capture/SKILL.md +1 -1
  69. package/skill-bundle/insight-cli/SKILL.md +4 -1
  70. package/skill-bundle/insight-cli/references/persistent-client.mjs +125 -0
  71. package/skill-bundle/insight-cli/references/persistent.md +50 -0
  72. package/skill-bundle/insight-cli/references/recovery.md +36 -0
  73. package/skill-bundle/insight-cli/references/setup.md +1 -1
  74. package/skill-bundle/insight-die/SKILL.md +1 -1
  75. package/skill-bundle/insight-dnspy/SKILL.md +1 -1
  76. package/skill-bundle/insight-dotnet-dump/SKILL.md +1 -1
  77. package/skill-bundle/insight-dotnet-stack/SKILL.md +1 -1
  78. package/skill-bundle/insight-dotnet-trace/SKILL.md +1 -1
  79. package/skill-bundle/insight-frida/SKILL.md +1 -1
  80. package/skill-bundle/insight-ghidra/SKILL.md +14 -1
  81. package/skill-bundle/insight-ghidra/references/persistent-client.mjs +125 -0
  82. package/skill-bundle/insight-ghidra/references/persistent.md +50 -0
  83. package/skill-bundle/insight-ghidra/references/query-semantics.md +12 -0
  84. package/skill-bundle/insight-ghidra/references/recovery.md +36 -0
  85. package/skill-bundle/insight-ghidra/references/research.md +87 -0
  86. package/skill-bundle/insight-ghidra/references/setup.md +1 -1
  87. package/skill-bundle/insight-ghidra/references/sparse-navigation.md +173 -0
  88. package/skill-bundle/insight-ilspy/SKILL.md +1 -1
  89. package/skill-bundle/insight-interop/SKILL.md +1 -1
  90. package/skill-bundle/insight-lldb/SKILL.md +1 -1
  91. package/skill-bundle/insight-llvm/SKILL.md +1 -1
  92. package/skill-bundle/insight-managed-heap/SKILL.md +1 -1
  93. package/skill-bundle/insight-managed-metadata/SKILL.md +1 -1
  94. package/skill-bundle/insight-mitmproxy/SKILL.md +1 -1
  95. package/skill-bundle/insight-overview/SKILL.md +1 -1
  96. package/skill-bundle/insight-procmon/SKILL.md +1 -1
  97. package/skill-bundle/insight-re/SKILL.md +4 -1
  98. package/skill-bundle/insight-re/references/persistent-client.mjs +125 -0
  99. package/skill-bundle/insight-re/references/persistent.md +50 -0
  100. package/skill-bundle/insight-re/references/recovery.md +36 -0
  101. package/skill-bundle/insight-sevenzip/SKILL.md +1 -1
  102. package/skill-bundle/insight-strace/SKILL.md +1 -1
  103. package/skill-bundle/insight-tshark/SKILL.md +1 -1
  104. package/skill-bundle/insight-windbg/SKILL.md +1 -1
  105. package/skill-bundle/manifest.json +1 -1
  106. package/skills/insight-artifacts/SKILL.md +1 -1
  107. package/skills/insight-artifacts/references/recovery.md +36 -0
  108. package/skills/insight-binary/SKILL.md +1 -1
  109. package/skills/insight-capture/SKILL.md +1 -1
  110. package/skills/insight-die/SKILL.md +1 -1
  111. package/skills/insight-dnspy/SKILL.md +1 -1
  112. package/skills/insight-dotnet-dump/SKILL.md +1 -1
  113. package/skills/insight-dotnet-stack/SKILL.md +1 -1
  114. package/skills/insight-dotnet-trace/SKILL.md +1 -1
  115. package/skills/insight-frida/SKILL.md +1 -1
  116. package/skills/insight-ghidra/SKILL.md +14 -1
  117. package/skills/insight-ghidra/references/persistent-client.mjs +125 -0
  118. package/skills/insight-ghidra/references/persistent.md +50 -0
  119. package/skills/insight-ghidra/references/query-semantics.md +12 -0
  120. package/skills/insight-ghidra/references/recovery.md +36 -0
  121. package/skills/insight-ghidra/references/research.md +87 -0
  122. package/skills/insight-ghidra/references/setup.md +1 -1
  123. package/skills/insight-ghidra/references/sparse-navigation.md +173 -0
  124. package/skills/insight-ilspy/SKILL.md +1 -1
  125. package/skills/insight-interop/SKILL.md +1 -1
  126. package/skills/insight-lldb/SKILL.md +1 -1
  127. package/skills/insight-llvm/SKILL.md +1 -1
  128. package/skills/insight-managed-heap/SKILL.md +1 -1
  129. package/skills/insight-managed-metadata/SKILL.md +1 -1
  130. package/skills/insight-mitmproxy/SKILL.md +1 -1
  131. package/skills/insight-overview/SKILL.md +1 -1
  132. package/skills/insight-procmon/SKILL.md +1 -1
  133. package/skills/insight-re/SKILL.md +4 -1
  134. package/skills/insight-re/references/persistent-client.mjs +125 -0
  135. package/skills/insight-re/references/persistent.md +50 -0
  136. package/skills/insight-re/references/recovery.md +36 -0
  137. package/skills/insight-sevenzip/SKILL.md +1 -1
  138. package/skills/insight-strace/SKILL.md +1 -1
  139. package/skills/insight-tshark/SKILL.md +1 -1
  140. package/skills/insight-windbg/SKILL.md +1 -1
@@ -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 = (previous ? previous.catch(() => { }) : Promise.resolve()).then(action);
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 = (previous ? previous.catch(() => { }) : Promise.resolve()).then(action);
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
- if (this.workspace.unresolved(reserved.context).some(other => other.id !== request.id)) {
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
- fail('blocked', 'This context retains an unproved effect; continuation did not start a second operation.', result);
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 observation = await backend.observation?.(selected);
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
- const raw = await backend.call(selected, operation, parameters), response = raw && typeof raw === 'object' && !Array.isArray(raw) ? object(raw) : { output: raw };
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
+ }
@@ -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,3 @@
1
+ import type { ResearchBackend } from './research.js';
2
+ /** Observe existing boundaries; retain original method receivers and domain behavior. */
3
+ export declare function timedBackend(backend: ResearchBackend): ResearchBackend;
@@ -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
+ }
@@ -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>;
@@ -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.
@@ -197,9 +197,12 @@ export class Workspace {
197
197
  existingRequest(callerId, intentHash) {
198
198
  if (callerId === undefined)
199
199
  return;
200
- if (this.db.prepare('SELECT id FROM administration_requests WHERE caller_id=?').get(id(callerId)))
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.2",
3
+ "version": "0.4.0",
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
+ }