omni-viewer-core 0.6.0 → 0.8.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 (110) hide show
  1. package/README.md +52 -0
  2. package/dist/i18n/catalog.de.js +6 -1
  3. package/dist/i18n/catalog.en.js +30 -6
  4. package/dist/i18n/catalog.fr.js +6 -1
  5. package/dist/i18n/catalog.it.js +6 -1
  6. package/dist/i18n/catalog.ja.js +30 -6
  7. package/dist/i18n/catalog.ko.js +30 -6
  8. package/dist/i18n/catalog.th.js +6 -1
  9. package/dist/i18n/catalog.zh-cn.js +29 -5
  10. package/dist/parsers/archive/index.d.ts +2 -0
  11. package/dist/parsers/archive/index.js +6 -5
  12. package/dist/parsers/archive/model.d.ts +36 -11
  13. package/dist/parsers/archive/worker.d.ts +8 -1
  14. package/dist/parsers/doc-binary/index.d.ts +23 -1
  15. package/dist/parsers/doc-binary/index.js +175 -7
  16. package/dist/parsers/parquet/index.d.ts +14 -0
  17. package/dist/parsers/parquet/index.js +23 -8
  18. package/dist/parsers/pptx/index.d.ts +2 -0
  19. package/dist/parsers/pptx/index.js +1 -1
  20. package/dist/parsers/pptx/limits.d.ts +26 -0
  21. package/dist/parsers/pptx/limits.js +154 -0
  22. package/dist/parsers/pptx/vscode-parser.d.ts +49 -3
  23. package/dist/parsers/pptx/vscode-parser.js +516 -60
  24. package/dist/parsers/pptx/vscode.d.ts +6 -2
  25. package/dist/parsers/pptx/vscode.js +95 -10
  26. package/dist/parsers/safetensors/index.d.ts +22 -0
  27. package/dist/parsers/safetensors/index.js +70 -9
  28. package/dist/parsers/safetensors/node.d.ts +13 -0
  29. package/dist/parsers/safetensors/node.js +52 -0
  30. package/dist/parsers/slide-model.d.ts +89 -6
  31. package/dist/parsers/types.d.ts +2 -0
  32. package/dist/parsers/zip-scan.d.ts +7 -0
  33. package/dist/parsers/zip-scan.js +22 -0
  34. package/dist/registry/index.js +2 -2
  35. package/dist/styles/archive.css +3 -1
  36. package/dist/styles/hdf5.css +1 -1
  37. package/dist/styles/jsonl.css +1 -1
  38. package/dist/styles/markdown.css +2 -2
  39. package/dist/styles/mermaid.css +10 -1
  40. package/dist/styles/parquet.css +1 -1
  41. package/dist/styles/pdf.css +6 -1
  42. package/dist/styles/plantuml.css +10 -1
  43. package/dist/styles/proto.css +1 -1
  44. package/dist/styles/safetensors.css +1 -1
  45. package/dist/styles/toml.css +1 -1
  46. package/dist/styles/word.css +2 -2
  47. package/dist/styles/yaml.css +1 -1
  48. package/dist/viewers/archive/controller.d.ts +4 -0
  49. package/dist/viewers/archive/controller.js +22 -0
  50. package/dist/viewers/archive/index.d.ts +38 -1
  51. package/dist/viewers/archive/index.js +176 -41
  52. package/dist/viewers/archive/styles.d.ts +1 -1
  53. package/dist/viewers/archive/styles.js +3 -1
  54. package/dist/viewers/diagram-controller.d.ts +42 -0
  55. package/dist/viewers/diagram-controller.js +47 -0
  56. package/dist/viewers/diagram-styles.d.ts +1 -0
  57. package/dist/viewers/diagram-styles.js +11 -0
  58. package/dist/viewers/diagram.d.ts +16 -6
  59. package/dist/viewers/diagram.js +321 -75
  60. package/dist/viewers/hdf5/styles.d.ts +1 -1
  61. package/dist/viewers/hdf5/styles.js +1 -1
  62. package/dist/viewers/jsonl/controller.d.ts +2 -0
  63. package/dist/viewers/jsonl/controller.js +4 -0
  64. package/dist/viewers/jsonl/index.d.ts +1 -0
  65. package/dist/viewers/jsonl/index.js +97 -51
  66. package/dist/viewers/jsonl/popup.d.ts +16 -0
  67. package/dist/viewers/jsonl/popup.js +20 -0
  68. package/dist/viewers/jsonl/styles.d.ts +1 -1
  69. package/dist/viewers/jsonl/styles.js +1 -1
  70. package/dist/viewers/markdown/index.js +79 -14
  71. package/dist/viewers/markdown/styles.d.ts +1 -1
  72. package/dist/viewers/markdown/styles.js +2 -2
  73. package/dist/viewers/mermaid/index.d.ts +3 -4
  74. package/dist/viewers/mermaid/index.js +1 -1
  75. package/dist/viewers/mermaid/self-loading.js +4 -1
  76. package/dist/viewers/parquet/index.d.ts +9 -1
  77. package/dist/viewers/parquet/index.js +20 -10
  78. package/dist/viewers/parquet/styles.d.ts +1 -1
  79. package/dist/viewers/parquet/styles.js +1 -1
  80. package/dist/viewers/pdf/controller.d.ts +9 -0
  81. package/dist/viewers/pdf/controller.js +17 -0
  82. package/dist/viewers/pdf/index.js +90 -22
  83. package/dist/viewers/pdf/styles.d.ts +1 -1
  84. package/dist/viewers/pdf/styles.js +6 -1
  85. package/dist/viewers/plantuml/index.d.ts +3 -4
  86. package/dist/viewers/plantuml/index.js +1 -1
  87. package/dist/viewers/plantuml/self-loading.js +1 -1
  88. package/dist/viewers/ppt/chart.js +58 -18
  89. package/dist/viewers/ppt/index.d.ts +33 -2
  90. package/dist/viewers/ppt/index.js +245 -61
  91. package/dist/viewers/ppt/render.js +85 -40
  92. package/dist/viewers/proto/index.js +77 -2
  93. package/dist/viewers/proto/styles.d.ts +1 -1
  94. package/dist/viewers/proto/styles.js +1 -1
  95. package/dist/viewers/safetensors/index.d.ts +9 -1
  96. package/dist/viewers/safetensors/index.js +30 -2
  97. package/dist/viewers/safetensors/styles.d.ts +1 -1
  98. package/dist/viewers/safetensors/styles.js +1 -1
  99. package/dist/viewers/structured-styles.d.ts +1 -1
  100. package/dist/viewers/structured-styles.js +1 -1
  101. package/dist/viewers/structured.js +28 -1
  102. package/dist/viewers/word/docx-preprocess.d.ts +18 -2
  103. package/dist/viewers/word/docx-preprocess.js +248 -47
  104. package/dist/viewers/word/index.d.ts +108 -1
  105. package/dist/viewers/word/index.js +772 -85
  106. package/dist/viewers/word/self-loading.d.ts +5 -1
  107. package/dist/viewers/word/self-loading.js +17 -4
  108. package/dist/viewers/word/styles.d.ts +1 -1
  109. package/dist/viewers/word/styles.js +2 -2
  110. package/package.json +7 -3
