@coderook/cli 0.22.0 → 0.22.2
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/.claude-plugin/plugin.json +1 -1
- package/dist/cli/src/cli.js +50 -41
- package/dist/cli/src/offline.js +28 -0
- package/dist/desktop-app/src/main/compress.js +6 -31
- package/dist/desktop-app/src/main/download.js +200 -4
- package/dist/desktop-app/src/main/upload.js +602 -55
- package/dist/desktop-app/src/shared/chunking.js +21 -1
- package/dist/desktop-app/src/shared/compression_policy.js +35 -0
- package/dist/desktop-app/src/shared/delta.js +772 -0
- package/dist/desktop-app/src/shared/telemetry.js +47 -0
- package/package.json +1 -1
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CodeRook's changed-file transport delta.
|
|
4
|
+
*
|
|
5
|
+
* This is deliberately not a fourth permanent storage shape. The receiver
|
|
6
|
+
* describes blocks from the complete file it already has, the sender emits a
|
|
7
|
+
* short stream of COPY and INSERT operations, and the receiver reconstructs a
|
|
8
|
+
* complete object before it is admitted to the ordinary object catalogue.
|
|
9
|
+
*
|
|
10
|
+
* The shape follows the useful part of rsync and Git pack deltas without
|
|
11
|
+
* inheriting delta chains: a rolling weak checksum finds candidate blocks, a
|
|
12
|
+
* second checksum rejects ordinary collisions, and the final file SHA-256 is
|
|
13
|
+
* still the authority at the API boundary.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.DELTA_BATCH_MAX_SOURCE_BYTES = exports.DELTA_BATCH_MAX_FILES = exports.DELTA_MAX_FILE_BYTES = void 0;
|
|
17
|
+
exports.deltaBlockSize = deltaBlockSize;
|
|
18
|
+
exports.estimateDeltaSignatureBytes = estimateDeltaSignatureBytes;
|
|
19
|
+
exports.createDeltaSignature = createDeltaSignature;
|
|
20
|
+
exports.parseDeltaSignature = parseDeltaSignature;
|
|
21
|
+
exports.createDeltaPatch = createDeltaPatch;
|
|
22
|
+
exports.deltaPatchTargetSize = deltaPatchTargetSize;
|
|
23
|
+
exports.applyDeltaPatch = applyDeltaPatch;
|
|
24
|
+
exports.encodeDeltaEnvelope = encodeDeltaEnvelope;
|
|
25
|
+
exports.isDeltaEnvelope = isDeltaEnvelope;
|
|
26
|
+
exports.decodeDeltaEnvelope = decodeDeltaEnvelope;
|
|
27
|
+
exports.encodeDeltaSignatureBatch = encodeDeltaSignatureBatch;
|
|
28
|
+
exports.decodeDeltaSignatureBatch = decodeDeltaSignatureBatch;
|
|
29
|
+
exports.encodeDeltaBatchEnvelope = encodeDeltaBatchEnvelope;
|
|
30
|
+
exports.decodeDeltaBatchEnvelope = decodeDeltaBatchEnvelope;
|
|
31
|
+
const SIGNATURE_MAGIC_V1 = [0x43, 0x52, 0x53, 0x31]; // CRS1
|
|
32
|
+
const SIGNATURE_MAGIC_V2 = [0x43, 0x52, 0x53, 0x32]; // CRS2
|
|
33
|
+
const PATCH_MAGIC_V1 = [0x43, 0x52, 0x50, 0x31]; // CRP1
|
|
34
|
+
const PATCH_MAGIC_V2 = [0x43, 0x52, 0x50, 0x32]; // CRP2
|
|
35
|
+
const ENVELOPE_MAGIC_V2 = [0x43, 0x44, 0x45, 0x32]; // CDE2
|
|
36
|
+
const SIGNATURE_BATCH_MAGIC_V2 = [0x43, 0x44, 0x53, 0x32]; // CDS2
|
|
37
|
+
const DELTA_BATCH_MAGIC_V2 = [0x43, 0x44, 0x42, 0x32]; // CDB2
|
|
38
|
+
const SIGNATURE_HEADER_V1_BYTES = 16;
|
|
39
|
+
const SIGNATURE_HEADER_V2_BYTES = 12;
|
|
40
|
+
const SIGNATURE_RECORD_V1_BYTES = 8;
|
|
41
|
+
const SIGNATURE_RECORD_V2_BYTES = 6;
|
|
42
|
+
const PATCH_HEADER_V1_BYTES = 12;
|
|
43
|
+
const COPY_BYTES = 9;
|
|
44
|
+
const LITERAL_HEADER_BYTES = 5;
|
|
45
|
+
const ENVELOPE_V2_FIXED_BYTES = 55;
|
|
46
|
+
const MOD_ADLER = 65_521;
|
|
47
|
+
/** Bound Worker memory and make malformed requests cheap to reject. */
|
|
48
|
+
exports.DELTA_MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
49
|
+
exports.DELTA_BATCH_MAX_FILES = 64;
|
|
50
|
+
exports.DELTA_BATCH_MAX_SOURCE_BYTES = 24 * 1024 * 1024;
|
|
51
|
+
function sameMagic(bytes, expected) {
|
|
52
|
+
return expected.every((value, index) => bytes[index] === value);
|
|
53
|
+
}
|
|
54
|
+
function nextPowerOfTwo(value) {
|
|
55
|
+
let power = 1;
|
|
56
|
+
while (power < value && power < 0x4000_0000)
|
|
57
|
+
power *= 2;
|
|
58
|
+
return power;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Keep signature replies small while retaining fine matches.
|
|
62
|
+
*
|
|
63
|
+
* sqrt(file size), rounded to a power of two, is the traditional useful shape
|
|
64
|
+
* for receiver signatures: an 8 KiB file uses 128-byte blocks and sends only
|
|
65
|
+
* 512 bytes of checksum records; an 8 MiB file uses 4 KiB blocks and sends
|
|
66
|
+
* 16 KiB of records.
|
|
67
|
+
*/
|
|
68
|
+
function deltaBlockSize(size, version = 1) {
|
|
69
|
+
/*
|
|
70
|
+
V2 minimizes the bytes on both sides, rather than only the number of
|
|
71
|
+
literal bytes in the patch. Six checksum bytes are paid for every block,
|
|
72
|
+
while a narrow edit normally invalidates one complete block. The minimum
|
|
73
|
+
of those two costs is close to sqrt(fileSize * recordSize).
|
|
74
|
+
*/
|
|
75
|
+
const weight = version === 2 ? SIGNATURE_RECORD_V2_BYTES : 1;
|
|
76
|
+
const wanted = Math.ceil(Math.sqrt(Math.max(1, size) * weight));
|
|
77
|
+
return Math.min(16 * 1024, Math.max(64, nextPowerOfTwo(wanted)));
|
|
78
|
+
}
|
|
79
|
+
function weakChecksum(bytes, offset, length) {
|
|
80
|
+
let a = 0;
|
|
81
|
+
let b = 0;
|
|
82
|
+
for (let index = 0; index < length; index += 1) {
|
|
83
|
+
const value = bytes[offset + index] ?? 0;
|
|
84
|
+
a += value;
|
|
85
|
+
b += (length - index) * value;
|
|
86
|
+
}
|
|
87
|
+
a %= MOD_ADLER;
|
|
88
|
+
b %= MOD_ADLER;
|
|
89
|
+
return (((b << 16) | a) >>> 0);
|
|
90
|
+
}
|
|
91
|
+
function rollWeak(checksum, outgoing, incoming, length) {
|
|
92
|
+
let a = (checksum & 0xffff) - outgoing + incoming;
|
|
93
|
+
a %= MOD_ADLER;
|
|
94
|
+
if (a < 0)
|
|
95
|
+
a += MOD_ADLER;
|
|
96
|
+
let b = (checksum >>> 16) - length * outgoing + a;
|
|
97
|
+
b %= MOD_ADLER;
|
|
98
|
+
if (b < 0)
|
|
99
|
+
b += MOD_ADLER;
|
|
100
|
+
return (((b << 16) | a) >>> 0);
|
|
101
|
+
}
|
|
102
|
+
/** A fast second opinion. Final reconstruction is still verified by SHA-256. */
|
|
103
|
+
function strongChecksum(bytes, offset, length) {
|
|
104
|
+
let hash = 0x811c9dc5;
|
|
105
|
+
for (let index = 0; index < length; index += 1) {
|
|
106
|
+
hash ^= bytes[offset + index] ?? 0;
|
|
107
|
+
hash = Math.imul(hash, 0x01000193);
|
|
108
|
+
}
|
|
109
|
+
return hash >>> 0;
|
|
110
|
+
}
|
|
111
|
+
function estimateDeltaSignatureBytes(baseSize, version = 1) {
|
|
112
|
+
const blockSize = deltaBlockSize(baseSize, version);
|
|
113
|
+
return version === 2
|
|
114
|
+
? SIGNATURE_HEADER_V2_BYTES +
|
|
115
|
+
Math.floor(baseSize / blockSize) * SIGNATURE_RECORD_V2_BYTES
|
|
116
|
+
: SIGNATURE_HEADER_V1_BYTES +
|
|
117
|
+
Math.floor(baseSize / blockSize) * SIGNATURE_RECORD_V1_BYTES;
|
|
118
|
+
}
|
|
119
|
+
function createDeltaSignature(base, version = 1) {
|
|
120
|
+
if (base.byteLength > exports.DELTA_MAX_FILE_BYTES) {
|
|
121
|
+
throw new Error("Delta base exceeds the bounded transport limit");
|
|
122
|
+
}
|
|
123
|
+
const blockSize = deltaBlockSize(base.byteLength, version);
|
|
124
|
+
const count = Math.floor(base.byteLength / blockSize);
|
|
125
|
+
const headerBytes = version === 2 ? SIGNATURE_HEADER_V2_BYTES : SIGNATURE_HEADER_V1_BYTES;
|
|
126
|
+
const recordBytes = version === 2 ? SIGNATURE_RECORD_V2_BYTES : SIGNATURE_RECORD_V1_BYTES;
|
|
127
|
+
const out = new Uint8Array(headerBytes + count * recordBytes);
|
|
128
|
+
out.set(version === 2 ? SIGNATURE_MAGIC_V2 : SIGNATURE_MAGIC_V1, 0);
|
|
129
|
+
const view = new DataView(out.buffer);
|
|
130
|
+
if (version === 2) {
|
|
131
|
+
view.setUint16(4, blockSize, false);
|
|
132
|
+
out[6] = 16;
|
|
133
|
+
out[7] = 0;
|
|
134
|
+
view.setUint32(8, base.byteLength, false);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
view.setUint32(4, blockSize, false);
|
|
138
|
+
view.setUint32(8, base.byteLength, false);
|
|
139
|
+
view.setUint32(12, count, false);
|
|
140
|
+
}
|
|
141
|
+
for (let index = 0; index < count; index += 1) {
|
|
142
|
+
const offset = index * blockSize;
|
|
143
|
+
const at = headerBytes + index * recordBytes;
|
|
144
|
+
view.setUint32(at, weakChecksum(base, offset, blockSize), false);
|
|
145
|
+
if (version === 2) {
|
|
146
|
+
view.setUint16(at + 4, strongChecksum(base, offset, blockSize) & 0xffff, false);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
view.setUint32(at + 4, strongChecksum(base, offset, blockSize), false);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function parseDeltaSignature(bytes) {
|
|
155
|
+
const version = sameMagic(bytes, SIGNATURE_MAGIC_V2)
|
|
156
|
+
? 2
|
|
157
|
+
: sameMagic(bytes, SIGNATURE_MAGIC_V1)
|
|
158
|
+
? 1
|
|
159
|
+
: 0;
|
|
160
|
+
const headerBytes = version === 2 ? SIGNATURE_HEADER_V2_BYTES : SIGNATURE_HEADER_V1_BYTES;
|
|
161
|
+
const recordBytes = version === 2 ? SIGNATURE_RECORD_V2_BYTES : SIGNATURE_RECORD_V1_BYTES;
|
|
162
|
+
if (!version || bytes.byteLength < headerBytes) {
|
|
163
|
+
throw new Error("Delta signature is malformed");
|
|
164
|
+
}
|
|
165
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
166
|
+
const blockSize = version === 2 ? view.getUint16(4, false) : view.getUint32(4, false);
|
|
167
|
+
const baseSize = view.getUint32(8, false);
|
|
168
|
+
const count = version === 2 ? Math.floor(baseSize / blockSize) : view.getUint32(12, false);
|
|
169
|
+
if (blockSize < 64 ||
|
|
170
|
+
blockSize > 16 * 1024 ||
|
|
171
|
+
baseSize > exports.DELTA_MAX_FILE_BYTES ||
|
|
172
|
+
(version === 2 && (bytes[6] !== 16 || bytes[7] !== 0)) ||
|
|
173
|
+
count !== Math.floor(baseSize / blockSize) ||
|
|
174
|
+
bytes.byteLength !== headerBytes + count * recordBytes) {
|
|
175
|
+
throw new Error("Delta signature dimensions are invalid");
|
|
176
|
+
}
|
|
177
|
+
const blocks = [];
|
|
178
|
+
for (let index = 0; index < count; index += 1) {
|
|
179
|
+
const at = headerBytes + index * recordBytes;
|
|
180
|
+
blocks.push({
|
|
181
|
+
weak: view.getUint32(at, false),
|
|
182
|
+
strong: version === 2
|
|
183
|
+
? view.getUint16(at + 4, false)
|
|
184
|
+
: view.getUint32(at + 4, false),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
version,
|
|
189
|
+
blockSize,
|
|
190
|
+
baseSize,
|
|
191
|
+
strongBits: version === 2 ? 16 : 32,
|
|
192
|
+
blocks,
|
|
193
|
+
byteLength: bytes.byteLength,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function appendOperation(operations, operation) {
|
|
197
|
+
if (operation.kind === "literal" && operation.bytes.byteLength === 0)
|
|
198
|
+
return;
|
|
199
|
+
if (operation.kind === "copy" && operation.length === 0)
|
|
200
|
+
return;
|
|
201
|
+
const previous = operations[operations.length - 1];
|
|
202
|
+
if (previous?.kind === "copy" &&
|
|
203
|
+
operation.kind === "copy" &&
|
|
204
|
+
previous.offset + previous.length === operation.offset) {
|
|
205
|
+
previous.length += operation.length;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (previous?.kind === "literal" && operation.kind === "literal") {
|
|
209
|
+
const joined = new Uint8Array(previous.bytes.byteLength + operation.bytes.byteLength);
|
|
210
|
+
joined.set(previous.bytes);
|
|
211
|
+
joined.set(operation.bytes, previous.bytes.byteLength);
|
|
212
|
+
previous.bytes = joined;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
operations.push(operation);
|
|
216
|
+
}
|
|
217
|
+
function varUintBytes(value) {
|
|
218
|
+
let remaining = value >>> 0;
|
|
219
|
+
let count = 1;
|
|
220
|
+
while (remaining >= 0x80) {
|
|
221
|
+
remaining >>>= 7;
|
|
222
|
+
count += 1;
|
|
223
|
+
}
|
|
224
|
+
return count;
|
|
225
|
+
}
|
|
226
|
+
function writeVarUint(out, offset, value) {
|
|
227
|
+
let remaining = value >>> 0;
|
|
228
|
+
let at = offset;
|
|
229
|
+
while (remaining >= 0x80) {
|
|
230
|
+
out[at] = (remaining & 0x7f) | 0x80;
|
|
231
|
+
remaining >>>= 7;
|
|
232
|
+
at += 1;
|
|
233
|
+
}
|
|
234
|
+
out[at] = remaining;
|
|
235
|
+
return at + 1;
|
|
236
|
+
}
|
|
237
|
+
function readVarUint(bytes, offset) {
|
|
238
|
+
let value = 0;
|
|
239
|
+
let shift = 0;
|
|
240
|
+
let at = offset;
|
|
241
|
+
for (let count = 0; count < 5; count += 1) {
|
|
242
|
+
if (at >= bytes.byteLength)
|
|
243
|
+
throw new Error("Delta varint is truncated");
|
|
244
|
+
const byte = bytes[at] ?? 0;
|
|
245
|
+
if (count === 4 && (byte & 0xf0) !== 0) {
|
|
246
|
+
throw new Error("Delta varint exceeds 32 bits");
|
|
247
|
+
}
|
|
248
|
+
value = (value | ((byte & 0x7f) << shift)) >>> 0;
|
|
249
|
+
at += 1;
|
|
250
|
+
if ((byte & 0x80) === 0) {
|
|
251
|
+
if (count > 0 && byte === 0) {
|
|
252
|
+
throw new Error("Delta varint is not canonical");
|
|
253
|
+
}
|
|
254
|
+
return { value, next: at };
|
|
255
|
+
}
|
|
256
|
+
shift += 7;
|
|
257
|
+
}
|
|
258
|
+
throw new Error("Delta varint is malformed");
|
|
259
|
+
}
|
|
260
|
+
function encodePatchV1(targetSize, operations) {
|
|
261
|
+
const total = PATCH_HEADER_V1_BYTES +
|
|
262
|
+
operations.reduce((sum, operation) => sum +
|
|
263
|
+
(operation.kind === "copy"
|
|
264
|
+
? COPY_BYTES
|
|
265
|
+
: LITERAL_HEADER_BYTES + operation.bytes.byteLength), 0);
|
|
266
|
+
const out = new Uint8Array(total);
|
|
267
|
+
out.set(PATCH_MAGIC_V1, 0);
|
|
268
|
+
const view = new DataView(out.buffer);
|
|
269
|
+
view.setUint32(4, targetSize, false);
|
|
270
|
+
view.setUint32(8, operations.length, false);
|
|
271
|
+
let at = PATCH_HEADER_V1_BYTES;
|
|
272
|
+
for (const operation of operations) {
|
|
273
|
+
if (operation.kind === "copy") {
|
|
274
|
+
out[at] = 1;
|
|
275
|
+
view.setUint32(at + 1, operation.offset, false);
|
|
276
|
+
view.setUint32(at + 5, operation.length, false);
|
|
277
|
+
at += COPY_BYTES;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
out[at] = 0;
|
|
281
|
+
view.setUint32(at + 1, operation.bytes.byteLength, false);
|
|
282
|
+
out.set(operation.bytes, at + LITERAL_HEADER_BYTES);
|
|
283
|
+
at += LITERAL_HEADER_BYTES + operation.bytes.byteLength;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
function encodePatchV2(targetSize, operations) {
|
|
289
|
+
const total = 4 +
|
|
290
|
+
varUintBytes(targetSize) +
|
|
291
|
+
varUintBytes(operations.length) +
|
|
292
|
+
operations.reduce((sum, operation) => sum +
|
|
293
|
+
1 +
|
|
294
|
+
(operation.kind === "copy"
|
|
295
|
+
? varUintBytes(operation.offset) + varUintBytes(operation.length)
|
|
296
|
+
: varUintBytes(operation.bytes.byteLength) + operation.bytes.byteLength), 0);
|
|
297
|
+
const out = new Uint8Array(total);
|
|
298
|
+
out.set(PATCH_MAGIC_V2, 0);
|
|
299
|
+
let at = writeVarUint(out, 4, targetSize);
|
|
300
|
+
at = writeVarUint(out, at, operations.length);
|
|
301
|
+
for (const operation of operations) {
|
|
302
|
+
if (operation.kind === "copy") {
|
|
303
|
+
out[at] = 1;
|
|
304
|
+
at = writeVarUint(out, at + 1, operation.offset);
|
|
305
|
+
at = writeVarUint(out, at, operation.length);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
out[at] = 0;
|
|
309
|
+
at = writeVarUint(out, at + 1, operation.bytes.byteLength);
|
|
310
|
+
out.set(operation.bytes, at);
|
|
311
|
+
at += operation.bytes.byteLength;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
/** Build COPY/INSERT operations from receiver signatures, without base bytes. */
|
|
317
|
+
function createDeltaPatch(target, signature) {
|
|
318
|
+
if (target.byteLength > exports.DELTA_MAX_FILE_BYTES) {
|
|
319
|
+
throw new Error("Delta target exceeds the bounded transport limit");
|
|
320
|
+
}
|
|
321
|
+
if (signature.blocks.length === 0 || target.byteLength < signature.blockSize) {
|
|
322
|
+
const literal = [{ kind: "literal", bytes: target.slice() }];
|
|
323
|
+
return signature.version === 2
|
|
324
|
+
? encodePatchV2(target.byteLength, literal)
|
|
325
|
+
: encodePatchV1(target.byteLength, literal);
|
|
326
|
+
}
|
|
327
|
+
const candidates = new Map();
|
|
328
|
+
signature.blocks.forEach((block, index) => {
|
|
329
|
+
const list = candidates.get(block.weak) ?? [];
|
|
330
|
+
// Repeated zero-filled blocks can otherwise make adversarial work quadratic.
|
|
331
|
+
if (list.length < 64)
|
|
332
|
+
list.push(index);
|
|
333
|
+
candidates.set(block.weak, list);
|
|
334
|
+
});
|
|
335
|
+
const operations = [];
|
|
336
|
+
const width = signature.blockSize;
|
|
337
|
+
let cursor = 0;
|
|
338
|
+
let literalStart = 0;
|
|
339
|
+
let weak = weakChecksum(target, 0, width);
|
|
340
|
+
while (cursor + width <= target.byteLength) {
|
|
341
|
+
const possible = candidates.get(weak);
|
|
342
|
+
let matched = -1;
|
|
343
|
+
if (possible?.length) {
|
|
344
|
+
const strong = strongChecksum(target, cursor, width);
|
|
345
|
+
matched = possible.find((index) => signature.blocks[index]?.strong ===
|
|
346
|
+
(signature.strongBits === 16 ? strong & 0xffff : strong)) ?? -1;
|
|
347
|
+
}
|
|
348
|
+
if (matched >= 0) {
|
|
349
|
+
appendOperation(operations, {
|
|
350
|
+
kind: "literal",
|
|
351
|
+
bytes: target.slice(literalStart, cursor),
|
|
352
|
+
});
|
|
353
|
+
appendOperation(operations, {
|
|
354
|
+
kind: "copy",
|
|
355
|
+
offset: matched * width,
|
|
356
|
+
length: width,
|
|
357
|
+
});
|
|
358
|
+
cursor += width;
|
|
359
|
+
literalStart = cursor;
|
|
360
|
+
if (cursor + width <= target.byteLength) {
|
|
361
|
+
weak = weakChecksum(target, cursor, width);
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (cursor + width >= target.byteLength)
|
|
366
|
+
break;
|
|
367
|
+
weak = rollWeak(weak, target[cursor] ?? 0, target[cursor + width] ?? 0, width);
|
|
368
|
+
cursor += 1;
|
|
369
|
+
}
|
|
370
|
+
appendOperation(operations, {
|
|
371
|
+
kind: "literal",
|
|
372
|
+
bytes: target.slice(literalStart),
|
|
373
|
+
});
|
|
374
|
+
return signature.version === 2
|
|
375
|
+
? encodePatchV2(target.byteLength, operations)
|
|
376
|
+
: encodePatchV1(target.byteLength, operations);
|
|
377
|
+
}
|
|
378
|
+
/** Apply an untrusted patch with strict bounds; the route verifies SHA-256 next. */
|
|
379
|
+
function deltaPatchTargetSize(patch) {
|
|
380
|
+
if (sameMagic(patch, PATCH_MAGIC_V2)) {
|
|
381
|
+
const field = readVarUint(patch, 4);
|
|
382
|
+
if (field.value > exports.DELTA_MAX_FILE_BYTES) {
|
|
383
|
+
throw new Error("Delta patch target exceeds the transport limit");
|
|
384
|
+
}
|
|
385
|
+
return field.value;
|
|
386
|
+
}
|
|
387
|
+
if (patch.byteLength < PATCH_HEADER_V1_BYTES ||
|
|
388
|
+
!sameMagic(patch, PATCH_MAGIC_V1)) {
|
|
389
|
+
throw new Error("Delta patch is malformed");
|
|
390
|
+
}
|
|
391
|
+
const size = new DataView(patch.buffer, patch.byteOffset, patch.byteLength).getUint32(4, false);
|
|
392
|
+
if (size > exports.DELTA_MAX_FILE_BYTES) {
|
|
393
|
+
throw new Error("Delta patch target exceeds the transport limit");
|
|
394
|
+
}
|
|
395
|
+
return size;
|
|
396
|
+
}
|
|
397
|
+
function applyDeltaPatch(base, patch) {
|
|
398
|
+
if (sameMagic(patch, PATCH_MAGIC_V2)) {
|
|
399
|
+
return applyDeltaPatchV2(base, patch);
|
|
400
|
+
}
|
|
401
|
+
if (patch.byteLength < PATCH_HEADER_V1_BYTES ||
|
|
402
|
+
!sameMagic(patch, PATCH_MAGIC_V1)) {
|
|
403
|
+
throw new Error("Delta patch is malformed");
|
|
404
|
+
}
|
|
405
|
+
const view = new DataView(patch.buffer, patch.byteOffset, patch.byteLength);
|
|
406
|
+
const targetSize = view.getUint32(4, false);
|
|
407
|
+
const operationCount = view.getUint32(8, false);
|
|
408
|
+
if (targetSize > exports.DELTA_MAX_FILE_BYTES || operationCount > 1_000_000) {
|
|
409
|
+
throw new Error("Delta patch dimensions are invalid");
|
|
410
|
+
}
|
|
411
|
+
const out = new Uint8Array(targetSize);
|
|
412
|
+
let readAt = PATCH_HEADER_V1_BYTES;
|
|
413
|
+
let writeAt = 0;
|
|
414
|
+
for (let index = 0; index < operationCount; index += 1) {
|
|
415
|
+
if (readAt >= patch.byteLength)
|
|
416
|
+
throw new Error("Delta patch is truncated");
|
|
417
|
+
const opcode = patch[readAt];
|
|
418
|
+
if (opcode === 1) {
|
|
419
|
+
if (readAt + COPY_BYTES > patch.byteLength) {
|
|
420
|
+
throw new Error("Delta copy operation is truncated");
|
|
421
|
+
}
|
|
422
|
+
const offset = view.getUint32(readAt + 1, false);
|
|
423
|
+
const length = view.getUint32(readAt + 5, false);
|
|
424
|
+
if (length === 0 ||
|
|
425
|
+
offset + length > base.byteLength ||
|
|
426
|
+
writeAt + length > out.byteLength) {
|
|
427
|
+
throw new Error("Delta copy operation is out of bounds");
|
|
428
|
+
}
|
|
429
|
+
out.set(base.subarray(offset, offset + length), writeAt);
|
|
430
|
+
writeAt += length;
|
|
431
|
+
readAt += COPY_BYTES;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (opcode === 0) {
|
|
435
|
+
if (readAt + LITERAL_HEADER_BYTES > patch.byteLength) {
|
|
436
|
+
throw new Error("Delta literal operation is truncated");
|
|
437
|
+
}
|
|
438
|
+
const length = view.getUint32(readAt + 1, false);
|
|
439
|
+
const from = readAt + LITERAL_HEADER_BYTES;
|
|
440
|
+
if (length === 0 ||
|
|
441
|
+
from + length > patch.byteLength ||
|
|
442
|
+
writeAt + length > out.byteLength) {
|
|
443
|
+
throw new Error("Delta literal operation is out of bounds");
|
|
444
|
+
}
|
|
445
|
+
out.set(patch.subarray(from, from + length), writeAt);
|
|
446
|
+
writeAt += length;
|
|
447
|
+
readAt = from + length;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
throw new Error("Delta patch contains an unknown operation");
|
|
451
|
+
}
|
|
452
|
+
if (readAt !== patch.byteLength || writeAt !== out.byteLength) {
|
|
453
|
+
throw new Error("Delta patch does not reconstruct the declared target");
|
|
454
|
+
}
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
function applyDeltaPatchV2(base, patch) {
|
|
458
|
+
const targetField = readVarUint(patch, 4);
|
|
459
|
+
const targetSize = targetField.value;
|
|
460
|
+
const countField = readVarUint(patch, targetField.next);
|
|
461
|
+
const operationCount = countField.value;
|
|
462
|
+
if (targetSize > exports.DELTA_MAX_FILE_BYTES || operationCount > 1_000_000) {
|
|
463
|
+
throw new Error("Delta patch dimensions are invalid");
|
|
464
|
+
}
|
|
465
|
+
const out = new Uint8Array(targetSize);
|
|
466
|
+
let readAt = countField.next;
|
|
467
|
+
let writeAt = 0;
|
|
468
|
+
for (let index = 0; index < operationCount; index += 1) {
|
|
469
|
+
if (readAt >= patch.byteLength)
|
|
470
|
+
throw new Error("Delta patch is truncated");
|
|
471
|
+
const opcode = patch[readAt] ?? 0xff;
|
|
472
|
+
readAt += 1;
|
|
473
|
+
if (opcode === 1) {
|
|
474
|
+
const offsetField = readVarUint(patch, readAt);
|
|
475
|
+
const lengthField = readVarUint(patch, offsetField.next);
|
|
476
|
+
const offset = offsetField.value;
|
|
477
|
+
const length = lengthField.value;
|
|
478
|
+
if (length === 0 ||
|
|
479
|
+
offset + length > base.byteLength ||
|
|
480
|
+
writeAt + length > out.byteLength) {
|
|
481
|
+
throw new Error("Delta copy operation is out of bounds");
|
|
482
|
+
}
|
|
483
|
+
out.set(base.subarray(offset, offset + length), writeAt);
|
|
484
|
+
writeAt += length;
|
|
485
|
+
readAt = lengthField.next;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (opcode === 0) {
|
|
489
|
+
const lengthField = readVarUint(patch, readAt);
|
|
490
|
+
const length = lengthField.value;
|
|
491
|
+
const from = lengthField.next;
|
|
492
|
+
if (length === 0 ||
|
|
493
|
+
from + length > patch.byteLength ||
|
|
494
|
+
writeAt + length > out.byteLength) {
|
|
495
|
+
throw new Error("Delta literal operation is out of bounds");
|
|
496
|
+
}
|
|
497
|
+
out.set(patch.subarray(from, from + length), writeAt);
|
|
498
|
+
writeAt += length;
|
|
499
|
+
readAt = from + length;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
throw new Error("Delta patch contains an unknown operation");
|
|
503
|
+
}
|
|
504
|
+
if (readAt !== patch.byteLength || writeAt !== out.byteLength) {
|
|
505
|
+
throw new Error("Delta patch does not reconstruct the declared target");
|
|
506
|
+
}
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
function uuidBytes(value) {
|
|
510
|
+
const compact = value.replaceAll("-", "").toLowerCase();
|
|
511
|
+
if (!/^[0-9a-f]{32}$/.test(compact)) {
|
|
512
|
+
throw new Error("Delta base Version id is invalid");
|
|
513
|
+
}
|
|
514
|
+
const out = new Uint8Array(16);
|
|
515
|
+
for (let index = 0; index < out.byteLength; index += 1) {
|
|
516
|
+
out[index] = Number.parseInt(compact.slice(index * 2, index * 2 + 2), 16);
|
|
517
|
+
}
|
|
518
|
+
return out;
|
|
519
|
+
}
|
|
520
|
+
function digestBytes(value) {
|
|
521
|
+
if (!/^[0-9a-f]{64}$/.test(value)) {
|
|
522
|
+
throw new Error("Delta target digest is invalid");
|
|
523
|
+
}
|
|
524
|
+
const out = new Uint8Array(32);
|
|
525
|
+
for (let index = 0; index < out.byteLength; index += 1) {
|
|
526
|
+
out[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
|
|
527
|
+
}
|
|
528
|
+
return out;
|
|
529
|
+
}
|
|
530
|
+
function bytesHex(bytes) {
|
|
531
|
+
let value = "";
|
|
532
|
+
for (const byte of bytes)
|
|
533
|
+
value += byte.toString(16).padStart(2, "0");
|
|
534
|
+
return value;
|
|
535
|
+
}
|
|
536
|
+
function bytesUuid(bytes) {
|
|
537
|
+
const value = bytesHex(bytes);
|
|
538
|
+
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Frame V2 transport metadata without JSON field names or duplicate sizes.
|
|
542
|
+
*
|
|
543
|
+
* The selected immutable Version and path uniquely identify the base. Its
|
|
544
|
+
* digest is still checked when the signature is requested and again against
|
|
545
|
+
* the stored Version while reconstructing, so repeating 32 bytes in this
|
|
546
|
+
* second request adds no authority.
|
|
547
|
+
*/
|
|
548
|
+
function encodeDeltaEnvelope(input) {
|
|
549
|
+
const path = new TextEncoder().encode(input.path);
|
|
550
|
+
const mediaType = new TextEncoder().encode(input.mediaType);
|
|
551
|
+
if (path.byteLength === 0 || path.byteLength > 0xffff) {
|
|
552
|
+
throw new Error("Delta path length is invalid");
|
|
553
|
+
}
|
|
554
|
+
if (mediaType.byteLength === 0 || mediaType.byteLength > 0xff) {
|
|
555
|
+
throw new Error("Delta media type length is invalid");
|
|
556
|
+
}
|
|
557
|
+
if (input.patch.byteLength === 0)
|
|
558
|
+
throw new Error("Delta patch is empty");
|
|
559
|
+
const out = new Uint8Array(ENVELOPE_V2_FIXED_BYTES + path.byteLength + mediaType.byteLength + input.patch.byteLength);
|
|
560
|
+
out.set(ENVELOPE_MAGIC_V2, 0);
|
|
561
|
+
const view = new DataView(out.buffer);
|
|
562
|
+
view.setUint16(4, path.byteLength, false);
|
|
563
|
+
out[6] = mediaType.byteLength;
|
|
564
|
+
out.set(uuidBytes(input.baseVersionId), 7);
|
|
565
|
+
out.set(digestBytes(input.sha256), 23);
|
|
566
|
+
let at = ENVELOPE_V2_FIXED_BYTES;
|
|
567
|
+
out.set(path, at);
|
|
568
|
+
at += path.byteLength;
|
|
569
|
+
out.set(mediaType, at);
|
|
570
|
+
at += mediaType.byteLength;
|
|
571
|
+
out.set(input.patch, at);
|
|
572
|
+
return out;
|
|
573
|
+
}
|
|
574
|
+
function isDeltaEnvelope(bytes) {
|
|
575
|
+
return sameMagic(bytes, ENVELOPE_MAGIC_V2);
|
|
576
|
+
}
|
|
577
|
+
function decodeDeltaEnvelope(bytes) {
|
|
578
|
+
if (bytes.byteLength < ENVELOPE_V2_FIXED_BYTES + 1 || !isDeltaEnvelope(bytes)) {
|
|
579
|
+
throw new Error("Delta envelope is malformed");
|
|
580
|
+
}
|
|
581
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
582
|
+
const pathLength = view.getUint16(4, false);
|
|
583
|
+
const mediaLength = bytes[6] ?? 0;
|
|
584
|
+
const patchAt = ENVELOPE_V2_FIXED_BYTES + pathLength + mediaLength;
|
|
585
|
+
if (pathLength === 0 || mediaLength === 0 || patchAt >= bytes.byteLength) {
|
|
586
|
+
throw new Error("Delta envelope dimensions are invalid");
|
|
587
|
+
}
|
|
588
|
+
let path;
|
|
589
|
+
let mediaType;
|
|
590
|
+
try {
|
|
591
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
592
|
+
path = decoder.decode(bytes.subarray(ENVELOPE_V2_FIXED_BYTES, ENVELOPE_V2_FIXED_BYTES + pathLength));
|
|
593
|
+
mediaType = decoder.decode(bytes.subarray(ENVELOPE_V2_FIXED_BYTES + pathLength, patchAt));
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
throw new Error("Delta envelope text is invalid UTF-8");
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
baseVersionId: bytesUuid(bytes.subarray(7, 23)),
|
|
600
|
+
sha256: bytesHex(bytes.subarray(23, 55)),
|
|
601
|
+
path,
|
|
602
|
+
mediaType,
|
|
603
|
+
patch: bytes.subarray(patchAt),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
/** Frame positional V2 signatures once, without repeating paths in the reply. */
|
|
607
|
+
function encodeDeltaSignatureBatch(signatures) {
|
|
608
|
+
if (signatures.length === 0 ||
|
|
609
|
+
signatures.length > exports.DELTA_BATCH_MAX_FILES) {
|
|
610
|
+
throw new Error("Delta signature batch count is invalid");
|
|
611
|
+
}
|
|
612
|
+
let total = 6;
|
|
613
|
+
for (const signature of signatures) {
|
|
614
|
+
if (parseDeltaSignature(signature).version !== 2) {
|
|
615
|
+
throw new Error("A batch may contain only V2 delta signatures");
|
|
616
|
+
}
|
|
617
|
+
total += 4 + signature.byteLength;
|
|
618
|
+
}
|
|
619
|
+
const out = new Uint8Array(total);
|
|
620
|
+
out.set(SIGNATURE_BATCH_MAGIC_V2, 0);
|
|
621
|
+
const view = new DataView(out.buffer);
|
|
622
|
+
view.setUint16(4, signatures.length, false);
|
|
623
|
+
let at = 6;
|
|
624
|
+
for (const signature of signatures) {
|
|
625
|
+
view.setUint32(at, signature.byteLength, false);
|
|
626
|
+
at += 4;
|
|
627
|
+
out.set(signature, at);
|
|
628
|
+
at += signature.byteLength;
|
|
629
|
+
}
|
|
630
|
+
return out;
|
|
631
|
+
}
|
|
632
|
+
function decodeDeltaSignatureBatch(bytes) {
|
|
633
|
+
if (bytes.byteLength < 6 || !sameMagic(bytes, SIGNATURE_BATCH_MAGIC_V2)) {
|
|
634
|
+
throw new Error("Delta signature batch is malformed");
|
|
635
|
+
}
|
|
636
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
637
|
+
const count = view.getUint16(4, false);
|
|
638
|
+
if (count === 0 || count > exports.DELTA_BATCH_MAX_FILES) {
|
|
639
|
+
throw new Error("Delta signature batch count is invalid");
|
|
640
|
+
}
|
|
641
|
+
const signatures = [];
|
|
642
|
+
let at = 6;
|
|
643
|
+
for (let index = 0; index < count; index += 1) {
|
|
644
|
+
if (at + 4 > bytes.byteLength) {
|
|
645
|
+
throw new Error("Delta signature batch is truncated");
|
|
646
|
+
}
|
|
647
|
+
const length = view.getUint32(at, false);
|
|
648
|
+
at += 4;
|
|
649
|
+
if (length === 0 || at + length > bytes.byteLength) {
|
|
650
|
+
throw new Error("Delta signature batch member is truncated");
|
|
651
|
+
}
|
|
652
|
+
const signature = parseDeltaSignature(bytes.subarray(at, at + length));
|
|
653
|
+
if (signature.version !== 2) {
|
|
654
|
+
throw new Error("Delta signature batch contains an old signature");
|
|
655
|
+
}
|
|
656
|
+
signatures.push(signature);
|
|
657
|
+
at += length;
|
|
658
|
+
}
|
|
659
|
+
if (at !== bytes.byteLength) {
|
|
660
|
+
throw new Error("Delta signature batch contains trailing bytes");
|
|
661
|
+
}
|
|
662
|
+
return signatures;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Frame several independently verifiable patches under one immutable base
|
|
666
|
+
* Version. Each member keeps its own path, target digest, media type and patch;
|
|
667
|
+
* only the Version id and HTTP round trip are shared.
|
|
668
|
+
*/
|
|
669
|
+
function encodeDeltaBatchEnvelope(input) {
|
|
670
|
+
if (input.entries.length === 0 || input.entries.length > exports.DELTA_BATCH_MAX_FILES) {
|
|
671
|
+
throw new Error("Delta batch count is invalid");
|
|
672
|
+
}
|
|
673
|
+
const encoded = input.entries.map((entry) => {
|
|
674
|
+
const path = new TextEncoder().encode(entry.path);
|
|
675
|
+
const mediaType = new TextEncoder().encode(entry.mediaType);
|
|
676
|
+
if (path.byteLength === 0 || path.byteLength > 0xffff) {
|
|
677
|
+
throw new Error("Delta batch path length is invalid");
|
|
678
|
+
}
|
|
679
|
+
if (mediaType.byteLength === 0 || mediaType.byteLength > 0xff) {
|
|
680
|
+
throw new Error("Delta batch media type length is invalid");
|
|
681
|
+
}
|
|
682
|
+
if (entry.patch.byteLength === 0)
|
|
683
|
+
throw new Error("Delta batch patch is empty");
|
|
684
|
+
return { entry, path, mediaType };
|
|
685
|
+
});
|
|
686
|
+
const total = 22 +
|
|
687
|
+
encoded.reduce((sum, item) => sum +
|
|
688
|
+
39 +
|
|
689
|
+
item.path.byteLength +
|
|
690
|
+
item.mediaType.byteLength +
|
|
691
|
+
item.entry.patch.byteLength, 0);
|
|
692
|
+
const out = new Uint8Array(total);
|
|
693
|
+
out.set(DELTA_BATCH_MAGIC_V2, 0);
|
|
694
|
+
out.set(uuidBytes(input.baseVersionId), 4);
|
|
695
|
+
const view = new DataView(out.buffer);
|
|
696
|
+
view.setUint16(20, encoded.length, false);
|
|
697
|
+
let at = 22;
|
|
698
|
+
for (const item of encoded) {
|
|
699
|
+
view.setUint16(at, item.path.byteLength, false);
|
|
700
|
+
out[at + 2] = item.mediaType.byteLength;
|
|
701
|
+
out.set(digestBytes(item.entry.sha256), at + 3);
|
|
702
|
+
view.setUint32(at + 35, item.entry.patch.byteLength, false);
|
|
703
|
+
at += 39;
|
|
704
|
+
out.set(item.path, at);
|
|
705
|
+
at += item.path.byteLength;
|
|
706
|
+
out.set(item.mediaType, at);
|
|
707
|
+
at += item.mediaType.byteLength;
|
|
708
|
+
out.set(item.entry.patch, at);
|
|
709
|
+
at += item.entry.patch.byteLength;
|
|
710
|
+
}
|
|
711
|
+
return out;
|
|
712
|
+
}
|
|
713
|
+
function decodeDeltaBatchEnvelope(bytes) {
|
|
714
|
+
if (bytes.byteLength < 23 || !sameMagic(bytes, DELTA_BATCH_MAGIC_V2)) {
|
|
715
|
+
throw new Error("Delta batch envelope is malformed");
|
|
716
|
+
}
|
|
717
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
718
|
+
const count = view.getUint16(20, false);
|
|
719
|
+
if (count === 0 || count > exports.DELTA_BATCH_MAX_FILES) {
|
|
720
|
+
throw new Error("Delta batch count is invalid");
|
|
721
|
+
}
|
|
722
|
+
const entries = [];
|
|
723
|
+
const paths = new Set();
|
|
724
|
+
let at = 22;
|
|
725
|
+
for (let index = 0; index < count; index += 1) {
|
|
726
|
+
if (at + 39 > bytes.byteLength) {
|
|
727
|
+
throw new Error("Delta batch member is truncated");
|
|
728
|
+
}
|
|
729
|
+
const pathLength = view.getUint16(at, false);
|
|
730
|
+
const mediaLength = bytes[at + 2] ?? 0;
|
|
731
|
+
const sha256 = bytesHex(bytes.subarray(at + 3, at + 35));
|
|
732
|
+
const patchLength = view.getUint32(at + 35, false);
|
|
733
|
+
at += 39;
|
|
734
|
+
const pathAt = at;
|
|
735
|
+
const mediaAt = pathAt + pathLength;
|
|
736
|
+
const patchAt = mediaAt + mediaLength;
|
|
737
|
+
const next = patchAt + patchLength;
|
|
738
|
+
if (pathLength === 0 ||
|
|
739
|
+
mediaLength === 0 ||
|
|
740
|
+
patchLength === 0 ||
|
|
741
|
+
next > bytes.byteLength) {
|
|
742
|
+
throw new Error("Delta batch member dimensions are invalid");
|
|
743
|
+
}
|
|
744
|
+
let path;
|
|
745
|
+
let mediaType;
|
|
746
|
+
try {
|
|
747
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
748
|
+
path = decoder.decode(bytes.subarray(pathAt, mediaAt));
|
|
749
|
+
mediaType = decoder.decode(bytes.subarray(mediaAt, patchAt));
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
throw new Error("Delta batch text is invalid UTF-8");
|
|
753
|
+
}
|
|
754
|
+
if (paths.has(path))
|
|
755
|
+
throw new Error("Delta batch contains a duplicate path");
|
|
756
|
+
paths.add(path);
|
|
757
|
+
entries.push({
|
|
758
|
+
path,
|
|
759
|
+
mediaType,
|
|
760
|
+
sha256,
|
|
761
|
+
patch: bytes.subarray(patchAt, next),
|
|
762
|
+
});
|
|
763
|
+
at = next;
|
|
764
|
+
}
|
|
765
|
+
if (at !== bytes.byteLength) {
|
|
766
|
+
throw new Error("Delta batch contains trailing bytes");
|
|
767
|
+
}
|
|
768
|
+
return {
|
|
769
|
+
baseVersionId: bytesUuid(bytes.subarray(4, 20)),
|
|
770
|
+
entries,
|
|
771
|
+
};
|
|
772
|
+
}
|