clear-excel 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +26 -6
  3. package/dist/api/sanitize-excel-file.d.ts +0 -1
  4. package/dist/api/sanitize-excel-file.js +176 -0
  5. package/dist/api/sanitize-excel-file.js.map +1 -0
  6. package/dist/api/sanitize-excel.d.ts +0 -1
  7. package/dist/api/sanitize-excel.js +25 -0
  8. package/dist/api/sanitize-excel.js.map +1 -0
  9. package/dist/biff/workbook.d.ts +0 -1
  10. package/dist/biff/workbook.js +169 -0
  11. package/dist/biff/workbook.js.map +1 -0
  12. package/dist/constants.d.ts +0 -1
  13. package/dist/constants.js +28 -0
  14. package/dist/constants.js.map +1 -0
  15. package/dist/errors.d.ts +0 -1
  16. package/dist/errors.js +38 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/formats.d.ts +0 -1
  19. package/dist/formats.js +16 -0
  20. package/dist/formats.js.map +1 -0
  21. package/dist/index.d.ts +0 -1
  22. package/dist/index.js +4 -2776
  23. package/dist/index.js.map +1 -1
  24. package/dist/ooxml/cleanup.d.ts +0 -1
  25. package/dist/ooxml/cleanup.js +30 -0
  26. package/dist/ooxml/cleanup.js.map +1 -0
  27. package/dist/ooxml/constants.d.ts +0 -1
  28. package/dist/ooxml/constants.js +19 -0
  29. package/dist/ooxml/constants.js.map +1 -0
  30. package/dist/ooxml/parts.d.ts +1 -1
  31. package/dist/ooxml/parts.js +94 -0
  32. package/dist/ooxml/parts.js.map +1 -0
  33. package/dist/ooxml/relationships.d.ts +0 -1
  34. package/dist/ooxml/relationships.js +132 -0
  35. package/dist/ooxml/relationships.js.map +1 -0
  36. package/dist/ooxml/styles.d.ts +0 -1
  37. package/dist/ooxml/styles.js +67 -0
  38. package/dist/ooxml/styles.js.map +1 -0
  39. package/dist/ooxml/workbook.d.ts +0 -1
  40. package/dist/ooxml/workbook.js +119 -0
  41. package/dist/ooxml/workbook.js.map +1 -0
  42. package/dist/ooxml/xml.d.ts +26 -2
  43. package/dist/ooxml/xml.js +304 -0
  44. package/dist/ooxml/xml.js.map +1 -0
  45. package/dist/options.d.ts +0 -1
  46. package/dist/options.js +80 -0
  47. package/dist/options.js.map +1 -0
  48. package/dist/types.d.ts +0 -1
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/zip/central-directory.d.ts +0 -1
  52. package/dist/zip/central-directory.js +131 -0
  53. package/dist/zip/central-directory.js.map +1 -0
  54. package/package.json +17 -25
  55. package/dist/api/sanitize-excel-file.d.ts.map +0 -1
  56. package/dist/api/sanitize-excel.d.ts.map +0 -1
  57. package/dist/biff/workbook.d.ts.map +0 -1
  58. package/dist/constants.d.ts.map +0 -1
  59. package/dist/errors.d.ts.map +0 -1
  60. package/dist/formats.d.ts.map +0 -1
  61. package/dist/index.d.ts.map +0 -1
  62. package/dist/ooxml/cleanup.d.ts.map +0 -1
  63. package/dist/ooxml/constants.d.ts.map +0 -1
  64. package/dist/ooxml/parts.d.ts.map +0 -1
  65. package/dist/ooxml/relationships.d.ts.map +0 -1
  66. package/dist/ooxml/styles.d.ts.map +0 -1
  67. package/dist/ooxml/workbook.d.ts.map +0 -1
  68. package/dist/ooxml/xml.d.ts.map +0 -1
  69. package/dist/options.d.ts.map +0 -1
  70. package/dist/types.d.ts.map +0 -1
  71. package/dist/zip/central-directory.d.ts.map +0 -1