@@ -7,36 +7,60 @@ export interface ArchiveEntry {
7
7
  modifiedAt?: string | number | Date;
8
8
  mimeType?: string;
9
9
  encrypted?: boolean;
10
+ /** Decoder-native compression method, for example `store`, `deflate`, or `lzma`. */
11
+ compressionMethod?: string;
12
+ /** Explicit compression state when the decoder exposes it. */
13
+ compressed?: boolean;
14
+ /** Optional integrity metadata retained for host UI and diagnostics. */
15
+ crc32?: number;
10
16
  }
17
+ /** `encrypted` describes archive-wide encryption. Entry-level encryption is
18
+ * represented by {@link ArchiveEntry.encrypted}; adapters may provide both. */
11
19
  export interface ArchiveDocument {
12
20
  entries: readonly ArchiveEntry[];
21
+ encrypted?: boolean;
22
+ }
23
+ export interface ArchiveDecoderCapabilities {
24
+ /** Extensions/formats this concrete adapter can open. */
25
+ formats?: readonly string[];
26
+ /** `password` means the adapter can retry encrypted content with a password. */
27
+ encryption?: 'none' | 'detect' | 'password';
28
+ /** True when opening does not require buffering the complete archive. */
29
+ streaming?: boolean;
30
+ /** True when entries include `compressionMethod` and/or `compressed`. */
31
+ compressionMetadata?: boolean;
32
+ }
33
+ export interface ArchiveOpenOptions {
34
+ signal?: AbortSignal;
35
+ maxEntries?: number;
36
+ maxDecompressedBytes?: number;
37
+ /** Optional password supplied by a host-owned prompt. */
38
+ password?: string;
13
39
  }
14
40
  export interface ArchiveExtractOptions {
15
- signal?: AbortSignal; /** Decoder must stop before allocating beyond this output size. */
41
+ signal?: AbortSignal;
42
+ /** Decoder must stop before allocating beyond this output size. */
16
43
  maxBytes: number;
44
+ /** Optional password supplied by a host-owned prompt. */
45
+ password?: string;
17
46
  }
18
47
  export interface OpenArchiveHandle {
19
48
  entries: readonly ArchiveEntry[];
49
+ encrypted?: boolean;
20
50
  extract(entryId: number, options: ArchiveExtractOptions): Promise<Uint8Array>;
21
51
  close(): void | Promise<void>;
22
52
  }
