@zackees/soldr 0.8.29 → 0.8.31

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/README.md CHANGED
@@ -421,30 +421,29 @@ If you also have many separate test binaries, consider consolidating them under
421
421
 
422
422
  ## Architecture
423
423
 
424
- **Monocrate.** A single Rust crate `soldr-cli` under `crates/soldr-cli/`, with four module trees inside it. The earlier four-crate workspace (`soldr-core` / `soldr-fetch` / `soldr-cache` / `soldr-cli`) was collapsed in 2026-05; the regression guard `crates/soldr-cli/tests/monocrate_guard.rs` fails the build if anyone reintroduces a second crate.
424
+ **Internal workspace crates.** Five `publish = false` crates under `crates/`. The 2026-05 monocrate collapse was reversed by the #1490 split; soldr publishes no crates, so workspace membership has no external surface, and `soldr-cli` re-exports the others at their historical paths (`soldr_cli::core`, `soldr_cli::fetch`, …) so existing imports are unchanged.
425
425
 
426
426
  ```
427
427
  soldr/
428
428
  |-- crates/
429
- | `-- soldr-cli/
430
- | |-- src/
431
- | | |-- core/ # Shared types, config, cache paths (formerly soldr-core)
432
- | | |-- fetch/ # Binary resolution + download (formerly soldr-fetch)
433
- | | |-- cache_lib/ # RUSTC_WRAPPER + daemon IPC (formerly soldr-cache)
434
- | | `-- main.rs + cli/ # Mode detection, dispatch, cargo front door
435
- | `-- tests/
429
+ | |-- soldr-core/ # Shared types, config, target resolution, wire schema
430
+ | |-- soldr-fetch/ # Binary resolution + download
431
+ | |-- soldr-cache/ # RUSTC_WRAPPER + daemon IPC + archive transport
432
+ | |-- soldr-daemon/ # Daemon runtime + embedded zccache service
433
+ | `-- soldr-cli/ # Facade + the `soldr` binary
436
434
  |-- src/soldr/ # Python package (maturin bin bindings)
437
435
  `-- tests/
