Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion mordant-baseline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"defaulted_failure:src/bun_core/string/immutable.rs" = 1

[bun_install]
"defaulted_failure:src/install/npm.rs" = 1
"unread_none:src/install/PackageInstall.rs" = 1

[bun_jsc]
Expand Down
35 changes: 31 additions & 4 deletions src/install/npm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2599,16 +2599,43 @@ impl PackageManifest {
package_version.unpacked_size = n.value() as u32;
}

if let Some(shasum_str) = dist.get(b"integrity").and_then(|v| v.as_str()) {
package_version.integrity = Integrity::parse(shasum_str);
let integrity_str = dist
.get(b"integrity")
.and_then(|v| v.as_str())
.unwrap_or(b"");
if !integrity_str.is_empty() {
package_version.integrity = Integrity::parse(integrity_str);
if package_version.integrity.tag.is_supported() {
break 'integrity;
}
}

let mut malformed_shasum = false;
if let Some(shasum_str) = dist.get(b"shasum").and_then(|v| v.as_str()) {
package_version.integrity =
Integrity::parse_sha_sum(shasum_str).unwrap_or_default();
match Integrity::parse_sha_sum(shasum_str) {
Ok(integrity) => {
package_version.integrity = integrity;
if integrity.tag.is_supported() {
break 'integrity;
}
}
Err(_) => malformed_shasum = true,
}
}
Comment thread
claude[bot] marked this conversation as resolved.

// A registry that advertises no hash at all stays quiet; one
// that advertises a hash we cannot use gets a warning, since
// the version will install unverified either way.
Comment thread
robobun marked this conversation as resolved.
Outdated
if !integrity_str.is_empty() || malformed_shasum {
log.add_warning_fmt(
None,
bun_ast::Loc::EMPTY,
format_args!(
"Unsupported or malformed integrity hash in registry metadata for {}@{}; its tarball will not be verified",
bstr::BStr::new(expected_name),
bstr::BStr::new(version_name),
),
);
}
}
}
Expand Down
100 changes: 95 additions & 5 deletions test/cli/install/bun-install-tarball-integrity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,9 +649,11 @@ describe.concurrent("tarball integrity metadata forms", () => {
tgz,
sha512: "sha512-" + createHash("sha512").update(tgz).digest("base64"),
sha384: "sha384-" + createHash("sha384").update(tgz).digest("base64"),
sha1: "sha1-" + createHash("sha1").update(tgz).digest("base64"),
shasum: createHash("sha1").update(tgz).digest("hex"),
};
}
function serveManifest(integrity: string, tgz: Buffer) {
function serveManifest(dist: { integrity?: string; shasum?: string }, tgz: Buffer) {
const server = Bun.serve({
port: 0,
hostname: "127.0.0.1",
Expand All @@ -666,7 +668,7 @@ describe.concurrent("tarball integrity metadata forms", () => {
name: "pkg",
version: "1.0.0",
dist: {
integrity,
...dist,
tarball: `http://127.0.0.1:${server.port}/pkg/-/pkg-1.0.0.tgz`,
},
},
Expand Down Expand Up @@ -700,7 +702,7 @@ describe.concurrent("tarball integrity metadata forms", () => {
const real = buildTarball(Buffer.from('{"name":"pkg","version":"1.0.0"}\n'));
const other = buildTarball(Buffer.from('{"name":"other","version":"9.9.9"}\n'));

await using server = serveManifest(`${other.sha512} ${real.sha384}`, real.tgz);
await using server = serveManifest({ integrity: `${other.sha512} ${real.sha384}` }, real.tgz);
using dir = projectDir("integrity-multi-hash", server.port);

await using proc = spawn({
Expand All @@ -720,7 +722,7 @@ describe.concurrent("tarball integrity metadata forms", () => {
const real = buildTarball(Buffer.from('{"name":"pkg","version":"1.0.0"}\n'));
const other = buildTarball(Buffer.from('{"name":"other","version":"9.9.9"}\n'));

await using server = serveManifest(`${real.sha512} ${other.sha384}`, real.tgz);
await using server = serveManifest({ integrity: `${real.sha512} ${other.sha384}` }, real.tgz);
using dir = projectDir("integrity-multi-hash-lock", server.port);

await using proc = spawn({
Expand All @@ -743,7 +745,7 @@ describe.concurrent("tarball integrity metadata forms", () => {
const real = buildTarball(Buffer.from('{"name":"pkg","version":"1.0.0"}\n'));
const other = buildTarball(Buffer.from('{"name":"other","version":"9.9.9"}\n'));

await using server = serveManifest(`${other.sha512}?vcs=git`, real.tgz);
await using server = serveManifest({ integrity: `${other.sha512}?vcs=git` }, real.tgz);
using dir = projectDir("integrity-option-suffix", server.port);

await using proc = spawn({
Expand All @@ -758,6 +760,94 @@ describe.concurrent("tarball integrity metadata forms", () => {
expect(stdout).not.toContain("1 package installed");
expect(exitCode).not.toBe(0);
});

it("verifies the tarball against the manifest shasum when there is no integrity field", async () => {
const real = buildTarball(Buffer.from('{"name":"pkg","version":"1.0.0"}\n'));
const other = buildTarball(Buffer.from('{"name":"other","version":"9.9.9"}\n'));

await using server = serveManifest({ shasum: other.shasum }, real.tgz);
using dir = projectDir("integrity-shasum-mismatch", server.port);

await using proc = spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: { ...env, BUN_INSTALL_CACHE_DIR: join(String(dir), ".cache") },
stdout: "pipe",
stderr: "pipe",
});
const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]);
expect(stderr).not.toContain("Unsupported or malformed integrity hash");
expect(stderr + stdout).toContain("Integrity check failed");
expect(stdout).not.toContain("1 package installed");
expect(exitCode).not.toBe(0);
});

it("falls back to the manifest shasum when the integrity field is unusable, without warning", async () => {
const real = buildTarball(Buffer.from('{"name":"pkg","version":"1.0.0"}\n'));

await using server = serveManifest({ integrity: "md5-AAAAAAAAAAAAAAAAAAAAAA==", shasum: real.shasum }, real.tgz);
using dir = projectDir("integrity-shasum-fallback", server.port);

await using proc = spawn({
cmd: [bunExe(), "install", "--save-text-lockfile"],
cwd: String(dir),
env: { ...env, BUN_INSTALL_CACHE_DIR: join(String(dir), ".cache") },
stdout: "pipe",
stderr: "pipe",
});
const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]);
expect(stderr).not.toContain("Unsupported or malformed integrity hash");
expect(stdout).toContain("1 package installed");
expect(await file(join(String(dir), "bun.lock")).text()).toContain(real.sha1);
expect(exitCode).toBe(0);
});

it.each([
// Same length as a sha1 hex digest, but not hex.
["malformed shasum", { shasum: Buffer.alloc(40, "x").toString() }],
["unsupported integrity algorithm", { integrity: "md5-AAAAAAAAAAAAAAAAAAAAAA==" }],
["malformed integrity and empty shasum", { integrity: "sha512-!!!", shasum: "" }],
] as const)("warns instead of silently skipping verification: %s", async (_label, dist) => {
const real = buildTarball(Buffer.from('{"name":"pkg","version":"1.0.0"}\n'));

await using server = serveManifest(dist, real.tgz);
using dir = projectDir("integrity-unusable", server.port);

await using proc = spawn({
cmd: [bunExe(), "install", "--save-text-lockfile"],
cwd: String(dir),
env: { ...env, BUN_INSTALL_CACHE_DIR: join(String(dir), ".cache") },
stdout: "pipe",
stderr: "pipe",
});
const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]);
expect(stderr).toContain(
"warn: Unsupported or malformed integrity hash in registry metadata for pkg@1.0.0; its tarball will not be verified",
);
expect(stdout).toContain("1 package installed");
// Nothing usable was advertised, so the lockfile carries no pin for it.
expect(await file(join(String(dir), "bun.lock")).text()).not.toMatch(/sha\d+-/);
expect(exitCode).toBe(0);
});

it("stays quiet when the manifest advertises no hash at all", async () => {
const real = buildTarball(Buffer.from('{"name":"pkg","version":"1.0.0"}\n'));

await using server = serveManifest({}, real.tgz);
using dir = projectDir("integrity-absent", server.port);

await using proc = spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: { ...env, BUN_INSTALL_CACHE_DIR: join(String(dir), ".cache") },
stdout: "pipe",
stderr: "pipe",
});
const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]);
expect(stderr).not.toContain("Unsupported or malformed integrity hash");
expect(stdout).toContain("1 package installed");
expect(exitCode).toBe(0);
});
});

describe.concurrent.each(["hoisted", "isolated"] as const)("tarball download failure (%s)", linker => {
Expand Down