@intentic/fileq 1.317.0

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 (184) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/app.d.ts +2 -0
  4. package/dist/app.d.ts.map +1 -0
  5. package/dist/app.js +43 -0
  6. package/dist/app.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +4 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/commands/derive.command.d.ts +5 -0
  12. package/dist/commands/derive.command.d.ts.map +1 -0
  13. package/dist/commands/derive.command.js +53 -0
  14. package/dist/commands/derive.command.js.map +1 -0
  15. package/dist/commands/git-attributes.command.d.ts +3 -0
  16. package/dist/commands/git-attributes.command.d.ts.map +1 -0
  17. package/dist/commands/git-attributes.command.js +12 -0
  18. package/dist/commands/git-attributes.command.js.map +1 -0
  19. package/dist/commands/read.command.d.ts +3 -0
  20. package/dist/commands/read.command.d.ts.map +1 -0
  21. package/dist/commands/read.command.js +126 -0
  22. package/dist/commands/read.command.js.map +1 -0
  23. package/dist/commands/sweep.command.d.ts +3 -0
  24. package/dist/commands/sweep.command.d.ts.map +1 -0
  25. package/dist/commands/sweep.command.js +41 -0
  26. package/dist/commands/sweep.command.js.map +1 -0
  27. package/dist/lib/archives.d.ts +18 -0
  28. package/dist/lib/archives.d.ts.map +1 -0
  29. package/dist/lib/archives.js +133 -0
  30. package/dist/lib/archives.js.map +1 -0
  31. package/dist/lib/derive.d.ts +32 -0
  32. package/dist/lib/derive.d.ts.map +1 -0
  33. package/dist/lib/derive.js +99 -0
  34. package/dist/lib/derive.js.map +1 -0
  35. package/dist/lib/derivers/archive.d.ts +3 -0
  36. package/dist/lib/derivers/archive.d.ts.map +1 -0
  37. package/dist/lib/derivers/archive.js +132 -0
  38. package/dist/lib/derivers/archive.js.map +1 -0
  39. package/dist/lib/derivers/deriver.d.ts +13 -0
  40. package/dist/lib/derivers/deriver.d.ts.map +1 -0
  41. package/dist/lib/derivers/deriver.js +8 -0
  42. package/dist/lib/derivers/deriver.js.map +1 -0
  43. package/dist/lib/derivers/docx.d.ts +6 -0
  44. package/dist/lib/derivers/docx.d.ts.map +1 -0
  45. package/dist/lib/derivers/docx.js +29 -0
  46. package/dist/lib/derivers/docx.js.map +1 -0
  47. package/dist/lib/derivers/epub.d.ts +3 -0
  48. package/dist/lib/derivers/epub.d.ts.map +1 -0
  49. package/dist/lib/derivers/epub.js +102 -0
  50. package/dist/lib/derivers/epub.js.map +1 -0
  51. package/dist/lib/derivers/html.d.ts +3 -0
  52. package/dist/lib/derivers/html.d.ts.map +1 -0
  53. package/dist/lib/derivers/html.js +18 -0
  54. package/dist/lib/derivers/html.js.map +1 -0
  55. package/dist/lib/derivers/image.d.ts +3 -0
  56. package/dist/lib/derivers/image.d.ts.map +1 -0
  57. package/dist/lib/derivers/image.js +42 -0
  58. package/dist/lib/derivers/image.js.map +1 -0
  59. package/dist/lib/derivers/ipynb.d.ts +3 -0
  60. package/dist/lib/derivers/ipynb.d.ts.map +1 -0
  61. package/dist/lib/derivers/ipynb.js +75 -0
  62. package/dist/lib/derivers/ipynb.js.map +1 -0
  63. package/dist/lib/derivers/media.d.ts +3 -0
  64. package/dist/lib/derivers/media.d.ts.map +1 -0
  65. package/dist/lib/derivers/media.js +45 -0
  66. package/dist/lib/derivers/media.js.map +1 -0
  67. package/dist/lib/derivers/odp.d.ts +3 -0
  68. package/dist/lib/derivers/odp.d.ts.map +1 -0
  69. package/dist/lib/derivers/odp.js +41 -0
  70. package/dist/lib/derivers/odp.js.map +1 -0
  71. package/dist/lib/derivers/ods.d.ts +3 -0
  72. package/dist/lib/derivers/ods.d.ts.map +1 -0
  73. package/dist/lib/derivers/ods.js +86 -0
  74. package/dist/lib/derivers/ods.js.map +1 -0
  75. package/dist/lib/derivers/odt.d.ts +4 -0
  76. package/dist/lib/derivers/odt.d.ts.map +1 -0
  77. package/dist/lib/derivers/odt.js +77 -0
  78. package/dist/lib/derivers/odt.js.map +1 -0
  79. package/dist/lib/derivers/pdf-furniture.d.ts +7 -0
  80. package/dist/lib/derivers/pdf-furniture.d.ts.map +1 -0
  81. package/dist/lib/derivers/pdf-furniture.js +76 -0
  82. package/dist/lib/derivers/pdf-furniture.js.map +1 -0
  83. package/dist/lib/derivers/pdf.d.ts +4 -0
  84. package/dist/lib/derivers/pdf.d.ts.map +1 -0
  85. package/dist/lib/derivers/pdf.js +79 -0
  86. package/dist/lib/derivers/pdf.js.map +1 -0
  87. package/dist/lib/derivers/pptx.d.ts +4 -0
  88. package/dist/lib/derivers/pptx.d.ts.map +1 -0
  89. package/dist/lib/derivers/pptx.js +51 -0
  90. package/dist/lib/derivers/pptx.js.map +1 -0
  91. package/dist/lib/derivers/profile.d.ts +3 -0
  92. package/dist/lib/derivers/profile.d.ts.map +1 -0
  93. package/dist/lib/derivers/profile.js +115 -0
  94. package/dist/lib/derivers/profile.js.map +1 -0
  95. package/dist/lib/derivers/rtf.d.ts +4 -0
  96. package/dist/lib/derivers/rtf.d.ts.map +1 -0
  97. package/dist/lib/derivers/rtf.js +187 -0
  98. package/dist/lib/derivers/rtf.js.map +1 -0
  99. package/dist/lib/derivers/xlsx.d.ts +3 -0
  100. package/dist/lib/derivers/xlsx.d.ts.map +1 -0
  101. package/dist/lib/derivers/xlsx.js +68 -0
  102. package/dist/lib/derivers/xlsx.js.map +1 -0
  103. package/dist/lib/env.d.ts +2 -0
  104. package/dist/lib/env.d.ts.map +1 -0
  105. package/dist/lib/env.js +5 -0
  106. package/dist/lib/env.js.map +1 -0
  107. package/dist/lib/flags.d.ts +2 -0
  108. package/dist/lib/flags.d.ts.map +1 -0
  109. package/dist/lib/flags.js +8 -0
  110. package/dist/lib/flags.js.map +1 -0
  111. package/dist/lib/formats.d.ts +6 -0
  112. package/dist/lib/formats.d.ts.map +1 -0
  113. package/dist/lib/formats.js +110 -0
  114. package/dist/lib/formats.js.map +1 -0
  115. package/dist/lib/sidecar.d.ts +30 -0
  116. package/dist/lib/sidecar.d.ts.map +1 -0
  117. package/dist/lib/sidecar.js +108 -0
  118. package/dist/lib/sidecar.js.map +1 -0
  119. package/dist/lib/sweep.d.ts +7 -0
  120. package/dist/lib/sweep.d.ts.map +1 -0
  121. package/dist/lib/sweep.js +56 -0
  122. package/dist/lib/sweep.js.map +1 -0
  123. package/dist/lib/tools.d.ts +2 -0
  124. package/dist/lib/tools.d.ts.map +1 -0
  125. package/dist/lib/tools.js +15 -0
  126. package/dist/lib/tools.js.map +1 -0
  127. package/dist/lib/xml.d.ts +3 -0
  128. package/dist/lib/xml.d.ts.map +1 -0
  129. package/dist/lib/xml.js +16 -0
  130. package/dist/lib/xml.js.map +1 -0
  131. package/dist/skill.d.ts +5 -0
  132. package/dist/skill.d.ts.map +1 -0
  133. package/dist/skill.js +75 -0
  134. package/dist/skill.js.map +1 -0
  135. package/dist/testing.d.ts +25 -0
  136. package/dist/testing.d.ts.map +1 -0
  137. package/dist/testing.js +220 -0
  138. package/dist/testing.js.map +1 -0
  139. package/dist/version.d.ts +2 -0
  140. package/dist/version.d.ts.map +1 -0
  141. package/dist/version.js +3 -0
  142. package/dist/version.js.map +1 -0
  143. package/package.json +89 -0
  144. package/src/app.ts +48 -0
  145. package/src/cli.integration.test.ts +200 -0
  146. package/src/cli.ts +6 -0
  147. package/src/commands/derive.command.ts +59 -0
  148. package/src/commands/git-attributes.command.ts +13 -0
  149. package/src/commands/read.command.ts +158 -0
  150. package/src/commands/sweep.command.ts +48 -0
  151. package/src/derivers.integration.test.ts +415 -0
  152. package/src/lib/archives.test.ts +72 -0
  153. package/src/lib/archives.ts +209 -0
  154. package/src/lib/derive.ts +132 -0
  155. package/src/lib/derivers/archive.ts +168 -0
  156. package/src/lib/derivers/deriver.ts +33 -0
  157. package/src/lib/derivers/docx.test.ts +38 -0
  158. package/src/lib/derivers/docx.ts +44 -0
  159. package/src/lib/derivers/epub.ts +130 -0
  160. package/src/lib/derivers/html.ts +21 -0
  161. package/src/lib/derivers/image.ts +54 -0
  162. package/src/lib/derivers/ipynb.ts +113 -0
  163. package/src/lib/derivers/load-cost.test.ts +44 -0
  164. package/src/lib/derivers/media.ts +51 -0
  165. package/src/lib/derivers/odp.ts +52 -0
  166. package/src/lib/derivers/ods.ts +104 -0
  167. package/src/lib/derivers/odt.ts +101 -0
  168. package/src/lib/derivers/pdf-furniture.test.ts +60 -0
  169. package/src/lib/derivers/pdf-furniture.ts +121 -0
  170. package/src/lib/derivers/pdf.ts +102 -0
  171. package/src/lib/derivers/pptx.ts +61 -0
  172. package/src/lib/derivers/profile.ts +172 -0
  173. package/src/lib/derivers/rtf.ts +233 -0
  174. package/src/lib/derivers/xlsx.ts +82 -0
  175. package/src/lib/env.ts +7 -0
  176. package/src/lib/formats.test.ts +50 -0
  177. package/src/lib/formats.ts +141 -0
  178. package/src/lib/sidecar.test.ts +60 -0
  179. package/src/lib/sidecar.ts +156 -0
  180. package/src/lib/sweep.ts +72 -0
  181. package/src/lib/tools.ts +16 -0
  182. package/src/lib/xml.ts +20 -0
  183. package/src/skill.ts +90 -0
  184. package/src/testing.ts +333 -0