438
436
  ```
439
437
 
440
- | Module | Role |
438
+ | Crate | Role |
441
439
  |---|---|
442
- | `src/core/` | Shared types, config (`~/.soldr/config.toml`), target-triple resolution (MSVC default on Windows), cache paths, error types. No I/O beyond config files. |
443
- | `src/fetch/` | Binary resolution. `known_tools` registry, `trust` (SHA-256 pins + `SOLDR_TRUST_MODE` enforcement), rustup auto-bootstrap, resolution chain (local cache → repo lookup → GitHub Releases → extract). |
444
- | `src/cache_lib/` | `RUSTC_WRAPPER` mode: hash inputs (blake3), check `~/.soldr/cache/`, daemon IPC (Unix socket / Windows named pipe), LRU eviction, `soldr save` / `soldr load` archive transport, auto-GC. |
445
- | `src/main.rs` + cli/ | Mode detection (chameleon dispatch), clap for built-ins, exec for tool fetch, cargo front door (`soldr cargo ...`). |
440
+ | `soldr-core` | Shared types, config (`~/.soldr/config.toml`), target-triple resolution (MSVC default on Windows), the daemon wire schema, error types, the `timed_test!` watchdog. No I/O beyond config files. |
441
+ | `soldr-fetch` | Binary resolution. `known_tools` registry, `trust` (SHA-256 pins + `SOLDR_TRUST_MODE` enforcement), rustup auto-bootstrap, resolution chain (local cache → repo lookup → GitHub Releases → extract). |
442
+ | `soldr-cache` | `RUSTC_WRAPPER` mode: hash inputs (blake3), check `~/.soldr/cache/`, daemon IPC (Unix socket / Windows named pipe), LRU eviction, `soldr save` / `soldr load` archive transport, auto-GC. |
443
+ | `soldr-daemon` | Daemon lifecycle (spawn/displacement/relocation), IPC server, wire codec, and the embedded zccache service. Depends on `soldr-core` + `soldr-cache`. |
444
+ | `soldr-cli` | Mode detection (chameleon dispatch), clap for built-ins, exec for tool fetch, cargo front door (`soldr cargo ...`), and the `[[bin]]` entry point. |
446
445
 
447
- A thin `src/lib.rs` re-exports `pub mod core; pub mod fetch; pub mod cache_lib;` so the library integration tests can keep `use soldr_cli::core::*`-style imports. This is not a supported public Rust library API.
446
+ Dependency flow: every crate reaches into `core` for shared types; `fetch` and `cache` each consume `core`; `daemon` consumes `core` + `cache`; `cli` consumes all four. The re-exports are for internal consumers and tests — this is not a supported public Rust library API.
448
447
 
449
448
  ## Prior art
450
449
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zackees/soldr",
3
- "version": "0.8.29",
3
+ "version": "0.8.31",
4
4
  "description": "Instant Rust tools and builds from one command.",
5
5
  "license": "BSD-3-Clause",
6
6
  "homepage": "https://github.com/zackees/soldr",
@@ -40,49 +40,97 @@ const TARGETS = {
40
40
  // time based on `target.binary`.
41
41
  const BUNDLED_BINARIES = zccacheContract.RELEASE_BUNDLED_BINARIES;
42
42
 
43
- // Detect whether the running Linux uses musl or glibc. Three layered probes:
44
- // 1. process.report.header.glibcVersionRuntime is the documented Node
45
- // surface for runtime glibc version — present on glibc, absent /
46
- // empty on musl. Same approach used by @swc/core, @napi-rs/*, etc.
47
- // 2. Filesystem check for `/lib/ld-musl-*.so.1` covers cases where the
48
- // Node binary itself is glibc (e.g. someone running glibc Node on
49
- // alpine via apk add nodejs-current) but the system is musl the
50
- // soldr binary we download must match the SYSTEM libc, not Node's.
51
- // 3. Final fallback: assume glibc. The mismatch will surface
52
- // immediately at runtime as "soldr: not found" or
53
- // "soldr: error while loading shared libraries", which is louder
54
- // than silently downloading the wrong tarball.
55
- function detectLibc(platform = process.platform) {
43
+ // The lowest glibc a host must have before the `-gnu` artifact is worth
44
+ // downloading (soldr#1060).
45
+ //
46
+ // Those artifacts are built natively on ubuntu-24.04, so they currently
47
+ // require GLIBC_2.39 measured on the published v0.8.29 binaries, x86_64 and
48
+ // aarch64. On Debian 12 (glibc 2.36) the gnu binary dies with
49
+ // "version `GLIBC_2.39' not found" while the musl artifact from the same
50
+ // release runs fine, so "is this host glibc?" is the wrong question. The
51
+ // question is "is this host's glibc new enough for the binary we ship?".
52
+ //
53
+ // Kept in lockstep with the `--max-glibc` ceiling in release-auto.yml by a
54
+ // check in test-npm-package.js. When the release build is fixed to link
55
+ // against a 2.17 baseline that ceiling drops, and this must follow it down.
56
+ const MIN_GLIBC_FOR_GNU = "2.39";
57
+
58
+ function compareVersions(left, right) {
59
+ // Numeric, part by part. A lexical compare would rank "2.9" above "2.39"
60
+ // and wave through a host that cannot run the binary.
61
+ const a = String(left).split(".").map((p) => parseInt(p, 10) || 0);
62
+ const b = String(right).split(".").map((p) => parseInt(p, 10) || 0);
63
+ for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
64
+ const diff = (a[i] || 0) - (b[i] || 0);
65
+ if (diff !== 0) {
66
+ return diff < 0 ? -1 : 1;
67
+ }
68
+ }
69
+ return 0;
70
+ }
71
+
72
+ // Decide which Linux artifact this host should download. Ordered probes:
73
+ //
74
+ // 1. A musl loader in /lib means the SYSTEM is musl, and that outranks
75
+ // whatever Node was linked against. This has to run first: a glibc Node
76
+ // on alpine (`apk add nodejs-current`) reports a perfectly good glibc
77
+ // version, so checking Node's header first would answer "gnu" and never
78
+ // consult the filesystem at all — which is precisely the case the probe
79
+ // was written for.
80
+ // 2. Node's reported runtime glibc. This is the only source that gives a
81
+ // VERSION, and gnu is chosen only at or above MIN_GLIBC_FOR_GNU.
82
+ // 3. Anything else → musl.
83
+ //
84
+ // musl is the safe end of every unknown because that artifact is verified
85
+ // statically linked before it is ever staged (release-auto.yml → "Verify musl
86
+ // binary is statically linked"), so it has no dynamic loader dependency and
87
+ // runs on glibc hosts too. The mistakes are not symmetric:
88
+ //
89
+ // pick musl, actually glibc → works, nothing to resolve
90
+ // pick gnu, actually musl → hard failure, "soldr: not found"
91
+ // pick gnu, glibc too old → hard failure, "GLIBC_2.39 not found"
92
+ //
93
+ // Only the first is recoverable.
94
+ //
95
+ // `probes` exists so the branches can be tested on any host; the defaults are
96
+ // the real detectors.
97
+ function detectLibc(platform = process.platform, probes = {}) {
56
98
  if (platform !== "linux") {
57
99
  return null;
58
100
  }
101
+ const readHeader =
102
+ probes.readHeader ||
103
+ (() => process.report && process.report.getReport && process.report.getReport().header);
104
+ const listLib = probes.listLib || (() => fs.readdirSync("/lib"));
105
+
59
106
  try {
60
- const header = process.report && process.report.getReport && process.report.getReport().header;
61
- if (header && typeof header.glibcVersionRuntime === "string" && header.glibcVersionRuntime.length > 0) {
62
- return "gnu";
63
- }
64
- if (header && Object.prototype.hasOwnProperty.call(header, "glibcVersionRuntime")) {
65
- // Field present but empty / null → Node was built against musl.
107
+ const entries = listLib();
108
+ if (entries.some((name) => /^ld-musl-.+\.so\.1$/.test(name))) {
66
109
  return "musl";
67
110
  }
68
111
  } catch (err) {
69
- // process.report can throw on locked-down environments; fall through.
112
+ // /lib may not be readable in heavily sandboxed containers; fall through.
70
113
  }
71
114
  try {
72
- const entries = fs.readdirSync("/lib");
73
- if (entries.some((name) => /^ld-musl-.+\.so\.1$/.test(name))) {
74
- return "musl";
115
+ const header = readHeader();
116
+ const runtime = header && header.glibcVersionRuntime;
117
+ if (
118
+ typeof runtime === "string" &&
119
+ runtime.length > 0 &&
120
+ compareVersions(runtime, MIN_GLIBC_FOR_GNU) >= 0
121
+ ) {
122
+ return "gnu";
75
123
  }
76
124
  } catch (err) {
77
- // /lib may not be readable in heavily sandboxed containers; fall through.
125
+ // process.report can throw on locked-down environments; fall through.
78
126
  }
79
- return "gnu";
127
+ return "musl";
80
128
  }
81
129
 
82
130
  function platformTarget(platform = process.platform, arch = process.arch, libc = detectLibc(platform)) {
83
131
  const key =
84
132
  platform === "linux"
85
- ? `${platform}-${arch}-${libc || "gnu"}`
133
+ ? `${platform}-${arch}-${libc || "musl"}`
86
134
  : `${platform}-${arch}`;
87
135
  const target = TARGETS[key];
88
136
  if (!target) {
@@ -153,6 +201,24 @@ function checksumFor(checksumsText, filename) {
153
201
  throw new Error(`checksum entry not found for ${filename}`);
154
202
  }
155
203
 
204
+ // The integrity check for everything this package installs
205
+ // (docs/TRUST_BOUNDARIES.md). Extracted from `install()` so it can be
206
+ // tested: while it was inline, `checksumFor` was covered but the comparison
207
+ // itself was not, so deleting the mismatch branch would have disabled
208
+ // verification with every test still green.
209
+ //
210
+ // Throws rather than returning a boolean, because the only correct response
211
+ // to a mismatch is to stop, and a caller that forgot to check a returned
212
+ // false would install the archive anyway.
213
+ function verifyArchiveChecksum(archive, checksumsText, filename) {
214
+ const expected = checksumFor(checksumsText, filename);
215
+ const actual = crypto.createHash("sha256").update(archive).digest("hex");
216
+ if (actual !== expected) {
217
+ throw new Error(`checksum mismatch for ${filename}: expected ${expected}, got ${actual}`);
218
+ }
219
+ return actual;
220
+ }
221
+
156
222
  function run(command, args, options = {}) {
157
223
  const result = childProcess.spawnSync(command, args, {
158
224
  stdio: "inherit",
@@ -232,11 +298,7 @@ async function install() {
232
298
  download(checksumUrl).then((buffer) => buffer.toString("utf8")),
233
299
  ]);
234
300
 
235
- const expected = checksumFor(checksums, filename);
236
- const actual = crypto.createHash("sha256").update(archive).digest("hex");
237
- if (actual !== expected) {
238
- throw new Error(`checksum mismatch for ${filename}: expected ${expected}, got ${actual}`);
239
- }
301
+ verifyArchiveChecksum(archive, checksums, filename);
240
302
 
241
303
  const archivePath = path.join(tmp, filename);
242
304
  const extractDir = path.join(tmp, "extract");
@@ -312,9 +374,12 @@ if (require.main === module) {
312
374
 
313
375
  module.exports = {
314
376
  ARCHIVE_EXT,
377
+ MIN_GLIBC_FOR_GNU,
378
+ compareVersions,
315
379
  BUNDLED_BINARIES,
316
380
  TARGETS,
317
381
  checksumFor,
382
+ verifyArchiveChecksum,
318
383
  detectLibc,
319
384
  platformTarget,
320
385
  releaseBaseUrl,
@@ -100,6 +100,12 @@ assert.strictEqual(
100
100
  // Default (no libc arg) falls back to detectLibc; on most CI hosts that's
101
101
  // gnu. We don't assert the triple here — just that the call resolves.
102
102
  assert.ok(install.platformTarget("linux", "x64").triple.startsWith("x86_64-unknown-linux-"));
103
+ // An explicitly-unknown libc must take the runs-anywhere build, matching
104
+ // detectLibc's own unknown case rather than contradicting it.
105
+ assert.strictEqual(
106
+ install.platformTarget("linux", "x64", null).triple,
107
+ "x86_64-unknown-linux-musl",
108
+ );
103
109
  assert.strictEqual(install.platformTarget("darwin", "x64").triple, "x86_64-apple-darwin");
104
110
  assert.strictEqual(install.platformTarget("darwin", "arm64").triple, "aarch64-apple-darwin");
105
111
  assert.strictEqual(install.platformTarget("win32", "x64").triple, "x86_64-pc-windows-msvc");
@@ -115,6 +121,193 @@ assert.strictEqual(install.detectLibc("win32"), null);
115
121
  const linuxLibc = install.detectLibc("linux");
116
122
  assert.ok(linuxLibc === "gnu" || linuxLibc === "musl", `unexpected libc: ${linuxLibc}`);
117
123
 
124
+ // detectLibc's branches, driven through the probe seam so they are
125
+ // exercised on every host rather than only on whichever libc CI runs.
126
+ const throwingProbe = () => {
127
+ throw new Error("probe unavailable");
128
+ };
129
+ const NEW_ENOUGH = install.MIN_GLIBC_FOR_GNU;
130
+
131
+ // Numeric version comparison. A lexical compare ranks "2.9" above "2.39"
132
+ // and would wave through a host that cannot run the binary.
133
+ assert.strictEqual(install.compareVersions("2.9", "2.39"), -1);
134
+ assert.strictEqual(install.compareVersions("2.39", "2.9"), 1);
135
+ assert.strictEqual(install.compareVersions("2.39", "2.39"), 0);
136
+ assert.strictEqual(install.compareVersions("2.40", "2.39"), 1);
137
+ assert.strictEqual(install.compareVersions("2.2.5", "2.14"), -1);
138
+
139
+ // 1. A glibc new enough for the shipped artifact takes the gnu build.
140
+ assert.strictEqual(
141
+ install.detectLibc("linux", {
142
+ readHeader: () => ({ glibcVersionRuntime: NEW_ENOUGH }),
143
+ listLib: () => ["ld-linux-x86-64.so.2", "libc.so.6"],
144
+ }),
145
+ "gnu",
146
+ );
147
+
148
+ // 2. A glibc that is too old must NOT take the gnu build. This is the live
149
+ // bug: Debian 12 reports 2.36, the published gnu binary requires
150
+ // GLIBC_2.39, and it dies with "version `GLIBC_2.39' not found" while the
151
+ // musl artifact from the same release runs fine.
152
+ assert.strictEqual(
153
+ install.detectLibc("linux", {
154
+ readHeader: () => ({ glibcVersionRuntime: "2.36" }),
155
+ listLib: () => ["ld-linux-x86-64.so.2", "libc.so.6"],
156
+ }),
157
+ "musl",
158
+ );
159
+
160
+ // 3. A musl SYSTEM wins over whatever Node was linked against. Node built
161
+ // against glibc on alpine (`apk add nodejs-current`) reports a perfectly
162
+ // good glibc version, so the filesystem probe has to be consulted FIRST --
163
+ // otherwise this answers "gnu" and never looks at /lib at all.
164
+ assert.strictEqual(
165
+ install.detectLibc("linux", {
166
+ readHeader: () => ({ glibcVersionRuntime: NEW_ENOUGH }),
167
+ listLib: () => ["ld-musl-x86_64.so.1"],
168
+ }),
169
+ "musl",
170
+ );
171
+
172
+ // 4. Real musl Node omits the property entirely (verified on node:22-alpine:
173
+ // hasOwnProperty is false, not present-and-empty).
174
+ assert.strictEqual(
175
+ install.detectLibc("linux", {
176
+ readHeader: () => ({}),
177
+ listLib: () => ["ld-musl-x86_64.so.1"],
178
+ }),
179
+ "musl",
180
+ );
181
+
182
+ // 5. Both probes unavailable (heavily sandboxed container) -> musl, the only
183
+ // artifact that runs without knowing anything about the host.
184
+ assert.strictEqual(
185
+ install.detectLibc("linux", { readHeader: throwingProbe, listLib: throwingProbe }),
186
+ "musl",
187
+ );
188
+
189
+ // 6. A glibc host whose version cannot be read is also unknown: /lib says
190
+ // glibc but carries no version, so the floor cannot be confirmed.
191
+ assert.strictEqual(
192
+ install.detectLibc("linux", {
193
+ readHeader: throwingProbe,
194
+ listLib: () => ["ld-linux-x86-64.so.2", "libc.so.6"],
195
+ }),
196
+ "musl",
197
+ );
198
+
199
+ // The probe seam must not leak to other platforms: a non-Linux host still
200
+ // short-circuits to null before any probe runs.
201
+ assert.strictEqual(
202
+ install.detectLibc("darwin", { readHeader: throwingProbe, listLib: throwingProbe }),
203
+ null,
204
+ );
205
+
206
+ // MIN_GLIBC_FOR_GNU must track the ceiling release-auto.yml enforces on the
207
+ // gnu BINARIES. If the release build is fixed to link a 2.17 baseline and that
208
+ // ceiling drops, the installer must follow it down -- otherwise every glibc
209
+ // host below 2.39 keeps being sent to musl long after gnu would work.
210
+ //
211
+ // Anchored on the script name rather than on any `--max-glibc`. release-auto
212
+ // now passes that flag twice: 2.39 to verify_glibc_baseline.py for the
213
+ // binaries, and 2.17 to verify_wheel_glibc.py for the wheel contents. A bare
214
+ // match takes whichever appears first, so this was correct only by the order
215
+ // the steps happen to sit in. Reorder them and the lockstep would demand the
216
+ // installer drop to 2.17, routing glibc 2.17-2.38 hosts to a binary that needs
217
+ // 2.39 -- the bug #2081 fixed, reintroduced by its own guard.
218
+ function glibcCeilingsFor(workflowText, scriptName) {
219
+ const found = [];
220
+ const lines = workflowText.split(/\r?\n/);
221
+ for (let i = 0; i < lines.length; i += 1) {
222
+ if (!lines[i].includes(scriptName)) {
223
+ continue;
224
+ }
225
+ // The invocation may be split across continuation lines, so look at the
226
+ // matching line and the few that follow it.
227
+ const window = lines.slice(i, i + 4).join("\n");
228
+ const match = window.match(/--max-glibc\s+([0-9][0-9.]*)/);
229
+ if (match) {
230
+ found.push(match[1]);
231
+ }
232
+ }
233
+ return found;
234
+ }
235
+
236
+ function glibcCeilingFor(workflowText, scriptName) {
237
+ const found = glibcCeilingsFor(workflowText, scriptName);
238
+ if (found.length === 0) {
239
+ return null;
240
+ }
241
+ // release-auto invokes verify_glibc_baseline.py twice -- once pre-staging on
242
+ // the built binary, once post-staging across the whole bundle. Returning the
243
+ // first would let the two drift apart with the installer silently following
244
+ // only one of them, which is the same order-dependence this function was
245
+ // written to remove.
246
+ const distinct = [...new Set(found)];
247
+ assert.strictEqual(
248
+ distinct.length,
249
+ 1,
250
+ `release-auto.yml passes conflicting --max-glibc values to ${scriptName}: ` +
251
+ `${found.join(", ")}. They gate the same artifacts and must agree.`,
252
+ );
253
+ return distinct[0];
254
+ }
255
+
256
+ // Pin the anchoring itself: with the wheel invocation first, the binary
257
+ // ceiling must still resolve to the binary ceiling.
258
+ {
259
+ const reordered = [
260
+ "python3 .github/scripts/verify_wheel_glibc.py --max-glibc 2.17 dist/*.whl",
261
+ "python3 .github/scripts/verify_glibc_baseline.py --max-glibc 2.39 soldr",
262
+ ].join("\n");
263
+ assert.strictEqual(
264
+ glibcCeilingFor(reordered, "verify_glibc_baseline.py"),
265
+ "2.39",
266
+ "the binary ceiling must not be confused with the wheel ceiling",
267
+ );
268
+ assert.strictEqual(
269
+ glibcCeilingFor(reordered, "verify_wheel_glibc.py"),
270
+ "2.17",
271
+ "the wheel ceiling must resolve independently",
272
+ );
273
+ assert.strictEqual(glibcCeilingFor(reordered, "not_a_script.py"), null);
274
+
275
+ // Two invocations of the same script that agree resolve to that value...
276
+ const agreeing = [
277
+ "python3 .github/scripts/verify_glibc_baseline.py --max-glibc 2.39 soldr",
278
+ "python3 .github/scripts/verify_glibc_baseline.py --max-glibc 2.39 bundle",
279
+ ].join("\n");
280
+ assert.strictEqual(glibcCeilingFor(agreeing, "verify_glibc_baseline.py"), "2.39");
281
+
282
+ // ...and two that disagree are a hard error rather than a silent pick.
283
+ const conflicting = [
284
+ "python3 .github/scripts/verify_glibc_baseline.py --max-glibc 2.17 soldr",
285
+ "python3 .github/scripts/verify_glibc_baseline.py --max-glibc 2.39 bundle",
286
+ ].join("\n");
287
+ assert.throws(
288
+ () => glibcCeilingFor(conflicting, "verify_glibc_baseline.py"),
289
+ /conflicting --max-glibc/,
290
+ "two ceilings for the same script must not be silently reconciled",
291
+ );
292
+ }
293
+
294
+ const releaseWorkflow = fs.readFileSync(
295
+ path.join(root, ".github", "workflows", "release-auto.yml"),
296
+ "utf8",
297
+ );
298
+ const binaryCeiling = glibcCeilingFor(releaseWorkflow, "verify_glibc_baseline.py");
299
+ assert(
300
+ binaryCeiling,
301
+ "release-auto.yml must pass --max-glibc to verify_glibc_baseline.py",
302
+ );
303
+ assert.strictEqual(
304
+ install.MIN_GLIBC_FOR_GNU,
305
+ binaryCeiling,
306
+ `install.js MIN_GLIBC_FOR_GNU (${install.MIN_GLIBC_FOR_GNU}) must match the ` +
307
+ `--max-glibc ceiling verify_glibc_baseline.py enforces in release-auto.yml ` +
308
+ `(${binaryCeiling})`,
309
+ );
310
+
118
311
  assert.strictEqual(
119
312
  install.checksumFor(
120
313
  "abc123 soldr-v0.7.29-x86_64-unknown-linux-gnu.tar.zst\n",
@@ -123,6 +316,83 @@ assert.strictEqual(
123
316
  "abc123",
124
317
  );
125
318
 
319
+ // The integrity check for everything this package installs
320
+ // (docs/TRUST_BOUNDARIES.md). `checksumFor` was covered but the comparison
321
+ // that uses it was inline in install() and untested, so deleting the
322
+ // mismatch branch would have disabled verification with every test green.
323
+ const crypto = require("crypto");
324
+ const ARCHIVE_BYTES = Buffer.from("pretend this is a tar.zst");
325
+ const ARCHIVE_NAME = "soldr-v9.9.9-x86_64-unknown-linux-musl.tar.zst";
326
+ const ARCHIVE_SHA = crypto.createHash("sha256").update(ARCHIVE_BYTES).digest("hex");
327
+ const SUMS = `${ARCHIVE_SHA} ${ARCHIVE_NAME}
328
+ `;
329
+
330
+ // A matching digest returns it rather than throwing.
331
+ assert.strictEqual(
332
+ install.verifyArchiveChecksum(ARCHIVE_BYTES, SUMS, ARCHIVE_NAME),
333
+ ARCHIVE_SHA,
334
+ );
335
+
336
+ // A tampered archive must throw. This is the case that had no coverage.
337
+ assert.throws(
338
+ () =>
339
+ install.verifyArchiveChecksum(
340
+ Buffer.concat([ARCHIVE_BYTES, Buffer.from("tampered")]),
341
+ SUMS,
342
+ ARCHIVE_NAME,
343
+ ),
344
+ /checksum mismatch/,
345
+ "a tampered archive must be rejected",
346
+ );
347
+
348
+ // A digest for a DIFFERENT file must not be accepted for this one: the
349
+ // lookup is by exact filename, so a sums file listing only some other
350
+ // artifact fails closed rather than matching the first line it sees.
351
+ assert.throws(
352
+ () =>
353
+ install.verifyArchiveChecksum(
354
+ ARCHIVE_BYTES,
355
+ `${ARCHIVE_SHA} some-other-artifact.tar.zst
356
+ `,
357
+ ARCHIVE_NAME,
358
+ ),
359
+ /checksum entry not found/,
360
+ "a sums file without this artifact must be rejected",
361
+ );
362
+
363
+ // An empty sums file is not a free pass.
364
+ assert.throws(
365
+ () => install.verifyArchiveChecksum(ARCHIVE_BYTES, "", ARCHIVE_NAME),
366
+ /checksum entry not found/,
367
+ "an empty checksums file must be rejected",
368
+ );
369
+
370
+ // Real SHA256SUMS.txt files use two spaces and may carry CRLF; both must
371
+ // parse. Verified against the published v0.8.29 file, which is
372
+ // `<hash> <name>` with 16 entries covering every release asset.
373
+ assert.strictEqual(
374
+ install.verifyArchiveChecksum(
375
+ ARCHIVE_BYTES,
376
+ `deadbeef unrelated.whl
377
+ ${ARCHIVE_SHA} ${ARCHIVE_NAME}
378
+ `,
379
+ ARCHIVE_NAME,
380
+ ),
381
+ ARCHIVE_SHA,
382
+ );
383
+
384
+ // Digests are compared case-insensitively on the manifest side: an
385
+ // uppercase entry names the same bytes.
386
+ assert.strictEqual(
387
+ install.verifyArchiveChecksum(
388
+ ARCHIVE_BYTES,
389
+ `${ARCHIVE_SHA.toUpperCase()} ${ARCHIVE_NAME}
390
+ `,
391
+ ARCHIVE_NAME,
392
+ ),
393
+ ARCHIVE_SHA,
394
+ );
395
+
126
396
  assert.strictEqual(install.ARCHIVE_EXT, "tar.zst");
127
397
  assert.deepStrictEqual(
128
398
  install.BUNDLED_BINARIES,