Skip to content

Commit fde7e7e

Browse files
committed
fix: allow double hyphens in Composer project names
Signed-off-by: anilb <epipav@gmail.com>
1 parent 2fa15bf commit fde7e7e

2 files changed

Lines changed: 28 additions & 4 deletions

File tree

services/apps/packages_worker/src/packagist/__tests__/listPackages.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ describe('parsePackagistPackageList', () => {
3030
])
3131
})
3232

33+
it('accepts up to two consecutive hyphens in the project segment only', () => {
34+
// Composer's own name spec allows `-{1,2}` as a separator in the project segment
35+
// but only a single separator in the vendor segment.
36+
const { entries, invalid } = parsePackagistPackageList({
37+
packageNames: ['vendor/my--package', 'vendor/my---package', 'my--vendor/package'],
38+
})
39+
expect(entries.map((e) => e.purl)).toEqual(['pkg:composer/vendor/my--package'])
40+
expect(invalid).toBe(2)
41+
})
42+
3343
it('skips and counts invalid names', () => {
3444
const { entries, invalid } = parsePackagistPackageList({
3545
packageNames: [
@@ -58,6 +68,15 @@ describe('parsePackagistPackageList', () => {
5868
expect(invalid).toBe(1)
5969
})
6070

71+
it('rejects a long run of hyphens without catastrophic backtracking (project -{1,2} alternative)', () => {
72+
const pathological = 'a' + '-'.repeat(50) + '!/name'
73+
const start = Date.now()
74+
const { entries, invalid } = parsePackagistPackageList({ packageNames: [pathological] })
75+
expect(Date.now() - start).toBeLessThan(200)
76+
expect(entries).toEqual([])
77+
expect(invalid).toBe(1)
78+
})
79+
6180
it('lowercases and deduplicates names without counting duplicates as invalid', () => {
6281
const { entries, invalid } = parsePackagistPackageList({
6382
packageNames: ['Monolog/Monolog', 'monolog/monolog'],

services/apps/packages_worker/src/packagist/listPackages.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ const PACKAGIST_LIST = 'https://packagist.org/packages/list.json'
77
// Non-backtracking form: a mandatory separator per repetition (vs. an optional one)
88
// removes the ambiguity that let the old `([_.-]?[a-z0-9]+)*` pattern backtrack
99
// exponentially on a long run of the same character class (CodeQL js/redos).
10-
const COMPOSER_NAME_REGEX = /^[a-z0-9]+(?:[_.-][a-z0-9]+)*$/
10+
// Vendor and project segments have different rules per Composer's own name spec: the
11+
// vendor allows only a single separator between alnum runs, but the project segment
12+
// additionally allows up to two consecutive hyphens (e.g. `vendor/my--package` is a
13+
// valid, real Composer name) — verified against Composer's ArrayLoader name pattern.
14+
const COMPOSER_VENDOR_REGEX = /^[a-z0-9]+(?:[_.-][a-z0-9]+)*$/
15+
const COMPOSER_PROJECT_REGEX = /^[a-z0-9]+(?:(?:[_.]|-{1,2})[a-z0-9]+)*$/
1116

1217
export interface PackagistListEntry {
1318
vendor: string
@@ -41,13 +46,13 @@ export function parsePackagistPackageList(json: unknown): {
4146
const lowercased = item.toLowerCase()
4247
const parts = lowercased.split('/')
4348

44-
// Validate: exactly one slash, each side non-empty and matches composer name pattern
49+
// Validate: exactly one slash, each side non-empty and matches its composer name pattern
4550
if (
4651
parts.length !== 2 ||
4752
!parts[0] ||
4853
!parts[1] ||
49-
!COMPOSER_NAME_REGEX.test(parts[0]) ||
50-
!COMPOSER_NAME_REGEX.test(parts[1])
54+
!COMPOSER_VENDOR_REGEX.test(parts[0]) ||
55+
!COMPOSER_PROJECT_REGEX.test(parts[1])
5156
) {
5257
invalid++
5358
continue

0 commit comments

Comments
 (0)