kordoc 4.14.2 → 4.14.4

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 (151) hide show
  1. package/README.md +51 -6
  2. package/dist/{-AQKXLFIM.js → -Z4TJ3QGK.js} +13 -11
  3. package/dist/{chunk-QOMNEWCQ.cjs → chunk-5LAZQCUJ.cjs} +305 -31
  4. package/dist/chunk-5LAZQCUJ.cjs.map +1 -0
  5. package/dist/{chunk-7IAQ4HOK.js → chunk-6BWU6SQX.js} +1 -1
  6. package/dist/chunk-6BWU6SQX.js.map +1 -0
  7. package/dist/{chunk-FOJZNMMK.js → chunk-6CUIXJOC.js} +1 -1
  8. package/dist/chunk-6CUIXJOC.js.map +1 -0
  9. package/dist/{chunk-ZWD734EY.js → chunk-7SE4EZ45.js} +2 -2
  10. package/dist/{chunk-TSFS5VKD.js → chunk-D4U2TXDC.js} +332 -141
  11. package/dist/chunk-D4U2TXDC.js.map +1 -0
  12. package/dist/chunk-DPWWXUOG.js +2222 -0
  13. package/dist/chunk-DPWWXUOG.js.map +1 -0
  14. package/dist/{chunk-GDW4YP7K.js → chunk-GLSBEEZJ.js} +2 -2
  15. package/dist/{chunk-JQEFP45L.js → chunk-GUJ4Q73I.js} +2 -2
  16. package/dist/{chunk-ZWOFQRIB.js → chunk-HIJUKZHM.js} +3 -3
  17. package/dist/chunk-IMAAFSAX.cjs +1352 -0
  18. package/dist/chunk-IMAAFSAX.cjs.map +1 -0
  19. package/dist/{chunk-TNCKOL2O.js → chunk-LCW7SEPG.js} +2 -2
  20. package/dist/chunk-LFSQAMSM.js +1352 -0
  21. package/dist/chunk-LFSQAMSM.js.map +1 -0
  22. package/dist/chunk-MG6DJEX4.js +1299 -0
  23. package/dist/chunk-MG6DJEX4.js.map +1 -0
  24. package/dist/{chunk-QC5T6D4J.js → chunk-NLU6LBLY.js} +299 -25
  25. package/dist/chunk-NLU6LBLY.js.map +1 -0
  26. package/dist/{chunk-EQZI77PP.cjs → chunk-QEJUY6SK.cjs} +3 -3
  27. package/dist/{chunk-EQZI77PP.cjs.map → chunk-QEJUY6SK.cjs.map} +1 -1
  28. package/dist/{chunk-VQ5W4MYO.js → chunk-QIF2VXBQ.js} +298 -24
  29. package/dist/chunk-QIF2VXBQ.js.map +1 -0
  30. package/dist/{chunk-UET462HB.js → chunk-TITDJWVZ.js} +2 -2
  31. package/dist/{chunk-247ANM72.cjs → chunk-USHQCF5T.cjs} +870 -147
  32. package/dist/chunk-USHQCF5T.cjs.map +1 -0
  33. package/dist/{chunk-C7FQBSON.js → chunk-V3DX4DUF.js} +2 -2
  34. package/dist/{chunk-DOGPOY4B.js → chunk-VDN7LMPL.js} +2 -2
  35. package/dist/{chunk-EDNJLUSE.js → chunk-VJIL6PIL.js} +2 -2
  36. package/dist/{chunk-L7DMSMAG.cjs → chunk-VWUYVEFN.cjs} +1 -1
  37. package/dist/chunk-VWUYVEFN.cjs.map +1 -0
  38. package/dist/{chunk-HR2XNERB.cjs → chunk-WJHAU73B.cjs} +2 -2
  39. package/dist/{chunk-HR2XNERB.cjs.map → chunk-WJHAU73B.cjs.map} +1 -1
  40. package/dist/{chunk-NS55SM7A.js → chunk-WJJGW5PL.js} +2 -2
  41. package/dist/{chunk-EISO3POT.js → chunk-XVXHXQIO.js} +788 -194
  42. package/dist/chunk-XVXHXQIO.js.map +1 -0
  43. package/dist/{chunk-ILU76UF3.js → chunk-YAKC46HZ.js} +2 -2
  44. package/dist/chunk-YAKC46HZ.js.map +1 -0
  45. package/dist/{chunk-3DUADAY2.js → chunk-YAVHVFRY.js} +851 -128
  46. package/dist/chunk-YAVHVFRY.js.map +1 -0
  47. package/dist/{chunk-A7QC2ZVP.js → chunk-YT3GC62E.js} +851 -128
  48. package/dist/chunk-YT3GC62E.js.map +1 -0
  49. package/dist/chunks-CCMRL4MJ.js +10 -0
  50. package/dist/cli.js +228 -97
  51. package/dist/cli.js.map +1 -1
  52. package/dist/{formula-PJJ6S3Z2.cjs → formula-5LOVHAKE.cjs} +9 -9
  53. package/dist/{formula-PJJ6S3Z2.cjs.map → formula-5LOVHAKE.cjs.map} +1 -1
  54. package/dist/{formula-JGFTXWYT.js → formula-7RANCW5L.js} +3 -3
  55. package/dist/{formula-5S7WWXGH.js → formula-7UYJOVTB.js} +4 -4
  56. package/dist/{image-ocr-WCAYXLOA.js → image-ocr-I6JOCATS.js} +34 -16
  57. package/dist/image-ocr-I6JOCATS.js.map +1 -0
  58. package/dist/{image-ocr-ZTYAGV3W.cjs → image-ocr-NYBJEOH2.cjs} +36 -18
  59. package/dist/image-ocr-NYBJEOH2.cjs.map +1 -0
  60. package/dist/{image-ocr-FLIL7MGN.js → image-ocr-ZV6PDBUQ.js} +32 -14
  61. package/dist/image-ocr-ZV6PDBUQ.js.map +1 -0
  62. package/dist/index.cjs +4048 -1216
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +66 -28
  65. package/dist/index.d.ts +66 -28
  66. package/dist/index.js +3487 -655
  67. package/dist/index.js.map +1 -1
  68. package/dist/mcp.js +74 -53
  69. package/dist/mcp.js.map +1 -1
  70. package/dist/{model-bundle-FSPZZ2EM.js → model-bundle-2C5IYDLE.js} +5 -5
  71. package/dist/{models-CJPAQK5R.js → models-SEQZHZFT.js} +5 -5
  72. package/dist/{parser-SCUTZAIJ.js → parser-3FNXEP6T.js} +708 -250
  73. package/dist/parser-3FNXEP6T.js.map +1 -0
  74. package/dist/{parser-A4TMPJAM.js → parser-FJBJXRJK.js} +708 -250
  75. package/dist/parser-FJBJXRJK.js.map +1 -0
  76. package/dist/{parser-VCXE4I5A.cjs → parser-HVEKF73B.cjs} +733 -275
  77. package/dist/parser-HVEKF73B.cjs.map +1 -0
  78. package/dist/pdf-ocr-JRDBJ6TA.cjs +14 -0
  79. package/dist/{pdf-ocr-MBZZ57LD.cjs.map → pdf-ocr-JRDBJ6TA.cjs.map} +1 -1
  80. package/dist/pdf-ocr-UUBWBEHL.js +16 -0
  81. package/dist/pdf-ocr-VJSF2SQC.js +13 -0
  82. package/dist/{profile-io-LBM263K7.js → profile-io-RUFZOZO6.js} +3 -3
  83. package/dist/{rasterize-46RG4ZF6.js → rasterize-IOYXR5SI.js} +3 -3
  84. package/dist/redact-NMIKVHMP.js +18 -0
  85. package/dist/redact-doc-QBHNJ2BU.js +1284 -0
  86. package/dist/redact-doc-QBHNJ2BU.js.map +1 -0
  87. package/dist/{render-NGP5T24S.js → render-YZ2TEC3N.js} +7 -7
  88. package/dist/seal-ECV44TUH.js +10 -0
  89. package/dist/{setup-AU4OCHNH.js → setup-MPIEP6XT.js} +3 -3
  90. package/dist/visual-ICMOWUCR.js +19 -0
  91. package/dist/{watch-MKPUCPXM.js → watch-RYO7MN5B.js} +12 -12
  92. package/package.json +2 -2
  93. package/dist/chunk-247ANM72.cjs.map +0 -1
  94. package/dist/chunk-3DUADAY2.js.map +0 -1
  95. package/dist/chunk-422427XJ.cjs +0 -571
  96. package/dist/chunk-422427XJ.cjs.map +0 -1
  97. package/dist/chunk-7IAQ4HOK.js.map +0 -1
  98. package/dist/chunk-A7QC2ZVP.js.map +0 -1
  99. package/dist/chunk-DZIXKL7E.js +0 -145
  100. package/dist/chunk-DZIXKL7E.js.map +0 -1
  101. package/dist/chunk-EISO3POT.js.map +0 -1
  102. package/dist/chunk-EP7KOI2V.js +0 -571
  103. package/dist/chunk-EP7KOI2V.js.map +0 -1
  104. package/dist/chunk-FOJZNMMK.js.map +0 -1
  105. package/dist/chunk-ILU76UF3.js.map +0 -1
  106. package/dist/chunk-KVWV4ULP.js +0 -516
  107. package/dist/chunk-KVWV4ULP.js.map +0 -1
  108. package/dist/chunk-L7DMSMAG.cjs.map +0 -1
  109. package/dist/chunk-QC5T6D4J.js.map +0 -1
  110. package/dist/chunk-QOMNEWCQ.cjs.map +0 -1
  111. package/dist/chunk-TSFS5VKD.js.map +0 -1
  112. package/dist/chunk-VQ5W4MYO.js.map +0 -1
  113. package/dist/chunks-SX77P5CV.js +0 -10
  114. package/dist/image-ocr-FLIL7MGN.js.map +0 -1
  115. package/dist/image-ocr-WCAYXLOA.js.map +0 -1
  116. package/dist/image-ocr-ZTYAGV3W.cjs.map +0 -1
  117. package/dist/parser-A4TMPJAM.js.map +0 -1
  118. package/dist/parser-SCUTZAIJ.js.map +0 -1
  119. package/dist/parser-VCXE4I5A.cjs.map +0 -1
  120. package/dist/pdf-ocr-76SAUXMZ.js +0 -13
  121. package/dist/pdf-ocr-CPU55CVF.js +0 -16
  122. package/dist/pdf-ocr-MBZZ57LD.cjs +0 -14
  123. package/dist/redact-7ILEAUTS.js +0 -12
  124. package/dist/seal-RGUPOLXD.js +0 -10
  125. package/dist/visual-NZLYIYEM.js +0 -19
  126. /package/dist/{-AQKXLFIM.js.map → -Z4TJ3QGK.js.map} +0 -0
  127. /package/dist/{chunk-ZWD734EY.js.map → chunk-7SE4EZ45.js.map} +0 -0
  128. /package/dist/{chunk-GDW4YP7K.js.map → chunk-GLSBEEZJ.js.map} +0 -0
  129. /package/dist/{chunk-JQEFP45L.js.map → chunk-GUJ4Q73I.js.map} +0 -0
  130. /package/dist/{chunk-ZWOFQRIB.js.map → chunk-HIJUKZHM.js.map} +0 -0
  131. /package/dist/{chunk-TNCKOL2O.js.map → chunk-LCW7SEPG.js.map} +0 -0
  132. /package/dist/{chunk-UET462HB.js.map → chunk-TITDJWVZ.js.map} +0 -0
  133. /package/dist/{chunk-C7FQBSON.js.map → chunk-V3DX4DUF.js.map} +0 -0
  134. /package/dist/{chunk-DOGPOY4B.js.map → chunk-VDN7LMPL.js.map} +0 -0
  135. /package/dist/{chunk-EDNJLUSE.js.map → chunk-VJIL6PIL.js.map} +0 -0
  136. /package/dist/{chunk-NS55SM7A.js.map → chunk-WJJGW5PL.js.map} +0 -0
  137. /package/dist/{chunks-SX77P5CV.js.map → chunks-CCMRL4MJ.js.map} +0 -0
  138. /package/dist/{formula-JGFTXWYT.js.map → formula-7RANCW5L.js.map} +0 -0
  139. /package/dist/{formula-5S7WWXGH.js.map → formula-7UYJOVTB.js.map} +0 -0
  140. /package/dist/{model-bundle-FSPZZ2EM.js.map → model-bundle-2C5IYDLE.js.map} +0 -0
  141. /package/dist/{models-CJPAQK5R.js.map → models-SEQZHZFT.js.map} +0 -0
  142. /package/dist/{pdf-ocr-76SAUXMZ.js.map → pdf-ocr-UUBWBEHL.js.map} +0 -0
  143. /package/dist/{pdf-ocr-CPU55CVF.js.map → pdf-ocr-VJSF2SQC.js.map} +0 -0
  144. /package/dist/{profile-io-LBM263K7.js.map → profile-io-RUFZOZO6.js.map} +0 -0
  145. /package/dist/{rasterize-46RG4ZF6.js.map → rasterize-IOYXR5SI.js.map} +0 -0
  146. /package/dist/{redact-7ILEAUTS.js.map → redact-NMIKVHMP.js.map} +0 -0
  147. /package/dist/{render-NGP5T24S.js.map → render-YZ2TEC3N.js.map} +0 -0
  148. /package/dist/{seal-RGUPOLXD.js.map → seal-ECV44TUH.js.map} +0 -0
  149. /package/dist/{setup-AU4OCHNH.js.map → setup-MPIEP6XT.js.map} +0 -0
  150. /package/dist/{visual-NZLYIYEM.js.map → visual-ICMOWUCR.js.map} +0 -0
  151. /package/dist/{watch-MKPUCPXM.js.map → watch-RYO7MN5B.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/redact-scrub.ts","../src/redact-hwpx.ts","../src/redact-hwp5.ts","../src/redact-doc.ts"],"sourcesContent":["/**\n * 문서 파일 마스킹 공용 도구 — 텍스트 단위 탐지(룰 + 리터럴), 바이너리 안 문자열 조각 훑기,\n * 빈 미리보기 이미지. redact-hwpx.ts·redact-hwp5.ts 가 쓰고 redact-doc.ts 가 묶는다.\n */\n\nimport { createRequire } from \"module\"\nimport JSZip from \"jszip\"\nimport { redactText, normalizeForDetect, mdMarkerRanges, type RedactRule, type RedactHit } from \"./redact.js\"\nimport { nameParticleAt } from \"./redact-name-address.js\"\nimport { deflateSync, inflateSync } from \"zlib\"\n\nconst require = createRequire(import.meta.url)\nconst CFB: { parse(data: Buffer): { FileIndex: Array<{ type?: number; content?: Buffer | Uint8Array }>; FullPaths: string[] } } = require(\"cfb\")\n\n/** 파일 안 위치 분류 (리포트용) */\nexport type RedactWhere =\n | \"body\" | \"table\" | \"textbox\" | \"header\" | \"footer\" | \"footnote\" | \"endnote\" | \"caption\" | \"memo\"\n | \"hidden\" | \"field\" | \"attribute\" | \"metadata\" | \"preview\" | \"slack\" | \"other\"\n\n/** 파일 안에서 가린(또는 남은) PII 한 건 — 원본 값 없이 masked 만 */\nexport interface RedactFileHit {\n rule: RedactRule\n masked: string\n /** 컨테이너 파트 — ZIP 엔트리 이름 또는 OLE 스트림 경로 */\n part: string\n where: RedactWhere\n}\n\n// ─── 공용: 텍스트 단위 탐지 + 리터럴 ───────────────────\n\n/** 본문 마크다운에서 라벨·표 머리글 문맥으로 찾은 값 — 문맥 없는 파일 위치(미리보기·다른 셀)에서도 가린다 */\nexport interface Literal {\n norm: string\n value: string\n masked: string\n rule: RedactRule\n}\n\nexport interface ScrubCtx {\n rules: readonly RedactRule[]\n maskChar: string\n literals: readonly Literal[]\n}\n\n/** 어떤 룰도 못 걸리는 텍스트 빠른 배제 — \"@\" 가 없고 숫자가 4개 미만 (바이너리 문자열 조각 대부분) */\nfunction cannotContainPii(text: string): boolean {\n if (text.includes(\"@\") || text.includes(\"@\")) return false\n let digits = 0\n for (let i = 0; i < text.length; i++) {\n const c = text.charCodeAt(i)\n if ((c >= 0x30 && c <= 0x39) || (c >= 0xff10 && c <= 0xff19)) { if (++digits >= 4) return false }\n }\n return true\n}\n\n/**\n * 리터럴 색인 — 정규화 값 앞 6글자 → 후보 (긴 값 우선), 6글자보다 짧은 값(인명 \"홍길동\")은 앞 2글자로 따로.\n * 목록마다 1회. 수천 개(대형 명단)여도 본문 길이에 선형\n */\nconst LIT_KEY = 6\nconst LIT_KEY_SHORT = 2\nconst literalIndex = new WeakMap<readonly Literal[], { long: Map<string, Literal[]>; short: Map<string, Literal[]> }>()\nfunction literalBuckets(lits: readonly Literal[]): { long: Map<string, Literal[]>; short: Map<string, Literal[]> } {\n let m = literalIndex.get(lits)\n if (!m) {\n m = { long: new Map(), short: new Map() }\n for (const l of [...lits].sort((a, b) => b.norm.length - a.norm.length)) {\n const short = l.norm.length < LIT_KEY\n const k = l.norm.slice(0, short ? LIT_KEY_SHORT : LIT_KEY)\n const map = short ? m.short : m.long\n const list = map.get(k)\n if (list) list.push(l)\n else map.set(k, [l])\n }\n literalIndex.set(lits, m)\n }\n return m\n}\n\nconst isAlnum = (c: string | undefined): boolean => c !== undefined && /[0-9A-Za-z]/.test(c)\nconst isHangul = (c: string | undefined): boolean => c !== undefined && c >= \"가\" && c <= \"힣\"\n\n/**\n * 리터럴 경계 — 영숫자에 붙은 번호는 더 긴 코드의 일부, 한글로 시작·끝나는 값(인명·주소)은 낱말 한가운데가\n * 아니어야 한다: 앞 글자가 한글이 아니고, 뒤는 한글이 아니거나 조사·호칭(\"홍길동은\", \"홍길동 씨\")\n */\nfunction literalBoundaryOk(norm: string, i: number, l: Literal): boolean {\n const end = i + l.norm.length\n if (isAlnum(norm[end - 1]) && isAlnum(norm[end])) return false\n if (isHangul(l.norm[0]) && isHangul(norm[i - 1])) return false\n return !isHangul(l.norm[l.norm.length - 1]) || !isHangul(norm[end]) || nameParticleAt(norm, end)\n}\n\n/** 텍스트 안 리터럴 위치 (영숫자·한글 낱말 경계, 겹치지 않게 앞에서부터) */\nexport function findLiterals(norm: string, lits: readonly Literal[]): Array<{ index: number; lit: Literal }> {\n const out: Array<{ index: number; lit: Literal }> = []\n if (lits.length === 0) return out\n const { long, short } = literalBuckets(lits)\n const hit = (cand: Literal[] | undefined, i: number): Literal | undefined =>\n cand?.find((l) => norm.startsWith(l.norm, i) && literalBoundaryOk(norm, i, l))\n for (let i = 0; i + LIT_KEY_SHORT <= norm.length; i++) {\n if (isAlnum(norm[i - 1]) && isAlnum(norm[i])) continue\n const lit = (long.size > 0 ? hit(long.get(norm.slice(i, i + LIT_KEY)), i) : undefined)\n ?? (short.size > 0 ? hit(short.get(norm.slice(i, i + LIT_KEY_SHORT)), i) : undefined)\n if (!lit) continue\n out.push({ index: i, lit })\n i += lit.norm.length - 1\n }\n return out\n}\n\n/** 인명·주소 룰 — 번호가 없는 글에서도 찾아야 해서 빠른 배제(숫자·@)를 쓸 수 없다 */\nconst TEXT_RULES: ReadonlySet<RedactRule> = new Set([\"name\", \"address\"])\n\n/**\n * 바이너리에서 캐낸 문자열 조각용 — 인명·주소 룰은 빼고 번호형 룰과 리터럴만. 압축·이진 데이터를 UTF-16 으로 읽으면\n * 한자·한글이 섞인 긴 조각이 되고, \"…님\"·\"…씨\" 앞 한자 몇 글자가 이름 모양이 된다(코퍼스 HWP 원시 바이트 재검사\n * 799건이 전부 이것). 본문에서 찾은 이름·주소는 리터럴로 여기서도 가린다\n */\nfunction binaryCtx(ctx: ScrubCtx): ScrubCtx {\n return ctx.rules.some((r) => TEXT_RULES.has(r)) ? { ...ctx, rules: ctx.rules.filter((r) => !TEXT_RULES.has(r)) } : ctx\n}\n\n/**\n * 텍스트 한 단위(문단·텍스트 노드·줄)에서 PII 위치 — 룰 탐지 + 리터럴 일치.\n * 반환 hit 의 masked 는 text 원문 글자에 마스킹을 입힌 것 (길이 동일).\n */\nexport function findPii(text: string, ctx: ScrubCtx): RedactHit[] {\n // 빠른 배제는 번호형 룰에만 — 리터럴(짧은 이름 포함)은 따로 본다\n const textRules = ctx.rules.some((r) => TEXT_RULES.has(r))\n const hits = textRules ? redactText(text, { rules: ctx.rules, maskChar: ctx.maskChar }).hits\n : text.length < 5 || cannotContainPii(text) ? [] : redactText(text, { rules: ctx.rules, maskChar: ctx.maskChar }).hits\n if (ctx.literals.length === 0) return hits\n for (const { index: i, lit } of findLiterals(normalizeForDetect(text), ctx.literals)) {\n const end = i + lit.norm.length\n if (hits.some((h) => i < h.index + h.length && end > h.index)) continue\n let masked = \"\"\n for (let k = 0; k < lit.norm.length; k++) masked += lit.masked[k] !== lit.value[k] ? ctx.maskChar : text[i + k]\n hits.push({ rule: lit.rule, masked, index: i, length: lit.norm.length })\n }\n // 파서가 내보내는 모양 — 두 칸 이상 공백은 한 칸(\"하늘아파트 204동\"), 균등배분 글(\"홍 길 동\")은 붙인 값이라 리터럴도\n // 그 모양이다. 그렇게 본 사본에서 찾아 원문 자리로 되돌린다 (공백은 그대로, 글자만 가림)\n const view = parserView(text)\n if (view) {\n for (const { index: i, lit } of findLiterals(normalizeForDetect(view.text), ctx.literals)) {\n const start = view.pos[i]\n const end = view.pos[i + lit.norm.length - 1] + 1\n if (hits.some((h) => start < h.index + h.length && end > h.index)) continue\n const chars = text.slice(start, end).split(\"\")\n for (let k = 0; k < lit.norm.length; k++) if (lit.masked[k] !== lit.value[k]) chars[view.pos[i + k] - start] = ctx.maskChar\n hits.push({ rule: lit.rule, masked: chars.join(\"\"), index: start, length: end - start })\n }\n }\n return hits.sort((a, b) => a.index - b.index)\n}\n\n/**\n * 파서가 내보내는 모양으로 본 글과 원문 자리 — table/builder.ts sanitizeText 와 같은 기준. 두 칸 이상 공백은 한 칸,\n * 균등배분(30자 이하, 토큰 셋 이상, 한글 한 글자 토큰 70% 이상, 날짜·시각 단위 빈칸 \"년 월 일\" 은 아님)은 공백을 다 지운다.\n * 원문과 같으면 null\n */\nfunction parserView(text: string): { text: string; pos: number[] } | null {\n if (!text.includes(\" \")) return null\n let squeeze = false\n if (text.length <= 400) { // 한 칸으로 줄여 30자 이하여야 하므로 긴 글(바이너리 조각 등)은 셀 필요가 없다\n const tokens = text.replace(/ {2,}/g, \" \").trim().split(\" \")\n const hangul1 = (t: string): boolean => t.length === 1 && /[\\uAC00-\\uD7AF\\u3131-\\u318E]/.test(t)\n squeeze = tokens.join(\" \").length <= 30 && tokens.length >= 3 && tokens.filter(hangul1).length / tokens.length >= 0.7\n && !tokens.every((t) => !hangul1(t) || /[년월일시분초]/.test(t))\n }\n if (!squeeze && !text.includes(\" \")) return null\n const pos: number[] = []\n for (let i = 0; i < text.length; i++) if (text[i] !== \" \" || (!squeeze && text[i - 1] !== \" \")) pos.push(i)\n return { text: pos.map((i) => text[i]).join(\"\"), pos }\n}\n\n/** 마크다운 탐지 결과 → 리터럴 (인라인 서식 표지 제거, 값 기준 중복 제거, 너무 짧은 값 제외) */\nexport function literalsFromMarkdown(markdown: string, hits: readonly RedactHit[]): Literal[] {\n const out = new Map<string, Literal>()\n const lineMarkers = new Map<number, Array<[number, number]>>()\n for (const h of hits) {\n const rawValue = markdown.slice(h.index, h.index + h.length)\n // 표지 글자는 masked 에서도 같은 자리에 그대로 있다 — 같은 자리를 둘 다에서 뺀다 (짝 판정은 줄 전체로)\n const lineStart = markdown.lastIndexOf(\"\\n\", h.index - 1) + 1\n let ranges = lineMarkers.get(lineStart)\n if (!ranges) {\n const lineEnd = markdown.indexOf(\"\\n\", h.index)\n ranges = mdMarkerRanges(markdown.slice(lineStart, lineEnd < 0 ? undefined : lineEnd))\n lineMarkers.set(lineStart, ranges)\n }\n const drop = new Set<number>()\n for (const [ms, me] of ranges) {\n for (let p = ms + lineStart; p < me + lineStart; p++) if (p >= h.index && p < h.index + h.length) drop.add(p - h.index)\n }\n const value = rawValue.split(\"\").filter((_, k) => !drop.has(k)).join(\"\")\n const masked = h.masked.split(\"\").filter((_, k) => !drop.has(k)).join(\"\")\n const norm = normalizeForDetect(value)\n // 번호형은 영숫자 6개 이상(짧은 값은 흔한 숫자 조각), 인명·주소는 두 글자 이상 — 표 칸 이름(\"| 성명 |\" 열)은\n // 파일 문단에 머리글 문맥이 없어 리터럴로만 가려진다\n const tooShort = TEXT_RULES.has(h.rule) ? norm.length < 2 : norm.replace(/[^0-9A-Za-z]/g, \"\").length < 6\n if (tooShort || out.has(norm)) continue\n out.set(norm, { norm, value, masked, rule: h.rule })\n }\n return [...out.values()]\n}\n\n// ─── 공용: 빈 미리보기 이미지 ────────────────────────\n\nconst CRC_TABLE = (() => {\n const t = new Uint32Array(256)\n for (let n = 0; n < 256; n++) {\n let c = n\n for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1\n t[n] = c >>> 0\n }\n return t\n})()\n\nexport function crc32(data: Uint8Array): number {\n let crc = 0xffffffff\n for (let i = 0; i < data.length; i++) crc = CRC_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8)\n return (crc ^ 0xffffffff) >>> 0\n}\n\nfunction pngChunk(type: string, data: Uint8Array): Buffer {\n const head = Buffer.alloc(8)\n head.writeUInt32BE(data.length, 0)\n head.write(type, 4, \"ascii\")\n const crc = Buffer.alloc(4)\n crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0)\n return Buffer.concat([head, data, crc])\n}\n\n/** 같은 크기의 흰 PNG (8bit 회색조) */\nfunction blankPng(width: number, height: number): Buffer {\n const ihdr = Buffer.alloc(13)\n ihdr.writeUInt32BE(width, 0)\n ihdr.writeUInt32BE(height, 4)\n ihdr[8] = 8 // bit depth\n ihdr[9] = 0 // grayscale\n const row = Buffer.alloc(width + 1, 0xff)\n row[0] = 0 // filter none\n const raw = Buffer.alloc(row.length * height)\n for (let y = 0; y < height; y++) row.copy(raw, y * row.length)\n return Buffer.concat([\n Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),\n pngChunk(\"IHDR\", ihdr), pngChunk(\"IDAT\", deflateSync(raw)), pngChunk(\"IEND\", new Uint8Array(0)),\n ])\n}\n\n/** 논리 화면만 원본 크기인 흰 GIF (1×1 흰 프레임 + 흰 배경) */\nfunction blankGif(width: number, height: number): Buffer {\n const b = Buffer.from([\n 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0, 0, 0, 0, 0x80, 0, 0, // GIF89a, 화면 크기, 전역 팔레트 2색\n 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,\n 0x2c, 0, 0, 0, 0, 1, 0, 1, 0, 0, // 이미지 1×1 @(0,0)\n 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b, // LZW: clear·0·end, 트레일러\n ])\n b.writeUInt16LE(Math.min(width, 0xffff), 6)\n b.writeUInt16LE(Math.min(height, 0xffff), 8)\n return b\n}\n\n/**\n * 미리보기 이미지(첫 쪽 렌더) → 같은 크기의 흰 이미지. 724×1024 급이라 본문 글자가 읽힌다 — PII 를\n * 가린 문서라면 함께 지운다. 한컴은 저장할 때 다시 만든다. 모르는 형식은 null.\n */\nexport function blankPreviewImage(img: Uint8Array): Buffer | null {\n const b = Buffer.from(img.buffer, img.byteOffset, img.byteLength)\n if (b.length >= 24 && b.readUInt32BE(0) === 0x89504e47) {\n const w = b.readUInt32BE(16)\n const h = b.readUInt32BE(20)\n if (w > 0 && h > 0 && w <= 8192 && h <= 8192) return blankPng(w, h)\n return blankPng(1, 1)\n }\n if (b.length >= 10 && b.toString(\"latin1\", 0, 3) === \"GIF\") return blankGif(b.readUInt16LE(6), b.readUInt16LE(8))\n if (b.length >= 26 && b.toString(\"latin1\", 0, 2) === \"BM\") {\n // BMP — 헤더·팔레트 유지, 화소 배열만 0xFF (24/32bit 는 흰색, 팔레트형은 마지막 색)\n const out = Buffer.from(b)\n const pixelOffset = out.readUInt32LE(10)\n if (pixelOffset > 0 && pixelOffset < out.length) out.fill(0xff, pixelOffset)\n return out\n }\n return null\n}\n\n// ─── 공용: UTF-16LE / 1바이트 문자열 조각 훑기 ────────────\n\nconst isTextUnit = (u: number): boolean =>\n (u >= 0x20 && u < 0x7f) || (u >= 0xa0 && u <= 0xd7ff) || (u >= 0xe000 && u <= 0xfffd) || u === 0x09\n\n/**\n * 서식 데이터 연결 경로 — 셀·누름틀 이름으로 쓰이는 XPath 꼴(\"./DataArea/…/Job.Code@code.Name\").\n * 이메일 모양이 섞여 있어도 PII 가 아니고, 가리면 fill_form 이 이름으로 찾던 필드가 끊긴다.\n * 경로 꼴(./ 로 시작·[n]/ 포함)이거나 대문자 \"X.Code@code.Name\" 꼴만 — 소문자 실주소(hong@code.org)는 아니다.\n * 속성값·HWP5 레코드 문자열(셀 이름·필드 데이터)에서만 건너뛴다 — 본문·미리보기 텍스트는 그대로 검사\n */\nexport function isBindingPath(s: string): boolean {\n const t = s.trim()\n return /^\\.?\\/[^\\s]*\\//.test(t) || /^[^\\s]*\\[\\d+\\][^\\s]*\\//.test(t) || /[A-Za-z]\\.Code@code\\.[A-Z]/.test(t)\n}\n\n/**\n * 바이너리 레코드·스트림 안의 UTF-16LE 문자열 조각을 찾아 PII 를 같은 길이로 가린다 (buf 제자리 수정).\n * 짝수·홀수 정렬을 모두 본다 — 길이 접두 문자열이 홀수 오프셋에서 시작하는 레코드가 있다(필드 명령).\n * 엉뚱한 정렬로 읽은 ASCII 문자열은 U+3xxx 대 한자처럼 풀려 숫자 패턴이 생기지 않는다.\n * skipBinding: 데이터 연결 경로 조각은 건너뛴다 (HWP5 셀·필드 레코드용)\n */\nexport function scrubUtf16Runs(\n buf: Buffer, ctx: ScrubCtx, mode: \"mask\" | \"check\", onHit: (h: RedactHit) => void, skipBinding = false,\n): boolean {\n let changed = false\n const mc = ctx.maskChar.charCodeAt(0)\n // 조각은 6글자부터 룰 탐지. 본문에서 찾은 짧은 이름 리터럴(\"홍길동\")이 있으면 2~5글자 조각은 리터럴만\n // 본다(요약 정보 작성자 칸 등) — 짧은 조각에 룰을 돌릴 문맥은 없다\n const minRun = ctx.literals.some((l) => l.norm.length < 6) ? 2 : 6\n const literalOnly: ScrubCtx = { ...ctx, rules: [] }\n const binCtx = binaryCtx(ctx)\n for (let align = 0; align < 2; align++) {\n let runStart = -1\n let text = \"\"\n const flush = (): void => {\n if (runStart >= 0 && text.length >= minRun && !(skipBinding && isBindingPath(text))) {\n for (const h of findPii(text, text.length >= 6 ? binCtx : literalOnly)) {\n onHit(h)\n if (mode !== \"mask\") continue\n const orig = text.slice(h.index, h.index + h.length)\n for (let k = 0; k < h.length; k++) {\n if (h.masked[k] !== orig[k]) { buf.writeUInt16LE(mc, runStart + (h.index + k) * 2); changed = true }\n }\n }\n }\n runStart = -1\n text = \"\"\n }\n for (let off = align; off + 1 < buf.length; off += 2) {\n const u = buf.readUInt16LE(off)\n if (isTextUnit(u)) {\n if (runStart < 0) runStart = off\n text += String.fromCharCode(u)\n } else flush()\n }\n flush()\n }\n return changed\n}\n\n/**\n * 1바이트 문자열(ASCII·CP949 의 ASCII 부분) 조각 — VT_LPSTR 메타데이터·스크립트용. 멀티바이트 마스크\n * 문자는 길이를 바꾸므로 여기서는 \"*\" 로 가린다.\n */\nexport function scrubAsciiRuns(buf: Buffer, ctx: ScrubCtx, mode: \"mask\" | \"check\", onHit: (h: RedactHit) => void): boolean {\n let changed = false\n let runStart = -1\n const byteCtx: ScrubCtx = { ...binaryCtx(ctx), maskChar: \"*\" }\n const flush = (end: number): void => {\n if (runStart >= 0 && end - runStart >= 6) {\n const text = buf.toString(\"latin1\", runStart, end)\n for (const h of findPii(text, byteCtx)) {\n onHit({ ...h, masked: h.masked.split(\"*\").join(ctx.maskChar) })\n if (mode !== \"mask\") continue\n for (let k = 0; k < h.length; k++) {\n if (h.masked[k] !== text[h.index + k]) { buf[runStart + h.index + k] = 0x2a; changed = true }\n }\n }\n }\n runStart = -1\n }\n for (let i = 0; i < buf.length; i++) {\n const c = buf[i]\n if (c >= 0x20 && c < 0x7f) { if (runStart < 0) runStart = i } else flush(i)\n }\n flush(buf.length)\n return changed\n}\n\n/** 컨테이너 한 번 훑은 결과 (mask 모드는 data 가 수정본, check 모드는 입력 그대로) */\nexport interface ScrubResult {\n data: Uint8Array\n hits: RedactFileHit[]\n warnings: string[]\n /** 글자가 들어 있을 자리인데 읽지 못한 파트 (인코딩 불명 텍스트 엔트리 등) — 잔존과 같은 실패로 다룬다 */\n unscanned: string[]\n}\n\n/** NUL 이 섞인 텍스트 엔트리가 BOM 없는 UTF-16LE 인가 — 한컴 Scripts/*.js 전량이 이 꼴(코퍼스 213건) */\nexport function looksUtf16le(b: Uint8Array): boolean {\n if (b.length < 2 || b.length % 2 !== 0) return false\n let text = 0\n let asciiHi0 = 0\n const n = b.length / 2\n for (let i = 0; i < b.length; i += 2) {\n const u = b[i] | (b[i + 1] << 8)\n if (isTextUnit(u) || u === 0x0a || u === 0x0d || u === 0xfeff) text++\n if (b[i + 1] === 0 && b[i] !== 0) asciiHi0++\n }\n return text / n >= 0.95 && asciiHi0 > 0\n}\n\n// ─── 공용: 이미지 메타데이터 (XMP·JPEG 주석·PNG 텍스트) ──────\n\n/** UTF-8 바이트 → 글자별 바이트 위치가 붙은 문자열 (잘못된 바이트는 U+FFFD 한 글자) */\nfunction utf8WithOffsets(b: Buffer, start: number, end: number): { text: string; at: number[] } {\n const text: string[] = []\n const at: number[] = []\n let i = start\n while (i < end) {\n const c = b[i]\n const len = c < 0x80 ? 1 : c >= 0xf0 ? 4 : c >= 0xe0 ? 3 : c >= 0xc0 ? 2 : 1\n const ch = len === 1 && c >= 0x80 ? \"\\ufffd\" : b.toString(\"utf8\", i, Math.min(end, i + len))\n for (let k = 0; k < ch.length; k++) { text.push(ch[k]); at.push(i) }\n i += len\n }\n return { text: text.join(\"\"), at }\n}\n\n/** 텍스트 구간(바이트) 탐지 → ASCII 자리만 \"*\" 로 (바이트 길이 불변) */\nfunction scrubTextBytes(b: Buffer, start: number, end: number, ctx: ScrubCtx, mode: \"mask\" | \"check\", onHit: (h: RedactHit) => void): boolean {\n const { text, at } = utf8WithOffsets(b, start, end)\n let changed = false\n for (const h of findPii(text, { ...ctx, maskChar: \"*\" })) {\n onHit({ ...h, masked: h.masked.split(\"*\").join(ctx.maskChar) })\n if (mode !== \"mask\") continue\n for (let k = 0; k < h.length; k++) {\n const pos = at[h.index + k]\n if (h.masked[k] !== text[h.index + k] && b[pos] < 0x80) { b[pos] = 0x2a; changed = true }\n }\n }\n return changed\n}\n\nconst XMP_HEADER = \"http://ns.adobe.com/xap/1.0/\\0\"\n\n/**\n * 이미지 메타데이터의 글자 — JPEG XMP(APP1, 포토샵 레이어 이름·설명 등)·JPEG 주석(COM)·PNG tEXt/iTXt.\n * 같은 길이로 가린다(ASCII 자리만 \"*\"). PNG 는 청크 CRC 를 다시 계산한다. 압축 텍스트(zTXt·압축 iTXt)는\n * 가리지 않고 잔존으로 보고되게 탐지만 한다. 화소 데이터는 보지 않는다(오탐·손상 방지). buf 제자리 수정.\n */\nexport function scrubImageMeta(buf: Buffer, ctx: ScrubCtx, mode: \"mask\" | \"check\", onHit: (h: RedactHit) => void): boolean {\n let changed = false\n if (buf.length > 4 && buf[0] === 0xff && buf[1] === 0xd8) {\n for (let o = 2; o + 4 <= buf.length;) {\n if (buf[o] !== 0xff) break\n const m = buf[o + 1]\n if (m === 0xd8 || m === 0x01 || (m >= 0xd0 && m <= 0xd7)) { o += 2; continue }\n const len = buf.readUInt16BE(o + 2)\n const segStart = o + 4\n const segEnd = Math.min(buf.length, o + 2 + len)\n if (m === 0xe1 && buf.toString(\"latin1\", segStart, segStart + XMP_HEADER.length) === XMP_HEADER) {\n if (scrubTextBytes(buf, segStart + XMP_HEADER.length, segEnd, ctx, mode, onHit)) changed = true\n } else if (m === 0xfe) {\n if (scrubTextBytes(buf, segStart, segEnd, ctx, mode, onHit)) changed = true\n }\n if (m === 0xda) break // 영상 데이터 시작 — 메타데이터 끝\n o += 2 + len\n }\n } else if (buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47) {\n for (let o = 8; o + 12 <= buf.length;) {\n const len = buf.readUInt32BE(o)\n const type = buf.toString(\"latin1\", o + 4, o + 8)\n const dStart = o + 8\n const dEnd = Math.min(buf.length, dStart + len)\n let chunkChanged = false\n if (type === \"tEXt\" || type === \"iTXt\") {\n const k = buf.indexOf(0, dStart)\n let textStart = k + 1\n if (type === \"iTXt\" && k >= 0) {\n const compressed = buf[k + 1] === 1\n const lang = buf.indexOf(0, k + 3)\n const trans = lang >= 0 ? buf.indexOf(0, lang + 1) : -1\n textStart = trans + 1\n if (compressed) {\n // 압축 텍스트는 제자리에서 못 가린다 — 재검사(check)에서만 보고해 잔존으로 드러나게\n if (mode === \"check\") {\n try { for (const h of findPii(inflateSync(buf.subarray(textStart, dEnd)).toString(\"utf8\"), ctx)) onHit(h) } catch { /* 깨진 청크 */ }\n }\n textStart = -1\n }\n }\n if (k >= 0 && textStart > 0 && textStart <= dEnd) chunkChanged = scrubTextBytes(buf, textStart, dEnd, ctx, mode, onHit)\n } else if (type === \"zTXt\" && mode === \"check\") {\n const k = buf.indexOf(0, dStart)\n try {\n for (const h of findPii(inflateSync(buf.subarray(k + 2, dEnd)).toString(\"latin1\"), ctx)) onHit(h)\n } catch { /* 깨진 청크 */ }\n }\n if (chunkChanged) {\n changed = true\n buf.writeUInt32BE(crc32(buf.subarray(o + 4, dEnd)), dEnd)\n }\n if (type === \"IEND\") break\n o += 12 + len\n }\n }\n return changed\n}\n\n// ─── 공용: 삽입 개체(BinData) 판별·재검사 ─────────────────\n\nconst OLE_SIG = Buffer.from(\"d0cf11e0a1b11ae1\", \"hex\")\n\n/** 삽입 OLE 개체 — HWP·HWPX BinData 의 .ole 은 4바이트 크기 + OLE 복합 파일 */\nexport function isOleBin(b: Buffer): boolean {\n const i = b.indexOf(OLE_SIG)\n return i >= 0 && i < 16\n}\n\n/** 메타데이터 글자를 가릴 수 있는 그림 — JPEG·PNG */\nexport function isMetaImage(b: Buffer): boolean {\n return (b.length > 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) || (b.length > 8 && b.readUInt32BE(0) === 0x89504e47)\n}\n\n/** EMF 메타파일 — 글자 레코드(EXTTEXTOUTW)가 UTF-16 이다 */\nexport function isEmf(b: Buffer): boolean {\n return b.length > 44 && b.readUInt32LE(0) === 1 && b.readUInt32LE(40) === 0x464d4520\n}\n\nconst decodeXmlEntities = (s: string): string => s\n .replace(/&#x([0-9a-fA-F]+);/g, (e, h) => { const c = parseInt(h, 16); return c <= 0x10ffff ? String.fromCodePoint(c) : e })\n .replace(/&#(\\d+);/g, (e, d) => { const c = Number(d); return c <= 0x10ffff ? String.fromCodePoint(c) : e })\n .replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\").replace(/&quot;/g, '\"').replace(/&apos;/g, \"'\").replace(/&amp;/g, \"&\")\n\n/** XML 글자(태그 묶음은 공백 하나로 — run 에 갈린 번호도 이어진다)와 속성값 — 재검사 전용 */\nfunction checkXmlText(xml: string, ctx: ScrubCtx, onHit: (h: RedactHit) => void): void {\n for (const h of findPii(decodeXmlEntities(xml.replace(/(?:<[^>]*>)+/g, \" \")), ctx)) onHit(h)\n for (const m of xml.matchAll(/=\\s*\"([^\"]{5,})\"/g)) for (const h of findPii(decodeXmlEntities(m[1]), ctx)) onHit(h)\n}\n\n/**\n * 삽입 OLE 개체 재검사 — 제자리에서 가릴 수 없어 찾기만 한다(잔존 보고용). 안쪽 스트림을 열어 XML(차트\n * OOXMLChartContents)·ZIP(삽입 OOXML 문서 Package)은 글자와 속성값을, 엑셀 Workbook 은 UTF-16 과 1바이트\n * 문자열(숫자·이메일은 1바이트로 저장)을, 그 밖(차트 Contents·OlePres 메타파일·Ole10Native)은 UTF-16\n * 문자열 조각을 본다. 메타파일·그림의 1바이트 조각은 화소 자료에서 오탐이 많아 보지 않는다.\n */\nexport async function checkEmbeddedOle(data: Buffer, ctx: ScrubCtx, onHit: (h: RedactHit) => void): Promise<void> {\n let inner: ReturnType<typeof CFB.parse>\n try { inner = CFB.parse(data.subarray(data.indexOf(OLE_SIG))) } catch { scrubUtf16Runs(Buffer.from(data), ctx, \"check\", onHit); return }\n for (let i = 0; i < inner.FullPaths.length; i++) {\n const e = inner.FileIndex[i]\n if (e.type !== 2 || !e.content) continue\n const c = Buffer.from(e.content)\n if (c.length >= 4 && c.readUInt32LE(0) === 0x04034b50) {\n let zip: JSZip | null = null\n try { zip = await JSZip.loadAsync(c) } catch { /* 깨진 ZIP — 원시 조각만 */ }\n if (!zip) { scrubUtf16Runs(c, ctx, \"check\", onHit); continue }\n for (const [name, z] of Object.entries(zip.files)) {\n if (z.dir) continue\n const b = Buffer.from(await z.async(\"uint8array\"))\n if (/\\.(xml|rels|vml)$/i.test(name)) checkXmlText(b.toString(\"utf8\"), ctx, onHit)\n else scrubUtf16Runs(b, ctx, \"check\", onHit)\n }\n } else if (/^(?:\\xef\\xbb\\xbf)?\\s*<(?:\\?xml|[A-Za-z])/.test(c.toString(\"latin1\", 0, 64))) {\n checkXmlText(c.toString(\"utf8\"), ctx, onHit)\n } else {\n scrubUtf16Runs(c, ctx, \"check\", onHit)\n if (/(?:^|\\/)(?:Workbook|Book)$/.test(inner.FullPaths[i])) scrubAsciiRuns(c, ctx, \"check\", onHit)\n }\n }\n}\n","/**\n * HWPX 컨테이너 PII 수술 — ZIP 안 모든 글자 저장소를 같은 길이 글자 치환으로 가린다.\n *\n * 1) XML 문단(<hp:p>) — 본문·표·중첩표·글상자·머리말/꼬리말·각주/미주·캡션·메모. 문단의 <hp:t> 조각을\n * 파서처럼 이어 붙여(run 경계·필드·조판 제어·떠 있는 개체는 투명, 탭·줄바꿈·글자처럼 취급한 표·수식·\n * 각주 표지·변경 추적 표지는 공백) 탐지하므로 run 이 갈라 놓은 번호도 잡고, 지운 글자와 넣은 글자가\n * 붙어 번호가 안 보이는 일(변경 추적)도 없다. 가린 글자 자리만 splice (한 번호 안 이어진 글자는 splice\n * 하나, 엔트리마다 한 번에 적용) — run·charPr·탭은 그대로 (patchHwpx 의 문단 통째 재작성과 다름)\n * 2) 문단에 속하지 않은 텍스트 노드(주석으로 갈린 조각은 이어서)·주석·처리 지시·모든 속성값 — 필드 명령\n * (하이퍼링크 mailto·누름틀 안내문), 개체 설명, content.hpf 메타데이터 등. 서식 데이터 연결 경로\n * (\"./DataArea/…@code.Name\")·줄 배치 캐시 속성은 건너뛴다\n * 3) Preview/PrvText.txt(첫 쪽 텍스트 캐시)·Scripts(BOM 없는 UTF-16LE) 등 텍스트 엔트리 — 인코딩 보존\n * 4) Preview/PrvImage.*(첫 쪽 렌더 724×1024) — PII 를 가린 문서면 같은 크기 흰 이미지로 교체\n * 5) BinData — 그림 메타데이터(XMP·JPEG 주석·PNG 텍스트)는 가리고, SVG 는 XML 로, 삽입 OLE 개체(차트\n * XML·삽입 문서·엑셀 포함)와 EMF 글자는 재검사에서 보고(가릴 수 없음 → 잔존), 화소는 미검사로 알린다\n * 글자를 바꾼 XML 은 linesegarray(줄 배치 캐시)를 지운다 — patchHwpx 와 같은 이유(한컴 변조 경고 방지).\n * 바꾼 엔트리만 교체하고 나머지 ZIP 엔트리는 원본 바이트 그대로 (patchZipEntries).\n */\n\nimport JSZip from \"jszip\"\nimport {\n scanSectionXml, findElementEnd, allLinesegRemovalSplices,\n type ScanParagraph, type ScanTable, type SpliceEdit,\n} from \"./roundtrip/source-map.js\"\nimport { patchZipEntries } from \"./roundtrip/zip-patch.js\"\nimport {\n findPii, findLiterals, blankPreviewImage, isBindingPath, looksUtf16le, scrubImageMeta, scrubUtf16Runs,\n checkEmbeddedOle, isOleBin, isMetaImage, isEmf,\n type ScrubCtx, type ScrubResult, type RedactFileHit, type RedactWhere,\n} from \"./redact-scrub.js\"\nimport { normalizeForDetect, type RedactHit } from \"./redact.js\"\n\ntype Pos = [number, number] | null\n\n/** 글자별 XML 위치가 붙은 텍스트 — pos[i] = text[i] 가 나온 원문 범위(엔티티면 &…; 전체), 가상 공백은 null */\ninterface Mapped { text: string; pos: Pos[] }\n\n/** 공백처럼 읽히는 인라인 요소 (파서도 공백으로 본다) */\nconst WS_TAGS = new Set([\"tab\", \"lineBreak\", \"br\", \"fwSpace\", \"hwSpace\", \"nbSpace\"])\n\n/**\n * 문단 t 조각 사이에서 글자를 끊는 요소 — 파서가 공백·줄바꿈·표 경계·수식을 내는 요소와 변경 추적 표지\n * (지운 글과 넣은 글을 둘 다 가리므로 둘이 붙으면 안 된다). 그 밖(run 경계·필드·책갈피·쪽 번호 등 조판\n * 제어·그림·각주처럼 따로 흐르는 개체)은 파서처럼 투명 — 파서 본문과 같은 글자열을 봐야 본문에서 찾은\n * 값을 파일에서도 찾는다\n */\nconst GAP_SEPARATORS = new Set([...WS_TAGS, \"deleteBegin\", \"deleteEnd\", \"insertBegin\", \"insertEnd\"])\n\n/**\n * 문단 흐름 밖 개체 — 속 글자(표 셀·글상자·각주 문단)는 따로 본다. 파서처럼 문단 글자를 끊지 않되,\n * 글자처럼 취급한 표(treatAsChar=\"1\" — 파서가 경계 표지를 낸다)·수식(LaTeX 를 낸다)·각주/미주(번호\n * 표지 \"1)\" 을 낸다)는 끊는다\n */\nconst FLOW_OBJECTS = new Set([\n \"tbl\", \"pic\", \"rect\", \"ellipse\", \"line\", \"arc\", \"polygon\", \"curve\", \"connectLine\", \"container\", \"ole\", \"chart\",\n \"textart\", \"video\", \"equation\", \"footNote\", \"endNote\", \"header\", \"footer\", \"hiddenComment\", \"memo\", \"memogroup\",\n])\n\n/** <hp:t> 안에서 글자를 끊지 않는 요소 — 형광펜·소프트 하이픈·제목 차례 표시. 그 밖(변경 추적 등)은 구분자 */\nconst T_INNER_TRANSPARENT = new Set([\"markpenBegin\", \"markpenEnd\", \"hyphen\", \"titleMark\", \"indexmark\"])\n\nconst ENTITY_RE = /&(?:#x[0-9a-fA-F]+|#\\d+|[a-zA-Z]+);/y\n\nfunction decodeEntity(ent: string): string {\n switch (ent) {\n case \"&lt;\": return \"<\"\n case \"&gt;\": return \">\"\n case \"&amp;\": return \"&\"\n case \"&quot;\": return '\"'\n case \"&apos;\": return \"'\"\n }\n const m = /^&#(x?)([0-9a-fA-F]+);$/.exec(ent)\n if (m) {\n const code = parseInt(m[2], m[1] ? 16 : 10)\n if (code >= 0 && code <= 0x10ffff) return String.fromCodePoint(code)\n }\n return ent\n}\n\n/** 원문 [start,end) 텍스트(태그 없음)를 엔티티 풀어 글자별 위치와 함께 out 에 붙인다 */\nfunction appendDecoded(xml: string, start: number, end: number, out: Mapped): void {\n let i = start\n while (i < end) {\n if (xml[i] === \"&\") {\n ENTITY_RE.lastIndex = i\n const m = ENTITY_RE.exec(xml)\n if (m && i + m[0].length <= end) {\n const dec = decodeEntity(m[0])\n for (let k = 0; k < dec.length; k++) {\n out.text += dec[k]\n out.pos.push(k === 0 && dec.length === 1 ? [i, i + m[0].length] : null)\n }\n i += m[0].length\n continue\n }\n }\n out.text += xml[i]\n out.pos.push([i, i + 1])\n i++\n }\n}\n\n/** 원문 [start,end) 를 엔티티 해석 없이 글자 그대로 (주석·처리 지시·CDATA) */\nfunction appendRaw(xml: string, start: number, end: number, out: Mapped): void {\n for (let i = start; i < end; i++) {\n out.text += xml[i]\n out.pos.push([i, i + 1])\n }\n}\n\nconst localName = (qname: string): string => qname.slice(qname.indexOf(\":\") + 1)\n\n/**\n * <hp:t> 콘텐츠(내부 태그 가능) → 글자별 위치. 공백류·변경 추적 등은 가상 공백, 형광펜·하이픈은 투명,\n * CDATA 는 글자 그대로, 주석·처리 지시는 글자가 아니라 투명 (안쪽 글자는 느슨한 검사가 따로 본다)\n */\nfunction appendTContent(xml: string, start: number, end: number, out: Mapped): void {\n const tokRe = /<!--[\\s\\S]*?-->|<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>|<\\?[\\s\\S]*?\\?>|<\\/?([^\\s/>!?]+)[^>]*>/g\n tokRe.lastIndex = start\n let cursor = start\n let m: RegExpExecArray | null\n while ((m = tokRe.exec(xml)) !== null && m.index < end) {\n appendDecoded(xml, cursor, m.index, out)\n if (m[1] !== undefined) appendRaw(xml, m.index + 9, m.index + 9 + m[1].length, out)\n else if (m[2] !== undefined && !T_INNER_TRANSPARENT.has(localName(m[2]))) { out.text += \" \"; out.pos.push(null) }\n cursor = m.index + m[0].length\n }\n appendDecoded(xml, cursor, end, out)\n}\n\n/** 같은 문단 두 t 조각 사이 원문에 글자를 끊는 요소(탭·줄바꿈·글자처럼 취급한 표·수식·변경 추적)가 있는가 */\nfunction gapSeparates(gap: string): boolean {\n const tagRe = /<(\\/?)([^\\s/>!?]+)(?:\"[^\"]*\"|'[^']*'|[^>\"'])*>/g\n let m: RegExpExecArray | null\n while ((m = tagRe.exec(gap)) !== null) {\n const l = localName(m[2])\n if (m[1] === \"\" && FLOW_OBJECTS.has(l) && !m[0].endsWith(\"/>\")) {\n if (l === \"equation\" || l === \"footNote\" || l === \"endNote\") return true\n const end = findElementEnd(gap, m.index)\n if (end < 0) return true\n const pos = l === \"tbl\" ? /<(?:[\\w]+:)?pos\\b[^>]*>/.exec(gap.slice(m.index, end))?.[0] : undefined\n if (pos && /\\btreatAsChar=\"1\"/.test(pos)) return true\n tagRe.lastIndex = end // 개체 속(셀·글상자·각주 문단)은 건너뛴다\n continue\n }\n if (GAP_SEPARATORS.has(l)) return true\n }\n return false\n}\n\nfunction paragraphText(xml: string, para: ScanParagraph): Mapped {\n const out: Mapped = { text: \"\", pos: [] }\n let prevEnd = -1\n for (const t of para.tRanges) {\n if (prevEnd >= 0 && gapSeparates(xml.slice(prevEnd, t.contentStart))) { out.text += \" \"; out.pos.push(null) }\n if (!t.selfClosing) appendTContent(xml, t.contentStart, t.contentEnd, out)\n prevEnd = t.contentEnd\n }\n return out\n}\n\n/** 스캔 결과의 모든 문단 (본문·글상자·머리말류·표 셀·중첩표) */\nfunction allParagraphs(scan: ReturnType<typeof scanSectionXml>): ScanParagraph[] {\n const out = [...scan.bodyParagraphs, ...scan.excludedParagraphs]\n const walk = (t: ScanTable, depth: number): void => {\n if (depth > 32) return\n for (const row of t.rows) for (const cell of row) {\n out.push(...cell.paragraphs)\n for (const n of cell.tables) walk(n, depth + 1)\n }\n }\n for (const t of [...scan.tables, ...scan.orphanTables]) walk(t, 0)\n return out\n}\n\nconst CONTAINER_WHERE: Record<string, RedactWhere> = {\n header: \"header\", footer: \"footer\", footNote: \"footnote\", endNote: \"endnote\", fn: \"footnote\", en: \"endnote\",\n caption: \"caption\", memo: \"memo\", memogroup: \"memo\", hiddenComment: \"hidden\",\n}\n\n/** 머리말·각주 등 컨테이너 범위 목록 — excluded 문단 위치 분류용 */\nfunction containerRanges(xml: string): Array<{ start: number; end: number; where: RedactWhere }> {\n const out: Array<{ start: number; end: number; where: RedactWhere }> = []\n for (const m of xml.matchAll(/<((?:[\\w]+:)?(header|footer|footNote|endNote|caption|memogroup|memo|hiddenComment))[\\s>]/g)) {\n const end = findElementEnd(xml, m.index as number)\n if (end > 0) out.push({ start: m.index as number, end, where: CONTAINER_WHERE[m[2]] ?? \"other\" })\n }\n return out\n}\n\nfunction paragraphWhere(para: ScanParagraph, ranges: ReturnType<typeof containerRanges>): RedactWhere {\n if (para.kind === \"body\") return \"body\"\n if (para.kind === \"draw\") return \"textbox\"\n if (para.kind === \"cell\") return para.inTextbox ? \"textbox\" : \"table\"\n let best: { start: number; where: RedactWhere } | undefined\n for (const r of ranges) {\n if (r.start < para.start && para.start < r.end && (!best || r.start > best.start)) best = r\n }\n return best?.where ?? \"other\"\n}\n\n/**\n * 텍스트 한 단위 탐지 → 리포트 + splice. 한 번호 안에서 원문이 이어진 글자들은 splice 하나로 묶는다\n * (명단 2,000행에서 글자마다 splice 하던 것이 이차 시간이었다).\n */\nfunction maskMapped(m: Mapped, ctx: ScrubCtx, onHit: (h: RedactHit) => void, splices: SpliceEdit[] | null): void {\n for (const h of findPii(m.text, ctx)) {\n onHit(h)\n if (!splices) continue\n let run: SpliceEdit | null = null\n for (let k = 0; k < h.length; k++) {\n const p = m.pos[h.index + k]\n if (!p || h.masked[k] === m.text[h.index + k]) { run = null; continue }\n if (run && run.end === p[0]) { run.end = p[1]; run.replacement += ctx.maskChar }\n else { run = { start: p[0], end: p[1], replacement: ctx.maskChar }; splices.push(run) }\n }\n }\n}\n\n/** splice 일괄 적용 — 정렬 후 한 번에 이어 붙인다 (선형 시간). 겹치면 내부 오류 */\nfunction applySplicesLinear(src: string, splices: SpliceEdit[]): string {\n const sorted = [...splices].sort((a, b) => a.start - b.start || a.end - b.end)\n const parts: string[] = []\n let cursor = 0\n for (const s of sorted) {\n if (s.start < cursor) throw new Error(\"마스킹 splice 범위 겹침 — 내부 오류\")\n parts.push(src.slice(cursor, s.start), s.replacement)\n cursor = s.end\n }\n parts.push(src.slice(cursor))\n return parts.join(\"\")\n}\n\n/** 줄 배치 캐시 등 구조 속성만 있는 요소 — 속성값 검사를 건너뛴다 */\nconst STRUCTURAL_ELEMENTS = new Set([\"lineseg\", \"linesegarray\"])\n\n/**\n * 문단이 소유하지 않은 텍스트·속성값 탐지. owned = 문단 t 콘텐츠 범위(이미 문단 단위로 처리).\n * 요소 태그 사이 텍스트 노드는 주석·처리 지시로 갈려도 이어서 본다(tel:010-2345<!--x-->-6789).\n * 주석·처리 지시·DOCTYPE 의 안쪽 글자도 따로 본다. 위치 분류는 가장 가까운 여는 요소 이름으로.\n */\nfunction scrubLooseText(\n xml: string, owned: Array<[number, number]>, ctx: ScrubCtx, partWhere: RedactWhere | null,\n report: (h: RedactHit, where: RedactWhere) => void, splices: SpliceEdit[] | null,\n): void {\n const tokenRe = /<!--([\\s\\S]*?)-->|<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>|<\\?([\\s\\S]*?)\\?>|<!((?:\"[^\"]*\"|'[^']*'|[^>\"'])*)>|<(\\/?)([^\\s/>!?]+)((?:\"[^\"]*\"|'[^']*'|[^>\"'])*?)(\\/?)>|([^<]+)/g\n const stack: string[] = []\n // 짧은 글(5자 미만)·영숫자 없는 속성값은 번호가 못 들어간다 — 본문에서 찾은 짧은 이름 리터럴이 있으면 두 글자부터\n // 본다 (content.hpf 작성자 \"홍길동\" 등)\n const shortLits = ctx.literals.some((l) => l.norm.length < 6)\n const minText = shortLits ? 2 : 5\n let oi = 0\n let pending: Mapped | null = null\n let pendingWhere: RedactWhere = \"other\"\n const whereOf = (fallback: RedactWhere): RedactWhere => {\n if (partWhere) return partWhere\n for (let i = stack.length - 1; i >= 0; i--) {\n const l = stack[i]\n if (l === \"stringParam\" || l === \"fieldBegin\" || l === \"parameters\") return \"field\"\n if (l === \"shapeComment\") return \"attribute\"\n if (CONTAINER_WHERE[l]) return CONTAINER_WHERE[l]\n }\n return fallback\n }\n const flush = (): void => {\n if (pending && pending.text.length >= minText && !isBindingPath(pending.text)) {\n const where = pendingWhere\n maskMapped(pending, ctx, (h) => report(h, where), splices)\n }\n pending = null\n }\n const addText = (fill: (out: Mapped) => void): void => {\n if (!pending) { pending = { text: \"\", pos: [] }; pendingWhere = whereOf(\"other\") }\n fill(pending)\n }\n const scanInner = (start: number, text: string): void => {\n const mapped: Mapped = { text: \"\", pos: [] }\n appendRaw(xml, start, start + text.length, mapped)\n maskMapped(mapped, ctx, (h) => report(h, partWhere ?? \"other\"), splices)\n }\n const isOwned = (at: number): boolean => {\n while (oi < owned.length && owned[oi][1] <= at) oi++\n return oi < owned.length && owned[oi][0] <= at && at < owned[oi][1]\n }\n for (const m of xml.matchAll(tokenRe)) {\n const at = m.index as number\n if (m[9] !== undefined || m[2] !== undefined) {\n // 텍스트 노드·CDATA — 문단 소유 범위(<hp:t> 안)면 문단 단위로 이미 봤다: 그 앞에서 끊고 건너뜀\n if (isOwned(at)) { flush(); continue }\n if (m[9] !== undefined) addText((out) => appendDecoded(xml, at, at + m[9].length, out))\n else addText((out) => appendRaw(xml, at + 9, at + 9 + m[2].length, out))\n continue\n }\n if (m[1] !== undefined) { scanInner(at + 4, m[1]); continue } // 주석 — 안쪽 글자 따로, 텍스트는 이어 봄\n if (m[3] !== undefined) { scanInner(at + 2, m[3]); continue } // 처리 지시\n if (m[4] !== undefined) { flush(); scanInner(at + 2, m[4]); continue } // DOCTYPE·ENTITY 선언\n if (m[6] === undefined) continue\n flush()\n const local = localName(m[6])\n if (m[5] === \"/\") {\n const i = stack.lastIndexOf(local)\n if (i >= 0) stack.length = i\n continue\n }\n // 속성값 — 정수만인 값도 본다(누름틀 이름 \"01023456789\"). 데이터 연결 경로·줄 배치 캐시는 건너뜀\n const attrs = m[7] ?? \"\"\n if (attrs.includes(\"=\") && !STRUCTURAL_ELEMENTS.has(local)) {\n const attrBase = at + 1 + m[6].length\n for (const a of attrs.matchAll(/([^\\s=]+)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')/g)) {\n const val = a[2] ?? a[3] ?? \"\"\n if (val.length < minText || (!shortLits && !/[0-9A-Za-z]/.test(val)) || isBindingPath(val)) continue\n const valStart = attrBase + (a.index as number) + a[0].length - val.length - 1\n const mapped: Mapped = { text: \"\", pos: [] }\n appendDecoded(xml, valStart, valStart + val.length, mapped)\n maskMapped(mapped, ctx, (h) => report(h, partWhere ?? \"attribute\"), splices)\n }\n }\n if (m[8] !== \"/\") stack.push(local)\n }\n flush()\n}\n\n/** XML 한 엔트리 수술 — 문단 단위 + 느슨한 텍스트/속성. 바뀐 게 없으면 null */\nfunction scrubXml(\n xml: string, part: string, ctx: ScrubCtx, mode: \"mask\" | \"check\", hits: RedactFileHit[],\n): string | null {\n const splices: SpliceEdit[] | null = mode === \"mask\" ? [] : null\n const partWhere: RedactWhere | null = /content\\.hpf$/i.test(part) ? \"metadata\" : null\n const owned: Array<[number, number]> = []\n if (/<(?:[\\w]+:)?p[\\s>]/.test(xml) && /<(?:[\\w]+:)?t[\\s>/]/.test(xml)) {\n const scan = scanSectionXml(xml, 0)\n const ranges = containerRanges(xml)\n for (const para of allParagraphs(scan)) {\n for (const t of para.tRanges) owned.push([t.contentStart, t.contentEnd])\n const where = paragraphWhere(para, ranges)\n maskMapped(paragraphText(xml, para), ctx, (h) => hits.push({ rule: h.rule, masked: h.masked, part, where }), splices)\n }\n owned.sort((a, b) => a[0] - b[0])\n }\n scrubLooseText(xml, owned, ctx, partWhere, (h, where) => hits.push({ rule: h.rule, masked: h.masked, part, where }), splices)\n if (!splices || splices.length === 0) return null\n if (xml.includes(\"linesegarray\")) splices.push(...allLinesegRemovalSplices(xml))\n return applySplicesLinear(xml, splices)\n}\n\n/** 텍스트 엔트리(Preview/PrvText.txt·Scripts 등) — 줄 단위 탐지 */\nfunction scrubPlainText(text: string, part: string, ctx: ScrubCtx, mode: \"mask\" | \"check\", hits: RedactFileHit[]): string | null {\n const where: RedactWhere = /^Preview\\//i.test(part) ? \"preview\" : \"other\"\n let changed = false\n const lines = text.split(\"\\n\").map((line) => {\n const found = findPii(line, ctx)\n if (found.length === 0) return line\n let out = \"\"\n let cursor = 0\n for (const h of found) {\n hits.push({ rule: h.rule, masked: h.masked, part, where })\n out += line.slice(cursor, h.index) + h.masked\n cursor = h.index + h.length\n }\n changed = true\n return out + line.slice(cursor)\n })\n return mode === \"mask\" && changed ? lines.join(\"\\n\") : null\n}\n\nconst isXmlName = (n: string): boolean => /\\.(xml|hpf|rdf)$/i.test(n)\nconst isTextName = (n: string): boolean => /\\.(txt|js|json|csv|html?)$/i.test(n) || /^Scripts\\//i.test(n)\nconst isPreviewImage = (n: string): boolean => /^Preview\\/PrvImage\\./i.test(n)\nconst nameList = (names: string[]): string => names.length <= 5 ? names.join(\", \") : `${names.slice(0, 5).join(\", \")} 외 ${names.length - 5}개`\n\n/** 태그를 걷어 이은 글자에 리터럴(본문에서 찾은 값)이 남았는가 — 재검사 안전망 (문단 조립과 독립) */\nfunction literalLeftovers(xml: string, part: string, ctx: ScrubCtx, hits: RedactFileHit[]): void {\n if (ctx.literals.length === 0) return\n const stripped = normalizeForDetect(xml.replace(/<[^>]*>/g, \"\").replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\").replace(/&quot;/g, '\"').replace(/&apos;/g, \"'\").replace(/&amp;/g, \"&\"))\n for (const { lit } of findLiterals(stripped, ctx.literals)) {\n if (!hits.some((x) => x.part === part && x.masked === lit.masked)) hits.push({ rule: lit.rule, masked: lit.masked, part, where: \"other\" })\n }\n}\n\n/**\n * HWPX 한 파일을 훑는다. mask: 가린 파일 + 가린 곳 목록, check: 찾은 곳만 (data 는 입력 그대로 —\n * 가릴 수 없는 삽입 OLE 개체·압축 메타데이터 속 PII 도 여기서 잔존으로 드러난다).\n */\nexport async function scrubHwpx(original: Uint8Array, ctx: ScrubCtx, mode: \"mask\" | \"check\"): Promise<ScrubResult> {\n const zip = await JSZip.loadAsync(original)\n const hits: RedactFileHit[] = []\n const warnings: string[] = []\n const unscanned: string[] = []\n const replacements = new Map<string, Uint8Array>()\n const encoder = new TextEncoder()\n const previews: string[] = []\n const pictures: string[] = []\n const embedded: string[] = []\n\n /** 글자 엔트리 해독 — UTF-8, BOM 붙은 UTF-16LE, BOM 없는 UTF-16LE(한컴 Scripts). 모르면 null. 되쓸 때 같은 인코딩 */\n const decode = (bytes: Buffer): { text: string; encode: (s: string) => Uint8Array } | null => {\n if ((bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) || (bytes.includes(0) && looksUtf16le(bytes))) {\n return { text: bytes.toString(\"utf16le\"), encode: (x) => Buffer.from(x, \"utf16le\") }\n }\n return bytes.includes(0) ? null : { text: bytes.toString(\"utf8\"), encode: (x) => encoder.encode(x) }\n }\n\n for (const [name, entry] of Object.entries(zip.files)) {\n if (entry.dir || name === \"mimetype\") continue\n if (isPreviewImage(name)) { previews.push(name); continue }\n const bytes = Buffer.from(await entry.async(\"uint8array\"))\n if (isXmlName(name) || /^BinData\\/.*\\.svg$/i.test(name)) {\n const dec = decode(bytes)\n if (!dec) { unscanned.push(name); continue }\n const next = scrubXml(dec.text, name, ctx, mode, hits)\n if (next !== null) replacements.set(name, dec.encode(next))\n if (mode === \"check\") literalLeftovers(dec.text, name, ctx, hits)\n continue\n }\n if (/^BinData\\//i.test(name)) {\n const report = (where: RedactWhere) => (h: RedactHit): void => { hits.push({ rule: h.rule, masked: h.masked, part: name, where }) }\n if (isOleBin(bytes) || isEmf(bytes)) {\n // 삽입 OLE 개체(차트·삽입 문서·엑셀)·EMF — 제자리에서 못 가린다. 재검사에서 안의 글자를 보고해 잔존으로 드러낸다\n embedded.push(name)\n if (mode === \"check\") {\n if (isOleBin(bytes)) await checkEmbeddedOle(bytes, ctx, report(\"other\"))\n else scrubUtf16Runs(bytes, ctx, \"check\", report(\"other\"))\n }\n } else {\n pictures.push(name)\n if (isMetaImage(bytes) && scrubImageMeta(bytes, ctx, mode, report(\"metadata\"))) replacements.set(name, bytes)\n }\n continue\n }\n // 텍스트 엔트리(Preview/PrvText.txt·Scripts 등) — 같은 인코딩으로 되쓴다\n const dec = decode(bytes)\n if (dec) {\n const next = scrubPlainText(dec.text, name, ctx, mode, hits)\n if (next !== null) replacements.set(name, dec.encode(next))\n } else if (isTextName(name)) {\n unscanned.push(name) // 글자가 들어 있을 자리인데 인코딩을 몰라 못 읽음 — 잔존급 실패\n } else {\n warnings.push(`알 수 없는 바이너리 엔트리 ${name} 는 검사하지 못했습니다`)\n }\n }\n\n if (mode === \"mask\") {\n if (hits.length > 0 || ctx.literals.length > 0) {\n for (const name of previews) {\n const blank = blankPreviewImage(await zip.file(name)!.async(\"uint8array\"))\n if (blank) replacements.set(name, blank)\n else warnings.push(`${name}: 형식을 몰라 미리보기 이미지를 지우지 못했습니다 — 첫 쪽 렌더에 PII 가 보일 수 있습니다`)\n }\n if (previews.length > 0) warnings.push(`미리보기 이미지(${previews.join(\", \")})를 같은 크기의 흰 이미지로 바꿨습니다 — 한컴에서 저장하면 다시 만들어집니다`)\n }\n if (pictures.length > 0) warnings.push(`삽입 그림 ${pictures.length}개(${nameList(pictures)})는 화소 속 글자(스캔 문서 등)를 탐지하지 못합니다 — JPEG·PNG 메타데이터만 검사했습니다`)\n if (embedded.length > 0) warnings.push(`삽입 개체 ${embedded.length}개(${nameList(embedded)} — OLE·EMF)는 가리지 못합니다 — 안의 글자에 PII 가 있으면 재검사 잔존으로 보고됩니다`)\n if (unscanned.length > 0) warnings.push(`인코딩을 몰라 검사하지 못한 글자 엔트리: ${unscanned.join(\", \")}`)\n }\n\n // ZIP 원시 바이트는 따로 훑지 않는다 — 엔트리는 전부 압축 해제본으로 봤고, 압축 데이터를 1바이트\n // 조각으로 훑으면 \"8@F.eq\" 같은 난수 이메일 모양이 걸린다(실코퍼스 실측). 엔트리 사이 틈은 미검사.\n const data = mode === \"mask\" && replacements.size > 0 ? patchZipEntries(original, replacements) : original\n return { data, hits, warnings, unscanned }\n}\n","/**\n * HWP 5.x(OLE2) 컨테이너 PII 수술 — 모든 스트림의 글자를 같은 길이 UTF-16 코드 단위 치환으로 가린다.\n *\n * - BodyText/Section*: 레코드를 풀어 PARA_TEXT 는 문단 단위로(탭·줄바꿈·수식·자동 번호·각주 표지는 구분자,\n * 필드·책갈피·표·그림·조판 제어·하이픈 등 나머지는 파서처럼 투명) 탐지해 PII 글자 자리만 덮어쓴다. 머리말·꼬리말·각주·미주·\n * 표 셀·글상자·숨은 설명 문단도 전부 PARA_TEXT 라 같은 경로다. 그 밖의 레코드(필드 명령 문자열·\n * 개체 설명문 등)는 UTF-16 문자열 조각 단위로 훑는다(서식 데이터 연결 경로는 건너뜀). 레코드 크기·\n * nChars·글자모양 위치·LINE_SEG 는 그대로 — 길이가 같아 연쇄 갱신이 필요 없다\n * - DocInfo·Scripts·DocHistory 등 압축 스트림: 풀어서 문자열 조각 단위. 다시 압축할 때 한컴 꼬리\n * (CRC32+크기 8바이트)가 있던 스트림은 꼬리도 붙인다\n * - PrvText(첫 쪽 텍스트 캐시)·\\x05HwpSummaryInformation(제목 = 흔히 본문 첫 줄, 작성자, 설명):\n * UTF-16 문자열 조각 (+ VT_LPSTR 대비 1바이트 조각)\n * - PrvImage(첫 쪽 렌더): PII 를 가린 문서면 같은 크기 흰 이미지로 교체\n * - BinData: 풀어서 그림 메타데이터(XMP·JPEG 주석·PNG 텍스트)는 가리고, 삽입 OLE 개체(차트 XML·삽입\n * 문서·엑셀 포함)·EMF 의 글자는 재검사에서 보고한다(가릴 수 없음 → 잔존). 화소는 미검사로 알린다\n * - 미할당 섹터: 늘 0으로 비운다 — 한컴이 저장하며 남긴 옛 스트림 사본(압축 안 된 PrvText·압축된\n * BodyText)이 있다. 비운 자리에 있던 PII 는 \"slack\" 위치로 보고한다\n * 스트림 교체는 ole-surgeon(섹터 수술, 해제 섹터 0 채움)으로 — 나머지 바이트는 원본 그대로.\n * 배포용·암호·DRM 문서는 본문이 암호화돼 있어 거부한다.\n */\n\nimport { createRequire } from \"module\"\nimport { deflateRawSync, inflateRawSync, constants as zlibConstants } from \"zlib\"\nimport {\n decompressStream, parseFileHeader, readRecords, isExtendedOnlyCtrlChar,\n TAG_PARA_TEXT, TAG_CTRL_HEADER, TAG_CTRL_DATA,\n FLAG_COMPRESSED, FLAG_ENCRYPTED, FLAG_DISTRIBUTION, FLAG_DRM,\n} from \"./hwp5/record.js\"\nimport { replaceOleStream, wipeFreeSectors } from \"./roundtrip/ole-surgeon.js\"\nimport { compressWithTail } from \"./roundtrip/hwp5-patch.js\"\nimport {\n findPii, blankPreviewImage, crc32, scrubImageMeta, scrubUtf16Runs, scrubAsciiRuns,\n checkEmbeddedOle, isOleBin, isMetaImage, isEmf,\n type ScrubCtx, type ScrubResult, type RedactFileHit, type RedactWhere,\n} from \"./redact-scrub.js\"\nimport type { RedactHit } from \"./redact.js\"\n\nconst require = createRequire(import.meta.url)\nconst CFB: CfbModule = require(\"cfb\")\n\ninterface CfbEntry { name?: string; type?: number; content?: Buffer | Uint8Array; size?: number }\ninterface CfbContainer { FileIndex: CfbEntry[]; FullPaths: string[] }\ninterface CfbModule {\n parse(data: Buffer): CfbContainer\n find(cfb: CfbContainer, path: string): CfbEntry | null\n}\n\n/** 컨트롤 ID(4글자) → 그 안 문단의 위치 분류 */\nconst CTRL_WHERE: Record<string, RedactWhere> = {\n \"tbl \": \"table\", head: \"header\", foot: \"footer\", \"fn \": \"footnote\", \"en \": \"endnote\",\n \"gso \": \"textbox\", tcmt: \"hidden\",\n}\n\n/**\n * 문단 글자 흐름을 끊는 확장 컨트롤 — 파서(body.ts)가 그 자리에 글자를 내는 것: 수식(LaTeX)·자동 번호·\n * 각주/미주 번호 표지. 표·그림·필드·책갈피·구역/단 정의·머리말 등은 파서도 아무것도 내지 않아 투명\n */\nconst BREAKING_CTRLS = new Set([\"eqed\", \"atno\", \"fn \", \"en \"])\n\n/** CTRL_HEADER 첫 4바이트 → \"tbl \" 꼴 (파일엔 LE 로 뒤집혀 저장) */\nfunction ctrlName(data: Buffer): string {\n if (data.length < 4) return \"\"\n const id = data.readUInt32LE(0)\n return String.fromCharCode(id >>> 24, (id >>> 16) & 0xff, (id >>> 8) & 0xff, id & 0xff)\n}\n\n/**\n * PARA_TEXT 한 레코드 → 탐지용 텍스트 + 글자별 바이트 오프셋(-1 = 가상 구분자).\n * record.ts appendParaText 의 3분류(char 1단위 / inline·extended 8단위)를 그대로 따른다.\n */\nfunction decodeParaText(data: Buffer): { text: string; pos: number[] } {\n let text = \"\"\n const pos: number[] = []\n let i = 0\n const sep = (c = \" \"): void => { text += c; pos.push(-1) }\n while (i + 1 < data.length) {\n const ch = data.readUInt16LE(i)\n if (ch >= 0x20) { text += String.fromCharCode(ch); pos.push(i); i += 2; continue }\n if (ch === 0x0d) { sep(\"\\n\"); i += 2; continue }\n if (ch === 0x0a) {\n // 0x0a + 0x0b 수식 래퍼(확장 16바이트)는 개체 — 나머지는 강제 줄바꿈 1단위\n if (i + 18 <= data.length && data.readUInt16LE(i + 2) === 0x0b) { sep(); i += 18; continue }\n sep(\"\\n\"); i += 2; continue\n }\n const extended = isExtendedOnlyCtrlChar(ch)\n if ((extended || (ch >= 4 && ch <= 9) || ch === 19 || ch === 20) && i + 16 <= data.length) {\n // 탭과 파서가 글자를 내는 컨트롤만 끊는다 — 나머지(필드·책갈피·표·그림·구역/단 정의·머리말)는 파서\n // (record.ts appendParaText)처럼 투명: 누름틀·하이퍼링크 안 번호, 컨트롤이 끼어든 번호가 이어지게\n if (ch === 9 || (extended && BREAKING_CTRLS.has(ctrlName(data.subarray(i + 2, i + 6))))) sep()\n i += 16\n continue\n }\n if (ch === 0x1e || ch === 0x1f) sep() // 묶음·고정폭 빈칸\n i += 2 // 하이픈(24)·예약(25~29) 등은 투명\n }\n return { text, pos }\n}\n\ntype Report = (rule: RedactFileHit[\"rule\"], masked: string, where: RedactWhere) => void\n\nfunction scrubParaText(data: Buffer, ctx: ScrubCtx, mode: \"mask\" | \"check\", where: RedactWhere, report: Report): boolean {\n const { text, pos } = decodeParaText(data)\n let changed = false\n const mc = ctx.maskChar.charCodeAt(0)\n for (const h of findPii(text, ctx)) {\n report(h.rule, h.masked, where)\n if (mode !== \"mask\") continue\n for (let k = 0; k < h.length; k++) {\n const p = pos[h.index + k]\n if (p >= 0 && h.masked[k] !== text[h.index + k]) { data.writeUInt16LE(mc, p); changed = true }\n }\n }\n return changed\n}\n\n/** 레코드 스트림(BodyText 섹션·DocInfo) — data 는 압축 해제본, 제자리 수정 */\nfunction scrubRecords(data: Buffer, ctx: ScrubCtx, mode: \"mask\" | \"check\", report: Report): boolean {\n let changed = false\n const ctrlStack: Array<{ level: number; id: string }> = []\n for (const rec of readRecords(data)) {\n while (ctrlStack.length > 0 && ctrlStack[ctrlStack.length - 1].level >= rec.level) ctrlStack.pop()\n if (rec.tagId === TAG_PARA_TEXT) {\n let where: RedactWhere = \"body\"\n for (let i = ctrlStack.length - 1; i >= 0; i--) {\n const w = CTRL_WHERE[ctrlStack[i].id]\n if (w) { where = w; break }\n }\n if (scrubParaText(rec.data, ctx, mode, where, report)) changed = true\n continue\n }\n let where: RedactWhere = \"other\"\n if (rec.tagId === TAG_CTRL_HEADER) {\n const id = ctrlName(rec.data)\n ctrlStack.push({ level: rec.level, id })\n where = id.startsWith(\"%\") ? \"field\" : \"attribute\"\n } else if (rec.tagId === TAG_CTRL_DATA) where = \"field\"\n // 셀·누름틀 이름으로 쓰이는 데이터 연결 경로(\"./DataArea/…@code.Name\")는 건너뛴다 (fill_form 이 찾는 이름)\n if (scrubUtf16Runs(rec.data, ctx, mode, (h) => report(h.rule, h.masked, where), true)) changed = true\n }\n return changed\n}\n\n/**\n * 스트림 원시 바이트 — UTF-16 문자열 조각, ascii 면 1바이트 조각도(VT_LPSTR 속성·풀린 스크립트).\n * 바이너리 스트림에 1바이트 조각 검사를 켜면 우연히 번호 모양이 된 바이트를 덮어쓸 수 있어 끈다.\n */\nfunction scrubRaw(data: Buffer, ctx: ScrubCtx, mode: \"mask\" | \"check\", where: RedactWhere, report: Report, ascii: boolean): boolean {\n const a = scrubUtf16Runs(data, ctx, mode, (h) => report(h.rule, h.masked, where))\n const b = ascii && scrubAsciiRuns(data, ctx, mode, (h) => report(h.rule, h.masked, where))\n return a || b\n}\n\nfunction tryInflate(data: Buffer): Buffer | null {\n try { return decompressStream(data) } catch { return null }\n}\n\n/** 한컴 압축 스트림 꼬리(CRC32(비압축) LE + 비압축 크기 LE 8바이트)가 붙어 있었는가 — raw 는 가리기 전 해제본 */\nfunction hasHancomTail(stream: Buffer, raw: Buffer): boolean {\n return stream.length >= 8 && stream.readUInt32LE(stream.length - 4) === raw.length >>> 0 && stream.readUInt32LE(stream.length - 8) === crc32(raw)\n}\n\n/** 원래 모양대로 다시 압축 — 꼬리가 있던 스트림은 꼬리까지 (hwp5-patch 와 같은 형식) */\nconst recompress = (raw: Buffer, tail: boolean): Buffer => (tail ? compressWithTail(raw) : deflateRawSync(raw))\n\n// ─── 미할당 영역 ─────────────────────────────────────\n\nconst BLOCK = 64 // 미니 섹터 — 작은 스트림의 옛 사본은 미니 스트림 안 64바이트 경계에서 시작한다\n\n/** 두 버퍼를 64바이트 블록 단위로 비교한 바뀐 구간 (비운 미할당 영역) */\nfunction changedSpans(a: Buffer, b: Buffer): Array<[number, number]> {\n const spans: Array<[number, number]> = []\n const n = Math.min(a.length, b.length)\n let start = -1\n for (let o = 0; o < n; o += BLOCK) {\n const e = Math.min(n, o + BLOCK)\n const diff = a.compare(b, o, e, o, e) !== 0\n if (diff && start < 0) start = o\n else if (!diff && start >= 0) { spans.push([start, o]); start = -1 }\n }\n if (start >= 0) spans.push([start, n])\n return spans\n}\n\n/** raw deflate 블록 머리로 시작할 수 있는가 — 잘못된 블록 형식(3)·길이 보수 불일치 저장 블록은 바로 거른다 */\nfunction mayStartDeflate(b: Buffer, o: number): boolean {\n const type = (b[o] >> 1) & 3\n if (type === 3) return false\n if (type === 0) return o + 5 <= b.length && (b.readUInt16LE(o + 1) ^ b.readUInt16LE(o + 3)) === 0xffff\n return true\n}\n\n/**\n * 비운 영역(원래 바이트)에 있던 PII — 압축 안 된 UTF-16 조각과, 블록 경계에서 시작하는 옛 압축 스트림\n * (끝이 잘렸어도 풀린 만큼). 한컴은 다시 저장할 때 옛 BodyText 압축 사본을 미할당 섹터에 남긴다.\n */\nfunction scanSlack(before: Buffer, after: Buffer, ctx: ScrubCtx, onHit: (h: RedactHit) => void): number {\n let bytes = 0\n for (const [s, e] of changedSpans(before, after)) {\n bytes += e - s\n const region = Buffer.from(before.subarray(s, e))\n scrubUtf16Runs(region, ctx, \"check\", onHit)\n for (let o = 0; o + 8 <= region.length; o += BLOCK) {\n if (!mayStartDeflate(region, o)) continue\n let out: Buffer\n try {\n out = inflateRawSync(region.subarray(o), { finishFlush: zlibConstants.Z_SYNC_FLUSH, maxOutputLength: 64 * 1024 * 1024 })\n } catch { continue }\n if (out.length >= 16) scrubUtf16Runs(out, ctx, \"check\", onHit)\n }\n }\n return bytes\n}\n\nconst nameList = (names: string[]): string => names.length <= 5 ? names.join(\", \") : `${names.slice(0, 5).join(\", \")} 외 ${names.length - 5}개`\n\n/**\n * HWP5 한 파일을 훑는다. mask: 가린 파일 + 가린 곳 목록, check: 찾은 곳만 (data 는 입력 그대로 —\n * 삽입 OLE 개체·미할당 섹터 등 원시 바이트까지 본다).\n */\nexport async function scrubHwp5(original: Uint8Array, ctx: ScrubCtx, mode: \"mask\" | \"check\"): Promise<ScrubResult> {\n const file = Buffer.from(original.buffer, original.byteOffset, original.byteLength)\n const cfb = CFB.parse(file)\n const headerEntry = CFB.find(cfb, \"/FileHeader\")\n if (!headerEntry?.content) throw new Error(\"HWP FileHeader 스트림이 없습니다\")\n const header = parseFileHeader(Buffer.from(headerEntry.content))\n if (header.flags & (FLAG_ENCRYPTED | FLAG_DISTRIBUTION | FLAG_DRM)) {\n throw new Error(\"배포용·암호·DRM 문서는 본문이 암호화돼 있어 마스킹할 수 없습니다\")\n }\n const compressed = (header.flags & FLAG_COMPRESSED) !== 0\n\n const hits: RedactFileHit[] = []\n const warnings: string[] = []\n const replaced = new Map<string, Buffer>()\n const pictures: string[] = []\n const embedded: string[] = []\n let previewPath: string | null = null\n\n for (let idx = 0; idx < cfb.FullPaths.length; idx++) {\n const entry = cfb.FileIndex[idx]\n if (entry.type !== 2 || !entry.content) continue // 스트림만\n const path = cfb.FullPaths[idx].replace(/^[^/]*\\//, \"\") // \"Root Entry/\" 제거\n const content = Buffer.from(entry.content)\n const part = path.replace(/[\\x00-\\x1f]/g, (c) => `\\\\x${c.charCodeAt(0).toString(16).padStart(2, \"0\")}`)\n const report: Report = (rule, masked, where) => hits.push({ rule, masked, part, where })\n\n if (path === \"FileHeader\") continue\n if (path === \"PrvImage\") { previewPath = path; continue }\n\n let next: Buffer | null = null\n if (path.startsWith(\"BinData/\")) {\n // 개체별 압축 여부는 DocInfo BIN_DATA 속성 — 풀리면 푼 것으로 본다\n const inflated = tryInflate(content)\n const data = inflated ?? Buffer.from(content)\n const tail = inflated ? hasHancomTail(content, inflated) : false\n if (isOleBin(data) || isEmf(data)) {\n // OLE 개체(차트·삽입 문서·엑셀)·EMF — 제자리에서 못 가린다. 재검사에서 안의 글자를 보고해 잔존으로\n // 드러낸다 (압축된 채로는 원시 바이트 검사에 안 보인다 — 한컴 실저장본 OLE 는 전부 압축)\n embedded.push(part)\n if (mode === \"check\") {\n if (isOleBin(data)) await checkEmbeddedOle(data, ctx, (h) => report(h.rule, h.masked, \"other\"))\n else scrubUtf16Runs(data, ctx, \"check\", (h) => report(h.rule, h.masked, \"other\"))\n }\n } else {\n pictures.push(part)\n if (isMetaImage(data) && scrubImageMeta(data, ctx, mode, (h) => report(h.rule, h.masked, \"metadata\"))) next = inflated ? recompress(data, tail) : data\n }\n } else if (/^BodyText\\/Section\\d+$/.test(path) || path === \"DocInfo\") {\n const data = compressed ? decompressStream(content) : Buffer.from(content)\n const tail = compressed && hasHancomTail(content, data)\n if (scrubRecords(data, ctx, mode, report)) next = compressed ? recompress(data, tail) : data\n } else if (path === \"PrvText\") {\n const data = Buffer.from(content)\n if (scrubUtf16Runs(data, ctx, mode, (h) => report(h.rule, h.masked, \"preview\"))) next = data\n } else if (path.startsWith(\"\\x05\")) {\n const data = Buffer.from(content)\n if (scrubRaw(data, ctx, mode, \"metadata\", report, true)) next = data\n } else {\n // Scripts·DocHistory·DocOptions·XMLTemplate 등 — 압축이면 풀어서, 아니면 원시 그대로.\n // 1바이트 조각은 스크립트(JScript 원문)만\n const inflated = compressed ? tryInflate(content) : null\n const data = inflated ?? Buffer.from(content)\n const tail = inflated ? hasHancomTail(content, inflated) : false\n if (scrubRaw(data, ctx, mode, \"other\", report, path.startsWith(\"Scripts/\"))) next = inflated ? recompress(data, tail) : data\n if (path.startsWith(\"DocHistory/\")) warnings.push(`문서 이력(${path})이 들어 있습니다 — 이전 판의 글자는 문자열 조각 단위로만 검사했습니다`)\n }\n if (next) replaced.set(path, next)\n }\n\n let data: Uint8Array = original\n if (mode === \"mask\") {\n if (previewPath && (hits.length > 0 || ctx.literals.length > 0)) {\n const img = CFB.find(cfb, \"/PrvImage\")?.content\n const blank = img ? blankPreviewImage(Buffer.from(img)) : null\n if (blank) {\n replaced.set(previewPath, blank)\n warnings.push(\"미리보기 이미지(PrvImage)를 같은 크기의 흰 이미지로 바꿨습니다 — 한컴에서 저장하면 다시 만들어집니다\")\n } else warnings.push(\"미리보기 이미지(PrvImage) 형식을 몰라 지우지 못했습니다 — 첫 쪽 렌더에 PII 가 보일 수 있습니다\")\n }\n if (pictures.length > 0) warnings.push(`삽입 그림 ${pictures.length}개(${nameList(pictures)})는 화소 속 글자(스캔 문서 등)를 탐지하지 못합니다 — JPEG·PNG 메타데이터만 검사했습니다`)\n if (embedded.length > 0) warnings.push(`삽입 개체 ${embedded.length}개(${nameList(embedded)} — OLE·EMF)는 가리지 못합니다 — 안의 글자에 PII 가 있으면 재검사 잔존으로 보고됩니다`)\n\n let out: Buffer = Buffer.from(file)\n for (const [path, content] of replaced) out = replaceOleStream(out, path, content)\n // 미할당 영역은 PII 유무와 상관없이 늘 비운다 — 옛 사본이 압축돼 있으면 탐지로는 못 보기 때문\n try {\n const wiped = wipeFreeSectors(out)\n const before = hits.length\n const seen = new Set<string>()\n const bytes = scanSlack(out, wiped, ctx, (h) => {\n const key = `${h.rule}\\0${h.masked}`\n if (seen.has(key)) return\n seen.add(key)\n hits.push({ rule: h.rule, masked: h.masked, part: \"(미할당 섹터)\", where: \"slack\" })\n })\n if (bytes > 0) {\n const found = hits.length - before\n warnings.push(`미할당 영역(한컴이 남긴 옛 스트림 사본 등) ${Math.ceil(bytes / 1024)}KB를 0으로 비웠습니다${found > 0 ? ` — 그 안에 개인정보 ${found}건이 있었습니다` : \"\"}`)\n }\n out = wiped\n } catch (err) {\n warnings.push(`미할당 섹터를 비우지 못했습니다 (${err instanceof Error ? err.message : String(err)}) — 옛 스트림 사본이 남을 수 있습니다`)\n }\n if (!out.equals(file)) data = new Uint8Array(out)\n } else {\n // 원시 바이트 전체의 UTF-16 조각(압축 안 된 스트림·미할당 영역) + 미할당 영역의 옛 압축 사본 —\n // 스트림 단위에서 못 본 것만 추가\n const add = (part: string, where: RedactWhere) => (h: RedactHit): void => {\n if (!hits.some((x) => x.masked === h.masked && x.rule === h.rule)) hits.push({ rule: h.rule, masked: h.masked, part, where })\n }\n scrubUtf16Runs(Buffer.from(file), ctx, \"check\", add(\"(OLE 원시 바이트)\", \"other\"))\n try { scanSlack(file, wipeFreeSectors(Buffer.from(file)), ctx, add(\"(미할당 섹터)\", \"slack\")) } catch { /* 구조가 깨져 비울 수 없는 파일 — 원시 바이트 검사로 대신 */ }\n }\n return { data, hits, warnings, unscanned: [] }\n}\n","/**\n * 문서 파일 단위 PII 마스킹 — 본문 마크다운만이 아니라 파일 안 모든 글자 저장소를 가린다.\n *\n * 흐름: parse → 마크다운 탐지(표 머리글 문맥 포함, 리포트·리터럴용) → 컨테이너 직접 수술\n * - HWPX: 모든 XML 의 <hp:p> 문단(본문·표·중첩표·글상자·머리말/꼬리말·각주/미주·캡션·메모),\n * 그 밖의 텍스트 노드·속성값(필드 명령·개체 설명·content.hpf 메타데이터), Preview/PrvText.txt,\n * 미리보기 이미지(빈 이미지로 교체) → redact-hwpx.ts\n * - HWP5: BodyText 전 레코드(PARA_TEXT 문단 + 필드·개체 설명 문자열), DocInfo, PrvText,\n * \\x05HwpSummaryInformation(제목=첫 줄·작성자), Scripts·기타 스트림, PrvImage → redact-hwp5.ts\n * - 그 외 포맷(PDF·DOCX·XLSX·HWP3·이미지…): 원본 파일은 건드리지 않고 마스킹된 마크다운만\n *\n * 왜 patchHwpx/patchHwp(마크다운 편집 역반영)를 쓰지 않나: 그 경로는 마크다운에 없는 곳(머리말·각주·\n * 미리보기·메타데이터)을 못 가리고, 매핑 실패 문단(강제 줄바꿈·표 개수 불일치·탭 포함 HWP5 문단)은\n * 원문을 남긴 채 skip 한다. 여기서는 같은 길이 글자 치환만 하므로 문단 구조·run·글자모양이 그대로다.\n *\n * 마스킹 뒤 결과를 두 갈래로 다시 검사해 남은 PII(residual)를 보고한다 — 0 이 아니면 호출자가 실패로\n * 다뤄야 한다. (1) 결과 파일을 같은 컨테이너 검사기로 훑기(가리지 못한 OLE 개체·압축 메타데이터 포함),\n * (2) 결과 파일을 파서로 다시 읽은 본문에 탐지 룰·본문에서 찾은 값이 남았는지 — 컨테이너 검사기와\n * 독립된 경로라 문단 조립·마스킹 splice 의 결함도 드러난다. 리포트에는 원본 PII 를 담지 않는다(masked 만).\n */\n\nimport { parse } from \"./index.js\"\nimport { detectFormat, detectZipFormat, detectOle2Format } from \"./detect.js\"\nimport { redactMarkdown, normalizeForDetect, DEFAULT_REDACT_RULES, type RedactHit, type RedactOptions } from \"./redact.js\"\nimport { literalsFromMarkdown, findLiterals, type ScrubCtx, type RedactFileHit } from \"./redact-scrub.js\"\nimport { scrubHwpx } from \"./redact-hwpx.js\"\nimport { scrubHwp5 } from \"./redact-hwp5.js\"\n\nexport type { RedactFileHit, RedactWhere } from \"./redact-scrub.js\"\n\nexport interface RedactDocumentOptions extends RedactOptions {\n /** 파일 경로 (파서 힌트 — 확장자·OCR 캐시) */\n filePath?: string\n /** 탐지만 하고 결과 파일 바이트를 만들지 않는다 */\n dryRun?: boolean\n}\n\nexport interface RedactDocumentResult {\n /** 세분화 포맷 (hwpx·hwp·pdf·docx·xlsx …) */\n format: string\n /** 마스킹된 원본 형식 파일 — HWPX/HWP 만. 그 외 포맷·dryRun 은 undefined */\n data?: Uint8Array\n /** HWPX/HWP 결과 파일이 원본과 다른가 (가림·미할당 영역 비움 포함) — 다를 때만 저장할 가치가 있다 */\n changed: boolean\n /** 마스킹된 본문 마크다운 (모든 포맷) */\n markdown: string\n /** 본문 마크다운 기준 탐지 (index 는 마크다운 오프셋) */\n markdownHits: RedactHit[]\n /** 파일 안에서 실제로 가린 곳 (HWPX/HWP) — 같은 값이 본문·미리보기·메타데이터에 있으면 각각 1건 */\n fileHits: RedactFileHit[]\n /** 마스킹 결과를 다시 훑어 남은 PII (결과 파일 컨테이너 + 재파싱 본문) — 비어 있어야 정상 */\n residual: RedactFileHit[]\n /** 글자가 있을 자리인데 검사하지 못한 파트·결과 재파싱 실패 — 비어 있어야 정상 (잔존과 같은 실패) */\n unscanned: string[]\n /** 검사하지 못한 부분·주의 사항 (이미지 속 글자 등) */\n warnings: string[]\n}\n\n// ─── 진입점 ──────────────────────────────────────────\n\n/** 세분화 포맷 판정 — ZIP·OLE 내부 구조까지 (PPTX·XLS 를 hwpx·hwp 로 오인하지 않게) */\nasync function preciseFormat(buffer: ArrayBuffer): Promise<string> {\n let format: string = detectFormat(buffer)\n if (format === \"hwpx\") { const z = await detectZipFormat(buffer); if (z !== \"unknown\") format = z }\n else if (format === \"hwp\") { const o = detectOle2Format(buffer); if (o !== \"unknown\") format = o }\n return format\n}\n\n/**\n * 문서 한 건을 마스킹한다. HWPX/HWP 는 원본 서식 그대로 같은 길이 글자 치환한 파일 바이트를,\n * 그 외 포맷은 마스킹된 마크다운만 돌려준다 (원본 파일은 수정하지 않음 — warnings 에 명시).\n */\nexport async function redactDocument(input: ArrayBuffer | Uint8Array, options?: RedactDocumentOptions): Promise<RedactDocumentResult> {\n const bytes = input instanceof Uint8Array ? input : new Uint8Array(input)\n const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer\n const rules = options?.rules ?? DEFAULT_REDACT_RULES\n const maskChar = options?.maskChar ?? \"●\"\n const format = await preciseFormat(buffer)\n\n const parsed = await parse(buffer, options?.filePath ? { filePath: options.filePath } : undefined)\n if (!parsed.success) throw new Error(`파싱 실패: ${parsed.error}`)\n const md = redactMarkdown(parsed.markdown, { rules, maskChar })\n const ctx: ScrubCtx = { rules, maskChar, literals: literalsFromMarkdown(parsed.markdown, md.hits) }\n const lit = maskLiterals(md.text, ctx)\n const base = { format, markdown: lit.text, markdownHits: [...md.hits, ...lit.hits].sort((a, b) => a.index - b.index) }\n\n if (format === \"hwpx\" || format === \"hwp\") {\n const scrub = format === \"hwpx\" ? scrubHwpx : scrubHwp5\n const r = await scrub(bytes, ctx, \"mask\")\n // 마스킹 결과를 같은 탐지기로 다시 훑는다 — 손대지 못한 파트가 있으면 여기서 드러난다\n const check = await scrub(r.data, ctx, \"check\")\n const residual = [...check.hits]\n const unscanned = [...r.unscanned]\n // 파서 경로 교차 검증 — 결과 파일을 다시 읽은 본문. 바뀐 게 없으면 원본 본문 그대로라 다시 읽지\n // 않는다 (본문에서 찾은 PII 를 파일에서 하나도 못 가렸다면 여기서 전부 잔존으로 드러난다)\n if (r.data === bytes) residual.push(...markdownLeftovers(parsed.markdown, ctx))\n else {\n const re = await parse(r.data.buffer.slice(r.data.byteOffset, r.data.byteOffset + r.data.byteLength) as ArrayBuffer)\n if (re.success) residual.push(...markdownLeftovers(re.markdown, ctx))\n else unscanned.push(`결과 파일 재파싱 실패 (${re.error}) — 결과 본문을 검증하지 못했습니다`)\n }\n return {\n ...base,\n data: options?.dryRun ? undefined : r.data,\n changed: r.data !== bytes,\n fileHits: r.hits,\n residual,\n unscanned,\n warnings: r.warnings,\n }\n }\n\n const warnings = [\n `${format} 원본 파일은 수정하지 않습니다 — 마스킹된 마크다운만 만듭니다. 원본의 텍스트 레이어·이미지·메타데이터에는 PII 가 그대로 남습니다.`,\n ]\n if (format === \"pdf\") warnings.push(\"PDF 를 공개하려면 PDF 편집기의 가림(redaction) 기능으로 텍스트를 실제로 지우거나, 마스킹된 마크다운으로 새 문서를 만드세요.\")\n return { ...base, changed: false, fileHits: [], residual: markdownLeftovers(lit.text, ctx), unscanned: [], warnings }\n}\n\n/**\n * 본문에서 찾은 값이 문맥 없이 또 나오는 자리까지 마크다운에서 가린다 — 파일 수술의 리터럴과 같은 기준\n * (\"| 성명 |\" 열에서 찾은 이름이 본문에 맨이름으로 다시 나오는 경우). 이미지 data URI 줄은 건드리지 않는다\n */\nfunction maskLiterals(markdown: string, ctx: ScrubCtx): { text: string; hits: RedactHit[] } {\n if (ctx.literals.length === 0) return { text: markdown, hits: [] }\n const hits: RedactHit[] = []\n let offset = 0\n const lines = markdown.split(\"\\n\").map((line) => {\n const at = offset\n offset += line.length + 1\n if (line.includes(\"data:image/\")) return line\n const found = findLiterals(normalizeForDetect(line), ctx.literals)\n if (found.length === 0) return line\n let out = \"\"\n let cursor = 0\n for (const { index, lit } of found) {\n let masked = \"\"\n for (let k = 0; k < lit.norm.length; k++) masked += lit.masked[k] !== lit.value[k] ? ctx.maskChar : line[index + k]\n out += line.slice(cursor, index) + masked\n cursor = index + lit.norm.length\n hits.push({ rule: lit.rule, masked, index: at + index, length: lit.norm.length })\n }\n return out + line.slice(cursor)\n })\n return { text: lines.join(\"\\n\"), hits }\n}\n\n/** 마스킹 결과 본문에 남은 PII — 탐지 룰 + 원본 본문에서 찾은 값(리터럴) 그대로 */\nfunction markdownLeftovers(markdown: string, ctx: ScrubCtx): RedactFileHit[] {\n const out: RedactFileHit[] = redactMarkdown(markdown, { rules: ctx.rules, maskChar: ctx.maskChar }).hits\n .map((h) => ({ rule: h.rule, masked: h.masked, part: \"(결과 본문 재파싱)\", where: \"body\" as const }))\n for (const { lit } of findLiterals(normalizeForDetect(markdown), ctx.literals)) {\n if (!out.some((h) => h.masked === lit.masked)) out.push({ rule: lit.rule, masked: lit.masked, part: \"(결과 본문 재파싱)\", where: \"body\" })\n }\n return out\n}\n"],"mappings":"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","names":["require","JSZip","JSZip","dec","createRequire","require","createRequire","CFB","where","nameList","data"]}
