fix(install): manifest package name mismatch (#11549)

This commit is contained in:
Dylan Conway
2024-06-02 22:56:57 -07:00
committed by GitHub
parent 1d89c5988e
commit c2eef9eded
6 changed files with 151 additions and 8 deletions

View File

@@ -1077,7 +1077,7 @@ pub const PackageManifest = struct {
};
if (json.asProperty("name")) |name_q| {
const field = name_q.expr.asString(allocator) orelse return null;
const received_name = name_q.expr.asString(allocator) orelse return null;
// This is intentionally a case insensitive comparision. If the registry is running on a system
// with a case insensitive filesystem, you'll be able to install dependencies with casing that doesn't match.
@@ -1091,12 +1091,52 @@ pub const PackageManifest = struct {
// }
//
// https://github.com/oven-sh/bun/issues/5189
if (!strings.eqlCaseInsensitiveASCII(expected_name, field, true)) {
Output.panic("<r>internal: <red>package name mismatch<r> expected <b>\"{s}\"<r> but received <red>\"{s}\"<r>", .{ expected_name, field });
const equal = if (expected_name.len == 0 or expected_name[0] != '@')
// Unscoped package, just normal case insensitive comparison
strings.eqlCaseInsensitiveASCII(expected_name, received_name, true)
else brk: {
// Scoped package. The registry might url encode the package name changing either or both `@` and `/` into `%40` and `%2F`.
// e.g. "name": "@std%2fsemver" // real world example from crash report
// Expected name `@` exists, check received has either `@` or `%40`
var received_remain = received_name;
if (received_remain.len > 0 and received_remain[0] == '@') {
received_remain = received_remain[1..];
} else if (received_remain.len > 2 and strings.eqlComptime(received_remain[0..3], "%40")) {
received_remain = received_remain[3..];
} else {
break :brk false;
}
var expected_remain = expected_name[1..];
// orelse is invalid because scoped package is missing `/`, but we allow just in case
const slash_index = strings.indexOfChar(expected_remain, '/') orelse break :brk strings.eqlCaseInsensitiveASCII(expected_remain, received_remain, true);
if (slash_index >= received_remain.len) break :brk false;
if (!strings.eqlCaseInsensitiveASCIIIgnoreLength(expected_remain[0..slash_index], received_remain[0..slash_index])) break :brk false;
expected_remain = expected_remain[slash_index + 1 ..];
// Expected name `/` exists, check that received is either `/`, `%2f`, or `%2F`
received_remain = received_remain[slash_index..];
if (received_remain.len > 0 and received_remain[0] == '/') {
received_remain = received_remain[1..];
} else if (received_remain.len > 2 and strings.eqlCaseInsensitiveASCIIIgnoreLength(received_remain[0..3], "%2f")) {
received_remain = received_remain[3..];
} else {
break :brk false;
}
break :brk strings.eqlCaseInsensitiveASCII(expected_remain, received_remain, true);
};
if (!equal) {
Output.panic("<r>internal: <red>Package name mismatch.<r> Expected <b>\"{s}\"<r> but received <red>\"{s}\"<r>", .{ expected_name, received_name });
return null;
}
string_builder.count(field);
string_builder.count(expected_name);
}
if (json.asProperty("modified")) |name_q| {
@@ -1290,10 +1330,10 @@ pub const PackageManifest = struct {
string_buf = ptr[0..string_builder.cap];
}
if (json.asProperty("name")) |name_q| {
const field = name_q.expr.asString(allocator) orelse return null;
result.pkg.name = string_builder.append(ExternalString, field);
}
// Using `expected_name` instead of the name from the manifest. We've already
// checked that they are equal above, but `expected_name` will not have `@`
// or `/` changed to `%40` or `%2f`, ensuring lookups will work later
result.pkg.name = string_builder.append(ExternalString, expected_name);
get_versions: {
if (json.asProperty("versions")) |versions_q| {

View File

@@ -2370,6 +2370,33 @@ describe("workspaces", async () => {
}
});
test("name from manifest is scoped and url encoded", async () => {
await write(
join(packageDir, "package.json"),
JSON.stringify({
name: "foo",
dependencies: {
// `name` in the manifest for these packages is manually changed
// to use `%40` and `%2f`
"@url/encoding.2": "1.0.1",
"@url/encoding.3": "1.0.1",
},
}),
);
await runBunInstall(env, packageDir);
const files = await Promise.all([
file(join(packageDir, "node_modules", "@url", "encoding.2", "package.json")).json(),
file(join(packageDir, "node_modules", "@url", "encoding.3", "package.json")).json(),
]);
expect(files).toEqual([
{ name: "@url/encoding.2", version: "1.0.1" },
{ name: "@url/encoding.3", version: "1.0.1" },
]);
});
describe("update", () => {
test("duplicate peer dependency (one package is invalid_package_id)", async () => {
await write(

View File

@@ -0,0 +1,38 @@
{
"name": "@url%2fencoding.2",
"versions": {
"1.0.1": {
"name": "@url/encoding.2",
"version": "1.0.1",
"_id": "@url/encoding.2@1.0.1",
"_nodeVersion": "22.2.0",
"_npmVersion": "10.8.1",
"dist": {
"integrity": "sha512-IWtV06UQpxWKEbRgmgnInjdPSVqaj88gLcbsJKbX4TuvmU9PpArzyHK5h5H73q5CzKoBDIwptb+cKvr98j+QNA==",
"shasum": "bc2994336b291322c242f3570cc486cf8fcc9756",
"tarball": "http://localhost:4873/@url/encoding.2/-/@url/encoding.2-1.0.1.tgz"
},
"contributors": []
}
},
"time": {
"modified": "2024-06-03T00:01:11.853Z",
"created": "2024-06-03T00:01:11.853Z",
"1.0.1": "2024-06-03T00:01:11.853Z"
},
"users": {},
"dist-tags": {
"latest": "1.0.1"
},
"_uplinks": {},
"_distfiles": {},
"_attachments": {
"encoding.2-1.0.1.tgz": {
"shasum": "bc2994336b291322c242f3570cc486cf8fcc9756",
"version": "1.0.1"
}
},
"_rev": "",
"_id": "@url/encoding.2",
"readme": "ERROR: No README data found!"
}

View File

@@ -0,0 +1,38 @@
{
"name": "%40url%2fencoding.3",
"versions": {
"1.0.1": {
"name": "@url/encoding.3",
"version": "1.0.1",
"_id": "@url/encoding.3@1.0.1",
"_nodeVersion": "22.2.0",
"_npmVersion": "10.8.1",
"dist": {
"integrity": "sha512-LkuYnUyQgbhee/Sz/QL+WSMzvRElhJqzdYCs6oZFcAlZwEMcmyE10X0LfN6UcQ8zX7z0vjSezs+WinFafTlDSw==",
"shasum": "34a69650f7a471f29578381144110db7319c6992",
"tarball": "http://localhost:4873/@url/encoding.3/-/@url/encoding.3-1.0.1.tgz"
},
"contributors": []
}
},
"time": {
"modified": "2024-06-03T00:01:16.079Z",
"created": "2024-06-03T00:01:16.079Z",
"1.0.1": "2024-06-03T00:01:16.079Z"
},
"users": {},
"dist-tags": {
"latest": "1.0.1"
},
"_uplinks": {},
"_distfiles": {},
"_attachments": {
"encoding.3-1.0.1.tgz": {
"shasum": "34a69650f7a471f29578381144110db7319c6992",
"version": "1.0.1"
}
},
"_rev": "",
"_id": "@url/encoding.3",
"readme": "ERROR: No README data found!"
}