-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchunksSection.js
More file actions
111 lines (107 loc) · 3.8 KB
/
Copy pathchunksSection.js
File metadata and controls
111 lines (107 loc) · 3.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
* Shared encode/decode for the "chunks section" embedded inside `DoubleSyncPatch`
* and `DoubleSyncDiffPatch` — both use exactly the same wire layout:
*
* 0 4 chunkCount (u32, little-endian)
* then per chunk:
* 32B hash
* 4B length (u32)
* length bytes of content
*
* Keeping the format in one place guarantees the two patch types stay
* byte-compatible and lets the receiver-mirror logic in `DoubleSyncSession`
* iterate either with the same code.
*/
/**
* Walk a chunks-section subarray once and produce an offset index so subsequent
* iteration is O(N) instead of O(N²).
*
* @param {Uint8Array} section
* @param {string} owner - prefix used in thrown error messages ("DoubleSyncPatch", ...)
* @returns {{ chunkCount: number, index: Array<{hashOff: number, dataOff: number, length: number}> }}
*/
export function parseChunksSection(section, owner) {
if (section.length < 4) {
throw new Error(`${owner}: chunks section too short to hold count`);
}
const dv = new DataView(section.buffer, section.byteOffset, section.byteLength);
const chunkCount = dv.getUint32(0, true);
const index = [];
let cur = 4;
for (let i = 0; i < chunkCount; i++) {
if (cur + 32 + 4 > section.length) {
throw new Error(`${owner}: truncated chunk header at index ${i}`);
}
const hashOff = cur;
const length = dv.getUint32(cur + 32, true);
const dataOff = cur + 32 + 4;
if (dataOff + length > section.length) {
throw new Error(`${owner}: truncated chunk body at index ${i}`);
}
index.push({ hashOff, dataOff, length });
cur = dataOff + length;
}
if (cur !== section.length) {
throw new Error(`${owner}: trailing ${section.length - cur} bytes after chunks`);
}
return { chunkCount, index };
}
/**
* Iterate `(hash, bytes)` for every chunk in `section` using a prebuilt `index`
* from `parseChunksSection`. Both yielded views are zero-copy subarrays — do not mutate.
*
* @param {Uint8Array} section
* @param {Array<{hashOff: number, dataOff: number, length: number}>} index
* @yields {{hash: Uint8Array, bytes: Uint8Array}}
*/
export function* iterateChunks(section, index) {
for (const rec of index) {
yield {
hash: section.subarray(rec.hashOff, rec.hashOff + 32),
bytes: section.subarray(rec.dataOff, rec.dataOff + rec.length),
};
}
}
/**
* Validate every record and return the byte length the section will consume.
*
* @param {Array<{hash: Uint8Array, bytes: Uint8Array}>} records
* @param {string} owner - prefix used in thrown error messages
* @returns {number}
*/
export function measureChunksSection(records, owner) {
let len = 4; // chunkCount header
for (const c of records) {
if (!(c.hash instanceof Uint8Array) || c.hash.length !== 32) {
throw new Error(`${owner}: each chunk.hash must be a 32-byte Uint8Array`);
}
if (!(c.bytes instanceof Uint8Array)) {
throw new Error(`${owner}: each chunk.bytes must be Uint8Array`);
}
len += 32 + 4 + c.bytes.length;
}
return len;
}
/**
* Write the section into `out` at byte offset `cur`. Caller must have allocated
* enough space (use `measureChunksSection` to compute it).
*
* @param {Uint8Array} out
* @param {DataView} dv
* @param {number} cur
* @param {Array<{hash: Uint8Array, bytes: Uint8Array}>} records
* @returns {number} new cursor position
*/
export function writeChunksSection(out, dv, cur, records) {
dv.setUint32(cur, records.length, true);
cur += 4;
for (const c of records) {
out.set(c.hash, cur);
cur += 32;
dv.setUint32(cur, c.bytes.length, true);
cur += 4;
out.set(c.bytes, cur);
cur += c.bytes.length;
}
return cur;
}