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,134 @@
|
|
|
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
|
+
import * as flatbuffers from 'flatbuffers';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export class SearchTermRef implements flatbuffers.IUnpackableObject<SearchTermRefT> {
|
|
10
|
+
bb: flatbuffers.ByteBuffer|null = null;
|
|
11
|
+
bb_pos = 0;
|
|
12
|
+
__init(i:number, bb:flatbuffers.ByteBuffer):SearchTermRef {
|
|
13
|
+
this.bb_pos = i;
|
|
14
|
+
this.bb = bb;
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static getRootAsSearchTermRef(bb:flatbuffers.ByteBuffer, obj?:SearchTermRef):SearchTermRef {
|
|
19
|
+
return (obj || new SearchTermRef()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static getSizePrefixedRootAsSearchTermRef(bb:flatbuffers.ByteBuffer, obj?:SearchTermRef):SearchTermRef {
|
|
23
|
+
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
|
24
|
+
return (obj || new SearchTermRef()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
term():string|null
|
|
28
|
+
term(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
|
29
|
+
term(optionalEncoding?:any):string|Uint8Array|null {
|
|
30
|
+
const offset = this.bb!.__offset(this.bb_pos, 4);
|
|
31
|
+
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
dictionaryOffset():bigint {
|
|
35
|
+
const offset = this.bb!.__offset(this.bb_pos, 6);
|
|
36
|
+
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
dictionaryLength():bigint {
|
|
40
|
+
const offset = this.bb!.__offset(this.bb_pos, 8);
|
|
41
|
+
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
postingsOffset():bigint {
|
|
45
|
+
const offset = this.bb!.__offset(this.bb_pos, 10);
|
|
46
|
+
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
postingsLength():bigint {
|
|
50
|
+
const offset = this.bb!.__offset(this.bb_pos, 12);
|
|
51
|
+
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static startSearchTermRef(builder:flatbuffers.Builder) {
|
|
55
|
+
builder.startObject(5);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static addTerm(builder:flatbuffers.Builder, termOffset:flatbuffers.Offset) {
|
|
59
|
+
builder.addFieldOffset(0, termOffset, 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static addDictionaryOffset(builder:flatbuffers.Builder, dictionaryOffset:bigint) {
|
|
63
|
+
builder.addFieldInt64(1, dictionaryOffset, BigInt('0'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static addDictionaryLength(builder:flatbuffers.Builder, dictionaryLength:bigint) {
|
|
67
|
+
builder.addFieldInt64(2, dictionaryLength, BigInt('0'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static addPostingsOffset(builder:flatbuffers.Builder, postingsOffset:bigint) {
|
|
71
|
+
builder.addFieldInt64(3, postingsOffset, BigInt('0'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static addPostingsLength(builder:flatbuffers.Builder, postingsLength:bigint) {
|
|
75
|
+
builder.addFieldInt64(4, postingsLength, BigInt('0'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static endSearchTermRef(builder:flatbuffers.Builder):flatbuffers.Offset {
|
|
79
|
+
const offset = builder.endObject();
|
|
80
|
+
return offset;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static createSearchTermRef(builder:flatbuffers.Builder, termOffset:flatbuffers.Offset, dictionaryOffset:bigint, dictionaryLength:bigint, postingsOffset:bigint, postingsLength:bigint):flatbuffers.Offset {
|
|
84
|
+
SearchTermRef.startSearchTermRef(builder);
|
|
85
|
+
SearchTermRef.addTerm(builder, termOffset);
|
|
86
|
+
SearchTermRef.addDictionaryOffset(builder, dictionaryOffset);
|
|
87
|
+
SearchTermRef.addDictionaryLength(builder, dictionaryLength);
|
|
88
|
+
SearchTermRef.addPostingsOffset(builder, postingsOffset);
|
|
89
|
+
SearchTermRef.addPostingsLength(builder, postingsLength);
|
|
90
|
+
return SearchTermRef.endSearchTermRef(builder);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
unpack(): SearchTermRefT {
|
|
94
|
+
return new SearchTermRefT(
|
|
95
|
+
this.term(),
|
|
96
|
+
this.dictionaryOffset(),
|
|
97
|
+
this.dictionaryLength(),
|
|
98
|
+
this.postingsOffset(),
|
|
99
|
+
this.postingsLength()
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
unpackTo(_o: SearchTermRefT): void {
|
|
105
|
+
_o.term = this.term();
|
|
106
|
+
_o.dictionaryOffset = this.dictionaryOffset();
|
|
107
|
+
_o.dictionaryLength = this.dictionaryLength();
|
|
108
|
+
_o.postingsOffset = this.postingsOffset();
|
|
109
|
+
_o.postingsLength = this.postingsLength();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class SearchTermRefT implements flatbuffers.IGeneratedObject {
|
|
114
|
+
constructor(
|
|
115
|
+
public term: string|Uint8Array|null = null,
|
|
116
|
+
public dictionaryOffset: bigint = BigInt('0'),
|
|
117
|
+
public dictionaryLength: bigint = BigInt('0'),
|
|
118
|
+
public postingsOffset: bigint = BigInt('0'),
|
|
119
|
+
public postingsLength: bigint = BigInt('0')
|
|
120
|
+
){}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
|
124
|
+
const term = (this.term !== null ? builder.createString(this.term!) : 0);
|
|
125
|
+
|
|
126
|
+
return SearchTermRef.createSearchTermRef(builder,
|
|
127
|
+
term,
|
|
128
|
+
this.dictionaryOffset,
|
|
129
|
+
this.dictionaryLength,
|
|
130
|
+
this.postingsOffset,
|
|
131
|
+
this.postingsLength
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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 { OCRBlock, OCRBlockT } from './yom/ocrblock.js';
|
|
6
|
+
export { OCRLayout, OCRLayoutT } from './yom/ocrlayout.js';
|
|
7
|
+
export { OCRLine, OCRLineT } from './yom/ocrline.js';
|
|
8
|
+
export { OCRPage, OCRPageT } from './yom/ocrpage.js';
|
|
9
|
+
export { OCRToken, OCRTokenT } from './yom/ocrtoken.js';
|
package/src/yom/hash.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** SHA-256 over `data`, returned as raw bytes (matches CryptoKit's `SHA256.hash` used natively). */
|
|
2
|
+
export async function sha256(data: Uint8Array): Promise<Uint8Array> {
|
|
3
|
+
const digest = await crypto.subtle.digest("SHA-256", toArrayBuffer(data));
|
|
4
|
+
return new Uint8Array(digest);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
8
|
+
if (a.length !== b.length) return false;
|
|
9
|
+
for (let i = 0; i < a.length; i++) {
|
|
10
|
+
if (a[i] !== b[i]) return false;
|
|
11
|
+
}
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// `crypto.subtle.digest` requires an ArrayBuffer-backed view; a Uint8Array
|
|
16
|
+
// sliced from a larger buffer (e.g. via `subarray`) may have a non-zero
|
|
17
|
+
// `byteOffset`, so copy defensively rather than pass `data.buffer` directly.
|
|
18
|
+
function toArrayBuffer(data: Uint8Array): ArrayBuffer {
|
|
19
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
|
|
20
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { YOM_HASH_LENGTH, YOM_HEADER_SIZE } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fixed binary layout shared by the YOM v1 header (4096 bytes, zero padded)
|
|
5
|
+
* and its footer mirror (the same fields, unpadded, appended at the end of
|
|
6
|
+
* the file). Mirrors the Swift implementation in
|
|
7
|
+
* `swift/Sources/YOMFormat/YOMHeader.swift` byte-for-byte:
|
|
8
|
+
* little-endian `magic(4) | formatVersion(4) | indexOffset(8) |
|
|
9
|
+
* indexCompressedLength(8) | indexUncompressedLength(8) | indexHash(32) |
|
|
10
|
+
* fileHash(32)`, 96 bytes total. Both `indexHash` and `fileHash` are zeroed
|
|
11
|
+
* out of the header/footer bytes fed into the whole-file SHA-256
|
|
12
|
+
* calculation (see `zeroingHashes`); the real digests are patched in only
|
|
13
|
+
* after that hash has been computed.
|
|
14
|
+
*/
|
|
15
|
+
export const HEADER_FIELDS_SIZE = 4 + 4 + 8 + 8 + 8 + YOM_HASH_LENGTH + YOM_HASH_LENGTH;
|
|
16
|
+
|
|
17
|
+
export interface YOMHeader {
|
|
18
|
+
magic: Uint8Array;
|
|
19
|
+
formatVersion: number;
|
|
20
|
+
indexOffset: bigint;
|
|
21
|
+
indexCompressedLength: bigint;
|
|
22
|
+
indexUncompressedLength: bigint;
|
|
23
|
+
indexHash: Uint8Array;
|
|
24
|
+
fileHash: Uint8Array;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class YOMHeaderParseError extends Error {
|
|
28
|
+
constructor() {
|
|
29
|
+
super("YOM header truncated");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The fixed 96-byte field block, identical for header and footer. */
|
|
34
|
+
export function encodeHeaderFields(header: YOMHeader): Uint8Array {
|
|
35
|
+
const out = new Uint8Array(HEADER_FIELDS_SIZE);
|
|
36
|
+
const view = new DataView(out.buffer);
|
|
37
|
+
let cursor = 0;
|
|
38
|
+
out.set(header.magic, cursor);
|
|
39
|
+
cursor += 4;
|
|
40
|
+
view.setUint32(cursor, header.formatVersion, true);
|
|
41
|
+
cursor += 4;
|
|
42
|
+
view.setBigUint64(cursor, header.indexOffset, true);
|
|
43
|
+
cursor += 8;
|
|
44
|
+
view.setBigUint64(cursor, header.indexCompressedLength, true);
|
|
45
|
+
cursor += 8;
|
|
46
|
+
view.setBigUint64(cursor, header.indexUncompressedLength, true);
|
|
47
|
+
cursor += 8;
|
|
48
|
+
out.set(header.indexHash, cursor);
|
|
49
|
+
cursor += YOM_HASH_LENGTH;
|
|
50
|
+
out.set(header.fileHash, cursor);
|
|
51
|
+
cursor += YOM_HASH_LENGTH;
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The full 4096-byte, zero-padded header. */
|
|
56
|
+
export function encodeHeader(header: YOMHeader): Uint8Array {
|
|
57
|
+
const fields = encodeHeaderFields(header);
|
|
58
|
+
const out = new Uint8Array(YOM_HEADER_SIZE);
|
|
59
|
+
out.set(fields, 0);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The unpadded footer mirror (just the field block). */
|
|
64
|
+
export function encodeFooter(header: YOMHeader): Uint8Array {
|
|
65
|
+
return encodeHeaderFields(header);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parses the fixed 96-byte field block from the front of `data`. Does NOT
|
|
70
|
+
* validate magic/version/semantic correctness -- callers (reader,
|
|
71
|
+
* validator) decide what to do with the parsed values.
|
|
72
|
+
*/
|
|
73
|
+
export function parseHeaderFields(data: Uint8Array): YOMHeader {
|
|
74
|
+
if (data.length < HEADER_FIELDS_SIZE) throw new YOMHeaderParseError();
|
|
75
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
76
|
+
let cursor = 0;
|
|
77
|
+
const magic = data.slice(cursor, cursor + 4);
|
|
78
|
+
cursor += 4;
|
|
79
|
+
const formatVersion = view.getUint32(cursor, true);
|
|
80
|
+
cursor += 4;
|
|
81
|
+
const indexOffset = view.getBigUint64(cursor, true);
|
|
82
|
+
cursor += 8;
|
|
83
|
+
const indexCompressedLength = view.getBigUint64(cursor, true);
|
|
84
|
+
cursor += 8;
|
|
85
|
+
const indexUncompressedLength = view.getBigUint64(cursor, true);
|
|
86
|
+
cursor += 8;
|
|
87
|
+
const indexHash = data.slice(cursor, cursor + YOM_HASH_LENGTH);
|
|
88
|
+
cursor += YOM_HASH_LENGTH;
|
|
89
|
+
const fileHash = data.slice(cursor, cursor + YOM_HASH_LENGTH);
|
|
90
|
+
cursor += YOM_HASH_LENGTH;
|
|
91
|
+
return {
|
|
92
|
+
magic,
|
|
93
|
+
formatVersion,
|
|
94
|
+
indexOffset,
|
|
95
|
+
indexCompressedLength,
|
|
96
|
+
indexUncompressedLength,
|
|
97
|
+
indexHash,
|
|
98
|
+
fileHash,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A copy with both hash fields zeroed, used as the input to the whole-file
|
|
104
|
+
* hash calculation (see module doc comment).
|
|
105
|
+
*/
|
|
106
|
+
export function zeroingHashes(header: YOMHeader): YOMHeader {
|
|
107
|
+
return {
|
|
108
|
+
...header,
|
|
109
|
+
indexHash: new Uint8Array(YOM_HASH_LENGTH),
|
|
110
|
+
fileHash: new Uint8Array(YOM_HASH_LENGTH),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function headersEqual(a: YOMHeader, b: YOMHeader): boolean {
|
|
115
|
+
return (
|
|
116
|
+
bytesEqual(a.magic, b.magic) &&
|
|
117
|
+
a.formatVersion === b.formatVersion &&
|
|
118
|
+
a.indexOffset === b.indexOffset &&
|
|
119
|
+
a.indexCompressedLength === b.indexCompressedLength &&
|
|
120
|
+
a.indexUncompressedLength === b.indexUncompressedLength &&
|
|
121
|
+
bytesEqual(a.indexHash, b.indexHash) &&
|
|
122
|
+
bytesEqual(a.fileHash, b.fileHash)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
127
|
+
if (a.length !== b.length) return false;
|
|
128
|
+
for (let i = 0; i < a.length; i++) {
|
|
129
|
+
if (a[i] !== b[i]) return false;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { MainIndex } from "./generated/yomu/yom/main-index.js";
|
|
2
|
+
import type { PageEntry } from "./generated/yomu/yom/page-entry.js";
|
|
3
|
+
import { Compression } from "./generated/yomu/yom/compression.js";
|
|
4
|
+
import { YOM_FORMAT_VERSION, YOM_HEADER_SIZE, YOM_MAGIC } from "./constants.js";
|
|
5
|
+
import { parseHeaderFields } from "./header.js";
|
|
6
|
+
import { bytesEqual, sha256 } from "./hash.js";
|
|
7
|
+
import { zstdDecompress } from "./zstd.js";
|
|
8
|
+
import { getVerifiedRootAsMainIndex } from "./flatbufferVerify.js";
|
|
9
|
+
import { enforceBlobSizePolicy } from "./blobLimits.js";
|
|
10
|
+
import {
|
|
11
|
+
HttpRangeByteSource,
|
|
12
|
+
MemoryByteSource,
|
|
13
|
+
type ByteSource,
|
|
14
|
+
type HttpRangeOptions,
|
|
15
|
+
} from "./byteSource.js";
|
|
16
|
+
import { RangeNotSupportedError } from "./errors.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Opens a YOM v1 packfile for random-access reading over a {@link ByteSource}
|
|
20
|
+
* -- either an in-memory buffer ({@link MemoryByteSource}) or an HTTP server
|
|
21
|
+
* that honours `Range` requests ({@link HttpRangeByteSource}). Ports the Swift
|
|
22
|
+
* implementation in `swift/Sources/YOMFormat/YOMPackfileReader.swift`.
|
|
23
|
+
*
|
|
24
|
+
* `open`/`openSource`/`openURL` read only the fixed header and the main
|
|
25
|
+
* FlatBuffers index up front -- never the blob area. The index is
|
|
26
|
+
* bounds-verified (Gap 3) before traversal; each blob read enforces the
|
|
27
|
+
* reader-side size caps (Gap 2) before decompressing, then verifies the
|
|
28
|
+
* blob's stored SHA-256 as the final gate.
|
|
29
|
+
*
|
|
30
|
+
* Because reads may hit the network, all blob accessors are `Promise`-based.
|
|
31
|
+
* `pageImage`/`thumbnailImage` never invoke zstd: page and thumbnail blobs are
|
|
32
|
+
* always `.none` compression per `requiredCompression`.
|
|
33
|
+
*/
|
|
34
|
+
export class YOMReaderError extends Error {}
|
|
35
|
+
|
|
36
|
+
export class YOMPackfileReader {
|
|
37
|
+
readonly mainIndex: MainIndex;
|
|
38
|
+
private readonly source: ByteSource;
|
|
39
|
+
// Retained so the index ByteBuffer that `mainIndex` reads from stays alive.
|
|
40
|
+
private readonly indexBytes: Uint8Array;
|
|
41
|
+
|
|
42
|
+
private constructor(source: ByteSource, indexBytes: Uint8Array, mainIndex: MainIndex) {
|
|
43
|
+
this.source = source;
|
|
44
|
+
this.indexBytes = indexBytes;
|
|
45
|
+
this.mainIndex = mainIndex;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Opens from an in-memory buffer (wraps a {@link MemoryByteSource}). */
|
|
49
|
+
static async open(bytes: Uint8Array): Promise<YOMPackfileReader> {
|
|
50
|
+
return YOMPackfileReader.openSource(new MemoryByteSource(bytes));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Opens over any {@link ByteSource}. Reads only the header + index. */
|
|
54
|
+
static async openSource(source: ByteSource): Promise<YOMPackfileReader> {
|
|
55
|
+
if (source.size < YOM_HEADER_SIZE) throw new YOMReaderError("file too small");
|
|
56
|
+
|
|
57
|
+
const headerBytes = await source.read(0, YOM_HEADER_SIZE);
|
|
58
|
+
const header = parseHeaderFields(headerBytes);
|
|
59
|
+
if (!bytesEqual(header.magic, YOM_MAGIC)) throw new YOMReaderError("bad magic");
|
|
60
|
+
if (header.formatVersion !== YOM_FORMAT_VERSION) {
|
|
61
|
+
throw new YOMReaderError(`unsupported format version ${header.formatVersion}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const indexOffset = Number(header.indexOffset);
|
|
65
|
+
const indexLength = Number(header.indexUncompressedLength);
|
|
66
|
+
if (indexOffset < YOM_HEADER_SIZE || indexOffset + indexLength > source.size) {
|
|
67
|
+
throw new YOMReaderError("index out of bounds");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const indexBytes = await source.read(indexOffset, indexLength);
|
|
71
|
+
// Cheap root + vector-length bounds verify on the hot open path (Gap 3).
|
|
72
|
+
const mainIndex = getVerifiedRootAsMainIndex(indexBytes, false);
|
|
73
|
+
|
|
74
|
+
return new YOMPackfileReader(source, indexBytes, mainIndex);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Opens a `.yom` over HTTP using `Range` requests -- fetching only the
|
|
79
|
+
* header + index up front, then each blob on demand. When the server does
|
|
80
|
+
* not support ranges, throws `RangeNotSupportedError` unless
|
|
81
|
+
* `fallbackToFullFetch` is set, in which case it does a single full `GET`
|
|
82
|
+
* and reads from memory.
|
|
83
|
+
*/
|
|
84
|
+
static async openURL(
|
|
85
|
+
url: string,
|
|
86
|
+
opts: HttpRangeOptions & { fallbackToFullFetch?: boolean } = {},
|
|
87
|
+
): Promise<YOMPackfileReader> {
|
|
88
|
+
const { fallbackToFullFetch, ...rangeOpts } = opts;
|
|
89
|
+
try {
|
|
90
|
+
const source = await HttpRangeByteSource.create(url, rangeOpts);
|
|
91
|
+
return await YOMPackfileReader.openSource(source);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (fallbackToFullFetch && err instanceof RangeNotSupportedError) {
|
|
94
|
+
const fetchFn = rangeOpts.fetch ?? ((input: string, init?: RequestInit) => fetch(input, init));
|
|
95
|
+
const res = await fetchFn(url, { headers: rangeOpts.headers, signal: rangeOpts.signal });
|
|
96
|
+
if (!res.ok) throw new YOMReaderError(`full fetch failed with status ${res.status} for ${url}`);
|
|
97
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
98
|
+
return YOMPackfileReader.openSource(new MemoryByteSource(bytes));
|
|
99
|
+
}
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get pageCount(): number {
|
|
105
|
+
return this.mainIndex.pagesLength();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The book title stored in the package's metadata, or `""` if absent. */
|
|
109
|
+
get title(): string {
|
|
110
|
+
const book = this.mainIndex.book();
|
|
111
|
+
const title = book?.title();
|
|
112
|
+
return typeof title === "string" ? title : "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
page(index: number): PageEntry {
|
|
116
|
+
const page = this.mainIndex.pages(index);
|
|
117
|
+
if (!page) throw new YOMReaderError("page index out of range");
|
|
118
|
+
return page;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Reads exactly one blob's byte range, enforcing the reader-side size caps
|
|
123
|
+
* (Gap 2) before decompressing, then verifying its stored hash against the
|
|
124
|
+
* decompressed (original) bytes -- the same convention the writer uses when
|
|
125
|
+
* hashing.
|
|
126
|
+
*/
|
|
127
|
+
async readBlob(blobIndex: number): Promise<Uint8Array> {
|
|
128
|
+
if (blobIndex < 0 || blobIndex >= this.mainIndex.blobsLength()) {
|
|
129
|
+
throw new YOMReaderError("blob index out of range");
|
|
130
|
+
}
|
|
131
|
+
const blob = this.mainIndex.blobs(blobIndex);
|
|
132
|
+
if (!blob) throw new YOMReaderError("blob index out of range");
|
|
133
|
+
|
|
134
|
+
const start = Number(blob.offset());
|
|
135
|
+
const compressedLength = Number(blob.compressedLength());
|
|
136
|
+
const uncompressedLength = Number(blob.uncompressedLength());
|
|
137
|
+
if (start < 0 || compressedLength < 0 || start + compressedLength > this.source.size) {
|
|
138
|
+
throw new YOMReaderError("blob out of bounds");
|
|
139
|
+
}
|
|
140
|
+
enforceBlobSizePolicy(blob.kind(), compressedLength, uncompressedLength);
|
|
141
|
+
|
|
142
|
+
const rawBytes = await this.source.read(start, compressedLength);
|
|
143
|
+
const original =
|
|
144
|
+
blob.compression() === Compression.None ? rawBytes : await zstdDecompress(rawBytes, uncompressedLength);
|
|
145
|
+
|
|
146
|
+
const actualHash = await sha256(original);
|
|
147
|
+
const expectedHash = blob.hashArray();
|
|
148
|
+
if (!expectedHash || !bytesEqual(actualHash, expectedHash)) {
|
|
149
|
+
throw new YOMReaderError("blob hash mismatch");
|
|
150
|
+
}
|
|
151
|
+
return original;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Never invokes zstd -- page image blobs are always `.none` by policy. */
|
|
155
|
+
async pageImage(pageIndex: number): Promise<Uint8Array> {
|
|
156
|
+
return this.readBlob(this.page(pageIndex).pageImageBlobIndex());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Never invokes zstd -- thumbnail blobs are always `.none` by policy. */
|
|
160
|
+
async thumbnailImage(pageIndex: number): Promise<Uint8Array> {
|
|
161
|
+
return this.readBlob(this.page(pageIndex).thumbnailBlobIndex());
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds YOM's search dictionary + postings blobs over normalized text
|
|
3
|
+
* (`textNormalizer.ts` output), using character bigrams. Ports the Swift
|
|
4
|
+
* implementation in `swift/Sources/YOMFormat/YOMSearchIndexBuilder.swift`.
|
|
5
|
+
*
|
|
6
|
+
* **Dictionary blob**: the concatenated UTF-8 bytes of every term, in sorted
|
|
7
|
+
* order. Redundant with `SearchTermRef.term` in the main index but is a
|
|
8
|
+
* deliberately simple, schema-valid choice (see the Swift source's doc
|
|
9
|
+
* comment) -- the format only requires the byte range exist, not that it
|
|
10
|
+
* hold any particular content.
|
|
11
|
+
*
|
|
12
|
+
* **Postings blob**: for each term, a delta+varint-encoded sequence of
|
|
13
|
+
* `(page, position)` hits sorted by page then position, where `position` is
|
|
14
|
+
* the *code-point* offset within that page's normalized text. Never
|
|
15
|
+
* compressed (`.none`) per the fixed policy.
|
|
16
|
+
*
|
|
17
|
+
* Iterates by Unicode code point (`Array.from`), the closest JS analogue of
|
|
18
|
+
* Swift's extended-grapheme-cluster `Character` iteration -- the two only
|
|
19
|
+
* diverge for combining marks/emoji sequences, which NFKC-normalized
|
|
20
|
+
* Japanese book text does not produce in practice.
|
|
21
|
+
*/
|
|
22
|
+
export interface SearchTerm {
|
|
23
|
+
term: string;
|
|
24
|
+
dictionaryOffset: number;
|
|
25
|
+
dictionaryLength: number;
|
|
26
|
+
postingsOffset: number;
|
|
27
|
+
postingsLength: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SearchIndexResult {
|
|
31
|
+
dictionaryBlob: Uint8Array;
|
|
32
|
+
postingsBlob: Uint8Array;
|
|
33
|
+
searchTerms: SearchTerm[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Hit {
|
|
37
|
+
page: number;
|
|
38
|
+
position: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildSearchIndex(normalizedPageTexts: string[]): SearchIndexResult {
|
|
42
|
+
const postingsByTerm = new Map<string, Hit[]>();
|
|
43
|
+
|
|
44
|
+
normalizedPageTexts.forEach((text, pageIndex) => {
|
|
45
|
+
const characters = Array.from(text);
|
|
46
|
+
if (characters.length < 2) return;
|
|
47
|
+
for (let index = 0; index <= characters.length - 2; index++) {
|
|
48
|
+
const gram = characters[index] + characters[index + 1];
|
|
49
|
+
let hits = postingsByTerm.get(gram);
|
|
50
|
+
if (!hits) {
|
|
51
|
+
hits = [];
|
|
52
|
+
postingsByTerm.set(gram, hits);
|
|
53
|
+
}
|
|
54
|
+
hits.push({ page: pageIndex, position: index });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const encoder = new TextEncoder();
|
|
59
|
+
const dictionaryChunks: Uint8Array[] = [];
|
|
60
|
+
const postingsChunks: Uint8Array[] = [];
|
|
61
|
+
const searchTerms: SearchTerm[] = [];
|
|
62
|
+
let dictionaryCursor = 0;
|
|
63
|
+
let postingsCursor = 0;
|
|
64
|
+
|
|
65
|
+
const terms = Array.from(postingsByTerm.keys()).sort();
|
|
66
|
+
for (const term of terms) {
|
|
67
|
+
const hits = postingsByTerm.get(term)!.sort((a, b) => (a.page === b.page ? a.position - b.position : a.page - b.page));
|
|
68
|
+
|
|
69
|
+
const termBytes = encoder.encode(term);
|
|
70
|
+
dictionaryChunks.push(termBytes);
|
|
71
|
+
const dictionaryOffset = dictionaryCursor;
|
|
72
|
+
dictionaryCursor += termBytes.length;
|
|
73
|
+
|
|
74
|
+
const postingsBytes = encodePostings(hits);
|
|
75
|
+
postingsChunks.push(postingsBytes);
|
|
76
|
+
const postingsOffset = postingsCursor;
|
|
77
|
+
postingsCursor += postingsBytes.length;
|
|
78
|
+
|
|
79
|
+
searchTerms.push({
|
|
80
|
+
term,
|
|
81
|
+
dictionaryOffset,
|
|
82
|
+
dictionaryLength: termBytes.length,
|
|
83
|
+
postingsOffset,
|
|
84
|
+
postingsLength: postingsBytes.length,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
dictionaryBlob: concatBytes(dictionaryChunks, dictionaryCursor),
|
|
90
|
+
postingsBlob: concatBytes(postingsChunks, postingsCursor),
|
|
91
|
+
searchTerms,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Decodes one term's postings byte range back into `(page, position)` hits. */
|
|
96
|
+
export function decodePostings(blob: Uint8Array): Hit[] {
|
|
97
|
+
const hits: Hit[] = [];
|
|
98
|
+
let offset = 0;
|
|
99
|
+
let previousPage = 0;
|
|
100
|
+
let previousPosition = 0;
|
|
101
|
+
let isFirst = true;
|
|
102
|
+
|
|
103
|
+
while (offset < blob.length) {
|
|
104
|
+
const first = readVarint(blob, offset);
|
|
105
|
+
offset = first.nextOffset;
|
|
106
|
+
const second = readVarint(blob, offset);
|
|
107
|
+
offset = second.nextOffset;
|
|
108
|
+
|
|
109
|
+
if (isFirst) {
|
|
110
|
+
previousPage = first.value;
|
|
111
|
+
previousPosition = second.value;
|
|
112
|
+
isFirst = false;
|
|
113
|
+
} else if (first.value === 0) {
|
|
114
|
+
previousPosition += second.value;
|
|
115
|
+
} else {
|
|
116
|
+
previousPage += first.value;
|
|
117
|
+
previousPosition = second.value;
|
|
118
|
+
}
|
|
119
|
+
hits.push({ page: previousPage, position: previousPosition });
|
|
120
|
+
}
|
|
121
|
+
return hits;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function encodePostings(hits: Hit[]): Uint8Array {
|
|
125
|
+
const bytes: number[] = [];
|
|
126
|
+
let previousPage = 0;
|
|
127
|
+
let previousPosition = 0;
|
|
128
|
+
hits.forEach((hit, index) => {
|
|
129
|
+
if (index === 0) {
|
|
130
|
+
writeVarint(hit.page, bytes);
|
|
131
|
+
writeVarint(hit.position, bytes);
|
|
132
|
+
} else if (hit.page === previousPage) {
|
|
133
|
+
writeVarint(0, bytes);
|
|
134
|
+
writeVarint(hit.position - previousPosition, bytes);
|
|
135
|
+
} else {
|
|
136
|
+
writeVarint(hit.page - previousPage, bytes);
|
|
137
|
+
writeVarint(hit.position, bytes);
|
|
138
|
+
}
|
|
139
|
+
previousPage = hit.page;
|
|
140
|
+
previousPosition = hit.position;
|
|
141
|
+
});
|
|
142
|
+
return Uint8Array.from(bytes);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function writeVarint(value: number, out: number[]): void {
|
|
146
|
+
let remaining = value >>> 0;
|
|
147
|
+
while (true) {
|
|
148
|
+
let byte = remaining & 0x7f;
|
|
149
|
+
remaining >>>= 7;
|
|
150
|
+
if (remaining !== 0) byte |= 0x80;
|
|
151
|
+
out.push(byte);
|
|
152
|
+
if (remaining === 0) break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readVarint(bytes: Uint8Array, offset: number): { value: number; nextOffset: number } {
|
|
157
|
+
let result = 0;
|
|
158
|
+
let shift = 0;
|
|
159
|
+
let cursor = offset;
|
|
160
|
+
while (true) {
|
|
161
|
+
const byte = bytes[cursor];
|
|
162
|
+
cursor += 1;
|
|
163
|
+
result |= (byte & 0x7f) << shift;
|
|
164
|
+
if ((byte & 0x80) === 0) break;
|
|
165
|
+
shift += 7;
|
|
166
|
+
}
|
|
167
|
+
return { value: result >>> 0, nextOffset: cursor };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function concatBytes(chunks: Uint8Array[], totalLength: number): Uint8Array {
|
|
171
|
+
const out = new Uint8Array(totalLength);
|
|
172
|
+
let offset = 0;
|
|
173
|
+
for (const chunk of chunks) {
|
|
174
|
+
out.set(chunk, offset);
|
|
175
|
+
offset += chunk.length;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|