@doccloak/core 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@ All notable changes to `@doccloak/core` are documented here. The format is
4
4
  based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.12.1] - 2026-09-24
8
+
9
+ First run of the real-file corpus job (LibreOffice-generated docx, doc and
10
+ xlsx) on CI after 0.12.0 was published. One reader/writer gap and three test
11
+ expectations came out of it; no published API changed.
12
+
13
+ ### Fixed
14
+
15
+ - **xlsx: identifiers stored as numbers (T176, minimal).** A PESEL, phone or
16
+ account number typed into a cell is a numeric `<v>`. Numbers of seven or
17
+ more digits are now part of the extracted text, and a replaced numeric cell
18
+ is written as an inline string cell without its formula, so Excel opens
19
+ the workbook without repair. Shorter numbers (amounts, counts, date
20
+ serials) are left alone.
21
+
22
+ ### Tests
23
+
24
+ - Corpus assertions distinguish consented parts (OLE ObjectPool and Macros
25
+ storages in .doc reported by `inspectDoc`; embedded parts in docx/xlsx
26
+ reported by the readers) from the rest of the package, and account for
27
+ occurrences inside tracked deletions that `acceptTrackedChanges` drops.
28
+ - A .doc whose PII sits only in frames or text boxes re-parses clean but
29
+ placeholder-free (the legacy writer overwrites those stories in place);
30
+ the suite checks for surviving needles instead of a placeholder count.
31
+ - Timing tests guard linearity with budgets that tolerate shared CI runners.
32
+ - Part-name inventory refreshed from the LibreOffice corpus.
33
+
7
34
  ## [0.12.0] - 2026-09-24
8
35
 
9
36
  Security remediation release (audit `SECURITY_REPORT-2026-09.md`, plan
package/dist/dom/xlsx.js CHANGED
@@ -56,6 +56,46 @@ function sharedStringUnits(xmlDoc) {
56
56
  }
57
57
  return units;
58
58
  }
59
+ /** A cell number that can be an identifier: an integer of seven or more digits. */
60
+ const NUMERIC_IDENTIFIER = /^[+-]?\d{7,}$/;
61
+ /** What SpreadsheetML accepts as a numeric <v>. */
62
+ const NUMERIC_VALUE = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
63
+ /**
64
+ * After replacement, a numeric cell whose <v> no longer holds a number
65
+ * (it now carries a placeholder) would make Excel repair the workbook. Turn
66
+ * it into an inline string cell and drop its formula, which would otherwise
67
+ * recompute the original number (T176).
68
+ */
69
+ function normalizeReplacedNumericCells(xmlDoc) {
70
+ for (const cell of elementsByLocalName(xmlDoc, OOXML_NS.ss, 'c')) {
71
+ const type = cell.getAttribute('t');
72
+ if (type !== null && type !== 'n')
73
+ continue;
74
+ let v = null;
75
+ let f = null;
76
+ for (let c = 0; c < cell.children.length; c++) {
77
+ const child = cell.children[c];
78
+ if (isSpreadsheetEl(child, 'v'))
79
+ v = child;
80
+ else if (isSpreadsheetEl(child, 'f'))
81
+ f = child;
82
+ }
83
+ if (!v)
84
+ continue;
85
+ const text = v.textContent ?? '';
86
+ if (text.length === 0 || NUMERIC_VALUE.test(text))
87
+ continue;
88
+ cell.setAttribute('t', 'inlineStr');
89
+ if (f)
90
+ cell.removeChild(f);
91
+ const is = xmlDoc.createElementNS(cell.namespaceURI, 'is');
92
+ const t = xmlDoc.createElementNS(cell.namespaceURI, 't');
93
+ t.setAttribute('xml:space', 'preserve');
94
+ t.textContent = text;
95
+ is.appendChild(t);
96
+ cell.replaceChild(is, v);
97
+ }
98
+ }
59
99
  const HEADER_FOOTER_NAMES = new Set([
60
100
  'oddHeader', 'oddFooter', 'evenHeader', 'evenFooter', 'firstHeader', 'firstFooter',
61
101
  ]);
@@ -85,6 +125,19 @@ function worksheetUnits(xmlDoc) {
85
125
  units.push([child]);
86
126
  }
87
127
  }
128
+ else if (type === null || type === 'n') {
129
+ // Numeric cells (T176): a PESEL, phone or account number typed into
130
+ // a cell is stored as a number. Values of at least seven digits are
131
+ // extracted so the detectors see them; a replaced cell is turned
132
+ // into an inline string on write (normalizeReplacedNumericCells).
133
+ // Shorter numbers (amounts, counts, dates as serials) are not
134
+ // identifiers and stay out of the text.
135
+ for (let c = 0; c < el.children.length; c++) {
136
+ const child = el.children[c];
137
+ if (isSpreadsheetEl(child, 'v') && NUMERIC_IDENTIFIER.test(child.textContent ?? ''))
138
+ units.push([child]);
139
+ }
140
+ }
88
141
  }
89
142
  else if (HEADER_FOOTER_NAMES.has(el.localName)) {
90
143
  units.push([el]);
@@ -311,6 +364,8 @@ export async function writeAnonymizedXlsxWithReport(extraction, replacements, va
311
364
  applyTextReplacements(extraction.textNodes, replacements);
312
365
  const contentPaths = new Set();
313
366
  for (const part of extraction.contentParts) {
367
+ if (/^xl\/worksheets\//.test(part.path))
368
+ normalizeReplacedNumericCells(part.xmlDoc);
314
369
  sanitizeXlsxPart(part.xmlDoc, valueReplacements);
315
370
  writeXmlPart(extraction.zip, part.path, part.xmlDoc);
316
371
  contentPaths.add(part.path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccloak/core",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Environment-agnostic document anonymization engine for the DocCloak ecosystem.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Witold Lojek",