@@ -15,17 +15,17 @@ import {
15
15
  renderHwpxToSvg,
16
16
  renderSceneToHtml,
17
17
  unionBBox
18
- } from "./chunk-TSFS5VKD.js";
18
+ } from "./chunk-D4U2TXDC.js";
19
19
  import "./chunk-7FCNOKOR.js";
20
- import "./chunk-7IAQ4HOK.js";
20
+ import "./chunk-6BWU6SQX.js";
21
21
  import "./chunk-MOL7MDBG.js";
22
- import "./chunk-ILU76UF3.js";
23
- import "./chunk-QC5T6D4J.js";
22
+ import "./chunk-YAKC46HZ.js";
23
+ import "./chunk-NLU6LBLY.js";
24
24
  import {
25
25
  rasterizePageSvg,
26
26
  rasterizeSvg
27
- } from "./chunk-C7FQBSON.js";
28
- import "./chunk-UET462HB.js";
27
+ } from "./chunk-V3DX4DUF.js";
28
+ import "./chunk-TITDJWVZ.js";
29
29
  export {
30
30
  RegionCollector,
31
31
  buildHwp5RenderStyles,
@@ -45,4 +45,4 @@ export {
45
45
  renderSceneToHtml,
46
46
  unionBBox
47
47
  };
48
- //# sourceMappingURL=render-NGP5T24S.js.map
48
+ //# sourceMappingURL=render-YZ2TEC3N.js.map
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ placeSealHwpx
4
+ } from "./chunk-HIJUKZHM.js";
5
+ import "./chunk-YAKC46HZ.js";
6
+ import "./chunk-TITDJWVZ.js";
7
+ export {
8
+ placeSealHwpx
9
+ };
10
+ //# sourceMappingURL=seal-ECV44TUH.js.map
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  getAccessRoot,
4
4
  isOfflineMode
