@coderook/cli 0.22.0 → 0.22.1
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.
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "coderook",
|
|
3
3
|
"displayName": "CodeRook",
|
|
4
4
|
"description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, from Claude Code.",
|
|
5
|
-
"version": "0.22.
|
|
5
|
+
"version": "0.22.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
|
@@ -27,6 +27,7 @@ const profile_js_1 = require("./profile.js");
|
|
|
27
27
|
const solid_js_1 = require("./solid.js");
|
|
28
28
|
const retry_js_1 = require("./retry.js");
|
|
29
29
|
const staging_js_1 = require("./staging.js");
|
|
30
|
+
const delta_js_1 = require("../shared/delta.js");
|
|
30
31
|
/** Above this the API insists on a multipart session. */
|
|
31
32
|
const DIRECT_LIMIT = 95 * 1024 * 1024;
|
|
32
33
|
/*
|
|
@@ -105,6 +106,10 @@ class Uploader {
|
|
|
105
106
|
chunkingSupported = null;
|
|
106
107
|
/** Null until the service has been asked; see packingAllowed. */
|
|
107
108
|
packingSupported = null;
|
|
109
|
+
/** Null until the service has advertised its best changed-file transport. */
|
|
110
|
+
deltaProtocol = null;
|
|
111
|
+
/** Batch delta is separate so V2 single-file support remains compatible. */
|
|
112
|
+
deltaBatchSupported = null;
|
|
108
113
|
constructor(credentials) {
|
|
109
114
|
this.credentials = credentials;
|
|
110
115
|
}
|
|
@@ -345,8 +350,15 @@ class Uploader {
|
|
|
345
350
|
});
|
|
346
351
|
totalBytes += size;
|
|
347
352
|
}
|
|
348
|
-
|
|
353
|
+
/*
|
|
354
|
+
A deletion-only save has no file to hash or upload, but it is still a
|
|
355
|
+
real new Version: the retained base minus the explicitly selected path.
|
|
356
|
+
Refusing it here made the object phase look healthy while preventing the
|
|
357
|
+
manifest-only operation that deletion is supposed to be.
|
|
358
|
+
*/
|
|
359
|
+
if (!declarations.length && !deletions.size) {
|
|
349
360
|
throw new Error("Nothing is selected to upload");
|
|
361
|
+
}
|
|
350
362
|
// 2. The project needs somewhere on the account to live.
|
|
351
363
|
this.check();
|
|
352
364
|
let repositoryId = request.repositoryId;
|
|
@@ -394,6 +406,24 @@ class Uploader {
|
|
|
394
406
|
*/
|
|
395
407
|
const allDeclarations = declarations;
|
|
396
408
|
const declarationByPath = new Map(allDeclarations.map((one) => [one.path, one]));
|
|
409
|
+
/*
|
|
410
|
+
The narrow first use of the delta transport.
|
|
411
|
+
|
|
412
|
+
One changed small file used to miss solid packing (which correctly
|
|
413
|
+
requires a group) and upload its complete compressed bytes. When more
|
|
414
|
+
than one small file changes, the existing pack remains the better
|
|
415
|
+
answer and is deliberately untouched. New files have no receiver base
|
|
416
|
+
and also stay on the existing path.
|
|
417
|
+
*/
|
|
418
|
+
const logicallyChanged = allDeclarations.filter((one) => prior.get(one.path)?.sha256 !== one.sha256);
|
|
419
|
+
const deltaDeclaration = logicallyChanged.length === 1 &&
|
|
420
|
+
request.baseVersionId &&
|
|
421
|
+
prior.has(logicallyChanged[0].path) &&
|
|
422
|
+
logicallyChanged[0].size <= BATCH_FILE_LIMIT &&
|
|
423
|
+
logicallyChanged[0].size <= delta_js_1.DELTA_MAX_FILE_BYTES &&
|
|
424
|
+
prior.get(logicallyChanged[0].path).sourceSize <= delta_js_1.DELTA_MAX_FILE_BYTES
|
|
425
|
+
? logicallyChanged[0]
|
|
426
|
+
: null;
|
|
397
427
|
const sections = (0, staging_js_1.planSections)(allDeclarations.map((one) => ({ path: one.path, size: one.size })));
|
|
398
428
|
(0, profile_js_1.counted)("sections planned", sections.length);
|
|
399
429
|
const uploaded = [];
|
|
@@ -544,7 +574,22 @@ class Uploader {
|
|
|
544
574
|
an object of their own.
|
|
545
575
|
*/
|
|
546
576
|
const packedInto = new Map();
|
|
547
|
-
if (
|
|
577
|
+
if (deltaDeclaration &&
|
|
578
|
+
smallOnes.some((one) => one.path === deltaDeclaration.path)) {
|
|
579
|
+
const held = prior.get(deltaDeclaration.path);
|
|
580
|
+
const placed = await (0, profile_js_1.timed)("send changed-file delta", () => this.putDelta(repositoryId, request.baseVersionId, held, deltaDeclaration));
|
|
581
|
+
if (placed) {
|
|
582
|
+
batched.set(deltaDeclaration.sha256, {
|
|
583
|
+
objectId: placed.objectId,
|
|
584
|
+
size: placed.storedSize,
|
|
585
|
+
});
|
|
586
|
+
sentBytes += deltaDeclaration.size;
|
|
587
|
+
transferred += placed.sentBytes;
|
|
588
|
+
(0, profile_js_1.counted)("files sent as deltas", 1);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const smallForPacking = smallOnes.filter((one) => !batched.has(one.sha256));
|
|
592
|
+
if (smallForPacking.length > 1) {
|
|
548
593
|
let pack = [];
|
|
549
594
|
let packBytes = 0;
|
|
550
595
|
/*
|
|
@@ -569,6 +614,7 @@ class Uploader {
|
|
|
569
614
|
pack = [];
|
|
570
615
|
packBytes = 0;
|
|
571
616
|
const task = (async () => {
|
|
617
|
+
let exactWireBytes = null;
|
|
572
618
|
try {
|
|
573
619
|
/*
|
|
574
620
|
Compressed together first. This is what the .cbx format is for and
|
|
@@ -577,18 +623,36 @@ class Uploader {
|
|
|
577
623
|
and 30.2% compressed together, because a two-kilobyte file gives a
|
|
578
624
|
compressor no dictionary worth having.
|
|
579
625
|
*/
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
626
|
+
const packItems = taking.map((item) => ({
|
|
627
|
+
declaration: item.declaration,
|
|
628
|
+
body: item.body,
|
|
629
|
+
}));
|
|
630
|
+
const built = (await this.packingAllowed())
|
|
631
|
+
? await (0, solid_js_1.buildSolidPack)(packItems.map((item) => ({
|
|
632
|
+
sha256: item.declaration.sha256,
|
|
583
633
|
body: item.body,
|
|
584
|
-
})))
|
|
634
|
+
})))
|
|
635
|
+
: null;
|
|
636
|
+
const packedBytes = built ? this.frameSolidPack(built).byteLength : 0;
|
|
637
|
+
const delta = built
|
|
638
|
+
? await (0, profile_js_1.timed)("price changed-file batch", () => this.putDeltaBatch(repositoryId, request.baseVersionId, taking, prior, packedBytes))
|
|
639
|
+
: null;
|
|
640
|
+
if (delta) {
|
|
641
|
+
(0, profile_js_1.counted)("files sent as delta batch", taking.length);
|
|
642
|
+
exactWireBytes = delta.sentBytes;
|
|
643
|
+
for (const [digest, one] of delta.objects)
|
|
644
|
+
batched.set(digest, one);
|
|
645
|
+
}
|
|
646
|
+
const placed = !delta && built
|
|
647
|
+
? await (0, profile_js_1.timed)("send packs", () => this.putPack(repositoryId, packItems, built))
|
|
585
648
|
: null;
|
|
586
649
|
if (placed) {
|
|
587
650
|
(0, profile_js_1.counted)("files sent in packs", taking.length);
|
|
651
|
+
exactWireBytes = packedBytes;
|
|
588
652
|
for (const [digest, one] of placed)
|
|
589
653
|
packedInto.set(digest, one);
|
|
590
654
|
}
|
|
591
|
-
else {
|
|
655
|
+
else if (!delta) {
|
|
592
656
|
/*
|
|
593
657
|
Not worth packing — already-compressed content, or too few files.
|
|
594
658
|
They still travel together, just as separate objects.
|
|
@@ -617,8 +681,11 @@ class Uploader {
|
|
|
617
681
|
if (!batched.has(digest) && !packedInto.has(digest))
|
|
618
682
|
continue;
|
|
619
683
|
sentBytes += item.declaration.size;
|
|
620
|
-
|
|
684
|
+
if (exactWireBytes === null)
|
|
685
|
+
transferred += item.encoded.body.byteLength;
|
|
621
686
|
}
|
|
687
|
+
if (exactWireBytes !== null)
|
|
688
|
+
transferred += exactWireBytes;
|
|
622
689
|
report({
|
|
623
690
|
stage: "upload",
|
|
624
691
|
files: 0,
|
|
@@ -634,7 +701,7 @@ class Uploader {
|
|
|
634
701
|
if (flying.size >= BATCH_LANES)
|
|
635
702
|
await Promise.race(flying);
|
|
636
703
|
};
|
|
637
|
-
for (const declaration of
|
|
704
|
+
for (const declaration of smallForPacking) {
|
|
638
705
|
this.check();
|
|
639
706
|
let body;
|
|
640
707
|
try {
|
|
@@ -1128,24 +1195,14 @@ class Uploader {
|
|
|
1128
1195
|
* service would not take it — in which case the caller falls back to sending
|
|
1129
1196
|
* them separately, which is slower and larger but always works.
|
|
1130
1197
|
*/
|
|
1131
|
-
async putPack(repositoryId, items) {
|
|
1132
|
-
const built = await (0, solid_js_1.buildSolidPack)(items.map((item) => ({
|
|
1198
|
+
async putPack(repositoryId, items, alreadyBuilt) {
|
|
1199
|
+
const built = alreadyBuilt ?? await (0, solid_js_1.buildSolidPack)(items.map((item) => ({
|
|
1133
1200
|
sha256: item.declaration.sha256,
|
|
1134
1201
|
body: item.body,
|
|
1135
1202
|
})));
|
|
1136
1203
|
if (!built)
|
|
1137
1204
|
return null;
|
|
1138
|
-
const
|
|
1139
|
-
sha256: built.sha256,
|
|
1140
|
-
storedSha256: built.storedSha256,
|
|
1141
|
-
size: built.size,
|
|
1142
|
-
members: built.members,
|
|
1143
|
-
});
|
|
1144
|
-
const manifestBytes = new TextEncoder().encode(manifest);
|
|
1145
|
-
const packed = new Uint8Array(4 + manifestBytes.byteLength + built.body.byteLength);
|
|
1146
|
-
new DataView(packed.buffer).setUint32(0, manifestBytes.byteLength, false);
|
|
1147
|
-
packed.set(manifestBytes, 4);
|
|
1148
|
-
packed.set(built.body, 4 + manifestBytes.byteLength);
|
|
1205
|
+
const packed = this.frameSolidPack(built);
|
|
1149
1206
|
const answer = await this.call(`/v1/repositories/${repositoryId}/objects/pack`, {
|
|
1150
1207
|
method: "POST",
|
|
1151
1208
|
contentType: "application/octet-stream",
|
|
@@ -1174,6 +1231,216 @@ class Uploader {
|
|
|
1174
1231
|
}
|
|
1175
1232
|
return placed;
|
|
1176
1233
|
}
|
|
1234
|
+
/** The exact HTTP body used for the solid-pack alternative. */
|
|
1235
|
+
frameSolidPack(built) {
|
|
1236
|
+
const manifestBytes = new TextEncoder().encode(JSON.stringify({
|
|
1237
|
+
sha256: built.sha256,
|
|
1238
|
+
storedSha256: built.storedSha256,
|
|
1239
|
+
size: built.size,
|
|
1240
|
+
members: built.members,
|
|
1241
|
+
}));
|
|
1242
|
+
const packed = new Uint8Array(4 + manifestBytes.byteLength + built.body.byteLength);
|
|
1243
|
+
new DataView(packed.buffer).setUint32(0, manifestBytes.byteLength, false);
|
|
1244
|
+
packed.set(manifestBytes, 4);
|
|
1245
|
+
packed.set(built.body, 4 + manifestBytes.byteLength);
|
|
1246
|
+
return packed;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Price several independent file deltas against the already-built solid
|
|
1250
|
+
* pack. The batch is optional and may only replace the pack when every
|
|
1251
|
+
* member has an immutable base and the complete two-way wire cost wins by
|
|
1252
|
+
* the same twenty-percent margin used by the single-file transport.
|
|
1253
|
+
*/
|
|
1254
|
+
async putDeltaBatch(repositoryId, baseVersionId, items, prior, solidPackBytes) {
|
|
1255
|
+
if (!baseVersionId ||
|
|
1256
|
+
solidPackBytes <= 0 ||
|
|
1257
|
+
items.length < 2 ||
|
|
1258
|
+
items.length > delta_js_1.DELTA_BATCH_MAX_FILES ||
|
|
1259
|
+
!(await this.deltaBatchAllowed()))
|
|
1260
|
+
return null;
|
|
1261
|
+
let sourceBytes = 0;
|
|
1262
|
+
let signatureEstimate = 6 + items.length * 4;
|
|
1263
|
+
const bases = [];
|
|
1264
|
+
for (const item of items) {
|
|
1265
|
+
const base = prior.get(item.declaration.path);
|
|
1266
|
+
if (!base ||
|
|
1267
|
+
!base.sha256 ||
|
|
1268
|
+
item.declaration.size > delta_js_1.DELTA_MAX_FILE_BYTES ||
|
|
1269
|
+
base.sourceSize > delta_js_1.DELTA_MAX_FILE_BYTES)
|
|
1270
|
+
return null;
|
|
1271
|
+
if ((0, node_crypto_1.createHash)("sha256").update(item.body).digest("hex") !==
|
|
1272
|
+
item.declaration.sha256)
|
|
1273
|
+
return null;
|
|
1274
|
+
sourceBytes += item.declaration.size;
|
|
1275
|
+
if (sourceBytes > delta_js_1.DELTA_BATCH_MAX_SOURCE_BYTES)
|
|
1276
|
+
return null;
|
|
1277
|
+
signatureEstimate += (0, delta_js_1.estimateDeltaSignatureBytes)(base.sourceSize, 2);
|
|
1278
|
+
bases.push(base);
|
|
1279
|
+
}
|
|
1280
|
+
if (signatureEstimate >= solidPackBytes * 0.8)
|
|
1281
|
+
return null;
|
|
1282
|
+
try {
|
|
1283
|
+
const token = await this.credentials.token();
|
|
1284
|
+
if (!token)
|
|
1285
|
+
return null;
|
|
1286
|
+
const response = await fetch(`${this.credentials.origin()}/v1/repositories/${repositoryId}/objects/delta/signatures`, {
|
|
1287
|
+
method: "POST",
|
|
1288
|
+
headers: {
|
|
1289
|
+
accept: "application/vnd.coderook.delta-signature-batch; version=2",
|
|
1290
|
+
authorization: `Bearer ${token}`,
|
|
1291
|
+
"content-type": "application/json",
|
|
1292
|
+
"user-agent": "CodeRook/0.1",
|
|
1293
|
+
...(0, identify_js_1.clientHeaders)(),
|
|
1294
|
+
},
|
|
1295
|
+
body: JSON.stringify({
|
|
1296
|
+
baseVersionId,
|
|
1297
|
+
files: items.map((item, index) => ({
|
|
1298
|
+
path: item.declaration.path,
|
|
1299
|
+
baseSha256: bases[index].sha256,
|
|
1300
|
+
})),
|
|
1301
|
+
}),
|
|
1302
|
+
signal: this.controller.signal,
|
|
1303
|
+
});
|
|
1304
|
+
if (!response.ok)
|
|
1305
|
+
return null;
|
|
1306
|
+
const signatureBytes = new Uint8Array(await response.arrayBuffer());
|
|
1307
|
+
const signatures = (0, delta_js_1.decodeDeltaSignatureBatch)(signatureBytes);
|
|
1308
|
+
if (signatures.length !== items.length)
|
|
1309
|
+
return null;
|
|
1310
|
+
const framed = (0, delta_js_1.encodeDeltaBatchEnvelope)({
|
|
1311
|
+
baseVersionId,
|
|
1312
|
+
entries: items.map((item, index) => ({
|
|
1313
|
+
path: item.declaration.path,
|
|
1314
|
+
sha256: item.declaration.sha256,
|
|
1315
|
+
mediaType: item.declaration.mediaType,
|
|
1316
|
+
patch: (0, delta_js_1.createDeltaPatch)(item.body, signatures[index]),
|
|
1317
|
+
})),
|
|
1318
|
+
});
|
|
1319
|
+
const wireBytes = signatureBytes.byteLength + framed.byteLength;
|
|
1320
|
+
if (wireBytes >= solidPackBytes * 0.8)
|
|
1321
|
+
return null;
|
|
1322
|
+
const answer = await this.call(`/v1/repositories/${repositoryId}/objects/delta/batch`, {
|
|
1323
|
+
method: "POST",
|
|
1324
|
+
contentType: "application/octet-stream",
|
|
1325
|
+
body: framed,
|
|
1326
|
+
});
|
|
1327
|
+
if (answer.objects.length !== items.length)
|
|
1328
|
+
return null;
|
|
1329
|
+
const objects = new Map();
|
|
1330
|
+
for (const object of answer.objects) {
|
|
1331
|
+
if (!items.some((item) => item.declaration.sha256 === object.sha256)) {
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1334
|
+
objects.set(object.sha256, {
|
|
1335
|
+
objectId: object.objectId,
|
|
1336
|
+
size: object.storedSize,
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
const expectedDigests = new Set(items.map((item) => item.declaration.sha256)).size;
|
|
1340
|
+
return objects.size === expectedDigests
|
|
1341
|
+
? { objects, sentBytes: wireBytes }
|
|
1342
|
+
: null;
|
|
1343
|
+
}
|
|
1344
|
+
catch {
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Try the negotiated receiver-signature transport for one changed file.
|
|
1350
|
+
*
|
|
1351
|
+
* Any refusal, old deployment, stale base, or poor result returns null and
|
|
1352
|
+
* the caller sends the complete object exactly as it did before. The patch
|
|
1353
|
+
* is never a repository object: the service reconstructs and verifies the
|
|
1354
|
+
* complete target before returning the ordinary object id used below.
|
|
1355
|
+
*/
|
|
1356
|
+
async putDelta(repositoryId, baseVersionId, prior, declaration) {
|
|
1357
|
+
const deltaVersion = await this.deltaVersion();
|
|
1358
|
+
if (deltaVersion === 0)
|
|
1359
|
+
return null;
|
|
1360
|
+
let target;
|
|
1361
|
+
try {
|
|
1362
|
+
target = new Uint8Array(await (0, promises_1.readFile)(declaration.full));
|
|
1363
|
+
}
|
|
1364
|
+
catch {
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
const digest = (0, node_crypto_1.createHash)("sha256").update(target).digest("hex");
|
|
1368
|
+
if (digest !== declaration.sha256)
|
|
1369
|
+
return null;
|
|
1370
|
+
const ordinary = await (0, compress_js_1.encodeForUpload)(target, declaration.path, await this.gzipAllowed());
|
|
1371
|
+
const signatureEstimate = (0, delta_js_1.estimateDeltaSignatureBytes)(prior.sourceSize, deltaVersion);
|
|
1372
|
+
if (signatureEstimate >= ordinary.body.byteLength * 0.8)
|
|
1373
|
+
return null;
|
|
1374
|
+
try {
|
|
1375
|
+
const signatureBytes = await this.deltaSignature(repositoryId, baseVersionId, declaration.path, prior.sha256, deltaVersion);
|
|
1376
|
+
const signature = (0, delta_js_1.parseDeltaSignature)(signatureBytes);
|
|
1377
|
+
const patch = (0, delta_js_1.createDeltaPatch)(target, signature);
|
|
1378
|
+
let framed;
|
|
1379
|
+
if (signature.version === 2) {
|
|
1380
|
+
framed = (0, delta_js_1.encodeDeltaEnvelope)({
|
|
1381
|
+
baseVersionId,
|
|
1382
|
+
path: declaration.path,
|
|
1383
|
+
sha256: declaration.sha256,
|
|
1384
|
+
mediaType: declaration.mediaType,
|
|
1385
|
+
patch,
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
const manifest = new TextEncoder().encode(JSON.stringify({
|
|
1390
|
+
baseVersionId,
|
|
1391
|
+
path: declaration.path,
|
|
1392
|
+
baseSha256: prior.sha256,
|
|
1393
|
+
sha256: declaration.sha256,
|
|
1394
|
+
size: target.byteLength,
|
|
1395
|
+
mediaType: declaration.mediaType,
|
|
1396
|
+
}));
|
|
1397
|
+
framed = new Uint8Array(4 + manifest.byteLength + patch.byteLength);
|
|
1398
|
+
new DataView(framed.buffer).setUint32(0, manifest.byteLength, false);
|
|
1399
|
+
framed.set(manifest, 4);
|
|
1400
|
+
framed.set(patch, 4 + manifest.byteLength);
|
|
1401
|
+
}
|
|
1402
|
+
// Count both directions. A patch that only looks small because its
|
|
1403
|
+
// receiver signature was ignored is not an improvement.
|
|
1404
|
+
const wireBytes = signatureBytes.byteLength + framed.byteLength;
|
|
1405
|
+
if (wireBytes >= ordinary.body.byteLength * 0.8)
|
|
1406
|
+
return null;
|
|
1407
|
+
const answer = await this.call(`/v1/repositories/${repositoryId}/objects/delta`, {
|
|
1408
|
+
method: "POST",
|
|
1409
|
+
contentType: "application/octet-stream",
|
|
1410
|
+
body: framed,
|
|
1411
|
+
});
|
|
1412
|
+
return {
|
|
1413
|
+
objectId: answer.objectId,
|
|
1414
|
+
storedSize: answer.storedSize,
|
|
1415
|
+
sentBytes: wireBytes,
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
catch {
|
|
1419
|
+
// This is an optional transport. The complete-object path is authority.
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
async deltaSignature(repositoryId, baseVersionId, filePath, baseSha256, version) {
|
|
1424
|
+
const token = await this.credentials.token();
|
|
1425
|
+
if (!token)
|
|
1426
|
+
throw new Error("Sign in again before uploading");
|
|
1427
|
+
const response = await fetch(`${this.credentials.origin()}/v1/repositories/${repositoryId}/objects/delta/signature`, {
|
|
1428
|
+
method: "POST",
|
|
1429
|
+
headers: {
|
|
1430
|
+
accept: `application/vnd.coderook.delta-signature; version=${version}`,
|
|
1431
|
+
authorization: `Bearer ${token}`,
|
|
1432
|
+
"content-type": "application/json",
|
|
1433
|
+
"user-agent": "CodeRook/0.1",
|
|
1434
|
+
...(0, identify_js_1.clientHeaders)(),
|
|
1435
|
+
},
|
|
1436
|
+
body: JSON.stringify({ baseVersionId, path: filePath, baseSha256 }),
|
|
1437
|
+
signal: this.controller.signal,
|
|
1438
|
+
});
|
|
1439
|
+
if (!response.ok) {
|
|
1440
|
+
throw new Error(`Delta signature failed with HTTP ${response.status}`);
|
|
1441
|
+
}
|
|
1442
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1443
|
+
}
|
|
1177
1444
|
/**
|
|
1178
1445
|
* Send many small objects in one request.
|
|
1179
1446
|
*
|
|
@@ -1501,6 +1768,23 @@ class Uploader {
|
|
|
1501
1768
|
this.packingSupported = (await this.serviceFeatures()).includes("solid-packs");
|
|
1502
1769
|
return this.packingSupported;
|
|
1503
1770
|
}
|
|
1771
|
+
async deltaVersion() {
|
|
1772
|
+
if (this.deltaProtocol !== null)
|
|
1773
|
+
return this.deltaProtocol;
|
|
1774
|
+
const features = await this.serviceFeatures();
|
|
1775
|
+
this.deltaProtocol = features.includes("delta-transport-v2")
|
|
1776
|
+
? 2
|
|
1777
|
+
: features.includes("delta-transport")
|
|
1778
|
+
? 1
|
|
1779
|
+
: 0;
|
|
1780
|
+
return this.deltaProtocol;
|
|
1781
|
+
}
|
|
1782
|
+
async deltaBatchAllowed() {
|
|
1783
|
+
if (this.deltaBatchSupported !== null)
|
|
1784
|
+
return this.deltaBatchSupported;
|
|
1785
|
+
this.deltaBatchSupported = (await this.serviceFeatures()).includes("delta-batch-v2");
|
|
1786
|
+
return this.deltaBatchSupported;
|
|
1787
|
+
}
|
|
1504
1788
|
async chunkingAllowed() {
|
|
1505
1789
|
if (this.chunkingSupported !== null)
|
|
1506
1790
|
return this.chunkingSupported;
|
|
@@ -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
|
+
}
|