Compare normalized purls to account for encoding quirks

This commit is contained in:
Justin Holguín
2026-02-20 00:02:37 +00:00
committed by GitHub
parent 9284e0c621
commit 2ced98cbe8
6 changed files with 140 additions and 11 deletions

View File

@@ -1,6 +1,6 @@
import {Change, Changes} from './schemas'
import {octokitClient} from './utils'
import {parsePURL, PackageURL} from './purl'
import {parsePURL, PackageURL, purlsMatch} from './purl'
import * as spdx from './spdx'
/**
@@ -180,11 +180,8 @@ async function groupChanges(
// If it does, we want to filter it out and therefore return false
// If it doesn't, we want to keep it and therefore return true
if (
licenseExclusions.findIndex(
exclusion =>
exclusion.type === changeAsPackageURL.type &&
exclusion.namespace === changeAsPackageURL.namespace &&
exclusion.name === changeAsPackageURL.name
licenseExclusions.findIndex(exclusion =>
purlsMatch(exclusion, changeAsPackageURL)
) !== -1
) {
return false

View File

@@ -70,3 +70,25 @@ export function parsePURL(purl: string): PackageURL {
// we don't parse subpath or attributes, so we're done here
return result
}
// Returns the full name of a package, combining namespace and name.
// This normalizes PURLs where the namespace separator '/' may have been
// percent-encoded as '%2F', causing it to be parsed as part of the name
// rather than splitting namespace and name.
function fullName(purl: PackageURL): string | null {
if (purl.namespace && purl.name) {
return `${purl.namespace}/${purl.name}`
}
return purl.name ?? purl.namespace
}
// Compare two PackageURLs for equality, ignoring version and normalizing
// namespace/name splits. This handles the case where a PURL like
// 'pkg:npm/%40scope%2Fname' is parsed as {namespace: null, name: '@scope/name'}
// while 'pkg:npm/%40scope/name' is parsed as {namespace: '@scope', name: 'name'}.
export function purlsMatch(a: PackageURL, b: PackageURL): boolean {
if (a.type !== b.type) {
return false
}
return fullName(a) === fullName(b)
}