omni-viewer-core 0.7.0 → 0.9.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/README.md +32 -0
- package/dist/i18n/catalog.de.js +6 -1
- package/dist/i18n/catalog.en.js +30 -6
- package/dist/i18n/catalog.fr.js +6 -1
- package/dist/i18n/catalog.it.js +6 -1
- package/dist/i18n/catalog.ja.js +30 -6
- package/dist/i18n/catalog.ko.js +30 -6
- package/dist/i18n/catalog.th.js +6 -1
- package/dist/i18n/catalog.zh-cn.js +29 -5
- package/dist/parsers/archive/index.d.ts +2 -0
- package/dist/parsers/archive/index.js +6 -5
- package/dist/parsers/archive/model.d.ts +36 -11
- package/dist/parsers/archive/worker.d.ts +8 -1
- package/dist/parsers/doc-binary/index.d.ts +23 -1
- package/dist/parsers/doc-binary/index.js +175 -7
- package/dist/parsers/parquet/index.d.ts +14 -0
- package/dist/parsers/parquet/index.js +23 -8
- package/dist/parsers/pptx/index.d.ts +2 -0
- package/dist/parsers/pptx/index.js +1 -1
- package/dist/parsers/pptx/limits.d.ts +26 -0
- package/dist/parsers/pptx/limits.js +154 -0
- package/dist/parsers/pptx/vscode-parser.d.ts +49 -3
- package/dist/parsers/pptx/vscode-parser.js +517 -63
- package/dist/parsers/pptx/vscode.d.ts +6 -2
- package/dist/parsers/pptx/vscode.js +95 -10
- package/dist/parsers/slide-model.d.ts +89 -6
- package/dist/parsers/types.d.ts +2 -0
- package/dist/parsers/zip-scan.d.ts +7 -0
- package/dist/parsers/zip-scan.js +22 -0
- package/dist/registry/index.js +2 -2
- package/dist/styles/archive.css +3 -1
- package/dist/styles/hdf5.css +1 -1
- package/dist/styles/jsonl.css +1 -1
- package/dist/styles/markdown.css +2 -2
- package/dist/styles/mermaid.css +10 -1
- package/dist/styles/parquet.css +1 -1
- package/dist/styles/pdf.css +6 -1
- package/dist/styles/plantuml.css +10 -1
- package/dist/styles/proto.css +1 -1
- package/dist/styles/safetensors.css +1 -1
- package/dist/styles/toml.css +1 -1
- package/dist/styles/word.css +2 -2
- package/dist/styles/yaml.css +1 -1
- package/dist/viewers/archive/controller.d.ts +4 -0
- package/dist/viewers/archive/controller.js +22 -0
- package/dist/viewers/archive/index.d.ts +38 -1
- package/dist/viewers/archive/index.js +176 -41
- package/dist/viewers/archive/styles.d.ts +1 -1
- package/dist/viewers/archive/styles.js +3 -1
- package/dist/viewers/diagram-controller.d.ts +42 -0
- package/dist/viewers/diagram-controller.js +47 -0
- package/dist/viewers/diagram-styles.d.ts +1 -0
- package/dist/viewers/diagram-styles.js +11 -0
- package/dist/viewers/diagram.d.ts +16 -6
- package/dist/viewers/diagram.js +321 -75
- package/dist/viewers/hdf5/styles.d.ts +1 -1
- package/dist/viewers/hdf5/styles.js +1 -1
- package/dist/viewers/hwp/self-loading.d.ts +2 -0
- package/dist/viewers/hwp/self-loading.js +6 -0
- package/dist/viewers/jsonl/controller.d.ts +2 -0
- package/dist/viewers/jsonl/controller.js +4 -0
- package/dist/viewers/jsonl/index.d.ts +1 -0
- package/dist/viewers/jsonl/index.js +97 -51
- package/dist/viewers/jsonl/popup.d.ts +16 -0
- package/dist/viewers/jsonl/popup.js +20 -0
- package/dist/viewers/jsonl/styles.d.ts +1 -1
- package/dist/viewers/jsonl/styles.js +1 -1
- package/dist/viewers/markdown/index.js +79 -14
- package/dist/viewers/markdown/styles.d.ts +1 -1
- package/dist/viewers/markdown/styles.js +2 -2
- package/dist/viewers/mermaid/index.d.ts +3 -4
- package/dist/viewers/mermaid/index.js +1 -1
- package/dist/viewers/mermaid/self-loading.js +4 -1
- package/dist/viewers/parquet/index.d.ts +9 -1
- package/dist/viewers/parquet/index.js +20 -10
- package/dist/viewers/parquet/styles.d.ts +1 -1
- package/dist/viewers/parquet/styles.js +1 -1
- package/dist/viewers/pdf/controller.d.ts +9 -0
- package/dist/viewers/pdf/controller.js +17 -0
- package/dist/viewers/pdf/index.js +90 -22
- package/dist/viewers/pdf/styles.d.ts +1 -1
- package/dist/viewers/pdf/styles.js +6 -1
- package/dist/viewers/plantuml/index.d.ts +3 -4
- package/dist/viewers/plantuml/index.js +1 -1
- package/dist/viewers/plantuml/self-loading.js +1 -1
- package/dist/viewers/ppt/chart.js +58 -18
- package/dist/viewers/ppt/index.d.ts +33 -2
- package/dist/viewers/ppt/index.js +245 -61
- package/dist/viewers/ppt/render.js +85 -40
- package/dist/viewers/ppt/self-loading.d.ts +2 -0
- package/dist/viewers/ppt/self-loading.js +4 -1
- package/dist/viewers/proto/index.js +77 -2
- package/dist/viewers/proto/styles.d.ts +1 -1
- package/dist/viewers/proto/styles.js +1 -1
- package/dist/viewers/safetensors/styles.d.ts +1 -1
- package/dist/viewers/safetensors/styles.js +1 -1
- package/dist/viewers/structured-styles.d.ts +1 -1
- package/dist/viewers/structured-styles.js +1 -1
- package/dist/viewers/structured.js +28 -1
- package/dist/viewers/word/docx-preprocess.d.ts +18 -2
- package/dist/viewers/word/docx-preprocess.js +248 -47
- package/dist/viewers/word/index.d.ts +108 -1
- package/dist/viewers/word/index.js +772 -85
- package/dist/viewers/word/self-loading.d.ts +5 -1
- package/dist/viewers/word/self-loading.js +17 -4
- package/dist/viewers/word/styles.d.ts +1 -1
- package/dist/viewers/word/styles.js +2 -2
- package/package.json +6 -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;
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
throw new DOMException('Parsing was cancelled.', 'AbortError');
|
|
8
|
-
const file = {
|
|
7
|
+
const source = {
|
|
9
8
|
byteLength: data.byteLength,
|
|
10
|
-
|
|
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 =
|
|
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:
|
|
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
|
+
}
|