sproutboat 0.28.0 → 0.29.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/CHANGELOG.md +21 -0
- package/SURFACE.md +3 -3
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/package.json +6 -6
- package/src/build.ts +12 -2
- package/src/compile.ts +23 -1
- package/src/main.ts +13 -2
- package/src/native-restore-sources.ts +48 -0
- package/src/native-restore.ts +155 -0
- package/src/surface.ts +2 -2
- package/vendor/ltx-restore/LICENSE +202 -0
- package/vendor/ltx-restore/NOTICE +32 -0
- package/vendor/ltx-restore/crc64-native.ts.txt +7 -0
- package/vendor/ltx-restore/errors.ts.txt +74 -0
- package/vendor/ltx-restore/file-io.ts.txt +54 -0
- package/vendor/ltx-restore/ltx/byte-reader.ts.txt +46 -0
- package/vendor/ltx-restore/ltx/bytes.ts.txt +87 -0
- package/vendor/ltx-restore/ltx/checksum.ts.txt +46 -0
- package/vendor/ltx-restore/ltx/constants.ts.txt +71 -0
- package/vendor/ltx-restore/ltx/crc64.ts.txt +73 -0
- package/vendor/ltx-restore/ltx/decoder.ts.txt +285 -0
- package/vendor/ltx-restore/ltx/header.ts.txt +126 -0
- package/vendor/ltx-restore/ltx/lz4.ts.txt +260 -0
- package/vendor/ltx-restore/ltx/page-index.ts.txt +140 -0
- package/vendor/ltx-restore/ltx/page.ts.txt +80 -0
- package/vendor/ltx-restore/ltx/sink.ts.txt +33 -0
- package/vendor/ltx-restore/ltx/trailer.ts.txt +58 -0
- package/vendor/ltx-restore/ltx/types.ts.txt +152 -0
- package/vendor/ltx-restore/lz4js/README.md +39 -0
- package/vendor/ltx-restore/lz4js/lz4.js.txt +560 -0
- package/vendor/ltx-restore/lz4js/package.json +36 -0
- package/vendor/ltx-restore/lz4js/util.js.txt +54 -0
- package/vendor/ltx-restore/lz4js/xxh32.js.txt +102 -0
- package/vendor/ltx-restore/program.ts.txt +3 -0
- package/vendor/ltx-restore/restore-native.ts.txt +120 -0
- package/vendor/ltx-restore/restore.ts.txt +96 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** CRC-64 ISO, compatible with Go hash/crc64. BigInt is used only at the API boundary. */
|
|
2
|
+
import { updateNativeCrc64 } from '../crc64-native.ts';
|
|
3
|
+
const HI = new Uint32Array(256);
|
|
4
|
+
const LO = new Uint32Array(256);
|
|
5
|
+
// Prototype controls: profiling flag, elapsed milliseconds, byte count, native backend.
|
|
6
|
+
export const CRC_PROFILE = new Float64Array(4);
|
|
7
|
+
for (let i = 0; i < 256; i++) {
|
|
8
|
+
let hi = 0, lo = i;
|
|
9
|
+
for (let j = 0; j < 8; j++) {
|
|
10
|
+
const carry = lo & 1;
|
|
11
|
+
lo = ((lo >>> 1) | ((hi & 1) << 31)) >>> 0;
|
|
12
|
+
hi >>>= 1;
|
|
13
|
+
if (carry) hi = (hi ^ 0xd8000000) >>> 0;
|
|
14
|
+
}
|
|
15
|
+
HI[i] = hi;
|
|
16
|
+
LO[i] = lo;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Crc64 {
|
|
20
|
+
private hi = 0xffffffff;
|
|
21
|
+
private lo = 0xffffffff;
|
|
22
|
+
private native = false;
|
|
23
|
+
private nativeState = new Uint8Array(8);
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
this.native = CRC_PROFILE[3] === 1;
|
|
27
|
+
this.nativeState.fill(255);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
update(buf: Uint8Array): void {
|
|
31
|
+
const started = CRC_PROFILE[0] ? performance.now() : 0;
|
|
32
|
+
if (this.native) {
|
|
33
|
+
updateNativeCrc64(this.nativeState, buf);
|
|
34
|
+
} else {
|
|
35
|
+
let hi = this.hi, lo = this.lo;
|
|
36
|
+
for (let i = 0; i < buf.length; i++) {
|
|
37
|
+
const t = (lo ^ buf[i]) & 0xff;
|
|
38
|
+
const nextLo = ((lo >>> 8) | (hi << 24)) >>> 0;
|
|
39
|
+
hi = (HI[t] ^ (hi >>> 8)) >>> 0;
|
|
40
|
+
lo = (LO[t] ^ nextLo) >>> 0;
|
|
41
|
+
}
|
|
42
|
+
this.hi = hi;
|
|
43
|
+
this.lo = lo;
|
|
44
|
+
}
|
|
45
|
+
if (CRC_PROFILE[0]) {
|
|
46
|
+
CRC_PROFILE[1] += performance.now() - started;
|
|
47
|
+
CRC_PROFILE[2] += buf.length;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
reset(): void {
|
|
52
|
+
this.hi = 0xffffffff;
|
|
53
|
+
this.lo = 0xffffffff;
|
|
54
|
+
this.nativeState.fill(255);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sum64(): bigint {
|
|
58
|
+
if (this.native) {
|
|
59
|
+
const s = this.nativeState;
|
|
60
|
+
const hi = (s[0] << 24 | s[1] << 16 | s[2] << 8 | s[3]) >>> 0;
|
|
61
|
+
const lo = (s[4] << 24 | s[5] << 16 | s[6] << 8 | s[7]) >>> 0;
|
|
62
|
+
return (BigInt((hi ^ 0xffffffff) >>> 0) << 32n) | BigInt((lo ^ 0xffffffff) >>> 0);
|
|
63
|
+
}
|
|
64
|
+
return (BigInt((this.hi ^ 0xffffffff) >>> 0) << 32n) |
|
|
65
|
+
BigInt((this.lo ^ 0xffffffff) >>> 0);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function crc64ISO(buf: Uint8Array): bigint {
|
|
70
|
+
const h = new Crc64();
|
|
71
|
+
h.update(buf);
|
|
72
|
+
return h.sum64();
|
|
73
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LTX decoder.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Go's `ltx.Decoder`. Reads header, page frames, page index, and
|
|
5
|
+
* trailer from an in-memory buffer, verifying the file checksum and (for
|
|
6
|
+
* checksum-tracking snapshots) the post-apply rolling checksum.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ChecksumFlag,
|
|
11
|
+
HeaderSize,
|
|
12
|
+
PageHeaderFlagSize,
|
|
13
|
+
PageHeaderSize,
|
|
14
|
+
TrailerChecksumOffset,
|
|
15
|
+
TrailerSize,
|
|
16
|
+
lockPgno,
|
|
17
|
+
} from './constants.ts';
|
|
18
|
+
import { Crc64 } from './crc64.ts';
|
|
19
|
+
import { u32BE } from './bytes.ts';
|
|
20
|
+
import { ByteReader } from './byte-reader.ts';
|
|
21
|
+
import type { ByteSink } from './sink.ts';
|
|
22
|
+
import type { Header, PageHeader, PageIndex, Trailer } from './types.ts';
|
|
23
|
+
import { isSnapshot } from './types.ts';
|
|
24
|
+
import { unmarshalHeader, validateHeader } from './header.ts';
|
|
25
|
+
import { unmarshalPageHeader } from './page.ts';
|
|
26
|
+
import { unmarshalTrailer } from './trailer.ts';
|
|
27
|
+
import { parsePageIndex } from './page-index.ts';
|
|
28
|
+
import { compressBlockBound, decompressBlock } from './lz4.ts';
|
|
29
|
+
import { checksumPage } from './checksum.ts';
|
|
30
|
+
import { LTXFormatError } from '../errors.ts';
|
|
31
|
+
|
|
32
|
+
const stateHeader = 'header';
|
|
33
|
+
const statePage = 'page';
|
|
34
|
+
const stateClose = 'close';
|
|
35
|
+
const stateClosed = 'closed';
|
|
36
|
+
|
|
37
|
+
function noChecksum(h: Header): boolean {
|
|
38
|
+
return (h.flags & 0x00000002) !== 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Decoder reads an LTX file from an in-memory buffer. */
|
|
42
|
+
export class Decoder {
|
|
43
|
+
private r: ByteReader;
|
|
44
|
+
private header!: Header;
|
|
45
|
+
private trailer: Trailer = { postApplyChecksum: 0n, fileChecksum: 0n };
|
|
46
|
+
private pageIndex: PageIndex | null = null;
|
|
47
|
+
private state = stateHeader;
|
|
48
|
+
private hash = new Crc64();
|
|
49
|
+
private pageSeq = new Crc64();
|
|
50
|
+
private chksum = 0n;
|
|
51
|
+
private pageN = 0;
|
|
52
|
+
private retainPageIndex: boolean;
|
|
53
|
+
|
|
54
|
+
constructor(buf: Uint8Array) {
|
|
55
|
+
this.r = new ByteReader(buf);
|
|
56
|
+
this.retainPageIndex = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setReader(reader: ByteReader): void { this.r = reader; }
|
|
60
|
+
|
|
61
|
+
/** SetRetainPageIndex controls whether Close materializes the page-index map. */
|
|
62
|
+
setRetainPageIndex(retain: boolean): void {
|
|
63
|
+
this.retainPageIndex = retain;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** N returns the number of source bytes read. */
|
|
67
|
+
N(): number {
|
|
68
|
+
return this.r.pos;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** PageN returns the number of pages decoded. */
|
|
72
|
+
PageN(): number {
|
|
73
|
+
return this.pageN;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Header returns a copy of the header. */
|
|
77
|
+
Header(): Header {
|
|
78
|
+
return this.header;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Trailer returns the trailer. File checksum valid after Close(). */
|
|
82
|
+
Trailer(): Trailer {
|
|
83
|
+
return this.trailer;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** PageIndex returns the page index (after Close), or null if not retained. */
|
|
87
|
+
PageIndex(): PageIndex | null {
|
|
88
|
+
return this.pageIndex;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** PostApplyPos returns the replication position after applying this file. */
|
|
92
|
+
PostApplyPos() {
|
|
93
|
+
return {
|
|
94
|
+
txID: this.header.maxTXID,
|
|
95
|
+
postApplyChecksum: this.trailer.postApplyChecksum,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** DecodeHeader reads and validates the header frame. */
|
|
100
|
+
decodeHeader(): void {
|
|
101
|
+
const b = this.r.readFull(HeaderSize);
|
|
102
|
+
this.header = unmarshalHeader(b);
|
|
103
|
+
this.hash.update(b);
|
|
104
|
+
this.state = statePage;
|
|
105
|
+
validateHeader(this.header);
|
|
106
|
+
if (!noChecksum(this.header)) {
|
|
107
|
+
this.chksum = ChecksumFlag;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* DecodePage reads the next page frame into `data`. Returns the page header,
|
|
113
|
+
* or null when the end-of-pages marker is reached.
|
|
114
|
+
*/
|
|
115
|
+
decodePage(data: Uint8Array): PageHeader | null {
|
|
116
|
+
if (this.state === stateClosed) throw new LTXFormatError('ltx decoder closed');
|
|
117
|
+
if (this.state === stateClose) return null;
|
|
118
|
+
if (this.state !== statePage) {
|
|
119
|
+
throw new LTXFormatError(`cannot read page header, expected ${this.state}`);
|
|
120
|
+
}
|
|
121
|
+
if (data.length !== this.header.pageSize) {
|
|
122
|
+
throw new LTXFormatError(`invalid page buffer size: ${data.length}, expecting ${this.header.pageSize}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const b = this.r.readFull(PageHeaderSize);
|
|
126
|
+
const hdr = unmarshalPageHeader(b);
|
|
127
|
+
this.hash.update(b);
|
|
128
|
+
|
|
129
|
+
// An empty page header marks the end of the page block.
|
|
130
|
+
if (hdr.pgno === 0 && hdr.flags === 0) {
|
|
131
|
+
this.state = stateClose;
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (hdr.pgno > this.header.commit) {
|
|
136
|
+
throw new LTXFormatError(`page number ${hdr.pgno} out-of-bounds for commit size ${this.header.commit}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (hdr.flags & PageHeaderFlagSize) {
|
|
140
|
+
// New block format: read size prefix, then LZ4 block data.
|
|
141
|
+
const sizeBuf = this.r.readFull(4);
|
|
142
|
+
this.hash.update(sizeBuf);
|
|
143
|
+
const dataSize = (sizeBuf[0] << 24 | sizeBuf[1] << 16 | sizeBuf[2] << 8 | sizeBuf[3]) >>> 0;
|
|
144
|
+
if (dataSize < 1 || dataSize > compressBlockBound(this.header.pageSize)) throw new LTXFormatError('invalid compressed page size');
|
|
145
|
+
const compressed = this.r.readFull(dataSize);
|
|
146
|
+
const decompressed = decompressBlock(compressed, this.header.pageSize);
|
|
147
|
+
data.set(decompressed);
|
|
148
|
+
} else {
|
|
149
|
+
throw new LTXFormatError('legacy LZ4 frame format not supported');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.hash.update(data);
|
|
153
|
+
this.hashPgo(this.pageSeq, hdr.pgno);
|
|
154
|
+
this.pageN++;
|
|
155
|
+
|
|
156
|
+
// Track the rolling DB checksum for checksum-tracking snapshots.
|
|
157
|
+
if (isSnapshot(this.header) && !noChecksum(this.header)) {
|
|
158
|
+
if (hdr.pgno !== lockPgno(this.header.pageSize)) {
|
|
159
|
+
this.chksum = ChecksumFlag | (this.chksum ^ checksumPage(hdr.pgno, data));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return hdr;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Verify reads the entire file, discarding page data (for validation). */
|
|
167
|
+
verify(): void {
|
|
168
|
+
this.decodeHeader();
|
|
169
|
+
const data = new Uint8Array(this.header.pageSize);
|
|
170
|
+
for (;;) {
|
|
171
|
+
if (this.decodePage(data) === null) break;
|
|
172
|
+
}
|
|
173
|
+
this.close();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** DecodeDatabaseTo writes the snapshot as a SQLite database to `sink`. */
|
|
177
|
+
decodeDatabaseTo(sink: ByteSink): void {
|
|
178
|
+
this.decodeHeader();
|
|
179
|
+
const hdr = this.header;
|
|
180
|
+
if (!isSnapshot(hdr)) {
|
|
181
|
+
throw new LTXFormatError('cannot decode non-snapshot LTX file to SQLite database');
|
|
182
|
+
}
|
|
183
|
+
const lockPg = lockPgno(hdr.pageSize);
|
|
184
|
+
const data = new Uint8Array(hdr.pageSize);
|
|
185
|
+
for (let pgno = 1; pgno <= hdr.commit; pgno++) {
|
|
186
|
+
if (pgno === lockPg) {
|
|
187
|
+
sink.write(new Uint8Array(hdr.pageSize));
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const ph = this.decodePage(data);
|
|
191
|
+
if (ph === null) {
|
|
192
|
+
throw new LTXFormatError(`unexpected end of pages at pgno ${pgno}`);
|
|
193
|
+
}
|
|
194
|
+
if (ph.pgno !== pgno) {
|
|
195
|
+
throw new LTXFormatError(`unexpected pgno while decoding page: read ${ph.pgno}, expected ${pgno}`);
|
|
196
|
+
}
|
|
197
|
+
sink.write(data);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Expect one final read to be the end-of-pages marker.
|
|
201
|
+
if (this.decodePage(data) !== null) {
|
|
202
|
+
throw new LTXFormatError('unexpected page after end of database');
|
|
203
|
+
}
|
|
204
|
+
this.close();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Close verifies the page index and trailer, then checks the file checksum. */
|
|
208
|
+
close(): void {
|
|
209
|
+
if (this.state === stateClosed) return;
|
|
210
|
+
if (this.state !== stateClose) {
|
|
211
|
+
throw new LTXFormatError(`cannot close, expected ${this.state}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const index: PageIndex = new Map();
|
|
215
|
+
let n = 0;
|
|
216
|
+
let nextOffset = HeaderSize;
|
|
217
|
+
const seq = new Crc64();
|
|
218
|
+
const self = this;
|
|
219
|
+
|
|
220
|
+
parsePageIndex(
|
|
221
|
+
this.r,
|
|
222
|
+
(buf) => self.hash.update(buf),
|
|
223
|
+
(pgno, offset, size) => {
|
|
224
|
+
if (pgno > self.header.commit) {
|
|
225
|
+
throw new LTXFormatError(`page index pgno ${pgno} exceeds commit ${self.header.commit}`);
|
|
226
|
+
}
|
|
227
|
+
if (offset < nextOffset) {
|
|
228
|
+
throw new LTXFormatError(
|
|
229
|
+
`page index pgno ${pgno} offset ${offset} overlaps previous frame ending at ${nextOffset}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (size <= PageHeaderSize) {
|
|
233
|
+
throw new LTXFormatError(`page index pgno ${pgno} has invalid frame size ${size}`);
|
|
234
|
+
}
|
|
235
|
+
nextOffset = offset + size;
|
|
236
|
+
n++;
|
|
237
|
+
self.hashPgo(seq, pgno);
|
|
238
|
+
if (self.retainPageIndex) {
|
|
239
|
+
index.set(pgno, {
|
|
240
|
+
level: 0,
|
|
241
|
+
minTXID: self.header.minTXID,
|
|
242
|
+
maxTXID: self.header.maxTXID,
|
|
243
|
+
offset,
|
|
244
|
+
size,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (n !== this.pageN) {
|
|
251
|
+
throw new LTXFormatError(`page index has ${n} entries but ${this.pageN} pages were decoded`);
|
|
252
|
+
}
|
|
253
|
+
if (seq.sum64() !== this.pageSeq.sum64()) {
|
|
254
|
+
throw new LTXFormatError('page index does not match the decoded page numbers');
|
|
255
|
+
}
|
|
256
|
+
this.pageIndex = this.retainPageIndex ? index : null;
|
|
257
|
+
|
|
258
|
+
// Read the trailer; everything except the file checksum field is hashed.
|
|
259
|
+
const t = this.r.readFull(TrailerSize);
|
|
260
|
+
this.hash.update(t.subarray(0, TrailerChecksumOffset));
|
|
261
|
+
this.trailer = unmarshalTrailer(t);
|
|
262
|
+
if (this.r.remaining > 0) {
|
|
263
|
+
throw new LTXFormatError('unexpected data after trailer');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const computed = ChecksumFlag | this.hash.sum64();
|
|
267
|
+
if (computed !== this.trailer.fileChecksum) {
|
|
268
|
+
throw new LTXFormatError('file checksum mismatch');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (isSnapshot(this.header) && !noChecksum(this.header)) {
|
|
272
|
+
if (this.trailer.postApplyChecksum !== this.chksum) {
|
|
273
|
+
throw new LTXFormatError(
|
|
274
|
+
`post-apply checksum in trailer (${this.trailer.postApplyChecksum.toString(16)}) does not match calculated checksum (${this.chksum.toString(16)})`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.state = stateClosed;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private hashPgo(h: Crc64, pgno: number): void {
|
|
283
|
+
h.update(u32BE(pgno));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LTX header frame marshalling.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { HeaderSize, Magic, Version } from './constants.ts';
|
|
6
|
+
import {
|
|
7
|
+
ChecksumFlag,
|
|
8
|
+
isValidHeaderFlags,
|
|
9
|
+
isValidPageSize,
|
|
10
|
+
} from './constants.ts';
|
|
11
|
+
import { assertFull, readU32BE, readU64BE, writeU32BE, writeU64BE } from './bytes.ts';
|
|
12
|
+
import type { Header } from './types.ts';
|
|
13
|
+
import { isSnapshot } from './types.ts';
|
|
14
|
+
import { lockPgno } from './constants.ts';
|
|
15
|
+
import { LTXFormatError } from '../errors.ts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* MarshalBinary encodes a header to its 100-byte representation.
|
|
19
|
+
*/
|
|
20
|
+
export function marshalHeader(h: Header): Uint8Array {
|
|
21
|
+
const b = new Uint8Array(HeaderSize);
|
|
22
|
+
for (let i = 0; i < 4; i++) b[i] = Magic.charCodeAt(i);
|
|
23
|
+
writeU32BE(b, 4, h.flags);
|
|
24
|
+
writeU32BE(b, 8, h.pageSize);
|
|
25
|
+
writeU32BE(b, 12, h.commit);
|
|
26
|
+
writeU64BE(b, 16, h.minTXID);
|
|
27
|
+
writeU64BE(b, 24, h.maxTXID);
|
|
28
|
+
writeU64BE(b, 32, h.timestamp);
|
|
29
|
+
writeU64BE(b, 40, h.preApplyChecksum);
|
|
30
|
+
writeU64BE(b, 48, h.walOffset);
|
|
31
|
+
writeU64BE(b, 56, h.walSize);
|
|
32
|
+
writeU32BE(b, 64, h.walSalt1);
|
|
33
|
+
writeU32BE(b, 68, h.walSalt2);
|
|
34
|
+
writeU64BE(b, 72, h.nodeID);
|
|
35
|
+
return b;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* UnmarshalBinary decodes a header from a byte slice.
|
|
40
|
+
* Sets `version` from the magic and returns the header.
|
|
41
|
+
*/
|
|
42
|
+
export function unmarshalHeader(b: Uint8Array): Header {
|
|
43
|
+
assertFull(b, 0, HeaderSize);
|
|
44
|
+
const magic = String.fromCharCode(b[0], b[1], b[2], b[3]);
|
|
45
|
+
if (magic !== Magic) {
|
|
46
|
+
throw new LTXFormatError('invalid LTX file');
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
version: Version,
|
|
50
|
+
flags: readU32BE(b, 4),
|
|
51
|
+
pageSize: readU32BE(b, 8),
|
|
52
|
+
commit: readU32BE(b, 12),
|
|
53
|
+
minTXID: readU64BE(b, 16),
|
|
54
|
+
maxTXID: readU64BE(b, 24),
|
|
55
|
+
timestamp: readU64BE(b, 32),
|
|
56
|
+
preApplyChecksum: readU64BE(b, 40),
|
|
57
|
+
walOffset: readU64BE(b, 48),
|
|
58
|
+
walSize: readU64BE(b, 56),
|
|
59
|
+
walSalt1: readU32BE(b, 64),
|
|
60
|
+
walSalt2: readU32BE(b, 68),
|
|
61
|
+
nodeID: readU64BE(b, 72),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function noChecksum(flags: number): boolean {
|
|
66
|
+
return (flags & 0x00000002) !== 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Validate returns an error (thrown) if h is invalid. */
|
|
70
|
+
export function validateHeader(h: Header): void {
|
|
71
|
+
if (h.version !== Version) {
|
|
72
|
+
throw new LTXFormatError('invalid version');
|
|
73
|
+
}
|
|
74
|
+
if (!isValidHeaderFlags(h.flags)) {
|
|
75
|
+
throw new LTXFormatError(`invalid flags: 0x${h.flags.toString(16)}`);
|
|
76
|
+
}
|
|
77
|
+
if (!isValidPageSize(h.pageSize)) {
|
|
78
|
+
throw new LTXFormatError(`invalid page size: ${h.pageSize}`);
|
|
79
|
+
}
|
|
80
|
+
if (h.minTXID === 0n) {
|
|
81
|
+
throw new LTXFormatError('minimum transaction id required');
|
|
82
|
+
}
|
|
83
|
+
if (h.maxTXID === 0n) {
|
|
84
|
+
throw new LTXFormatError('maximum transaction id required');
|
|
85
|
+
}
|
|
86
|
+
if (h.minTXID > h.maxTXID) {
|
|
87
|
+
throw new LTXFormatError(`transaction ids out of order: (${h.minTXID},${h.maxTXID})`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (h.walOffset < 0n) {
|
|
91
|
+
throw new LTXFormatError(`wal offset cannot be negative: ${h.walOffset}`);
|
|
92
|
+
}
|
|
93
|
+
if (h.walSize < 0n) {
|
|
94
|
+
throw new LTXFormatError(`wal size cannot be negative: ${h.walSize}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ((h.walSalt1 !== 0 || h.walSalt2 !== 0) && h.walOffset === 0n) {
|
|
98
|
+
throw new LTXFormatError('wal offset required if salt exists');
|
|
99
|
+
}
|
|
100
|
+
if (h.walOffset === 0n && h.walSize !== 0n) {
|
|
101
|
+
throw new LTXFormatError('wal offset required if wal size exists');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const snap = isSnapshot(h);
|
|
105
|
+
const nc = noChecksum(h.flags);
|
|
106
|
+
if (snap) {
|
|
107
|
+
if (h.preApplyChecksum !== 0n) {
|
|
108
|
+
throw new LTXFormatError('pre-apply checksum must be zero on snapshots');
|
|
109
|
+
}
|
|
110
|
+
} else if (nc) {
|
|
111
|
+
if (h.preApplyChecksum !== 0n) {
|
|
112
|
+
throw new LTXFormatError('pre-apply checksum not allowed');
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
if (h.preApplyChecksum === 0n) {
|
|
116
|
+
throw new LTXFormatError('pre-apply checksum required on non-snapshot files');
|
|
117
|
+
}
|
|
118
|
+
if ((h.preApplyChecksum & ChecksumFlag) === 0n) {
|
|
119
|
+
throw new LTXFormatError('invalid pre-apply checksum format');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function headerLockPgno(h: Header): number {
|
|
125
|
+
return lockPgno(h.pageSize);
|
|
126
|
+
}
|