@opengeni/artifact-tool 0.2.2
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/LICENSE +190 -0
- package/README.md +104 -0
- package/dist/bounded-zip.d.ts +28 -0
- package/dist/chunk-4BSENMAE.js +39 -0
- package/dist/chunk-4BSENMAE.js.map +1 -0
- package/dist/chunk-4FRFNNBM.js +276 -0
- package/dist/chunk-4FRFNNBM.js.map +1 -0
- package/dist/chunk-5XIA6KHZ.js +49 -0
- package/dist/chunk-5XIA6KHZ.js.map +1 -0
- package/dist/chunk-66CT2ZRP.js +2229 -0
- package/dist/chunk-66CT2ZRP.js.map +1 -0
- package/dist/chunk-72SYDU4O.js +319 -0
- package/dist/chunk-72SYDU4O.js.map +1 -0
- package/dist/chunk-B2ZJRZDA.js +279 -0
- package/dist/chunk-B2ZJRZDA.js.map +1 -0
- package/dist/chunk-DCTLTNPW.js +2458 -0
- package/dist/chunk-DCTLTNPW.js.map +1 -0
- package/dist/chunk-FGZM4FQ6.js +473 -0
- package/dist/chunk-FGZM4FQ6.js.map +1 -0
- package/dist/chunk-HTJBLBDE.js +591 -0
- package/dist/chunk-HTJBLBDE.js.map +1 -0
- package/dist/chunk-IYFXZQ34.js +5306 -0
- package/dist/chunk-IYFXZQ34.js.map +1 -0
- package/dist/chunk-J6N7IR44.js +172 -0
- package/dist/chunk-J6N7IR44.js.map +1 -0
- package/dist/chunk-N2IHNWBS.js +1002 -0
- package/dist/chunk-N2IHNWBS.js.map +1 -0
- package/dist/chunk-NAG44K67.js +549 -0
- package/dist/chunk-NAG44K67.js.map +1 -0
- package/dist/chunk-O24KTEGP.js +33 -0
- package/dist/chunk-O24KTEGP.js.map +1 -0
- package/dist/chunk-PI2AZVGK.js +2074 -0
- package/dist/chunk-PI2AZVGK.js.map +1 -0
- package/dist/chunk-Q2FFHEY7.js +40 -0
- package/dist/chunk-Q2FFHEY7.js.map +1 -0
- package/dist/chunk-RBGQADXA.js +57 -0
- package/dist/chunk-RBGQADXA.js.map +1 -0
- package/dist/chunk-U5G2BKM2.js +613 -0
- package/dist/chunk-U5G2BKM2.js.map +1 -0
- package/dist/chunk-VOJHMSM3.js +39 -0
- package/dist/chunk-VOJHMSM3.js.map +1 -0
- package/dist/chunk-VUNV25KB.js +16 -0
- package/dist/chunk-VUNV25KB.js.map +1 -0
- package/dist/chunk-XIFUNVBO.js +101 -0
- package/dist/chunk-XIFUNVBO.js.map +1 -0
- package/dist/chunk-ZCJYRJGB.js +489 -0
- package/dist/chunk-ZCJYRJGB.js.map +1 -0
- package/dist/chunk-ZZXPEE4S.js +2260 -0
- package/dist/chunk-ZZXPEE4S.js.map +1 -0
- package/dist/document-docx-api.d.ts +49 -0
- package/dist/document-docx-codec.d.ts +9 -0
- package/dist/document-docx-codec.js +4765 -0
- package/dist/document-docx-codec.js.map +1 -0
- package/dist/document-docx-import.d.ts +261 -0
- package/dist/document-docx-packer.d.ts +18 -0
- package/dist/document-docx-state.d.ts +12 -0
- package/dist/document-render.d.ts +3 -0
- package/dist/document-render.js +492 -0
- package/dist/document-render.js.map +1 -0
- package/dist/document.d.ts +432 -0
- package/dist/document.js +37 -0
- package/dist/document.js.map +1 -0
- package/dist/errors.d.ts +23 -0
- package/dist/file-blob.d.ts +11 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +186 -0
- package/dist/index.js.map +1 -0
- package/dist/inspection.d.ts +12 -0
- package/dist/kernel.d.ts +101 -0
- package/dist/lib-GQLPACFK.js +9746 -0
- package/dist/lib-GQLPACFK.js.map +1 -0
- package/dist/materializer-cli-entry.d.ts +2 -0
- package/dist/materializer-cli-entry.js +29 -0
- package/dist/materializer-cli-entry.js.map +1 -0
- package/dist/materializer-cli.d.ts +24 -0
- package/dist/materializer-cli.js +24 -0
- package/dist/materializer-cli.js.map +1 -0
- package/dist/native-raster.d.ts +10 -0
- package/dist/native.d.ts +129 -0
- package/dist/native.js +31 -0
- package/dist/native.js.map +1 -0
- package/dist/office-import.d.ts +48 -0
- package/dist/office-import.js +154 -0
- package/dist/office-import.js.map +1 -0
- package/dist/presentation-pptx-api.d.ts +60 -0
- package/dist/presentation-pptx-import.d.ts +5 -0
- package/dist/presentation-pptx-state-digest-765CZZED.js +10 -0
- package/dist/presentation-pptx-state-digest-765CZZED.js.map +1 -0
- package/dist/presentation-pptx-state-digest.d.ts +3 -0
- package/dist/presentation-pptx-state.d.ts +4 -0
- package/dist/presentation-pptx-xml.d.ts +29 -0
- package/dist/presentation-pptx.d.ts +5 -0
- package/dist/presentation-pptx.js +2365 -0
- package/dist/presentation-pptx.js.map +1 -0
- package/dist/presentation-render.d.ts +29 -0
- package/dist/presentation-render.js +375 -0
- package/dist/presentation-render.js.map +1 -0
- package/dist/presentation.d.ts +565 -0
- package/dist/presentation.js +71 -0
- package/dist/presentation.js.map +1 -0
- package/dist/production-composite.d.ts +68 -0
- package/dist/production-document.d.ts +2 -0
- package/dist/production-document.js +176 -0
- package/dist/production-document.js.map +1 -0
- package/dist/production-native-codecs.d.ts +94 -0
- package/dist/production-presentation.d.ts +2 -0
- package/dist/production-presentation.js +176 -0
- package/dist/production-presentation.js.map +1 -0
- package/dist/production-sha256.d.ts +2 -0
- package/dist/production-spreadsheet-native.d.ts +12 -0
- package/dist/production-spreadsheet.d.ts +2 -0
- package/dist/production-spreadsheet.js +176 -0
- package/dist/production-spreadsheet.js.map +1 -0
- package/dist/production.d.ts +96 -0
- package/dist/raster-image.d.ts +34 -0
- package/dist/reference.d.ts +17 -0
- package/dist/reference.js +574 -0
- package/dist/reference.js.map +1 -0
- package/dist/runtime-cli-entry.d.ts +2 -0
- package/dist/runtime-cli-entry.js +21 -0
- package/dist/runtime-cli-entry.js.map +1 -0
- package/dist/runtime-cli.d.ts +76 -0
- package/dist/runtime-cli.js +26 -0
- package/dist/runtime-cli.js.map +1 -0
- package/dist/runtime-development.d.ts +73 -0
- package/dist/runtime-development.js +30 -0
- package/dist/runtime-development.js.map +1 -0
- package/dist/runtime-receipt.d.ts +19 -0
- package/dist/runtime.d.ts +355 -0
- package/dist/runtime.js +42 -0
- package/dist/runtime.js.map +1 -0
- package/dist/snapshot.d.ts +26 -0
- package/dist/snapshot.js +61 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/spreadsheet-address.d.ts +18 -0
- package/dist/spreadsheet-file.d.ts +17 -0
- package/dist/spreadsheet-formula.d.ts +67 -0
- package/dist/spreadsheet-image.d.ts +17 -0
- package/dist/spreadsheet-render.d.ts +5 -0
- package/dist/spreadsheet-render.js +752 -0
- package/dist/spreadsheet-render.js.map +1 -0
- package/dist/spreadsheet-types.d.ts +155 -0
- package/dist/spreadsheet-xlsx-api.d.ts +88 -0
- package/dist/spreadsheet-xlsx-codec.d.ts +16 -0
- package/dist/spreadsheet-xlsx-codec.js +16 -0
- package/dist/spreadsheet-xlsx-codec.js.map +1 -0
- package/dist/spreadsheet-xlsx-state.d.ts +16 -0
- package/dist/spreadsheet.d.ts +724 -0
- package/dist/spreadsheet.js +67 -0
- package/dist/spreadsheet.js.map +1 -0
- package/package.json +129 -0
- package/src/bounded-zip.ts +450 -0
- package/src/document-docx-api.ts +64 -0
- package/src/document-docx-codec.ts +2253 -0
- package/src/document-docx-import.ts +3814 -0
- package/src/document-docx-packer.ts +72 -0
- package/src/document-docx-state.ts +59 -0
- package/src/document-render.ts +615 -0
- package/src/document.ts +2800 -0
- package/src/errors.ts +45 -0
- package/src/file-blob.ts +43 -0
- package/src/index.ts +4 -0
- package/src/inspection.ts +50 -0
- package/src/kernel.ts +525 -0
- package/src/materializer-cli-entry.ts +11 -0
- package/src/materializer-cli.ts +743 -0
- package/src/native-raster.ts +141 -0
- package/src/native.ts +390 -0
- package/src/office-import.ts +243 -0
- package/src/presentation-pptx-api.ts +107 -0
- package/src/presentation-pptx-import.ts +2065 -0
- package/src/presentation-pptx-state-digest.ts +46 -0
- package/src/presentation-pptx-state.ts +18 -0
- package/src/presentation-pptx-xml.ts +473 -0
- package/src/presentation-pptx.ts +447 -0
- package/src/presentation-render.ts +532 -0
- package/src/presentation.ts +2599 -0
- package/src/production-composite.ts +805 -0
- package/src/production-document.ts +2 -0
- package/src/production-native-codecs.ts +1141 -0
- package/src/production-presentation.ts +2 -0
- package/src/production-sha256.ts +150 -0
- package/src/production-spreadsheet-native.ts +686 -0
- package/src/production-spreadsheet.ts +2 -0
- package/src/production.ts +1022 -0
- package/src/raster-image.ts +586 -0
- package/src/reference.ts +17 -0
- package/src/runtime-cli-entry.ts +13 -0
- package/src/runtime-cli.ts +670 -0
- package/src/runtime-development.ts +689 -0
- package/src/runtime-receipt.ts +169 -0
- package/src/runtime.ts +1463 -0
- package/src/snapshot.ts +88 -0
- package/src/spreadsheet-address.ts +90 -0
- package/src/spreadsheet-file.ts +71 -0
- package/src/spreadsheet-formula.ts +1212 -0
- package/src/spreadsheet-image.ts +242 -0
- package/src/spreadsheet-render.ts +985 -0
- package/src/spreadsheet-types.ts +171 -0
- package/src/spreadsheet-xlsx-api.ts +123 -0
- package/src/spreadsheet-xlsx-codec.ts +3063 -0
- package/src/spreadsheet-xlsx-state.ts +191 -0
- package/src/spreadsheet.ts +5195 -0
|
@@ -0,0 +1,2458 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LOSS_PRESERVATION,
|
|
3
|
+
SpreadsheetFidelityError,
|
|
4
|
+
SpreadsheetSecurityError,
|
|
5
|
+
XLSX_MEDIA_TYPE,
|
|
6
|
+
cloneOpaqueContent,
|
|
7
|
+
hasOpaqueContent,
|
|
8
|
+
opaqueContentIssues,
|
|
9
|
+
spreadsheetFidelityReport,
|
|
10
|
+
spreadsheetLossPreservationEnvelope
|
|
11
|
+
} from "./chunk-J6N7IR44.js";
|
|
12
|
+
import {
|
|
13
|
+
Workbook,
|
|
14
|
+
validateSupportedFormula
|
|
15
|
+
} from "./chunk-IYFXZQ34.js";
|
|
16
|
+
import {
|
|
17
|
+
formatCellAddress,
|
|
18
|
+
formatRangeAddress,
|
|
19
|
+
normalizeSpreadsheetImageConfig,
|
|
20
|
+
parseRangeAddress
|
|
21
|
+
} from "./chunk-4FRFNNBM.js";
|
|
22
|
+
import {
|
|
23
|
+
inflateBoundedZipEntry,
|
|
24
|
+
parseBoundedZip,
|
|
25
|
+
verifyBoundedZipEntry
|
|
26
|
+
} from "./chunk-72SYDU4O.js";
|
|
27
|
+
import {
|
|
28
|
+
boundedUtf8ByteLength,
|
|
29
|
+
inspectRasterImage
|
|
30
|
+
} from "./chunk-FGZM4FQ6.js";
|
|
31
|
+
import {
|
|
32
|
+
FileBlob
|
|
33
|
+
} from "./chunk-4BSENMAE.js";
|
|
34
|
+
|
|
35
|
+
// src/spreadsheet-xlsx-codec.ts
|
|
36
|
+
import { SaxesParser } from "saxes";
|
|
37
|
+
var DEFAULT_XLSX_IMPORT_LIMITS = Object.freeze({
|
|
38
|
+
compressedBytes: 64 * 1024 * 1024,
|
|
39
|
+
expandedBytes: 512 * 1024 * 1024,
|
|
40
|
+
entryBytes: 128 * 1024 * 1024,
|
|
41
|
+
entries: 2e4,
|
|
42
|
+
compressionRatio: 100,
|
|
43
|
+
inspectedXmlBytes: 64 * 1024 * 1024,
|
|
44
|
+
xmlDepth: 128,
|
|
45
|
+
xmlAttributesPerElement: 256,
|
|
46
|
+
xmlAttributesPerPart: 1e5,
|
|
47
|
+
xmlAttributes: 5e5,
|
|
48
|
+
xmlElementsPerPart: 1e6,
|
|
49
|
+
xmlElements: 2e6,
|
|
50
|
+
xmlTextCharactersPerPart: 16 * 1024 * 1024,
|
|
51
|
+
xmlTextCharacters: 64 * 1024 * 1024,
|
|
52
|
+
worksheetCellsPerPart: 1e6,
|
|
53
|
+
worksheetCells: 1e6,
|
|
54
|
+
formulasPerPart: 25e4,
|
|
55
|
+
formulas: 1e6,
|
|
56
|
+
formulaBytesPerPart: 16 * 1024 * 1024,
|
|
57
|
+
formulaBytes: 64 * 1024 * 1024,
|
|
58
|
+
sharedStrings: 1e6,
|
|
59
|
+
cellStyles: 1e5,
|
|
60
|
+
styleRecords: 25e4,
|
|
61
|
+
commentsPerPart: 1e5,
|
|
62
|
+
comments: 1e5,
|
|
63
|
+
relationshipsPerPart: 4096,
|
|
64
|
+
relationships: 2e4,
|
|
65
|
+
mediaEntries: 1024,
|
|
66
|
+
mediaEntryBytes: 32 * 1024 * 1024,
|
|
67
|
+
mediaBytes: 128 * 1024 * 1024,
|
|
68
|
+
imagePixels: 67108864
|
|
69
|
+
});
|
|
70
|
+
var SpreadsheetXlsxCodec = class {
|
|
71
|
+
static async importXlsx(input, options = {}) {
|
|
72
|
+
validateImportOptions(options);
|
|
73
|
+
const limits = resolveImportLimits(options.limits);
|
|
74
|
+
const sourceBytes = await toBytes(input, limits.compressedBytes);
|
|
75
|
+
const preflight = await preflightXlsx(sourceBytes, limits);
|
|
76
|
+
if (hasOpaqueContent(preflight.opaqueContent) && options.unsupportedContent === "error") {
|
|
77
|
+
throw new SpreadsheetFidelityError(
|
|
78
|
+
"The XLSX contains bounded inert OOXML outside the editable spreadsheet model",
|
|
79
|
+
opaqueContentIssues(preflight.opaqueContent, "error")
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const excelWorkbook = await createExcelWorkbook();
|
|
83
|
+
await excelWorkbook.xlsx.load(
|
|
84
|
+
sourceBytes.buffer
|
|
85
|
+
);
|
|
86
|
+
const workbook = importWorkbook(excelWorkbook, preflight.comments);
|
|
87
|
+
LOSS_PRESERVATION.set(workbook, {
|
|
88
|
+
sourceBytes,
|
|
89
|
+
sourceDigest: await sha256Hex(sourceBytes),
|
|
90
|
+
importedRevision: workbook.revision,
|
|
91
|
+
opaqueContent: cloneOpaqueContent(preflight.opaqueContent),
|
|
92
|
+
modelDigest: await workbookModelDigest(workbook)
|
|
93
|
+
});
|
|
94
|
+
return workbook;
|
|
95
|
+
}
|
|
96
|
+
static async exportXlsx(workbook, options = {}) {
|
|
97
|
+
validateExportOptions(options);
|
|
98
|
+
workbook.toJSON();
|
|
99
|
+
const envelope = LOSS_PRESERVATION.get(workbook);
|
|
100
|
+
if (envelope && await workbookModelDigest(workbook) === envelope.modelDigest) {
|
|
101
|
+
return FileBlob.fromBytes(envelope.sourceBytes.slice(), {
|
|
102
|
+
type: XLSX_MEDIA_TYPE,
|
|
103
|
+
name: options.fileName ?? "workbook.xlsx"
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const issues = this.fidelityReport(workbook).filter((issue) => issue.severity === "error");
|
|
107
|
+
if (issues.length > 0 && options.unsupportedContent !== "discard") {
|
|
108
|
+
throw new SpreadsheetFidelityError(
|
|
109
|
+
'XLSX export would discard unsupported editable content; remove it or explicitly export with unsupportedContent: "discard"',
|
|
110
|
+
issues
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const excelWorkbook = await exportWorkbook(workbook);
|
|
114
|
+
const output = await excelWorkbook.xlsx.writeBuffer({
|
|
115
|
+
zip: { compression: "DEFLATE", compressionOptions: { level: 6 } }
|
|
116
|
+
});
|
|
117
|
+
return FileBlob.fromBytes(normalizeGeneratedZip(new Uint8Array(output)), {
|
|
118
|
+
type: XLSX_MEDIA_TYPE,
|
|
119
|
+
name: options.fileName ?? "workbook.xlsx"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
static fidelityReport(workbook) {
|
|
123
|
+
workbook.toJSON();
|
|
124
|
+
return spreadsheetFidelityReport(workbook);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Returns a portable copy of the opaque original-package envelope.
|
|
128
|
+
* Persist it beside Workbook.toJSON(); the Workbook JSON intentionally does not contain Office bytes.
|
|
129
|
+
*/
|
|
130
|
+
static lossPreservationEnvelope(workbook) {
|
|
131
|
+
workbook.toJSON();
|
|
132
|
+
return spreadsheetLossPreservationEnvelope(workbook);
|
|
133
|
+
}
|
|
134
|
+
/** Reattaches a separately persisted envelope to a restored canonical workbook snapshot. */
|
|
135
|
+
static async attachLossPreservationEnvelope(workbook, envelope, options = {}) {
|
|
136
|
+
workbook.toJSON();
|
|
137
|
+
validateAttachOptions(options);
|
|
138
|
+
const normalizedEnvelope = normalizeLossPreservationEnvelope(envelope);
|
|
139
|
+
const sourceBytes = normalizedEnvelope.sourceBytes;
|
|
140
|
+
if (normalizedEnvelope.sourceDigest !== await sha256Hex(sourceBytes)) {
|
|
141
|
+
throw new SpreadsheetSecurityError(
|
|
142
|
+
"Spreadsheet loss-preservation source digest does not match its bytes",
|
|
143
|
+
"invalid-package"
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const preflight = await preflightXlsx(sourceBytes, resolveImportLimits(options.limits));
|
|
147
|
+
if (!sameOpaqueContent(preflight.opaqueContent, normalizedEnvelope.opaqueContent)) {
|
|
148
|
+
throw new SpreadsheetSecurityError(
|
|
149
|
+
"Spreadsheet loss-preservation envelope metadata does not match its XLSX bytes",
|
|
150
|
+
"invalid-package"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const projectedSource = await createExcelWorkbook();
|
|
154
|
+
await projectedSource.xlsx.load(
|
|
155
|
+
sourceBytes.buffer
|
|
156
|
+
);
|
|
157
|
+
const projectedDigest = await workbookModelDigest(
|
|
158
|
+
importWorkbook(projectedSource, preflight.comments)
|
|
159
|
+
);
|
|
160
|
+
const targetDigest = await workbookModelDigest(workbook);
|
|
161
|
+
if (projectedDigest !== normalizedEnvelope.modelDigest || targetDigest !== normalizedEnvelope.modelDigest) {
|
|
162
|
+
throw new SpreadsheetSecurityError(
|
|
163
|
+
"Spreadsheet loss-preservation envelope belongs to a different workbook snapshot",
|
|
164
|
+
"invalid-package"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
LOSS_PRESERVATION.set(workbook, {
|
|
168
|
+
sourceBytes,
|
|
169
|
+
sourceDigest: normalizedEnvelope.sourceDigest,
|
|
170
|
+
importedRevision: workbook.revision,
|
|
171
|
+
opaqueContent: cloneOpaqueContent(preflight.opaqueContent),
|
|
172
|
+
modelDigest: normalizedEnvelope.modelDigest
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
function normalizeLossPreservationEnvelope(input) {
|
|
177
|
+
if (input === null || typeof input !== "object" || Array.isArray(input) || Object.getPrototypeOf(input) !== Object.prototype) {
|
|
178
|
+
throw new TypeError("Spreadsheet loss-preservation envelope must be a plain object");
|
|
179
|
+
}
|
|
180
|
+
const record = input;
|
|
181
|
+
const required = /* @__PURE__ */ new Set([
|
|
182
|
+
"version",
|
|
183
|
+
"mediaType",
|
|
184
|
+
"sourceBytes",
|
|
185
|
+
"sourceDigest",
|
|
186
|
+
"opaqueContent",
|
|
187
|
+
"modelDigest"
|
|
188
|
+
]);
|
|
189
|
+
const allowed = required;
|
|
190
|
+
for (const key of Reflect.ownKeys(record)) {
|
|
191
|
+
if (typeof key !== "string" || !allowed.has(key)) {
|
|
192
|
+
throw new TypeError("Spreadsheet loss-preservation envelope contains an unknown property");
|
|
193
|
+
}
|
|
194
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
195
|
+
if (!descriptor || !("value" in descriptor)) {
|
|
196
|
+
throw new TypeError(`Spreadsheet loss-preservation envelope ${key} must be plain data`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const key of required) {
|
|
200
|
+
if (!Object.hasOwn(record, key)) {
|
|
201
|
+
throw new TypeError(`Spreadsheet loss-preservation envelope ${key} is required`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (record.version !== 1 || record.mediaType !== XLSX_MEDIA_TYPE) {
|
|
205
|
+
throw new TypeError("Unsupported spreadsheet loss-preservation envelope");
|
|
206
|
+
}
|
|
207
|
+
const source = record.sourceBytes;
|
|
208
|
+
if (!(source instanceof Uint8Array) || Object.getPrototypeOf(source) !== Uint8Array.prototype || source.byteLength > DEFAULT_XLSX_IMPORT_LIMITS.compressedBytes) {
|
|
209
|
+
throw new TypeError("Spreadsheet loss-preservation sourceBytes must be a bounded Uint8Array");
|
|
210
|
+
}
|
|
211
|
+
if (typeof record.sourceDigest !== "string" || !/^[0-9a-f]{64}$/.test(record.sourceDigest)) {
|
|
212
|
+
throw new TypeError("Spreadsheet loss-preservation sourceDigest must be canonical SHA-256");
|
|
213
|
+
}
|
|
214
|
+
const opaqueContent = normalizeOpaqueContent(record.opaqueContent);
|
|
215
|
+
if (typeof record.modelDigest !== "string" || !/^[0-9a-f]{64}$/.test(record.modelDigest)) {
|
|
216
|
+
throw new TypeError("Spreadsheet loss-preservation modelDigest must be canonical SHA-256");
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
sourceBytes: Uint8Array.prototype.slice.call(source),
|
|
220
|
+
sourceDigest: record.sourceDigest,
|
|
221
|
+
opaqueContent,
|
|
222
|
+
modelDigest: record.modelDigest
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function normalizeOpaqueContent(input) {
|
|
226
|
+
assertPlainDataRecord(
|
|
227
|
+
input,
|
|
228
|
+
/* @__PURE__ */ new Set(["parts", "relationships", "contentTypes", "features"]),
|
|
229
|
+
"Spreadsheet opaque-content metadata"
|
|
230
|
+
);
|
|
231
|
+
const parts = denseDataArray(
|
|
232
|
+
input.parts,
|
|
233
|
+
DEFAULT_XLSX_IMPORT_LIMITS.entries,
|
|
234
|
+
"Spreadsheet opaque part list"
|
|
235
|
+
).map((part) => {
|
|
236
|
+
if (typeof part !== "string" || !isCanonicalPartName(part)) {
|
|
237
|
+
throw new TypeError("Spreadsheet opaque part name is invalid");
|
|
238
|
+
}
|
|
239
|
+
return part;
|
|
240
|
+
});
|
|
241
|
+
const relationships = denseDataArray(
|
|
242
|
+
input.relationships,
|
|
243
|
+
DEFAULT_XLSX_IMPORT_LIMITS.relationships,
|
|
244
|
+
"Spreadsheet opaque relationship list"
|
|
245
|
+
).map((relationship) => {
|
|
246
|
+
assertPlainDataRecord(
|
|
247
|
+
relationship,
|
|
248
|
+
/* @__PURE__ */ new Set(["sourcePart", "type", "targetPart"]),
|
|
249
|
+
"Spreadsheet opaque relationship"
|
|
250
|
+
);
|
|
251
|
+
const { sourcePart, type, targetPart } = relationship;
|
|
252
|
+
if (typeof sourcePart !== "string" || sourcePart !== "" && !isCanonicalPartName(sourcePart) || typeof type !== "string" || type.length === 0 || type.length > 2048 || !/^[\x21-\x7e]+$/.test(type) || typeof targetPart !== "string" || !isCanonicalPartName(targetPart)) {
|
|
253
|
+
throw new TypeError("Spreadsheet opaque relationship metadata is invalid");
|
|
254
|
+
}
|
|
255
|
+
return { sourcePart, type, targetPart };
|
|
256
|
+
});
|
|
257
|
+
const contentTypes = denseDataArray(
|
|
258
|
+
input.contentTypes,
|
|
259
|
+
DEFAULT_XLSX_IMPORT_LIMITS.entries,
|
|
260
|
+
"Spreadsheet opaque content-type list"
|
|
261
|
+
).map((contentType) => {
|
|
262
|
+
assertPlainDataRecord(
|
|
263
|
+
contentType,
|
|
264
|
+
/* @__PURE__ */ new Set(["partName", "contentType"]),
|
|
265
|
+
"Spreadsheet opaque content type"
|
|
266
|
+
);
|
|
267
|
+
if (typeof contentType.partName !== "string" || !isCanonicalPartName(contentType.partName) || typeof contentType.contentType !== "string" || contentType.contentType.length === 0 || contentType.contentType.length > 2048 || !/^[\x21-\x7e]+$/.test(contentType.contentType)) {
|
|
268
|
+
throw new TypeError("Spreadsheet opaque content-type metadata is invalid");
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
partName: contentType.partName,
|
|
272
|
+
contentType: contentType.contentType
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
const features = denseDataArray(input.features, 4096, "Spreadsheet opaque feature list").map(
|
|
276
|
+
(feature) => {
|
|
277
|
+
if (typeof feature !== "string" || !/^[a-z0-9][a-z0-9._-]{0,127}$/.test(feature)) {
|
|
278
|
+
throw new TypeError("Spreadsheet opaque feature name is invalid");
|
|
279
|
+
}
|
|
280
|
+
return feature;
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
return {
|
|
284
|
+
parts: canonicalUnique(parts, compareStrings, "Spreadsheet opaque part list"),
|
|
285
|
+
relationships: canonicalUnique(
|
|
286
|
+
relationships,
|
|
287
|
+
compareOpaqueRelationships,
|
|
288
|
+
"Spreadsheet opaque relationship list"
|
|
289
|
+
),
|
|
290
|
+
contentTypes: canonicalUnique(
|
|
291
|
+
contentTypes,
|
|
292
|
+
compareOpaqueContentTypes,
|
|
293
|
+
"Spreadsheet opaque content-type list"
|
|
294
|
+
),
|
|
295
|
+
features: canonicalUnique(features, compareStrings, "Spreadsheet opaque feature list")
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function canonicalUnique(values, compare, label) {
|
|
299
|
+
const sorted = [...values].sort(compare);
|
|
300
|
+
for (let index = 1; index < sorted.length; index += 1) {
|
|
301
|
+
if (compare(sorted[index - 1], sorted[index]) === 0) {
|
|
302
|
+
throw new TypeError(`${label} contains duplicate metadata`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return sorted;
|
|
306
|
+
}
|
|
307
|
+
function denseDataArray(value, maximum, label) {
|
|
308
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || value.length > maximum) {
|
|
309
|
+
throw new TypeError(`${label} must be a bounded ordinary array`);
|
|
310
|
+
}
|
|
311
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
312
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
313
|
+
const descriptor = descriptors[String(index)];
|
|
314
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
315
|
+
throw new TypeError(`${label} must contain only dense data elements`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const key of Object.keys(descriptors)) {
|
|
319
|
+
if (key !== "length" && !/^(?:0|[1-9][0-9]*)$/.test(key)) {
|
|
320
|
+
throw new TypeError(`${label} contains an unknown property`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return Array.prototype.slice.call(value);
|
|
324
|
+
}
|
|
325
|
+
function assertPlainDataRecord(value, allowedKeys, label) {
|
|
326
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
327
|
+
throw new TypeError(`${label} must be a plain data object`);
|
|
328
|
+
}
|
|
329
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
330
|
+
throw new TypeError(`${label} must not contain symbol properties`);
|
|
331
|
+
}
|
|
332
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
333
|
+
if (!("value" in descriptor) || !descriptor.enumerable) {
|
|
334
|
+
throw new TypeError(`${label} must contain only enumerable data properties`);
|
|
335
|
+
}
|
|
336
|
+
if (!allowedKeys.has(key)) throw new TypeError(`${label} contains unknown property ${key}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function sameOpaqueContent(left, right) {
|
|
340
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
341
|
+
}
|
|
342
|
+
function validateImportOptions(options) {
|
|
343
|
+
assertPlainDataRecord(options, /* @__PURE__ */ new Set(["unsupportedContent", "limits"]), "XLSX import options");
|
|
344
|
+
if (options.unsupportedContent !== void 0 && options.unsupportedContent !== "preserve" && options.unsupportedContent !== "error") {
|
|
345
|
+
throw new TypeError("XLSX unsupportedContent import policy is invalid");
|
|
346
|
+
}
|
|
347
|
+
if (options.limits !== void 0) {
|
|
348
|
+
assertPlainDataRecord(
|
|
349
|
+
options.limits,
|
|
350
|
+
new Set(Object.keys(DEFAULT_XLSX_IMPORT_LIMITS)),
|
|
351
|
+
"XLSX import limits"
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function validateExportOptions(options) {
|
|
356
|
+
assertPlainDataRecord(
|
|
357
|
+
options,
|
|
358
|
+
/* @__PURE__ */ new Set(["fileName", "unsupportedContent"]),
|
|
359
|
+
"XLSX export options"
|
|
360
|
+
);
|
|
361
|
+
if (options.unsupportedContent !== void 0 && options.unsupportedContent !== "error" && options.unsupportedContent !== "discard") {
|
|
362
|
+
throw new TypeError("XLSX unsupportedContent export policy is invalid");
|
|
363
|
+
}
|
|
364
|
+
if (options.fileName !== void 0 && (typeof options.fileName !== "string" || options.fileName.length === 0 || options.fileName.length > 255 || /[\u0000-\u001f\u007f]/.test(options.fileName))) {
|
|
365
|
+
throw new TypeError("XLSX export fileName must contain 1 through 255 safe characters");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function validateAttachOptions(options) {
|
|
369
|
+
assertPlainDataRecord(options, /* @__PURE__ */ new Set(["limits"]), "XLSX envelope attach options");
|
|
370
|
+
if (options.limits !== void 0) {
|
|
371
|
+
assertPlainDataRecord(
|
|
372
|
+
options.limits,
|
|
373
|
+
new Set(Object.keys(DEFAULT_XLSX_IMPORT_LIMITS)),
|
|
374
|
+
"XLSX import limits"
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function createExcelWorkbook() {
|
|
379
|
+
const module = await import("exceljs");
|
|
380
|
+
const runtime = module.default ?? module;
|
|
381
|
+
return new runtime.Workbook();
|
|
382
|
+
}
|
|
383
|
+
function importWorkbook(source, legacyComments = []) {
|
|
384
|
+
const workbook = Workbook.create();
|
|
385
|
+
workbook.transact(() => {
|
|
386
|
+
for (const sourceSheet of source.worksheets) {
|
|
387
|
+
const worksheet = workbook.worksheets.add(sourceSheet.name);
|
|
388
|
+
const view = sourceSheet.views[0];
|
|
389
|
+
worksheet.showGridLines = view?.showGridLines ?? true;
|
|
390
|
+
if (view?.state === "frozen") {
|
|
391
|
+
if (view.ySplit) worksheet.freezePanes.freezeRows(view.ySplit);
|
|
392
|
+
if (view.xSplit) worksheet.freezePanes.freezeColumns(view.xSplit);
|
|
393
|
+
}
|
|
394
|
+
sourceSheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
|
|
395
|
+
if (row.height && row.height > 0)
|
|
396
|
+
worksheet.setRowHeight(rowNumber - 1, pointsToPixels(row.height));
|
|
397
|
+
row.eachCell({ includeEmpty: false }, (cell, columnNumber) => {
|
|
398
|
+
const value = importCellValue(cell.value);
|
|
399
|
+
const formula = formulaOf(cell.value);
|
|
400
|
+
worksheet.setCell(
|
|
401
|
+
rowNumber - 1,
|
|
402
|
+
columnNumber - 1,
|
|
403
|
+
{
|
|
404
|
+
value: formula ? null : value,
|
|
405
|
+
formula,
|
|
406
|
+
format: importCellFormat(cell)
|
|
407
|
+
},
|
|
408
|
+
"content"
|
|
409
|
+
);
|
|
410
|
+
importDataValidation(worksheet, rowNumber - 1, columnNumber - 1, cell.dataValidation);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
for (let columnNumber = 1; columnNumber <= sourceSheet.columnCount; columnNumber += 1) {
|
|
414
|
+
const width = sourceSheet.getColumn(columnNumber).width;
|
|
415
|
+
if (width && width > 0)
|
|
416
|
+
worksheet.setColumnWidth(columnNumber - 1, excelWidthToPixels(width));
|
|
417
|
+
}
|
|
418
|
+
for (const merge of sourceSheet.model.merges ?? []) worksheet.mergeCells(merge);
|
|
419
|
+
importTables(worksheet, sourceSheet);
|
|
420
|
+
importImages(source, worksheet, sourceSheet);
|
|
421
|
+
importConditionalFormattings(worksheet, sourceSheet);
|
|
422
|
+
}
|
|
423
|
+
for (const comment of legacyComments) {
|
|
424
|
+
const worksheet = workbook.worksheets.getItem(comment.sheetName);
|
|
425
|
+
workbook.comments.addThread({ cell: worksheet.getRange(comment.cell) }, comment.text);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
return workbook;
|
|
429
|
+
}
|
|
430
|
+
async function exportWorkbook(workbook) {
|
|
431
|
+
const output = await createExcelWorkbook();
|
|
432
|
+
output.creator = "OpenGeni";
|
|
433
|
+
output.lastModifiedBy = "OpenGeni";
|
|
434
|
+
output.created = /* @__PURE__ */ new Date(0);
|
|
435
|
+
output.modified = /* @__PURE__ */ new Date(0);
|
|
436
|
+
output.calcProperties.fullCalcOnLoad = true;
|
|
437
|
+
for (const worksheet of workbook.worksheets.items) {
|
|
438
|
+
const sourceFreeze = worksheet.freezePanes.snapshot();
|
|
439
|
+
const views = sourceFreeze.rows || sourceFreeze.columns ? [
|
|
440
|
+
{
|
|
441
|
+
state: "frozen",
|
|
442
|
+
xSplit: sourceFreeze.columns,
|
|
443
|
+
ySplit: sourceFreeze.rows,
|
|
444
|
+
showGridLines: worksheet.showGridLines
|
|
445
|
+
}
|
|
446
|
+
] : [{ state: "normal", showGridLines: worksheet.showGridLines }];
|
|
447
|
+
const sheet = output.addWorksheet(worksheet.name, { views });
|
|
448
|
+
for (const { row, col, data } of worksheet.cellEntries()) {
|
|
449
|
+
const cell = sheet.getCell(row + 1, col + 1);
|
|
450
|
+
if (data.formula) {
|
|
451
|
+
assertSafeFormula(data.formula, `${worksheet.name}!${formatCellAddress({ row, col })}`);
|
|
452
|
+
const result = workbook.valueAt(worksheet, { row, col });
|
|
453
|
+
cell.value = {
|
|
454
|
+
formula: data.formula.startsWith("=") ? data.formula.slice(1) : data.formula,
|
|
455
|
+
...formulaResultForExcel(result) === void 0 ? {} : { result: formulaResultForExcel(result) }
|
|
456
|
+
};
|
|
457
|
+
} else {
|
|
458
|
+
cell.value = exportCellValue(data.value);
|
|
459
|
+
}
|
|
460
|
+
applyCellFormat(cell, data.format);
|
|
461
|
+
}
|
|
462
|
+
const used = worksheet.usedRangeAddress();
|
|
463
|
+
if (used) {
|
|
464
|
+
for (let col = used.col; col < used.col + used.colCount; col += 1) {
|
|
465
|
+
sheet.getColumn(col + 1).width = pixelsToExcelWidth(worksheet.columnWidth(col));
|
|
466
|
+
}
|
|
467
|
+
for (let row = used.row; row < used.row + used.rowCount; row += 1) {
|
|
468
|
+
sheet.getRow(row + 1).height = pixelsToPoints(worksheet.rowHeight(row));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
for (const merge of worksheet.mergeRegions()) sheet.mergeCells(formatRangeAddress(merge));
|
|
472
|
+
exportDataValidations(worksheet, sheet);
|
|
473
|
+
exportConditionalFormattings(worksheet, sheet);
|
|
474
|
+
exportTables(worksheet, sheet);
|
|
475
|
+
exportImages(output, worksheet, sheet);
|
|
476
|
+
exportComments(workbook, worksheet, sheet);
|
|
477
|
+
}
|
|
478
|
+
return output;
|
|
479
|
+
}
|
|
480
|
+
function importCellValue(value) {
|
|
481
|
+
if (value === void 0 || value === null) return null;
|
|
482
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date)
|
|
483
|
+
return value;
|
|
484
|
+
if ("formula" in value || "sharedFormula" in value) {
|
|
485
|
+
const result = value.result;
|
|
486
|
+
if (result === void 0) return null;
|
|
487
|
+
if (typeof result === "string" || typeof result === "number" || typeof result === "boolean" || result instanceof Date)
|
|
488
|
+
return result;
|
|
489
|
+
if ("error" in result) return result.error;
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
if ("error" in value) return value.error;
|
|
493
|
+
if ("richText" in value) return value.richText.map((part) => part.text).join("");
|
|
494
|
+
if ("text" in value) return value.text;
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
function formulaOf(value) {
|
|
498
|
+
if (!value || typeof value !== "object") return null;
|
|
499
|
+
if ("formula" in value && value.formula) return normalizeFormula(value.formula);
|
|
500
|
+
if ("sharedFormula" in value && value.formula) return normalizeFormula(value.formula);
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
function importCellFormat(cell) {
|
|
504
|
+
const fill = cell.fill?.type === "pattern" ? colorFromExcel(cell.fill.fgColor) : void 0;
|
|
505
|
+
const fontColor = colorFromExcel(cell.font?.color);
|
|
506
|
+
const border = importBorders(cell.border);
|
|
507
|
+
const format = {};
|
|
508
|
+
if (fill) format.fill = fill;
|
|
509
|
+
const font = {};
|
|
510
|
+
if (cell.font?.name) font.name = cell.font.name;
|
|
511
|
+
if (cell.font?.size !== void 0) font.size = cell.font.size;
|
|
512
|
+
if (cell.font?.bold !== void 0) font.bold = cell.font.bold;
|
|
513
|
+
if (cell.font?.italic !== void 0) font.italic = cell.font.italic;
|
|
514
|
+
if (cell.font?.underline !== void 0)
|
|
515
|
+
font.underline = Boolean(cell.font.underline && cell.font.underline !== "none");
|
|
516
|
+
if (fontColor) font.color = fontColor;
|
|
517
|
+
if (Object.keys(font).length > 0) format.font = font;
|
|
518
|
+
if (cell.numFmt) format.numberFormat = cell.numFmt;
|
|
519
|
+
if (border) format.borders = border;
|
|
520
|
+
const horizontal = importHorizontalAlignment(cell.alignment?.horizontal);
|
|
521
|
+
if (horizontal) format.horizontalAlignment = horizontal;
|
|
522
|
+
const vertical = importVerticalAlignment(cell.alignment?.vertical);
|
|
523
|
+
if (vertical) format.verticalAlignment = vertical;
|
|
524
|
+
if (cell.alignment?.wrapText !== void 0) format.wrapText = cell.alignment.wrapText;
|
|
525
|
+
return format;
|
|
526
|
+
}
|
|
527
|
+
function applyCellFormat(cell, format) {
|
|
528
|
+
if (format.fill) {
|
|
529
|
+
cell.fill = {
|
|
530
|
+
type: "pattern",
|
|
531
|
+
pattern: "solid",
|
|
532
|
+
fgColor: { argb: colorToArgb(format.fill) }
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (format.font) {
|
|
536
|
+
const font = {};
|
|
537
|
+
if (format.font.name !== void 0) font.name = format.font.name;
|
|
538
|
+
if (format.font.size !== void 0) font.size = format.font.size;
|
|
539
|
+
if (format.font.bold !== void 0) font.bold = format.font.bold;
|
|
540
|
+
if (format.font.italic !== void 0) font.italic = format.font.italic;
|
|
541
|
+
if (format.font.underline !== void 0) font.underline = format.font.underline;
|
|
542
|
+
if (format.font.color) font.color = { argb: colorToArgb(format.font.color) };
|
|
543
|
+
cell.font = font;
|
|
544
|
+
}
|
|
545
|
+
if (format.numberFormat) cell.numFmt = format.numberFormat;
|
|
546
|
+
if (format.borders) cell.border = exportBorders(format.borders);
|
|
547
|
+
const alignment = {};
|
|
548
|
+
if (format.horizontalAlignment) alignment.horizontal = format.horizontalAlignment;
|
|
549
|
+
if (format.verticalAlignment)
|
|
550
|
+
alignment.vertical = format.verticalAlignment === "center" ? "middle" : format.verticalAlignment;
|
|
551
|
+
if (format.wrapText !== void 0) alignment.wrapText = format.wrapText;
|
|
552
|
+
if (Object.keys(alignment).length > 0) cell.alignment = alignment;
|
|
553
|
+
}
|
|
554
|
+
function importBorders(borders) {
|
|
555
|
+
if (!borders) return void 0;
|
|
556
|
+
const imported = {};
|
|
557
|
+
const top = importBorder(borders.top);
|
|
558
|
+
const right = importBorder(borders.right);
|
|
559
|
+
const bottom = importBorder(borders.bottom);
|
|
560
|
+
const left = importBorder(borders.left);
|
|
561
|
+
if (top) imported.top = top;
|
|
562
|
+
if (right) imported.right = right;
|
|
563
|
+
if (bottom) imported.bottom = bottom;
|
|
564
|
+
if (left) imported.left = left;
|
|
565
|
+
return Object.keys(imported).length > 0 ? imported : void 0;
|
|
566
|
+
}
|
|
567
|
+
function importBorder(border) {
|
|
568
|
+
if (!border?.style) return void 0;
|
|
569
|
+
const imported = { style: border.style };
|
|
570
|
+
const color = colorFromExcel(border.color);
|
|
571
|
+
if (color) imported.color = color;
|
|
572
|
+
return imported;
|
|
573
|
+
}
|
|
574
|
+
function exportBorders(borders) {
|
|
575
|
+
const fallback = borders.style || borders.color ? {
|
|
576
|
+
style: borders.style ?? "thin",
|
|
577
|
+
...borders.color ? { color: borders.color } : {}
|
|
578
|
+
} : void 0;
|
|
579
|
+
const useFallback = borders.preset === "all" || borders.preset === "outside";
|
|
580
|
+
const exported = {};
|
|
581
|
+
const top = exportBorder(borders.top ?? (useFallback ? fallback : void 0));
|
|
582
|
+
const right = exportBorder(borders.right ?? (useFallback ? fallback : void 0));
|
|
583
|
+
const doubleBottom = {
|
|
584
|
+
style: "double",
|
|
585
|
+
...borders.color ? { color: borders.color } : {}
|
|
586
|
+
};
|
|
587
|
+
const bottom = exportBorder(
|
|
588
|
+
borders.bottom ?? (borders.preset === "doubleBottom" ? doubleBottom : useFallback ? fallback : void 0)
|
|
589
|
+
);
|
|
590
|
+
const left = exportBorder(borders.left ?? (useFallback ? fallback : void 0));
|
|
591
|
+
if (top) exported.top = top;
|
|
592
|
+
if (right) exported.right = right;
|
|
593
|
+
if (bottom) exported.bottom = bottom;
|
|
594
|
+
if (left) exported.left = left;
|
|
595
|
+
return exported;
|
|
596
|
+
}
|
|
597
|
+
function exportBorder(border) {
|
|
598
|
+
if (!border) return void 0;
|
|
599
|
+
const exported = {
|
|
600
|
+
style: excelBorderStyle(border.style)
|
|
601
|
+
};
|
|
602
|
+
if (border.color) exported.color = { argb: colorToArgb(border.color) };
|
|
603
|
+
return exported;
|
|
604
|
+
}
|
|
605
|
+
function importDataValidation(worksheet, row, col, validation) {
|
|
606
|
+
if (!validation?.type) return;
|
|
607
|
+
worksheet.dataValidations.add({
|
|
608
|
+
range: formatCellAddress({ row, col }),
|
|
609
|
+
rule: importDataValidationRule(validation)
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
function exportDataValidations(worksheet, target) {
|
|
613
|
+
for (const entry of worksheet.dataValidations.all()) {
|
|
614
|
+
const validation = exportDataValidationRule(entry.config.rule);
|
|
615
|
+
forEachCell(entry.range, (row, col) => {
|
|
616
|
+
target.getCell(row + 1, col + 1).dataValidation = structuredClone(validation);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function importDataValidationRule(validation) {
|
|
621
|
+
const source = structuredClone(validation);
|
|
622
|
+
const formulae = source.formulae;
|
|
623
|
+
if (source.type === "list" && Array.isArray(formulae) && formulae.length === 1) {
|
|
624
|
+
const values = inlineValidationValues(formulae[0]);
|
|
625
|
+
if (values) {
|
|
626
|
+
delete source.formulae;
|
|
627
|
+
source.values = values;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return source;
|
|
631
|
+
}
|
|
632
|
+
function exportDataValidationRule(rule) {
|
|
633
|
+
const output = structuredClone(rule);
|
|
634
|
+
if (Array.isArray(output.values)) {
|
|
635
|
+
if (output.type !== "list" || output.values.some((value) => typeof value !== "string")) {
|
|
636
|
+
throw new TypeError("Data validation values require a list of strings");
|
|
637
|
+
}
|
|
638
|
+
const literal = `"${output.values.map((value) => value.replaceAll('"', '""')).join(",")}"`;
|
|
639
|
+
if (literal.length > 255) {
|
|
640
|
+
throw new TypeError("Inline data validation values exceed Excel's 255-character limit");
|
|
641
|
+
}
|
|
642
|
+
output.formulae = [literal];
|
|
643
|
+
delete output.values;
|
|
644
|
+
} else if (!Array.isArray(output.formulae)) {
|
|
645
|
+
const formulae = [];
|
|
646
|
+
if (output.formula1 !== void 0) formulae.push(output.formula1);
|
|
647
|
+
if (output.formula2 !== void 0) formulae.push(output.formula2);
|
|
648
|
+
if (formulae.length > 0) output.formulae = formulae;
|
|
649
|
+
delete output.formula1;
|
|
650
|
+
delete output.formula2;
|
|
651
|
+
}
|
|
652
|
+
return output;
|
|
653
|
+
}
|
|
654
|
+
function inlineValidationValues(value) {
|
|
655
|
+
if (typeof value !== "string" || value.length < 2 || !value.startsWith('"') || !value.endsWith('"')) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
const input = value.slice(1, -1);
|
|
659
|
+
const values = [];
|
|
660
|
+
let current = "";
|
|
661
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
662
|
+
const character = input[index];
|
|
663
|
+
if (character === '"' && input[index + 1] === '"') {
|
|
664
|
+
current += '"';
|
|
665
|
+
index += 1;
|
|
666
|
+
} else if (character === ",") {
|
|
667
|
+
values.push(current);
|
|
668
|
+
current = "";
|
|
669
|
+
} else current += character;
|
|
670
|
+
}
|
|
671
|
+
values.push(current);
|
|
672
|
+
return values;
|
|
673
|
+
}
|
|
674
|
+
function importConditionalFormattings(worksheet, source) {
|
|
675
|
+
const entries = source.conditionalFormattings ?? [];
|
|
676
|
+
for (const entry of entries) {
|
|
677
|
+
const range = parseRangeAddress(entry.ref);
|
|
678
|
+
for (const sourceRule of entry.rules ?? []) {
|
|
679
|
+
const rule = stripUndefinedProperties(structuredClone(sourceRule));
|
|
680
|
+
const type = rule.type;
|
|
681
|
+
if (typeof type !== "string") throw new TypeError("XLSX conditional format type is missing");
|
|
682
|
+
if (!SUPPORTED_CONDITIONAL_FORMAT_RULE_TYPES.has(type.toLowerCase())) continue;
|
|
683
|
+
delete rule.type;
|
|
684
|
+
delete rule.priority;
|
|
685
|
+
const formulae = rule.formulae;
|
|
686
|
+
if (Array.isArray(formulae)) {
|
|
687
|
+
const normalizedFormulae = formulae.map(importConditionalFormula);
|
|
688
|
+
rule.formula = normalizedFormulae.length === 1 ? normalizedFormulae[0] : normalizedFormulae;
|
|
689
|
+
delete rule.formulae;
|
|
690
|
+
}
|
|
691
|
+
if (rule.style && typeof rule.style === "object") {
|
|
692
|
+
rule.format = importConditionalStyle(rule.style);
|
|
693
|
+
delete rule.style;
|
|
694
|
+
}
|
|
695
|
+
worksheet.conditionalFormattings.add(range, type, rule);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function stripUndefinedProperties(value) {
|
|
700
|
+
if (Array.isArray(value)) {
|
|
701
|
+
return value.map((item) => stripUndefinedProperties(item));
|
|
702
|
+
}
|
|
703
|
+
if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
704
|
+
const output = {};
|
|
705
|
+
for (const [key, item] of Object.entries(value)) {
|
|
706
|
+
if (item !== void 0) output[key] = stripUndefinedProperties(item);
|
|
707
|
+
}
|
|
708
|
+
return output;
|
|
709
|
+
}
|
|
710
|
+
return value;
|
|
711
|
+
}
|
|
712
|
+
function exportConditionalFormattings(worksheet, target) {
|
|
713
|
+
const add = target.addConditionalFormatting.bind(target);
|
|
714
|
+
for (const entry of worksheet.conditionalFormattings.all()) {
|
|
715
|
+
const config = structuredClone(entry.config);
|
|
716
|
+
const formula = config.formula;
|
|
717
|
+
if (formula !== void 0) {
|
|
718
|
+
config.formulae = Array.isArray(formula) ? formula : [formula];
|
|
719
|
+
delete config.formula;
|
|
720
|
+
}
|
|
721
|
+
if (config.format && typeof config.format === "object") {
|
|
722
|
+
config.style = exportConditionalStyle(config.format);
|
|
723
|
+
delete config.format;
|
|
724
|
+
}
|
|
725
|
+
add({
|
|
726
|
+
ref: formatRangeAddress(entry.range),
|
|
727
|
+
rules: [{ type: entry.ruleType, ...config }]
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function importConditionalStyle(style) {
|
|
732
|
+
const output = {};
|
|
733
|
+
const fill = style.fill;
|
|
734
|
+
const fillColor = colorFromExcel(fill?.fgColor ?? fill?.bgColor);
|
|
735
|
+
if (fillColor) output.fill = fillColor;
|
|
736
|
+
const font = style.font;
|
|
737
|
+
const fontColor = colorFromExcel(font?.color);
|
|
738
|
+
if (font) {
|
|
739
|
+
const normalizedFont = {};
|
|
740
|
+
for (const key of ["name", "size", "bold", "italic", "underline"]) {
|
|
741
|
+
if (font[key] !== null && font[key] !== void 0) normalizedFont[key] = font[key];
|
|
742
|
+
}
|
|
743
|
+
if (fontColor) normalizedFont.color = fontColor;
|
|
744
|
+
if (Object.keys(normalizedFont).length > 0) output.font = normalizedFont;
|
|
745
|
+
}
|
|
746
|
+
if (style.border && typeof style.border === "object") output.border = style.border;
|
|
747
|
+
if (typeof style.numFmt === "string") output.numberFormat = style.numFmt;
|
|
748
|
+
return output;
|
|
749
|
+
}
|
|
750
|
+
function importConditionalFormula(value) {
|
|
751
|
+
if (typeof value === "string" && /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[Ee][+-]?\d+)?$/.test(value)) {
|
|
752
|
+
const number = Number(value);
|
|
753
|
+
if (Number.isFinite(number)) return number;
|
|
754
|
+
}
|
|
755
|
+
return value;
|
|
756
|
+
}
|
|
757
|
+
function exportConditionalStyle(format) {
|
|
758
|
+
const output = structuredClone(format);
|
|
759
|
+
if (typeof output.fill === "string") {
|
|
760
|
+
output.fill = {
|
|
761
|
+
type: "pattern",
|
|
762
|
+
pattern: "solid",
|
|
763
|
+
fgColor: { argb: colorToArgb(output.fill) }
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
const font = output.font;
|
|
767
|
+
if (font && typeof font.color === "string") {
|
|
768
|
+
output.font = { ...font, color: { argb: colorToArgb(font.color) } };
|
|
769
|
+
}
|
|
770
|
+
if (output.numberFormat !== void 0 && output.numFmt === void 0) {
|
|
771
|
+
output.numFmt = output.numberFormat;
|
|
772
|
+
delete output.numberFormat;
|
|
773
|
+
}
|
|
774
|
+
return output;
|
|
775
|
+
}
|
|
776
|
+
function exportComments(workbook, worksheet, target) {
|
|
777
|
+
for (const comment of workbook.toJSON().comments) {
|
|
778
|
+
if (comment.sheetId !== worksheet.id || comment.comments.length === 0) continue;
|
|
779
|
+
target.getCell(comment.cell.row + 1, comment.cell.col + 1).note = comment.comments.map((item) => item.text).join("\n\n");
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function importTables(worksheet, source) {
|
|
783
|
+
const tables = source.getTables();
|
|
784
|
+
for (const sourceTable of tables) {
|
|
785
|
+
const model = sourceTable.table;
|
|
786
|
+
if (!model?.name || !model.tableRef) continue;
|
|
787
|
+
const table = worksheet.tables.add(model.tableRef, model.headerRow !== false, model.name);
|
|
788
|
+
table.showTotals = model.totalsRow === true;
|
|
789
|
+
table.style = model.style?.theme ?? "TableStyleMedium2";
|
|
790
|
+
table.showBandedColumns = model.style?.showColumnStripes ?? false;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function exportTables(worksheet, target) {
|
|
794
|
+
for (const table of worksheet.tables.items) {
|
|
795
|
+
const values = table.range.values;
|
|
796
|
+
const firstRow = values[0] ?? [];
|
|
797
|
+
const columns = firstRow.map((value, index) => ({
|
|
798
|
+
name: String(value ?? `Column${index + 1}`),
|
|
799
|
+
filterButton: table.showFilterButton
|
|
800
|
+
}));
|
|
801
|
+
if (columns.length === 0) continue;
|
|
802
|
+
target.addTable({
|
|
803
|
+
name: table.name,
|
|
804
|
+
ref: formatCellAddress(table.range.address),
|
|
805
|
+
headerRow: table.hasHeaders && table.showHeaders,
|
|
806
|
+
totalsRow: table.showTotals,
|
|
807
|
+
style: {
|
|
808
|
+
theme: excelTableTheme(table.style),
|
|
809
|
+
showColumnStripes: table.showBandedColumns,
|
|
810
|
+
showRowStripes: !table.showBandedColumns
|
|
811
|
+
},
|
|
812
|
+
columns,
|
|
813
|
+
rows: exportTableRows(
|
|
814
|
+
worksheet,
|
|
815
|
+
table.range.address,
|
|
816
|
+
table.hasHeaders ? 1 : 0,
|
|
817
|
+
table.showTotals ? 1 : 0
|
|
818
|
+
)
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function exportTableRows(worksheet, address, headerRows, totalRows) {
|
|
823
|
+
const rows = [];
|
|
824
|
+
const end = address.row + address.rowCount - totalRows;
|
|
825
|
+
for (let row = address.row + headerRows; row < end; row += 1) {
|
|
826
|
+
const values = [];
|
|
827
|
+
for (let col = address.col; col < address.col + address.colCount; col += 1) {
|
|
828
|
+
const data = worksheet.cellData(row, col);
|
|
829
|
+
if (data.formula) {
|
|
830
|
+
assertSafeFormula(data.formula, `${worksheet.name}!${formatCellAddress({ row, col })}`);
|
|
831
|
+
const result = formulaResultForExcel(worksheet.workbook.valueAt(worksheet, { row, col }));
|
|
832
|
+
values.push({
|
|
833
|
+
formula: data.formula.startsWith("=") ? data.formula.slice(1) : data.formula,
|
|
834
|
+
...result === void 0 ? {} : { result }
|
|
835
|
+
});
|
|
836
|
+
} else {
|
|
837
|
+
values.push(exportCellValue(data.value));
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
rows.push(values);
|
|
841
|
+
}
|
|
842
|
+
return rows;
|
|
843
|
+
}
|
|
844
|
+
function importImages(source, worksheet, sourceSheet) {
|
|
845
|
+
for (const image of sourceSheet.getImages()) {
|
|
846
|
+
const media = source.getImage(Number(image.imageId));
|
|
847
|
+
if (!media?.buffer || !["png", "jpeg", "gif"].includes(media.extension)) continue;
|
|
848
|
+
const range = image.range;
|
|
849
|
+
const fromRow = Math.max(0, Math.floor(range.tl.row));
|
|
850
|
+
const fromCol = Math.max(0, Math.floor(range.tl.col));
|
|
851
|
+
const rowOffsetPx = Math.max(0, (range.tl.row - fromRow) * worksheet.rowHeight(fromRow));
|
|
852
|
+
const colOffsetPx = Math.max(0, (range.tl.col - fromCol) * worksheet.columnWidth(fromCol));
|
|
853
|
+
const width = Math.max(
|
|
854
|
+
1,
|
|
855
|
+
range.ext?.width ?? ((range.br?.col ?? range.tl.col + 1) - range.tl.col) * worksheet.columnWidth(fromCol)
|
|
856
|
+
);
|
|
857
|
+
const height = Math.max(
|
|
858
|
+
1,
|
|
859
|
+
range.ext?.height ?? ((range.br?.row ?? range.tl.row + 1) - range.tl.row) * worksheet.rowHeight(fromRow)
|
|
860
|
+
);
|
|
861
|
+
worksheet.images.add({
|
|
862
|
+
blob: copyArrayBuffer(media.buffer),
|
|
863
|
+
contentType: media.extension === "jpeg" ? "image/jpeg" : `image/${media.extension}`,
|
|
864
|
+
anchor: {
|
|
865
|
+
from: {
|
|
866
|
+
row: fromRow,
|
|
867
|
+
col: fromCol,
|
|
868
|
+
...rowOffsetPx > 0 ? { rowOffsetPx } : {},
|
|
869
|
+
...colOffsetPx > 0 ? { colOffsetPx } : {}
|
|
870
|
+
},
|
|
871
|
+
extent: { widthPx: width, heightPx: height }
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function exportImages(source, worksheet, target) {
|
|
877
|
+
for (const image of worksheet.images.items) {
|
|
878
|
+
const data = safeExportImage(image.config);
|
|
879
|
+
const extension = imageExtension(data);
|
|
880
|
+
const imageId = data.dataUrl ? source.addImage({ base64: data.dataUrl, extension }) : data.blob ? source.addImage({
|
|
881
|
+
buffer: data.blob,
|
|
882
|
+
extension
|
|
883
|
+
}) : null;
|
|
884
|
+
if (imageId === null) continue;
|
|
885
|
+
const rowOffset = (data.anchor.from.rowOffsetPx ?? 0) / worksheet.rowHeight(data.anchor.from.row);
|
|
886
|
+
const colOffset = (data.anchor.from.colOffsetPx ?? 0) / worksheet.columnWidth(data.anchor.from.col);
|
|
887
|
+
target.addImage(imageId, {
|
|
888
|
+
tl: {
|
|
889
|
+
col: data.anchor.from.col + colOffset,
|
|
890
|
+
row: data.anchor.from.row + rowOffset
|
|
891
|
+
},
|
|
892
|
+
ext: {
|
|
893
|
+
width: data.anchor.extent.widthPx,
|
|
894
|
+
height: data.anchor.extent.heightPx
|
|
895
|
+
},
|
|
896
|
+
editAs: "oneCell"
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function imageExtension(image) {
|
|
901
|
+
const contentType = image.contentType ?? image.dataUrl?.slice(5, image.dataUrl.indexOf(";"));
|
|
902
|
+
if (contentType === "image/jpeg" || contentType === "image/jpg") return "jpeg";
|
|
903
|
+
if (contentType === "image/gif") return "gif";
|
|
904
|
+
return "png";
|
|
905
|
+
}
|
|
906
|
+
function formulaResultForExcel(value) {
|
|
907
|
+
if (value === null) return void 0;
|
|
908
|
+
if (typeof value === "string" && isFormulaError(value)) {
|
|
909
|
+
if (value === "#CYCLE!") return { error: "#VALUE!" };
|
|
910
|
+
return { error: value };
|
|
911
|
+
}
|
|
912
|
+
return value;
|
|
913
|
+
}
|
|
914
|
+
function exportCellValue(value) {
|
|
915
|
+
return value;
|
|
916
|
+
}
|
|
917
|
+
function normalizeFormula(formula) {
|
|
918
|
+
return formula.startsWith("=") ? formula : `=${formula}`;
|
|
919
|
+
}
|
|
920
|
+
function importHorizontalAlignment(value) {
|
|
921
|
+
if (value === "left" || value === "center" || value === "right" || value === "justify")
|
|
922
|
+
return value;
|
|
923
|
+
return void 0;
|
|
924
|
+
}
|
|
925
|
+
function importVerticalAlignment(value) {
|
|
926
|
+
if (value === "middle") return "center";
|
|
927
|
+
if (value === "top" || value === "bottom") return value;
|
|
928
|
+
return void 0;
|
|
929
|
+
}
|
|
930
|
+
function colorFromExcel(color) {
|
|
931
|
+
const argb = color?.argb;
|
|
932
|
+
if (!argb) return void 0;
|
|
933
|
+
const normalized = argb.replace(/^#/, "");
|
|
934
|
+
if (/^[0-9a-f]{8}$/i.test(normalized)) return `#${normalized.slice(2)}`;
|
|
935
|
+
if (/^[0-9a-f]{6}$/i.test(normalized)) return `#${normalized}`;
|
|
936
|
+
return void 0;
|
|
937
|
+
}
|
|
938
|
+
function colorToArgb(color) {
|
|
939
|
+
const normalized = color.trim().replace(/^#/, "");
|
|
940
|
+
if (/^[0-9a-f]{8}$/i.test(normalized)) return normalized.toUpperCase();
|
|
941
|
+
if (/^[0-9a-f]{6}$/i.test(normalized)) return `FF${normalized.toUpperCase()}`;
|
|
942
|
+
if (/^[0-9a-f]{3}$/i.test(normalized)) {
|
|
943
|
+
return `FF${normalized.split("").map((character) => character.repeat(2)).join("")}`.toUpperCase();
|
|
944
|
+
}
|
|
945
|
+
return "FF000000";
|
|
946
|
+
}
|
|
947
|
+
function excelBorderStyle(value) {
|
|
948
|
+
const supported = [
|
|
949
|
+
"thin",
|
|
950
|
+
"dotted",
|
|
951
|
+
"hair",
|
|
952
|
+
"medium",
|
|
953
|
+
"double",
|
|
954
|
+
"thick",
|
|
955
|
+
"dashed",
|
|
956
|
+
"dashDot",
|
|
957
|
+
"dashDotDot",
|
|
958
|
+
"slantDashDot",
|
|
959
|
+
"mediumDashed",
|
|
960
|
+
"mediumDashDotDot",
|
|
961
|
+
"mediumDashDot"
|
|
962
|
+
];
|
|
963
|
+
return supported.includes(value) ? value : "thin";
|
|
964
|
+
}
|
|
965
|
+
function excelTableTheme(value) {
|
|
966
|
+
return /^TableStyle(?:Dark|Light|Medium)\d+$/.test(value) ? value : "TableStyleMedium2";
|
|
967
|
+
}
|
|
968
|
+
function excelWidthToPixels(width) {
|
|
969
|
+
return Math.max(1, Math.round(width * 7 + 5));
|
|
970
|
+
}
|
|
971
|
+
function pixelsToExcelWidth(pixels) {
|
|
972
|
+
return Math.max(0.1, Number(((pixels - 5) / 7).toFixed(2)));
|
|
973
|
+
}
|
|
974
|
+
function pointsToPixels(points) {
|
|
975
|
+
return points * (96 / 72);
|
|
976
|
+
}
|
|
977
|
+
function pixelsToPoints(pixels) {
|
|
978
|
+
return Number((pixels * (72 / 96)).toFixed(2));
|
|
979
|
+
}
|
|
980
|
+
function copyArrayBuffer(value) {
|
|
981
|
+
return new Uint8Array(value).slice().buffer;
|
|
982
|
+
}
|
|
983
|
+
function isFormulaError(value) {
|
|
984
|
+
return ["#DIV/0!", "#N/A", "#NAME?", "#NUM!", "#REF!", "#VALUE!", "#CYCLE!"].includes(value);
|
|
985
|
+
}
|
|
986
|
+
function forEachCell(address, callback) {
|
|
987
|
+
for (let row = address.row; row < address.row + address.rowCount; row += 1) {
|
|
988
|
+
for (let col = address.col; col < address.col + address.colCount; col += 1) callback(row, col);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
async function toBytes(input, maxBytes) {
|
|
992
|
+
if (input instanceof Uint8Array) {
|
|
993
|
+
if (input.byteLength > maxBytes) {
|
|
994
|
+
throw securityError("XLSX exceeds the compressed-size limit", "limit-exceeded");
|
|
995
|
+
}
|
|
996
|
+
return Uint8Array.prototype.slice.call(input);
|
|
997
|
+
}
|
|
998
|
+
if (input instanceof ArrayBuffer) {
|
|
999
|
+
if (input.byteLength > maxBytes) {
|
|
1000
|
+
throw securityError("XLSX exceeds the compressed-size limit", "limit-exceeded");
|
|
1001
|
+
}
|
|
1002
|
+
return new Uint8Array(input.slice(0));
|
|
1003
|
+
}
|
|
1004
|
+
if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
1005
|
+
const size = Object.getOwnPropertyDescriptor(Blob.prototype, "size")?.get?.call(input);
|
|
1006
|
+
if (typeof size !== "number" || size > maxBytes) {
|
|
1007
|
+
throw securityError("XLSX exceeds the compressed-size limit", "limit-exceeded");
|
|
1008
|
+
}
|
|
1009
|
+
return new Uint8Array(await Blob.prototype.arrayBuffer.call(input));
|
|
1010
|
+
}
|
|
1011
|
+
throw new TypeError("XLSX input must be a Blob, ArrayBuffer, or Uint8Array");
|
|
1012
|
+
}
|
|
1013
|
+
async function workbookModelDigest(workbook) {
|
|
1014
|
+
return sha256Hex(new TextEncoder().encode(JSON.stringify(workbook.toJSON())));
|
|
1015
|
+
}
|
|
1016
|
+
async function sha256Hex(bytes) {
|
|
1017
|
+
const owned = new Uint8Array(bytes.byteLength);
|
|
1018
|
+
owned.set(bytes);
|
|
1019
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", owned));
|
|
1020
|
+
return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1021
|
+
}
|
|
1022
|
+
function normalizeGeneratedZip(input) {
|
|
1023
|
+
const bytes = Uint8Array.from(input);
|
|
1024
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1025
|
+
const minimumEocdBytes = 22;
|
|
1026
|
+
if (bytes.byteLength < minimumEocdBytes) {
|
|
1027
|
+
throw new Error("Generated XLSX ZIP has no end-of-central-directory record");
|
|
1028
|
+
}
|
|
1029
|
+
const firstCandidate = Math.max(0, bytes.byteLength - 65557);
|
|
1030
|
+
let eocdOffset = -1;
|
|
1031
|
+
for (let offset2 = bytes.byteLength - minimumEocdBytes; offset2 >= firstCandidate; offset2 -= 1) {
|
|
1032
|
+
if (view.getUint32(offset2, true) === 101010256 && offset2 + minimumEocdBytes + view.getUint16(offset2 + 20, true) === bytes.byteLength) {
|
|
1033
|
+
eocdOffset = offset2;
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (eocdOffset < 0) {
|
|
1038
|
+
throw new Error("Generated XLSX ZIP has an invalid end-of-central-directory record");
|
|
1039
|
+
}
|
|
1040
|
+
const disk = view.getUint16(eocdOffset + 4, true);
|
|
1041
|
+
const centralDisk = view.getUint16(eocdOffset + 6, true);
|
|
1042
|
+
const diskEntries = view.getUint16(eocdOffset + 8, true);
|
|
1043
|
+
const entries = view.getUint16(eocdOffset + 10, true);
|
|
1044
|
+
const centralBytes = view.getUint32(eocdOffset + 12, true);
|
|
1045
|
+
const centralOffset = view.getUint32(eocdOffset + 16, true);
|
|
1046
|
+
if (disk !== 0 || centralDisk !== 0 || diskEntries !== entries || entries === 65535 || centralBytes === 4294967295 || centralOffset === 4294967295 || centralOffset + centralBytes !== eocdOffset) {
|
|
1047
|
+
throw new Error("Generated XLSX ZIP uses an unsupported central-directory layout");
|
|
1048
|
+
}
|
|
1049
|
+
let offset = centralOffset;
|
|
1050
|
+
for (let index = 0; index < entries; index += 1) {
|
|
1051
|
+
if (offset + 46 > eocdOffset || view.getUint32(offset, true) !== 33639248) {
|
|
1052
|
+
throw new Error("Generated XLSX ZIP central directory is malformed");
|
|
1053
|
+
}
|
|
1054
|
+
const nameBytes = view.getUint16(offset + 28, true);
|
|
1055
|
+
const extraBytes = view.getUint16(offset + 30, true);
|
|
1056
|
+
const commentBytes = view.getUint16(offset + 32, true);
|
|
1057
|
+
const localOffset = view.getUint32(offset + 42, true);
|
|
1058
|
+
const nextOffset = offset + 46 + nameBytes + extraBytes + commentBytes;
|
|
1059
|
+
if (nextOffset > eocdOffset || localOffset + 30 > centralOffset || view.getUint32(localOffset, true) !== 67324752) {
|
|
1060
|
+
throw new Error("Generated XLSX ZIP entry offsets are malformed");
|
|
1061
|
+
}
|
|
1062
|
+
const localNameBytes = view.getUint16(localOffset + 26, true);
|
|
1063
|
+
const localExtraBytes = view.getUint16(localOffset + 28, true);
|
|
1064
|
+
const localExtraOffset = localOffset + 30 + localNameBytes;
|
|
1065
|
+
if (localExtraOffset + localExtraBytes > centralOffset) {
|
|
1066
|
+
throw new Error("Generated XLSX ZIP local metadata is malformed");
|
|
1067
|
+
}
|
|
1068
|
+
view.setUint16(offset + 12, 0, true);
|
|
1069
|
+
view.setUint16(offset + 14, 33, true);
|
|
1070
|
+
view.setUint16(localOffset + 10, 0, true);
|
|
1071
|
+
view.setUint16(localOffset + 12, 33, true);
|
|
1072
|
+
normalizeZipTimestampExtras(view, offset + 46 + nameBytes, extraBytes);
|
|
1073
|
+
normalizeZipTimestampExtras(view, localExtraOffset, localExtraBytes);
|
|
1074
|
+
offset = nextOffset;
|
|
1075
|
+
}
|
|
1076
|
+
if (offset !== eocdOffset) {
|
|
1077
|
+
throw new Error("Generated XLSX ZIP central-directory size is inconsistent");
|
|
1078
|
+
}
|
|
1079
|
+
return bytes;
|
|
1080
|
+
}
|
|
1081
|
+
function normalizeZipTimestampExtras(view, start, length) {
|
|
1082
|
+
const end = start + length;
|
|
1083
|
+
let offset = start;
|
|
1084
|
+
while (offset < end) {
|
|
1085
|
+
if (offset + 4 > end) throw new Error("Generated XLSX ZIP extra field is malformed");
|
|
1086
|
+
const kind = view.getUint16(offset, true);
|
|
1087
|
+
const fieldBytes = view.getUint16(offset + 2, true);
|
|
1088
|
+
const dataStart = offset + 4;
|
|
1089
|
+
const dataEnd = dataStart + fieldBytes;
|
|
1090
|
+
if (dataEnd > end) throw new Error("Generated XLSX ZIP extra field is malformed");
|
|
1091
|
+
if (kind === 21589 && fieldBytes > 0) {
|
|
1092
|
+
const flags = view.getUint8(dataStart);
|
|
1093
|
+
let timestampOffset = dataStart + 1;
|
|
1094
|
+
for (const flag of [1, 2, 4]) {
|
|
1095
|
+
if ((flags & flag) === 0) continue;
|
|
1096
|
+
if (timestampOffset + 4 > dataEnd) {
|
|
1097
|
+
throw new Error("Generated XLSX ZIP timestamp field is malformed");
|
|
1098
|
+
}
|
|
1099
|
+
view.setUint32(timestampOffset, 315532800, true);
|
|
1100
|
+
timestampOffset += 4;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
offset = dataEnd;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
async function preflightXlsx(bytes, limits) {
|
|
1107
|
+
if (bytes.byteLength > limits.compressedBytes) {
|
|
1108
|
+
throw securityError(
|
|
1109
|
+
`XLSX exceeds the ${limits.compressedBytes} byte compressed-size limit`,
|
|
1110
|
+
"limit-exceeded"
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
const entries = zipEntries(bytes, limits);
|
|
1114
|
+
const entryNames = new Map(entries.map((entry) => [entry.name.toLowerCase(), entry.name]));
|
|
1115
|
+
for (const required of [
|
|
1116
|
+
"[content_types].xml",
|
|
1117
|
+
"_rels/.rels",
|
|
1118
|
+
"xl/workbook.xml",
|
|
1119
|
+
"xl/_rels/workbook.xml.rels"
|
|
1120
|
+
]) {
|
|
1121
|
+
if (!entryNames.has(required)) {
|
|
1122
|
+
throw securityError("Required XLSX package part is missing", "invalid-package", required);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
let expandedBytes = 0;
|
|
1126
|
+
let inspectedXmlBytes = 0;
|
|
1127
|
+
let xmlAttributes = 0;
|
|
1128
|
+
let xmlElements = 0;
|
|
1129
|
+
let xmlTextCharacters = 0;
|
|
1130
|
+
let worksheetCells = 0;
|
|
1131
|
+
let formulas = 0;
|
|
1132
|
+
let formulaBytes = 0;
|
|
1133
|
+
let sharedStrings = 0;
|
|
1134
|
+
let cellStyles = 0;
|
|
1135
|
+
let styleRecords = 0;
|
|
1136
|
+
let comments = 0;
|
|
1137
|
+
let relationships = 0;
|
|
1138
|
+
let mediaEntries = 0;
|
|
1139
|
+
let mediaBytes = 0;
|
|
1140
|
+
let imagePixels = 0;
|
|
1141
|
+
const allRelationships = [];
|
|
1142
|
+
const contentTypeDeclarations = [];
|
|
1143
|
+
const workbookSheets = [];
|
|
1144
|
+
const legacyCommentParts = [];
|
|
1145
|
+
const opaqueParts = /* @__PURE__ */ new Set();
|
|
1146
|
+
const opaqueFeatures = /* @__PURE__ */ new Set();
|
|
1147
|
+
const mediaTypes = /* @__PURE__ */ new Map();
|
|
1148
|
+
for (const entry of entries) {
|
|
1149
|
+
if (entry.directory) {
|
|
1150
|
+
await verifyBoundedZipEntry(bytes, entry, 0, xlsxZipFailure);
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
expandedBytes += entry.expandedSize;
|
|
1154
|
+
if (expandedBytes > limits.expandedBytes) {
|
|
1155
|
+
throw securityError(
|
|
1156
|
+
`XLSX expands beyond the ${limits.expandedBytes} byte package limit`,
|
|
1157
|
+
"limit-exceeded",
|
|
1158
|
+
entry.name
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
if (entry.expandedSize > limits.entryBytes) {
|
|
1162
|
+
throw securityError(
|
|
1163
|
+
`XLSX entry exceeds the ${limits.entryBytes} byte entry limit`,
|
|
1164
|
+
"limit-exceeded",
|
|
1165
|
+
entry.name
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
if (entry.expandedSize > 1048576 && entry.expandedSize / Math.max(1, entry.compressedSize) > limits.compressionRatio) {
|
|
1169
|
+
throw securityError(
|
|
1170
|
+
`XLSX entry exceeds the ${limits.compressionRatio}:1 compression-ratio limit`,
|
|
1171
|
+
"limit-exceeded",
|
|
1172
|
+
entry.name
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
rejectActiveEntry(entry.name);
|
|
1176
|
+
if (isMediaPart(entry.name)) {
|
|
1177
|
+
mediaEntries += 1;
|
|
1178
|
+
mediaBytes += entry.expandedSize;
|
|
1179
|
+
if (mediaEntries > limits.mediaEntries || entry.expandedSize > limits.mediaEntryBytes || mediaBytes > limits.mediaBytes) {
|
|
1180
|
+
throw securityError(
|
|
1181
|
+
"Spreadsheet media exceeds its bounded import limits",
|
|
1182
|
+
"limit-exceeded",
|
|
1183
|
+
entry.name
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
const media = await inflateBoundedZipEntry(
|
|
1187
|
+
bytes,
|
|
1188
|
+
entry,
|
|
1189
|
+
limits.mediaEntryBytes,
|
|
1190
|
+
xlsxZipFailure
|
|
1191
|
+
);
|
|
1192
|
+
let metadata;
|
|
1193
|
+
try {
|
|
1194
|
+
metadata = inspectRasterImage(media);
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
const failure = securityError(
|
|
1197
|
+
`Spreadsheet media is not a structurally valid bounded raster image (${error instanceof Error ? error.message : "invalid image"})`,
|
|
1198
|
+
"active-content",
|
|
1199
|
+
entry.name
|
|
1200
|
+
);
|
|
1201
|
+
Object.defineProperty(failure, "cause", {
|
|
1202
|
+
value: error,
|
|
1203
|
+
enumerable: false
|
|
1204
|
+
});
|
|
1205
|
+
throw failure;
|
|
1206
|
+
}
|
|
1207
|
+
const expectedType = mediaTypeForExtension(entry.name);
|
|
1208
|
+
if (metadata.contentType !== expectedType) {
|
|
1209
|
+
throw securityError(
|
|
1210
|
+
"Spreadsheet media MIME does not match its file signature",
|
|
1211
|
+
"active-content",
|
|
1212
|
+
entry.name
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
imagePixels += metadata.width * metadata.height;
|
|
1216
|
+
if (imagePixels > limits.imagePixels) {
|
|
1217
|
+
throw securityError(
|
|
1218
|
+
"Spreadsheet images exceed their decoded-pixel limit",
|
|
1219
|
+
"limit-exceeded",
|
|
1220
|
+
entry.name
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
mediaTypes.set(entry.name.toLowerCase(), metadata.contentType);
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
if (!isInspectableXml(entry.name)) {
|
|
1227
|
+
await verifyBoundedZipEntry(bytes, entry, limits.entryBytes, xlsxZipFailure);
|
|
1228
|
+
throw securityError(
|
|
1229
|
+
"XLSX part is neither modeled nor in the bounded inert OOXML allowlist",
|
|
1230
|
+
"active-content",
|
|
1231
|
+
entry.name
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
const disposition = partDisposition(entry.name);
|
|
1235
|
+
inspectedXmlBytes += entry.expandedSize;
|
|
1236
|
+
if (inspectedXmlBytes > limits.inspectedXmlBytes) {
|
|
1237
|
+
throw securityError(
|
|
1238
|
+
`Inspectable OOXML exceeds the ${limits.inspectedXmlBytes} byte inspection limit`,
|
|
1239
|
+
"limit-exceeded",
|
|
1240
|
+
entry.name
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
const xmlBytes = await inflateBoundedZipEntry(bytes, entry, limits.entryBytes, xlsxZipFailure);
|
|
1244
|
+
const xml = decodeXml(xmlBytes, entry.name);
|
|
1245
|
+
const counts = inspectXml(xml, entry.name, limits, entryNames);
|
|
1246
|
+
xmlAttributes += counts.attributes;
|
|
1247
|
+
xmlElements += counts.elements;
|
|
1248
|
+
xmlTextCharacters += counts.textCharacters;
|
|
1249
|
+
worksheetCells += counts.worksheetCells;
|
|
1250
|
+
formulas += counts.formulas;
|
|
1251
|
+
formulaBytes += counts.formulaBytes;
|
|
1252
|
+
sharedStrings += counts.sharedStrings;
|
|
1253
|
+
cellStyles += counts.cellStyles;
|
|
1254
|
+
styleRecords += counts.styleRecords;
|
|
1255
|
+
comments += counts.comments.length;
|
|
1256
|
+
relationships += counts.relationships.length;
|
|
1257
|
+
allRelationships.push(...counts.relationships);
|
|
1258
|
+
contentTypeDeclarations.push(...counts.contentTypes);
|
|
1259
|
+
workbookSheets.push(...counts.workbookSheets);
|
|
1260
|
+
if (counts.comments.length > 0) {
|
|
1261
|
+
legacyCommentParts.push({
|
|
1262
|
+
partName: entry.name,
|
|
1263
|
+
comments: counts.comments
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
for (const feature of counts.opaqueFeatures) {
|
|
1267
|
+
opaqueFeatures.add(feature);
|
|
1268
|
+
opaqueParts.add(entry.name);
|
|
1269
|
+
}
|
|
1270
|
+
if (xmlAttributes > limits.xmlAttributes) {
|
|
1271
|
+
throw securityError(
|
|
1272
|
+
`OOXML package exceeds the ${limits.xmlAttributes} attribute limit`,
|
|
1273
|
+
"limit-exceeded",
|
|
1274
|
+
entry.name
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
if (relationships > limits.relationships) {
|
|
1278
|
+
throw securityError(
|
|
1279
|
+
`OOXML package exceeds the ${limits.relationships} relationship limit`,
|
|
1280
|
+
"limit-exceeded",
|
|
1281
|
+
entry.name
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
if (xmlElements > limits.xmlElements) {
|
|
1285
|
+
throw securityError(
|
|
1286
|
+
`OOXML package exceeds the ${limits.xmlElements} element limit`,
|
|
1287
|
+
"limit-exceeded",
|
|
1288
|
+
entry.name
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
if (xmlTextCharacters > limits.xmlTextCharacters) {
|
|
1292
|
+
throw securityError(
|
|
1293
|
+
`OOXML package exceeds the ${limits.xmlTextCharacters} text-character limit`,
|
|
1294
|
+
"limit-exceeded",
|
|
1295
|
+
entry.name
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
if (worksheetCells > limits.worksheetCells) {
|
|
1299
|
+
throw securityError(
|
|
1300
|
+
`OOXML package exceeds the ${limits.worksheetCells} worksheet-cell limit`,
|
|
1301
|
+
"limit-exceeded",
|
|
1302
|
+
entry.name
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
if (sharedStrings > limits.sharedStrings) {
|
|
1306
|
+
throw securityError(
|
|
1307
|
+
`OOXML package exceeds the ${limits.sharedStrings} shared-string limit`,
|
|
1308
|
+
"limit-exceeded",
|
|
1309
|
+
entry.name
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
if (cellStyles > limits.cellStyles) {
|
|
1313
|
+
throw securityError(
|
|
1314
|
+
`OOXML package exceeds the ${limits.cellStyles} cell-style limit`,
|
|
1315
|
+
"limit-exceeded",
|
|
1316
|
+
entry.name
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
if (formulas > limits.formulas || formulaBytes > limits.formulaBytes || styleRecords > limits.styleRecords) {
|
|
1320
|
+
throw securityError(
|
|
1321
|
+
"OOXML package exceeds formula or style aggregate limits",
|
|
1322
|
+
"limit-exceeded",
|
|
1323
|
+
entry.name
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
if (comments > limits.comments) {
|
|
1327
|
+
throw securityError(
|
|
1328
|
+
`OOXML package exceeds the ${limits.comments} comment limit`,
|
|
1329
|
+
"limit-exceeded",
|
|
1330
|
+
entry.name
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
if (disposition === "reject") {
|
|
1334
|
+
throw securityError(
|
|
1335
|
+
"XLSX XML part is neither modeled nor in the bounded inert allowlist",
|
|
1336
|
+
"active-content",
|
|
1337
|
+
entry.name
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
if (disposition === "opaque") {
|
|
1341
|
+
opaqueParts.add(entry.name);
|
|
1342
|
+
opaqueFeatures.add(opaqueFeatureForPart(entry.name));
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
const opaqueRelationships = [];
|
|
1346
|
+
for (const relationship of allRelationships) {
|
|
1347
|
+
if (relationship.opaque || opaqueParts.has(relationship.sourcePart) || opaqueParts.has(relationship.targetPart)) {
|
|
1348
|
+
const { opaque: _opaque, id: _id, kind: _kind, ...metadata } = relationship;
|
|
1349
|
+
opaqueRelationships.push(metadata);
|
|
1350
|
+
opaqueParts.add(relationshipPartForSource(relationship.sourcePart));
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
const contentTypes = resolveContentTypes(
|
|
1354
|
+
entries,
|
|
1355
|
+
contentTypeDeclarations,
|
|
1356
|
+
mediaTypes,
|
|
1357
|
+
opaqueParts
|
|
1358
|
+
);
|
|
1359
|
+
const importedComments = resolveLegacyComments(
|
|
1360
|
+
workbookSheets,
|
|
1361
|
+
allRelationships,
|
|
1362
|
+
legacyCommentParts
|
|
1363
|
+
);
|
|
1364
|
+
return {
|
|
1365
|
+
opaqueContent: {
|
|
1366
|
+
parts: [...opaqueParts].sort(compareStrings),
|
|
1367
|
+
relationships: opaqueRelationships.sort(compareOpaqueRelationships),
|
|
1368
|
+
contentTypes: contentTypes.filter((contentType) => opaqueParts.has(contentType.partName)).sort(compareOpaqueContentTypes),
|
|
1369
|
+
features: [...opaqueFeatures].sort(compareStrings)
|
|
1370
|
+
},
|
|
1371
|
+
comments: importedComments
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
function resolveLegacyComments(workbookSheets, relationships, parts) {
|
|
1375
|
+
const worksheetPartsByRelationship = new Map(
|
|
1376
|
+
relationships.filter(
|
|
1377
|
+
(relationship) => relationship.sourcePart.toLowerCase() === "xl/workbook.xml" && relationship.kind === "worksheet"
|
|
1378
|
+
).map((relationship) => [relationship.id, relationship.targetPart])
|
|
1379
|
+
);
|
|
1380
|
+
const sheetNamesByPart = /* @__PURE__ */ new Map();
|
|
1381
|
+
for (const sheet of workbookSheets) {
|
|
1382
|
+
const partName = worksheetPartsByRelationship.get(sheet.relationshipId);
|
|
1383
|
+
if (!partName || sheetNamesByPart.has(partName.toLowerCase())) {
|
|
1384
|
+
throw securityError(
|
|
1385
|
+
"Workbook sheet relationship is missing or duplicated",
|
|
1386
|
+
"invalid-package",
|
|
1387
|
+
"xl/workbook.xml"
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
sheetNamesByPart.set(partName.toLowerCase(), sheet.name);
|
|
1391
|
+
}
|
|
1392
|
+
const sheetPartsByCommentPart = /* @__PURE__ */ new Map();
|
|
1393
|
+
for (const relationship of relationships) {
|
|
1394
|
+
if (relationship.kind !== "comments") continue;
|
|
1395
|
+
const target = relationship.targetPart.toLowerCase();
|
|
1396
|
+
if (sheetPartsByCommentPart.has(target)) {
|
|
1397
|
+
throw securityError(
|
|
1398
|
+
"Spreadsheet comment part is linked from multiple worksheets",
|
|
1399
|
+
"invalid-package",
|
|
1400
|
+
relationship.targetPart
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
sheetPartsByCommentPart.set(target, relationship.sourcePart);
|
|
1404
|
+
}
|
|
1405
|
+
const result = [];
|
|
1406
|
+
const cells = /* @__PURE__ */ new Set();
|
|
1407
|
+
for (const part of parts) {
|
|
1408
|
+
const sheetPart = sheetPartsByCommentPart.get(part.partName.toLowerCase());
|
|
1409
|
+
if (!sheetPart) continue;
|
|
1410
|
+
const sheetName = sheetNamesByPart.get(sheetPart.toLowerCase());
|
|
1411
|
+
if (!sheetName) {
|
|
1412
|
+
throw securityError(
|
|
1413
|
+
"Spreadsheet comment relationship does not target a workbook sheet",
|
|
1414
|
+
"invalid-package",
|
|
1415
|
+
part.partName
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
for (const comment of part.comments) {
|
|
1419
|
+
const key = `${sheetName}\0${comment.cell}`;
|
|
1420
|
+
if (cells.has(key)) {
|
|
1421
|
+
throw securityError(
|
|
1422
|
+
"Spreadsheet has duplicate comments for one cell",
|
|
1423
|
+
"invalid-package",
|
|
1424
|
+
part.partName
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
cells.add(key);
|
|
1428
|
+
result.push({ sheetName, cell: comment.cell, text: comment.text });
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return result;
|
|
1432
|
+
}
|
|
1433
|
+
function isMediaPart(name) {
|
|
1434
|
+
return /^xl\/media\/[^/]+\.(?:png|jpe?g|gif)$/i.test(name);
|
|
1435
|
+
}
|
|
1436
|
+
function mediaTypeForExtension(name) {
|
|
1437
|
+
if (/\.png$/i.test(name)) return "image/png";
|
|
1438
|
+
if (/\.jpe?g$/i.test(name)) return "image/jpeg";
|
|
1439
|
+
if (/\.gif$/i.test(name)) return "image/gif";
|
|
1440
|
+
throw securityError("Spreadsheet media extension is unsupported", "active-content", name);
|
|
1441
|
+
}
|
|
1442
|
+
function partDisposition(name) {
|
|
1443
|
+
if (name === "[Content_Types].xml" || name === "_rels/.rels" || name === "xl/workbook.xml" || name === "xl/_rels/workbook.xml.rels" || /^xl\/worksheets\/[^/]+\.xml$/i.test(name) || /^xl\/worksheets\/_rels\/[^/]+\.xml\.rels$/i.test(name) || /^xl\/(?:styles|sharedStrings|calcChain)\.xml$/i.test(name) || /^xl\/tables\/[^/]+\.xml$/i.test(name) || /^xl\/drawings\/[^/]+\.xml$/i.test(name) || /^xl\/drawings\/_rels\/[^/]+\.xml\.rels$/i.test(name) || /^xl\/comments[^/]*\.xml$/i.test(name) || /^xl\/drawings\/[^/]+\.vml$/i.test(name)) {
|
|
1444
|
+
return "modeled";
|
|
1445
|
+
}
|
|
1446
|
+
if (/^docProps\/custom\.xml$/i.test(name) || /^xl\/charts\/[^/]+\.xml$/i.test(name) || /^xl\/charts\/_rels\/[^/]+\.xml\.rels$/i.test(name) || /^customXml\/item(?:Props)?\d+\.xml$/i.test(name) || /^customXml\/_rels\/item\d+\.xml\.rels$/i.test(name)) {
|
|
1447
|
+
return "opaque";
|
|
1448
|
+
}
|
|
1449
|
+
if (/^docProps\/(?:core|app)\.xml$/i.test(name) || /^xl\/theme\/[^/]+\.xml$/i.test(name)) {
|
|
1450
|
+
return "modeled";
|
|
1451
|
+
}
|
|
1452
|
+
return "reject";
|
|
1453
|
+
}
|
|
1454
|
+
function opaqueFeatureForPart(name) {
|
|
1455
|
+
if (/^docProps\//i.test(name)) return "workbook-properties";
|
|
1456
|
+
if (/^xl\/theme\//i.test(name)) return "workbook-theme";
|
|
1457
|
+
if (/^xl\/charts\//i.test(name)) return "editable-charts";
|
|
1458
|
+
if (/^customXml\//i.test(name)) return "custom-xml";
|
|
1459
|
+
return "opaque-ooxml";
|
|
1460
|
+
}
|
|
1461
|
+
function resolveContentTypes(entries, declarations, mediaTypes, opaqueParts) {
|
|
1462
|
+
const defaults = /* @__PURE__ */ new Map();
|
|
1463
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
1464
|
+
const existing = new Set(
|
|
1465
|
+
entries.filter((entry) => !entry.directory).map((entry) => entry.name.toLowerCase())
|
|
1466
|
+
);
|
|
1467
|
+
for (const declaration of declarations) {
|
|
1468
|
+
if (declaration.kind === "default") {
|
|
1469
|
+
if (defaults.has(declaration.extension)) {
|
|
1470
|
+
throw securityError(
|
|
1471
|
+
"XLSX has duplicate default content types",
|
|
1472
|
+
"invalid-package",
|
|
1473
|
+
"[Content_Types].xml"
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
defaults.set(declaration.extension, declaration.contentType);
|
|
1477
|
+
} else {
|
|
1478
|
+
const key = declaration.partName.toLowerCase();
|
|
1479
|
+
if (overrides.has(key) || !existing.has(key)) {
|
|
1480
|
+
throw securityError(
|
|
1481
|
+
"XLSX has duplicate or dangling content type overrides",
|
|
1482
|
+
"invalid-package",
|
|
1483
|
+
declaration.partName
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
overrides.set(key, declaration.contentType);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
const output = [];
|
|
1490
|
+
for (const entry of entries) {
|
|
1491
|
+
if (entry.directory || entry.name === "[Content_Types].xml") continue;
|
|
1492
|
+
const extension = entry.name.includes(".") ? entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase() : "";
|
|
1493
|
+
const contentType = overrides.get(entry.name.toLowerCase()) ?? defaults.get(extension);
|
|
1494
|
+
if (!contentType) {
|
|
1495
|
+
throw securityError("XLSX part has no declared content type", "invalid-package", entry.name);
|
|
1496
|
+
}
|
|
1497
|
+
rejectActiveContentType(contentType, entry.name);
|
|
1498
|
+
if (/\.rels$/i.test(entry.name)) {
|
|
1499
|
+
if (contentType !== "application/vnd.openxmlformats-package.relationships+xml") {
|
|
1500
|
+
throw securityError("Relationship part MIME is inconsistent", "active-content", entry.name);
|
|
1501
|
+
}
|
|
1502
|
+
} else if (isMediaPart(entry.name)) {
|
|
1503
|
+
if (contentType !== mediaTypes.get(entry.name.toLowerCase())) {
|
|
1504
|
+
throw securityError(
|
|
1505
|
+
"Spreadsheet media content type is MIME-confused",
|
|
1506
|
+
"active-content",
|
|
1507
|
+
entry.name
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
} else if (/\.vml$/i.test(entry.name)) {
|
|
1511
|
+
if (contentType !== "application/vnd.openxmlformats-officedocument.vmlDrawing") {
|
|
1512
|
+
throw securityError("VML content type is inconsistent", "active-content", entry.name);
|
|
1513
|
+
}
|
|
1514
|
+
} else if (isInspectableXml(entry.name) && !isXmlContentType(contentType)) {
|
|
1515
|
+
throw securityError("XML part has a non-XML content type", "active-content", entry.name);
|
|
1516
|
+
}
|
|
1517
|
+
if (opaqueParts.has(entry.name)) output.push({ partName: entry.name, contentType });
|
|
1518
|
+
}
|
|
1519
|
+
return output;
|
|
1520
|
+
}
|
|
1521
|
+
function isXmlContentType(contentType) {
|
|
1522
|
+
return contentType === "application/xml" || contentType === "text/xml" || /\+xml(?:;|$)/i.test(contentType);
|
|
1523
|
+
}
|
|
1524
|
+
function isCanonicalPartName(name) {
|
|
1525
|
+
return name.length > 0 && name.length <= 2048 && !name.startsWith("/") && !name.endsWith("/") && !name.includes("\\") && !name.includes("%") && !/[\u0000-\u001f\u007f]/.test(name) && name.normalize("NFC") === name && name.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
1526
|
+
}
|
|
1527
|
+
function relationshipPartForSource(sourcePart) {
|
|
1528
|
+
if (sourcePart === "") return "_rels/.rels";
|
|
1529
|
+
const slash = sourcePart.lastIndexOf("/");
|
|
1530
|
+
const directory = slash < 0 ? "" : sourcePart.slice(0, slash + 1);
|
|
1531
|
+
const file = sourcePart.slice(slash + 1);
|
|
1532
|
+
return `${directory}_rels/${file}.rels`;
|
|
1533
|
+
}
|
|
1534
|
+
function compareStrings(left, right) {
|
|
1535
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1536
|
+
}
|
|
1537
|
+
function compareOpaqueRelationships(left, right) {
|
|
1538
|
+
return compareStrings(left.sourcePart, right.sourcePart) || compareStrings(left.type, right.type) || compareStrings(left.targetPart, right.targetPart);
|
|
1539
|
+
}
|
|
1540
|
+
function compareOpaqueContentTypes(left, right) {
|
|
1541
|
+
return compareStrings(left.partName, right.partName) || compareStrings(left.contentType, right.contentType);
|
|
1542
|
+
}
|
|
1543
|
+
function resolveImportLimits(options) {
|
|
1544
|
+
const resolved = { ...DEFAULT_XLSX_IMPORT_LIMITS, ...options };
|
|
1545
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
1546
|
+
if (!Number.isFinite(value) || value <= 0 || !Number.isInteger(value)) {
|
|
1547
|
+
throw new TypeError(`XLSX import limit ${name} must be a positive integer`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
for (const [name, maximum] of Object.entries(DEFAULT_XLSX_IMPORT_LIMITS)) {
|
|
1551
|
+
if (resolved[name] > maximum) {
|
|
1552
|
+
throw new TypeError(
|
|
1553
|
+
`XLSX import limit ${name} cannot exceed its hard safety cap of ${maximum}`
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return resolved;
|
|
1558
|
+
}
|
|
1559
|
+
function zipEntries(bytes, limits) {
|
|
1560
|
+
return parseBoundedZip(
|
|
1561
|
+
bytes,
|
|
1562
|
+
{
|
|
1563
|
+
entries: limits.entries,
|
|
1564
|
+
compressedEntryBytes: limits.compressedBytes,
|
|
1565
|
+
expandedEntryBytes: limits.entryBytes,
|
|
1566
|
+
expandedBytes: limits.expandedBytes,
|
|
1567
|
+
compressionRatio: limits.compressionRatio,
|
|
1568
|
+
compressionRatioThreshold: 1048576
|
|
1569
|
+
},
|
|
1570
|
+
xlsxZipFailure
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
function rejectActiveEntry(name) {
|
|
1574
|
+
if (/(?:^|\/)(?:vbaProject|vbaData)\.bin$/i.test(name) || /(?:^|\/)(?:activeX|embeddings|externalLinks|queryTables|ctrlProps|webextensions)(?:\/|$)/i.test(
|
|
1575
|
+
name
|
|
1576
|
+
) || /^xl\/connections\.xml$/i.test(name) || /\.(?:exe|dll|com|msi|js|vbs|vbe|bat|cmd|ps1|sh|scr|jar)$/i.test(name)) {
|
|
1577
|
+
throw securityError(
|
|
1578
|
+
"Active or externally connected OOXML content is unsupported",
|
|
1579
|
+
"active-content",
|
|
1580
|
+
name
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
function isInspectableXml(name) {
|
|
1585
|
+
return /(?:\.xml|\.rels|\.vml)$/i.test(name) || /^\[Content_Types\]\.xml$/i.test(name);
|
|
1586
|
+
}
|
|
1587
|
+
function decodeXml(bytes, entryName) {
|
|
1588
|
+
try {
|
|
1589
|
+
if (bytes[0] === 255 && bytes[1] === 254)
|
|
1590
|
+
return new TextDecoder("utf-16le", { fatal: true }).decode(bytes);
|
|
1591
|
+
if (bytes[0] === 254 && bytes[1] === 255)
|
|
1592
|
+
return new TextDecoder("utf-16be", { fatal: true }).decode(bytes);
|
|
1593
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1594
|
+
} catch {
|
|
1595
|
+
throw securityError("OOXML text is not valid UTF-8/UTF-16", "unsafe-xml", entryName);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
var FORMULA_ELEMENTS = /* @__PURE__ */ new Set([
|
|
1599
|
+
"f",
|
|
1600
|
+
"formula",
|
|
1601
|
+
"formula1",
|
|
1602
|
+
"formula2",
|
|
1603
|
+
"definedName",
|
|
1604
|
+
"calculatedColumnFormula",
|
|
1605
|
+
"totalsRowFormula"
|
|
1606
|
+
]);
|
|
1607
|
+
var STYLE_RECORD_ELEMENTS = /* @__PURE__ */ new Set([
|
|
1608
|
+
"numFmt",
|
|
1609
|
+
"font",
|
|
1610
|
+
"fill",
|
|
1611
|
+
"border",
|
|
1612
|
+
"xf",
|
|
1613
|
+
"cellStyle",
|
|
1614
|
+
"dxf"
|
|
1615
|
+
]);
|
|
1616
|
+
var SUPPORTED_CONDITIONAL_FORMAT_RULE_TYPES = /* @__PURE__ */ new Set([
|
|
1617
|
+
"cellis",
|
|
1618
|
+
"expression",
|
|
1619
|
+
"colorscale",
|
|
1620
|
+
"databar",
|
|
1621
|
+
"iconset",
|
|
1622
|
+
"containstext",
|
|
1623
|
+
"notcontainstext",
|
|
1624
|
+
"beginswith",
|
|
1625
|
+
"endswith",
|
|
1626
|
+
"containsblanks",
|
|
1627
|
+
"notcontainsblanks",
|
|
1628
|
+
"containserrors",
|
|
1629
|
+
"notcontainserrors",
|
|
1630
|
+
"duplicatevalues",
|
|
1631
|
+
"uniquevalues",
|
|
1632
|
+
"timeperiod",
|
|
1633
|
+
"top10",
|
|
1634
|
+
"aboveaverage"
|
|
1635
|
+
]);
|
|
1636
|
+
function inspectXml(xml, entryName, limits, entryNames) {
|
|
1637
|
+
const result = {
|
|
1638
|
+
attributes: 0,
|
|
1639
|
+
elements: 0,
|
|
1640
|
+
textCharacters: 0,
|
|
1641
|
+
worksheetCells: 0,
|
|
1642
|
+
formulas: 0,
|
|
1643
|
+
formulaBytes: 0,
|
|
1644
|
+
sharedStrings: 0,
|
|
1645
|
+
cellStyles: 0,
|
|
1646
|
+
styleRecords: 0,
|
|
1647
|
+
workbookSheets: [],
|
|
1648
|
+
comments: [],
|
|
1649
|
+
relationships: [],
|
|
1650
|
+
contentTypes: [],
|
|
1651
|
+
opaqueFeatures: []
|
|
1652
|
+
};
|
|
1653
|
+
const features = /* @__PURE__ */ new Set();
|
|
1654
|
+
const relationshipIds = /* @__PURE__ */ new Set();
|
|
1655
|
+
const relationshipTags = [];
|
|
1656
|
+
const stack = [];
|
|
1657
|
+
let activeFormula = null;
|
|
1658
|
+
let activeMetadata = null;
|
|
1659
|
+
let activeComment = null;
|
|
1660
|
+
let failed;
|
|
1661
|
+
const parser = new SaxesParser({ xmlns: true });
|
|
1662
|
+
const chargeText = (text) => {
|
|
1663
|
+
result.textCharacters += text.length;
|
|
1664
|
+
if (result.textCharacters > limits.xmlTextCharactersPerPart) {
|
|
1665
|
+
throw securityError(
|
|
1666
|
+
`OOXML part exceeds the ${limits.xmlTextCharactersPerPart} text-character limit`,
|
|
1667
|
+
"limit-exceeded",
|
|
1668
|
+
entryName
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
if (activeFormula) {
|
|
1672
|
+
activeFormula.text += text;
|
|
1673
|
+
if (boundedUtf8ByteLength(activeFormula.text, 8192) > 8192) {
|
|
1674
|
+
throw securityError(
|
|
1675
|
+
"Spreadsheet formula exceeds its byte limit",
|
|
1676
|
+
"limit-exceeded",
|
|
1677
|
+
entryName
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (activeMetadata) activeMetadata.text += text;
|
|
1682
|
+
if (activeComment && stack.at(-1) === "t") activeComment.text += text;
|
|
1683
|
+
};
|
|
1684
|
+
parser.on("doctype", () => {
|
|
1685
|
+
throw securityError(
|
|
1686
|
+
"DTD and entity declarations are forbidden in OOXML",
|
|
1687
|
+
"unsafe-xml",
|
|
1688
|
+
entryName
|
|
1689
|
+
);
|
|
1690
|
+
});
|
|
1691
|
+
parser.on("processinginstruction", () => {
|
|
1692
|
+
throw securityError("OOXML processing instructions are unsupported", "unsafe-xml", entryName);
|
|
1693
|
+
});
|
|
1694
|
+
parser.on("comment", chargeText);
|
|
1695
|
+
parser.on("text", chargeText);
|
|
1696
|
+
parser.on("cdata", chargeText);
|
|
1697
|
+
parser.on("opentag", (tag) => {
|
|
1698
|
+
const localName = tag.local;
|
|
1699
|
+
if (activeFormula) {
|
|
1700
|
+
throw securityError(
|
|
1701
|
+
"Spreadsheet formula elements cannot contain child markup",
|
|
1702
|
+
"unsafe-xml",
|
|
1703
|
+
entryName
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
result.elements += 1;
|
|
1707
|
+
if (result.elements > limits.xmlElementsPerPart) {
|
|
1708
|
+
throw securityError(
|
|
1709
|
+
`OOXML part exceeds the ${limits.xmlElementsPerPart} element limit`,
|
|
1710
|
+
"limit-exceeded",
|
|
1711
|
+
entryName
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
const elementAttributes = Object.keys(tag.attributes).length;
|
|
1715
|
+
if (elementAttributes > limits.xmlAttributesPerElement) {
|
|
1716
|
+
throw securityError(
|
|
1717
|
+
`OOXML element exceeds the ${limits.xmlAttributesPerElement} attribute limit`,
|
|
1718
|
+
"limit-exceeded",
|
|
1719
|
+
entryName
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
result.attributes += elementAttributes;
|
|
1723
|
+
if (result.attributes > limits.xmlAttributesPerPart) {
|
|
1724
|
+
throw securityError(
|
|
1725
|
+
`OOXML part exceeds the ${limits.xmlAttributesPerPart} attribute limit`,
|
|
1726
|
+
"limit-exceeded",
|
|
1727
|
+
entryName
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
stack.push(localName);
|
|
1731
|
+
if (stack.length > limits.xmlDepth) {
|
|
1732
|
+
throw securityError(
|
|
1733
|
+
`OOXML nesting exceeds the ${limits.xmlDepth} level limit`,
|
|
1734
|
+
"limit-exceeded",
|
|
1735
|
+
entryName
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
const attributes = localAttributes(tag, entryName);
|
|
1739
|
+
rejectActiveXmlElement(entryName, localName, attributes);
|
|
1740
|
+
detectOpaqueXmlFeature(entryName, localName, attributes, stack, features);
|
|
1741
|
+
if (/^xl\/workbook\.xml$/i.test(entryName) && localName === "sheet") {
|
|
1742
|
+
const name = attributes.get("name");
|
|
1743
|
+
const relationshipId = attributes.get("id");
|
|
1744
|
+
if (!name || name.length > 31 || !relationshipId || relationshipId.length > 512) {
|
|
1745
|
+
throw securityError(
|
|
1746
|
+
"Workbook sheet binding is missing or invalid",
|
|
1747
|
+
"invalid-package",
|
|
1748
|
+
entryName
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
result.workbookSheets.push({ name, relationshipId });
|
|
1752
|
+
}
|
|
1753
|
+
if (/^xl\/comments[^/]*\.xml$/i.test(entryName) && localName === "comment") {
|
|
1754
|
+
if (activeComment) {
|
|
1755
|
+
throw securityError("Spreadsheet comments cannot be nested", "unsafe-xml", entryName);
|
|
1756
|
+
}
|
|
1757
|
+
if (result.comments.length >= limits.commentsPerPart) {
|
|
1758
|
+
throw securityError(
|
|
1759
|
+
`OOXML part exceeds the ${limits.commentsPerPart} comment limit`,
|
|
1760
|
+
"limit-exceeded",
|
|
1761
|
+
entryName
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
const ref = attributes.get("ref");
|
|
1765
|
+
let range;
|
|
1766
|
+
try {
|
|
1767
|
+
if (!ref) throw new Error("missing comment reference");
|
|
1768
|
+
range = parseRangeAddress(ref);
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
const failure = securityError(
|
|
1771
|
+
"Spreadsheet comment reference is invalid",
|
|
1772
|
+
"invalid-package",
|
|
1773
|
+
entryName
|
|
1774
|
+
);
|
|
1775
|
+
Object.defineProperty(failure, "cause", {
|
|
1776
|
+
value: error,
|
|
1777
|
+
enumerable: false
|
|
1778
|
+
});
|
|
1779
|
+
throw failure;
|
|
1780
|
+
}
|
|
1781
|
+
if (range.rowCount !== 1 || range.colCount !== 1) {
|
|
1782
|
+
throw securityError(
|
|
1783
|
+
"Spreadsheet comment must reference one cell",
|
|
1784
|
+
"invalid-package",
|
|
1785
|
+
entryName
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
activeComment = { cell: formatCellAddress(range), text: "" };
|
|
1789
|
+
}
|
|
1790
|
+
if (/^xl\/worksheets\/[^/]+\.xml$/i.test(entryName) && localName === "c") {
|
|
1791
|
+
result.worksheetCells += 1;
|
|
1792
|
+
if (result.worksheetCells > limits.worksheetCellsPerPart) {
|
|
1793
|
+
throw securityError(
|
|
1794
|
+
`Worksheet exceeds the ${limits.worksheetCellsPerPart} cell limit`,
|
|
1795
|
+
"limit-exceeded",
|
|
1796
|
+
entryName
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
if (/^xl\/sharedStrings\.xml$/i.test(entryName) && localName === "si") {
|
|
1801
|
+
result.sharedStrings += 1;
|
|
1802
|
+
if (result.sharedStrings > limits.sharedStrings) {
|
|
1803
|
+
throw securityError(
|
|
1804
|
+
`Shared strings exceed the ${limits.sharedStrings} item limit`,
|
|
1805
|
+
"limit-exceeded",
|
|
1806
|
+
entryName
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (/^xl\/styles\.xml$/i.test(entryName) && STYLE_RECORD_ELEMENTS.has(localName)) {
|
|
1811
|
+
result.styleRecords += 1;
|
|
1812
|
+
if (localName === "xf") result.cellStyles += 1;
|
|
1813
|
+
if (result.cellStyles > limits.cellStyles || result.styleRecords > limits.styleRecords) {
|
|
1814
|
+
throw securityError(
|
|
1815
|
+
"Spreadsheet styles exceed their record limit",
|
|
1816
|
+
"limit-exceeded",
|
|
1817
|
+
entryName
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
if (FORMULA_ELEMENTS.has(localName)) {
|
|
1822
|
+
result.formulas += 1;
|
|
1823
|
+
if (result.formulas > limits.formulasPerPart) {
|
|
1824
|
+
throw securityError(
|
|
1825
|
+
`OOXML part exceeds the ${limits.formulasPerPart} formula limit`,
|
|
1826
|
+
"limit-exceeded",
|
|
1827
|
+
entryName
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
activeFormula = { name: localName, text: "" };
|
|
1831
|
+
}
|
|
1832
|
+
if (/^docProps\/(?:core|app)\.xml$/i.test(entryName) && (/* @__PURE__ */ new Set([
|
|
1833
|
+
"creator",
|
|
1834
|
+
"title",
|
|
1835
|
+
"subject",
|
|
1836
|
+
"description",
|
|
1837
|
+
"keywords",
|
|
1838
|
+
"category",
|
|
1839
|
+
"lastModifiedBy",
|
|
1840
|
+
"Company",
|
|
1841
|
+
"Manager"
|
|
1842
|
+
])).has(localName)) {
|
|
1843
|
+
activeMetadata = { name: localName, text: "" };
|
|
1844
|
+
}
|
|
1845
|
+
if (/\.rels$/i.test(entryName) && localName === "Relationship") {
|
|
1846
|
+
if (relationshipTags.length >= limits.relationshipsPerPart) {
|
|
1847
|
+
throw securityError(
|
|
1848
|
+
`OOXML part exceeds the ${limits.relationshipsPerPart} relationship limit`,
|
|
1849
|
+
"limit-exceeded",
|
|
1850
|
+
entryName
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
const id = attributes.get("Id");
|
|
1854
|
+
if (!id || relationshipIds.has(id)) {
|
|
1855
|
+
throw securityError(
|
|
1856
|
+
"OOXML relationship id is missing or duplicated",
|
|
1857
|
+
"invalid-package",
|
|
1858
|
+
entryName
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
relationshipIds.add(id);
|
|
1862
|
+
relationshipTags.push(attributes);
|
|
1863
|
+
}
|
|
1864
|
+
if (entryName === "[Content_Types].xml" && (localName === "Default" || localName === "Override")) {
|
|
1865
|
+
result.contentTypes.push(inspectContentTypeTag(localName, attributes, entryName));
|
|
1866
|
+
}
|
|
1867
|
+
});
|
|
1868
|
+
parser.on("closetag", (tag) => {
|
|
1869
|
+
if (activeFormula && activeFormula.name === tag.local) {
|
|
1870
|
+
const formula = activeFormula.text.trim();
|
|
1871
|
+
if (formula.length > 0) {
|
|
1872
|
+
const formulaBytes = boundedUtf8ByteLength(formula, 8192);
|
|
1873
|
+
result.formulaBytes += formulaBytes;
|
|
1874
|
+
if (result.formulaBytes > limits.formulaBytesPerPart) {
|
|
1875
|
+
throw securityError(
|
|
1876
|
+
`OOXML part exceeds the ${limits.formulaBytesPerPart} formula-byte limit`,
|
|
1877
|
+
"limit-exceeded",
|
|
1878
|
+
entryName
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
assertSafeFormula(formula, entryName);
|
|
1882
|
+
}
|
|
1883
|
+
activeFormula = null;
|
|
1884
|
+
}
|
|
1885
|
+
if (activeMetadata && activeMetadata.name === tag.local) {
|
|
1886
|
+
const value = activeMetadata.text.trim();
|
|
1887
|
+
if (value.length > 0 && !(["creator", "lastModifiedBy"].includes(activeMetadata.name) && value === "OpenGeni")) {
|
|
1888
|
+
features.add("workbook-properties");
|
|
1889
|
+
}
|
|
1890
|
+
activeMetadata = null;
|
|
1891
|
+
}
|
|
1892
|
+
if (activeComment && tag.local === "comment") {
|
|
1893
|
+
result.comments.push(activeComment);
|
|
1894
|
+
activeComment = null;
|
|
1895
|
+
}
|
|
1896
|
+
stack.pop();
|
|
1897
|
+
});
|
|
1898
|
+
parser.on("error", (error) => {
|
|
1899
|
+
failed = securityError(`OOXML is malformed (${error.message})`, "unsafe-xml", entryName);
|
|
1900
|
+
throw failed;
|
|
1901
|
+
});
|
|
1902
|
+
try {
|
|
1903
|
+
parser.write(xml).close();
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
if (error instanceof SpreadsheetSecurityError) throw error;
|
|
1906
|
+
throw securityError(
|
|
1907
|
+
`OOXML is malformed (${error instanceof Error ? error.message : "parse failure"})`,
|
|
1908
|
+
"unsafe-xml",
|
|
1909
|
+
entryName
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
if (failed) throw failed;
|
|
1913
|
+
if (stack.length !== 0 || activeFormula || activeComment) {
|
|
1914
|
+
throw securityError("OOXML tags are structurally unbalanced", "unsafe-xml", entryName);
|
|
1915
|
+
}
|
|
1916
|
+
result.relationships = relationshipTags.map(
|
|
1917
|
+
(attributes) => inspectRelationshipTag(attributes, entryName, entryNames)
|
|
1918
|
+
);
|
|
1919
|
+
result.opaqueFeatures = [...features].sort();
|
|
1920
|
+
return result;
|
|
1921
|
+
}
|
|
1922
|
+
function localAttributes(tag, entryName) {
|
|
1923
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
1924
|
+
for (const attribute of Object.values(tag.attributes)) {
|
|
1925
|
+
const name = attribute.local;
|
|
1926
|
+
if (attributes.has(name)) {
|
|
1927
|
+
throw securityError("OOXML element has duplicate local attributes", "unsafe-xml", entryName);
|
|
1928
|
+
}
|
|
1929
|
+
attributes.set(name, attribute.value);
|
|
1930
|
+
}
|
|
1931
|
+
return attributes;
|
|
1932
|
+
}
|
|
1933
|
+
function inspectContentTypeTag(localName, attributes, entryName) {
|
|
1934
|
+
const expected = localName === "Default" ? /* @__PURE__ */ new Set(["Extension", "ContentType"]) : /* @__PURE__ */ new Set(["PartName", "ContentType"]);
|
|
1935
|
+
if ([...attributes.keys()].some((name) => !expected.has(name))) {
|
|
1936
|
+
throw securityError("OOXML content type has unsupported attributes", "unsafe-xml", entryName);
|
|
1937
|
+
}
|
|
1938
|
+
const contentType = attributes.get("ContentType");
|
|
1939
|
+
if (!contentType || contentType.length > 2048 || !/^[\x21-\x7e]+$/.test(contentType)) {
|
|
1940
|
+
throw securityError("OOXML content type is missing or invalid", "unsafe-xml", entryName);
|
|
1941
|
+
}
|
|
1942
|
+
rejectActiveContentType(contentType, entryName);
|
|
1943
|
+
if (localName === "Default") {
|
|
1944
|
+
const extension = attributes.get("Extension");
|
|
1945
|
+
if (!extension || extension.length > 32 || !/^[A-Za-z0-9]+$/.test(extension)) {
|
|
1946
|
+
throw securityError("OOXML default extension is invalid", "unsafe-xml", entryName);
|
|
1947
|
+
}
|
|
1948
|
+
return { kind: "default", extension: extension.toLowerCase(), contentType };
|
|
1949
|
+
}
|
|
1950
|
+
const rawPartName = attributes.get("PartName");
|
|
1951
|
+
if (!rawPartName || !rawPartName.startsWith("/") || rawPartName.length > 2049) {
|
|
1952
|
+
throw securityError("OOXML override part name is invalid", "unsafe-xml", entryName);
|
|
1953
|
+
}
|
|
1954
|
+
const partName = rawPartName.slice(1);
|
|
1955
|
+
if (!isCanonicalPartName(partName)) {
|
|
1956
|
+
throw securityError("OOXML override part name is non-canonical", "invalid-package", entryName);
|
|
1957
|
+
}
|
|
1958
|
+
return { kind: "override", partName, contentType };
|
|
1959
|
+
}
|
|
1960
|
+
function rejectActiveContentType(contentType, entryName) {
|
|
1961
|
+
if (/(?:macroenabled|vba|ole|activex|external(?:link|data)|connections?|querytable|webextension|html|javascript|svg|executable|encrypted|signature)/i.test(
|
|
1962
|
+
contentType
|
|
1963
|
+
)) {
|
|
1964
|
+
throw securityError("Active OOXML content types are unsupported", "active-content", entryName);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
function rejectActiveXmlElement(entryName, localName, attributes) {
|
|
1968
|
+
if ((/* @__PURE__ */ new Set([
|
|
1969
|
+
"oleObject",
|
|
1970
|
+
"oleObjects",
|
|
1971
|
+
"controls",
|
|
1972
|
+
"control",
|
|
1973
|
+
"activeX",
|
|
1974
|
+
"externalLink",
|
|
1975
|
+
"externalData",
|
|
1976
|
+
"connection",
|
|
1977
|
+
"connections",
|
|
1978
|
+
"queryTable",
|
|
1979
|
+
"webExtension"
|
|
1980
|
+
])).has(localName)) {
|
|
1981
|
+
throw securityError(
|
|
1982
|
+
"Active or externally connected OOXML is unsupported",
|
|
1983
|
+
"active-content",
|
|
1984
|
+
entryName
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
if (/\.vml$/i.test(entryName)) {
|
|
1988
|
+
if (["imagedata", "OLEObject"].includes(localName)) {
|
|
1989
|
+
throw securityError(
|
|
1990
|
+
"Active or linked VML content is unsupported",
|
|
1991
|
+
"active-content",
|
|
1992
|
+
entryName
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
for (const name of ["href", "src"]) {
|
|
1996
|
+
if (attributes.has(name)) {
|
|
1997
|
+
throw securityError(
|
|
1998
|
+
"Active or linked VML content is unsupported",
|
|
1999
|
+
"active-content",
|
|
2000
|
+
entryName
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
function detectOpaqueXmlFeature(entryName, localName, attributes, stack, features) {
|
|
2007
|
+
if (/^xl\/charts\//i.test(entryName)) features.add("editable-charts");
|
|
2008
|
+
if (/^customXml\//i.test(entryName)) features.add("custom-xml");
|
|
2009
|
+
if (/^xl\/comments[^/]*\.xml$/i.test(entryName)) features.add("comment-metadata");
|
|
2010
|
+
if (/\.vml$/i.test(entryName)) features.add("comment-layout");
|
|
2011
|
+
if (/^xl\/workbook\.xml$/i.test(entryName)) {
|
|
2012
|
+
if (localName === "sheet" && (attributes.get("state") ?? "visible") !== "visible") {
|
|
2013
|
+
features.add("sheet-visibility");
|
|
2014
|
+
}
|
|
2015
|
+
if (localName === "definedName" || localName === "definedNames") {
|
|
2016
|
+
features.add("defined-names");
|
|
2017
|
+
}
|
|
2018
|
+
if (localName === "workbookPr" && isTruthyXml(attributes.get("date1904"))) {
|
|
2019
|
+
features.add("1904-date-system");
|
|
2020
|
+
}
|
|
2021
|
+
if (localName === "workbookView" && (positiveXmlNumber(attributes.get("activeTab")) || positiveXmlNumber(attributes.get("firstSheet")))) {
|
|
2022
|
+
features.add("workbook-view-state");
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
if (/^xl\/theme\/[^/]+\.xml$/i.test(entryName)) {
|
|
2026
|
+
if (localName === "theme" && attributes.get("name") !== "Office Theme" || localName === "clrScheme" && attributes.get("name") !== "Office") {
|
|
2027
|
+
features.add("workbook-theme");
|
|
2028
|
+
}
|
|
2029
|
+
const colorRole = stack.at(-2);
|
|
2030
|
+
const expectedThemeColors = {
|
|
2031
|
+
dk1: "000000",
|
|
2032
|
+
lt1: "FFFFFF",
|
|
2033
|
+
dk2: "1F497D",
|
|
2034
|
+
lt2: "EEECE1",
|
|
2035
|
+
accent1: "4F81BD",
|
|
2036
|
+
accent2: "C0504D",
|
|
2037
|
+
accent3: "9BBB59",
|
|
2038
|
+
accent4: "8064A2",
|
|
2039
|
+
accent5: "4BACC6",
|
|
2040
|
+
accent6: "F79646",
|
|
2041
|
+
hlink: "0000FF",
|
|
2042
|
+
folHlink: "800080"
|
|
2043
|
+
};
|
|
2044
|
+
if ((localName === "sysClr" || localName === "srgbClr") && colorRole) {
|
|
2045
|
+
const expected = expectedThemeColors[colorRole];
|
|
2046
|
+
const actual = (attributes.get("lastClr") ?? attributes.get("val"))?.toUpperCase();
|
|
2047
|
+
if (expected !== void 0 && actual !== expected) features.add("workbook-theme");
|
|
2048
|
+
}
|
|
2049
|
+
if (localName === "latin") {
|
|
2050
|
+
const fontRole = stack.at(-2);
|
|
2051
|
+
const expected = fontRole === "majorFont" ? "Cambria" : fontRole === "minorFont" ? "Calibri" : void 0;
|
|
2052
|
+
if (expected !== void 0 && attributes.get("typeface") !== expected) {
|
|
2053
|
+
features.add("workbook-theme");
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
if (/^xl\/worksheets\/[^/]+\.xml$/i.test(entryName)) {
|
|
2058
|
+
if ((/* @__PURE__ */ new Set([
|
|
2059
|
+
"hyperlinks",
|
|
2060
|
+
"hyperlink",
|
|
2061
|
+
"sheetProtection",
|
|
2062
|
+
"protectedRanges",
|
|
2063
|
+
"autoFilter",
|
|
2064
|
+
"sortState",
|
|
2065
|
+
"printOptions",
|
|
2066
|
+
"headerFooter",
|
|
2067
|
+
"rowBreaks",
|
|
2068
|
+
"colBreaks",
|
|
2069
|
+
"selection"
|
|
2070
|
+
])).has(localName)) {
|
|
2071
|
+
features.add(`worksheet-${localName}`);
|
|
2072
|
+
}
|
|
2073
|
+
if (localName === "pageMargins" && !xmlAttributesEqual(attributes, {
|
|
2074
|
+
left: "0.7",
|
|
2075
|
+
right: "0.7",
|
|
2076
|
+
top: "0.75",
|
|
2077
|
+
bottom: "0.75",
|
|
2078
|
+
header: "0.3",
|
|
2079
|
+
footer: "0.3"
|
|
2080
|
+
})) {
|
|
2081
|
+
features.add("worksheet-pageMargins");
|
|
2082
|
+
}
|
|
2083
|
+
if (localName === "pageSetup" && !xmlAttributesEqual(attributes, {
|
|
2084
|
+
orientation: "portrait",
|
|
2085
|
+
horizontalDpi: "4294967295",
|
|
2086
|
+
verticalDpi: "4294967295",
|
|
2087
|
+
scale: "100",
|
|
2088
|
+
fitToWidth: "1",
|
|
2089
|
+
fitToHeight: "1"
|
|
2090
|
+
})) {
|
|
2091
|
+
features.add("worksheet-pageSetup");
|
|
2092
|
+
}
|
|
2093
|
+
if (localName === "row" && ["hidden", "outlineLevel", "collapsed", "s", "customFormat"].some(
|
|
2094
|
+
(name) => attributes.has(name)
|
|
2095
|
+
)) {
|
|
2096
|
+
features.add("row-metadata");
|
|
2097
|
+
}
|
|
2098
|
+
if (localName === "col" && ["hidden", "outlineLevel", "collapsed", "style", "bestFit"].some(
|
|
2099
|
+
(name) => attributes.has(name)
|
|
2100
|
+
)) {
|
|
2101
|
+
features.add("column-metadata");
|
|
2102
|
+
}
|
|
2103
|
+
if (localName === "sheetView") {
|
|
2104
|
+
const supported = /* @__PURE__ */ new Set(["workbookViewId", "showGridLines"]);
|
|
2105
|
+
if ([...attributes.keys()].some((name) => !supported.has(name))) {
|
|
2106
|
+
features.add("worksheet-view-state");
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
if (localName === "pane") {
|
|
2110
|
+
const supported = /* @__PURE__ */ new Set(["xSplit", "ySplit", "state"]);
|
|
2111
|
+
if ((attributes.get("state") ?? "frozen") !== "frozen" || [...attributes.keys()].some((name) => !supported.has(name))) {
|
|
2112
|
+
features.add("worksheet-pane-state");
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
if (localName === "f" && attributes.size > 0) features.add("special-formulas");
|
|
2116
|
+
if (localName === "extLst") features.add("worksheet-extensions");
|
|
2117
|
+
if (localName === "sparklineGroups" || localName === "sparklineGroup") {
|
|
2118
|
+
features.add("sparklines");
|
|
2119
|
+
}
|
|
2120
|
+
if (localName === "r" && stack.includes("is")) features.add("rich-text");
|
|
2121
|
+
if (localName === "cfRule") {
|
|
2122
|
+
const type = attributes.get("type");
|
|
2123
|
+
if (type && !SUPPORTED_CONDITIONAL_FORMAT_RULE_TYPES.has(type.toLowerCase())) {
|
|
2124
|
+
features.add("conditional-format-rules");
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (/^xl\/sharedStrings\.xml$/i.test(entryName) && localName === "r") {
|
|
2129
|
+
features.add("rich-text");
|
|
2130
|
+
}
|
|
2131
|
+
if (/^xl\/styles\.xml$/i.test(entryName)) {
|
|
2132
|
+
if ([
|
|
2133
|
+
"gradientFill",
|
|
2134
|
+
"protection",
|
|
2135
|
+
"vertical",
|
|
2136
|
+
"horizontal",
|
|
2137
|
+
"strike",
|
|
2138
|
+
"outline",
|
|
2139
|
+
"shadow",
|
|
2140
|
+
"condense",
|
|
2141
|
+
"extend",
|
|
2142
|
+
"vertAlign"
|
|
2143
|
+
].includes(localName)) {
|
|
2144
|
+
features.add("advanced-cell-styles");
|
|
2145
|
+
}
|
|
2146
|
+
if ([...attributes].some(
|
|
2147
|
+
([name, value]) => [
|
|
2148
|
+
"indexed",
|
|
2149
|
+
"auto",
|
|
2150
|
+
"textRotation",
|
|
2151
|
+
"indent",
|
|
2152
|
+
"shrinkToFit",
|
|
2153
|
+
"readingOrder",
|
|
2154
|
+
"justifyLastLine",
|
|
2155
|
+
"relativeIndent",
|
|
2156
|
+
"diagonalUp",
|
|
2157
|
+
"diagonalDown"
|
|
2158
|
+
].includes(name) || name === "theme" && value !== "1"
|
|
2159
|
+
)) {
|
|
2160
|
+
features.add("advanced-cell-styles");
|
|
2161
|
+
}
|
|
2162
|
+
if (localName === "patternFill" && ![void 0, "none", "gray125", "solid"].includes(attributes.get("patternType"))) {
|
|
2163
|
+
features.add("advanced-cell-styles");
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
if (/^xl\/tables\/[^/]+\.xml$/i.test(entryName)) {
|
|
2167
|
+
if (["calculatedColumnFormula", "totalsRowFormula", "sortState", "filterColumn"].includes(
|
|
2168
|
+
localName
|
|
2169
|
+
)) {
|
|
2170
|
+
features.add("advanced-table-semantics");
|
|
2171
|
+
}
|
|
2172
|
+
if (localName === "tableStyleInfo" && (isTruthyXml(attributes.get("showFirstColumn")) || isTruthyXml(attributes.get("showLastColumn")) || isTruthyXml(attributes.get("showRowStripes")) === isTruthyXml(attributes.get("showColumnStripes")))) {
|
|
2173
|
+
features.add("advanced-table-style");
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
if (/^xl\/drawings\/[^/]+\.xml$/i.test(entryName)) {
|
|
2177
|
+
if (["sp", "grpSp", "cxnSp", "graphicFrame", "absoluteAnchor", "twoCellAnchor"].includes(
|
|
2178
|
+
localName
|
|
2179
|
+
)) {
|
|
2180
|
+
features.add("drawing-objects");
|
|
2181
|
+
}
|
|
2182
|
+
if (localName === "cNvPr" && (attributes.has("descr") || attributes.has("title"))) {
|
|
2183
|
+
features.add("image-metadata");
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
function isTruthyXml(value) {
|
|
2188
|
+
return value === "1" || value?.toLowerCase() === "true";
|
|
2189
|
+
}
|
|
2190
|
+
function positiveXmlNumber(value) {
|
|
2191
|
+
if (value === void 0) return false;
|
|
2192
|
+
const number = Number(value);
|
|
2193
|
+
return Number.isFinite(number) && number > 0;
|
|
2194
|
+
}
|
|
2195
|
+
function xmlAttributesEqual(actual, expected) {
|
|
2196
|
+
const entries = Object.entries(expected);
|
|
2197
|
+
return actual.size === entries.length && entries.every(([name, value]) => actual.get(name) === value);
|
|
2198
|
+
}
|
|
2199
|
+
var RELATIONSHIP_POLICIES = [
|
|
2200
|
+
{ source: /^$/, kind: "officeDocument", target: /^xl\/workbook\.xml$/i },
|
|
2201
|
+
{ source: /^$/, kind: "core-properties", target: /^docProps\/core\.xml$/i },
|
|
2202
|
+
{
|
|
2203
|
+
source: /^$/,
|
|
2204
|
+
kind: "extended-properties",
|
|
2205
|
+
target: /^docProps\/app\.xml$/i
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
source: /^$/,
|
|
2209
|
+
kind: "custom-properties",
|
|
2210
|
+
target: /^docProps\/custom\.xml$/i,
|
|
2211
|
+
opaque: true
|
|
2212
|
+
},
|
|
2213
|
+
{
|
|
2214
|
+
source: /^xl\/workbook\.xml$/i,
|
|
2215
|
+
kind: "styles",
|
|
2216
|
+
target: /^xl\/styles\.xml$/i
|
|
2217
|
+
},
|
|
2218
|
+
{
|
|
2219
|
+
source: /^xl\/workbook\.xml$/i,
|
|
2220
|
+
kind: "theme",
|
|
2221
|
+
target: /^xl\/theme\/[^/]+\.xml$/i
|
|
2222
|
+
},
|
|
2223
|
+
{
|
|
2224
|
+
source: /^xl\/workbook\.xml$/i,
|
|
2225
|
+
kind: "sharedStrings",
|
|
2226
|
+
target: /^xl\/sharedStrings\.xml$/i
|
|
2227
|
+
},
|
|
2228
|
+
{
|
|
2229
|
+
source: /^xl\/workbook\.xml$/i,
|
|
2230
|
+
kind: "worksheet",
|
|
2231
|
+
target: /^xl\/worksheets\/[^/]+\.xml$/i
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
source: /^xl\/workbook\.xml$/i,
|
|
2235
|
+
kind: "calcChain",
|
|
2236
|
+
target: /^xl\/calcChain\.xml$/i
|
|
2237
|
+
},
|
|
2238
|
+
{
|
|
2239
|
+
source: /^xl\/worksheets\/[^/]+\.xml$/i,
|
|
2240
|
+
kind: "comments",
|
|
2241
|
+
target: /^xl\/comments[^/]*\.xml$/i,
|
|
2242
|
+
opaque: true
|
|
2243
|
+
},
|
|
2244
|
+
{
|
|
2245
|
+
source: /^xl\/worksheets\/[^/]+\.xml$/i,
|
|
2246
|
+
kind: "vmlDrawing",
|
|
2247
|
+
target: /^xl\/drawings\/[^/]+\.vml$/i,
|
|
2248
|
+
opaque: true
|
|
2249
|
+
},
|
|
2250
|
+
{
|
|
2251
|
+
source: /^xl\/worksheets\/[^/]+\.xml$/i,
|
|
2252
|
+
kind: "table",
|
|
2253
|
+
target: /^xl\/tables\/[^/]+\.xml$/i
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
source: /^xl\/worksheets\/[^/]+\.xml$/i,
|
|
2257
|
+
kind: "drawing",
|
|
2258
|
+
target: /^xl\/drawings\/[^/]+\.xml$/i
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
source: /^xl\/drawings\/[^/]+\.xml$/i,
|
|
2262
|
+
kind: "image",
|
|
2263
|
+
target: /^xl\/media\/[^/]+\.(?:png|jpe?g|gif)$/i
|
|
2264
|
+
},
|
|
2265
|
+
{
|
|
2266
|
+
source: /^xl\/drawings\/[^/]+\.xml$/i,
|
|
2267
|
+
kind: "chart",
|
|
2268
|
+
target: /^xl\/charts\/[^/]+\.xml$/i,
|
|
2269
|
+
opaque: true
|
|
2270
|
+
},
|
|
2271
|
+
{
|
|
2272
|
+
source: /^xl\/charts\/[^/]+\.xml$/i,
|
|
2273
|
+
kind: "image",
|
|
2274
|
+
target: /^xl\/media\/[^/]+\.(?:png|jpe?g|gif)$/i,
|
|
2275
|
+
opaque: true
|
|
2276
|
+
},
|
|
2277
|
+
{
|
|
2278
|
+
source: /^xl\/charts\/[^/]+\.xml$/i,
|
|
2279
|
+
kind: "themeOverride",
|
|
2280
|
+
target: /^xl\/theme\/[^/]+\.xml$/i,
|
|
2281
|
+
opaque: true
|
|
2282
|
+
},
|
|
2283
|
+
{
|
|
2284
|
+
source: /^xl\/workbook\.xml$/i,
|
|
2285
|
+
kind: "customXml",
|
|
2286
|
+
target: /^customXml\/item\d+\.xml$/i,
|
|
2287
|
+
opaque: true
|
|
2288
|
+
},
|
|
2289
|
+
{
|
|
2290
|
+
source: /^customXml\/item\d+\.xml$/i,
|
|
2291
|
+
kind: "customXmlProps",
|
|
2292
|
+
target: /^customXml\/itemProps\d+\.xml$/i,
|
|
2293
|
+
opaque: true
|
|
2294
|
+
}
|
|
2295
|
+
];
|
|
2296
|
+
var OFFICE_RELATIONSHIP_PREFIXES = [
|
|
2297
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/",
|
|
2298
|
+
"http://purl.oclc.org/ooxml/officeDocument/relationships/",
|
|
2299
|
+
"http://schemas.microsoft.com/office/2006/relationships/"
|
|
2300
|
+
];
|
|
2301
|
+
function inspectRelationshipTag(attributes, entryName, entryNames) {
|
|
2302
|
+
const source = relationshipSource(entryName);
|
|
2303
|
+
for (const name of attributes.keys()) {
|
|
2304
|
+
if (!["Id", "Type", "Target", "TargetMode"].includes(name)) {
|
|
2305
|
+
throw securityError("OOXML relationship attributes are unsupported", "unsafe-xml", entryName);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const type = attributes.get("Type");
|
|
2309
|
+
const target = attributes.get("Target");
|
|
2310
|
+
const id = attributes.get("Id");
|
|
2311
|
+
if (!id || id.length > 512 || !type || type.length > 2048 || !target || target.length > 4096) {
|
|
2312
|
+
throw securityError(
|
|
2313
|
+
"OOXML relationship is missing or exceeds required attributes",
|
|
2314
|
+
"unsafe-xml",
|
|
2315
|
+
entryName
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
if ((attributes.get("TargetMode") ?? "Internal") !== "Internal") {
|
|
2319
|
+
throw securityError(
|
|
2320
|
+
"External OOXML relationships are forbidden",
|
|
2321
|
+
"external-relationship",
|
|
2322
|
+
entryName
|
|
2323
|
+
);
|
|
2324
|
+
}
|
|
2325
|
+
const kind = relationshipKind(type);
|
|
2326
|
+
const resolvedTarget = resolveRelationshipTarget(source, target, entryName);
|
|
2327
|
+
if (source !== "" && !entryNames.has(source.toLowerCase())) {
|
|
2328
|
+
throw securityError("OOXML relationship source part is missing", "invalid-package", entryName);
|
|
2329
|
+
}
|
|
2330
|
+
const policy = RELATIONSHIP_POLICIES.find(
|
|
2331
|
+
(candidate) => candidate.kind === kind && candidate.source.test(source) && candidate.target.test(resolvedTarget)
|
|
2332
|
+
);
|
|
2333
|
+
if (!policy) {
|
|
2334
|
+
throw securityError(
|
|
2335
|
+
"Unsupported OOXML relationship type or target",
|
|
2336
|
+
"active-content",
|
|
2337
|
+
entryName
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
if (!entryNames.has(resolvedTarget.toLowerCase())) {
|
|
2341
|
+
throw securityError(
|
|
2342
|
+
"OOXML relationship target is missing from the package",
|
|
2343
|
+
"invalid-package",
|
|
2344
|
+
entryName
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
return {
|
|
2348
|
+
id,
|
|
2349
|
+
kind,
|
|
2350
|
+
sourcePart: source,
|
|
2351
|
+
type,
|
|
2352
|
+
targetPart: entryNames.get(resolvedTarget.toLowerCase()),
|
|
2353
|
+
opaque: policy.opaque === true
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
function relationshipKind(type) {
|
|
2357
|
+
if (type === "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties") {
|
|
2358
|
+
return "core-properties";
|
|
2359
|
+
}
|
|
2360
|
+
for (const prefix of OFFICE_RELATIONSHIP_PREFIXES) {
|
|
2361
|
+
if (type.startsWith(prefix)) {
|
|
2362
|
+
const kind = type.slice(prefix.length);
|
|
2363
|
+
return /^[A-Za-z][A-Za-z0-9.-]*$/.test(kind) ? kind : null;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
return null;
|
|
2367
|
+
}
|
|
2368
|
+
function relationshipSource(entryName) {
|
|
2369
|
+
if (entryName.toLowerCase() === "_rels/.rels") return "";
|
|
2370
|
+
const match = /^(.*\/)_rels\/([^/]+)\.rels$/i.exec(entryName);
|
|
2371
|
+
if (!match) {
|
|
2372
|
+
throw securityError(
|
|
2373
|
+
"OOXML relationship part has an invalid location",
|
|
2374
|
+
"invalid-package",
|
|
2375
|
+
entryName
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
return `${match[1]}${match[2]}`;
|
|
2379
|
+
}
|
|
2380
|
+
function resolveRelationshipTarget(source, target, entryName) {
|
|
2381
|
+
if (target.length === 0 || target.includes("\\") || target.includes("%") || target.includes("?") || target.includes("#") || target.startsWith("/") || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(target) || /[\u0000-\u001f\u007f]/.test(target)) {
|
|
2382
|
+
throw securityError(
|
|
2383
|
+
"External or ambiguous OOXML relationship target",
|
|
2384
|
+
"external-relationship",
|
|
2385
|
+
entryName
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
const segments = source.includes("/") ? source.split("/").slice(0, -1) : [];
|
|
2389
|
+
for (const segment of target.split("/")) {
|
|
2390
|
+
if (segment === "" || segment === ".") continue;
|
|
2391
|
+
if (segment === "..") {
|
|
2392
|
+
if (segments.length === 0) {
|
|
2393
|
+
throw securityError(
|
|
2394
|
+
"OOXML relationship escapes the package root",
|
|
2395
|
+
"external-relationship",
|
|
2396
|
+
entryName
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
segments.pop();
|
|
2400
|
+
} else {
|
|
2401
|
+
segments.push(segment);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
if (segments.length === 0) {
|
|
2405
|
+
throw securityError("OOXML relationship target is empty", "invalid-package", entryName);
|
|
2406
|
+
}
|
|
2407
|
+
return segments.join("/");
|
|
2408
|
+
}
|
|
2409
|
+
function securityError(message, code, entryName) {
|
|
2410
|
+
return entryName ? new SpreadsheetSecurityError(`${message}: ${entryName}`, code, entryName) : new SpreadsheetSecurityError(message, code);
|
|
2411
|
+
}
|
|
2412
|
+
function xlsxZipFailure(kind, message, entryName) {
|
|
2413
|
+
throw securityError(
|
|
2414
|
+
message,
|
|
2415
|
+
kind === "limit" ? "limit-exceeded" : kind === "encrypted" ? "encrypted-content" : "invalid-package",
|
|
2416
|
+
entryName
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
function assertSafeFormula(formula, location) {
|
|
2420
|
+
try {
|
|
2421
|
+
validateSupportedFormula(formula);
|
|
2422
|
+
} catch (error) {
|
|
2423
|
+
const failure = securityError(
|
|
2424
|
+
`Unsupported or potentially active spreadsheet formula at ${location}`,
|
|
2425
|
+
"active-content"
|
|
2426
|
+
);
|
|
2427
|
+
Object.defineProperty(failure, "cause", {
|
|
2428
|
+
value: error,
|
|
2429
|
+
enumerable: false
|
|
2430
|
+
});
|
|
2431
|
+
throw failure;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
function safeExportImage(image) {
|
|
2435
|
+
let normalized;
|
|
2436
|
+
try {
|
|
2437
|
+
normalized = normalizeSpreadsheetImageConfig(image);
|
|
2438
|
+
} catch (error) {
|
|
2439
|
+
const failure = securityError("Spreadsheet image data is invalid", "active-content");
|
|
2440
|
+
Object.defineProperty(failure, "cause", {
|
|
2441
|
+
value: error,
|
|
2442
|
+
enumerable: false
|
|
2443
|
+
});
|
|
2444
|
+
throw failure;
|
|
2445
|
+
}
|
|
2446
|
+
if (!normalized.dataUrl || !["image/png", "image/jpeg", "image/gif"].includes(normalized.contentType ?? "")) {
|
|
2447
|
+
throw securityError(
|
|
2448
|
+
"Spreadsheet images must be inline PNG, JPEG, or GIF data",
|
|
2449
|
+
"active-content"
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
return normalized;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
export {
|
|
2456
|
+
SpreadsheetXlsxCodec
|
|
2457
|
+
};
|
|
2458
|
+
//# sourceMappingURL=chunk-DCTLTNPW.js.map
|