23
53
  export interface ArchiveDecoder {
24
- openArchive(data: Uint8Array, options?: {
25
- signal?: AbortSignal;
26
- maxEntries?: number;
27
- maxDecompressedBytes?: number;
28
- }): Promise<OpenArchiveHandle>;
54
+ readonly capabilities?: ArchiveDecoderCapabilities;
55
+ openArchive(data: Uint8Array, options?: ArchiveOpenOptions): Promise<OpenArchiveHandle>;
29
56
  }
30
57
  /** Path/handle-based decoder that opens an archive without materializing the
31
58
  * whole file in memory (e.g. an adapter wrapping the system `7z`/`tar`). Core
32
59
  * drives listing and single-entry extraction lazily through the returned
33
60
  * handle; it never receives the archive bytes. */
34
61
  export interface ArchiveStreamDecoder {
35
- openArchive(options?: {
36
- signal?: AbortSignal;
37
- maxEntries?: number;
38
- maxDecompressedBytes?: number;
39
- }): Promise<OpenArchiveHandle>;
62
+ readonly capabilities?: ArchiveDecoderCapabilities;
63
+ openArchive(options?: ArchiveOpenOptions): Promise<OpenArchiveHandle>;
40
64
  }
41
65
  /** Streams one archive entry to an adapter-chosen destination without loading
42
66
  * the entry into memory. Core never sees the bytes — the adapter owns the
@@ -45,5 +69,6 @@ export interface ArchiveStreamDecoder {
45
69
  export interface ArchiveEntrySaver {
46
70
  saveEntry(entry: ArchiveEntry, options: {
47
71
  signal?: AbortSignal;
72
+ password?: string;
48
73
  }): Promise<string | null>;
49
74
  }
@@ -1,15 +1,22 @@
1
- /** Protocol names shared by decoder-specific Worker adapters. */
1
+ import type { ArchiveDecoderCapabilities } from './model.js';
2
+ /** Protocol names shared by decoder-specific Worker adapters. Optional fields
3
+ * keep existing JSZip/libarchive workers source-compatible. */
2
4
  export type ArchiveWorkerRequest = {
3
5
  type: 'list';
6
+ password?: string;
4
7
  } | {
5
8
  type: 'extract';
6
9
  entryId: number;
10
+ maxBytes?: number;
11
+ password?: string;
7
12
  } | {
8
13
  type: 'close';
9
14
  };
