@serverkgg/bridge 0.0.7 → 0.0.9
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/package.json +4 -3
- package/src/cli/validate.ts +1 -0
- package/src/context/bridgeContext.ts +10 -0
- package/src/context/index.ts +1 -0
- package/src/core/bridgeDownloadError.ts +10 -0
- package/src/core/bridgeHold.ts +12 -0
- package/src/core/bridgeKind.ts +2 -0
- package/src/core/bridgeUserError.test.ts +23 -0
- package/src/core/bridgeUserError.ts +12 -0
- package/src/core/index.ts +3 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +85 -0
- package/src/manifest/manifest.ts +21 -0
- package/src/manifest/presence.ts +48 -0
- package/src/manifest/primitives.ts +6 -0
- package/src/modules/bridge.ts +45 -2
- package/src/modules/bridgeDriver.ts +2 -0
- package/src/modules/bridgeModule.ts +25 -3
- package/src/panel/bridgeDetail.ts +49 -0
- package/src/panel/bridgeLayout.ts +1 -0
- package/src/panel/bridgePanel.ts +30 -0
- package/src/panel/bridgeUploadMode.ts +4 -0
- package/src/panel/index.ts +2 -0
- package/src/protocol/bridgeEgress.ts +8 -1
- package/src/protocol/bridgeEvents.ts +4 -0
- package/src/protocol/bridgeFrame.ts +28 -1
- package/src/protocol/bridgeLimits.ts +10 -0
- package/src/runtime/bridgeActions.ts +7 -0
- package/src/runtime/bridgeDriver.test.ts +477 -2
- package/src/runtime/bridgeDriver.ts +116 -3
- package/src/runtime/bridgeEgress.ts +3 -0
- package/src/runtime/bridgeEvents.ts +7 -5
- package/src/runtime/bridgeFiles.test.ts +625 -15
- package/src/runtime/bridgeFiles.ts +152 -45
- package/src/runtime/bridgeInstall.test.ts +118 -0
- package/src/runtime/bridgeInstall.ts +55 -0
- package/src/runtime/bridgeNet.ts +32 -3
- package/src/runtime/bridgePending.test.ts +148 -0
- package/src/runtime/bridgePending.ts +43 -0
- package/src/runtime/bridgePrivilege.test.ts +77 -0
- package/src/runtime/bridgePrivilege.ts +5 -3
- package/src/runtime/bridgeRegistry.test.ts +134 -0
- package/src/runtime/bridgeRegistry.ts +26 -4
- package/src/runtime/bridgeServer.test.ts +338 -0
- package/src/runtime/bridgeServer.ts +25 -3
- package/src/runtime/bridgeSupervisor.ts +18 -0
- package/src/runtime/bridgeZip.ts +179 -58
- package/src/runtime/serve.ts +64 -4
- package/src/wire/index.ts +2 -0
- package/src/wire/panel.test.ts +451 -0
- package/src/wire/panel.ts +295 -0
- package/src/wire/zEnvKey.ts +7 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
+
import { deflateRawSync } from "node:zlib";
|
|
4
5
|
import type { BridgeFiles } from "../context";
|
|
5
|
-
import
|
|
6
|
+
import { BridgeDownloadError } from "../core";
|
|
7
|
+
import { type BridgeEgress, BridgeEgressCallError } from "./bridgeEgress";
|
|
6
8
|
import { createBridgeFiles } from "./bridgeFiles";
|
|
9
|
+
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
7
10
|
|
|
8
11
|
const LOCAL_SIGNATURE = 0x04_03_4b_50;
|
|
9
12
|
|
|
@@ -11,6 +14,26 @@ const CENTRAL_SIGNATURE = 0x02_01_4b_50;
|
|
|
11
14
|
|
|
12
15
|
const EOCD_SIGNATURE = 0x06_05_4b_50;
|
|
13
16
|
|
|
17
|
+
const ZIP64_LOCATOR_SIGNATURE = 0x07_06_4b_50;
|
|
18
|
+
|
|
19
|
+
const ZIP64_EOCD_SIGNATURE = 0x06_06_4b_50;
|
|
20
|
+
|
|
21
|
+
const ZIP64_EXTRA_ID = 0x00_01;
|
|
22
|
+
|
|
23
|
+
const ZIP64_EXTRA_LENGTH = 28;
|
|
24
|
+
|
|
25
|
+
const ZIP64_EOCD_LENGTH = 56;
|
|
26
|
+
|
|
27
|
+
const ZIP64_LOCATOR_LENGTH = 20;
|
|
28
|
+
|
|
29
|
+
const COUNT_SENTINEL = 0xff_ff;
|
|
30
|
+
|
|
31
|
+
const SIZE_SENTINEL = 0xff_ff_ff_ff;
|
|
32
|
+
|
|
33
|
+
const STORED = 0;
|
|
34
|
+
|
|
35
|
+
const DEFLATED = 8;
|
|
36
|
+
|
|
14
37
|
const FILE_ATTRIBUTES = 0x81_a4_00_00;
|
|
15
38
|
|
|
16
39
|
const SYMLINK_ATTRIBUTES = 0xa1_ff_00_00;
|
|
@@ -19,9 +42,15 @@ interface ArchiveEntry {
|
|
|
19
42
|
name: string;
|
|
20
43
|
content: string;
|
|
21
44
|
symlink?: boolean;
|
|
45
|
+
compressed?: Buffer;
|
|
22
46
|
}
|
|
23
47
|
|
|
24
|
-
|
|
48
|
+
interface ArchiveOptions {
|
|
49
|
+
zip64?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const buildZip = (entries: ArchiveEntry[], archiveOptions: ArchiveOptions = {}) => {
|
|
53
|
+
const zip64 = archiveOptions.zip64 === true;
|
|
25
54
|
const locals: Buffer[] = [];
|
|
26
55
|
const centrals: Buffer[] = [];
|
|
27
56
|
|
|
@@ -29,28 +58,43 @@ const buildZip = (entries: ArchiveEntry[]) => {
|
|
|
29
58
|
|
|
30
59
|
for (const entry of entries) {
|
|
31
60
|
const name = Buffer.from(entry.name, "utf8");
|
|
32
|
-
const
|
|
61
|
+
const plain = Buffer.from(entry.content, "utf8");
|
|
62
|
+
const data = entry.compressed ?? plain;
|
|
63
|
+
const method = entry.compressed === undefined ? STORED : DEFLATED;
|
|
33
64
|
|
|
34
65
|
const local = Buffer.alloc(30);
|
|
35
66
|
|
|
36
67
|
local.writeUInt32LE(LOCAL_SIGNATURE, 0);
|
|
37
68
|
local.writeUInt16LE(20, 4);
|
|
69
|
+
local.writeUInt16LE(method, 8);
|
|
38
70
|
local.writeUInt32LE(data.length, 18);
|
|
39
|
-
local.writeUInt32LE(
|
|
71
|
+
local.writeUInt32LE(plain.length, 22);
|
|
40
72
|
local.writeUInt16LE(name.length, 26);
|
|
41
73
|
|
|
74
|
+
const extra = Buffer.alloc(zip64 ? ZIP64_EXTRA_LENGTH : 0);
|
|
75
|
+
|
|
76
|
+
if (zip64) {
|
|
77
|
+
extra.writeUInt16LE(ZIP64_EXTRA_ID, 0);
|
|
78
|
+
extra.writeUInt16LE(ZIP64_EXTRA_LENGTH - 4, 2);
|
|
79
|
+
extra.writeBigUInt64LE(BigInt(plain.length), 4);
|
|
80
|
+
extra.writeBigUInt64LE(BigInt(data.length), 12);
|
|
81
|
+
extra.writeBigUInt64LE(BigInt(offset), 20);
|
|
82
|
+
}
|
|
83
|
+
|
|
42
84
|
const central = Buffer.alloc(46);
|
|
43
85
|
|
|
44
86
|
central.writeUInt32LE(CENTRAL_SIGNATURE, 0);
|
|
45
87
|
central.writeUInt16LE(20, 6);
|
|
46
|
-
central.
|
|
47
|
-
central.writeUInt32LE(data.length,
|
|
88
|
+
central.writeUInt16LE(method, 10);
|
|
89
|
+
central.writeUInt32LE(zip64 ? SIZE_SENTINEL : data.length, 20);
|
|
90
|
+
central.writeUInt32LE(zip64 ? SIZE_SENTINEL : plain.length, 24);
|
|
48
91
|
central.writeUInt16LE(name.length, 28);
|
|
92
|
+
central.writeUInt16LE(extra.length, 30);
|
|
49
93
|
central.writeUInt32LE(entry.symlink === true ? SYMLINK_ATTRIBUTES : FILE_ATTRIBUTES, 38);
|
|
50
|
-
central.writeUInt32LE(offset, 42);
|
|
94
|
+
central.writeUInt32LE(zip64 ? SIZE_SENTINEL : offset, 42);
|
|
51
95
|
|
|
52
96
|
locals.push(local, name, data);
|
|
53
|
-
centrals.push(central, name);
|
|
97
|
+
centrals.push(central, name, extra);
|
|
54
98
|
|
|
55
99
|
offset += 30 + name.length + data.length;
|
|
56
100
|
}
|
|
@@ -60,14 +104,41 @@ const buildZip = (entries: ArchiveEntry[]) => {
|
|
|
60
104
|
const end = Buffer.alloc(22);
|
|
61
105
|
|
|
62
106
|
end.writeUInt32LE(EOCD_SIGNATURE, 0);
|
|
63
|
-
end.writeUInt16LE(entries.length, 8);
|
|
64
|
-
end.writeUInt16LE(entries.length, 10);
|
|
65
|
-
end.writeUInt32LE(directory.length, 12);
|
|
66
|
-
end.writeUInt32LE(body.length, 16);
|
|
107
|
+
end.writeUInt16LE(zip64 ? COUNT_SENTINEL : entries.length, 8);
|
|
108
|
+
end.writeUInt16LE(zip64 ? COUNT_SENTINEL : entries.length, 10);
|
|
109
|
+
end.writeUInt32LE(zip64 ? SIZE_SENTINEL : directory.length, 12);
|
|
110
|
+
end.writeUInt32LE(zip64 ? SIZE_SENTINEL : body.length, 16);
|
|
111
|
+
|
|
112
|
+
if (!zip64) {
|
|
113
|
+
return Buffer.concat([
|
|
114
|
+
body,
|
|
115
|
+
directory,
|
|
116
|
+
end,
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const record = Buffer.alloc(ZIP64_EOCD_LENGTH);
|
|
121
|
+
|
|
122
|
+
record.writeUInt32LE(ZIP64_EOCD_SIGNATURE, 0);
|
|
123
|
+
record.writeBigUInt64LE(BigInt(ZIP64_EOCD_LENGTH - 12), 4);
|
|
124
|
+
record.writeUInt16LE(45, 12);
|
|
125
|
+
record.writeUInt16LE(45, 14);
|
|
126
|
+
record.writeBigUInt64LE(BigInt(entries.length), 24);
|
|
127
|
+
record.writeBigUInt64LE(BigInt(entries.length), 32);
|
|
128
|
+
record.writeBigUInt64LE(BigInt(directory.length), 40);
|
|
129
|
+
record.writeBigUInt64LE(BigInt(body.length), 48);
|
|
130
|
+
|
|
131
|
+
const locator = Buffer.alloc(ZIP64_LOCATOR_LENGTH);
|
|
132
|
+
|
|
133
|
+
locator.writeUInt32LE(ZIP64_LOCATOR_SIGNATURE, 0);
|
|
134
|
+
locator.writeBigUInt64LE(BigInt(body.length + directory.length), 8);
|
|
135
|
+
locator.writeUInt32LE(1, 16);
|
|
67
136
|
|
|
68
137
|
return Buffer.concat([
|
|
69
138
|
body,
|
|
70
139
|
directory,
|
|
140
|
+
record,
|
|
141
|
+
locator,
|
|
71
142
|
end,
|
|
72
143
|
]);
|
|
73
144
|
};
|
|
@@ -82,12 +153,17 @@ const egress: BridgeEgress = {
|
|
|
82
153
|
},
|
|
83
154
|
};
|
|
84
155
|
|
|
156
|
+
const silent: BridgeLeveledLogger = Object.assign(() => undefined, {
|
|
157
|
+
warn: () => undefined,
|
|
158
|
+
error: () => undefined,
|
|
159
|
+
});
|
|
160
|
+
|
|
85
161
|
describe("extract", () => {
|
|
86
162
|
let root = "";
|
|
87
163
|
let files: BridgeFiles;
|
|
88
164
|
|
|
89
|
-
const archive = async (entries: ArchiveEntry[]) => {
|
|
90
|
-
await Bun.write(`${root}/pack.zip`, buildZip(entries));
|
|
165
|
+
const archive = async (entries: ArchiveEntry[], archiveOptions: ArchiveOptions = {}) => {
|
|
166
|
+
await Bun.write(`${root}/pack.zip`, buildZip(entries, archiveOptions));
|
|
91
167
|
|
|
92
168
|
return "pack.zip";
|
|
93
169
|
};
|
|
@@ -104,7 +180,7 @@ describe("extract", () => {
|
|
|
104
180
|
uid: 0,
|
|
105
181
|
gid: 0,
|
|
106
182
|
egress,
|
|
107
|
-
onLog:
|
|
183
|
+
onLog: silent,
|
|
108
184
|
});
|
|
109
185
|
});
|
|
110
186
|
|
|
@@ -331,4 +407,538 @@ describe("extract", () => {
|
|
|
331
407
|
}),
|
|
332
408
|
).rejects.toThrow("contained no matching files");
|
|
333
409
|
});
|
|
410
|
+
|
|
411
|
+
test("takes every selected prefix in one pass and strips the one that matched", async () => {
|
|
412
|
+
const path = await archive([
|
|
413
|
+
{
|
|
414
|
+
name: "overrides/mods/alpha.jar",
|
|
415
|
+
content: "alpha",
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: "server-overrides/config/beta.json",
|
|
419
|
+
content: "beta",
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: "client-overrides/options.txt",
|
|
423
|
+
content: "client",
|
|
424
|
+
},
|
|
425
|
+
]);
|
|
426
|
+
|
|
427
|
+
const written = await files.extract(path, "", {
|
|
428
|
+
tree: true,
|
|
429
|
+
selects: [
|
|
430
|
+
"overrides/",
|
|
431
|
+
"server-overrides/",
|
|
432
|
+
],
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
expect(written).toEqual([
|
|
436
|
+
"mods/alpha.jar",
|
|
437
|
+
"config/beta.json",
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
expect(await read("mods/alpha.jar")).toBe("alpha");
|
|
441
|
+
expect(await read("config/beta.json")).toBe("beta");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("takes a single named entry when the select carries no trailing slash", async () => {
|
|
445
|
+
const path = await archive([
|
|
446
|
+
{
|
|
447
|
+
name: "modrinth.index.json",
|
|
448
|
+
content: "{}",
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: "overrides/config/beta.json",
|
|
452
|
+
content: "beta",
|
|
453
|
+
},
|
|
454
|
+
]);
|
|
455
|
+
|
|
456
|
+
const written = await files.extract(path, "staging", {
|
|
457
|
+
tree: true,
|
|
458
|
+
selects: [
|
|
459
|
+
"modrinth.index.json",
|
|
460
|
+
],
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
expect(written).toEqual([
|
|
464
|
+
"staging/modrinth.index.json",
|
|
465
|
+
]);
|
|
466
|
+
|
|
467
|
+
expect(await read("staging/modrinth.index.json")).toBe("{}");
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("drops excluded paths after the prefix is stripped", async () => {
|
|
471
|
+
const path = await archive([
|
|
472
|
+
{
|
|
473
|
+
name: "overrides/mods/alpha.jar",
|
|
474
|
+
content: "alpha",
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: "overrides/shaderpacks/heavy.zip",
|
|
478
|
+
content: "shader",
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: "overrides/saves/world/level.dat",
|
|
482
|
+
content: "world",
|
|
483
|
+
},
|
|
484
|
+
]);
|
|
485
|
+
|
|
486
|
+
const written = await files.extract(path, "", {
|
|
487
|
+
tree: true,
|
|
488
|
+
selects: [
|
|
489
|
+
"overrides/",
|
|
490
|
+
],
|
|
491
|
+
exclude: [
|
|
492
|
+
"shaderpacks/**",
|
|
493
|
+
"saves/**",
|
|
494
|
+
],
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
expect(written).toEqual([
|
|
498
|
+
"mods/alpha.jar",
|
|
499
|
+
]);
|
|
500
|
+
|
|
501
|
+
expect(await Bun.file(`${root}/shaderpacks/heavy.zip`).exists()).toBe(false);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("answers with nothing when a select matches nothing", async () => {
|
|
505
|
+
const path = await archive([
|
|
506
|
+
{
|
|
507
|
+
name: "overrides/mods/alpha.jar",
|
|
508
|
+
content: "alpha",
|
|
509
|
+
},
|
|
510
|
+
]);
|
|
511
|
+
|
|
512
|
+
const written = await files.extract(path, "staging", {
|
|
513
|
+
tree: true,
|
|
514
|
+
selects: [
|
|
515
|
+
"server-overrides/",
|
|
516
|
+
],
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
expect(written).toEqual([]);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("inflates a deflated entry", async () => {
|
|
523
|
+
const path = await archive([
|
|
524
|
+
{
|
|
525
|
+
name: "mods/alpha.jar",
|
|
526
|
+
content: "alpha the deflated",
|
|
527
|
+
compressed: deflateRawSync(Buffer.from("alpha the deflated", "utf8")),
|
|
528
|
+
},
|
|
529
|
+
]);
|
|
530
|
+
|
|
531
|
+
const written = await files.extract(path, "mods");
|
|
532
|
+
|
|
533
|
+
expect(written).toEqual([
|
|
534
|
+
"mods/alpha.jar",
|
|
535
|
+
]);
|
|
536
|
+
|
|
537
|
+
expect(await read("mods/alpha.jar")).toBe("alpha the deflated");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("reads a zip64 archive through its locator and extended field", async () => {
|
|
541
|
+
const path = await archive(
|
|
542
|
+
[
|
|
543
|
+
{
|
|
544
|
+
name: "overrides/mods/alpha.jar",
|
|
545
|
+
content: "alpha",
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
name: "overrides/config/beta.json",
|
|
549
|
+
content: "beta",
|
|
550
|
+
compressed: deflateRawSync(Buffer.from("beta", "utf8")),
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
{
|
|
554
|
+
zip64: true,
|
|
555
|
+
},
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
const written = await files.extract(path, "", {
|
|
559
|
+
tree: true,
|
|
560
|
+
select: "overrides/",
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
expect(written).toEqual([
|
|
564
|
+
"mods/alpha.jar",
|
|
565
|
+
"config/beta.json",
|
|
566
|
+
]);
|
|
567
|
+
|
|
568
|
+
expect(await read("mods/alpha.jar")).toBe("alpha");
|
|
569
|
+
expect(await read("config/beta.json")).toBe("beta");
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("takes a suffix select from anywhere in the archive", async () => {
|
|
573
|
+
const path = await archive([
|
|
574
|
+
{
|
|
575
|
+
name: "BMC5 Server/mods/alpha.jar",
|
|
576
|
+
content: "alpha",
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: "mods/beta.jar",
|
|
580
|
+
content: "beta",
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: "config/alpha.jar",
|
|
584
|
+
content: "config",
|
|
585
|
+
},
|
|
586
|
+
]);
|
|
587
|
+
|
|
588
|
+
const written = await files.extract(path, "staging", {
|
|
589
|
+
selects: [
|
|
590
|
+
"**/mods/alpha.jar",
|
|
591
|
+
"**/mods/beta.jar",
|
|
592
|
+
],
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
expect(written).toEqual([
|
|
596
|
+
"staging/alpha.jar",
|
|
597
|
+
"staging/beta.jar",
|
|
598
|
+
]);
|
|
599
|
+
|
|
600
|
+
expect(await read("staging/alpha.jar")).toBe("alpha");
|
|
601
|
+
expect(await read("staging/beta.jar")).toBe("beta");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("keeps the suffix path under the destination in tree mode", async () => {
|
|
605
|
+
const path = await archive([
|
|
606
|
+
{
|
|
607
|
+
name: "BMC5 Server/mods/alpha.jar",
|
|
608
|
+
content: "alpha",
|
|
609
|
+
},
|
|
610
|
+
]);
|
|
611
|
+
|
|
612
|
+
const written = await files.extract(path, "staging", {
|
|
613
|
+
tree: true,
|
|
614
|
+
selects: [
|
|
615
|
+
"**/mods/alpha.jar",
|
|
616
|
+
],
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
expect(written).toEqual([
|
|
620
|
+
"staging/mods/alpha.jar",
|
|
621
|
+
]);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("never inflates an entry it did not select", async () => {
|
|
625
|
+
const entries: ArchiveEntry[] = [
|
|
626
|
+
{
|
|
627
|
+
name: "BMC5 Server/mods/alpha.jar",
|
|
628
|
+
content: "alpha",
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: "BMC5 Server/mods/broken.jar",
|
|
632
|
+
content: "a payload no inflater can read",
|
|
633
|
+
compressed: Buffer.from("this is not deflate data", "utf8"),
|
|
634
|
+
},
|
|
635
|
+
];
|
|
636
|
+
|
|
637
|
+
const path = await archive(entries);
|
|
638
|
+
|
|
639
|
+
const written = await files.extract(path, "staging", {
|
|
640
|
+
selects: [
|
|
641
|
+
"**/mods/alpha.jar",
|
|
642
|
+
],
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
expect(written).toEqual([
|
|
646
|
+
"staging/alpha.jar",
|
|
647
|
+
]);
|
|
648
|
+
|
|
649
|
+
await expect(
|
|
650
|
+
files.extract(path, "broken", {
|
|
651
|
+
selects: [
|
|
652
|
+
"**/mods/broken.jar",
|
|
653
|
+
],
|
|
654
|
+
}),
|
|
655
|
+
).rejects.toThrow();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("answers with nothing when every match is excluded", async () => {
|
|
659
|
+
const path = await archive([
|
|
660
|
+
{
|
|
661
|
+
name: "overrides/shaderpacks/heavy.zip",
|
|
662
|
+
content: "shader",
|
|
663
|
+
},
|
|
664
|
+
]);
|
|
665
|
+
|
|
666
|
+
const written = await files.extract(path, "", {
|
|
667
|
+
tree: true,
|
|
668
|
+
selects: [
|
|
669
|
+
"overrides/",
|
|
670
|
+
],
|
|
671
|
+
exclude: [
|
|
672
|
+
"shaderpacks/**",
|
|
673
|
+
],
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
expect(written).toEqual([]);
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
describe("digest", () => {
|
|
681
|
+
let root = "";
|
|
682
|
+
let files: BridgeFiles;
|
|
683
|
+
|
|
684
|
+
beforeEach(async () => {
|
|
685
|
+
root = await mkdtemp(`${tmpdir()}/serverk-bridge-`);
|
|
686
|
+
|
|
687
|
+
files = createBridgeFiles({
|
|
688
|
+
root,
|
|
689
|
+
uid: 0,
|
|
690
|
+
gid: 0,
|
|
691
|
+
egress,
|
|
692
|
+
onLog: silent,
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
afterEach(async () => {
|
|
697
|
+
await rm(root, {
|
|
698
|
+
force: true,
|
|
699
|
+
recursive: true,
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test("answers with the known vectors for abc", async () => {
|
|
704
|
+
await Bun.write(`${root}/mods/alpha.jar`, "abc");
|
|
705
|
+
|
|
706
|
+
expect(await files.digest("mods/alpha.jar", "sha1")).toBe("a9993e364706816aba3e25717850c26c9cd0d89d");
|
|
707
|
+
|
|
708
|
+
expect(await files.digest("mods/alpha.jar", "sha256")).toBe(
|
|
709
|
+
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
expect(await files.digest("mods/alpha.jar", "sha512")).toBe(
|
|
713
|
+
"ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f",
|
|
714
|
+
);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("answers with the known vector for an empty file", async () => {
|
|
718
|
+
await Bun.write(`${root}/empty.jar`, "");
|
|
719
|
+
|
|
720
|
+
expect(await files.digest("empty.jar", "sha1")).toBe("da39a3ee5e6b4b0d3255bfef95601890afd80709");
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test("hashes a file larger than one read chunk", async () => {
|
|
724
|
+
const content = "a".repeat(4 * 1024 * 1024 + 17);
|
|
725
|
+
|
|
726
|
+
await Bun.write(`${root}/big.bin`, content);
|
|
727
|
+
|
|
728
|
+
expect(await files.digest("big.bin", "sha256")).toBe(new Bun.CryptoHasher("sha256").update(content).digest("hex"));
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
test("refuses a path that is not a readable file", async () => {
|
|
732
|
+
await files.ensure("mods");
|
|
733
|
+
|
|
734
|
+
await expect(files.digest("mods", "sha1")).rejects.toThrow("is not a readable file");
|
|
735
|
+
await expect(files.digest("mods/missing.jar", "sha1")).rejects.toThrow("is not a readable file");
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test("refuses a path outside the server directory", async () => {
|
|
739
|
+
await expect(files.digest("../escape.jar", "sha1")).rejects.toThrow("is not allowed");
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
describe("download", () => {
|
|
744
|
+
let root = "";
|
|
745
|
+
let warnings: string[] = [];
|
|
746
|
+
let messages: string[] = [];
|
|
747
|
+
|
|
748
|
+
const digestOf = (content: string) => {
|
|
749
|
+
return `sha512:${new Bun.CryptoHasher("sha512").update(content).digest("hex")}`;
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
const serving = (content: string): BridgeEgress => {
|
|
753
|
+
return {
|
|
754
|
+
fetch() {
|
|
755
|
+
throw new Error("the egress channel is not part of this test");
|
|
756
|
+
},
|
|
757
|
+
|
|
758
|
+
async download() {
|
|
759
|
+
return {
|
|
760
|
+
status: 200,
|
|
761
|
+
sizeBytes: Buffer.byteLength(content),
|
|
762
|
+
fromCache: false,
|
|
763
|
+
body: (async function* () {
|
|
764
|
+
yield new Uint8Array(Buffer.from(content));
|
|
765
|
+
})(),
|
|
766
|
+
};
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const filesServing = (content: string) => {
|
|
772
|
+
return createBridgeFiles({
|
|
773
|
+
root,
|
|
774
|
+
uid: 0,
|
|
775
|
+
gid: 0,
|
|
776
|
+
egress: serving(content),
|
|
777
|
+
onLog: Object.assign(
|
|
778
|
+
(message: string) => {
|
|
779
|
+
messages.push(message);
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
warn: (message: string) => {
|
|
783
|
+
warnings.push(message);
|
|
784
|
+
},
|
|
785
|
+
error: () => undefined,
|
|
786
|
+
},
|
|
787
|
+
),
|
|
788
|
+
});
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
beforeEach(async () => {
|
|
792
|
+
root = await mkdtemp(`${tmpdir()}/serverk-bridge-`);
|
|
793
|
+
warnings = [];
|
|
794
|
+
messages = [];
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
afterEach(async () => {
|
|
798
|
+
await rm(root, {
|
|
799
|
+
force: true,
|
|
800
|
+
recursive: true,
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test("keeps a file the source undersized when the checksum passes", async () => {
|
|
805
|
+
const content = "a mod jar the index measured wrong";
|
|
806
|
+
|
|
807
|
+
await filesServing(content).download("mods/alpha.jar", "https://cdn.example.com/alpha.jar", {
|
|
808
|
+
digest: digestOf(content),
|
|
809
|
+
sizeBytes: Buffer.byteLength(content) - 2,
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
expect(await Bun.file(`${root}/mods/alpha.jar`).text()).toBe(content);
|
|
813
|
+
expect(warnings).toEqual([
|
|
814
|
+
"the source declared a different size, the checksum passed so we kept the file",
|
|
815
|
+
]);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test("refuses a file whose checksum does not match, however right its size", async () => {
|
|
819
|
+
const content = "a mod jar somebody swapped";
|
|
820
|
+
|
|
821
|
+
await expect(
|
|
822
|
+
filesServing(content).download("mods/alpha.jar", "https://cdn.example.com/alpha.jar", {
|
|
823
|
+
digest: digestOf("what the index promised"),
|
|
824
|
+
sizeBytes: Buffer.byteLength(content),
|
|
825
|
+
}),
|
|
826
|
+
).rejects.toThrow("failed digest verification");
|
|
827
|
+
|
|
828
|
+
expect(await Bun.file(`${root}/mods/alpha.jar`).exists()).toBe(false);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("holds an unverified file to its declared size", async () => {
|
|
832
|
+
const content = "a mod jar with no checksum to trust";
|
|
833
|
+
|
|
834
|
+
await expect(
|
|
835
|
+
filesServing(content).download("mods/alpha.jar", "https://cdn.example.com/alpha.jar", {
|
|
836
|
+
sizeBytes: Buffer.byteLength(content) - 2,
|
|
837
|
+
}),
|
|
838
|
+
).rejects.toThrow("returned more than");
|
|
839
|
+
|
|
840
|
+
expect(await Bun.file(`${root}/mods/alpha.jar`).exists()).toBe(false);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
test("holds an unverified file that stops short of its declared size", async () => {
|
|
844
|
+
const content = "a mod jar with no checksum to trust";
|
|
845
|
+
|
|
846
|
+
await expect(
|
|
847
|
+
filesServing(content).download("mods/alpha.jar", "https://cdn.example.com/alpha.jar", {
|
|
848
|
+
sizeBytes: Buffer.byteLength(content) + 2,
|
|
849
|
+
}),
|
|
850
|
+
).rejects.toThrow(`returned ${Buffer.byteLength(content)} bytes, expected ${Buffer.byteLength(content) + 2}`);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
test("carries the upstream status when the egress channel refuses", async () => {
|
|
854
|
+
const refusing: BridgeEgress = {
|
|
855
|
+
fetch() {
|
|
856
|
+
throw new Error("the egress channel is not part of this test");
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
download() {
|
|
860
|
+
throw new BridgeEgressCallError(429, "cdn.example.com answered 429");
|
|
861
|
+
},
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
const failing = createBridgeFiles({
|
|
865
|
+
root,
|
|
866
|
+
uid: 0,
|
|
867
|
+
gid: 0,
|
|
868
|
+
egress: refusing,
|
|
869
|
+
onLog: silent,
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
let thrown: unknown = null;
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
await failing.download("mods/alpha.jar", "https://cdn.example.com/alpha.jar");
|
|
876
|
+
} catch (error) {
|
|
877
|
+
thrown = error;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
expect(thrown).toBeInstanceOf(BridgeDownloadError);
|
|
881
|
+
expect(thrown instanceof BridgeDownloadError ? thrown.status : null).toBe(429);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
test("narrates a file it downloads unless the caller asks otherwise", async () => {
|
|
885
|
+
const content = "a server jar worth watching";
|
|
886
|
+
|
|
887
|
+
await filesServing(content).download("server.jar", "https://cdn.example.com/server.jar", {
|
|
888
|
+
digest: digestOf(content),
|
|
889
|
+
sizeBytes: Buffer.byteLength(content),
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
expect(messages).toEqual([
|
|
893
|
+
"downloading",
|
|
894
|
+
"downloaded",
|
|
895
|
+
]);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test("says nothing about a quiet file", async () => {
|
|
899
|
+
const content = "a mod jar in a batch of hundreds";
|
|
900
|
+
|
|
901
|
+
await filesServing(content).download("mods/alpha.jar", "https://cdn.example.com/alpha.jar", {
|
|
902
|
+
digest: digestOf(content),
|
|
903
|
+
sizeBytes: Buffer.byteLength(content),
|
|
904
|
+
quiet: true,
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
expect(await Bun.file(`${root}/mods/alpha.jar`).text()).toBe(content);
|
|
908
|
+
expect(messages).toEqual([]);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
test("still warns and still fails on a quiet file", async () => {
|
|
912
|
+
const content = "a mod jar the index measured wrong";
|
|
913
|
+
|
|
914
|
+
await filesServing(content).download("mods/alpha.jar", "https://cdn.example.com/alpha.jar", {
|
|
915
|
+
digest: digestOf(content),
|
|
916
|
+
sizeBytes: Buffer.byteLength(content) - 2,
|
|
917
|
+
quiet: true,
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
await expect(
|
|
921
|
+
filesServing(content).download("mods/beta.jar", "https://cdn.example.com/beta.jar", {
|
|
922
|
+
digest: digestOf("what the index promised"),
|
|
923
|
+
quiet: true,
|
|
924
|
+
}),
|
|
925
|
+
).rejects.toThrow("failed digest verification");
|
|
926
|
+
|
|
927
|
+
expect(await Bun.file(`${root}/mods/beta.jar`).exists()).toBe(false);
|
|
928
|
+
expect(warnings).toEqual([
|
|
929
|
+
"the source declared a different size, the checksum passed so we kept the file",
|
|
930
|
+
]);
|
|
931
|
+
expect(messages).toEqual([]);
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
test("keeps no warning when the declared size is right", async () => {
|
|
935
|
+
const content = "a mod jar the index measured right";
|
|
936
|
+
|
|
937
|
+
await filesServing(content).download("mods/alpha.jar", "https://cdn.example.com/alpha.jar", {
|
|
938
|
+
digest: digestOf(content),
|
|
939
|
+
sizeBytes: Buffer.byteLength(content),
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
expect(warnings).toEqual([]);
|
|
943
|
+
});
|
|
334
944
|
});
|