5
- } from "./chunk-GDW4YP7K.js";
6
- import "./chunk-UET462HB.js";
5
+ } from "./chunk-GLSBEEZJ.js";
6
+ import "./chunk-TITDJWVZ.js";
7
7
 
8
8
  // src/setup.ts
9
9
  import { createInterface } from "readline/promises";
@@ -243,4 +243,4 @@ function printManualConfig() {
243
243
  export {
244
244
  runSetup
245
245
  };
246
- //# sourceMappingURL=setup-AU4OCHNH.js.map
246
+ //# sourceMappingURL=setup-MPIEP6XT.js.map
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ extractTables
4
+ } from "./chunk-XVXHXQIO.js";
5
+ import "./chunk-HIJUKZHM.js";
6
+ import "./chunk-DPWWXUOG.js";
7
+ import "./chunk-VJIL6PIL.js";
8
+ import "./chunk-D4U2TXDC.js";
9
+ import "./chunk-7FCNOKOR.js";
10
+ import "./chunk-6BWU6SQX.js";
11
+ import "./chunk-MOL7MDBG.js";
12
+ import "./chunk-YAKC46HZ.js";
13
+ import "./chunk-NLU6LBLY.js";
14
+ import "./chunk-V3DX4DUF.js";
15
+ import "./chunk-TITDJWVZ.js";
16
+ export {
17
+ extractTables
18
+ };
19
+ //# sourceMappingURL=visual-ICMOWUCR.js.map
@@ -1,25 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  assertNetworkAllowed
4
- } from "./chunk-GDW4YP7K.js";
4
+ } from "./chunk-GLSBEEZJ.js";
5
5
  import {
6
6
  parse
7
- } from "./chunk-EISO3POT.js";
8
- import "./chunk-ZWOFQRIB.js";
9
- import "./chunk-DZIXKL7E.js";
10
- import "./chunk-EDNJLUSE.js";
7
+ } from "./chunk-XVXHXQIO.js";
8
+ import "./chunk-HIJUKZHM.js";
9
+ import "./chunk-DPWWXUOG.js";
10
+ import "./chunk-VJIL6PIL.js";
11
11
  import {
12
12
  detectFormat
13
- } from "./chunk-TSFS5VKD.js";
13
+ } from "./chunk-D4U2TXDC.js";
14
14
  import "./chunk-7FCNOKOR.js";
