Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
45 changes: 39 additions & 6 deletions packages/node/src/__tests__/feature-flags.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3291,8 +3291,14 @@ describe('semver parsing', () => {
expect(parseSemver('1.2.3.4.5.6')).toEqual([1, 2, 3])
})

it('handles leading zeros', () => {
expect(parseSemver('01.02.03')).toEqual([1, 2, 3])
it('rejects leading zeros per semver 2.0.0 §2', () => {
expect(() => parseSemver('01.02.03')).toThrow(InconclusiveMatchError)
expect(() => parseSemver('1.07.3')).toThrow(InconclusiveMatchError)
expect(() => parseSemver('001.2.3')).toThrow(InconclusiveMatchError)
// Literal "0" components remain valid.
expect(parseSemver('0.1.0')).toEqual([0, 1, 0])
expect(parseSemver('1.0.0')).toEqual([1, 0, 0])
expect(parseSemver('0.0.0')).toEqual([0, 0, 0])
})

it('throws on invalid input', () => {
Expand Down Expand Up @@ -3639,10 +3645,37 @@ describe('semver operators', () => {
)
})

it('handles leading zeros', () => {
expect(matchProperty({ key: 'version', value: '01.02.03', operator: 'semver_eq' }, { version: '1.2.3' })).toBe(
true
)
it('rejects leading zeros in override values across operators', () => {
// Per semver 2.0.0 §2, numeric identifiers MUST NOT include leading zeros.
for (const bad of ['01.2.3', '1.02.3', '1.2.03', '1.07.3', '001.2.3']) {
expect(() =>
matchProperty({ key: 'version', value: '1.2.3', operator: 'semver_eq' }, { version: bad })
).toThrow(InconclusiveMatchError)
}
Comment thread
dmarticus marked this conversation as resolved.
Outdated
})

it('rejects leading zeros in flag values across range operators', () => {
expect(() =>
matchProperty({ key: 'version', value: '1.07.3', operator: 'semver_gt' }, { version: '1.8.0' })
).toThrow(InconclusiveMatchError)
expect(() =>
matchProperty({ key: 'version', value: '01.2.3', operator: 'semver_caret' }, { version: '1.2.3' })
).toThrow(InconclusiveMatchError)
expect(() =>
matchProperty({ key: 'version', value: '1.02.3', operator: 'semver_tilde' }, { version: '1.2.3' })
).toThrow(InconclusiveMatchError)
expect(() =>
matchProperty({ key: 'version', value: '01.*', operator: 'semver_wildcard' }, { version: '1.2.3' })
).toThrow(InconclusiveMatchError)
expect(() =>
matchProperty({ key: 'version', value: '1.07.*', operator: 'semver_wildcard' }, { version: '1.7.3' })
).toThrow(InconclusiveMatchError)
})
Comment thread
dmarticus marked this conversation as resolved.
Outdated

it('still accepts literal zero components', () => {
expect(matchProperty({ key: 'version', value: '0.1.0', operator: 'semver_eq' }, { version: '0.1.0' })).toBe(true)
expect(matchProperty({ key: 'version', value: '1.0.0', operator: 'semver_eq' }, { version: '1.0.0' })).toBe(true)
expect(matchProperty({ key: 'version', value: '0.0.0', operator: 'semver_eq' }, { version: '0.0.0' })).toBe(true)
})

it('handles 4-part versions', () => {
Expand Down
34 changes: 23 additions & 11 deletions packages/node/src/extensions/feature-flags/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,20 @@ function isValidRegex(regex: string): boolean {

type SemverTuple = [number, number, number]

/**
* Parse a single numeric identifier from a semver string.
* Per semver 2.0.0 §2, numeric identifiers MUST NOT include leading zeros.
*/
function parseSemverNumericIdentifier(part: string, raw: string): number {
if (!/^\d+$/.test(part)) {
throw new InconclusiveMatchError(`Invalid semver: ${raw}`)
}
if (part.length > 1 && part[0] === '0') {
throw new InconclusiveMatchError(`Invalid semver: ${raw}`)
}
return parseInt(part, 10)
}

/**
* Parse a version string into a [major, minor, patch] tuple.
* - Strips leading/trailing whitespace
Expand All @@ -1354,10 +1368,7 @@ function parseSemver(value: string): SemverTuple {
if (part === undefined || part === '') {
return 0
}
if (!/^\d+$/.test(part)) {
throw new InconclusiveMatchError(`Invalid semver: ${value}`)
}
return parseInt(part, 10)
return parseSemverNumericIdentifier(part, value)
}

const major = parsePart(parts[0])
Expand Down Expand Up @@ -1428,10 +1439,14 @@ function computeWildcardBounds(value: string): { lower: SemverTuple; upper: Semv
}

const parts = cleanedText.split('.')
const major = parseInt(parts[0], 10)
if (isNaN(major)) {
throw new InconclusiveMatchError(`Invalid wildcard semver: ${value}`)
const parseWildcardPart = (part: string): number => {
try {
return parseSemverNumericIdentifier(part, value)
} catch {
throw new InconclusiveMatchError(`Invalid wildcard semver: ${value}`)
}
}
const major = parseWildcardPart(parts[0])

let lower: SemverTuple
let upper: SemverTuple
Expand All @@ -1442,10 +1457,7 @@ function computeWildcardBounds(value: string): { lower: SemverTuple; upper: Semv
upper = [major + 1, 0, 0]
} else {
// X.Y.* pattern
const minor = parseInt(parts[1], 10)
if (isNaN(minor)) {
throw new InconclusiveMatchError(`Invalid wildcard semver: ${value}`)
}
const minor = parseWildcardPart(parts[1])
lower = [major, minor, 0]
upper = [major, minor + 1, 0]
}
Expand Down
Loading