(input: string, opts: ScanOptions = {})
| 450 | * structure-preserving path.) |
| 451 | */ |
| 452 | export function redactFindingSpans(input: string, opts: ScanOptions = {}): string | null { |
| 453 | const { findings } = scan(input, opts); |
| 454 | if (findings.some((f) => MARKER_ONLY_PATTERN_IDS.has(f.id))) return null; |
| 455 | const targets = findings.map((f) => ({ f, ...locateSpan(input, f) })); |
| 456 | if (targets.some((t) => t.start < 0)) return null; |
| 457 | |
| 458 | // Coalesce overlapping/touching ranges — splicing two intersecting spans |
| 459 | // independently applies a stale end offset to already-modified text and |
| 460 | // can leave trailing secret bytes in place. |
| 461 | targets.sort((a, b) => a.start - b.start); |
| 462 | const merged: Array<{ start: number; end: number; ids: string[] }> = []; |
| 463 | for (const t of targets) { |
| 464 | const last = merged[merged.length - 1]; |
| 465 | if (last && t.start <= last.end) { |
| 466 | last.end = Math.max(last.end, t.end); |
| 467 | if (!last.ids.includes(t.f.id)) last.ids.push(t.f.id); |
| 468 | } else { |
| 469 | merged.push({ start: t.start, end: t.end, ids: [t.f.id] }); |
| 470 | } |
| 471 | } |
| 472 | |
| 473 | // Right-to-left so earlier offsets remain valid after splicing. |
| 474 | let body = input; |
| 475 | for (let i = merged.length - 1; i >= 0; i--) { |
| 476 | const m = merged[i]; |
| 477 | body = body.slice(0, m.start) + `<REDACTED-${m.ids.join("+")}>` + body.slice(m.end); |
| 478 | } |
| 479 | return body; |
| 480 | } |
| 481 | |
| 482 | function locateSpan(input: string, f: Finding): { start: number; end: number } { |
| 483 | // Re-derive the offset from line/col on the original text. |
no test coverage detected