@@ -0,0 +1,94 @@
1
+ import { TextDecoder } from "node:util";
2
+ import { limitExceeded } from "../errors.js";
3
+ function getNodeBufferStream(file) {
4
+ // JSZip's declarations omit its public internalStream helper, while the
5
+ // runtime API is required to enforce byte budgets before full inflation.
6
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
7
+ return file.internalStream("nodebuffer");
8
+ }
9
+ export async function requireXml(zip, partName, budget, partLimitBytes) {
10
+ const file = zip.file(partName);
11
+ if (!file) {
12
+ throw new Error(`Required workbook part is missing: ${partName}`);
13
+ }
14
+ return readXmlPart(file, partName, budget, partLimitBytes);
15
+ }
16
+ export async function readXmlPart(file, partName, budget, partLimitBytes) {
17
+ const chunks = [];
18
+ await consumePart(file, partName, budget, "maxXmlBytes", (chunk) => chunks.push(Buffer.from(chunk)), partLimitBytes);
19
+ return decodeXmlBytes(Buffer.concat(chunks), partName);
20
+ }
21
+ export async function validateUncompressedSize(zip, limitBytes) {
22
+ const budget = { consumedBytes: 0, limitBytes };
23
+ for (const file of Object.values(zip.files)) {
24
+ if (!file.dir) {
25
+ // Reads stay sequential to enforce the shared budget without retaining file contents.
26
+ // oxlint-disable-next-line no-await-in-loop
27
+ await consumePart(file, file.name, budget, "maxUncompressedBytes");
28
+ }
29
+ }
30
+ }
31
+ function consumePart(file, partName, budget, budgetLimitName, onChunk, partLimitBytes) {
32
+ return new Promise((resolve, reject) => {
33
+ const stream = getNodeBufferStream(file);
34
+ let partBytes = 0;
35
+ let settled = false;
36
+ const fail = (error) => {
37
+ if (settled) {
38
+ return;
39
+ }
40
+ settled = true;
41
+ stream.pause();
42
+ reject(error);
43
+ };
44
+ stream
45
+ .on("data", (chunk) => {
46
+ if (settled) {
47
+ return;
48
+ }
49
+ partBytes += chunk.length;
50
+ budget.consumedBytes += chunk.length;
51
+ if (partLimitBytes !== undefined && partBytes > partLimitBytes) {
52
+ fail(limitExceeded("partBytes", partLimitBytes, partBytes, partName));
53
+ return;
54
+ }
55
+ if (budget.consumedBytes > budget.limitBytes) {
56
+ fail(limitExceeded(budgetLimitName, budget.limitBytes, budget.consumedBytes, partName));
57
+ return;
58
+ }
59
+ onChunk?.(chunk);
60
+ })
61
+ .on("error", (error) => fail(error))
62
+ .on("end", () => {
63
+ if (settled) {
64
+ return;
65
+ }
66
+ settled = true;
67
+ resolve();
68
+ })
69
+ .resume();
70
+ });
71
+ }
72
+ function decodeXmlBytes(bytes, partName) {
73
+ let encoding = "utf-8";
74
+ if ((bytes[0] === 0xff && bytes[1] === 0xfe) || (bytes[0] === 0x3c && bytes[1] === 0x00)) {
75
+ encoding = "utf-16le";
76
+ }
77
+ else if ((bytes[0] === 0xfe && bytes[1] === 0xff) || (bytes[0] === 0x00 && bytes[1] === 0x3c)) {
78
+ encoding = "utf-16be";
79
+ }
80
+ const xml = new TextDecoder(encoding, { fatal: true }).decode(bytes);
81
+ const declaration = /^<\?xml\s[^?]*\?>/.exec(xml)?.[0];
82
+ const declaredEncoding = declaration
83
+ ?.match(/\bencoding\s*=\s*(["'])([^"']+)\1/i)?.[2]
84
+ ?.toLowerCase();
85
+ if (declaredEncoding !== undefined &&
86
+ declaredEncoding !== encoding &&
87
+ !(declaredEncoding === "utf-16" && encoding.startsWith("utf-16"))) {
88
+ throw new Error(`Unsupported or inconsistent XML encoding in ${partName}: ${declaredEncoding}`);
89
+ }
90
+ return declaration
91
+ ? xml.replace(declaration, declaration.replace(/(\bencoding\s*=\s*)(["'])[^"']+\2/i, '$1"UTF-8"'))
92
+ : xml;
93
+ }
94
+ //# sourceMappingURL=parts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parts.js","sourceRoot":"","sources":["../../src/ooxml/parts.ts"],"names":[],"mappings":"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","sourcesContent":["import { TextDecoder } from \"node:util\";\n\nimport type JSZip from \"jszip\";\n\nimport { limitExceeded } from \"../errors.js\";\nimport type { ReadBudget } from \"../types.js\";\n\ninterface StreamHelper {\n on(event: \"data\", listener: (chunk: Buffer) => void): StreamHelper;\n on(event: \"error\", listener: (error: unknown) => void): StreamHelper;\n on(event: \"end\", listener: () => void): StreamHelper;\n pause(): StreamHelper;\n resume(): StreamHelper;\n}\n\ninterface StreamableZipObject {\n internalStream(type: \"nodebuffer\"): StreamHelper;\n}\n\nfunction getNodeBufferStream(file: JSZip.JSZipObject): StreamHelper {\n // JSZip's declarations omit its public internalStream helper, while the\n // runtime API is required to enforce byte budgets before full inflation.\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion\n return (file as unknown as StreamableZipObject).internalStream(\"nodebuffer\");\n}\n\nexport async function requireXml(\n zip: JSZip,\n partName: string,\n budget: ReadBudget,\n partLimitBytes: number,\n): Promise<string> {\n const file = zip.file(partName);\n if (!file) {\n throw new Error(`Required workbook part is missing: ${partName}`);\n }\n return readXmlPart(file, partName, budget, partLimitBytes);\n}\n\nexport async function readXmlPart(\n file: JSZip.JSZipObject,\n partName: string,\n budget: ReadBudget,\n partLimitBytes: number,\n): Promise<string> {\n const chunks: Buffer[] = [];\n await consumePart(\n file,\n partName,\n budget,\n \"maxXmlBytes\",\n (chunk) => chunks.push(Buffer.from(chunk)),\n partLimitBytes,\n );\n return decodeXmlBytes(Buffer.concat(chunks), partName);\n}\n\nexport async function validateUncompressedSize(zip: JSZip, limitBytes: number): Promise<void> {\n const budget: ReadBudget = { consumedBytes: 0, limitBytes };\n for (const file of Object.values(zip.files)) {\n if (!file.dir) {\n // Reads stay sequential to enforce the shared budget without retaining file contents.\n // oxlint-disable-next-line no-await-in-loop\n await consumePart(file, file.name, budget, \"maxUncompressedBytes\");\n }\n }\n}\n\nfunction consumePart(\n file: JSZip.JSZipObject,\n partName: string,\n budget: ReadBudget,\n budgetLimitName: \"maxXmlBytes\" | \"maxUncompressedBytes\",\n onChunk?: (chunk: Buffer) => void,\n partLimitBytes?: number,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n const stream = getNodeBufferStream(file);\n let partBytes = 0;\n let settled = false;\n\n const fail = (error: unknown): void => {\n if (settled) {\n return;\n }\n settled = true;\n stream.pause();\n reject(error);\n };\n\n stream\n .on(\"data\", (chunk) => {\n if (settled) {\n return;\n }\n partBytes += chunk.length;\n budget.consumedBytes += chunk.length;\n if (partLimitBytes !== undefined && partBytes > partLimitBytes) {\n fail(limitExceeded(\"partBytes\", partLimitBytes, partBytes, partName));\n return;\n }\n if (budget.consumedBytes > budget.limitBytes) {\n fail(limitExceeded(budgetLimitName, budget.limitBytes, budget.consumedBytes, partName));\n return;\n }\n onChunk?.(chunk);\n })\n .on(\"error\", (error) => fail(error))\n .on(\"end\", () => {\n if (settled) {\n return;\n }\n settled = true;\n resolve();\n })\n .resume();\n });\n}\n\nfunction decodeXmlBytes(bytes: Buffer, partName: string): string {\n let encoding = \"utf-8\";\n if ((bytes[0] === 0xff && bytes[1] === 0xfe) || (bytes[0] === 0x3c && bytes[1] === 0x00)) {\n encoding = \"utf-16le\";\n } else if ((bytes[0] === 0xfe && bytes[1] === 0xff) || (bytes[0] === 0x00 && bytes[1] === 0x3c)) {\n encoding = \"utf-16be\";\n }\n const xml = new TextDecoder(encoding, { fatal: true }).decode(bytes);\n const declaration = /^<\\?xml\\s[^?]*\\?>/.exec(xml)?.[0];\n const declaredEncoding = declaration\n ?.match(/\\bencoding\\s*=\\s*([\"'])([^\"']+)\\1/i)?.[2]\n ?.toLowerCase();\n if (\n declaredEncoding !== undefined &&\n declaredEncoding !== encoding &&\n !(declaredEncoding === \"utf-16\" && encoding.startsWith(\"utf-16\"))\n ) {\n throw new Error(`Unsupported or inconsistent XML encoding in ${partName}: ${declaredEncoding}`);\n }\n return declaration\n ? xml.replace(\n declaration,\n declaration.replace(/(\\bencoding\\s*=\\s*)([\"'])[^\"']+\\2/i, '$1\"UTF-8\"'),\n )\n : xml;\n}\n"]}
@@ -18,4 +18,3 @@ export declare function relationshipFileForPart(partName: string): string;
18
18
  export declare function sourcePartForRelationshipFile(fileName: string): string | undefined;
19
19
  export declare function removeUnreachableParts(zip: JSZip, detachedRoots: Set<string>, readBudget: ReadBudget, limits: Readonly<ExcelSanitizationLimits>): Promise<void>;
20
20
  export declare function collectReachableParts(initial: ReadonlySet<string>, graph: ReadonlyMap<string, readonly Relationship[]>): Set<string>;
21
- //# sourceMappingURL=relationships.d.ts.map
@@ -0,0 +1,132 @@
1
+ import { posix as packagePath } from "node:path";
2
+ import { readXmlPart, requireXml } from "./parts.js";
3
+ import { findElements, getNamespacedAttribute, removeElements } from "./xml.js";
4
+ const PACKAGE_RELATIONSHIP_NAMESPACES = new Set([
5
+ "",
6
+ "http://schemas.openxmlformats.org/package/2006/relationships",
7
+ "http://purl.oclc.org/ooxml/package/relationships",
8
+ ]);
9
+ export function removeRelationships(xml, source, relationshipSuffixes) {
10
+ const targets = new Set();
11
+ const removedIds = new Set();
12
+ for (const relationship of parseRelationships(xml, source)) {
13
+ if (relationship.targetMode?.toLowerCase() !== "external" &&
14
+ relationshipSuffixes.some((suffix) => relationship.type.endsWith(suffix))) {
15
+ removedIds.add(relationship.id);
16
+ targets.add(relationship.target);
17
+ }
18
+ }
19
+ const cleaned = removeElements(xml, (element) => isRelationshipElement(element) &&
20
+ removedIds.has(getNamespacedAttribute(element, "Id", [""]) ?? ""));
21
+ return { xml: cleaned, targets };
22
+ }
23
+ export function parseRelationships(xml, source) {
24
+ const ids = new Set();
25
+ return findElements(xml, "Relationship")
26
+ .filter(isRelationshipElement)
27
+ .map((element) => {
28
+ const id = getNamespacedAttribute(element, "Id", [""]);
29
+ const target = getNamespacedAttribute(element, "Target", [""]);
30
+ const type = getNamespacedAttribute(element, "Type", [""]);
31
+ if (!id || !target || !type) {
32
+ throw new Error(`Invalid relationship in ${source || "package root"}`);
33
+ }
34
+ if (ids.has(id)) {
35
+ throw new Error(`Duplicate relationship ID in ${source || "package root"}: ${id}`);
36
+ }
37
+ ids.add(id);
38
+ const targetMode = getNamespacedAttribute(element, "TargetMode", [""]);
39
+ if (targetMode !== undefined &&
40
+ !["internal", "external"].includes(targetMode.toLowerCase())) {
41
+ throw new Error(`Invalid relationship target mode: ${targetMode}`);
42
+ }
43
+ return {
44
+ id,
45
+ target: targetMode?.toLowerCase() === "external" ? target : resolveTarget(source, target),
46
+ targetMode,
47
+ type,
48
+ };
49
+ });
50
+ }
51
+ function isRelationshipElement(element) {
52
+ return (element.localName === "Relationship" &&
53
+ PACKAGE_RELATIONSHIP_NAMESPACES.has(element.namespaceUri));
54
+ }
55
+ export function resolveTarget(source, target) {
56
+ const resolved = target.startsWith("/")
57
+ ? packagePath.normalize(target.slice(1))
58
+ : packagePath.normalize(packagePath.join(packagePath.dirname(source), target));
59
+ if (resolved === ".." || resolved.startsWith("../")) {
60
+ throw new Error(`Relationship target escapes package: ${target}`);
61
+ }
62
+ return resolved;
63
+ }
64
+ export function relationshipFileForPart(partName) {
65
+ return packagePath.join(packagePath.dirname(partName), "_rels", `${packagePath.basename(partName)}.rels`);
66
+ }
67
+ export function sourcePartForRelationshipFile(fileName) {
68
+ if (fileName === "_rels/.rels") {
69
+ return "";
70
+ }
71
+ const match = fileName.match(/^(.*\/)_rels\/([^/]+)\.rels$/);
72
+ return match ? `${match[1]}${match[2]}` : undefined;
73
+ }
74
+ export async function removeUnreachableParts(zip, detachedRoots, readBudget, limits) {
75
+ if (detachedRoots.size === 0) {
76
+ return;
77
+ }
78
+ const graph = await buildRelationshipGraph(zip, readBudget, limits);
79
+ const candidates = collectReachableParts(detachedRoots, graph);
80
+ const packageReachable = collectReachableParts(new Set([""]), graph);
81
+ packageReachable.delete("");
82
+ const deletedParts = new Set([...candidates].filter((partName) => !packageReachable.has(partName)));
83
+ for (const partName of deletedParts) {
84
+ zip.remove(partName);
85
+ zip.remove(relationshipFileForPart(partName));
86
+ }
87
+ const contentTypes = await requireXml(zip, "[Content_Types].xml", readBudget, limits.maxMetadataXmlBytes);
88
+ zip.file("[Content_Types].xml", removeElements(contentTypes, (element) => {
89
+ if (element.localName !== "Override" ||
90
+ !["", "http://schemas.openxmlformats.org/package/2006/content-types"].includes(element.namespaceUri)) {
91
+ return false;
92
+ }
93
+ const partName = getNamespacedAttribute(element, "PartName", [""])?.replace(/^\//, "");
94
+ return partName !== undefined && deletedParts.has(partName);
95
+ }));
96
+ }
97
+ async function buildRelationshipGraph(zip, readBudget, limits) {
98
+ const graph = new Map();
99
+ for (const fileName of Object.keys(zip.files)) {
100
+ if (!fileName.endsWith(".rels")) {
101
+ continue;
102
+ }
103
+ const source = sourcePartForRelationshipFile(fileName);
104
+ const file = zip.file(fileName);
105
+ if (source !== undefined && file) {
106
+ graph.set(source, parseRelationships(
107
+ // Reads intentionally stay sequential because they share a mutable byte budget.
108
+ // oxlint-disable-next-line no-await-in-loop
109
+ await readXmlPart(file, fileName, readBudget, limits.maxRelationshipsXmlBytes), source));
110
+ }
111
+ }
112
+ return graph;
113
+ }
114
+ export function collectReachableParts(initial, graph) {
115
+ const reachable = new Set(initial);
116
+ const pending = [...initial];
117
+ while (pending.length > 0) {
118
+ const source = pending.pop();
119
+ if (source === undefined) {
120
+ continue;
121
+ }
122
+ for (const relationship of graph.get(source) ?? []) {
123
+ if (relationship.targetMode?.toLowerCase() !== "external" &&
124
+ !reachable.has(relationship.target)) {
125
+ reachable.add(relationship.target);
126
+ pending.push(relationship.target);
127
+ }
128
+ }
129
+ }
130
+ return reachable;
131
+ }
132
+ //# sourceMappingURL=relationships.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relationships.js","sourceRoot":"","sources":["../../src/ooxml/relationships.ts"],"names":[],"mappings":"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","sourcesContent":["import { posix as packagePath } from \"node:path\";\n\nimport type JSZip from \"jszip\";\n\nimport type { ExcelSanitizationLimits, ReadBudget } from \"../types.js\";\nimport { readXmlPart, requireXml } from \"./parts.js\";\nimport { findElements, getNamespacedAttribute, removeElements, type XmlElement } from \"./xml.js\";\n\nconst PACKAGE_RELATIONSHIP_NAMESPACES = new Set([\n \"\",\n \"http://schemas.openxmlformats.org/package/2006/relationships\",\n \"http://purl.oclc.org/ooxml/package/relationships\",\n]);\n\nexport interface Relationship {\n id: string;\n target: string;\n targetMode: string | undefined;\n type: string;\n}\n\nexport type RelationshipGraph = Map<string, Relationship[]>;\n\nexport interface RemovedRelationships {\n xml: string;\n targets: Set<string>;\n}\n\nexport function removeRelationships(\n xml: string,\n source: string,\n relationshipSuffixes: readonly string[],\n): RemovedRelationships {\n const targets = new Set<string>();\n const removedIds = new Set<string>();\n for (const relationship of parseRelationships(xml, source)) {\n if (\n relationship.targetMode?.toLowerCase() !== \"external\" &&\n relationshipSuffixes.some((suffix) => relationship.type.endsWith(suffix))\n ) {\n removedIds.add(relationship.id);\n targets.add(relationship.target);\n }\n }\n const cleaned = removeElements(\n xml,\n (element) =>\n isRelationshipElement(element) &&\n removedIds.has(getNamespacedAttribute(element, \"Id\", [\"\"]) ?? \"\"),\n );\n return { xml: cleaned, targets };\n}\n\nexport function parseRelationships(xml: string, source: string): Relationship[] {\n const ids = new Set<string>();\n return findElements(xml, \"Relationship\")\n .filter(isRelationshipElement)\n .map((element) => {\n const id = getNamespacedAttribute(element, \"Id\", [\"\"]);\n const target = getNamespacedAttribute(element, \"Target\", [\"\"]);\n const type = getNamespacedAttribute(element, \"Type\", [\"\"]);\n if (!id || !target || !type) {\n throw new Error(`Invalid relationship in ${source || \"package root\"}`);\n }\n if (ids.has(id)) {\n throw new Error(`Duplicate relationship ID in ${source || \"package root\"}: ${id}`);\n }\n ids.add(id);\n const targetMode = getNamespacedAttribute(element, \"TargetMode\", [\"\"]);\n if (\n targetMode !== undefined &&\n ![\"internal\", \"external\"].includes(targetMode.toLowerCase())\n ) {\n throw new Error(`Invalid relationship target mode: ${targetMode}`);\n }\n return {\n id,\n target: targetMode?.toLowerCase() === \"external\" ? target : resolveTarget(source, target),\n targetMode,\n type,\n };\n });\n}\n\nfunction isRelationshipElement(element: XmlElement): boolean {\n return (\n element.localName === \"Relationship\" &&\n PACKAGE_RELATIONSHIP_NAMESPACES.has(element.namespaceUri)\n );\n}\n\nexport function resolveTarget(source: string, target: string): string {\n const resolved = target.startsWith(\"/\")\n ? packagePath.normalize(target.slice(1))\n : packagePath.normalize(packagePath.join(packagePath.dirname(source), target));\n if (resolved === \"..\" || resolved.startsWith(\"../\")) {\n throw new Error(`Relationship target escapes package: ${target}`);\n }\n return resolved;\n}\n\nexport function relationshipFileForPart(partName: string): string {\n return packagePath.join(\n packagePath.dirname(partName),\n \"_rels\",\n `${packagePath.basename(partName)}.rels`,\n );\n}\n\nexport function sourcePartForRelationshipFile(fileName: string): string | undefined {\n if (fileName === \"_rels/.rels\") {\n return \"\";\n }\n const match = fileName.match(/^(.*\\/)_rels\\/([^/]+)\\.rels$/);\n return match ? `${match[1]}${match[2]}` : undefined;\n}\n\nexport async function removeUnreachableParts(\n zip: JSZip,\n detachedRoots: Set<string>,\n readBudget: ReadBudget,\n limits: Readonly<ExcelSanitizationLimits>,\n): Promise<void> {\n if (detachedRoots.size === 0) {\n return;\n }\n\n const graph = await buildRelationshipGraph(zip, readBudget, limits);\n const candidates = collectReachableParts(detachedRoots, graph);\n const packageReachable = collectReachableParts(new Set([\"\"]), graph);\n packageReachable.delete(\"\");\n const deletedParts = new Set(\n [...candidates].filter((partName) => !packageReachable.has(partName)),\n );\n for (const partName of deletedParts) {\n zip.remove(partName);\n zip.remove(relationshipFileForPart(partName));\n }\n\n const contentTypes = await requireXml(\n zip,\n \"[Content_Types].xml\",\n readBudget,\n limits.maxMetadataXmlBytes,\n );\n zip.file(\n \"[Content_Types].xml\",\n removeElements(contentTypes, (element) => {\n if (\n element.localName !== \"Override\" ||\n ![\"\", \"http://schemas.openxmlformats.org/package/2006/content-types\"].includes(\n element.namespaceUri,\n )\n ) {\n return false;\n }\n const partName = getNamespacedAttribute(element, \"PartName\", [\"\"])?.replace(/^\\//, \"\");\n return partName !== undefined && deletedParts.has(partName);\n }),\n );\n}\n\nasync function buildRelationshipGraph(\n zip: JSZip,\n readBudget: ReadBudget,\n limits: Readonly<ExcelSanitizationLimits>,\n): Promise<RelationshipGraph> {\n const graph: RelationshipGraph = new Map();\n for (const fileName of Object.keys(zip.files)) {\n if (!fileName.endsWith(\".rels\")) {\n continue;\n }\n const source = sourcePartForRelationshipFile(fileName);\n const file = zip.file(fileName);\n if (source !== undefined && file) {\n graph.set(\n source,\n parseRelationships(\n // Reads intentionally stay sequential because they share a mutable byte budget.\n // oxlint-disable-next-line no-await-in-loop\n await readXmlPart(file, fileName, readBudget, limits.maxRelationshipsXmlBytes),\n source,\n ),\n );\n }\n }\n return graph;\n}\n\nexport function collectReachableParts(\n initial: ReadonlySet<string>,\n graph: ReadonlyMap<string, readonly Relationship[]>,\n): Set<string> {\n const reachable = new Set(initial);\n const pending = [...initial];\n while (pending.length > 0) {\n const source = pending.pop();\n if (source === undefined) {\n continue;\n }\n for (const relationship of graph.get(source) ?? []) {\n if (\n relationship.targetMode?.toLowerCase() !== \"external\" &&\n !reachable.has(relationship.target)\n ) {\n reachable.add(relationship.target);\n pending.push(relationship.target);\n }\n }\n }\n return reachable;\n}\n"]}
@@ -4,4 +4,3 @@ export declare function removeUnusedCellStyles(zip: JSZip, worksheets: ReadonlyM
4
4
  export declare function collectStyleReferences(xml: string, references: Set<number>): void;
5
5
  export declare function remapWorksheetStyles(xml: string, indexMap: ReadonlyMap<number, number>): string;
6
6
  export declare function remapNumericAttribute(tag: string, attributeName: string, indexMap: ReadonlyMap<number, number>): string;
7
- //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1,67 @@
1
+ import { readXmlPart } from "./parts.js";
2
+ import { findCollection, findElements, getAttribute, isSpreadsheetElement, mapOpeningElements, replaceAttribute, replaceCollection, } from "./xml.js";
3
+ export async function removeUnusedCellStyles(zip, worksheets, readBudget, limits) {
4
+ const stylesFile = zip.file("xl/styles.xml");
5
+ if (!stylesFile) {
6
+ return;
7
+ }
8
+ const usedStyles = new Set([0]);
9
+ for (const xml of worksheets.values()) {
10
+ collectStyleReferences(xml, usedStyles);
11
+ }
12
+ const stylesXml = await readXmlPart(stylesFile, "xl/styles.xml", readBudget, limits.maxStylesXmlBytes);
13
+ const cellXfs = findCollection(stylesXml, "cellXfs");
14
+ if (!cellXfs) {
15
+ return;
16
+ }
17
+ const xfs = findElements(stylesXml, "xf")
18
+ .filter((element) => isSpreadsheetElement(element) && element.parent?.start === cellXfs.element.start)
19
+ .map((element) => stylesXml.slice(element.start, element.end));
20
+ // The spread already creates a fresh array, so this sort cannot mutate the source set.
21
+ // oxlint-disable-next-line unicorn/no-array-sort
22
+ const retainedIndexes = [...usedStyles].sort((left, right) => left - right);
23
+ validateStyleIndexes(retainedIndexes, xfs.length);
24
+ const indexMap = new Map(retainedIndexes.map((oldIndex, newIndex) => [oldIndex, newIndex]));
25
+ const retainedStyles = retainedIndexes.map((index) => {
26
+ const style = xfs[index];
27
+ if (style === undefined) {
28
+ throw new Error(`Worksheet references an invalid style index: ${index}`);
29
+ }
30
+ return style;
31
+ });
32
+ zip.file("xl/styles.xml", replaceCollection(stylesXml, "cellXfs", retainedStyles.join(""), retainedIndexes.length));
33
+ for (const [worksheetPath, xml] of worksheets) {
34
+ zip.file(worksheetPath, remapWorksheetStyles(xml, indexMap));
35
+ }
36
+ }
37
+ function validateStyleIndexes(indexes, styleCount) {
38
+ if (indexes.some((index) => !Number.isInteger(index) || index < 0 || index >= styleCount)) {
39
+ throw new Error(`Worksheet references an invalid style index (${indexes.join(",")} of ${styleCount})`);
40
+ }
41
+ }
42
+ export function collectStyleReferences(xml, references) {
43
+ mapOpeningElements(xml, (element, tag) => {
44
+ const attribute = element.localName === "col" ? "style" : "s";
45
+ if (isSpreadsheetElement(element) && ["c", "row", "col"].includes(element.localName)) {
46
+ const value = getAttribute(tag, attribute);
47
+ if (value !== undefined) {
48
+ references.add(Number(value));
49
+ }
50
+ }
51
+ return tag;
52
+ });
53
+ }
54
+ export function remapWorksheetStyles(xml, indexMap) {
55
+ return mapOpeningElements(xml, (element, tag) => {
56
+ if (!isSpreadsheetElement(element) || !["c", "row", "col"].includes(element.localName)) {
57
+ return tag;
58
+ }
59
+ return remapNumericAttribute(tag, element.localName === "col" ? "style" : "s", indexMap);
60
+ });
61
+ }
62
+ export function remapNumericAttribute(tag, attributeName, indexMap) {
63
+ const value = getAttribute(tag, attributeName);
64
+ const remapped = value === undefined ? undefined : indexMap.get(Number(value));
65
+ return remapped === undefined ? tag : replaceAttribute(tag, attributeName, String(remapped));
66
+ }
67
+ //# sourceMappingURL=styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.js","sourceRoot":"","sources":["../../src/ooxml/styles.ts"],"names":[],"mappings":"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","sourcesContent":["import type JSZip from \"jszip\";\n\nimport type { ExcelSanitizationLimits, ReadBudget } from \"../types.js\";\nimport { readXmlPart } from \"./parts.js\";\nimport {\n findCollection,\n findElements,\n getAttribute,\n isSpreadsheetElement,\n mapOpeningElements,\n replaceAttribute,\n replaceCollection,\n} from \"./xml.js\";\n\nexport async function removeUnusedCellStyles(\n zip: JSZip,\n worksheets: ReadonlyMap<string, string>,\n readBudget: ReadBudget,\n limits: Readonly<ExcelSanitizationLimits>,\n): Promise<void> {\n const stylesFile = zip.file(\"xl/styles.xml\");\n if (!stylesFile) {\n return;\n }\n\n const usedStyles = new Set([0]);\n for (const xml of worksheets.values()) {\n collectStyleReferences(xml, usedStyles);\n }\n\n const stylesXml = await readXmlPart(\n stylesFile,\n \"xl/styles.xml\",\n readBudget,\n limits.maxStylesXmlBytes,\n );\n const cellXfs = findCollection(stylesXml, \"cellXfs\");\n if (!cellXfs) {\n return;\n }\n\n const xfs = findElements(stylesXml, \"xf\")\n .filter(\n (element) => isSpreadsheetElement(element) && element.parent?.start === cellXfs.element.start,\n )\n .map((element) => stylesXml.slice(element.start, element.end));\n // The spread already creates a fresh array, so this sort cannot mutate the source set.\n // oxlint-disable-next-line unicorn/no-array-sort\n const retainedIndexes = [...usedStyles].sort((left, right) => left - right);\n validateStyleIndexes(retainedIndexes, xfs.length);\n const indexMap = new Map(retainedIndexes.map((oldIndex, newIndex) => [oldIndex, newIndex]));\n const retainedStyles = retainedIndexes.map((index) => {\n const style = xfs[index];\n if (style === undefined) {\n throw new Error(`Worksheet references an invalid style index: ${index}`);\n }\n return style;\n });\n\n zip.file(\n \"xl/styles.xml\",\n replaceCollection(stylesXml, \"cellXfs\", retainedStyles.join(\"\"), retainedIndexes.length),\n );\n for (const [worksheetPath, xml] of worksheets) {\n zip.file(worksheetPath, remapWorksheetStyles(xml, indexMap));\n }\n}\n\nfunction validateStyleIndexes(indexes: readonly number[], styleCount: number): void {\n if (indexes.some((index) => !Number.isInteger(index) || index < 0 || index >= styleCount)) {\n throw new Error(\n `Worksheet references an invalid style index (${indexes.join(\",\")} of ${styleCount})`,\n );\n }\n}\n\nexport function collectStyleReferences(xml: string, references: Set<number>): void {\n mapOpeningElements(xml, (element, tag) => {\n const attribute = element.localName === \"col\" ? \"style\" : \"s\";\n if (isSpreadsheetElement(element) && [\"c\", \"row\", \"col\"].includes(element.localName)) {\n const value = getAttribute(tag, attribute);\n if (value !== undefined) {\n references.add(Number(value));\n }\n }\n return tag;\n });\n}\n\nexport function remapWorksheetStyles(xml: string, indexMap: ReadonlyMap<number, number>): string {\n return mapOpeningElements(xml, (element, tag) => {\n if (!isSpreadsheetElement(element) || ![\"c\", \"row\", \"col\"].includes(element.localName)) {\n return tag;\n }\n return remapNumericAttribute(tag, element.localName === \"col\" ? \"style\" : \"s\", indexMap);\n });\n}\n\nexport function remapNumericAttribute(\n tag: string,\n attributeName: string,\n indexMap: ReadonlyMap<number, number>,\n): string {\n const value = getAttribute(tag, attributeName);\n const remapped = value === undefined ? undefined : indexMap.get(Number(value));\n return remapped === undefined ? tag : replaceAttribute(tag, attributeName, String(remapped));\n}\n"]}
@@ -1,3 +1,2 @@
1
1
  import type { NormalizedSanitizeOptions } from "../types.js";
2
2
  export declare function sanitizeOoxmlWorkbook(buffer: Buffer, options: NormalizedSanitizeOptions): Promise<Buffer>;
3
- //# sourceMappingURL=workbook.d.ts.map
@@ -0,0 +1,119 @@
1
+ import JSZip from "jszip";
2
+ import { createError } from "../errors.js";
3
+ import { hasZipSignature, validateZipCentralDirectory } from "../zip/central-directory.js";
4
+ import { removeThreadedCommentExtensions, removeWorksheetDrawingMarkup } from "./cleanup.js";
5
+ import { DRAWING_RELATIONSHIP_SUFFIXES, PERSON_RELATIONSHIP_SUFFIXES } from "./constants.js";
6
+ import { readXmlPart, requireXml, validateUncompressedSize } from "./parts.js";
7
+ import { parseRelationships, relationshipFileForPart, removeRelationships, removeUnreachableParts, } from "./relationships.js";
8
+ import { removeUnusedCellStyles } from "./styles.js";
9
+ import { findElements, getNamespacedAttribute, isSpreadsheetElement } from "./xml.js";
10
+ const WORKBOOK_PATH = "xl/workbook.xml";
11
+ const WORKBOOK_RELATIONSHIPS_PATH = "xl/_rels/workbook.xml.rels";
12
+ const CONTENT_TYPES_PATH = "[Content_Types].xml";
13
+ export async function sanitizeOoxmlWorkbook(buffer, options) {
14
+ if (!hasZipSignature(buffer)) {
15
+ throw new Error("Invalid OOXML ZIP container");
16
+ }
17
+ validateZipCentralDirectory(buffer, options.limits);
18
+ const context = await loadWorkbookContext(buffer, options);
19
+ const worksheetPaths = findWorksheetPaths(context.workbookXml, context.workbookRelationships, options.requireSingleWorksheet);
20
+ const detachedRoots = cleanWorkbookRelationships(context);
21
+ const worksheetXmlByPath = await cleanWorksheets(context, worksheetPaths, detachedRoots, options);
22
+ await removeUnreachableParts(context.zip, detachedRoots, context.readBudget, options.limits);
23
+ await removeUnusedCellStyles(context.zip, worksheetXmlByPath, context.readBudget, options.limits);
24
+ await validateUncompressedSize(context.zip, options.limits.maxUncompressedBytes);
25
+ return context.zip.generateAsync({
26
+ type: "nodebuffer",
27
+ compression: "DEFLATE",
28
+ compressionOptions: { level: 6 },
29
+ });
30
+ }
31
+ async function loadWorkbookContext(buffer, options) {
32
+ const readBudget = {
33
+ consumedBytes: 0,
34
+ limitBytes: options.limits.maxXmlBytes,
35
+ };
36
+ // CRC verification eagerly inflates every archive entry. Required XML parts
37
+ // are instead inflated lazily below and constrained by per-part budgets.
38
+ const zip = await JSZip.loadAsync(buffer);
39
+ const workbookXml = await requireXml(zip, WORKBOOK_PATH, readBudget, options.limits.maxMetadataXmlBytes);
40
+ const workbookRelationshipsXml = await requireXml(zip, WORKBOOK_RELATIONSHIPS_PATH, readBudget, options.limits.maxRelationshipsXmlBytes);
41
+ await requireXml(zip, CONTENT_TYPES_PATH, readBudget, options.limits.maxMetadataXmlBytes);
42
+ // readXmlPart normalizes encoding declarations; persist the decoded metadata as UTF-8.
43
+ zip.file(WORKBOOK_PATH, workbookXml);
44
+ return {
45
+ zip,
46
+ readBudget,
47
+ workbookXml,
48
+ workbookRelationshipsXml,
49
+ workbookRelationships: parseRelationships(workbookRelationshipsXml, WORKBOOK_PATH),
50
+ };
51
+ }
52
+ function findWorksheetPaths(workbookXml, workbookRelationships, requireSingleWorksheet) {
53
+ const sheets = findElements(workbookXml, "sheet").filter((element) => isSpreadsheetElement(element) &&
54
+ element.parent !== undefined &&
55
+ isSpreadsheetElement(element.parent, "sheets"));
56
+ enforceWorksheetCount(sheets.length, requireSingleWorksheet);
57
+ if (sheets.length === 0) {
58
+ throw new Error("Workbook contains no sheets");
59
+ }
60
+ const relationshipsById = new Map(workbookRelationships.map((relationship) => [relationship.id, relationship]));
61
+ const worksheetPaths = sheets.flatMap((sheet) => {
62
+ const id = getNamespacedAttribute(sheet, "id", [
63
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
64
+ "http://purl.oclc.org/ooxml/officeDocument/relationships",
65
+ ]);
66
+ const relationship = id ? relationshipsById.get(id) : undefined;
67
+ if (!relationship || relationship.targetMode?.toLowerCase() === "external") {
68
+ throw new Error(`Missing or invalid workbook sheet relationship: ${id ?? "no relationship ID"}`);
69
+ }
70
+ if (!["/worksheet", "/chartsheet", "/dialogsheet", "/macrosheet", "/intlMacrosheet"].some((suffix) => relationship.type.endsWith(suffix))) {
71
+ throw new Error(`Invalid workbook sheet relationship type: ${relationship.type}`);
72
+ }
73
+ return relationship.type.endsWith("/worksheet") ? [relationship.target] : [];
74
+ });
75
+ if (worksheetPaths.length === 0) {
76
+ throw new Error("Workbook contains no worksheets");
77
+ }
78
+ return worksheetPaths;
79
+ }
80
+ function cleanWorkbookRelationships(context) {
81
+ const cleaned = removeRelationships(context.workbookRelationshipsXml, WORKBOOK_PATH, PERSON_RELATIONSHIP_SUFFIXES);
82
+ context.zip.file(WORKBOOK_RELATIONSHIPS_PATH, cleaned.xml);
83
+ return cleaned.targets;
84
+ }
85
+ async function cleanWorksheets(context, worksheetPaths, detachedRoots, options) {
86
+ const worksheetXmlByPath = new Map();
87
+ for (const worksheetPath of worksheetPaths) {
88
+ // Reads intentionally stay sequential because they share a mutable byte budget.
89
+ // oxlint-disable-next-line no-await-in-loop
90
+ const worksheetXml = await requireXml(context.zip, worksheetPath, context.readBudget, options.limits.maxWorksheetXmlBytes);
91
+ const cleanedWorksheetXml = removeThreadedCommentExtensions(removeWorksheetDrawingMarkup(worksheetXml));
92
+ worksheetXmlByPath.set(worksheetPath, cleanedWorksheetXml);
93
+ context.zip.file(worksheetPath, cleanedWorksheetXml);
94
+ // Relationship reads consume the same shared byte budget as worksheet reads.
95
+ // oxlint-disable-next-line no-await-in-loop
96
+ await cleanWorksheetRelationships(context, worksheetPath, detachedRoots, options);
97
+ }
98
+ return worksheetXmlByPath;
99
+ }
100
+ async function cleanWorksheetRelationships(context, worksheetPath, detachedRoots, options) {
101
+ const relationshipsPath = relationshipFileForPart(worksheetPath);
102
+ const relationshipsFile = context.zip.file(relationshipsPath);
103
+ if (!relationshipsFile) {
104
+ return;
105
+ }
106
+ const cleaned = removeRelationships(await readXmlPart(relationshipsFile, relationshipsPath, context.readBudget, options.limits.maxRelationshipsXmlBytes), worksheetPath, DRAWING_RELATIONSHIP_SUFFIXES);
107
+ context.zip.file(relationshipsPath, cleaned.xml);
108
+ for (const target of cleaned.targets) {
109
+ detachedRoots.add(target);
110
+ }
111
+ }
112
+ function enforceWorksheetCount(count, required) {
113
+ if (required && count !== 1) {
114
+ throw createError("ERR_WORKSHEET_COUNT", {
115
+ details: { expected: 1, actual: count },
116
+ });
117
+ }
118
+ }
119
+ //# sourceMappingURL=workbook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workbook.js","sourceRoot":"","sources":["../../src/ooxml/workbook.ts"],"names":[],"mappings":"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","sourcesContent":["import JSZip from \"jszip\";\n\nimport { createError } from \"../errors.js\";\nimport type { NormalizedSanitizeOptions, ReadBudget } from \"../types.js\";\nimport { hasZipSignature, validateZipCentralDirectory } from \"../zip/central-directory.js\";\nimport { removeThreadedCommentExtensions, removeWorksheetDrawingMarkup } from \"./cleanup.js\";\nimport { DRAWING_RELATIONSHIP_SUFFIXES, PERSON_RELATIONSHIP_SUFFIXES } from \"./constants.js\";\nimport { readXmlPart, requireXml, validateUncompressedSize } from \"./parts.js\";\nimport {\n parseRelationships,\n relationshipFileForPart,\n removeRelationships,\n removeUnreachableParts,\n type Relationship,\n} from \"./relationships.js\";\nimport { removeUnusedCellStyles } from \"./styles.js\";\nimport { findElements, getNamespacedAttribute, isSpreadsheetElement } from \"./xml.js\";\n\nconst WORKBOOK_PATH = \"xl/workbook.xml\";\nconst WORKBOOK_RELATIONSHIPS_PATH = \"xl/_rels/workbook.xml.rels\";\nconst CONTENT_TYPES_PATH = \"[Content_Types].xml\";\n\ninterface WorkbookContext {\n zip: JSZip;\n readBudget: ReadBudget;\n workbookXml: string;\n workbookRelationshipsXml: string;\n workbookRelationships: Relationship[];\n}\n\nexport async function sanitizeOoxmlWorkbook(\n buffer: Buffer,\n options: NormalizedSanitizeOptions,\n): Promise<Buffer> {\n if (!hasZipSignature(buffer)) {\n throw new Error(\"Invalid OOXML ZIP container\");\n }\n validateZipCentralDirectory(buffer, options.limits);\n\n const context = await loadWorkbookContext(buffer, options);\n const worksheetPaths = findWorksheetPaths(\n context.workbookXml,\n context.workbookRelationships,\n options.requireSingleWorksheet,\n );\n const detachedRoots = cleanWorkbookRelationships(context);\n const worksheetXmlByPath = await cleanWorksheets(context, worksheetPaths, detachedRoots, options);\n\n await removeUnreachableParts(context.zip, detachedRoots, context.readBudget, options.limits);\n await removeUnusedCellStyles(context.zip, worksheetXmlByPath, context.readBudget, options.limits);\n await validateUncompressedSize(context.zip, options.limits.maxUncompressedBytes);\n return context.zip.generateAsync({\n type: \"nodebuffer\",\n compression: \"DEFLATE\",\n compressionOptions: { level: 6 },\n });\n}\n\nasync function loadWorkbookContext(\n buffer: Buffer,\n options: NormalizedSanitizeOptions,\n): Promise<WorkbookContext> {\n const readBudget: ReadBudget = {\n consumedBytes: 0,\n limitBytes: options.limits.maxXmlBytes,\n };\n\n // CRC verification eagerly inflates every archive entry. Required XML parts\n // are instead inflated lazily below and constrained by per-part budgets.\n const zip = await JSZip.loadAsync(buffer);\n const workbookXml = await requireXml(\n zip,\n WORKBOOK_PATH,\n readBudget,\n options.limits.maxMetadataXmlBytes,\n );\n const workbookRelationshipsXml = await requireXml(\n zip,\n WORKBOOK_RELATIONSHIPS_PATH,\n readBudget,\n options.limits.maxRelationshipsXmlBytes,\n );\n await requireXml(zip, CONTENT_TYPES_PATH, readBudget, options.limits.maxMetadataXmlBytes);\n // readXmlPart normalizes encoding declarations; persist the decoded metadata as UTF-8.\n zip.file(WORKBOOK_PATH, workbookXml);\n\n return {\n zip,\n readBudget,\n workbookXml,\n workbookRelationshipsXml,\n workbookRelationships: parseRelationships(workbookRelationshipsXml, WORKBOOK_PATH),\n };\n}\n\nfunction findWorksheetPaths(\n workbookXml: string,\n workbookRelationships: readonly Relationship[],\n requireSingleWorksheet: boolean,\n): string[] {\n const sheets = findElements(workbookXml, \"sheet\").filter(\n (element) =>\n isSpreadsheetElement(element) &&\n element.parent !== undefined &&\n isSpreadsheetElement(element.parent, \"sheets\"),\n );\n enforceWorksheetCount(sheets.length, requireSingleWorksheet);\n if (sheets.length === 0) {\n throw new Error(\"Workbook contains no sheets\");\n }\n\n const relationshipsById = new Map(\n workbookRelationships.map((relationship) => [relationship.id, relationship]),\n );\n const worksheetPaths = sheets.flatMap((sheet) => {\n const id = getNamespacedAttribute(sheet, \"id\", [\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships\",\n \"http://purl.oclc.org/ooxml/officeDocument/relationships\",\n ]);\n const relationship = id ? relationshipsById.get(id) : undefined;\n if (!relationship || relationship.targetMode?.toLowerCase() === \"external\") {\n throw new Error(\n `Missing or invalid workbook sheet relationship: ${id ?? \"no relationship ID\"}`,\n );\n }\n if (\n ![\"/worksheet\", \"/chartsheet\", \"/dialogsheet\", \"/macrosheet\", \"/intlMacrosheet\"].some(\n (suffix) => relationship.type.endsWith(suffix),\n )\n ) {\n throw new Error(`Invalid workbook sheet relationship type: ${relationship.type}`);\n }\n return relationship.type.endsWith(\"/worksheet\") ? [relationship.target] : [];\n });\n if (worksheetPaths.length === 0) {\n throw new Error(\"Workbook contains no worksheets\");\n }\n return worksheetPaths;\n}\n\nfunction cleanWorkbookRelationships(context: WorkbookContext): Set<string> {\n const cleaned = removeRelationships(\n context.workbookRelationshipsXml,\n WORKBOOK_PATH,\n PERSON_RELATIONSHIP_SUFFIXES,\n );\n context.zip.file(WORKBOOK_RELATIONSHIPS_PATH, cleaned.xml);\n return cleaned.targets;\n}\n\nasync function cleanWorksheets(\n context: WorkbookContext,\n worksheetPaths: readonly string[],\n detachedRoots: Set<string>,\n options: NormalizedSanitizeOptions,\n): Promise<Map<string, string>> {\n const worksheetXmlByPath = new Map<string, string>();\n for (const worksheetPath of worksheetPaths) {\n // Reads intentionally stay sequential because they share a mutable byte budget.\n // oxlint-disable-next-line no-await-in-loop\n const worksheetXml = await requireXml(\n context.zip,\n worksheetPath,\n context.readBudget,\n options.limits.maxWorksheetXmlBytes,\n );\n const cleanedWorksheetXml = removeThreadedCommentExtensions(\n removeWorksheetDrawingMarkup(worksheetXml),\n );\n worksheetXmlByPath.set(worksheetPath, cleanedWorksheetXml);\n context.zip.file(worksheetPath, cleanedWorksheetXml);\n\n // Relationship reads consume the same shared byte budget as worksheet reads.\n // oxlint-disable-next-line no-await-in-loop\n await cleanWorksheetRelationships(context, worksheetPath, detachedRoots, options);\n }\n return worksheetXmlByPath;\n}\n\nasync function cleanWorksheetRelationships(\n context: WorkbookContext,\n worksheetPath: string,\n detachedRoots: Set<string>,\n options: NormalizedSanitizeOptions,\n): Promise<void> {\n const relationshipsPath = relationshipFileForPart(worksheetPath);\n const relationshipsFile = context.zip.file(relationshipsPath);\n if (!relationshipsFile) {\n return;\n }\n\n const cleaned = removeRelationships(\n await readXmlPart(\n relationshipsFile,\n relationshipsPath,\n context.readBudget,\n options.limits.maxRelationshipsXmlBytes,\n ),\n worksheetPath,\n DRAWING_RELATIONSHIP_SUFFIXES,\n );\n context.zip.file(relationshipsPath, cleaned.xml);\n for (const target of cleaned.targets) {\n detachedRoots.add(target);\n }\n}\n\nfunction enforceWorksheetCount(count: number, required: boolean): void {\n if (required && count !== 1) {\n throw createError(\"ERR_WORKSHEET_COUNT\", {\n details: { expected: 1, actual: count },\n });\n }\n}\n"]}
@@ -1,12 +1,36 @@
1
+ interface XmlAttribute {
2
+ name: string;
3
+ localName: string;
4
+ namespaceUri: string;
5
+ value: string;
6
+ valueStart: number;
7
+ valueEnd: number;
8
+ }
9
+ export interface XmlElement {
10
+ name: string;
11
+ localName: string;
12
+ namespaceUri: string;
13
+ attributes: XmlAttribute[];
14
+ start: number;
15
+ openingEnd: number;
16
+ closingStart: number;
17
+ end: number;
18
+ parent: XmlElement | undefined;
19
+ }
1
20
  interface XmlCollection {
2
21
  body: string;
3
22
  full: string;
4
23
  opening: string;
24
+ element: XmlElement;
5
25
  }
6
26
  export declare function getAttribute(xml: string, name: string): string | undefined;
27
+ export declare function replaceAttribute(xml: string, name: string, value: string): string;
28
+ export declare function getNamespacedAttribute(element: XmlElement, name: string, namespaceUris: readonly string[]): string | undefined;
7
29
  export declare function decodeXml(value: string): string;
30
+ export declare function isSpreadsheetElement(element: XmlElement, name?: string): boolean;
31
+ export declare function findElements(xml: string, name: string): XmlElement[];
32
+ export declare function mapOpeningElements(xml: string, transform: (element: XmlElement, opening: string) => string): string;
33
+ export declare function removeElements(xml: string, predicate: (element: XmlElement) => boolean): string;
8
34
  export declare function findCollection(xml: string, name: string): XmlCollection | undefined;
9
35
  export declare function replaceCollection(xml: string, name: string, body: string, count: number): string;
10
- export declare function matchElements(xml: string, name: string): string[];
11
36
  export {};
12
- //# sourceMappingURL=xml.d.ts.map