@doccloak/core 0.11.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 +148 -2
- package/README.md +252 -19
- package/dist/doc.d.ts +68 -1
- package/dist/doc.js +447 -62
- package/dist/dom/docx.d.ts +135 -7
- package/dist/dom/docx.js +752 -125
- package/dist/dom/drawingml.d.ts +59 -0
- package/dist/dom/drawingml.js +176 -0
- package/dist/dom/errors.d.ts +35 -0
- package/dist/dom/errors.js +24 -0
- package/dist/dom/index.d.ts +3 -0
- package/dist/dom/index.js +6 -0
- package/dist/dom/office.d.ts +56 -0
- package/dist/dom/office.js +76 -23
- package/dist/dom/opc.d.ts +135 -7
- package/dist/dom/opc.js +407 -14
- package/dist/dom/package-policy.d.ts +137 -0
- package/dist/dom/package-policy.js +355 -0
- package/dist/dom/xlsx.d.ts +64 -6
- package/dist/dom/xlsx.js +130 -20
- package/dist/engine.d.ts +18 -0
- package/dist/engine.js +37 -13
- package/dist/env.d.ts +21 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +10 -5
- package/dist/model-loader.d.ts +61 -6
- package/dist/model-loader.js +122 -17
- package/dist/pipeline.d.ts +87 -8
- package/dist/pipeline.js +249 -50
- package/dist/providers/bardsai.d.ts +8 -1
- package/dist/providers/bardsai.js +22 -2
- package/dist/providers/gliner-base.d.ts +8 -1
- package/dist/providers/gliner-base.js +22 -2
- package/dist/providers/gliner.d.ts +8 -1
- package/dist/providers/gliner.js +22 -2
- package/dist/regex/loader.d.ts +2 -0
- package/dist/regex/loader.js +2 -0
- package/dist/regex/rules.data.js +15 -14
- package/dist/regex/types.d.ts +6 -0
- package/dist/session.d.ts +101 -3
- package/dist/session.js +397 -43
- package/dist/surrogates.js +57 -45
- package/dist/worker-protocol.d.ts +13 -1
- package/dist/worker-protocol.js +164 -12
- package/package.json +1 -1
- package/rules/at.json +1 -1
- package/rules/be.json +1 -1
- package/rules/ch.json +1 -1
- package/rules/de.json +1 -1
- package/rules/dk.json +1 -1
- package/rules/es.json +1 -1
- package/rules/fi.json +1 -1
- package/rules/fr.json +1 -1
- package/rules/it.json +1 -1
- package/rules/no.json +1 -1
- package/rules/pl.json +1 -1
- package/rules/pt.json +1 -1
- package/rules/schema.json +4 -0
- package/rules/se.json +1 -1
- package/rules/universal.json +2 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @doccloak/core/dom - package part policy (T177, security remediation 2026-09).
|
|
3
|
+
*
|
|
4
|
+
* Every part of a .docx / .xlsx package falls into one of four classes:
|
|
5
|
+
* - text: the reader extracts it and the writer applies offset replacements
|
|
6
|
+
* (document body, headers, footnotes, chart caches, SmartArt, customXml);
|
|
7
|
+
* - structural: styles, fonts, themes, relationships, media and metadata
|
|
8
|
+
* parts; nothing in them is user text, or the metadata scrub and layer
|
|
9
|
+
* zero (the package-wide value scrub) cover what is;
|
|
10
|
+
* - unredactable: content the engine cannot reach (embedded workbooks, OLE
|
|
11
|
+
* blobs, macros, HTML chunks, data connections, printer settings). These
|
|
12
|
+
* are REPORTED to the host as `UnredactablePart[]` with a plain-language
|
|
13
|
+
* label; the writer refuses to export them unless the host passes
|
|
14
|
+
* `allowUnredactable: true`, which the web app does only after the user
|
|
15
|
+
* confirmed (founder decision 2026-09-24: informed consent, no hard
|
|
16
|
+
* refusal);
|
|
17
|
+
* - unknown: a part name no table recognises. Treated like unredactable
|
|
18
|
+
* (reported, gated) so a new Office feature can never slip through as a
|
|
19
|
+
* silently copied part.
|
|
20
|
+
*
|
|
21
|
+
* The tables are calibrated on the part-name inventory of real files (Word
|
|
22
|
+
* 365 Windows/Mac, LibreOffice, Google Docs export, Pages export, Excel 365)
|
|
23
|
+
* and on the synthetic T173 fixtures; tests/package-policy.test.ts embeds
|
|
24
|
+
* that inventory and fails when any name in it classifies as unknown.
|
|
25
|
+
*
|
|
26
|
+
* Known limits (documented, not policy failures): media/* (images without
|
|
27
|
+
* OCR), ink/* (handwriting strokes), theme/media/* are structural.
|
|
28
|
+
*/
|
|
29
|
+
import type JSZip from 'jszip';
|
|
30
|
+
import type { UnredactablePart } from './errors.js';
|
|
31
|
+
export type PackageKind = 'docx' | 'xlsx';
|
|
32
|
+
export type PartClass = 'text' | 'structural' | 'unredactable' | 'unknown';
|
|
33
|
+
export type UnredactableKind = UnredactablePart['kind'];
|
|
34
|
+
/** One row of an unredactable table: what matches, how it is reported. */
|
|
35
|
+
export interface UnredactableRule {
|
|
36
|
+
pattern: RegExp;
|
|
37
|
+
kind: UnredactableKind;
|
|
38
|
+
/** A fixed label, or a function of the part name and its content type. */
|
|
39
|
+
label: string | ((path: string, contentType: string | null) => string);
|
|
40
|
+
}
|
|
41
|
+
/** Parts every OPC package has: manifest, relationships, document properties. */
|
|
42
|
+
export declare const COMMON_STRUCTURAL_PARTS: readonly RegExp[];
|
|
43
|
+
/**
|
|
44
|
+
* WordprocessingML parts the docx reader walks with the paragraph extractor:
|
|
45
|
+
* the main document, headers, footers, footnotes, endnotes, comments, the
|
|
46
|
+
* numbering definitions (w:lvlText units, T174), the web settings (frameset
|
|
47
|
+
* units, T175) and the same set inside the glossary (building blocks)
|
|
48
|
+
* subdocument.
|
|
49
|
+
*/
|
|
50
|
+
export declare const DOCX_WORD_TEXT_PARTS: RegExp;
|
|
51
|
+
/**
|
|
52
|
+
* DrawingML parts read with drawingUnits (T175): charts (title, axis and
|
|
53
|
+
* data-label rich text, string caches, series names), chartEx (2016+ chart
|
|
54
|
+
* types), SmartArt data models and their pre-rendered drawing, and chart
|
|
55
|
+
* user shapes.
|
|
56
|
+
*/
|
|
57
|
+
export declare const DOCX_DRAWING_TEXT_PARTS: RegExp;
|
|
58
|
+
/** Custom XML data stores read with leafTextUnits (T175): cover page, bibliography, SharePoint properties. */
|
|
59
|
+
export declare const DOCX_CUSTOM_XML_TEXT_PARTS: RegExp;
|
|
60
|
+
export declare const DOCX_TEXT_PARTS: readonly RegExp[];
|
|
61
|
+
export declare const DOCX_STRUCTURAL_PARTS: readonly RegExp[];
|
|
62
|
+
/** Content-type-derived label for an embedded package or OLE object. */
|
|
63
|
+
export declare function embeddedObjectLabel(path: string, contentType: string | null): string;
|
|
64
|
+
export declare const DOCX_UNREDACTABLE_PARTS: readonly UnredactableRule[];
|
|
65
|
+
/** Parts the xlsx reader extracts: shared strings, worksheets, comments. */
|
|
66
|
+
export declare const XLSX_SHEET_TEXT_PARTS: RegExp;
|
|
67
|
+
/** DrawingML parts read with drawingUnits (T175): drawing text boxes and charts. */
|
|
68
|
+
export declare const XLSX_DRAWING_TEXT_PARTS: RegExp;
|
|
69
|
+
export declare const XLSX_TEXT_PARTS: readonly RegExp[];
|
|
70
|
+
export declare const XLSX_STRUCTURAL_PARTS: readonly RegExp[];
|
|
71
|
+
export declare const XLSX_UNREDACTABLE_PARTS: readonly UnredactableRule[];
|
|
72
|
+
/** The unredactable rule that matches the part, if any. */
|
|
73
|
+
export declare function unredactableRuleFor(path: string, kind: PackageKind): UnredactableRule | null;
|
|
74
|
+
/**
|
|
75
|
+
* Classify one zip entry (relative path, no leading slash) of a docx or xlsx
|
|
76
|
+
* package. Unredactable rules win over the text and structural tables so a
|
|
77
|
+
* misplaced binary can never be declared structural by a broad pattern.
|
|
78
|
+
*/
|
|
79
|
+
export declare function classifyPart(path: string, kind: PackageKind): PartClass;
|
|
80
|
+
/**
|
|
81
|
+
* The UnredactablePart record for a part that classifies as unredactable
|
|
82
|
+
* or unknown (a text or structural part yields null).
|
|
83
|
+
*/
|
|
84
|
+
export declare function describeUnredactablePart(path: string, kind: PackageKind, contentType?: string | null): UnredactablePart | null;
|
|
85
|
+
export interface ContentTypeIndex {
|
|
86
|
+
/** Extension (lower case, no dot) to media type */
|
|
87
|
+
defaults: Map<string, string>;
|
|
88
|
+
/** Part name (no leading slash) to media type */
|
|
89
|
+
overrides: Map<string, string>;
|
|
90
|
+
}
|
|
91
|
+
/** Parse [Content_Types].xml; an absent or malformed manifest yields empty maps. */
|
|
92
|
+
export declare function readContentTypes(zip: JSZip): Promise<ContentTypeIndex>;
|
|
93
|
+
/** Media type of a part: its Override, else the Default for its extension. */
|
|
94
|
+
export declare function contentTypeOf(index: ContentTypeIndex, path: string): string | null;
|
|
95
|
+
/** Resolve a relationship target against the part that owns the relationship. */
|
|
96
|
+
export declare function resolvePartPath(ownerPart: string, target: string): string;
|
|
97
|
+
export interface PartRelationship {
|
|
98
|
+
/** Relationship type URI */
|
|
99
|
+
type: string;
|
|
100
|
+
/** Raw Target attribute */
|
|
101
|
+
target: string;
|
|
102
|
+
/** True for TargetMode="External" */
|
|
103
|
+
external: boolean;
|
|
104
|
+
/** Zip path of the target for internal relationships, null for external ones */
|
|
105
|
+
path: string | null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The relationships of one part (its `_rels/<name>.rels` sibling), keyed by
|
|
109
|
+
* Id, with internal targets resolved to zip paths. Absent or malformed rels
|
|
110
|
+
* yield an empty map.
|
|
111
|
+
*/
|
|
112
|
+
export declare function relationshipsOf(zip: JSZip, partPath: string): Promise<Map<string, PartRelationship>>;
|
|
113
|
+
export interface CollectUnredactableOptions {
|
|
114
|
+
/**
|
|
115
|
+
* Parts referenced by w:altChunk (resolved by the docx reader). They are
|
|
116
|
+
* reported as 'html-chunk' whatever their name, since Word imports their
|
|
117
|
+
* content verbatim into the document.
|
|
118
|
+
*/
|
|
119
|
+
htmlChunkParts?: Iterable<string>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Every part of the package the writer would copy verbatim, with a kind and
|
|
123
|
+
* a human label, sorted by part name. Never throws: the decision what to do
|
|
124
|
+
* with the list belongs to the host (informed-consent export).
|
|
125
|
+
*/
|
|
126
|
+
export declare function collectUnredactableParts(zip: JSZip, kind: PackageKind, options?: CollectUnredactableOptions): Promise<UnredactablePart[]>;
|
|
127
|
+
/** Default limit on the sum of declared uncompressed entry sizes: 200 MB. */
|
|
128
|
+
export declare const MAX_UNPACKED_BYTES: number;
|
|
129
|
+
/**
|
|
130
|
+
* Sum of the uncompressed sizes of every entry, from the zip directory (no
|
|
131
|
+
* inflation happens here). JSZip verifies the declared size against the
|
|
132
|
+
* inflated length when a part is read, so a directory that lies is refused
|
|
133
|
+
* later with a corrupted-zip error instead of being inflated past the limit.
|
|
134
|
+
*/
|
|
135
|
+
export declare function unpackedSize(zip: JSZip): number;
|
|
136
|
+
/** Throw UnsupportedDocumentError('too-large') when the package exceeds the limit. */
|
|
137
|
+
export declare function assertUnpackedSize(zip: JSZip, limit?: number): void;
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @doccloak/core/dom - package part policy (T177, security remediation 2026-09).
|
|
3
|
+
*
|
|
4
|
+
* Every part of a .docx / .xlsx package falls into one of four classes:
|
|
5
|
+
* - text: the reader extracts it and the writer applies offset replacements
|
|
6
|
+
* (document body, headers, footnotes, chart caches, SmartArt, customXml);
|
|
7
|
+
* - structural: styles, fonts, themes, relationships, media and metadata
|
|
8
|
+
* parts; nothing in them is user text, or the metadata scrub and layer
|
|
9
|
+
* zero (the package-wide value scrub) cover what is;
|
|
10
|
+
* - unredactable: content the engine cannot reach (embedded workbooks, OLE
|
|
11
|
+
* blobs, macros, HTML chunks, data connections, printer settings). These
|
|
12
|
+
* are REPORTED to the host as `UnredactablePart[]` with a plain-language
|
|
13
|
+
* label; the writer refuses to export them unless the host passes
|
|
14
|
+
* `allowUnredactable: true`, which the web app does only after the user
|
|
15
|
+
* confirmed (founder decision 2026-09-24: informed consent, no hard
|
|
16
|
+
* refusal);
|
|
17
|
+
* - unknown: a part name no table recognises. Treated like unredactable
|
|
18
|
+
* (reported, gated) so a new Office feature can never slip through as a
|
|
19
|
+
* silently copied part.
|
|
20
|
+
*
|
|
21
|
+
* The tables are calibrated on the part-name inventory of real files (Word
|
|
22
|
+
* 365 Windows/Mac, LibreOffice, Google Docs export, Pages export, Excel 365)
|
|
23
|
+
* and on the synthetic T173 fixtures; tests/package-policy.test.ts embeds
|
|
24
|
+
* that inventory and fails when any name in it classifies as unknown.
|
|
25
|
+
*
|
|
26
|
+
* Known limits (documented, not policy failures): media/* (images without
|
|
27
|
+
* OCR), ink/* (handwriting strokes), theme/media/* are structural.
|
|
28
|
+
*/
|
|
29
|
+
import { UnsupportedDocumentError } from "./errors.js";
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Shared (OPC) structural parts
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/** Parts every OPC package has: manifest, relationships, document properties. */
|
|
34
|
+
export const COMMON_STRUCTURAL_PARTS = [
|
|
35
|
+
/^\[Content_Types\]\.xml$/,
|
|
36
|
+
/(?:^|\/)_rels\/[^/]*\.rels$/,
|
|
37
|
+
/^docProps\/(?:core|app|custom)\.xml$/,
|
|
38
|
+
/^docProps\/thumbnail\.[A-Za-z0-9]+$/,
|
|
39
|
+
];
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// DOCX
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
/**
|
|
44
|
+
* WordprocessingML parts the docx reader walks with the paragraph extractor:
|
|
45
|
+
* the main document, headers, footers, footnotes, endnotes, comments, the
|
|
46
|
+
* numbering definitions (w:lvlText units, T174), the web settings (frameset
|
|
47
|
+
* units, T175) and the same set inside the glossary (building blocks)
|
|
48
|
+
* subdocument.
|
|
49
|
+
*/
|
|
50
|
+
export const DOCX_WORD_TEXT_PARTS = /^word\/(?:glossary\/)?(?:document|(?:header|footer)\d*|footnotes|endnotes|comments|numbering|webSettings)\.xml$/;
|
|
51
|
+
/**
|
|
52
|
+
* DrawingML parts read with drawingUnits (T175): charts (title, axis and
|
|
53
|
+
* data-label rich text, string caches, series names), chartEx (2016+ chart
|
|
54
|
+
* types), SmartArt data models and their pre-rendered drawing, and chart
|
|
55
|
+
* user shapes.
|
|
56
|
+
*/
|
|
57
|
+
export const DOCX_DRAWING_TEXT_PARTS = /^word\/(?:glossary\/)?(?:charts\/chart(?:Ex)?\d*|diagrams\/(?:data|drawing)\d*|drawings\/drawing\d*)\.xml$/;
|
|
58
|
+
/** Custom XML data stores read with leafTextUnits (T175): cover page, bibliography, SharePoint properties. */
|
|
59
|
+
export const DOCX_CUSTOM_XML_TEXT_PARTS = /^customXml\/item\d*\.xml$/;
|
|
60
|
+
export const DOCX_TEXT_PARTS = [
|
|
61
|
+
DOCX_WORD_TEXT_PARTS,
|
|
62
|
+
DOCX_DRAWING_TEXT_PARTS,
|
|
63
|
+
DOCX_CUSTOM_XML_TEXT_PARTS,
|
|
64
|
+
];
|
|
65
|
+
export const DOCX_STRUCTURAL_PARTS = [
|
|
66
|
+
...COMMON_STRUCTURAL_PARTS,
|
|
67
|
+
// Styles, fonts, settings (docVars, mailMerge, rsids removed by the
|
|
68
|
+
// metadata scrub), comment side tables (authors scrubbed), people.xml
|
|
69
|
+
// (author registry scrubbed).
|
|
70
|
+
/^word\/(?:glossary\/)?(?:styles|stylesWithEffects|fontTable|settings|people|commentsExtended|commentsIds|commentsExtensible)\.xml$/,
|
|
71
|
+
// Embedded (obfuscated) fonts: Word writes .odttf, LibreOffice .ttf/.otf,
|
|
72
|
+
// Google Docs export .fntdata.
|
|
73
|
+
/^word\/(?:glossary\/)?fonts\/[^/]+\.(?:odttf|ttf|otf|fntdata)$/i,
|
|
74
|
+
/^word\/(?:glossary\/)?theme\/.+$/,
|
|
75
|
+
// Known limit: images and other media are copied as-is (no OCR).
|
|
76
|
+
/^word\/(?:glossary\/)?media\/[^/]+$/,
|
|
77
|
+
// Chart and SmartArt presentation parts (no user text).
|
|
78
|
+
/^word\/(?:glossary\/)?charts\/(?:style|colors)\d*\.xml$/,
|
|
79
|
+
/^word\/(?:glossary\/)?diagrams\/(?:layout|quickStyle|colors)\d*\.xml$/,
|
|
80
|
+
// Data store properties (schema references and ids).
|
|
81
|
+
/^customXml\/itemProps\d*\.xml$/,
|
|
82
|
+
// Legacy VML drawing parts: layer zero scrubs attribute and text values.
|
|
83
|
+
/^word\/(?:glossary\/)?vmlDrawing\d*\.vml$/,
|
|
84
|
+
// Office add-in (web extension) manifests: ids and store references.
|
|
85
|
+
/^word\/(?:glossary\/)?webextensions\/[^/]+\.xml$/,
|
|
86
|
+
// Known limit: InkML pen strokes are coordinates, not text.
|
|
87
|
+
/^word\/(?:glossary\/)?ink\/[^/]+$/,
|
|
88
|
+
];
|
|
89
|
+
/** Content-type-derived label for an embedded package or OLE object. */
|
|
90
|
+
export function embeddedObjectLabel(path, contentType) {
|
|
91
|
+
const type = (contentType ?? '').toLowerCase();
|
|
92
|
+
const ext = extensionOf(path);
|
|
93
|
+
if (/spreadsheetml|ms-excel|x-excel/.test(type) || /^xls[xmb]?$/.test(ext))
|
|
94
|
+
return 'embedded Excel sheet';
|
|
95
|
+
if (/wordprocessingml|msword/.test(type) || /^do[ct][xm]?$/.test(ext))
|
|
96
|
+
return 'embedded document';
|
|
97
|
+
if (/presentationml|ms-powerpoint/.test(type) || /^pp[ts][xm]?$/.test(ext))
|
|
98
|
+
return 'embedded presentation';
|
|
99
|
+
return 'embedded object';
|
|
100
|
+
}
|
|
101
|
+
export const DOCX_UNREDACTABLE_PARTS = [
|
|
102
|
+
{ pattern: /^word\/(?:glossary\/)?embeddings\/[^/]+$/, kind: 'embedded-object', label: embeddedObjectLabel },
|
|
103
|
+
// vbaProject.bin, vbaData.xml, vbaProjectSignature*.bin
|
|
104
|
+
{ pattern: /^word\/(?:glossary\/)?vba[A-Za-z]*\.(?:bin|xml)$/, kind: 'macros', label: 'macros' },
|
|
105
|
+
// Word names the HTML/MHT payload of w:altChunk afchunk.mht; other
|
|
106
|
+
// generators number them (afchunk1.htm). Any other part referenced by a
|
|
107
|
+
// w:altChunk is reported by the reader through collectUnredactableParts.
|
|
108
|
+
{ pattern: /^word\/(?:glossary\/)?afchunk[^/]*$/i, kind: 'html-chunk', label: 'HTML chunk' },
|
|
109
|
+
{ pattern: /^word\/(?:glossary\/)?printerSettings\/[^/]+$/, kind: 'printer-settings', label: 'printer settings' },
|
|
110
|
+
{ pattern: /^word\/intelligence\d*\.xml$/, kind: 'unknown', label: 'editor analysis data' },
|
|
111
|
+
{ pattern: /^word\/(?:glossary\/)?activeX\/[^/]+$/, kind: 'embedded-object', label: 'ActiveX control' },
|
|
112
|
+
];
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// XLSX
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
/** Parts the xlsx reader extracts: shared strings, worksheets, comments. */
|
|
117
|
+
export const XLSX_SHEET_TEXT_PARTS = /^xl\/(?:sharedStrings\.xml|worksheets\/[^/]+\.xml|comments\d*\.xml|threadedComments\/[^/]+\.xml)$/;
|
|
118
|
+
/** DrawingML parts read with drawingUnits (T175): drawing text boxes and charts. */
|
|
119
|
+
export const XLSX_DRAWING_TEXT_PARTS = /^xl\/(?:drawings\/drawing\d*|charts\/chart(?:Ex)?\d*)\.xml$/;
|
|
120
|
+
export const XLSX_TEXT_PARTS = [
|
|
121
|
+
XLSX_SHEET_TEXT_PARTS,
|
|
122
|
+
XLSX_DRAWING_TEXT_PARTS,
|
|
123
|
+
];
|
|
124
|
+
export const XLSX_STRUCTURAL_PARTS = [
|
|
125
|
+
...COMMON_STRUCTURAL_PARTS,
|
|
126
|
+
// Workbook: sheet names and defined names are scrubbed by value (layer
|
|
127
|
+
// zero); T176 turns them into text units.
|
|
128
|
+
/^xl\/workbook\.xml$/,
|
|
129
|
+
/^xl\/(?:styles|calcChain|metadata)\.xml$/,
|
|
130
|
+
/^xl\/theme\/.+$/,
|
|
131
|
+
/^xl\/ctrlProps\/[^/]+$/,
|
|
132
|
+
// Threaded-comment person registry: display names scrubbed by the writer.
|
|
133
|
+
/^xl\/persons\/[^/]+\.xml$/,
|
|
134
|
+
// Tables, pivot tables and caches: header/cache strings scrubbed by value
|
|
135
|
+
// (layer zero); T176 owns their extraction.
|
|
136
|
+
/^xl\/(?:tables|pivotTables|pivotCache)\/[^/]+\.xml$/,
|
|
137
|
+
/^xl\/drawings\/vmlDrawing\d*\.vml$/,
|
|
138
|
+
/^xl\/charts\/(?:style|colors)\d*\.xml$/,
|
|
139
|
+
/^xl\/(?:chartsheets|dialogsheets)\/[^/]+\.xml$/,
|
|
140
|
+
// Known limit: images are copied as-is (no OCR).
|
|
141
|
+
/^xl\/media\/[^/]+$/,
|
|
142
|
+
// Rich data (data types, images in cells): values scrubbed by layer zero.
|
|
143
|
+
/^xl\/richData\/[^/]+\.xml$/,
|
|
144
|
+
/^xl\/(?:slicers|slicerCaches|timelines|timelineCaches|namedSheetViews|webextensions)\/[^/]+\.xml$/,
|
|
145
|
+
// customXml in a workbook holds Power Query mashups (base64) and data
|
|
146
|
+
// store properties; scrubbed by value, not extracted (T176 note).
|
|
147
|
+
/^customXml\/item(?:Props)?\d*\.xml$/,
|
|
148
|
+
];
|
|
149
|
+
export const XLSX_UNREDACTABLE_PARTS = [
|
|
150
|
+
{ pattern: /^xl\/embeddings\/[^/]+$/, kind: 'embedded-object', label: embeddedObjectLabel },
|
|
151
|
+
{ pattern: /^xl\/vba[A-Za-z]*\.(?:bin|xml)$/, kind: 'macros', label: 'macros' },
|
|
152
|
+
// Excel 4.0 macro sheets
|
|
153
|
+
{ pattern: /^xl\/macrosheets\/[^/]+\.xml$/, kind: 'macros', label: 'macro sheet' },
|
|
154
|
+
{ pattern: /^xl\/connections\.xml$/, kind: 'external-data', label: 'external data connection' },
|
|
155
|
+
{ pattern: /^xl\/queryTables\/[^/]+$/, kind: 'external-data', label: 'external data query' },
|
|
156
|
+
{ pattern: /^xl\/externalLinks\/[^/]+$/, kind: 'external-data', label: 'external workbook link' },
|
|
157
|
+
// Power Pivot data model (Analysis Services database with the source rows)
|
|
158
|
+
{ pattern: /^xl\/model\/[^/]+$/, kind: 'external-data', label: 'data model' },
|
|
159
|
+
{ pattern: /^xl\/printerSettings\/[^/]+$/, kind: 'printer-settings', label: 'printer settings' },
|
|
160
|
+
{ pattern: /^xl\/activeX\/[^/]+$/, kind: 'embedded-object', label: 'ActiveX control' },
|
|
161
|
+
];
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Classification
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
function tablesFor(kind) {
|
|
166
|
+
return kind === 'docx'
|
|
167
|
+
? { text: DOCX_TEXT_PARTS, structural: DOCX_STRUCTURAL_PARTS, unredactable: DOCX_UNREDACTABLE_PARTS }
|
|
168
|
+
: { text: XLSX_TEXT_PARTS, structural: XLSX_STRUCTURAL_PARTS, unredactable: XLSX_UNREDACTABLE_PARTS };
|
|
169
|
+
}
|
|
170
|
+
/** The unredactable rule that matches the part, if any. */
|
|
171
|
+
export function unredactableRuleFor(path, kind) {
|
|
172
|
+
for (const rule of tablesFor(kind).unredactable) {
|
|
173
|
+
if (rule.pattern.test(path))
|
|
174
|
+
return rule;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Classify one zip entry (relative path, no leading slash) of a docx or xlsx
|
|
180
|
+
* package. Unredactable rules win over the text and structural tables so a
|
|
181
|
+
* misplaced binary can never be declared structural by a broad pattern.
|
|
182
|
+
*/
|
|
183
|
+
export function classifyPart(path, kind) {
|
|
184
|
+
const tables = tablesFor(kind);
|
|
185
|
+
if (unredactableRuleFor(path, kind))
|
|
186
|
+
return 'unredactable';
|
|
187
|
+
if (tables.text.some((re) => re.test(path)))
|
|
188
|
+
return 'text';
|
|
189
|
+
if (tables.structural.some((re) => re.test(path)))
|
|
190
|
+
return 'structural';
|
|
191
|
+
return 'unknown';
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The UnredactablePart record for a part that classifies as unredactable
|
|
195
|
+
* or unknown (a text or structural part yields null).
|
|
196
|
+
*/
|
|
197
|
+
export function describeUnredactablePart(path, kind, contentType = null) {
|
|
198
|
+
const rule = unredactableRuleFor(path, kind);
|
|
199
|
+
if (rule) {
|
|
200
|
+
const label = typeof rule.label === 'function' ? rule.label(path, contentType) : rule.label;
|
|
201
|
+
return { part: path, kind: rule.kind, label };
|
|
202
|
+
}
|
|
203
|
+
if (classifyPart(path, kind) === 'unknown') {
|
|
204
|
+
return { part: path, kind: 'unknown', label: `unrecognised part ${path}` };
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
function extensionOf(path) {
|
|
209
|
+
const base = path.slice(path.lastIndexOf('/') + 1);
|
|
210
|
+
const dot = base.lastIndexOf('.');
|
|
211
|
+
return dot === -1 ? '' : base.slice(dot + 1).toLowerCase();
|
|
212
|
+
}
|
|
213
|
+
/** Parse [Content_Types].xml; an absent or malformed manifest yields empty maps. */
|
|
214
|
+
export async function readContentTypes(zip) {
|
|
215
|
+
const index = { defaults: new Map(), overrides: new Map() };
|
|
216
|
+
const file = zip.file('[Content_Types].xml');
|
|
217
|
+
if (!file)
|
|
218
|
+
return index;
|
|
219
|
+
const doc = new DOMParser().parseFromString(await file.async('string'), 'application/xml');
|
|
220
|
+
if (doc.getElementsByTagName('parsererror').length > 0)
|
|
221
|
+
return index;
|
|
222
|
+
const all = doc.getElementsByTagName('*');
|
|
223
|
+
for (let i = 0; i < all.length; i++) {
|
|
224
|
+
const el = all[i];
|
|
225
|
+
const type = el.getAttribute('ContentType');
|
|
226
|
+
if (!type)
|
|
227
|
+
continue;
|
|
228
|
+
if (el.localName === 'Default') {
|
|
229
|
+
const ext = el.getAttribute('Extension');
|
|
230
|
+
if (ext)
|
|
231
|
+
index.defaults.set(ext.toLowerCase(), type);
|
|
232
|
+
}
|
|
233
|
+
else if (el.localName === 'Override') {
|
|
234
|
+
const name = el.getAttribute('PartName');
|
|
235
|
+
if (name)
|
|
236
|
+
index.overrides.set(name.replace(/^\//, ''), type);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return index;
|
|
240
|
+
}
|
|
241
|
+
/** Media type of a part: its Override, else the Default for its extension. */
|
|
242
|
+
export function contentTypeOf(index, path) {
|
|
243
|
+
return index.overrides.get(path) ?? index.defaults.get(extensionOf(path)) ?? null;
|
|
244
|
+
}
|
|
245
|
+
/** Resolve a relationship target against the part that owns the relationship. */
|
|
246
|
+
export function resolvePartPath(ownerPart, target) {
|
|
247
|
+
const cleaned = target.replace(/^\.\//, '');
|
|
248
|
+
const base = cleaned.startsWith('/') ? [] : ownerPart.split('/').slice(0, -1);
|
|
249
|
+
const segments = [...base];
|
|
250
|
+
for (const segment of cleaned.replace(/^\//, '').split('/')) {
|
|
251
|
+
if (segment === '' || segment === '.')
|
|
252
|
+
continue;
|
|
253
|
+
if (segment === '..') {
|
|
254
|
+
segments.pop();
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
segments.push(segment);
|
|
258
|
+
}
|
|
259
|
+
return segments.join('/');
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The relationships of one part (its `_rels/<name>.rels` sibling), keyed by
|
|
263
|
+
* Id, with internal targets resolved to zip paths. Absent or malformed rels
|
|
264
|
+
* yield an empty map.
|
|
265
|
+
*/
|
|
266
|
+
export async function relationshipsOf(zip, partPath) {
|
|
267
|
+
const result = new Map();
|
|
268
|
+
const slash = partPath.lastIndexOf('/');
|
|
269
|
+
const dir = slash === -1 ? '' : partPath.slice(0, slash + 1);
|
|
270
|
+
const name = partPath.slice(slash + 1);
|
|
271
|
+
const file = zip.file(`${dir}_rels/${name}.rels`);
|
|
272
|
+
if (!file)
|
|
273
|
+
return result;
|
|
274
|
+
const doc = new DOMParser().parseFromString(await file.async('string'), 'application/xml');
|
|
275
|
+
if (doc.getElementsByTagName('parsererror').length > 0)
|
|
276
|
+
return result;
|
|
277
|
+
const rels = doc.getElementsByTagName('Relationship');
|
|
278
|
+
for (let i = 0; i < rels.length; i++) {
|
|
279
|
+
const rel = rels[i];
|
|
280
|
+
const id = rel.getAttribute('Id');
|
|
281
|
+
const target = rel.getAttribute('Target');
|
|
282
|
+
if (!id || target === null)
|
|
283
|
+
continue;
|
|
284
|
+
const external = rel.getAttribute('TargetMode') === 'External';
|
|
285
|
+
result.set(id, {
|
|
286
|
+
type: rel.getAttribute('Type') ?? '',
|
|
287
|
+
target,
|
|
288
|
+
external,
|
|
289
|
+
path: external ? null : resolvePartPath(partPath, target),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Every part of the package the writer would copy verbatim, with a kind and
|
|
296
|
+
* a human label, sorted by part name. Never throws: the decision what to do
|
|
297
|
+
* with the list belongs to the host (informed-consent export).
|
|
298
|
+
*/
|
|
299
|
+
export async function collectUnredactableParts(zip, kind, options = {}) {
|
|
300
|
+
const index = await readContentTypes(zip);
|
|
301
|
+
const chunks = new Set(options.htmlChunkParts ?? []);
|
|
302
|
+
const parts = [];
|
|
303
|
+
zip.forEach((path, entry) => {
|
|
304
|
+
if (entry.dir)
|
|
305
|
+
return;
|
|
306
|
+
if (chunks.has(path)) {
|
|
307
|
+
parts.push({ part: path, kind: 'html-chunk', label: 'HTML chunk' });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const described = describeUnredactablePart(path, kind, contentTypeOf(index, path));
|
|
311
|
+
if (described)
|
|
312
|
+
parts.push(described);
|
|
313
|
+
});
|
|
314
|
+
parts.sort((a, b) => (a.part < b.part ? -1 : a.part > b.part ? 1 : 0));
|
|
315
|
+
return parts;
|
|
316
|
+
}
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// Unpacked-size guard (L4, zip bomb)
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
/** Default limit on the sum of declared uncompressed entry sizes: 200 MB. */
|
|
321
|
+
export const MAX_UNPACKED_BYTES = 200 * 1024 * 1024;
|
|
322
|
+
/**
|
|
323
|
+
* Sum of the uncompressed sizes of every entry, from the zip directory (no
|
|
324
|
+
* inflation happens here). JSZip verifies the declared size against the
|
|
325
|
+
* inflated length when a part is read, so a directory that lies is refused
|
|
326
|
+
* later with a corrupted-zip error instead of being inflated past the limit.
|
|
327
|
+
*/
|
|
328
|
+
export function unpackedSize(zip) {
|
|
329
|
+
let total = 0;
|
|
330
|
+
zip.forEach((_path, entry) => {
|
|
331
|
+
if (entry.dir)
|
|
332
|
+
return;
|
|
333
|
+
const data = entry._data;
|
|
334
|
+
if (typeof data === 'string') {
|
|
335
|
+
total += data.length;
|
|
336
|
+
}
|
|
337
|
+
else if (data && typeof data === 'object') {
|
|
338
|
+
const size = data;
|
|
339
|
+
if (typeof size.uncompressedSize === 'number')
|
|
340
|
+
total += size.uncompressedSize;
|
|
341
|
+
else if (typeof size.byteLength === 'number')
|
|
342
|
+
total += size.byteLength;
|
|
343
|
+
else if (typeof size.length === 'number')
|
|
344
|
+
total += size.length;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
return total;
|
|
348
|
+
}
|
|
349
|
+
/** Throw UnsupportedDocumentError('too-large') when the package exceeds the limit. */
|
|
350
|
+
export function assertUnpackedSize(zip, limit = MAX_UNPACKED_BYTES) {
|
|
351
|
+
const total = unpackedSize(zip);
|
|
352
|
+
if (total > limit) {
|
|
353
|
+
throw new UnsupportedDocumentError('too-large', `Package unpacks to ${total} bytes, above the ${limit} byte limit`, [String(total), String(limit)]);
|
|
354
|
+
}
|
|
355
|
+
}
|
package/dist/dom/xlsx.d.ts
CHANGED
|
@@ -15,14 +15,19 @@
|
|
|
15
15
|
* - worksheet header/footer definitions
|
|
16
16
|
* - legacy cell comments (xl/comments*.xml) and threaded comments
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* - drawing text boxes and chart titles / string caches (xl/drawings,
|
|
19
|
+
* xl/charts) through the DrawingML units (T175)
|
|
20
|
+
*
|
|
21
|
+
* Deliberately out of scope (T176): sheet names, defined-name formulas, table
|
|
22
|
+
* column names, pivot caches; layer zero scrubs known values there. Data
|
|
23
|
+
* connections and external links are reported as unredactable (T177).
|
|
24
|
+
* Metadata scrubbing follows the docx module's policy via the shared OPC
|
|
25
|
+
* helpers.
|
|
22
26
|
*/
|
|
23
27
|
import JSZip from 'jszip';
|
|
24
28
|
import type { ValueReplacement } from '../docx.js';
|
|
25
29
|
import type { TextNodeMapping } from './docx.js';
|
|
30
|
+
import type { UnredactablePart } from './errors.js';
|
|
26
31
|
interface XlsxContentPart {
|
|
27
32
|
path: string;
|
|
28
33
|
xmlDoc: Document;
|
|
@@ -42,11 +47,53 @@ export interface XlsxExtraction {
|
|
|
42
47
|
contentParts: XlsxContentPart[];
|
|
43
48
|
/** All text node mappings across parts, in flat-text order */
|
|
44
49
|
textNodes: TextNodeMapping[];
|
|
50
|
+
/** True when no text-bearing content was found (T172). */
|
|
51
|
+
empty: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Parts the writer cannot redact (embedded objects, macros, data
|
|
54
|
+
* connections, external links, printer settings, unknown parts), from the
|
|
55
|
+
* package policy (T177). The writer refuses to export them unless
|
|
56
|
+
* allowUnredactable is passed.
|
|
57
|
+
*/
|
|
58
|
+
unredactable: UnredactablePart[];
|
|
59
|
+
/** Read-time notes (reserved; T172 reports none for xlsx). */
|
|
60
|
+
warnings: string[];
|
|
61
|
+
}
|
|
62
|
+
/** True for a SpreadsheetML element (transitional or strict namespace, T172). */
|
|
63
|
+
export declare function isSpreadsheetEl(el: Element, localName: string): boolean;
|
|
64
|
+
/** Options for readXlsx (T177). */
|
|
65
|
+
export interface XlsxReadOptions {
|
|
66
|
+
/**
|
|
67
|
+
* Limit on the sum of the declared uncompressed sizes of all zip entries
|
|
68
|
+
* (zip-bomb guard, L4); above it the reader throws
|
|
69
|
+
* UnsupportedDocumentError('too-large'). Defaults to MAX_UNPACKED_BYTES.
|
|
70
|
+
*/
|
|
71
|
+
maxUnpackedBytes?: number;
|
|
45
72
|
}
|
|
46
73
|
/**
|
|
47
74
|
* Read a .xlsx file and extract its text content with position mapping.
|
|
48
75
|
*/
|
|
49
|
-
export declare function readXlsx(file: File): Promise<XlsxExtraction>;
|
|
76
|
+
export declare function readXlsx(file: File, options?: XlsxReadOptions): Promise<XlsxExtraction>;
|
|
77
|
+
/** Result of writeAnonymizedXlsxWithReport (T172). */
|
|
78
|
+
export interface XlsxWriteResult {
|
|
79
|
+
/** The redacted .xlsx */
|
|
80
|
+
blob: Blob;
|
|
81
|
+
/**
|
|
82
|
+
* Parts the layer-zero scrub could not parse and left untouched, plus
|
|
83
|
+
* (with allowUnredactable) every unredactable part copied verbatim.
|
|
84
|
+
*/
|
|
85
|
+
warnings: string[];
|
|
86
|
+
}
|
|
87
|
+
/** Options for writeAnonymizedXlsx (T177). */
|
|
88
|
+
export interface XlsxWriteOptions {
|
|
89
|
+
/**
|
|
90
|
+
* Export even though extraction.unredactable is non-empty: the listed
|
|
91
|
+
* parts are copied verbatim and named in result.warnings. Default false:
|
|
92
|
+
* the writer throws UnsupportedDocumentError('unredactable-parts') before
|
|
93
|
+
* touching the extraction. See DocxWriteOptions.allowUnredactable.
|
|
94
|
+
*/
|
|
95
|
+
allowUnredactable?: boolean;
|
|
96
|
+
}
|
|
50
97
|
/**
|
|
51
98
|
* Apply text replacements to the xlsx XML, preserving formatting, then scrub
|
|
52
99
|
* metadata. Returns a new .xlsx file as a Blob; the input file is untouched.
|
|
@@ -55,7 +102,18 @@ export declare function writeAnonymizedXlsx(extraction: XlsxExtraction, replacem
|
|
|
55
102
|
start: number;
|
|
56
103
|
end: number;
|
|
57
104
|
replacement: string;
|
|
58
|
-
}>, valueReplacements?: ValueReplacement[]): Promise<Blob>;
|
|
105
|
+
}>, valueReplacements?: ValueReplacement[], options?: XlsxWriteOptions): Promise<Blob>;
|
|
106
|
+
/**
|
|
107
|
+
* Same as writeAnonymizedXlsx, with the write report. After the content
|
|
108
|
+
* parts and metadata, layer zero (T172) scrubs every known value from every
|
|
109
|
+
* other XML/rels/vml part: pivot cache records, drawings, charts, tables,
|
|
110
|
+
* defined names in the workbook, docProps.
|
|
111
|
+
*/
|
|
112
|
+
export declare function writeAnonymizedXlsxWithReport(extraction: XlsxExtraction, replacements: Array<{
|
|
113
|
+
start: number;
|
|
114
|
+
end: number;
|
|
115
|
+
replacement: string;
|
|
116
|
+
}>, valueReplacements?: ValueReplacement[], options?: XlsxWriteOptions): Promise<XlsxWriteResult>;
|
|
59
117
|
/**
|
|
60
118
|
* Check if a file is a supported Excel workbook (.xlsx only; the legacy
|
|
61
119
|
* binary .xls format is not supported in phase 1).
|