@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 +27 -0
- package/dist/dom/xlsx.js +55 -0
- package/package.json +1 -1
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);
|