15
- import "./chunk-7IAQ4HOK.js";
15
+ import "./chunk-6BWU6SQX.js";
16
16
  import "./chunk-MOL7MDBG.js";
17
- import "./chunk-ILU76UF3.js";
18
- import "./chunk-QC5T6D4J.js";
19
- import "./chunk-C7FQBSON.js";
17
+ import "./chunk-YAKC46HZ.js";
18
+ import "./chunk-NLU6LBLY.js";
19
+ import "./chunk-V3DX4DUF.js";
20
20
  import {
21
21
  toArrayBuffer
22
- } from "./chunk-UET462HB.js";
22
+ } from "./chunk-TITDJWVZ.js";
23
23
 
24
24
  // src/watch.ts
25
25
  import { watch, readFileSync, writeFileSync, mkdirSync, statSync, existsSync, realpathSync } from "fs";
@@ -210,4 +210,4 @@ export {
210
210
  isPrivateIp,
211
211
  watchDirectory
212
212
  };
213
- //# sourceMappingURL=watch-MKPUCPXM.js.map
213
+ //# sourceMappingURL=watch-RYO7MN5B.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kordoc",
3
- "version": "4.14.2",
3
+ "version": "4.14.4",
4
4
  "description": "Parse Korean documents (HWP3-5, HWPX, HWPML, PDF, XLS(X), DOCX) to Markdown — CLI + MCP server with lossless patch/roundtrip, form-filler, document diff, print & layout-preserving SVG renderer",
