@bendyline/squisq-formats 2.0.1 → 2.2.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.
- package/LICENSE +21 -0
- package/NOTICE.md +20 -0
- package/README.md +1 -1
- package/dist/{chunk-CRAVSMPZ.js → chunk-26ISNJ7Y.js} +356 -114
- package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
- package/dist/{chunk-HTW2M27H.js → chunk-3NKXBZSR.js} +193 -42
- package/dist/{chunk-U32AG3G3.js → chunk-4V3KCHAP.js} +3 -4
- package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
- package/dist/{chunk-QRVN6A6E.js → chunk-6S6GU3ZG.js} +5 -6
- package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
- package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
- package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
- package/dist/{chunk-XKUMNGBW.js → chunk-EJTNGKEA.js} +5 -8
- package/dist/chunk-GX7RAUME.js +121 -0
- package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
- package/dist/{chunk-ABVI556T.js → chunk-IPN56VLW.js} +83 -58
- package/dist/{chunk-LXYLOOST.js → chunk-JE6LSIHE.js} +83 -22
- package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
- package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
- package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
- package/dist/chunk-OGS5VCGJ.js +446 -0
- package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
- package/dist/{chunk-2KPARF2P.js → chunk-PU7REGWV.js} +5 -8
- package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
- package/dist/{chunk-VSYHZECT.js → chunk-TAAENIRB.js} +5 -8
- package/dist/{chunk-WC7WULGV.js → chunk-X2DEAXNK.js} +62 -2
- package/dist/container/index.js +1 -2
- package/dist/csv/index.d.ts +27 -2
- package/dist/csv/index.js +1 -2
- package/dist/docx/index.d.ts +5 -1
- package/dist/docx/index.js +9 -10
- package/dist/epub/index.d.ts +2 -0
- package/dist/epub/index.js +5 -6
- package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
- package/dist/extract-MN7LA3NL.js +13 -0
- package/dist/html/index.d.ts +11 -4
- package/dist/html/index.js +3 -4
- package/dist/images-ESPQKVTW.js +6 -0
- package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
- package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +28 -26
- package/dist/infer/index.d.ts +3 -3
- package/dist/infer/index.js +7 -9
- package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
- package/dist/layouts-DRWZGSPD.js +10 -0
- package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
- package/dist/ooxml/index.d.ts +3 -3
- package/dist/ooxml/index.js +14 -13
- package/dist/pdf/index.d.ts +18 -0
- package/dist/pdf/index.js +2 -3
- package/dist/pptx/index.d.ts +4 -4
- package/dist/pptx/index.js +11 -13
- package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
- package/dist/registry/index.d.ts +21 -5
- package/dist/registry/index.js +9 -6
- package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
- package/dist/xlsx/index.d.ts +3 -3
- package/dist/xlsx/index.js +6 -7
- package/package.json +6 -3
- package/dist/chunk-2KPARF2P.js.map +0 -1
- package/dist/chunk-4VUWTSGM.js.map +0 -1
- package/dist/chunk-6M7Z25LA.js +0 -46
- package/dist/chunk-6M7Z25LA.js.map +0 -1
- package/dist/chunk-ABVI556T.js.map +0 -1
- package/dist/chunk-CRAVSMPZ.js.map +0 -1
- package/dist/chunk-FE6OJV6O.js.map +0 -1
- package/dist/chunk-GVS2XXV6.js.map +0 -1
- package/dist/chunk-HTW2M27H.js.map +0 -1
- package/dist/chunk-LXYLOOST.js.map +0 -1
- package/dist/chunk-MLX2BOJC.js.map +0 -1
- package/dist/chunk-NKAJPJ4G.js.map +0 -1
- package/dist/chunk-O3GVVND4.js.map +0 -1
- package/dist/chunk-ODL3SSPT.js.map +0 -1
- package/dist/chunk-PN52A5AA.js.map +0 -1
- package/dist/chunk-QRVN6A6E.js.map +0 -1
- package/dist/chunk-RFAPOKHJ.js.map +0 -1
- package/dist/chunk-SSUPBUF5.js.map +0 -1
- package/dist/chunk-U32AG3G3.js.map +0 -1
- package/dist/chunk-U4MRIFKL.js.map +0 -1
- package/dist/chunk-VJJM2SSH.js +0 -275
- package/dist/chunk-VJJM2SSH.js.map +0 -1
- package/dist/chunk-VSYHZECT.js.map +0 -1
- package/dist/chunk-WC7WULGV.js.map +0 -1
- package/dist/chunk-XKUMNGBW.js.map +0 -1
- package/dist/chunk-YRT7GQ5Y.js +0 -28
- package/dist/chunk-YRT7GQ5Y.js.map +0 -1
- package/dist/container/index.js.map +0 -1
- package/dist/csv/index.js.map +0 -1
- package/dist/docx/index.js.map +0 -1
- package/dist/epub/index.js.map +0 -1
- package/dist/extract-H6RXJMHP.js +0 -15
- package/dist/extract-H6RXJMHP.js.map +0 -1
- package/dist/html/index.js.map +0 -1
- package/dist/images-7FBWPKE3.js +0 -7
- package/dist/images-7FBWPKE3.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infer/index.js.map +0 -1
- package/dist/layouts-QVPK3ZCU.js +0 -12
- package/dist/layouts-QVPK3ZCU.js.map +0 -1
- package/dist/mapTheme-IR27S6IV.js.map +0 -1
- package/dist/ooxml/index.js.map +0 -1
- package/dist/pdf/index.js.map +0 -1
- package/dist/pptx/index.js.map +0 -1
- package/dist/registry/index.js.map +0 -1
- package/dist/xlsx/index.js.map +0 -1
- package/src/__tests__/container.test.ts +0 -230
- package/src/__tests__/convert.test.ts +0 -495
- package/src/__tests__/csvImport.test.ts +0 -84
- package/src/__tests__/docxExport.test.ts +0 -457
- package/src/__tests__/docxImport.test.ts +0 -410
- package/src/__tests__/epub.test.ts +0 -649
- package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
- package/src/__tests__/formatRegistry.test.ts +0 -174
- package/src/__tests__/html.test.ts +0 -435
- package/src/__tests__/htmlImport.test.ts +0 -57
- package/src/__tests__/inferTheme.test.ts +0 -135
- package/src/__tests__/lossyWarnings.test.ts +0 -146
- package/src/__tests__/ooxml.test.ts +0 -271
- package/src/__tests__/ooxmlCancellation.test.ts +0 -113
- package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
- package/src/__tests__/pdfExport.test.ts +0 -322
- package/src/__tests__/pdfImport.test.ts +0 -384
- package/src/__tests__/plainHtml.test.ts +0 -417
- package/src/__tests__/plainHtmlBundle.test.ts +0 -253
- package/src/__tests__/pptxExport.test.ts +0 -138
- package/src/__tests__/pptxImport.test.ts +0 -145
- package/src/__tests__/pptxInferFixtures.ts +0 -314
- package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
- package/src/__tests__/roundTrip.test.ts +0 -201
- package/src/__tests__/roundTripAssets.test.ts +0 -50
- package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
- package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
- package/src/__tests__/roundTripMatrix.test.ts +0 -142
- package/src/__tests__/sharedContainer.test.ts +0 -41
- package/src/__tests__/sharedImages.test.ts +0 -61
- package/src/__tests__/xlsxExport.test.ts +0 -164
- package/src/__tests__/xlsxImport.test.ts +0 -80
- package/src/__tests__/zipSafety.test.ts +0 -317
- package/src/container/index.ts +0 -94
- package/src/csv/index.ts +0 -188
- package/src/docx/export.ts +0 -1267
- package/src/docx/import.ts +0 -995
- package/src/docx/index.ts +0 -26
- package/src/docx/styles.ts +0 -145
- package/src/epub/export.ts +0 -968
- package/src/epub/index.ts +0 -20
- package/src/html/docsHtmlBundle.ts +0 -373
- package/src/html/htmlTemplate.ts +0 -385
- package/src/html/imageUtils.ts +0 -61
- package/src/html/import.ts +0 -297
- package/src/html/index.ts +0 -212
- package/src/html/plainHtml.ts +0 -790
- package/src/html/plainHtmlBundle.ts +0 -421
- package/src/index.ts +0 -109
- package/src/infer/extract.ts +0 -127
- package/src/infer/index.ts +0 -199
- package/src/infer/mapTheme.ts +0 -176
- package/src/infer/types.ts +0 -27
- package/src/ooxml/index.ts +0 -111
- package/src/ooxml/namespaces.ts +0 -196
- package/src/ooxml/readUtils.ts +0 -44
- package/src/ooxml/reader.ts +0 -318
- package/src/ooxml/themeReader.ts +0 -197
- package/src/ooxml/types.ts +0 -103
- package/src/ooxml/writer.ts +0 -339
- package/src/ooxml/xmlUtils.ts +0 -123
- package/src/pdf/export.ts +0 -1084
- package/src/pdf/import.ts +0 -1164
- package/src/pdf/index.ts +0 -29
- package/src/pdf/styles.ts +0 -180
- package/src/pptx/export.ts +0 -1184
- package/src/pptx/import.ts +0 -455
- package/src/pptx/index.ts +0 -52
- package/src/pptx/layouts.ts +0 -1222
- package/src/pptx/styles.ts +0 -96
- package/src/pptx/templates.ts +0 -187
- package/src/registry/convert.ts +0 -433
- package/src/registry/defaultFormats.ts +0 -413
- package/src/registry/errors.ts +0 -46
- package/src/registry/index.ts +0 -43
- package/src/registry/registry.ts +0 -48
- package/src/registry/types.ts +0 -170
- package/src/shared/boundedZipArchive.ts +0 -383
- package/src/shared/container.ts +0 -28
- package/src/shared/fidelity.ts +0 -130
- package/src/shared/images.ts +0 -44
- package/src/shared/inlineRuns.ts +0 -99
- package/src/shared/text.ts +0 -41
- package/src/shared/zipEntryCount.ts +0 -151
- package/src/shared/zipLimits.ts +0 -296
- package/src/shared/zipSafety.ts +0 -19
- package/src/xlsx/export.ts +0 -253
- package/src/xlsx/import.ts +0 -160
- package/src/xlsx/index.ts +0 -35
package/src/shared/zipLimits.ts
DELETED
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
/** ZIP safety policy, structured errors, path checks, and metadata validation. */
|
|
2
|
-
|
|
3
|
-
import type JSZip from 'jszip';
|
|
4
|
-
|
|
5
|
-
export interface ZipSafetyLimits {
|
|
6
|
-
/** Cooperatively cancel archive opening, validation, and member traversal. */
|
|
7
|
-
signal?: AbortSignal;
|
|
8
|
-
/** Maximum number of archive records, including directories. Default: 10,000. */
|
|
9
|
-
maxEntries?: number;
|
|
10
|
-
/** Maximum uncompressed bytes for one member. Defaults to maxUncompressedBytes. */
|
|
11
|
-
maxEntryUncompressedBytes?: number;
|
|
12
|
-
/** Maximum total uncompressed bytes across the archive. Default: 512 MiB. */
|
|
13
|
-
maxUncompressedBytes?: number;
|
|
14
|
-
/** Maximum uncompressed/compressed ratio for a member. Default: 1,000. */
|
|
15
|
-
maxCompressionRatio?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type ZipSafetyErrorCode =
|
|
19
|
-
| 'invalid-limit'
|
|
20
|
-
| 'invalid-archive'
|
|
21
|
-
| 'unsafe-path'
|
|
22
|
-
| 'invalid-entry-metadata'
|
|
23
|
-
| 'too-many-entries'
|
|
24
|
-
| 'entry-too-large'
|
|
25
|
-
| 'archive-too-large'
|
|
26
|
-
| 'compression-ratio-exceeded'
|
|
27
|
-
| 'size-mismatch'
|
|
28
|
-
| 'crc-mismatch'
|
|
29
|
-
| 'decompression-failed';
|
|
30
|
-
|
|
31
|
-
export interface ZipSafetyErrorOptions {
|
|
32
|
-
path?: string;
|
|
33
|
-
limit?: number;
|
|
34
|
-
actual?: number;
|
|
35
|
-
cause?: unknown;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** A stable, machine-readable error raised while validating or reading a ZIP. */
|
|
39
|
-
export class ZipSafetyError extends Error {
|
|
40
|
-
readonly code: ZipSafetyErrorCode;
|
|
41
|
-
readonly path?: string;
|
|
42
|
-
readonly limit?: number;
|
|
43
|
-
readonly actual?: number;
|
|
44
|
-
|
|
45
|
-
constructor(code: ZipSafetyErrorCode, message: string, options: ZipSafetyErrorOptions = {}) {
|
|
46
|
-
super(message);
|
|
47
|
-
this.name = 'ZipSafetyError';
|
|
48
|
-
this.code = code;
|
|
49
|
-
this.path = options.path;
|
|
50
|
-
this.limit = options.limit;
|
|
51
|
-
this.actual = options.actual;
|
|
52
|
-
if (options.cause !== undefined) {
|
|
53
|
-
(this as Error & { cause?: unknown }).cause = options.cause;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface ValidatedZipEntry {
|
|
59
|
-
path: string;
|
|
60
|
-
entry: JSZip.JSZipObject;
|
|
61
|
-
declaredSize?: number;
|
|
62
|
-
compressedSize?: number;
|
|
63
|
-
crc32?: number;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface ResolvedZipSafetyLimits {
|
|
67
|
-
maxEntries: number;
|
|
68
|
-
maxEntryUncompressedBytes: number;
|
|
69
|
-
maxUncompressedBytes: number;
|
|
70
|
-
maxCompressionRatio: number;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export const DEFAULT_MAX_ZIP_ENTRIES = 10_000;
|
|
74
|
-
export const DEFAULT_MAX_ZIP_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
|
|
75
|
-
export const DEFAULT_MAX_ZIP_COMPRESSION_RATIO = 1_000;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Validate central-directory metadata before any member is decompressed.
|
|
79
|
-
* JSZip normalizes traversal names, so inspect `unsafeOriginalName` too.
|
|
80
|
-
*/
|
|
81
|
-
export function validateZipArchive(zip: JSZip, limits: ZipSafetyLimits = {}): ValidatedZipEntry[] {
|
|
82
|
-
throwIfZipAborted(limits.signal);
|
|
83
|
-
const resolved = resolveZipSafetyLimits(limits);
|
|
84
|
-
const files: ValidatedZipEntry[] = [];
|
|
85
|
-
let memberCount = 0;
|
|
86
|
-
let declaredTotal = 0;
|
|
87
|
-
let oversizedEntry: { path: string; actual: number; limit: number } | undefined;
|
|
88
|
-
|
|
89
|
-
zip.forEach((relativePath, entry) => {
|
|
90
|
-
throwIfZipAborted(limits.signal);
|
|
91
|
-
memberCount++;
|
|
92
|
-
if (entry.dir) return;
|
|
93
|
-
const originalPath = originalEntryPath(entry, relativePath);
|
|
94
|
-
if (!isSafeArchivePath(originalPath)) {
|
|
95
|
-
throw new ZipSafetyError(
|
|
96
|
-
'unsafe-path',
|
|
97
|
-
`ZIP import: archive contains an unsafe path: ${originalPath}.`,
|
|
98
|
-
{ path: originalPath },
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const metadata = compressedMetadata(entry, originalPath);
|
|
103
|
-
if (
|
|
104
|
-
metadata.uncompressedSize !== undefined &&
|
|
105
|
-
metadata.uncompressedSize > resolved.maxEntryUncompressedBytes
|
|
106
|
-
) {
|
|
107
|
-
oversizedEntry ??= {
|
|
108
|
-
path: originalPath,
|
|
109
|
-
actual: metadata.uncompressedSize,
|
|
110
|
-
limit: resolved.maxEntryUncompressedBytes,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const ratio =
|
|
115
|
-
metadata.uncompressedSize === undefined || metadata.compressedSize === undefined
|
|
116
|
-
? undefined
|
|
117
|
-
: compressionRatio(metadata.uncompressedSize, metadata.compressedSize);
|
|
118
|
-
if (ratio !== undefined && ratio > resolved.maxCompressionRatio) {
|
|
119
|
-
throw compressionRatioError(originalPath, ratio, resolved.maxCompressionRatio);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
declaredTotal += metadata.uncompressedSize ?? 0;
|
|
123
|
-
if (!Number.isSafeInteger(declaredTotal)) throw invalidMetadataError(originalPath);
|
|
124
|
-
files.push({
|
|
125
|
-
path: relativePath,
|
|
126
|
-
entry,
|
|
127
|
-
...(metadata.uncompressedSize !== undefined
|
|
128
|
-
? { declaredSize: metadata.uncompressedSize }
|
|
129
|
-
: {}),
|
|
130
|
-
...(metadata.compressedSize !== undefined ? { compressedSize: metadata.compressedSize } : {}),
|
|
131
|
-
...(metadata.crc32 !== undefined ? { crc32: metadata.crc32 } : {}),
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
if (memberCount > resolved.maxEntries) {
|
|
136
|
-
throw tooManyEntriesError(memberCount, resolved.maxEntries);
|
|
137
|
-
}
|
|
138
|
-
if (declaredTotal > resolved.maxUncompressedBytes) {
|
|
139
|
-
throw archiveTooLargeError(declaredTotal, resolved.maxUncompressedBytes);
|
|
140
|
-
}
|
|
141
|
-
if (oversizedEntry) {
|
|
142
|
-
throw entryTooLargeError(oversizedEntry.path, oversizedEntry.actual, oversizedEntry.limit);
|
|
143
|
-
}
|
|
144
|
-
return files;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Preserve the caller's exact cancellation reason across ZIP/OOXML layers. */
|
|
148
|
-
export function throwIfZipAborted(signal?: AbortSignal): void {
|
|
149
|
-
if (!signal?.aborted) return;
|
|
150
|
-
throw signal.reason ?? new Error('ZIP operation was cancelled');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Reject a path that could be interpreted outside an archive's logical root. */
|
|
154
|
-
export function assertSafeZipPath(path: string): void {
|
|
155
|
-
if (!isSafeArchivePath(path)) {
|
|
156
|
-
throw new ZipSafetyError('unsafe-path', `ZIP archive path is unsafe: ${path}.`, { path });
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function resolveZipSafetyLimits(limits: ZipSafetyLimits): ResolvedZipSafetyLimits {
|
|
161
|
-
const maxEntries = limits.maxEntries ?? DEFAULT_MAX_ZIP_ENTRIES;
|
|
162
|
-
const maxUncompressedBytes = limits.maxUncompressedBytes ?? DEFAULT_MAX_ZIP_UNCOMPRESSED_BYTES;
|
|
163
|
-
const maxEntryUncompressedBytes = limits.maxEntryUncompressedBytes ?? maxUncompressedBytes;
|
|
164
|
-
const maxCompressionRatio = limits.maxCompressionRatio ?? DEFAULT_MAX_ZIP_COMPRESSION_RATIO;
|
|
165
|
-
|
|
166
|
-
assertNonNegativeInteger('maxEntries', maxEntries);
|
|
167
|
-
assertNonNegativeFinite('maxUncompressedBytes', maxUncompressedBytes);
|
|
168
|
-
assertNonNegativeFinite('maxEntryUncompressedBytes', maxEntryUncompressedBytes);
|
|
169
|
-
if (!Number.isFinite(maxCompressionRatio) || maxCompressionRatio < 1) {
|
|
170
|
-
throw invalidLimitError(
|
|
171
|
-
'maxCompressionRatio must be a finite number greater than or equal to 1',
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
return {
|
|
175
|
-
maxEntries,
|
|
176
|
-
maxEntryUncompressedBytes,
|
|
177
|
-
maxUncompressedBytes,
|
|
178
|
-
maxCompressionRatio,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function assertNonNegativeFinite(name: string, value: number): void {
|
|
183
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
184
|
-
throw invalidLimitError(`${name} must be a non-negative finite number`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function entryTooLargeError(path: string, actual: number, limit: number): ZipSafetyError {
|
|
189
|
-
return new ZipSafetyError(
|
|
190
|
-
'entry-too-large',
|
|
191
|
-
`ZIP import: archive member ${path} exceeds ${limit} byte per-entry limit.`,
|
|
192
|
-
{ path, limit, actual },
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function tooManyEntriesError(actual: number, limit: number): ZipSafetyError {
|
|
197
|
-
return new ZipSafetyError(
|
|
198
|
-
'too-many-entries',
|
|
199
|
-
`ZIP import: archive has ${actual} files; limit is ${limit}.`,
|
|
200
|
-
{ limit, actual },
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function archiveTooLargeError(actual: number, limit: number): ZipSafetyError {
|
|
205
|
-
return new ZipSafetyError(
|
|
206
|
-
'archive-too-large',
|
|
207
|
-
`ZIP import: uncompressed content exceeds ${limit} byte limit.`,
|
|
208
|
-
{ limit, actual },
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function compressionRatioError(path: string, actual: number, limit: number): ZipSafetyError {
|
|
213
|
-
return new ZipSafetyError(
|
|
214
|
-
'compression-ratio-exceeded',
|
|
215
|
-
`ZIP import: archive member ${path} exceeds ${limit}:1 compression-ratio limit.`,
|
|
216
|
-
{ path, limit, actual },
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function compressionRatio(uncompressedBytes: number, compressedBytes: number): number {
|
|
221
|
-
if (uncompressedBytes === 0) return 0;
|
|
222
|
-
if (compressedBytes === 0) return Number.POSITIVE_INFINITY;
|
|
223
|
-
return uncompressedBytes / compressedBytes;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function assertNonNegativeInteger(name: string, value: number): void {
|
|
227
|
-
if (!Number.isInteger(value) || value < 0) {
|
|
228
|
-
throw invalidLimitError(`${name} must be a non-negative integer`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function invalidLimitError(detail: string): ZipSafetyError {
|
|
233
|
-
return new ZipSafetyError('invalid-limit', `ZIP import: ${detail}.`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function originalEntryPath(entry: JSZip.JSZipObject, relativePath: string): string {
|
|
237
|
-
return (
|
|
238
|
-
(
|
|
239
|
-
entry as JSZip.JSZipObject & {
|
|
240
|
-
unsafeOriginalName?: string;
|
|
241
|
-
}
|
|
242
|
-
).unsafeOriginalName ?? relativePath
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function compressedMetadata(
|
|
247
|
-
entry: JSZip.JSZipObject,
|
|
248
|
-
path: string,
|
|
249
|
-
): { uncompressedSize?: number; compressedSize?: number; crc32?: number } {
|
|
250
|
-
const data = (
|
|
251
|
-
entry as unknown as {
|
|
252
|
-
_data?: { uncompressedSize?: number; compressedSize?: number; crc32?: number };
|
|
253
|
-
}
|
|
254
|
-
)._data;
|
|
255
|
-
const uncompressedSize = data?.uncompressedSize;
|
|
256
|
-
const compressedSize = data?.compressedSize;
|
|
257
|
-
const crc32 = data?.crc32;
|
|
258
|
-
if (
|
|
259
|
-
(uncompressedSize !== undefined && !isValidMetadataInteger(uncompressedSize)) ||
|
|
260
|
-
(compressedSize !== undefined && !isValidMetadataInteger(compressedSize)) ||
|
|
261
|
-
(crc32 !== undefined && !Number.isInteger(crc32))
|
|
262
|
-
) {
|
|
263
|
-
throw invalidMetadataError(path);
|
|
264
|
-
}
|
|
265
|
-
return {
|
|
266
|
-
...(uncompressedSize !== undefined ? { uncompressedSize } : {}),
|
|
267
|
-
...(compressedSize !== undefined ? { compressedSize } : {}),
|
|
268
|
-
...(crc32 !== undefined ? { crc32 } : {}),
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function isValidMetadataInteger(value: number): boolean {
|
|
273
|
-
return Number.isSafeInteger(value) && value >= 0;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function invalidMetadataError(path: string): ZipSafetyError {
|
|
277
|
-
return new ZipSafetyError(
|
|
278
|
-
'invalid-entry-metadata',
|
|
279
|
-
`ZIP import: archive member has invalid size or checksum metadata: ${path}.`,
|
|
280
|
-
{ path },
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function isSafeArchivePath(path: string): boolean {
|
|
285
|
-
const segments = path.split('/');
|
|
286
|
-
return !(
|
|
287
|
-
path.startsWith('/') ||
|
|
288
|
-
/^[a-zA-Z]:[\\/]/.test(path) ||
|
|
289
|
-
path.includes('\\') ||
|
|
290
|
-
Array.from(path).some((character) => {
|
|
291
|
-
const codePoint = character.codePointAt(0) ?? 0;
|
|
292
|
-
return codePoint < 0x20 || codePoint === 0x7f;
|
|
293
|
-
}) ||
|
|
294
|
-
segments.some((segment) => segment === '' || segment === '.' || segment === '..')
|
|
295
|
-
);
|
|
296
|
-
}
|
package/src/shared/zipSafety.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/** Stable facade for ZIP validation and bounded JSZip member reads. */
|
|
2
|
-
|
|
3
|
-
export {
|
|
4
|
-
ZipSafetyError,
|
|
5
|
-
DEFAULT_MAX_ZIP_ENTRIES,
|
|
6
|
-
DEFAULT_MAX_ZIP_UNCOMPRESSED_BYTES,
|
|
7
|
-
DEFAULT_MAX_ZIP_COMPRESSION_RATIO,
|
|
8
|
-
assertSafeZipPath,
|
|
9
|
-
validateZipArchive,
|
|
10
|
-
} from './zipLimits.js';
|
|
11
|
-
export type {
|
|
12
|
-
ZipSafetyLimits,
|
|
13
|
-
ZipSafetyErrorCode,
|
|
14
|
-
ZipSafetyErrorOptions,
|
|
15
|
-
ValidatedZipEntry,
|
|
16
|
-
} from './zipLimits.js';
|
|
17
|
-
|
|
18
|
-
export { createBoundedZipArchive, openBoundedZipArchive } from './boundedZipArchive.js';
|
|
19
|
-
export type { BoundedZipArchive } from './boundedZipArchive.js';
|
package/src/xlsx/export.ts
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* XLSX export — MarkdownDocument → SpreadsheetML (.xlsx).
|
|
3
|
-
*
|
|
4
|
-
* Tables-only fidelity (honestly documented): every `table` node in the
|
|
5
|
-
* markdown AST becomes one worksheet; all other content (headings, prose,
|
|
6
|
-
* lists, images, …) is dropped except that the nearest preceding heading is
|
|
7
|
-
* used to name the sheet. This mirrors the import side (`xlsxToMarkdownDoc`),
|
|
8
|
-
* which turns each worksheet grid back into a markdown table.
|
|
9
|
-
*
|
|
10
|
-
* Cells are emitted as inline strings (`t="inlineStr"`) so no sharedStrings
|
|
11
|
-
* part is needed, except values matching a plain-number pattern, which are
|
|
12
|
-
* emitted as numeric cells. The package is assembled with the shared ooxml/
|
|
13
|
-
* writer (auto-generates `[Content_Types].xml` + `_rels`), so only the
|
|
14
|
-
* SpreadsheetML-specific parts (workbook, worksheets, styles) are written here.
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```ts
|
|
18
|
-
* import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
19
|
-
* import { markdownDocToXlsx } from '@bendyline/squisq-formats/xlsx';
|
|
20
|
-
*
|
|
21
|
-
* const md = parseMarkdown('# Metrics\n\n| A | B |\n| - | - |\n| 1 | 2 |');
|
|
22
|
-
* const buffer = await markdownDocToXlsx(md);
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import type { Doc } from '@bendyline/squisq/schemas';
|
|
27
|
-
import { docToMarkdown } from '@bendyline/squisq/doc';
|
|
28
|
-
import type {
|
|
29
|
-
MarkdownDocument,
|
|
30
|
-
MarkdownBlockNode,
|
|
31
|
-
MarkdownTable,
|
|
32
|
-
MarkdownTableRow,
|
|
33
|
-
} from '@bendyline/squisq/markdown';
|
|
34
|
-
|
|
35
|
-
import { createPackage } from '../ooxml/writer.js';
|
|
36
|
-
import { xmlDeclaration, escapeXml } from '../ooxml/xmlUtils.js';
|
|
37
|
-
import { extractPlainText } from '../shared/text.js';
|
|
38
|
-
import {
|
|
39
|
-
NS_SML,
|
|
40
|
-
NS_R,
|
|
41
|
-
REL_OFFICE_DOCUMENT,
|
|
42
|
-
REL_STYLES,
|
|
43
|
-
REL_WORKSHEET,
|
|
44
|
-
CONTENT_TYPE_XLSX_WORKBOOK,
|
|
45
|
-
CONTENT_TYPE_XLSX_WORKSHEET,
|
|
46
|
-
CONTENT_TYPE_XLSX_STYLES,
|
|
47
|
-
} from '../ooxml/namespaces.js';
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Options for XLSX export.
|
|
51
|
-
*/
|
|
52
|
-
export interface XlsxExportOptions {
|
|
53
|
-
/** Workbook title (written to core properties). */
|
|
54
|
-
title?: string;
|
|
55
|
-
/** Workbook author (written to core properties). */
|
|
56
|
-
author?: string;
|
|
57
|
-
/** Prefix used for auto-named sheets when no heading precedes a table. Default: "Sheet". */
|
|
58
|
-
sheetNamePrefix?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface SheetData {
|
|
62
|
-
name: string;
|
|
63
|
-
/** Grid of cell text: first row is the header. */
|
|
64
|
-
grid: string[][];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** A plain integer or decimal (optionally negative) — emitted as a numeric cell. */
|
|
68
|
-
const NUMERIC_RE = /^-?\d+(\.\d+)?$/;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Sanitize a candidate name into a valid, unique Excel sheet name.
|
|
72
|
-
* Strips characters Excel forbids (`[]:*?/\`), caps at 31 chars, and
|
|
73
|
-
* de-duplicates against already-used names by appending 2, 3, …
|
|
74
|
-
*/
|
|
75
|
-
function sanitizeSheetName(raw: string, used: Set<string>, fallback: string): string {
|
|
76
|
-
let name = raw.replace(/[[\]:*?/\\]/g, '').trim();
|
|
77
|
-
if (name.length === 0) name = fallback;
|
|
78
|
-
name = name.slice(0, 31);
|
|
79
|
-
|
|
80
|
-
if (!used.has(name)) {
|
|
81
|
-
used.add(name);
|
|
82
|
-
return name;
|
|
83
|
-
}
|
|
84
|
-
// Dedupe: "Name", "Name2", "Name3", … keeping within the 31-char cap.
|
|
85
|
-
for (let n = 2; ; n++) {
|
|
86
|
-
const suffix = String(n);
|
|
87
|
-
const base = name.slice(0, 31 - suffix.length);
|
|
88
|
-
const candidate = `${base}${suffix}`;
|
|
89
|
-
if (!used.has(candidate)) {
|
|
90
|
-
used.add(candidate);
|
|
91
|
-
return candidate;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Convert a table node into a grid of plain-text cells. */
|
|
97
|
-
function tableToGrid(table: MarkdownTable): string[][] {
|
|
98
|
-
return table.children.map((row: MarkdownTableRow) =>
|
|
99
|
-
row.children.map((cell) => extractPlainText(cell.children)),
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Walk the document, pairing each table with the nearest preceding heading. */
|
|
104
|
-
function collectSheets(doc: MarkdownDocument, prefix: string): SheetData[] {
|
|
105
|
-
const used = new Set<string>();
|
|
106
|
-
const sheets: SheetData[] = [];
|
|
107
|
-
let pendingHeading: string | null = null;
|
|
108
|
-
|
|
109
|
-
const visit = (nodes: MarkdownBlockNode[]): void => {
|
|
110
|
-
for (const node of nodes) {
|
|
111
|
-
if (node.type === 'heading') {
|
|
112
|
-
pendingHeading = extractPlainText(node.children);
|
|
113
|
-
} else if (node.type === 'table') {
|
|
114
|
-
const fallback = `${prefix}${sheets.length + 1}`;
|
|
115
|
-
const name = sanitizeSheetName(pendingHeading ?? fallback, used, fallback);
|
|
116
|
-
sheets.push({ name, grid: tableToGrid(node) });
|
|
117
|
-
pendingHeading = null;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
visit(doc.children);
|
|
123
|
-
return sheets;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Column letters for a zero-based index (0 → "A", 26 → "AA"). */
|
|
127
|
-
function columnLetter(index: number): string {
|
|
128
|
-
let n = index + 1;
|
|
129
|
-
let letters = '';
|
|
130
|
-
while (n > 0) {
|
|
131
|
-
const rem = (n - 1) % 26;
|
|
132
|
-
letters = String.fromCharCode(65 + rem) + letters;
|
|
133
|
-
n = Math.floor((n - 1) / 26);
|
|
134
|
-
}
|
|
135
|
-
return letters;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function cellXml(value: string, ref: string): string {
|
|
139
|
-
if (NUMERIC_RE.test(value)) {
|
|
140
|
-
return `<c r="${ref}"><v>${escapeXml(value)}</v></c>`;
|
|
141
|
-
}
|
|
142
|
-
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(value)}</t></is></c>`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function worksheetXml(grid: string[][]): string {
|
|
146
|
-
const rows: string[] = [];
|
|
147
|
-
for (let r = 0; r < grid.length; r++) {
|
|
148
|
-
const cells = grid[r]!;
|
|
149
|
-
const cellsXml = cells.map((value, c) => cellXml(value, `${columnLetter(c)}${r + 1}`)).join('');
|
|
150
|
-
rows.push(`<row r="${r + 1}">${cellsXml}</row>`);
|
|
151
|
-
}
|
|
152
|
-
return (
|
|
153
|
-
`${xmlDeclaration()}\n` +
|
|
154
|
-
`<worksheet xmlns="${NS_SML}" xmlns:r="${NS_R}">` +
|
|
155
|
-
`<sheetData>${rows.join('')}</sheetData>` +
|
|
156
|
-
`</worksheet>`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function workbookXml(sheets: SheetData[]): string {
|
|
161
|
-
const sheetEls = sheets
|
|
162
|
-
.map(
|
|
163
|
-
(sheet, i) =>
|
|
164
|
-
`<sheet name="${escapeXml(sheet.name)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`,
|
|
165
|
-
)
|
|
166
|
-
.join('');
|
|
167
|
-
return (
|
|
168
|
-
`${xmlDeclaration()}\n` +
|
|
169
|
-
`<workbook xmlns="${NS_SML}" xmlns:r="${NS_R}">` +
|
|
170
|
-
`<sheets>${sheetEls}</sheets>` +
|
|
171
|
-
`</workbook>`
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Minimal but valid styles part (Excel expects the referenced collections to exist). */
|
|
176
|
-
function stylesXml(): string {
|
|
177
|
-
return (
|
|
178
|
-
`${xmlDeclaration()}\n` +
|
|
179
|
-
`<styleSheet xmlns="${NS_SML}">` +
|
|
180
|
-
`<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>` +
|
|
181
|
-
`<fills count="1"><fill><patternFill patternType="none"/></fill></fills>` +
|
|
182
|
-
`<borders count="1"><border/></borders>` +
|
|
183
|
-
`<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>` +
|
|
184
|
-
`<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>` +
|
|
185
|
-
`</styleSheet>`
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Convert a MarkdownDocument to a .xlsx file (tables-only fidelity).
|
|
191
|
-
*
|
|
192
|
-
* Each markdown `table` becomes one worksheet; a document with no tables
|
|
193
|
-
* yields a single empty sheet (a valid, openable file — never throws).
|
|
194
|
-
*/
|
|
195
|
-
export async function markdownDocToXlsx(
|
|
196
|
-
doc: MarkdownDocument,
|
|
197
|
-
options: XlsxExportOptions = {},
|
|
198
|
-
): Promise<ArrayBuffer> {
|
|
199
|
-
const prefix = options.sheetNamePrefix ?? 'Sheet';
|
|
200
|
-
let sheets = collectSheets(doc, prefix);
|
|
201
|
-
|
|
202
|
-
// Zero tables → one empty sheet so the file is still valid.
|
|
203
|
-
if (sheets.length === 0) {
|
|
204
|
-
sheets = [{ name: `${prefix}1`, grid: [] }];
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const pkg = createPackage();
|
|
208
|
-
|
|
209
|
-
// Worksheets + workbook→worksheet relationships.
|
|
210
|
-
sheets.forEach((sheet, i) => {
|
|
211
|
-
const sheetPath = `xl/worksheets/sheet${i + 1}.xml`;
|
|
212
|
-
pkg.addPart(sheetPath, worksheetXml(sheet.grid), CONTENT_TYPE_XLSX_WORKSHEET);
|
|
213
|
-
pkg.addRelationship('xl/workbook.xml', {
|
|
214
|
-
id: `rId${i + 1}`,
|
|
215
|
-
type: REL_WORKSHEET,
|
|
216
|
-
target: `worksheets/sheet${i + 1}.xml`,
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// Styles part (referenced after the sheet relationships).
|
|
221
|
-
pkg.addPart('xl/styles.xml', stylesXml(), CONTENT_TYPE_XLSX_STYLES);
|
|
222
|
-
pkg.addRelationship('xl/workbook.xml', {
|
|
223
|
-
id: `rId${sheets.length + 1}`,
|
|
224
|
-
type: REL_STYLES,
|
|
225
|
-
target: 'styles.xml',
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// Workbook + root relationship.
|
|
229
|
-
pkg.addPart('xl/workbook.xml', workbookXml(sheets), CONTENT_TYPE_XLSX_WORKBOOK);
|
|
230
|
-
pkg.addRelationship('', {
|
|
231
|
-
id: 'rId1',
|
|
232
|
-
type: REL_OFFICE_DOCUMENT,
|
|
233
|
-
target: 'xl/workbook.xml',
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
if (options.title || options.author) {
|
|
237
|
-
pkg.setCoreProperties({
|
|
238
|
-
title: options.title,
|
|
239
|
-
creator: options.author,
|
|
240
|
-
created: new Date().toISOString(),
|
|
241
|
-
modified: new Date().toISOString(),
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return pkg.toArrayBuffer();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Convert a squisq Doc to a .xlsx file (via the markdown table model).
|
|
250
|
-
*/
|
|
251
|
-
export async function docToXlsx(doc: Doc, options?: XlsxExportOptions): Promise<ArrayBuffer> {
|
|
252
|
-
return markdownDocToXlsx(docToMarkdown(doc), options);
|
|
253
|
-
}
|