@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.
Files changed (203) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +104 -0
  3. package/dist/bounded-zip.d.ts +28 -0
  4. package/dist/chunk-4BSENMAE.js +39 -0
  5. package/dist/chunk-4BSENMAE.js.map +1 -0
  6. package/dist/chunk-4FRFNNBM.js +276 -0
  7. package/dist/chunk-4FRFNNBM.js.map +1 -0
  8. package/dist/chunk-5XIA6KHZ.js +49 -0
  9. package/dist/chunk-5XIA6KHZ.js.map +1 -0
  10. package/dist/chunk-66CT2ZRP.js +2229 -0
  11. package/dist/chunk-66CT2ZRP.js.map +1 -0
  12. package/dist/chunk-72SYDU4O.js +319 -0
  13. package/dist/chunk-72SYDU4O.js.map +1 -0
  14. package/dist/chunk-B2ZJRZDA.js +279 -0
  15. package/dist/chunk-B2ZJRZDA.js.map +1 -0
  16. package/dist/chunk-DCTLTNPW.js +2458 -0
  17. package/dist/chunk-DCTLTNPW.js.map +1 -0
  18. package/dist/chunk-FGZM4FQ6.js +473 -0
  19. package/dist/chunk-FGZM4FQ6.js.map +1 -0
  20. package/dist/chunk-HTJBLBDE.js +591 -0
  21. package/dist/chunk-HTJBLBDE.js.map +1 -0
  22. package/dist/chunk-IYFXZQ34.js +5306 -0
  23. package/dist/chunk-IYFXZQ34.js.map +1 -0
  24. package/dist/chunk-J6N7IR44.js +172 -0
  25. package/dist/chunk-J6N7IR44.js.map +1 -0
  26. package/dist/chunk-N2IHNWBS.js +1002 -0
  27. package/dist/chunk-N2IHNWBS.js.map +1 -0
  28. package/dist/chunk-NAG44K67.js +549 -0
  29. package/dist/chunk-NAG44K67.js.map +1 -0
  30. package/dist/chunk-O24KTEGP.js +33 -0
  31. package/dist/chunk-O24KTEGP.js.map +1 -0
  32. package/dist/chunk-PI2AZVGK.js +2074 -0
  33. package/dist/chunk-PI2AZVGK.js.map +1 -0
  34. package/dist/chunk-Q2FFHEY7.js +40 -0
  35. package/dist/chunk-Q2FFHEY7.js.map +1 -0
  36. package/dist/chunk-RBGQADXA.js +57 -0
  37. package/dist/chunk-RBGQADXA.js.map +1 -0
  38. package/dist/chunk-U5G2BKM2.js +613 -0
  39. package/dist/chunk-U5G2BKM2.js.map +1 -0
  40. package/dist/chunk-VOJHMSM3.js +39 -0
  41. package/dist/chunk-VOJHMSM3.js.map +1 -0
  42. package/dist/chunk-VUNV25KB.js +16 -0
  43. package/dist/chunk-VUNV25KB.js.map +1 -0
  44. package/dist/chunk-XIFUNVBO.js +101 -0
  45. package/dist/chunk-XIFUNVBO.js.map +1 -0
  46. package/dist/chunk-ZCJYRJGB.js +489 -0
  47. package/dist/chunk-ZCJYRJGB.js.map +1 -0
  48. package/dist/chunk-ZZXPEE4S.js +2260 -0
  49. package/dist/chunk-ZZXPEE4S.js.map +1 -0
  50. package/dist/document-docx-api.d.ts +49 -0
  51. package/dist/document-docx-codec.d.ts +9 -0
  52. package/dist/document-docx-codec.js +4765 -0
  53. package/dist/document-docx-codec.js.map +1 -0
  54. package/dist/document-docx-import.d.ts +261 -0
  55. package/dist/document-docx-packer.d.ts +18 -0
  56. package/dist/document-docx-state.d.ts +12 -0
  57. package/dist/document-render.d.ts +3 -0
  58. package/dist/document-render.js +492 -0
  59. package/dist/document-render.js.map +1 -0
  60. package/dist/document.d.ts +432 -0
  61. package/dist/document.js +37 -0
  62. package/dist/document.js.map +1 -0
  63. package/dist/errors.d.ts +23 -0
  64. package/dist/file-blob.d.ts +11 -0
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.js +186 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/inspection.d.ts +12 -0
  69. package/dist/kernel.d.ts +101 -0
  70. package/dist/lib-GQLPACFK.js +9746 -0
  71. package/dist/lib-GQLPACFK.js.map +1 -0
  72. package/dist/materializer-cli-entry.d.ts +2 -0
  73. package/dist/materializer-cli-entry.js +29 -0
  74. package/dist/materializer-cli-entry.js.map +1 -0
  75. package/dist/materializer-cli.d.ts +24 -0
  76. package/dist/materializer-cli.js +24 -0
  77. package/dist/materializer-cli.js.map +1 -0
  78. package/dist/native-raster.d.ts +10 -0
  79. package/dist/native.d.ts +129 -0
  80. package/dist/native.js +31 -0
  81. package/dist/native.js.map +1 -0
  82. package/dist/office-import.d.ts +48 -0
  83. package/dist/office-import.js +154 -0
  84. package/dist/office-import.js.map +1 -0
  85. package/dist/presentation-pptx-api.d.ts +60 -0
  86. package/dist/presentation-pptx-import.d.ts +5 -0
  87. package/dist/presentation-pptx-state-digest-765CZZED.js +10 -0
  88. package/dist/presentation-pptx-state-digest-765CZZED.js.map +1 -0
  89. package/dist/presentation-pptx-state-digest.d.ts +3 -0
  90. package/dist/presentation-pptx-state.d.ts +4 -0
  91. package/dist/presentation-pptx-xml.d.ts +29 -0
  92. package/dist/presentation-pptx.d.ts +5 -0
  93. package/dist/presentation-pptx.js +2365 -0
  94. package/dist/presentation-pptx.js.map +1 -0
  95. package/dist/presentation-render.d.ts +29 -0
  96. package/dist/presentation-render.js +375 -0
  97. package/dist/presentation-render.js.map +1 -0
  98. package/dist/presentation.d.ts +565 -0
  99. package/dist/presentation.js +71 -0
  100. package/dist/presentation.js.map +1 -0
  101. package/dist/production-composite.d.ts +68 -0
  102. package/dist/production-document.d.ts +2 -0
  103. package/dist/production-document.js +176 -0
  104. package/dist/production-document.js.map +1 -0
  105. package/dist/production-native-codecs.d.ts +94 -0
  106. package/dist/production-presentation.d.ts +2 -0
  107. package/dist/production-presentation.js +176 -0
  108. package/dist/production-presentation.js.map +1 -0
  109. package/dist/production-sha256.d.ts +2 -0
  110. package/dist/production-spreadsheet-native.d.ts +12 -0
  111. package/dist/production-spreadsheet.d.ts +2 -0
  112. package/dist/production-spreadsheet.js +176 -0
  113. package/dist/production-spreadsheet.js.map +1 -0
  114. package/dist/production.d.ts +96 -0
  115. package/dist/raster-image.d.ts +34 -0
  116. package/dist/reference.d.ts +17 -0
  117. package/dist/reference.js +574 -0
  118. package/dist/reference.js.map +1 -0
  119. package/dist/runtime-cli-entry.d.ts +2 -0
  120. package/dist/runtime-cli-entry.js +21 -0
  121. package/dist/runtime-cli-entry.js.map +1 -0
  122. package/dist/runtime-cli.d.ts +76 -0
  123. package/dist/runtime-cli.js +26 -0
  124. package/dist/runtime-cli.js.map +1 -0
  125. package/dist/runtime-development.d.ts +73 -0
  126. package/dist/runtime-development.js +30 -0
  127. package/dist/runtime-development.js.map +1 -0
  128. package/dist/runtime-receipt.d.ts +19 -0
  129. package/dist/runtime.d.ts +355 -0
  130. package/dist/runtime.js +42 -0
  131. package/dist/runtime.js.map +1 -0
  132. package/dist/snapshot.d.ts +26 -0
  133. package/dist/snapshot.js +61 -0
  134. package/dist/snapshot.js.map +1 -0
  135. package/dist/spreadsheet-address.d.ts +18 -0
  136. package/dist/spreadsheet-file.d.ts +17 -0
  137. package/dist/spreadsheet-formula.d.ts +67 -0
  138. package/dist/spreadsheet-image.d.ts +17 -0
  139. package/dist/spreadsheet-render.d.ts +5 -0
  140. package/dist/spreadsheet-render.js +752 -0
  141. package/dist/spreadsheet-render.js.map +1 -0
  142. package/dist/spreadsheet-types.d.ts +155 -0
  143. package/dist/spreadsheet-xlsx-api.d.ts +88 -0
  144. package/dist/spreadsheet-xlsx-codec.d.ts +16 -0
  145. package/dist/spreadsheet-xlsx-codec.js +16 -0
  146. package/dist/spreadsheet-xlsx-codec.js.map +1 -0
  147. package/dist/spreadsheet-xlsx-state.d.ts +16 -0
  148. package/dist/spreadsheet.d.ts +724 -0
  149. package/dist/spreadsheet.js +67 -0
  150. package/dist/spreadsheet.js.map +1 -0
  151. package/package.json +129 -0
  152. package/src/bounded-zip.ts +450 -0
  153. package/src/document-docx-api.ts +64 -0
  154. package/src/document-docx-codec.ts +2253 -0
  155. package/src/document-docx-import.ts +3814 -0
  156. package/src/document-docx-packer.ts +72 -0
  157. package/src/document-docx-state.ts +59 -0
  158. package/src/document-render.ts +615 -0
  159. package/src/document.ts +2800 -0
  160. package/src/errors.ts +45 -0
  161. package/src/file-blob.ts +43 -0
  162. package/src/index.ts +4 -0
  163. package/src/inspection.ts +50 -0
  164. package/src/kernel.ts +525 -0
  165. package/src/materializer-cli-entry.ts +11 -0
  166. package/src/materializer-cli.ts +743 -0
  167. package/src/native-raster.ts +141 -0
  168. package/src/native.ts +390 -0
  169. package/src/office-import.ts +243 -0
  170. package/src/presentation-pptx-api.ts +107 -0
  171. package/src/presentation-pptx-import.ts +2065 -0
  172. package/src/presentation-pptx-state-digest.ts +46 -0
  173. package/src/presentation-pptx-state.ts +18 -0
  174. package/src/presentation-pptx-xml.ts +473 -0
  175. package/src/presentation-pptx.ts +447 -0
  176. package/src/presentation-render.ts +532 -0
  177. package/src/presentation.ts +2599 -0
  178. package/src/production-composite.ts +805 -0
  179. package/src/production-document.ts +2 -0
  180. package/src/production-native-codecs.ts +1141 -0
  181. package/src/production-presentation.ts +2 -0
  182. package/src/production-sha256.ts +150 -0
  183. package/src/production-spreadsheet-native.ts +686 -0
  184. package/src/production-spreadsheet.ts +2 -0
  185. package/src/production.ts +1022 -0
  186. package/src/raster-image.ts +586 -0
  187. package/src/reference.ts +17 -0
  188. package/src/runtime-cli-entry.ts +13 -0
  189. package/src/runtime-cli.ts +670 -0
  190. package/src/runtime-development.ts +689 -0
  191. package/src/runtime-receipt.ts +169 -0
  192. package/src/runtime.ts +1463 -0
  193. package/src/snapshot.ts +88 -0
  194. package/src/spreadsheet-address.ts +90 -0
  195. package/src/spreadsheet-file.ts +71 -0
  196. package/src/spreadsheet-formula.ts +1212 -0
  197. package/src/spreadsheet-image.ts +242 -0
  198. package/src/spreadsheet-render.ts +985 -0
  199. package/src/spreadsheet-types.ts +171 -0
  200. package/src/spreadsheet-xlsx-api.ts +123 -0
  201. package/src/spreadsheet-xlsx-codec.ts +3063 -0
  202. package/src/spreadsheet-xlsx-state.ts +191 -0
  203. package/src/spreadsheet.ts +5195 -0