5
5
  "type": "module",
6
6
  "exports": {
@@ -28,7 +28,7 @@
28
28
  "dev": "tsup --watch",
29
29
  "test": "node --import tsx --test tests/*.test.ts",
30
30
  "typecheck": "tsc --noEmit",
31
- "bench:gate": "node bench/verify-pages.mjs --gate && node bench/score.mjs && node bench/roundtrip.mjs --gate && node bench/pdf-table-gt.mjs --gate && node bench/formats-sweep.mjs --gate && node bench/fuzz-sweep.mjs --gate && node bench/verify-reflow.mjs --gate",
31
+ "bench:gate": "node bench/verify-pages.mjs --gate && node bench/score.mjs && node bench/roundtrip.mjs --gate && node bench/pdf-table-gt.mjs --gate --no-ocr && node bench/pdf-text-gt.mjs --gate && node bench/formats-sweep.mjs --gate && node bench/fuzz-sweep.mjs --gate && node bench/verify-reflow.mjs --gate && node bench/redact-bench.mjs --gate && node bench/ocr-accuracy.mjs --gate",
32
32
  "bench:visual": "node bench/visual/verify-visual.mjs --gate",
33
33
  "sync-meta": "node scripts/sync-meta.mjs",
34
34
  "prepublishOnly": "node scripts/sync-meta.mjs --check && node scripts/check-notices.mjs && npm run typecheck && npm run test && npm run build && node bench/gate-if-corpus.mjs"