yom-format 0.1.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/README.md +86 -0
- package/dist/cli.d.ts +30 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +344 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/avif.d.ts +3 -0
- package/dist/pipeline/avif.d.ts.map +1 -0
- package/dist/pipeline/avif.js +30 -0
- package/dist/pipeline/avif.js.map +1 -0
- package/dist/pipeline/convert.d.ts +28 -0
- package/dist/pipeline/convert.d.ts.map +1 -0
- package/dist/pipeline/convert.js +111 -0
- package/dist/pipeline/convert.js.map +1 -0
- package/dist/pipeline/index.d.ts +17 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +17 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/ocrLayoutBuilder.d.ts +10 -0
- package/dist/pipeline/ocrLayoutBuilder.d.ts.map +1 -0
- package/dist/pipeline/ocrLayoutBuilder.js +62 -0
- package/dist/pipeline/ocrLayoutBuilder.js.map +1 -0
- package/dist/pipeline/pdfDocument.d.ts +12 -0
- package/dist/pipeline/pdfDocument.d.ts.map +1 -0
- package/dist/pipeline/pdfDocument.js +76 -0
- package/dist/pipeline/pdfDocument.js.map +1 -0
- package/dist/pipeline/renderPage.d.ts +10 -0
- package/dist/pipeline/renderPage.d.ts.map +1 -0
- package/dist/pipeline/renderPage.js +36 -0
- package/dist/pipeline/renderPage.js.map +1 -0
- package/dist/pipeline/textExtract.d.ts +29 -0
- package/dist/pipeline/textExtract.d.ts.map +1 -0
- package/dist/pipeline/textExtract.js +48 -0
- package/dist/pipeline/textExtract.js.map +1 -0
- package/dist/yom/blobLimits.d.ts +15 -0
- package/dist/yom/blobLimits.d.ts.map +1 -0
- package/dist/yom/blobLimits.js +25 -0
- package/dist/yom/blobLimits.js.map +1 -0
- package/dist/yom/byteSource.d.ts +56 -0
- package/dist/yom/byteSource.d.ts.map +1 -0
- package/dist/yom/byteSource.js +111 -0
- package/dist/yom/byteSource.js.map +1 -0
- package/dist/yom/bytes.d.ts +2 -0
- package/dist/yom/bytes.d.ts.map +1 -0
- package/dist/yom/bytes.js +13 -0
- package/dist/yom/bytes.js.map +1 -0
- package/dist/yom/constants.d.ts +34 -0
- package/dist/yom/constants.d.ts.map +1 -0
- package/dist/yom/constants.js +82 -0
- package/dist/yom/constants.js.map +1 -0
- package/dist/yom/errors.d.ts +44 -0
- package/dist/yom/errors.d.ts.map +1 -0
- package/dist/yom/errors.js +44 -0
- package/dist/yom/errors.js.map +1 -0
- package/dist/yom/flatbufferVerify.d.ts +21 -0
- package/dist/yom/flatbufferVerify.d.ts.map +1 -0
- package/dist/yom/flatbufferVerify.js +255 -0
- package/dist/yom/flatbufferVerify.js.map +1 -0
- package/dist/yom/generated/yomu/yom/blob-kind.d.ts +9 -0
- package/dist/yom/generated/yomu/yom/blob-kind.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/blob-kind.js +12 -0
- package/dist/yom/generated/yomu/yom/blob-kind.js.map +1 -0
- package/dist/yom/generated/yomu/yom/blob-ref.d.ts +45 -0
- package/dist/yom/generated/yomu/yom/blob-ref.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/blob-ref.js +141 -0
- package/dist/yom/generated/yomu/yom/blob-ref.js.map +1 -0
- package/dist/yom/generated/yomu/yom/book-metadata.d.ts +32 -0
- package/dist/yom/generated/yomu/yom/book-metadata.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/book-metadata.js +89 -0
- package/dist/yom/generated/yomu/yom/book-metadata.js.map +1 -0
- package/dist/yom/generated/yomu/yom/compression.d.ts +5 -0
- package/dist/yom/generated/yomu/yom/compression.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/compression.js +8 -0
- package/dist/yom/generated/yomu/yom/compression.js.map +1 -0
- package/dist/yom/generated/yomu/yom/main-index.d.ts +63 -0
- package/dist/yom/generated/yomu/yom/main-index.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/main-index.js +208 -0
- package/dist/yom/generated/yomu/yom/main-index.js.map +1 -0
- package/dist/yom/generated/yomu/yom/ocrblock.d.ts +32 -0
- package/dist/yom/generated/yomu/yom/ocrblock.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/ocrblock.js +84 -0
- package/dist/yom/generated/yomu/yom/ocrblock.js.map +1 -0
- package/dist/yom/generated/yomu/yom/ocrlayout.d.ts +27 -0
- package/dist/yom/generated/yomu/yom/ocrlayout.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/ocrlayout.js +76 -0
- package/dist/yom/generated/yomu/yom/ocrlayout.js.map +1 -0
- package/dist/yom/generated/yomu/yom/ocrline.d.ts +35 -0
- package/dist/yom/generated/yomu/yom/ocrline.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/ocrline.js +95 -0
- package/dist/yom/generated/yomu/yom/ocrline.js.map +1 -0
- package/dist/yom/generated/yomu/yom/ocrpage.d.ts +46 -0
- package/dist/yom/generated/yomu/yom/ocrpage.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/ocrpage.js +147 -0
- package/dist/yom/generated/yomu/yom/ocrpage.js.map +1 -0
- package/dist/yom/generated/yomu/yom/ocrtoken.d.ts +44 -0
- package/dist/yom/generated/yomu/yom/ocrtoken.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/ocrtoken.js +128 -0
- package/dist/yom/generated/yomu/yom/ocrtoken.js.map +1 -0
- package/dist/yom/generated/yomu/yom/outline-entry.d.ts +31 -0
- package/dist/yom/generated/yomu/yom/outline-entry.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/outline-entry.js +92 -0
- package/dist/yom/generated/yomu/yom/outline-entry.js.map +1 -0
- package/dist/yom/generated/yomu/yom/page-entry.d.ts +95 -0
- package/dist/yom/generated/yomu/yom/page-entry.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/page-entry.js +288 -0
- package/dist/yom/generated/yomu/yom/page-entry.js.map +1 -0
- package/dist/yom/generated/yomu/yom/pdfexport-metadata.d.ts +32 -0
- package/dist/yom/generated/yomu/yom/pdfexport-metadata.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/pdfexport-metadata.js +96 -0
- package/dist/yom/generated/yomu/yom/pdfexport-metadata.js.map +1 -0
- package/dist/yom/generated/yomu/yom/reading-direction.d.ts +5 -0
- package/dist/yom/generated/yomu/yom/reading-direction.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/reading-direction.js +8 -0
- package/dist/yom/generated/yomu/yom/reading-direction.js.map +1 -0
- package/dist/yom/generated/yomu/yom/search-term-ref.d.ts +34 -0
- package/dist/yom/generated/yomu/yom/search-term-ref.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom/search-term-ref.js +99 -0
- package/dist/yom/generated/yomu/yom/search-term-ref.js.map +1 -0
- package/dist/yom/generated/yomu/yom.d.ts +6 -0
- package/dist/yom/generated/yomu/yom.d.ts.map +1 -0
- package/dist/yom/generated/yomu/yom.js +8 -0
- package/dist/yom/generated/yomu/yom.js.map +1 -0
- package/dist/yom/hash.d.ts +4 -0
- package/dist/yom/hash.d.ts.map +1 -0
- package/dist/yom/hash.js +21 -0
- package/dist/yom/hash.js.map +1 -0
- package/dist/yom/header.d.ts +44 -0
- package/dist/yom/header.d.ts.map +1 -0
- package/dist/yom/header.js +115 -0
- package/dist/yom/header.js.map +1 -0
- package/dist/yom/reader.d.ts +57 -0
- package/dist/yom/reader.d.ts.map +1 -0
- package/dist/yom/reader.js +141 -0
- package/dist/yom/reader.js.map +1 -0
- package/dist/yom/searchIndex.d.ts +41 -0
- package/dist/yom/searchIndex.d.ts.map +1 -0
- package/dist/yom/searchIndex.js +133 -0
- package/dist/yom/searchIndex.js.map +1 -0
- package/dist/yom/textNormalizer.d.ts +19 -0
- package/dist/yom/textNormalizer.d.ts.map +1 -0
- package/dist/yom/textNormalizer.js +77 -0
- package/dist/yom/textNormalizer.js.map +1 -0
- package/dist/yom/validator.d.ts +14 -0
- package/dist/yom/validator.d.ts.map +1 -0
- package/dist/yom/validator.js +197 -0
- package/dist/yom/validator.js.map +1 -0
- package/dist/yom/writer.d.ts +76 -0
- package/dist/yom/writer.d.ts.map +1 -0
- package/dist/yom/writer.js +154 -0
- package/dist/yom/writer.js.map +1 -0
- package/dist/yom/zstd.d.ts +14 -0
- package/dist/yom/zstd.d.ts.map +1 -0
- package/dist/yom/zstd.js +43 -0
- package/dist/yom/zstd.js.map +1 -0
- package/package.json +65 -0
- package/src/cli.ts +372 -0
- package/src/index.ts +109 -0
- package/src/pipeline/avif.ts +33 -0
- package/src/pipeline/convert.ts +148 -0
- package/src/pipeline/index.ts +28 -0
- package/src/pipeline/ocrLayoutBuilder.ts +73 -0
- package/src/pipeline/pdfDocument.ts +86 -0
- package/src/pipeline/renderPage.ts +48 -0
- package/src/pipeline/textExtract.ts +64 -0
- package/src/worker/convert.worker.ts +54 -0
- package/src/yom/blobLimits.ts +30 -0
- package/src/yom/byteSource.ts +144 -0
- package/src/yom/bytes.ts +11 -0
- package/src/yom/constants.ts +89 -0
- package/src/yom/errors.ts +43 -0
- package/src/yom/flatbufferVerify.ts +298 -0
- package/src/yom/generated/yomu/yom/blob-kind.ts +12 -0
- package/src/yom/generated/yomu/yom/blob-ref.ts +184 -0
- package/src/yom/generated/yomu/yom/book-metadata.ts +121 -0
- package/src/yom/generated/yomu/yom/compression.ts +8 -0
- package/src/yom/generated/yomu/yom/main-index.ts +257 -0
- package/src/yom/generated/yomu/yom/ocrblock.ts +117 -0
- package/src/yom/generated/yomu/yom/ocrlayout.ts +102 -0
- package/src/yom/generated/yomu/yom/ocrline.ts +131 -0
- package/src/yom/generated/yomu/yom/ocrpage.ts +191 -0
- package/src/yom/generated/yomu/yom/ocrtoken.ts +173 -0
- package/src/yom/generated/yomu/yom/outline-entry.ts +124 -0
- package/src/yom/generated/yomu/yom/page-entry.ts +388 -0
- package/src/yom/generated/yomu/yom/pdfexport-metadata.ts +128 -0
- package/src/yom/generated/yomu/yom/reading-direction.ts +8 -0
- package/src/yom/generated/yomu/yom/search-term-ref.ts +134 -0
- package/src/yom/generated/yomu/yom.ts +9 -0
- package/src/yom/hash.ts +20 -0
- package/src/yom/header.ts +132 -0
- package/src/yom/reader.ts +163 -0
- package/src/yom/searchIndex.ts +178 -0
- package/src/yom/textNormalizer.ts +92 -0
- package/src/yom/validator.ts +245 -0
- package/src/yom/writer.ts +337 -0
- package/src/yom/zstd.ts +47 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { RangeNotSupportedError, YOMError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gap 1: the byte-source abstraction every reader read goes through, so the
|
|
5
|
+
* same `YOMPackfileReader` can serve a fully in-memory `.yom` or one streamed
|
|
6
|
+
* over HTTP `Range` requests without ever loading the whole file.
|
|
7
|
+
*/
|
|
8
|
+
export interface ByteSource {
|
|
9
|
+
/** Total file length in bytes. */
|
|
10
|
+
readonly size: number;
|
|
11
|
+
/** Reads exactly `[offset, offset+length)`; rejects if it can't. */
|
|
12
|
+
read(offset: number, length: number): Promise<Uint8Array>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Raised when a `ByteSource.read` cannot return the requested extent. */
|
|
16
|
+
export class ByteSourceError extends YOMError {}
|
|
17
|
+
|
|
18
|
+
/** A `ByteSource` over an already-resident `Uint8Array` (zero-copy slices). */
|
|
19
|
+
export class MemoryByteSource implements ByteSource {
|
|
20
|
+
readonly size: number;
|
|
21
|
+
private readonly bytes: Uint8Array;
|
|
22
|
+
|
|
23
|
+
constructor(bytes: Uint8Array) {
|
|
24
|
+
this.bytes = bytes;
|
|
25
|
+
this.size = bytes.length;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
read(offset: number, length: number): Promise<Uint8Array> {
|
|
29
|
+
if (offset < 0 || length < 0 || offset + length > this.bytes.length) {
|
|
30
|
+
return Promise.reject(new ByteSourceError(`read [${offset}, ${offset + length}) out of bounds (size ${this.size})`));
|
|
31
|
+
}
|
|
32
|
+
return Promise.resolve(this.bytes.subarray(offset, offset + length));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
37
|
+
|
|
38
|
+
export interface HttpRangeOptions {
|
|
39
|
+
/** Override the global `fetch` (for tests / custom transports). */
|
|
40
|
+
fetch?: FetchLike;
|
|
41
|
+
/** Extra request headers (auth, etc.). */
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
/** Skip the size probe if the caller already knows `Content-Length`. */
|
|
44
|
+
size?: number;
|
|
45
|
+
/** Abort in-flight requests. */
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A `ByteSource` backed by HTTP `Range` requests. `create()` learns the total
|
|
51
|
+
* size and confirms range support up front (one `Range: bytes=0-` probe, or a
|
|
52
|
+
* caller-supplied `size`); each `read()` issues `GET Range: bytes=a-b`,
|
|
53
|
+
* validates the `206` + `Content-Range`/length, and returns the slice.
|
|
54
|
+
*
|
|
55
|
+
* The server must answer `206 Partial Content` to ranged requests; a plain
|
|
56
|
+
* `200 OK` means ranges are unsupported and `create()` throws
|
|
57
|
+
* `RangeNotSupportedError`. For cross-origin use the server must expose
|
|
58
|
+
* `Content-Range`, `Accept-Ranges`, and `Content-Length` via CORS.
|
|
59
|
+
*/
|
|
60
|
+
export class HttpRangeByteSource implements ByteSource {
|
|
61
|
+
readonly size: number;
|
|
62
|
+
private readonly url: string;
|
|
63
|
+
private readonly fetchFn: FetchLike;
|
|
64
|
+
private readonly headers: Record<string, string>;
|
|
65
|
+
private readonly signal?: AbortSignal;
|
|
66
|
+
|
|
67
|
+
private constructor(url: string, size: number, opts: HttpRangeOptions) {
|
|
68
|
+
this.url = url;
|
|
69
|
+
this.size = size;
|
|
70
|
+
this.fetchFn = opts.fetch ?? ((input, init) => fetch(input, init));
|
|
71
|
+
this.headers = opts.headers ?? {};
|
|
72
|
+
this.signal = opts.signal;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static async create(url: string, opts: HttpRangeOptions = {}): Promise<HttpRangeByteSource> {
|
|
76
|
+
const fetchFn: FetchLike = opts.fetch ?? ((input, init) => fetch(input, init));
|
|
77
|
+
const headers = opts.headers ?? {};
|
|
78
|
+
|
|
79
|
+
if (typeof opts.size === "number") {
|
|
80
|
+
return new HttpRangeByteSource(url, opts.size, opts);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Probe with an open-ended range: a range-capable server answers 206 with
|
|
84
|
+
// a `Content-Range: bytes 0-.../TOTAL` from which we learn the size.
|
|
85
|
+
const res = await fetchFn(url, {
|
|
86
|
+
method: "GET",
|
|
87
|
+
headers: { ...headers, Range: "bytes=0-0" },
|
|
88
|
+
signal: opts.signal,
|
|
89
|
+
});
|
|
90
|
+
if (res.status === 200) {
|
|
91
|
+
// Server ignored the range and sent the whole body.
|
|
92
|
+
throw new RangeNotSupportedError(`server does not support range requests (status 200) for ${url}`);
|
|
93
|
+
}
|
|
94
|
+
if (res.status !== 206) {
|
|
95
|
+
throw new ByteSourceError(`unexpected status ${res.status} probing ${url}`);
|
|
96
|
+
}
|
|
97
|
+
const total = parseContentRangeTotal(res.headers.get("Content-Range"));
|
|
98
|
+
if (total === null) {
|
|
99
|
+
const len = res.headers.get("Content-Length");
|
|
100
|
+
throw new ByteSourceError(
|
|
101
|
+
`range probe missing Content-Range total for ${url} (Content-Length ${len ?? "?"})`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
// Drain the tiny probe body to release the connection.
|
|
105
|
+
await res.arrayBuffer().catch(() => undefined);
|
|
106
|
+
return new HttpRangeByteSource(url, total, opts);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async read(offset: number, length: number): Promise<Uint8Array> {
|
|
110
|
+
if (offset < 0 || length < 0 || offset + length > this.size) {
|
|
111
|
+
throw new ByteSourceError(`read [${offset}, ${offset + length}) out of bounds (size ${this.size})`);
|
|
112
|
+
}
|
|
113
|
+
if (length === 0) return new Uint8Array(0);
|
|
114
|
+
|
|
115
|
+
const end = offset + length - 1; // HTTP ranges are inclusive
|
|
116
|
+
const res = await this.fetchFn(this.url, {
|
|
117
|
+
method: "GET",
|
|
118
|
+
headers: { ...this.headers, Range: `bytes=${offset}-${end}` },
|
|
119
|
+
signal: this.signal,
|
|
120
|
+
});
|
|
121
|
+
if (res.status === 200) {
|
|
122
|
+
throw new RangeNotSupportedError(`server ignored range request (status 200) for ${this.url}`);
|
|
123
|
+
}
|
|
124
|
+
if (res.status !== 206) {
|
|
125
|
+
throw new ByteSourceError(`unexpected status ${res.status} reading ${this.url}`);
|
|
126
|
+
}
|
|
127
|
+
const body = new Uint8Array(await res.arrayBuffer());
|
|
128
|
+
if (body.length !== length) {
|
|
129
|
+
throw new ByteSourceError(`range read returned ${body.length} bytes, expected ${length}`);
|
|
130
|
+
}
|
|
131
|
+
return body;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Parses the `TOTAL` out of a `Content-Range: bytes A-B/TOTAL` header. */
|
|
136
|
+
function parseContentRangeTotal(header: string | null): number | null {
|
|
137
|
+
if (!header) return null;
|
|
138
|
+
const slash = header.lastIndexOf("/");
|
|
139
|
+
if (slash < 0) return null;
|
|
140
|
+
const total = header.slice(slash + 1).trim();
|
|
141
|
+
if (total === "" || total === "*") return null;
|
|
142
|
+
const parsed = Number(total);
|
|
143
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
|
|
144
|
+
}
|
package/src/yom/bytes.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function concatBytes(chunks: Uint8Array[]): Uint8Array {
|
|
2
|
+
let total = 0;
|
|
3
|
+
for (const chunk of chunks) total += chunk.length;
|
|
4
|
+
const out = new Uint8Array(total);
|
|
5
|
+
let offset = 0;
|
|
6
|
+
for (const chunk of chunks) {
|
|
7
|
+
out.set(chunk, offset);
|
|
8
|
+
offset += chunk.length;
|
|
9
|
+
}
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { BlobKind } from "./generated/yomu/yom/blob-kind.js";
|
|
2
|
+
import { Compression } from "./generated/yomu/yom/compression.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* YOM v1 format constants shared by the writer, reader, and validator.
|
|
6
|
+
* Mirrors the Swift implementation in `swift/Sources/YOMFormat/YOMConstants.swift`
|
|
7
|
+
* field-for-field so Web- and native-generated `.yom` packages agree on
|
|
8
|
+
* layout. See docs/format-spec.md for rationale.
|
|
9
|
+
*/
|
|
10
|
+
export const YOM_FILE_EXTENSION = "yom";
|
|
11
|
+
export const YOM_MIME_TYPE = "application/vnd.yomu.yom";
|
|
12
|
+
export const YOM_MAGIC = new Uint8Array([0x59, 0x4f, 0x4d, 0x31]); // "YOM1"
|
|
13
|
+
export const YOM_HEADER_SIZE = 4096;
|
|
14
|
+
export const YOM_FORMAT_VERSION = 1;
|
|
15
|
+
export const YOM_HASH_LENGTH = 32;
|
|
16
|
+
export const YOM_IMAGE_BLOB_ALIGNMENT = 16;
|
|
17
|
+
export const YOM_DEFAULT_BLOB_ALIGNMENT = 1;
|
|
18
|
+
|
|
19
|
+
export function requiredCompression(kind: BlobKind): Compression {
|
|
20
|
+
switch (kind) {
|
|
21
|
+
case BlobKind.PageImage:
|
|
22
|
+
case BlobKind.ThumbnailImage:
|
|
23
|
+
case BlobKind.SearchPostings:
|
|
24
|
+
return Compression.None;
|
|
25
|
+
case BlobKind.OCRLayout:
|
|
26
|
+
case BlobKind.NormalizedText:
|
|
27
|
+
case BlobKind.SearchDictionary:
|
|
28
|
+
return Compression.Zstd;
|
|
29
|
+
default:
|
|
30
|
+
throw new Error(`unknown blob kind: ${kind}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function requiredAlignment(kind: BlobKind): number {
|
|
35
|
+
switch (kind) {
|
|
36
|
+
case BlobKind.PageImage:
|
|
37
|
+
case BlobKind.ThumbnailImage:
|
|
38
|
+
return YOM_IMAGE_BLOB_ALIGNMENT;
|
|
39
|
+
default:
|
|
40
|
+
return YOM_DEFAULT_BLOB_ALIGNMENT;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* -------------------------------------------------------------------------
|
|
46
|
+
* Reader-side safety limits (Gap 2 hardening).
|
|
47
|
+
*
|
|
48
|
+
* IMPORTANT: these are **reader policy, NOT format requirements**. The writer
|
|
49
|
+
* MUST NOT enforce them and the on-disk format places no upper bound on blob
|
|
50
|
+
* sizes -- a conforming `.yom` may legally exceed any of these. They exist
|
|
51
|
+
* only so a *reader* can refuse to allocate unbounded memory for a malformed
|
|
52
|
+
* or hostile file (decompression bombs, forged declared sizes) before it
|
|
53
|
+
* touches the data. Values are chosen comfortably above every real-world and
|
|
54
|
+
* fixture blob size; raise them if a legitimate corpus needs more.
|
|
55
|
+
* -------------------------------------------------------------------------
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/** Per-kind cap on a blob's declared `uncompressed_length`, in bytes. */
|
|
59
|
+
export const MAX_BLOB_UNCOMPRESSED_IMAGE = 64 * 1024 * 1024; // page/thumbnail AVIF
|
|
60
|
+
export const MAX_BLOB_UNCOMPRESSED_TEXT = 16 * 1024 * 1024; // OCR / normalized text / dictionary
|
|
61
|
+
export const MAX_BLOB_UNCOMPRESSED_POSTINGS = 64 * 1024 * 1024; // search postings scale with book size
|
|
62
|
+
|
|
63
|
+
/** Cap on the sum of every blob's declared `uncompressed_length` (whole-file validation). */
|
|
64
|
+
export const MAX_TOTAL_UNCOMPRESSED = 4 * 1024 * 1024 * 1024;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reject a blob whose declared inflation ratio
|
|
68
|
+
* (`uncompressed_length / max(compressed_length, 1)`) exceeds this -- the
|
|
69
|
+
* classic zip/zstd-bomb signature. zstd rarely exceeds ~3-5x on scanned-book
|
|
70
|
+
* text/OCR, so 1000x is a generous safety margin.
|
|
71
|
+
*/
|
|
72
|
+
export const MAX_INFLATION_RATIO = 1000;
|
|
73
|
+
|
|
74
|
+
/** The `uncompressed_length` cap that applies to a blob of the given kind. */
|
|
75
|
+
export function maxBlobUncompressed(kind: BlobKind): number {
|
|
76
|
+
switch (kind) {
|
|
77
|
+
case BlobKind.PageImage:
|
|
78
|
+
case BlobKind.ThumbnailImage:
|
|
79
|
+
return MAX_BLOB_UNCOMPRESSED_IMAGE;
|
|
80
|
+
case BlobKind.SearchPostings:
|
|
81
|
+
return MAX_BLOB_UNCOMPRESSED_POSTINGS;
|
|
82
|
+
case BlobKind.OCRLayout:
|
|
83
|
+
case BlobKind.NormalizedText:
|
|
84
|
+
case BlobKind.SearchDictionary:
|
|
85
|
+
return MAX_BLOB_UNCOMPRESSED_TEXT;
|
|
86
|
+
default:
|
|
87
|
+
return MAX_BLOB_UNCOMPRESSED_TEXT;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error hierarchy for the YOM reader/validator hardening layers.
|
|
3
|
+
*
|
|
4
|
+
* These are all *reader-side* failures raised while opening or reading a
|
|
5
|
+
* possibly-hostile `.yom`. They exist so callers can distinguish "the input
|
|
6
|
+
* is malformed/oversized/corrupt" from ordinary programming errors, and so
|
|
7
|
+
* the size-cap / FlatBuffers-verify / HTTP-Range layers throw a stable,
|
|
8
|
+
* catchable type instead of a bare `Error`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Base class for every typed reader error added by the hardening layers. */
|
|
12
|
+
export class YOMError extends Error {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A blob's declared `uncompressed_length` exceeds the reader's per-kind size
|
|
16
|
+
* cap, or its declared inflation ratio (`uncompressed / compressed`) exceeds
|
|
17
|
+
* `MAX_INFLATION_RATIO`. Raised *before* any decompression happens, so a
|
|
18
|
+
* decompression-bomb never gets to allocate memory.
|
|
19
|
+
*/
|
|
20
|
+
export class BlobTooLargeError extends YOMError {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A blob decompressed to a different byte length than its declared
|
|
24
|
+
* `uncompressed_length` (or decompression otherwise failed). Raised before
|
|
25
|
+
* the decompressed bytes are used, ahead of the final SHA-256 gate.
|
|
26
|
+
*/
|
|
27
|
+
export class CorruptBlobError extends YOMError {}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The main FlatBuffers index (or an OCR layout blob) failed bounded
|
|
31
|
+
* structural verification: an offset, vtable, table, string, or vector would
|
|
32
|
+
* dereference outside the known index byte range, or a vector length is
|
|
33
|
+
* absurdly large. Raised instead of returning a booby-trapped `getRootAs*`
|
|
34
|
+
* root that could later read out of bounds.
|
|
35
|
+
*/
|
|
36
|
+
export class MalformedIndexError extends YOMError {}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An HTTP source does not support range requests (it answered a ranged `GET`
|
|
40
|
+
* with `200 OK` / no `Accept-Ranges` instead of `206 Partial Content`).
|
|
41
|
+
* Callers may retry with `openURL(url, { fallbackToFullFetch: true })`.
|
|
42
|
+
*/
|
|
43
|
+
export class RangeNotSupportedError extends YOMError {}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { ByteBuffer } from "flatbuffers";
|
|
2
|
+
import { MainIndex } from "./generated/yomu/yom/main-index.js";
|
|
3
|
+
import { MalformedIndexError } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gap 3: schema-aware bounded verification of the main FlatBuffers index
|
|
7
|
+
* before `getRootAsMainIndex` traversal.
|
|
8
|
+
*
|
|
9
|
+
* `flatc` emits no TypeScript verifier, so this is a hand-written structural
|
|
10
|
+
* walk (spec option B). Given the index occupies exactly `[0, indexLength)`,
|
|
11
|
+
* it bounds-checks every offset the reader could dereference -- the root table
|
|
12
|
+
* offset, each vtable, each field, each string, and each vector -- against
|
|
13
|
+
* that range. Critically, before trusting any vector it asserts
|
|
14
|
+
* `4 + count * elementStride <= remaining bytes`, so a forged huge vector
|
|
15
|
+
* count can never drive an out-of-bounds read or an O(count) allocation/hang.
|
|
16
|
+
*
|
|
17
|
+
* Two depths:
|
|
18
|
+
* - `deep: false` (reader open, hot path): verify the root table plus the
|
|
19
|
+
* header + length bounds of its own vectors/sub-object pointers. Cheap.
|
|
20
|
+
* - `deep: true` (`validateYOM`): additionally recurse into every page, blob,
|
|
21
|
+
* search term, and outline sub-table -- i.e. touch every field the reader
|
|
22
|
+
* or validator will ever touch.
|
|
23
|
+
*
|
|
24
|
+
* On any violation it throws `MalformedIndexError`; it never reads out of
|
|
25
|
+
* bounds, never allocates from an attacker-controlled length, and never hangs.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type FieldSpec =
|
|
29
|
+
| { readonly slot: number; readonly kind: "scalar"; readonly size: number }
|
|
30
|
+
| { readonly slot: number; readonly kind: "string" }
|
|
31
|
+
| { readonly slot: number; readonly kind: "table"; readonly table: string }
|
|
32
|
+
| { readonly slot: number; readonly kind: "vectorScalar"; readonly stride: number }
|
|
33
|
+
| { readonly slot: number; readonly kind: "vectorString" }
|
|
34
|
+
| { readonly slot: number; readonly kind: "vectorTable"; readonly table: string };
|
|
35
|
+
|
|
36
|
+
// vtable slot for field id N == 4 + N*2 (see the generated `__offset(_, slot)` calls).
|
|
37
|
+
const SCHEMAS: Record<string, readonly FieldSpec[]> = {
|
|
38
|
+
MainIndex: [
|
|
39
|
+
{ slot: 4, kind: "scalar", size: 4 }, // formatVersion
|
|
40
|
+
{ slot: 6, kind: "table", table: "BookMetadata" }, // book
|
|
41
|
+
{ slot: 8, kind: "vectorTable", table: "PageEntry" }, // pages
|
|
42
|
+
{ slot: 10, kind: "vectorTable", table: "BlobRef" }, // blobs
|
|
43
|
+
{ slot: 12, kind: "scalar", size: 4 }, // ocrLayoutBlobIndex
|
|
44
|
+
{ slot: 14, kind: "scalar", size: 4 }, // normalizedTextBlobIndex
|
|
45
|
+
{ slot: 16, kind: "scalar", size: 4 }, // searchDictionaryBlobIndex
|
|
46
|
+
{ slot: 18, kind: "scalar", size: 4 }, // searchPostingsBlobIndex
|
|
47
|
+
{ slot: 20, kind: "vectorTable", table: "SearchTermRef" }, // searchTerms
|
|
48
|
+
// NOTE: `pdfExport` (slot 22) is intentionally NOT traversed. The TS
|
|
49
|
+
// reader/validator never dereference it, and the writer emits it via
|
|
50
|
+
// FlatBuffers `forceDefaults(true)`, which encodes an *absent* offset
|
|
51
|
+
// field as a present-but-sentinel value that points outside the buffer.
|
|
52
|
+
// Dereferencing it would falsely reject every writer-produced file. Its
|
|
53
|
+
// schema (below) is retained only for reference / future use.
|
|
54
|
+
],
|
|
55
|
+
BookMetadata: [
|
|
56
|
+
{ slot: 4, kind: "string" }, // title
|
|
57
|
+
{ slot: 6, kind: "scalar", size: 4 }, // pageCount
|
|
58
|
+
{ slot: 8, kind: "scalar", size: 1 }, // readingDirection
|
|
59
|
+
{ slot: 10, kind: "scalar", size: 8 }, // createdAtUnix
|
|
60
|
+
],
|
|
61
|
+
PageEntry: [
|
|
62
|
+
{ slot: 4, kind: "scalar", size: 4 }, // pageId
|
|
63
|
+
{ slot: 6, kind: "string" }, // pageLabel
|
|
64
|
+
{ slot: 8, kind: "scalar", size: 4 }, // pixelWidth
|
|
65
|
+
{ slot: 10, kind: "scalar", size: 4 }, // pixelHeight
|
|
66
|
+
{ slot: 12, kind: "scalar", size: 4 }, // dpi
|
|
67
|
+
{ slot: 14, kind: "string" }, // colorSpace
|
|
68
|
+
{ slot: 16, kind: "string" }, // colorProfileRef
|
|
69
|
+
{ slot: 18, kind: "vectorScalar", stride: 4 }, // mediaBox
|
|
70
|
+
{ slot: 20, kind: "vectorScalar", stride: 4 }, // cropBox
|
|
71
|
+
{ slot: 22, kind: "vectorScalar", stride: 4 }, // contentBox
|
|
72
|
+
{ slot: 24, kind: "scalar", size: 4 }, // rotation
|
|
73
|
+
{ slot: 26, kind: "scalar", size: 1 }, // readingDirection
|
|
74
|
+
{ slot: 28, kind: "scalar", size: 4 }, // pageImageBlobIndex
|
|
75
|
+
{ slot: 30, kind: "scalar", size: 4 }, // thumbnailBlobIndex
|
|
76
|
+
{ slot: 32, kind: "vectorScalar", stride: 1 }, // imageHash
|
|
77
|
+
],
|
|
78
|
+
BlobRef: [
|
|
79
|
+
{ slot: 4, kind: "scalar", size: 1 }, // kind
|
|
80
|
+
{ slot: 6, kind: "scalar", size: 8 }, // offset
|
|
81
|
+
{ slot: 8, kind: "scalar", size: 8 }, // compressedLength
|
|
82
|
+
{ slot: 10, kind: "scalar", size: 8 }, // uncompressedLength
|
|
83
|
+
{ slot: 12, kind: "scalar", size: 1 }, // compression
|
|
84
|
+
{ slot: 14, kind: "scalar", size: 4 }, // alignment
|
|
85
|
+
{ slot: 16, kind: "vectorScalar", stride: 1 }, // hash
|
|
86
|
+
],
|
|
87
|
+
SearchTermRef: [
|
|
88
|
+
{ slot: 4, kind: "string" }, // term
|
|
89
|
+
{ slot: 6, kind: "scalar", size: 8 }, // dictionaryOffset
|
|
90
|
+
{ slot: 8, kind: "scalar", size: 8 }, // dictionaryLength
|
|
91
|
+
{ slot: 10, kind: "scalar", size: 8 }, // postingsOffset
|
|
92
|
+
{ slot: 12, kind: "scalar", size: 8 }, // postingsLength
|
|
93
|
+
],
|
|
94
|
+
PDFExportMetadata: [
|
|
95
|
+
{ slot: 4, kind: "vectorString" }, // pageLabels
|
|
96
|
+
{ slot: 6, kind: "vectorTable", table: "OutlineEntry" }, // outline
|
|
97
|
+
],
|
|
98
|
+
OutlineEntry: [
|
|
99
|
+
{ slot: 4, kind: "string" }, // title
|
|
100
|
+
{ slot: 6, kind: "scalar", size: 4 }, // pageId
|
|
101
|
+
{ slot: 8, kind: "vectorTable", table: "OutlineEntry" }, // children (recursive)
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const MAX_DEPTH = 64;
|
|
106
|
+
const UOFFSET = 4;
|
|
107
|
+
|
|
108
|
+
class BoundsVerifier {
|
|
109
|
+
private readonly buf: Uint8Array;
|
|
110
|
+
private readonly len: number;
|
|
111
|
+
|
|
112
|
+
constructor(buf: Uint8Array) {
|
|
113
|
+
this.buf = buf;
|
|
114
|
+
this.len = buf.length;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private fail(reason: string): never {
|
|
118
|
+
throw new MalformedIndexError(`malformed index: ${reason}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Assert `[pos, pos+size)` lies inside the index. */
|
|
122
|
+
private require(pos: number, size: number, reason: string): void {
|
|
123
|
+
if (!Number.isInteger(pos) || pos < 0 || size < 0 || pos + size > this.len) {
|
|
124
|
+
this.fail(`${reason} (pos=${pos} size=${size} len=${this.len})`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private i32(pos: number): number {
|
|
129
|
+
this.require(pos, 4, "read int32");
|
|
130
|
+
const b = this.buf;
|
|
131
|
+
return b[pos] | (b[pos + 1] << 8) | (b[pos + 2] << 16) | (b[pos + 3] << 24);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private u32(pos: number): number {
|
|
135
|
+
return this.i32(pos) >>> 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private u16(pos: number): number {
|
|
139
|
+
this.require(pos, 2, "read uint16");
|
|
140
|
+
return this.buf[pos] | (this.buf[pos + 1] << 8);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Bounds-verify a single optional field of the root table. Returns `false`
|
|
145
|
+
* when the field is genuinely absent OR encoded as a forced-default sentinel
|
|
146
|
+
* that points out of bounds (so a caller can safely decide not to read it);
|
|
147
|
+
* returns `true` only when the field is present and structurally sound.
|
|
148
|
+
*/
|
|
149
|
+
verifyOptionalRootField(rootTable: string, field: FieldSpec, deep: boolean): boolean {
|
|
150
|
+
const tablePos = this.i32(0);
|
|
151
|
+
this.require(tablePos, 4, "root soffset");
|
|
152
|
+
const soffset = this.i32(tablePos);
|
|
153
|
+
const vtablePos = tablePos - soffset;
|
|
154
|
+
this.require(vtablePos, 4, "root vtable header");
|
|
155
|
+
const vtableSize = this.u16(vtablePos);
|
|
156
|
+
if (vtableSize < 4) this.fail("root vtable too small");
|
|
157
|
+
this.require(vtablePos, vtableSize, "root vtable body");
|
|
158
|
+
if (field.slot + 2 > vtableSize) return false;
|
|
159
|
+
const rel = this.u16(vtablePos + field.slot);
|
|
160
|
+
if (rel === 0) return false;
|
|
161
|
+
this.verifyField(tablePos + rel, field, deep, 0, rootTable);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Verify the root object offset and the table it points at. */
|
|
166
|
+
verifyRoot(rootTable: string, deep: boolean): void {
|
|
167
|
+
// getRootAs* computes rootPos = readInt32(0) + 0 (signed, like every
|
|
168
|
+
// FlatBuffers offset -- child objects can lie at lower addresses).
|
|
169
|
+
const rootPos = this.i32(0);
|
|
170
|
+
this.verifyTable(rootPos, rootTable, deep, 0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private verifyTable(tablePos: number, tableName: string, deep: boolean, depth: number): void {
|
|
174
|
+
if (depth > MAX_DEPTH) this.fail("table nesting too deep");
|
|
175
|
+
this.require(tablePos, 4, `table ${tableName} soffset`);
|
|
176
|
+
const soffset = this.i32(tablePos);
|
|
177
|
+
const vtablePos = tablePos - soffset;
|
|
178
|
+
this.require(vtablePos, 4, `table ${tableName} vtable header`);
|
|
179
|
+
const vtableSize = this.u16(vtablePos);
|
|
180
|
+
if (vtableSize < 4) this.fail(`table ${tableName} vtable too small`);
|
|
181
|
+
this.require(vtablePos, vtableSize, `table ${tableName} vtable body`);
|
|
182
|
+
const tableSize = this.u16(vtablePos + 2);
|
|
183
|
+
this.require(tablePos, tableSize, `table ${tableName} inline data`);
|
|
184
|
+
|
|
185
|
+
const fields = SCHEMAS[tableName];
|
|
186
|
+
if (!fields) this.fail(`unknown table ${tableName}`);
|
|
187
|
+
for (const field of fields) {
|
|
188
|
+
// A field's vtable slot may lie beyond this vtable (field absent).
|
|
189
|
+
if (field.slot + 2 > vtableSize) continue;
|
|
190
|
+
const rel = this.u16(vtablePos + field.slot);
|
|
191
|
+
if (rel === 0) continue; // absent / default
|
|
192
|
+
const fieldPos = tablePos + rel;
|
|
193
|
+
this.verifyField(fieldPos, field, deep, depth, tableName);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private verifyField(fieldPos: number, field: FieldSpec, deep: boolean, depth: number, owner: string): void {
|
|
198
|
+
switch (field.kind) {
|
|
199
|
+
case "scalar":
|
|
200
|
+
this.require(fieldPos, field.size, `${owner} scalar field`);
|
|
201
|
+
return;
|
|
202
|
+
case "string":
|
|
203
|
+
this.verifyString(fieldPos, owner);
|
|
204
|
+
return;
|
|
205
|
+
case "table": {
|
|
206
|
+
// Sub-object pointer: always bounds-check the offset; recurse when deep.
|
|
207
|
+
const subPos = fieldPos + this.i32(fieldPos);
|
|
208
|
+
this.require(subPos, 4, `${owner} sub-table pointer`);
|
|
209
|
+
if (deep) this.verifyTable(subPos, field.table, deep, depth + 1);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
case "vectorScalar": {
|
|
213
|
+
this.verifyVectorHeader(fieldPos, field.stride, owner);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
case "vectorString": {
|
|
217
|
+
const { dataStart, count } = this.verifyVectorHeader(fieldPos, UOFFSET, owner);
|
|
218
|
+
if (deep) {
|
|
219
|
+
for (let i = 0; i < count; i++) {
|
|
220
|
+
const elemPos = dataStart + i * UOFFSET;
|
|
221
|
+
const strPos = elemPos + this.i32(elemPos);
|
|
222
|
+
this.verifyString(strPos, owner);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
case "vectorTable": {
|
|
228
|
+
const { dataStart, count } = this.verifyVectorHeader(fieldPos, UOFFSET, owner);
|
|
229
|
+
if (deep) {
|
|
230
|
+
for (let i = 0; i < count; i++) {
|
|
231
|
+
const elemPos = dataStart + i * UOFFSET;
|
|
232
|
+
const subPos = elemPos + this.i32(elemPos);
|
|
233
|
+
this.verifyTable(subPos, field.table, deep, depth + 1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private verifyString(fieldPos: number, owner: string): void {
|
|
242
|
+
const strPos = fieldPos + this.i32(fieldPos);
|
|
243
|
+
this.require(strPos, 4, `${owner} string length`);
|
|
244
|
+
const strLen = this.u32(strPos);
|
|
245
|
+
// +1 for FlatBuffers' implicit null terminator.
|
|
246
|
+
this.require(strPos + 4, strLen + 1, `${owner} string body`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private verifyVectorHeader(fieldPos: number, stride: number, owner: string): { dataStart: number; count: number } {
|
|
250
|
+
const vecPos = fieldPos + this.i32(fieldPos);
|
|
251
|
+
this.require(vecPos, 4, `${owner} vector length`);
|
|
252
|
+
const count = this.u32(vecPos);
|
|
253
|
+
const dataStart = vecPos + 4;
|
|
254
|
+
// The load-bearing OOM guard: reject an oversized forged count before use.
|
|
255
|
+
if (count > (this.len - dataStart) / stride) {
|
|
256
|
+
this.fail(`${owner} vector count ${count} exceeds buffer (stride ${stride})`);
|
|
257
|
+
}
|
|
258
|
+
this.require(dataStart, count * stride, `${owner} vector body`);
|
|
259
|
+
return { dataStart, count };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Bounded structural verification of a raw (uncompressed) main-index byte
|
|
265
|
+
* range. `deep` controls whether sub-tables/vectors are recursed into
|
|
266
|
+
* (validator) or only their offsets/lengths bounds-checked (reader open).
|
|
267
|
+
* Throws `MalformedIndexError` on any violation.
|
|
268
|
+
*/
|
|
269
|
+
export function verifyMainIndex(indexBytes: Uint8Array, deep: boolean): void {
|
|
270
|
+
new BoundsVerifier(indexBytes).verifyRoot("MainIndex", deep);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Verify then construct the `MainIndex` root, so a malformed buffer throws
|
|
275
|
+
* `MalformedIndexError` instead of yielding a booby-trapped lazy root.
|
|
276
|
+
*/
|
|
277
|
+
export function getVerifiedRootAsMainIndex(indexBytes: Uint8Array, deep: boolean): MainIndex {
|
|
278
|
+
verifyMainIndex(indexBytes, deep);
|
|
279
|
+
return MainIndex.getRootAsMainIndex(new ByteBuffer(indexBytes));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* True only when the index's optional `pdfExport` metadata is genuinely
|
|
284
|
+
* present and safely traversable. Because the writer emits an absent
|
|
285
|
+
* `pdfExport` via `forceDefaults(true)` (a sentinel pointing out of bounds),
|
|
286
|
+
* callers must gate any `pdfExport()`/outline read on this check.
|
|
287
|
+
*/
|
|
288
|
+
export function pdfExportPresent(indexBytes: Uint8Array): boolean {
|
|
289
|
+
try {
|
|
290
|
+
return new BoundsVerifier(indexBytes).verifyOptionalRootField(
|
|
291
|
+
"MainIndex",
|
|
292
|
+
{ slot: 22, kind: "table", table: "PDFExportMetadata" },
|
|
293
|
+
true,
|
|
294
|
+
);
|
|
295
|
+
} catch {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// automatically generated by the FlatBuffers compiler, do not modify
|
|
2
|
+
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
|
4
|
+
|
|
5
|
+
export enum BlobKind {
|
|
6
|
+
PageImage = 0,
|
|
7
|
+
ThumbnailImage = 1,
|
|
8
|
+
OCRLayout = 2,
|
|
9
|
+
NormalizedText = 3,
|
|
10
|
+
SearchDictionary = 4,
|
|
11
|
+
SearchPostings = 5
|
|
12
|
+
}
|