package/src/testing.ts ADDED
@@ -0,0 +1,333 @@
1
+ import { gzipSync, strToU8, zipSync } from "fflate";
2
+
3
+ // Fixture builders, one per binary format the suites derive, shared so deriver tests and CLI tests can't disagree on
4
+ // what "a docx" means.
5
+ // Each builds the smallest file its real-world parser accepts, in code rather than committed binaries, so the fixture's
6
+ // content is reviewable in a diff.
7
+
8
+ // Fixture text sits inside XML text nodes; a hostile fixture's payload must be text, not broken XML.
9
+ const xmlEscape = (text: string): string => text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
10
+
11
+ const CONTENT_TYPES = (overrides: string): string =>
12
+ `<?xml version="1.0" encoding="UTF-8"?>
13
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
14
+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
15
+ <Default Extension="xml" ContentType="application/xml"/>
16
+ ${overrides}
17
+ </Types>`;
18
+
19
+ const RELS = (target: string): string =>
20
+ `<?xml version="1.0" encoding="UTF-8"?>
21
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
22
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="${target}"/>
23
+ </Relationships>`;
24
+
25
+ /** A one-heading, N-paragraph Word document mammoth accepts. */
26
+ export const docxBytes = (heading: string, paragraphs: readonly string[]): Uint8Array => {
27
+ const body = [
28
+ `<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:t>${xmlEscape(heading)}</w:t></w:r></w:p>`,
29
+ ...paragraphs.map((text) => `<w:p><w:r><w:t xml:space="preserve">${xmlEscape(text)}</w:t></w:r></w:p>`),
30
+ ].join("");
31
+ return zipSync({
32
+ "[Content_Types].xml": strToU8(
33
+ CONTENT_TYPES(
34
+ '<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>',
35
+ ),
36
+ ),
37
+ "_rels/.rels": strToU8(RELS("word/document.xml")),
38
+ "word/document.xml": strToU8(
39
+ `<?xml version="1.0" encoding="UTF-8"?>
40
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>${body}</w:body></w:document>`,
41
+ ),
42
+ });
43
+ };
44
+
45
+ const slideXml = (lines: readonly string[]): string =>
46
+ `<?xml version="1.0" encoding="UTF-8"?>
47
+ <p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
48
+ <p:cSld><p:spTree>${lines.map((line) => `<p:sp><p:txBody><a:p><a:r><a:t>${xmlEscape(line)}</a:t></a:r></a:p></p:txBody></p:sp>`).join("")}</p:spTree></p:cSld>
49
+ </p:sld>`;
50
+
51
+ /** A presentation with one slide per entry of `slides`, each slide one text line per entry. */
52
+ export const pptxBytes = (slides: readonly (readonly string[])[]): Uint8Array => {
53
+ const files: Record<string, Uint8Array> = {
54
+ "[Content_Types].xml": strToU8(
55
+ CONTENT_TYPES(
56
+ '<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>',
57
+ ),
58
+ ),
59
+ "_rels/.rels": strToU8(RELS("ppt/presentation.xml")),
60
+ "ppt/presentation.xml": strToU8(
61
+ '<?xml version="1.0"?><p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"/>',
62
+ ),
63
+ };
64
+ slides.forEach((lines, index) => {
65
+ files[`ppt/slides/slide${index + 1}.xml`] = strToU8(slideXml(lines));
66
+ });
67
+ return zipSync(files);
68
+ };
69
+
70
+ /**
71
+ * Smallest well-formed one-page PDF with a text layer saying `text`; honest xref offsets keep it out of pdf.js's
72
+ * damaged-file recovery path.
73
+ */
74
+ export const pdfBytes = (text: string): Uint8Array => {
75
+ const objects = [
76
+ "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
77
+ "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
78
+ "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\nendobj\n",
79
+ `4 0 obj\n<< /Length ${text.length + 31} >>\nstream\nBT /F1 12 Tf 72 720 Td (${text}) Tj ET\nendstream\nendobj\n`,
80
+ "5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n",
81
+ ];
82
+ const header = "%PDF-1.4\n";
83
+ const offsets: number[] = [];
84
+ let position = header.length;
85
+ for (const object of objects) {
86
+ offsets.push(position);
87
+ position += object.length;
88
+ }
89
+ const xref = [
90
+ "xref",
91
+ `0 ${objects.length + 1}`,
92
+ "0000000000 65535 f ",
93
+ ...offsets.map((offset) => `${String(offset).padStart(10, "0")} 00000 n `),
94
+ "",
95
+ ].join("\n");
96
+ const trailer = `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${position}\n%%EOF\n`;
97
+ return strToU8(`${header}${objects.join("")}${xref}${trailer}`);
98
+ };
99
+
100
+ /** One second of 8kHz mono 8-bit silence in a well-formed RIFF/WAVE container. */
101
+ export const wavBytes = (): Uint8Array => {
102
+ const sampleRate = 8000;
103
+ const data = new Uint8Array(sampleRate).fill(128);
104
+ const buffer = new ArrayBuffer(44 + data.length);
105
+ const view = new DataView(buffer);
106
+ const ascii = (offset: number, value: string): void => {
107
+ for (let i = 0; i < value.length; i++) {
108
+ view.setUint8(offset + i, value.charCodeAt(i));
109
+ }
110
+ };
111
+ ascii(0, "RIFF");
112
+ view.setUint32(4, 36 + data.length, true);
113
+ ascii(8, "WAVE");
114
+ ascii(12, "fmt ");
115
+ view.setUint32(16, 16, true); // fmt chunk size
116
+ view.setUint16(20, 1, true); // PCM
117
+ view.setUint16(22, 1, true); // mono
118
+ view.setUint32(24, sampleRate, true);
119
+ view.setUint32(28, sampleRate, true); // byte rate (8-bit mono)
120
+ view.setUint16(32, 1, true); // block align
121
+ view.setUint16(34, 8, true); // bits per sample
122
+ ascii(36, "data");
123
+ view.setUint32(40, data.length, true);
124
+ const bytes = new Uint8Array(buffer);
125
+ bytes.set(data, 44);
126
+ return bytes;
127
+ };
128
+
129
+ // OpenDocument/EPUB need `mimetype` first in the archive and stored (level 0), per spec.
130
+ const storedMimetype = (mimetype: string): [Uint8Array, { level: 0 }] => [strToU8(mimetype), { level: 0 }];
131
+
132
+ /** A one-heading, N-paragraph OpenDocument text with a two-item list and a 1×2 table, in ODF's own vocabulary. */
133
+ export const odtBytes = (heading: string, paragraphs: readonly string[]): Uint8Array =>
134
+ zipSync({
135
+ mimetype: storedMimetype("application/vnd.oasis.opendocument.text"),
136
+ "META-INF/manifest.xml": strToU8(
137
+ `<?xml version="1.0" encoding="UTF-8"?>
138
+ <manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">
139
+ <manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.text"/>
140
+ <manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
141
+ </manifest:manifest>`,
142
+ ),
143
+ "meta.xml": strToU8(
144
+ `<?xml version="1.0" encoding="UTF-8"?>
145
+ <office:document-meta xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><office:meta><dc:title>${xmlEscape(heading)}</dc:title></office:meta></office:document-meta>`,
146
+ ),
147
+ "content.xml": strToU8(
148
+ `<?xml version="1.0" encoding="UTF-8"?>
149
+ <office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0">
150
+ <office:automatic-styles><style:style style:name="P1" style:family="paragraph"/></office:automatic-styles>
151
+ <office:body><office:text>
152
+ <text:h text:style-name="Heading_20_1" text:outline-level="1">${xmlEscape(heading)}</text:h>
153
+ ${paragraphs.map((text) => `<text:p text:style-name="P1">${xmlEscape(text)}</text:p>`).join("\n")}
154
+ <office:annotation><text:p>a reviewer's comment, not the document</text:p></office:annotation>
155
+ <text:list text:style-name="L1"><text:list-item><text:p>first item</text:p></text:list-item><text:list-item><text:p>second<text:s text:c="2"/>item</text:p></text:list-item></text:list>
156
+ <table:table table:name="T1"><table:table-column table:number-columns-repeated="2"/><table:table-row><table:table-cell office:value-type="string"><text:p>cell a</text:p></table:table-cell><table:table-cell office:value-type="string"><text:p>cell b</text:p></table:table-cell></table:table-row></table:table>
157
+ </office:text></office:body></office:document-content>`,
158
+ ),
159
+ });
160
+
161
+ const odfPackage = (mimetype: string, content: string): Uint8Array =>
162
+ zipSync({
163
+ mimetype: storedMimetype(mimetype),
164
+ "META-INF/manifest.xml": strToU8(
165
+ `<?xml version="1.0" encoding="UTF-8"?>
166
+ <manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">
167
+ <manifest:file-entry manifest:full-path="/" manifest:media-type="${mimetype}"/>
168
+ <manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
169
+ </manifest:manifest>`,
170
+ ),
171
+ "content.xml": strToU8(
172
+ `<?xml version="1.0" encoding="UTF-8"?>
173
+ <office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0">
174
+ <office:body>${content}</office:body></office:document-content>`,
175
+ ),
176
+ });
177
+
178
+ /** An OpenDocument spreadsheet: one sheet per entry, each row padded out the way a real .ods pads its own. */
179
+ export const odsBytes = (sheets: readonly { readonly name: string; readonly rows: readonly (readonly string[])[] }[]): Uint8Array =>
180
+ odfPackage(
181
+ "application/vnd.oasis.opendocument.spreadsheet",
182
+ `<office:spreadsheet>${sheets
183
+ .map(
184
+ (sheet) =>
185
+ `<table:table table:name="${xmlEscape(sheet.name)}">${sheet.rows
186
+ .map(
187
+ (row) =>
188
+ `<table:table-row>${row
189
+ .map((cell) => `<table:table-cell office:value-type="string"><text:p>${xmlEscape(cell)}</text:p></table:table-cell>`)
190
+ .join("")}<table:table-cell table:number-columns-repeated="1013"/></table:table-row>`,
191
+ )
192
+ .join("")}<table:table-row table:number-rows-repeated="1048576"><table:table-cell table:number-columns-repeated="1024"/></table:table-row></table:table>`,
193
+ )
194
+ .join("")}</office:spreadsheet>`,
195
+ );
196
+
197
+ /** An OpenDocument presentation: one page per entry, each with its lines and a speaker's note. */
198
+ export const odpBytes = (slides: readonly { readonly name: string; readonly lines: readonly string[]; readonly note?: string }[]): Uint8Array =>
199
+ odfPackage(
200
+ "application/vnd.oasis.opendocument.presentation",
201
+ `<office:presentation>${slides
202
+ .map(
203
+ (slide) =>
204
+ `<draw:page draw:name="${xmlEscape(slide.name)}">` +
205
+ `<draw:frame><draw:text-box>${slide.lines.map((line) => `<text:p>${xmlEscape(line)}</text:p>`).join("")}</draw:text-box></draw:frame>${
206
+ slide.note === undefined
207
+ ? ""
208
+ : `<presentation:notes><draw:frame><draw:text-box><text:p>${xmlEscape(slide.note)}</text:p></draw:text-box></draw:frame></presentation:notes>`
209
+ }</draw:page>`,
210
+ )
211
+ .join("")}</office:presentation>`,
212
+ );
213
+
214
+ /** A Word-shaped RTF: a font table and an ignorable group to skip, a \\'hh escape, and one table row. */
215
+ export const rtfBytes = (paragraphs: readonly string[]): Uint8Array =>
216
+ strToU8(
217
+ `{\\rtf1\\ansi\\ansicpg1252\\deff0{\\fonttbl{\\f0\\froman Times New Roman;}}{\\*\\generator Not text;}${
218
+ paragraphs.map((text) => `\\pard ${text.replaceAll("\\", "\\\\").replaceAll("{", "\\{").replaceAll("}", "\\}")}\\par`).join("")
219
+ }\\pard\\trowd\\cellx1440\\cellx2880 left\\cell right\\cell\\row}`,
220
+ );
221
+
222
+ /** An EPUB 3 with one XHTML chapter per entry, spine-ordered, plus a nav document that must not read as a chapter. */
223
+ export const epubBytes = (title: string, chapters: readonly { readonly title: string; readonly body: string }[]): Uint8Array => {
224
+ const xhtml = (heading: string, body: string): string =>
225
+ `<?xml version="1.0" encoding="UTF-8"?>
226
+ <html xmlns="http://www.w3.org/1999/xhtml"><head><title>${xmlEscape(heading)}</title></head><body><h1>${xmlEscape(heading)}</h1><p>${xmlEscape(body)}</p></body></html>`;
227
+ const files: Record<string, Uint8Array | [Uint8Array, { level: 0 }]> = {
228
+ mimetype: storedMimetype("application/epub+zip"),
229
+ "META-INF/container.xml": strToU8(
230
+ `<?xml version="1.0" encoding="UTF-8"?>
231
+ <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>`,
232
+ ),
233
+ "OEBPS/content.opf": strToU8(
234
+ `<?xml version="1.0" encoding="UTF-8"?>
235
+ <package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="id"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>${xmlEscape(title)}</dc:title><dc:identifier id="id">urn:uuid:fixture</dc:identifier></metadata>
236
+ <manifest><item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>${chapters.map((_, index) => `<item id="c${index + 1}" href="text/chapter${index + 1}.xhtml" media-type="application/xhtml+xml"/>`).join("")}</manifest>
237
+ <spine><itemref idref="nav"/>${chapters.map((_, index) => `<itemref idref="c${index + 1}"/>`).join("")}</spine></package>`,
238
+ ),
239
+ "OEBPS/nav.xhtml": strToU8(
240
+ `<?xml version="1.0" encoding="UTF-8"?>
241
+ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops"><head><title>Contents</title></head><body><nav epub:type="toc"><ol>${chapters.map((chapter, index) => `<li><a href="text/chapter${index + 1}.xhtml">${xmlEscape(chapter.title)}</a></li>`).join("")}</ol></nav></body></html>`,
242
+ ),
243
+ };
244
+ chapters.forEach((chapter, index) => {
245
+ files[`OEBPS/text/chapter${index + 1}.xhtml`] = strToU8(xhtml(chapter.title, chapter.body));
246
+ });
247
+ return zipSync(files);
248
+ };
249
+
250
+ /** A Jupyter notebook (nbformat 4) with a markdown title cell, a code cell with stream output, and one rich output. */
251
+ export const ipynbText = (title: string, code: string, outputLines: readonly string[]): string =>
252
+ JSON.stringify({
253
+ cells: [
254
+ { cell_type: "markdown", metadata: {}, source: [`# ${title}\n`, "\n", "Some prose about the analysis.\n"] },
255
+ {
256
+ cell_type: "code",
257
+ execution_count: 1,
258
+ metadata: {},
259
+ source: [code],
260
+ outputs: [
261
+ { output_type: "stream", name: "stdout", text: outputLines.map((line) => `${line}\n`) },
262
+ { output_type: "display_data", metadata: {}, data: { "image/png": "iVBORw0KGgo=" } },
263
+ ],
264
+ },
265
+ ],
266
+ metadata: { kernelspec: { language: "python", name: "python3" }, language_info: { name: "python" } },
267
+ nbformat: 4,
268
+ nbformat_minor: 5,
269
+ });
270
+
271
+ /** A zip holding one member per key, deflated the way any packing tool writes it. */
272
+ export const zipBytes = (files: Readonly<Record<string, string>>): Uint8Array =>
273
+ zipSync(Object.fromEntries(Object.entries(files).map(([path, text]) => [path, strToU8(text)])));
274
+
275
+ const TAR_BLOCK = 512;
276
+
277
+ // One ustar header block. The checksum is summed with its own field read as spaces, which is the rule that makes a
278
+ // header verifiable at all.
279
+ const tarHeader = (path: string, size: number, type: string): Uint8Array => {
280
+ const block = new Uint8Array(TAR_BLOCK);
281
+ const put = (offset: number, value: string): void => block.set(strToU8(value), offset);
282
+ put(0, path.slice(0, 100));
283
+ put(100, "0000644\0");
284
+ put(108, "0000000\0");
285
+ put(116, "0000000\0");
286
+ put(124, `${size.toString(8).padStart(11, "0")}\0`);
287
+ put(136, "00000000000\0");
288
+ block.fill(0x20, 148, 156);
289
+ put(156, type);
290
+ put(257, "ustar\0");
291
+ put(263, "00");
292
+ const sum = block.reduce((total, byte) => total + byte, 0);
293
+ put(148, `${sum.toString(8).padStart(6, "0")}\0 `);
294
+ return block;
295
+ };
296
+
297
+ const padded = (bytes: Uint8Array): Uint8Array => {
298
+ const block = new Uint8Array(Math.ceil(bytes.length / TAR_BLOCK) * TAR_BLOCK);
299
+ block.set(bytes);
300
+ return block;
301
+ };
302
+
303
+ /**
304
+ * A tar holding one member per key, in the layout GNU tar writes: a header per member, data padded to the block,
305
+ * two zero blocks at the end. A path past the 100-byte name field gets an 'L' block in front of it, as GNU tar does.
306
+ */
307
+ export const tarBytes = (files: Readonly<Record<string, string>>): Uint8Array => {
308
+ const parts: Uint8Array[] = [];
309
+ for (const [path, text] of Object.entries(files)) {
310
+ const data = strToU8(text);
311
+ if (path.length > 100) {
312
+ parts.push(tarHeader("././@LongLink", path.length + 1, "L"), padded(strToU8(`${path}\0`)));
313
+ }
314
+ parts.push(tarHeader(path, data.length, path.endsWith("/") ? "5" : "0"), padded(data));
315
+ }
316
+ parts.push(new Uint8Array(TAR_BLOCK * 2));
317
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
318
+ const tar = new Uint8Array(total);
319
+ let offset = 0;
320
+ for (const part of parts) {
321
+ tar.set(part, offset);
322
+ offset += part.length;
323
+ }
324
+ return tar;
325
+ };
326
+
327
+ /** A gzip member carrying the original name in its header, the way `gzip <file>` writes one. */
328
+ export const gzipBytes = (name: string, content: Uint8Array | string): Uint8Array =>
329
+ gzipSync(typeof content === "string" ? strToU8(content) : content, { filename: name });
330
+
331
+ /** A 1×1 PNG (no EXIF — dimensions are what image derivation reads off it). */
332
+ export const pngBytes = (): Uint8Array =>
333
+ Uint8Array.from(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64"));