10
15
  export type ArchiveWorkerResponse = {
11
16
  type: 'entries';
12
17
  entries: unknown[];
18
+ encrypted?: boolean;
19
+ capabilities?: ArchiveDecoderCapabilities;
13
20
  } | {
14
21
  type: 'bytes';
15
22
  data: Uint8Array;
@@ -1,5 +1,7 @@
1
- import type { ParseOutcome } from '../types.js';
1
+ import type { ParseOptions, ParseOutcome } from '../types.js';
2
+ import { type DocBinaryRuntimeDeps } from './vscode-parser.js';
2
3
  export { DocBinaryParser } from './vscode-parser.js';
4
+ export type { DocBinaryRuntimeDeps } from './vscode-parser.js';
3
5
  export interface DocTextRun {
4
6
  text: string;
5
7
  bold?: boolean;
@@ -32,4 +34,24 @@ export interface DocBinaryDocument {
32
34
  sections: DocSection[];
33
35
  text: string;
34
36
  }
37
+ /**
38
+ * Compatibility document used by the feature-complete legacy DOC renderer.
39
+ *
40
+ * The older parser currently preserves substantially more layout information
41
+ * than {@link DocBinaryDocument}. Wrapping its output in ParseOutcome keeps
42
+ * failures and diagnostics typed while the semantic parser catches up, without
43
+ * regressing tables, columns, images, and section layout.
44
+ */
45
+ export interface DocBinaryHtmlDocument {
46
+ html: string;
47
+ }
35
48
  export declare function parseDocBinary(input: Uint8Array): ParseOutcome<DocBinaryDocument>;
49
+ /**
50
+ * Typed bridge for the feature-complete legacy DOC parser.
51
+ *
52
+ * Input-caused failures are returned rather than thrown. The viewer owns the
53
+ * separate HTML-to-DOM rendering and sanitization step.
54
+ */
55
+ export declare function parseDocBinaryHtml(input: Uint8Array, deps?: DocBinaryRuntimeDeps, options?: ParseOptions): Promise<ParseOutcome<DocBinaryHtmlDocument>>;
56
+ /** Cheap CFB directory-name probe used to keep xlsx lazy for ordinary DOC. */
57
+ export declare function hasEmbeddedDocWorkbook(input: Uint8Array): boolean;
@@ -1,3 +1,4 @@
1
+ import { DocBinaryParser } from './vscode-parser.js';
1
2
  export { DocBinaryParser } from './vscode-parser.js';
2
3
  const FREESECT = 0xffffffff;
3
4
  const ENDOFCHAIN = 0xfffffffe;
@@ -22,13 +23,21 @@ export function parseDocBinary(input) {
22
23
  const table = cfb.getStream(fib.tableStreamName);
23
24
  if (!table)
24
25
  return failed('corrupted');
25
- const pieces = parsePieceTable(table, fib);
26
+ const pieceTable = parsePieceTable(table, fib);
27
+ const pieces = pieceTable.pieces;
26
28
  if (pieces.length === 0)
27
29
  return failed('corrupted');
28
30
  const decoded = decodePieces(word, pieces);
29
31
  const mainText = normalize(decoded.slice(0, fib.ccpText || decoded.length));
30
32
  const headerText = fib.ccpHdd > 0 ? normalize(decoded.slice(fib.ccpText, fib.ccpText + fib.ccpHdd)) : '';
31
33
  const diagnostics = [];
34
+ if (pieceTable.recovered) {
35
+ diagnostics.push({
36
+ severity: 'warning',
37
+ code: 'recovered-corruption',
38
+ messageKey: 'diag.word.recovered-corruption'
39
+ });
40
+ }
32
41
  const blocks = buildBlocks(mainText);
33
42
  const images = extractImages(cfb);
34
43
  if (images.length)
@@ -46,9 +55,127 @@ export function parseDocBinary(input) {
46
55
  return outcome({ status: diagnostics.length ? 'partial' : 'ok', document: { sections: [section], text: mainText }, diagnostics });
47
56
  }
48
57
  catch (error) {
58
+ if (error instanceof DocPasswordRequiredError) {
59
+ return outcome({
60
+ status: 'failed',
61
+ failure: {
62
+ code: 'password-required',
63
+ retryable: true,
64
+ messageKey: 'diag.word.password-required'
65
+ },
66
+ diagnostics: []
67
+ });
68
+ }
49
69
  return outcome({ status: 'failed', failure: { code: 'invalid-format', retryable: false, messageKey: 'diag.word.invalid-format' }, diagnostics: [{ severity: 'error', code: 'parse-error', messageKey: 'diag.word.invalid-format', location: error instanceof Error ? error.message : String(error) }] });
50
70
  }
51
71
  }
72
+ /**
73
+ * Typed bridge for the feature-complete legacy DOC parser.
74
+ *
75
+ * Input-caused failures are returned rather than thrown. The viewer owns the
76
+ * separate HTML-to-DOM rendering and sanitization step.
77
+ */
78
+ export async function parseDocBinaryHtml(input, deps = {}, options = {}) {
79
+ const started = Date.now();
80
+ const finish = (result) => ({
81
+ result,
82
+ execution: {
83
+ workerUsed: false,
84
+ hardLimitEnforced: false,
85
+ elapsedMillis: Date.now() - started
86
+ }
87
+ });
88
+ if (options.signal?.aborted) {
89
+ return finish({
90
+ status: 'failed',
91
+ failure: { code: 'aborted', retryable: true, messageKey: 'diag.aborted' },
92
+ diagnostics: []
93
+ });
94
+ }
95
+ if (options.limits?.maxInputBytes !== undefined &&
96
+ input.byteLength > options.limits.maxInputBytes) {
97
+ return finish({
98
+ status: 'failed',
99
+ failure: {
100
+ code: 'limit-exceeded',
101
+ retryable: false,
102
+ messageKey: 'diag.word.limit-exceeded'
103
+ },
104
+ diagnostics: []
105
+ });
106
+ }
107
+ if (isEncryptedDocBinary(input)) {
108
+ return finish({
109
+ status: 'failed',
110
+ failure: {
111
+ code: 'password-required',
112
+ retryable: true,
113
+ messageKey: 'diag.word.password-required'
114
+ },
115
+ diagnostics: []
116
+ });
117
+ }
118
+ const diagnostics = [];
119
+ if (hasRecoveredPieceTable(input)) {
120
+ diagnostics.push({
121
+ severity: 'warning',
122
+ code: 'recovered-corruption',
123
+ messageKey: 'diag.word.recovered-corruption'
124
+ });
125
+ }
126
+ const hasWorkbook = hasEmbeddedDocWorkbook(input);
127
+ if (hasWorkbook && !deps.xlsx) {
128
+ diagnostics.push({
129
+ severity: 'warning',
130
+ code: 'embedded-workbook-disabled',
131
+ messageKey: 'diag.word.embedded-workbook-disabled',
132
+ location: 'Workbook'
133
+ });
134
+ }
135
+ try {
136
+ const html = await DocBinaryParser.parseBytes(input, deps);
137
+ if (options.signal?.aborted) {
138
+ return finish({
139
+ status: 'failed',
140
+ failure: { code: 'aborted', retryable: true, messageKey: 'diag.aborted' },
141
+ diagnostics
142
+ });
143
+ }
144
+ return finish({
145
+ status: diagnostics.length ? 'partial' : 'ok',
146
+ document: { html },
147
+ diagnostics
148
+ });
149
+ }
150
+ catch (error) {
151
+ const detail = error instanceof Error ? error.message : String(error);
152
+ const passwordRequired = /password|encrypted|encryption/i.test(detail);
153
+ const corrupted = OLE_MAGIC.every((byte, index) => input[index] === byte) &&
154
+ /corrupt|invalid.*(?:cfb|ole)|missing .*stream|sector|fat|chain/i.test(detail);
155
+ const code = passwordRequired ? 'password-required' : corrupted ? 'corrupted' : 'invalid-format';
156
+ const messageKey = passwordRequired
157
+ ? 'diag.word.password-required'
158
+ : corrupted ? 'diag.word.corrupted' : 'diag.word.invalid-format';
159
+ diagnostics.push({ severity: 'error', code, messageKey, location: detail });
160
+ return finish({
161
+ status: 'failed',
162
+ failure: { code, retryable: passwordRequired, messageKey },
163
+ diagnostics
164
+ });
165
+ }
166
+ }
167
+ /** Cheap CFB directory-name probe used to keep xlsx lazy for ordinary DOC. */
168
+ export function hasEmbeddedDocWorkbook(input) {
169
+ return containsAscii(input, 'Workbook') || containsUtf16Le(input, 'Workbook');
170
+ }
171
+ function containsAscii(input, text) {
172
+ const needle = [...text].map((char) => char.charCodeAt(0));
173
+ return indexOf(input, needle) >= 0;
174
+ }
175
+ function containsUtf16Le(input, text) {
176
+ const needle = [...text].flatMap((char) => [char.charCodeAt(0), 0]);
177
+ return indexOf(input, needle) >= 0;
178
+ }
52
179
  function failed(code) {
53
180
  return outcome({ status: 'failed', failure: { code, retryable: false, messageKey: 'diag.word.invalid-format' }, diagnostics: [] });
54
181
  }
@@ -56,6 +183,9 @@ function parseFib(word) {
56
183
  if (word.length < 64 || u16(word, 0) !== 0xa5ec)
57
184
  throw new Error('invalid FIB');
58
185
  const flags = u16(word, 10);
186
+ if ((flags & 0x0100) !== 0 || (flags & 0x8000) !== 0) {
187
+ throw new DocPasswordRequiredError();
188
+ }
59
189
  const tableStreamName = ((flags >> 9) & 1) ? '1Table' : '0Table';
60
190
  let offset = 32;
61
191
  const csw = u16(word, offset);
@@ -72,20 +202,58 @@ function parseFib(word) {
72
202
  const [fcClx, lcbClx] = pair(33);
73
203
  return { tableStreamName, ccpText, ccpHdd, fcClx, lcbClx };
74
204
  }
205
+ class DocPasswordRequiredError extends Error {
206
+ constructor() {
207
+ super('password required');
208
+ this.name = 'DocPasswordRequiredError';
209
+ }
210
+ }
211
+ function isEncryptedDocBinary(input) {
212
+ try {
213
+ const word = parseCfb(input).getStream('WordDocument');
214
+ if (!word || word.length < 12 || u16(word, 0) !== 0xa5ec)
215
+ return false;
216
+ const flags = u16(word, 10);
217
+ return (flags & 0x0100) !== 0 || (flags & 0x8000) !== 0;
218
+ }
219
+ catch {
220
+ return false;
221
+ }
222
+ }
75
223
  function parsePieceTable(table, fib) {
76
- if (fib.lcbClx <= 0 || fib.fcClx + fib.lcbClx > table.length)
77
- return scanPieceTables(table);
224
+ if (fib.lcbClx <= 0 || fib.fcClx + fib.lcbClx > table.length) {
225
+ return { pieces: scanPieceTables(table), recovered: true };
226
+ }
78
227
  const clx = table.subarray(fib.fcClx, fib.fcClx + fib.lcbClx);
79
228
  let offset = 0;
80
229
  while (offset < clx.length && clx[offset] === 1) {
81
230
  if (offset + 3 > clx.length)
82
- return [];
231
+ return { pieces: [], recovered: false };
83
232
  offset += 3 + u16(clx, offset + 1);
84
233
  }
85
- if (clx[offset] !== 2 || offset + 5 > clx.length)
86
- return scanPieceTables(table);
234
+ if (clx[offset] !== 2 || offset + 5 > clx.length) {
235
+ return { pieces: scanPieceTables(table), recovered: true };
236
+ }
87
237
  const length = u32(clx, offset + 1);
88
- return decodePlc(clx.subarray(offset + 5, offset + 5 + length));
238
+ const pieces = decodePlc(clx.subarray(offset + 5, offset + 5 + length));
239
+ if (pieces.length)
240
+ return { pieces, recovered: false };
241
+ const recovered = scanPieceTables(table);
242
+ return { pieces: recovered, recovered: recovered.length > 0 };
243
+ }
244
+ function hasRecoveredPieceTable(input) {
245
+ try {
246
+ const cfb = parseCfb(input);
247
+ const word = cfb.getStream('WordDocument');
248
+ if (!word)
249
+ return false;
250
+ const fib = parseFib(word);
251
+ const table = cfb.getStream(fib.tableStreamName);
252
+ return table ? parsePieceTable(table, fib).recovered : false;
253
+ }
254
+ catch {
255
+ return false;
256
+ }
89
257
  }
90
258
  function scanPieceTables(table) {
91
259
  let best = [];
@@ -18,4 +18,18 @@ export interface ParquetParseOptions {
18
18
  rowStart?: number;
19
19
  signal?: AbortSignal;
20
20
  }
21
+ /**
22
+ * Random-access source: hyparquet's `AsyncBuffer` shape. The decoder consumes it
23
+ * natively, so a lazy host (Blob range reads, Node `fs.open/read`) can supply one
24
+ * directly and only the footer + requested row-group pages are ever read
25
+ * (DESIGN.md 메모리 규율 — 랜덤액세스 입력). {@link parseParquet} remains the
26
+ * compatibility entry point for callers that already hold the whole file.
27
+ */
28
+ export interface ParquetSource {
29
+ byteLength: number;
30
+ slice(start: number, end?: number): ArrayBuffer | Promise<ArrayBuffer>;
31
+ }
32
+ /** Parses from a fully in-memory buffer. Wraps the bytes into a {@link ParquetSource}. */
21
33
  export declare function parseParquet(data: Uint8Array, options?: ParquetParseOptions): Promise<ParquetDocument>;
34
+ /** Parses from a random-access source, materializing only the footer + preview pages. */
35
+ export declare function parseParquetSource(source: ParquetSource, options?: ParquetParseOptions): Promise<ParquetDocument>;
@@ -2,23 +2,27 @@ import { parquetMetadataAsync, parquetReadObjects, parquetSchema } from 'hyparqu
2
2
  import { compressors } from 'hyparquet-compressors';
3
3
  export const PARQUET_PREVIEW_BYTES = 50 * 1024 * 1024;
4
4
  export const PARQUET_PREVIEW_ROWS = 10_000;
5
+ /** Parses from a fully in-memory buffer. Wraps the bytes into a {@link ParquetSource}. */
5
6
  export async function parseParquet(data, options = {}) {
6
- if (options.signal?.aborted)
7
- throw new DOMException('Parsing was cancelled.', 'AbortError');
8
- const file = {
7
+ const source = {
9
8
  byteLength: data.byteLength,
10
- async slice(start, end) {
11
- if (options.signal?.aborted)
12
- throw new DOMException('Parsing was cancelled.', 'AbortError');
9
+ slice(start, end) {
13
10
  const part = data.subarray(start, end);
14
11
  return part.buffer.slice(part.byteOffset, part.byteOffset + part.byteLength);
15
12
  }
16
13
  };
14
+ return parseParquetSource(source, options);
15
+ }
16
+ /** Parses from a random-access source, materializing only the footer + preview pages. */
17
+ export async function parseParquetSource(source, options = {}) {
18
+ if (options.signal?.aborted)
19
+ throw new DOMException('Parsing was cancelled.', 'AbortError');
20
+ const file = options.signal ? withAbort(source, options.signal) : source;
17
21
  const metadata = await parquetMetadataAsync(file);
18
22
  const schema = parquetSchema(metadata);
19
23
  const totalRows = Number(metadata.num_rows ?? 0);
20
24
  const rowStart = Math.max(0, options.rowStart ?? 0);
21
- const shouldChunk = data.byteLength >= (options.maxPreviewBytes ?? PARQUET_PREVIEW_BYTES);
25
+ const shouldChunk = source.byteLength >= (options.maxPreviewBytes ?? PARQUET_PREVIEW_BYTES);
22
26
  const readOptions = { file, metadata, compressors };
23
27
  if (shouldChunk) {
24
28
  readOptions.rowStart = rowStart;
@@ -33,7 +37,18 @@ export async function parseParquet(data, options = {}) {
33
37
  const types = collectColumnTypes(schema);
34
38
  const rows = objects.map((row) => headers.map((header) => convertValue(row[header], types.get(header))));
35
39
  return { headers, rows, schema: convertValue(schema), totalRows, loadedRows: rows.length,
36
- fileSizeBytes: data.byteLength, isLimited: shouldChunk && rowStart + rows.length < totalRows };
40
+ fileSizeBytes: source.byteLength, isLimited: shouldChunk && rowStart + rows.length < totalRows };
41
+ }
42
+ /** Wraps a source so each range read observes cancellation (parity with the buffer path). */
43
+ function withAbort(source, signal) {
44
+ return {
45
+ byteLength: source.byteLength,
46
+ slice(start, end) {
47
+ if (signal.aborted)
48
+ throw new DOMException('Parsing was cancelled.', 'AbortError');
49
+ return source.slice(start, end);
50
+ }
51
+ };
37
52
  }
38
53
  function convertValue(value, type) {
39
54
  if (value == null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number')
@@ -9,9 +9,11 @@ export interface PptxZipReader {
9
9
  }
10
10
  export interface PptxParserDeps {
11
11
  openZip(data: Uint8Array, options?: ParseOptions): Promise<PptxZipReader>;
12
+ renderMetafile?(input: Uint8Array, type: 'wmf' | 'emf', signal?: AbortSignal): Promise<string | Uint8Array | undefined>;
12
13
  }
13
14
  export { openPptxZip } from './zip-reader.js';
14
15
  import { parsePptxVscode } from './vscode.js';
15
16
  export { parsePptxVscode };
17
+ export type { PptxVscodeParserDeps } from './vscode.js';
16
18
  export declare function parsePptxLegacy(input: Uint8Array, deps: PptxParserDeps, options?: ParseOptions): Promise<ParseOutcome<SlideDeck>>;
17
19
  export declare function parsePptx(input: Uint8Array, deps: PptxParserDeps, options?: ParseOptions): Promise<ParseOutcome<SlideDeck>>;
@@ -87,5 +87,5 @@ catch {
87
87
  finally {
88
88
  await zip?.close?.();
89
89
  } }
90
- export async function parsePptx(input, deps, options = {}) { const full = await parsePptxVscode(input, options); if (full.result.status !== 'failed' || full.result.failure.code !== 'invalid-format')
90
+ export async function parsePptx(input, deps, options = {}) { const full = await parsePptxVscode(input, options, { renderMetafile: deps.renderMetafile }); if (full.result.status !== 'failed' || full.result.failure.code !== 'invalid-format')
91
91
  return full; return parsePptxLegacy(input, deps, options); }
@@ -0,0 +1,26 @@
1
+ import type JSZip from 'jszip';
2
+ import { type LimitViolation, type ParseOptions, type ResourceLimits } from '../types.js';
3
+ export declare const PPTX_DEFAULT_LIMITS: Required<Pick<ResourceLimits, 'maxInputBytes' | 'maxEntries' | 'maxDecompressedBytes' | 'maxParseMillis'>>;
4
+ export declare class PptxLimitError extends Error {
5
+ readonly violation: LimitViolation;
6
+ readonly location?: string | undefined;
7
+ constructor(violation: LimitViolation, location?: string | undefined);
8
+ }
9
+ export declare function pptxLimits(options: ParseOptions): ResourceLimits;
10
+ export declare function preflightPptxZip(input: Uint8Array, limits: ResourceLimits): void;
11
+ export declare class PptxParseGuard {
12
+ readonly options: ParseOptions;
13
+ readonly limits: ResourceLimits;
14
+ private readonly tracker;
15
+ constructor(options: ParseOptions, limits?: ResourceLimits);
16
+ checkpoint(location?: string): void;
17
+ addDecompressedBytes(bytes: number, location?: string): void;
18
+ }
19
+ /**
20
+ * Replaces JSZip's per-entry `async()` accumulator with a bounded streaming
21
+ * implementation. This is intentionally installed after central-directory
22
+ * preflight: declared limits reject ordinary ZIP bombs before inflate, while
23
+ * this wrapper catches lying headers and cancellation between 16 KiB chunks.
24
+ */
25
+ export declare function installBoundedZipReaders(zip: JSZip, guard: PptxParseGuard): void;
26
+ export declare function limitDiagnosticArgs(error: PptxLimitError): Record<string, string | number>;
@@ -0,0 +1,154 @@
1
+ import { declaredZipEntryCount, declaredZipUncompressedBytes } from '../zip-scan.js';
2
+ import { DEFAULT_LIMITS, LimitTracker } from '../types.js';
3
+ export const PPTX_DEFAULT_LIMITS = {
4
+ maxInputBytes: 50 * 1024 * 1024,
5
+ maxEntries: Math.min(DEFAULT_LIMITS.maxEntries, 100_000),
6
+ maxDecompressedBytes: 256 * 1024 * 1024,
7
+ maxParseMillis: DEFAULT_LIMITS.maxParseMillis
8
+ };
9
+ export class PptxLimitError extends Error {
10
+ violation;
11
+ location;
12
+ constructor(violation, location) {
13
+ super(`PPTX parsing stopped: ${violation.kind}`);
14
+ this.violation = violation;
15
+ this.location = location;
16
+ this.name = 'PptxLimitError';
17
+ }
18
+ }
19
+ export function pptxLimits(options) {
20
+ return { ...PPTX_DEFAULT_LIMITS, ...options.limits };
21
+ }
22
+ export function preflightPptxZip(input, limits) {
23
+ const count = declaredZipEntryCount(input);
24
+ if (count !== null &&
25
+ Number.isFinite(count) &&
26
+ limits.maxEntries !== undefined &&
27
+ count > limits.maxEntries) {
28
+ throw new PptxLimitError({ kind: 'entries', count });
29
+ }
30
+ const decompressed = declaredZipUncompressedBytes(input);
31
+ if (decompressed !== null &&
32
+ Number.isFinite(decompressed) &&
33
+ limits.maxDecompressedBytes !== undefined &&
34
+ decompressed > limits.maxDecompressedBytes) {
35
+ throw new PptxLimitError({ kind: 'decompressed', bytes: decompressed });
36
+ }
37
+ }
38
+ export class PptxParseGuard {
39
+ options;
40
+ limits;
41
+ tracker;
42
+ constructor(options, limits = pptxLimits(options)) {
43
+ this.options = options;
44
+ this.limits = limits;
45
+ this.tracker = new LimitTracker(limits, options.signal);
46
+ }
47
+ checkpoint(location) {
48
+ const violation = this.tracker.checkpoint();
49
+ if (violation)
50
+ throw new PptxLimitError(violation, location);
51
+ }
52
+ addDecompressedBytes(bytes, location) {
53
+ this.tracker.addDecompressedBytes(bytes);
54
+ this.checkpoint(location);
55
+ }
56
+ }
57
+ function bytesToBase64(bytes) {
58
+ let binary = '';
59
+ const chunkSize = 0x8000;
60
+ for (let i = 0; i < bytes.length; i += chunkSize) {
61
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
62
+ }
63
+ return btoa(binary);
64
+ }
65
+ function convertBytes(bytes, type) {
66
+ switch (type.toLowerCase()) {
67
+ case 'text':
68
+ case 'string':
69
+ return new TextDecoder().decode(bytes);
70
+ case 'base64':
71
+ return bytesToBase64(bytes);
72
+ case 'arraybuffer':
73
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
74
+ case 'array':
75
+ return Array.from(bytes);
76
+ case 'nodebuffer': {
77
+ const NodeBuffer = globalThis.Buffer;
78
+ return NodeBuffer ? NodeBuffer.from(bytes) : bytes;
79
+ }
80
+ default:
81
+ return bytes;
82
+ }
83
+ }
84
+ /**
85
+ * Replaces JSZip's per-entry `async()` accumulator with a bounded streaming
86
+ * implementation. This is intentionally installed after central-directory
87
+ * preflight: declared limits reject ordinary ZIP bombs before inflate, while
88
+ * this wrapper catches lying headers and cancellation between 16 KiB chunks.
89
+ */
90
+ export function installBoundedZipReaders(zip, guard) {
91
+ for (const [entryPath, entry] of Object.entries(zip.files)) {
92
+ if (entry.dir)
93
+ continue;
94
+ let cachedBytes;
95
+ entry.async = (async (type) => {
96
+ guard.checkpoint(entryPath);
97
+ if (cachedBytes)
98
+ return convertBytes(cachedBytes, type);
99
+ const helper = entry.internalStream('uint8array');
100
+ const chunks = [];
101
+ let total = 0;
102
+ const bytes = await new Promise((resolve, reject) => {
103
+ let settled = false;
104
+ const fail = (error) => {
105
+ if (settled)
106
+ return;
107
+ settled = true;
108
+ helper.pause();
109
+ reject(error);
110
+ };
111
+ helper
112
+ .on('data', (chunk) => {
113
+ if (settled)
114
+ return;
115
+ try {
116
+ guard.addDecompressedBytes(chunk.byteLength, entryPath);
117
+ chunks.push(chunk);
118
+ total += chunk.byteLength;
119
+ }
120
+ catch (error) {
121
+ fail(error);
122
+ }
123
+ })
124
+ .on('error', fail)
125
+ .on('end', () => {
126
+ if (settled)
127
+ return;
128
+ settled = true;
129
+ const output = new Uint8Array(total);
130
+ let offset = 0;
131
+ for (const chunk of chunks) {
132
+ output.set(chunk, offset);
133
+ offset += chunk.byteLength;
134
+ }
135
+ resolve(output);
136
+ })
137
+ .resume();
138
+ });
139
+ guard.checkpoint(entryPath);
140
+ cachedBytes = bytes;
141
+ return convertBytes(bytes, type);
142
+ });
143
+ }
144
+ }
145
+ export function limitDiagnosticArgs(error) {
146
+ const { violation } = error;
147
+ if (violation.kind === 'entries')
148
+ return { kind: violation.kind, count: violation.count };
149
+ if (violation.kind === 'decompressed')
150
+ return { kind: violation.kind, bytes: violation.bytes };
151
+ if (violation.kind === 'time')
152
+ return { kind: violation.kind, elapsedMillis: violation.elapsedMillis };
153
+ return { kind: violation.kind };
154
+ }