fix(javascript): strip peer-dep suffix from deno.lock npm keys (#5055)

deno.lock npm keys append resolved peer dependencies after the version,
separated by "_" (e.g. "typedoc@0.28.19_typescript@6.0.3" or the scoped
"@scope/name@1.2.3_@scope+peer@4.5.6"). parseDenoNpmNameVersion split on the
last "@", capturing a peer's "@" and producing a garbage package name plus
the peer's version (and a corrupt PURL that breaks vulnerability matching).

Strip the peer suffix (the first "_" after the version "@") before splitting
name@version; a semver version never contains "_".

Signed-off-by: Synvoya <16019863+Synvoya@users.noreply.github.com>
Co-authored-by: Synvoya <16019863+Synvoya@users.noreply.github.com>
This commit is contained in:
Synvoya 2026-07-14 00:38:32 +10:00 committed by GitHub
parent 1dcac54b0e
commit bd0917fe93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 51 additions and 7 deletions

View File

@ -96,19 +96,35 @@ func parseDenoJsrNameVersion(nameVersion string) (name, version string) {
} }
func parseDenoNpmNameVersion(nameVersion string) (name, version string) { func parseDenoNpmNameVersion(nameVersion string) (name, version string) {
// deno.lock npm keys append resolved peer dependencies after the version,
// separated by "_", e.g. "typedoc@0.28.19_typescript@6.0.3" or the scoped
// "@scope/name@1.2.3_@scope+peer@4.5.6". The peer suffix must be stripped
// before splitting name@version, otherwise LastIndex("@") splits on a peer's
// "@" and yields a garbage name and the peer's version.
//
// A semver version never contains "_", and it begins immediately after the
// package name's version "@", so the first "_" following that "@" starts the
// peer list.
var versionAt int
if strings.HasPrefix(nameVersion, "@") { if strings.HasPrefix(nameVersion, "@") {
rest := nameVersion[1:] // scoped "@scope/name@version": the version "@" is the second "@"
idx := strings.LastIndex(rest, "@") idx := strings.Index(nameVersion[1:], "@")
if idx <= 0 { if idx <= 0 {
return "", "" return "", ""
} }
return nameVersion[:idx+1], rest[idx+1:] versionAt = idx + 1
} else {
versionAt = strings.Index(nameVersion, "@")
if versionAt <= 0 {
return "", ""
}
} }
idx := strings.LastIndex(nameVersion, "@")
if idx <= 0 { base := nameVersion
return "", "" if underscore := strings.Index(nameVersion[versionAt:], "_"); underscore >= 0 {
base = nameVersion[:versionAt+underscore]
} }
return nameVersion[:idx], nameVersion[idx+1:] return base[:versionAt], base[versionAt+1:]
} }
func newDenoJsrPackage(location file.Location, name, version string, meta denoJsrPackage) pkg.Package { func newDenoJsrPackage(location file.Location, name, version string, meta denoJsrPackage) pkg.Package {

View File

@ -55,6 +55,28 @@ func TestParseDenoLock(t *testing.T) {
Integrity: "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d", Integrity: "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d",
}, },
}, },
{
// npm key carries a resolved peer dependency suffix ("_typescript@6.0.3")
Name: "typedoc",
Version: "0.28.19",
PURL: "pkg:npm/typedoc@0.28.19",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.NpmPackageLockEntry{
Integrity: "sha512-TYPEDOCtestfixtureintegrityAAAA==",
},
},
{
// scoped npm key with a scope-encoded ("+") peer dependency suffix
Name: "@algolia/autocomplete-preset-algolia",
Version: "1.17.9",
PURL: "pkg:npm/%40algolia/autocomplete-preset-algolia@1.17.9",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.NpmPackageLockEntry{
Integrity: "sha512-ALGOLIAtestfixtureintegrityBBBB==",
},
},
} }
for i := range expectedPkgs { for i := range expectedPkgs {

View File

@ -19,6 +19,12 @@
"npm": { "npm": {
"chalk@5.3.0": { "chalk@5.3.0": {
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="
},
"typedoc@0.28.19_typescript@6.0.3": {
"integrity": "sha512-TYPEDOCtestfixtureintegrityAAAA=="
},
"@algolia/autocomplete-preset-algolia@1.17.9_@algolia+client-search@5.50.0": {
"integrity": "sha512-ALGOLIAtestfixtureintegrityBBBB=="
} }
}, },
"remote": { "remote": {