@@ -0,0 +1,46 @@
1
+ import type { Presentation, PresentationElement } from "./presentation";
2
+
3
+ export async function presentationModelDigest(presentation: Presentation): Promise<string> {
4
+ const imageDigests: Array<{ id: string; digest: string }> = [];
5
+ const visit = async (element: PresentationElement): Promise<void> => {
6
+ if ("sourceForSvg" in element) {
7
+ imageDigests.push({
8
+ id: element.id,
9
+ digest: await sha256Hex(new TextEncoder().encode(element.sourceForSvg() ?? "")),
10
+ });
11
+ } else if ("children" in element) {
12
+ for (const child of element.children) await visit(child);
13
+ }
14
+ };
15
+ for (const slide of presentation.slides.items) {
16
+ for (const element of slide.elements) await visit(element);
17
+ }
18
+ const normalized = await normalizeDigestValue({
19
+ layout: presentation.layoutSnapshot(),
20
+ imageDigests,
21
+ });
22
+ return await sha256Hex(new TextEncoder().encode(JSON.stringify(normalized)));
23
+ }
24
+
25
+ export async function sha256Hex(bytes: Uint8Array): Promise<string> {
26
+ const digest = new Uint8Array(
27
+ await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes).buffer),
28
+ );
29
+ return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
30
+ }
31
+
32
+ async function normalizeDigestValue(value: unknown): Promise<unknown> {
33
+ if (value instanceof Uint8Array) return { $bytes: await sha256Hex(value) };
34
+ if (value instanceof ArrayBuffer) return { $bytes: await sha256Hex(new Uint8Array(value)) };
35
+ if (Array.isArray(value)) return await Promise.all(value.map(normalizeDigestValue));
36
+ if (value && typeof value === "object") {
37
+ const output: Record<string, unknown> = {};
38
+ for (const key of Object.keys(value).sort()) {
39
+ const child = (value as Record<string, unknown>)[key];
40
+ if (child !== undefined) output[key] = await normalizeDigestValue(child);
41
+ }
42
+ return output;
43
+ }
44
+ if (typeof value === "number" && Object.is(value, -0)) return 0;
45
+ return value;
46
+ }
@@ -0,0 +1,18 @@
1
+ import type { Presentation } from "./presentation";
2
+ import type { PresentationLossPreservationEnvelope } from "./presentation-pptx-api";
3
+
4
+ const lossState = new WeakMap<Presentation, PresentationLossPreservationEnvelope>();
5
+
6
+ export function presentationLossState(
7
+ presentation: Presentation,
8
+ ): PresentationLossPreservationEnvelope | undefined {
9
+ return lossState.get(presentation);
10
+ }
11
+
12
+ export function setPresentationLossState(
13
+ presentation: Presentation,
14
+ envelope: PresentationLossPreservationEnvelope | undefined,
15
+ ): void {
16
+ if (envelope) lossState.set(presentation, envelope);
17
+ else lossState.delete(presentation);
18
+ }
@@ -0,0 +1,473 @@
1
+ import {
2
+ PresentationSecurityError,
3
+ type PresentationPptxImportLimits,
4
+ } from "./presentation-pptx-api";
5
+
6
+ export type PptxXmlNode = PptxXmlElement | { type: "text"; value: string };
7
+
8
+ export type PptxXmlElement = {
9
+ type: "element";
10
+ name: string;
11
+ localName: string;
12
+ attributes: ReadonlyMap<string, string>;
13
+ /** Resolved namespace bindings, populated by parsePptxXmlPart validation. */
14
+ namespaceUri?: string;
15
+ attributeNamespaces?: ReadonlyMap<string, string | undefined>;
16
+ children: PptxXmlNode[];
17
+ };
18
+
19
+ export type PptxXmlBudget = {
20
+ limits: PresentationPptxImportLimits;
21
+ totalBytes: number;
22
+ totalNodes: number;
23
+ };
24
+
25
+ export function parsePptxXmlPart(
26
+ bytes: Uint8Array,
27
+ partName: string,
28
+ budget: PptxXmlBudget,
29
+ ): PptxXmlElement {
30
+ if (bytes.byteLength > budget.limits.xmlBytes) {
31
+ throw pptxXmlLimit("XML part exceeds its byte limit", partName);
32
+ }
33
+ budget.totalBytes += bytes.byteLength;
34
+ if (budget.totalBytes > budget.limits.totalXmlBytes) {
35
+ throw pptxXmlLimit("PPTX exceeds its total XML byte limit", partName);
36
+ }
37
+ let xml: string;
38
+ try {
39
+ if (bytes[0] === 0xff && bytes[1] === 0xfe) {
40
+ xml = new TextDecoder("utf-16le", { fatal: true }).decode(bytes);
41
+ } else if (bytes[0] === 0xfe && bytes[1] === 0xff) {
42
+ xml = new TextDecoder("utf-16be", { fatal: true }).decode(bytes);
43
+ } else {
44
+ xml = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
45
+ }
46
+ } catch {
47
+ throw pptxXmlSecurity("OOXML text is not valid UTF-8 or UTF-16", "unsafe-xml", partName);
48
+ }
49
+ if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(xml)) {
50
+ throw pptxXmlSecurity("DOCTYPE and entity declarations are forbidden", "unsafe-xml", partName);
51
+ }
52
+ const parsed = parseXml(xml, partName, budget);
53
+ validateNamespaceBindings(parsed, partName, new Map());
54
+ return parsed;
55
+ }
56
+
57
+ function parseXml(xml: string, partName: string, budget: PptxXmlBudget): PptxXmlElement {
58
+ const stack: PptxXmlElement[] = [];
59
+ let root: PptxXmlElement | undefined;
60
+ let cursor = 0;
61
+ let sawDeclaration = false;
62
+ const addNode = (node: PptxXmlNode): void => {
63
+ budget.totalNodes += 1;
64
+ if (budget.totalNodes > budget.limits.xmlNodes) {
65
+ throw pptxXmlLimit("PPTX exceeds its XML node limit", partName);
66
+ }
67
+ const parent = stack.at(-1);
68
+ if (parent) parent.children.push(node);
69
+ else if (node.type === "text" && isXmlWhitespace(node.value)) return;
70
+ else if (node.type === "element" && !root) root = node;
71
+ else
72
+ throw pptxXmlSecurity(
73
+ "XML has text outside its root or multiple roots",
74
+ "unsafe-xml",
75
+ partName,
76
+ );
77
+ };
78
+
79
+ while (cursor < xml.length) {
80
+ const opening = xml.indexOf("<", cursor);
81
+ if (opening < 0) {
82
+ if (cursor < xml.length)
83
+ addNode({ type: "text", value: decodeEntities(xml.slice(cursor), partName) });
84
+ cursor = xml.length;
85
+ break;
86
+ }
87
+ if (opening > cursor) {
88
+ addNode({ type: "text", value: decodeEntities(xml.slice(cursor, opening), partName) });
89
+ }
90
+ if (xml.startsWith("<!--", opening)) {
91
+ const end = xml.indexOf("-->", opening + 4);
92
+ const body = end < 0 ? "" : xml.slice(opening + 4, end);
93
+ if (end < 0 || body.includes("--") || body.endsWith("-")) {
94
+ throw pptxXmlSecurity("Malformed XML comment", "unsafe-xml", partName);
95
+ }
96
+ validateXmlCharacters(body, partName);
97
+ cursor = end + 3;
98
+ continue;
99
+ }
100
+ if (xml.startsWith("<?", opening)) {
101
+ const end = xml.indexOf("?>", opening + 2);
102
+ const instruction = end < 0 ? "" : xml.slice(opening + 2, end);
103
+ const declaration =
104
+ /^xml[\x20\t\r\n]+version=(?:"1\.[01]"|'1\.[01]')(?:[\x20\t\r\n]+encoding=(?:"[Uu][Tt][Ff]-(?:8|16)"|'[Uu][Tt][Ff]-(?:8|16)'))?(?:[\x20\t\r\n]+standalone=(?:"(?:yes|no)"|'(?:yes|no)'))?[\x20\t\r\n]*$/.test(
105
+ instruction,
106
+ );
107
+ if (end < 0 || !declaration || opening !== 0 || root || stack.length > 0 || sawDeclaration) {
108
+ throw pptxXmlSecurity(
109
+ "XML processing instructions are unsupported",
110
+ "unsafe-xml",
111
+ partName,
112
+ );
113
+ }
114
+ sawDeclaration = true;
115
+ cursor = end + 2;
116
+ continue;
117
+ }
118
+ if (xml.startsWith("<!", opening)) {
119
+ throw pptxXmlSecurity("XML declarations and CDATA are unsupported", "unsafe-xml", partName);
120
+ }
121
+ const end = findTagEnd(xml, opening + 1, partName);
122
+ const source = xml.slice(opening + 1, end);
123
+ if (source.startsWith("/")) {
124
+ const match = /^\/([^\x20\t\r\n]+)[\x20\t\r\n]*$/.exec(source);
125
+ const name = match?.[1];
126
+ if (!name || !XML_QNAME.test(name)) {
127
+ throw pptxXmlSecurity("Malformed XML closing tag", "unsafe-xml", partName);
128
+ }
129
+ const current = stack.pop();
130
+ if (!current || current.name !== name) {
131
+ throw pptxXmlSecurity("Mismatched XML closing tag", "unsafe-xml", partName);
132
+ }
133
+ } else {
134
+ if (isXmlSpace(source[0])) {
135
+ throw pptxXmlSecurity("XML start tag has leading whitespace", "unsafe-xml", partName);
136
+ }
137
+ const selfClosing = /\/[\x20\t\r\n]*$/.test(source);
138
+ const body = selfClosing ? source.replace(/\/[\x20\t\r\n]*$/, "") : source;
139
+ const element = parseStartTag(body, partName, budget.limits.xmlAttributesPerElement);
140
+ addNode(element);
141
+ if (!selfClosing) {
142
+ stack.push(element);
143
+ if (stack.length > budget.limits.xmlDepth) {
144
+ throw pptxXmlLimit("XML exceeds its depth limit", partName);
145
+ }
146
+ }
147
+ }
148
+ cursor = end + 1;
149
+ }
150
+ if (stack.length > 0 || !root) {
151
+ throw pptxXmlSecurity("XML is empty or ends with unclosed elements", "unsafe-xml", partName);
152
+ }
153
+ return root;
154
+ }
155
+
156
+ function findTagEnd(xml: string, start: number, partName: string): number {
157
+ let quote: string | undefined;
158
+ for (let index = start; index < xml.length; index += 1) {
159
+ const character = xml[index];
160
+ if (quote) {
161
+ if (character === quote) quote = undefined;
162
+ } else if (character === '"' || character === "'") quote = character;
163
+ else if (character === ">") return index;
164
+ }
165
+ throw pptxXmlSecurity("Unterminated XML tag", "unsafe-xml", partName);
166
+ }
167
+
168
+ function parseStartTag(source: string, partName: string, maxAttributes: number): PptxXmlElement {
169
+ let cursor = 0;
170
+ const skipSpace = (): void => {
171
+ while (cursor < source.length && isXmlSpace(source[cursor])) cursor += 1;
172
+ };
173
+ const nameStart = cursor;
174
+ while (cursor < source.length && !isXmlSpace(source[cursor])) cursor += 1;
175
+ const name = source.slice(nameStart, cursor);
176
+ if (!XML_QNAME.test(name)) {
177
+ throw pptxXmlSecurity("Invalid XML element name", "unsafe-xml", partName);
178
+ }
179
+ const attributes = new Map<string, string>();
180
+ while (cursor < source.length) {
181
+ if (!isXmlSpace(source[cursor])) {
182
+ throw pptxXmlSecurity(
183
+ "XML attributes must be separated by whitespace",
184
+ "unsafe-xml",
185
+ partName,
186
+ );
187
+ }
188
+ skipSpace();
189
+ if (cursor >= source.length) break;
190
+ const attributeStart = cursor;
191
+ while (cursor < source.length && !isXmlSpace(source[cursor]) && source[cursor] !== "=")
192
+ cursor += 1;
193
+ const attributeName = source.slice(attributeStart, cursor);
194
+ if (!XML_QNAME.test(attributeName)) {
195
+ throw pptxXmlSecurity("Invalid XML attribute name", "unsafe-xml", partName);
196
+ }
197
+ skipSpace();
198
+ if (source[cursor] !== "=") {
199
+ throw pptxXmlSecurity("XML attribute lacks a value", "unsafe-xml", partName);
200
+ }
201
+ cursor += 1;
202
+ skipSpace();
203
+ const quote = source[cursor];
204
+ if (quote !== '"' && quote !== "'") {
205
+ throw pptxXmlSecurity("XML attribute value must be quoted", "unsafe-xml", partName);
206
+ }
207
+ cursor += 1;
208
+ const valueStart = cursor;
209
+ const valueEnd = source.indexOf(quote, valueStart);
210
+ if (valueEnd < 0) {
211
+ throw pptxXmlSecurity("Unterminated XML attribute", "unsafe-xml", partName);
212
+ }
213
+ if (attributes.has(attributeName)) {
214
+ throw pptxXmlSecurity("Duplicate XML attribute", "unsafe-xml", partName);
215
+ }
216
+ if (attributes.size >= maxAttributes) {
217
+ throw pptxXmlLimit("XML element exceeds its attribute limit", partName);
218
+ }
219
+ const rawValue = source.slice(valueStart, valueEnd);
220
+ if (rawValue.includes("<")) {
221
+ throw pptxXmlSecurity(
222
+ "XML attribute contains an unescaped less-than sign",
223
+ "unsafe-xml",
224
+ partName,
225
+ );
226
+ }
227
+ attributes.set(attributeName, decodeEntities(rawValue, partName));
228
+ cursor = valueEnd + 1;
229
+ }
230
+ return { type: "element", name, localName: xmlLocalName(name), attributes, children: [] };
231
+ }
232
+
233
+ function validateNamespaceBindings(
234
+ element: PptxXmlElement,
235
+ partName: string,
236
+ inherited: ReadonlyMap<string, string>,
237
+ ): void {
238
+ const bindings = new Map(inherited);
239
+ for (const [name, value] of element.attributes) {
240
+ if (name === "xmlns") bindings.set("", value);
241
+ else if (name.startsWith("xmlns:")) {
242
+ const declaredPrefix = name.slice(6);
243
+ const required = REQUIRED_NAMESPACE_URIS.get(declaredPrefix);
244
+ if (required && required !== value) {
245
+ throw pptxXmlSecurity(
246
+ "OOXML namespace prefix is rebound to an unexpected URI",
247
+ "unsafe-xml",
248
+ partName,
249
+ );
250
+ }
251
+ bindings.set(declaredPrefix, value);
252
+ }
253
+ }
254
+ const prefix = xmlPrefix(element.name);
255
+ if (prefix && !bindings.has(prefix)) {
256
+ throw pptxXmlSecurity(
257
+ "XML element uses an undeclared namespace prefix",
258
+ "unsafe-xml",
259
+ partName,
260
+ );
261
+ }
262
+ const namespaceUri = bindings.get(prefix ?? "");
263
+ if (namespaceUri === undefined) delete element.namespaceUri;
264
+ else element.namespaceUri = namespaceUri;
265
+ const attributeNamespaces = new Map<string, string | undefined>();
266
+ for (const name of element.attributes.keys()) {
267
+ if (name === "xmlns" || name.startsWith("xmlns:")) continue;
268
+ const attributePrefix = xmlPrefix(name);
269
+ if (attributePrefix && attributePrefix !== "xml" && !bindings.has(attributePrefix)) {
270
+ throw pptxXmlSecurity(
271
+ "XML attribute uses an undeclared namespace prefix",
272
+ "unsafe-xml",
273
+ partName,
274
+ );
275
+ }
276
+ attributeNamespaces.set(
277
+ name,
278
+ attributePrefix === "xml"
279
+ ? "http://www.w3.org/XML/1998/namespace"
280
+ : attributePrefix
281
+ ? bindings.get(attributePrefix)
282
+ : undefined,
283
+ );
284
+ }
285
+ element.attributeNamespaces = attributeNamespaces;
286
+ for (const child of pptxXmlChildren(element))
287
+ validateNamespaceBindings(child, partName, bindings);
288
+ }
289
+
290
+ function decodeEntities(value: string, partName: string): string {
291
+ validateXmlCharacters(value, partName);
292
+ if (value.includes("]]>")) {
293
+ throw pptxXmlSecurity("XML contains malformed character data", "unsafe-xml", partName);
294
+ }
295
+ if (!value.includes("&")) return value;
296
+ const output: string[] = [];
297
+ let cursor = 0;
298
+ let ampersand = value.indexOf("&");
299
+ let references = 0;
300
+ while (ampersand >= 0) {
301
+ references += 1;
302
+ if (references > 65_536) {
303
+ throw pptxXmlLimit("XML text exceeds its entity reference limit", partName);
304
+ }
305
+ output.push(value.slice(cursor, ampersand));
306
+ const semicolon = value.indexOf(";", ampersand + 1);
307
+ const nested = value.indexOf("&", ampersand + 1);
308
+ if (semicolon < 0 || (nested >= 0 && nested < semicolon)) {
309
+ throw pptxXmlSecurity(
310
+ "XML contains an unterminated entity reference",
311
+ "unsafe-xml",
312
+ partName,
313
+ );
314
+ }
315
+ const entity = value.slice(ampersand + 1, semicolon);
316
+ if (entity.length === 0 || entity.length > 16) {
317
+ throw pptxXmlSecurity("XML contains a malformed entity reference", "unsafe-xml", partName);
318
+ }
319
+ output.push(decodeEntity(entity, partName));
320
+ cursor = semicolon + 1;
321
+ ampersand = value.indexOf("&", cursor);
322
+ }
323
+ output.push(value.slice(cursor));
324
+ return output.join("");
325
+ }
326
+
327
+ function decodeEntity(entity: string, partName: string): string {
328
+ if (entity === "amp") return "&";
329
+ if (entity === "lt") return "<";
330
+ if (entity === "gt") return ">";
331
+ if (entity === "quot") return '"';
332
+ if (entity === "apos") return "'";
333
+ const decimal = /^#([0-9]+)$/.exec(entity);
334
+ const hexadecimal = /^#x([0-9a-f]+)$/i.exec(entity);
335
+ const codePoint = decimal
336
+ ? Number(decimal[1])
337
+ : hexadecimal
338
+ ? Number.parseInt(hexadecimal[1] ?? "", 16)
339
+ : NaN;
340
+ if (!Number.isInteger(codePoint) || !isValidXmlCodePoint(codePoint)) {
341
+ throw pptxXmlSecurity(`Unsupported or invalid XML entity: &${entity};`, "unsafe-xml", partName);
342
+ }
343
+ return String.fromCodePoint(codePoint);
344
+ }
345
+
346
+ function validateXmlCharacters(value: string, partName: string): void {
347
+ for (const character of value) {
348
+ if (!isValidXmlCodePoint(character.codePointAt(0) ?? 0)) {
349
+ throw pptxXmlSecurity("XML contains a forbidden character", "unsafe-xml", partName);
350
+ }
351
+ }
352
+ }
353
+
354
+ function isValidXmlCodePoint(codePoint: number): boolean {
355
+ return (
356
+ codePoint === 0x09 ||
357
+ codePoint === 0x0a ||
358
+ codePoint === 0x0d ||
359
+ (codePoint >= 0x20 && codePoint <= 0xd7ff) ||
360
+ (codePoint >= 0xe000 && codePoint <= 0xfffd) ||
361
+ (codePoint >= 0x10000 && codePoint <= 0x10ffff)
362
+ );
363
+ }
364
+
365
+ export function pptxXmlChildren(element: PptxXmlElement | undefined): PptxXmlElement[] {
366
+ return (
367
+ element?.children.filter((child): child is PptxXmlElement => child.type === "element") ?? []
368
+ );
369
+ }
370
+
371
+ export function pptxXmlChild(
372
+ element: PptxXmlElement | undefined,
373
+ localName: string,
374
+ ): PptxXmlElement | undefined {
375
+ return pptxXmlChildren(element).find((child) => child.localName === localName);
376
+ }
377
+
378
+ export function pptxXmlDescendants(
379
+ element: PptxXmlElement | undefined,
380
+ localName: string,
381
+ ): PptxXmlElement[] {
382
+ const output: PptxXmlElement[] = [];
383
+ const visit = (node: PptxXmlElement): void => {
384
+ for (const child of pptxXmlChildren(node)) {
385
+ if (child.localName === localName) output.push(child);
386
+ visit(child);
387
+ }
388
+ };
389
+ if (element) visit(element);
390
+ return output;
391
+ }
392
+
393
+ export function pptxXmlAttribute(
394
+ element: PptxXmlElement | undefined,
395
+ localName: string,
396
+ ): string | undefined {
397
+ if (!element) return undefined;
398
+ for (const [name, value] of element.attributes) {
399
+ if (xmlLocalName(name) === localName && name !== "xmlns" && !name.startsWith("xmlns:"))
400
+ return value;
401
+ }
402
+ return undefined;
403
+ }
404
+
405
+ export function pptxXmlQualifiedAttribute(
406
+ element: PptxXmlElement | undefined,
407
+ qualifiedName: string,
408
+ ): string | undefined {
409
+ return element?.attributes.get(qualifiedName);
410
+ }
411
+
412
+ export function pptxXmlText(element: PptxXmlElement | undefined): string {
413
+ if (!element) return "";
414
+ let output = "";
415
+ const visit = (node: PptxXmlNode): void => {
416
+ if (node.type === "text") output += node.value;
417
+ else for (const child of node.children) visit(child);
418
+ };
419
+ visit(element);
420
+ return output;
421
+ }
422
+
423
+ export function pptxXmlDirectText(element: PptxXmlElement | undefined): string {
424
+ return (
425
+ element?.children
426
+ .filter((child): child is { type: "text"; value: string } => child.type === "text")
427
+ .map((child) => child.value)
428
+ .join("") ?? ""
429
+ );
430
+ }
431
+
432
+ export function pptxXmlSecurity(
433
+ message: string,
434
+ code: PresentationSecurityError["code"],
435
+ entryName?: string,
436
+ ): PresentationSecurityError {
437
+ return new PresentationSecurityError(
438
+ entryName ? `${message}: ${entryName}` : message,
439
+ code,
440
+ entryName,
441
+ );
442
+ }
443
+
444
+ function pptxXmlLimit(message: string, entryName?: string): PresentationSecurityError {
445
+ return pptxXmlSecurity(message, "limit-exceeded", entryName);
446
+ }
447
+
448
+ function xmlLocalName(value: string): string {
449
+ return value.slice(value.indexOf(":") + 1);
450
+ }
451
+
452
+ function xmlPrefix(value: string): string | undefined {
453
+ const index = value.indexOf(":");
454
+ return index < 0 ? undefined : value.slice(0, index);
455
+ }
456
+
457
+ function isXmlSpace(value: string | undefined): boolean {
458
+ return value === " " || value === "\t" || value === "\r" || value === "\n";
459
+ }
460
+
461
+ function isXmlWhitespace(value: string): boolean {
462
+ for (const character of value) if (!isXmlSpace(character)) return false;
463
+ return true;
464
+ }
465
+
466
+ const XML_QNAME = /^(?:[A-Za-z_][A-Za-z0-9_.-]*:)?[A-Za-z_][A-Za-z0-9_.-]*$/;
467
+
468
+ const REQUIRED_NAMESPACE_URIS = new Map([
469
+ ["a", "http://schemas.openxmlformats.org/drawingml/2006/main"],
470
+ ["c", "http://schemas.openxmlformats.org/drawingml/2006/chart"],
471
+ ["p", "http://schemas.openxmlformats.org/presentationml/2006/main"],
472
+ ["r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"],
473
+ ]);