diff --git a/lib/modules/versioning/rpm/index.spec.ts b/lib/modules/versioning/rpm/index.spec.ts index 4cee8b966fb5e6..c57f7306a2d15b 100644 --- a/lib/modules/versioning/rpm/index.spec.ts +++ b/lib/modules/versioning/rpm/index.spec.ts @@ -53,6 +53,8 @@ describe('modules/versioning/rpm/index', () => { ${'0z1b2c3'} | ${true} ${'0A1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d'} | ${true} ${'123098140293'} | ${true} + ${'3.12.0-1~a1^20231001'} | ${true} + ${'1.2.3^20231001'} | ${true} `('isValid("$version") === $expected', ({ version, expected }) => { expect(rpm.isValid(version)).toBe(expected); }); @@ -85,6 +87,7 @@ describe('modules/versioning/rpm/index', () => { ${'1.4-1'} | ${'1.4-2'} | ${false} ${'0:1.4'} | ${'a:1.4'} | ${false} ${'a:1.4'} | ${'0:1.4'} | ${false} + ${'3.12.0-1~^2023'} | ${'3.12.0-1^2023'} | ${false} `('equals("$a", "$b") === $expected', ({ a, b, expected }) => { expect(rpm.equals(a, b)).toBe(expected); }); @@ -100,7 +103,7 @@ describe('modules/versioning/rpm/index', () => { ${'2.4.0'} | ${'2.4.beta'} | ${true} ${'2.4.beta'} | ${'2.4'} | ${true} ${'2.4.beta'} | ${'2.4.0'} | ${false} - ${'2.4~'} | ${'2.4~~'} | ${false} + ${'2.4~'} | ${'2.4~~'} | ${true} ${'2.4'} | ${'2.4~'} | ${true} ${'2.4a'} | ${'2.4'} | ${true} ${'2.31-13+rpm11u5'} | ${'2.31-9'} | ${true} @@ -116,7 +119,7 @@ describe('modules/versioning/rpm/index', () => { ${'1:1.0Z0-0'} | ${'1:1.0A0-0'} | ${true} ${'1:1.0a0-0'} | ${'1:1.0Z0-0'} | ${true} ${'1:1.0z0-0'} | ${'1:1.0a0-0'} | ${true} - ${'1:1.0+0-0'} | ${'1:1.0z0-0'} | ${false} + ${'1:1.0+0-0'} | ${'1:1.0z0-0'} | ${true} ${'1:1.0-0-0'} | ${'1:1.0+0-0'} | ${false} ${'1:1.0.0-0'} | ${'1:1.0-0-0'} | ${true} ${'1:1.0:0-0'} | ${'1:1.0.0-0'} | ${false} @@ -130,6 +133,17 @@ describe('modules/versioning/rpm/index', () => { ${'10'} | ${'1.'} | ${true} ${'10'} | ${'1a'} | ${true} ${'a'} | ${'A'} | ${true} + ${'A'} | ${'a'} | ${false} + ${'A1'} | ${'Aa'} | ${false} + ${'aaaaa1'} | ${'aaaaaaaaaaaa2'} | ${false} + ${'a-1~^20231001'} | ${'a-1^20231001'} | ${false} + ${'1'} | ${'2'} | ${false} + ${'a-1~pre2^20231001'}| ${'a-1~pre2^20231002'} | ${false} + ${'a-1'} | ${'a-1~pre1'} | ${true} + ${'4.20-4~beta4'} | ${'4.20-4'} | ${false} + ${'1.2.3~beta2'} | ${'1.2.3~alpha1'} | ${true} + ${'1.2.3-4~alpha1'} | ${'1.2.3-4~beta2'} | ${false} + ${'}}}'} | ${'{{{'} | ${false} `('isGreaterThan("$a", "$b") === $expected', ({ a, b, expected }) => { expect(rpm.isGreaterThan(a, b)).toBe(expected); }); diff --git a/lib/modules/versioning/rpm/index.ts b/lib/modules/versioning/rpm/index.ts index f0315392ae1f64..053839fcd1563a 100644 --- a/lib/modules/versioning/rpm/index.ts +++ b/lib/modules/versioning/rpm/index.ts @@ -7,10 +7,12 @@ export const id = 'rpm'; export const displayName = 'RPM version'; export const urls = [ 'https://docs.fedoraproject.org/en-US/packaging-guidelines/Versioning/', + 'https://fedoraproject.org/wiki/Package_Versioning_Examples', + 'https://fedoraproject.org/wiki/User:Tibbs/TildeCaretVersioning', ]; export const supportsRanges = false; -const alphaNumPattern = regEx(/(\w+)|(\d+)|(~)/g); +const alphaNumPattern = regEx(/([a-zA-Z]+)|(\d+)|(~)/g); const epochPattern = regEx(/^\d+$/); const leadingZerosPattern = regEx(/^0+/); @@ -30,6 +32,21 @@ export interface RpmVersion extends GenericVersion { * same upstream version. */ rpmRelease: string; + + /** + * rpmPreRelease is used to distinguish versions of prerelease of the same upstream and release version + * Example: Python 3.12.0-1 > Python 3.12.0-1~a1 + */ + rpmPreRelease: string; + + /** + * snapshot is an archive taken from upstream's source code control system which is not equivalent to any release version. + * This field must at minimum consist of the date in eight-digit "YYYYMMDD" format. The packager MAY + * include up to 17 characters of additional information after the date. The following formats are suggested: + * YYYYMMDD. + * YYYYMMDD + */ + snapshot: string; } class RpmVersioningApi extends GenericVersioningApi { @@ -54,14 +71,47 @@ class RpmVersioningApi extends GenericVersioningApi { let upstreamVersion: string; let rpmRelease = ''; + let rpmPreRelease = ''; + let snapshot = ''; const releaseIndex = remainingVersion.indexOf('-'); + const prereleaseIndex = remainingVersion.indexOf('~'); + + // Note: There can be a snapshot if there is no prerelease. Snapshot always beat no snapshot, + // so if there is 3.12.0-1 vs 3.12.0-1^20231110, the snapshot wins. + // The logic below only creates snapshot IF there is a prerleease version. This logic is NOT + // correct, but the result is still correct due to the caret being ignored in release, and + // release continue comparing + // + // Note: If there IS a tilde preceding the caret, then snapshot DOES NOT win + // Example: 3.12.0-1~^20231001 LOSES to 3.12.0-1 and + // 3.12.0-1~^20231001 LOSES to 3.12.0-1^20231001 + const snapshotIndex = remainingVersion.indexOf('^'); + if (releaseIndex >= 0) { upstreamVersion = remainingVersion.slice(0, releaseIndex); - rpmRelease = remainingVersion.slice(releaseIndex + 1); + + // Do NOT splice out prerelease, we need to distinguish if the flag is set or not, regardless if there is a version. + // The tilde will get filtered out during regex + if (prereleaseIndex >= 0) { + rpmRelease = remainingVersion.slice(releaseIndex, prereleaseIndex); + if (snapshotIndex >= 0) { + rpmPreRelease = remainingVersion.slice( + prereleaseIndex, + snapshotIndex, + ); + snapshot = remainingVersion.slice(snapshotIndex + 1); + } else { + rpmPreRelease = remainingVersion.slice(prereleaseIndex); + } + } else { + rpmRelease = remainingVersion.slice(releaseIndex + 1); + } } else { upstreamVersion = remainingVersion; } + upstreamVersion; + const release = [...remainingVersion.matchAll(regEx(/\d+/g))].map((m) => parseInt(m[0], 10), ); @@ -71,43 +121,33 @@ class RpmVersioningApi extends GenericVersioningApi { upstreamVersion, rpmRelease, release, - suffix: rpmRelease, + rpmPreRelease, + snapshot, }; } - protected __compare_character(s1: any, s2: any): number { - const matches = Math.min(s1?.length ?? 0, s2?.length ?? 0); + protected _compare_string(s1: string, s2: string): number { + if (s1 === s2) { + return 0; + } - for (let i = 0; i < matches; i++) { + const minLength = Math.min(s1.length, s2.length); + + for (let i = 0; i < minLength; i++) { const c1 = s1[i]; const c2 = s2[i]; + if (c1 === c2) { continue; } - // Numbers are bigger than characters - // Because c1 is a number, it is bigger - else if (is.numericString(c1) && !is.numericString(c2)) { - return 1; - } - // Because c2 is a number, it is bigger - else if (!isNaN(c1) && isNaN(c2)) { - return -1; - } - - // Okay, they're the same type (aka alpha && alpha, or num && num) - // Let's continue with the regular compare - if ((c1 ?? '') > (c2 ?? '')) { + if (c1 > c2) { return 1; - } else if ((c1 ?? '') < (c2 ?? '')) { + } else if (c1 < c2) { return -1; } } - if (s1.length === s2.length) { - return 0; - } - // Okay, they've been the exact same up until now, so return the longer one return s1.length > s2.length ? 1 : -1; } @@ -115,14 +155,16 @@ class RpmVersioningApi extends GenericVersioningApi { /** * Taken from https://github.com/rpm-software-management/rpm/blob/master/rpmio/rpmvercmp.c */ - protected _compare_string(v1: string, v2: string): number { + protected _compare_glob(v1: string, v2: string): number { if (v1 === v2) { return 0; } - const matchesv1 = v1.match(alphaNumPattern); - const matchesv2 = v2.match(alphaNumPattern); - const matches = Math.min(matchesv1?.length ?? 0, matchesv2?.length ?? 0); + const matchesv1 = + (v1.match(alphaNumPattern) as Array) ?? ([] as Array); + const matchesv2 = + (v2.match(alphaNumPattern) as Array) ?? ([] as Array); + const matches = Math.min(matchesv1.length, matchesv2.length); for (let i = 0; i < matches; i++) { let matchv1 = matchesv1?.[i]; @@ -134,7 +176,7 @@ class RpmVersioningApi extends GenericVersioningApi { return 1; } - if (matchv2?.[0] === '~') { + if (matchv2?.[0] !== '~') { return -1; } } @@ -149,60 +191,101 @@ class RpmVersioningApi extends GenericVersioningApi { matchv1 = matchv1?.replace(leadingZerosPattern, ''); matchv2 = matchv2?.replace(leadingZerosPattern, ''); - // longest string wins without further comparison - if ((matchv1?.length ?? 0) > (matchv2?.length ?? 0)) { - return 1; - } + //We clearly have a number here, so return which is greater + const num1 = Number(matchv1); + const num2 = Number(matchv2); - if ((matchv1?.length ?? 0) < (matchv2?.length ?? 0)) { - return -1; + const result = num1 - num2; + + if (result === 0) { + continue; } + + return Math.sign(result); } else if (is.numericString(matchv2?.[0])) { return -1; } - // string compare - const compared_value = this.__compare_character(matchv1, matchv2); + // We have two string globs, compare them + const compared_value = this._compare_string( + matchv1 as string, + matchv2 as string, + ); if (compared_value !== 0) { return compared_value; } } // segments were all the same, but separators were different - if ((matchesv1?.length ?? 0) === (matchesv2?.length ?? 0)) { + if (matchesv1.length === matchesv2.length) { return 0; } // If there is a tilde in a segment past the minimum number of segments, find it - if ((matchesv1?.length ?? 0) > matches && matchesv1?.[matches][0] === '~') { + if (matchesv1.length > matches && matchesv1?.[matches][0] === '~') { return -1; } - if ((matchesv2?.length ?? 0) > matches && matchesv2?.[matches][0] === '~') { + if (matchesv2.length > matches && matchesv2?.[matches][0] === '~') { return 1; } // whichever has the most segments wins - return (matchesv1?.length ?? 0) > (matchesv2?.length ?? 0) ? 1 : -1; + return matchesv1.length > matchesv2.length ? 1 : -1; } protected override _compare(version: string, other: string): number { const parsed1 = this._parse(version); const parsed2 = this._parse(other); + if (!(parsed1 && parsed2)) { return 1; } + + // Greater epoch wins if (parsed1.epoch !== parsed2.epoch) { return Math.sign(parsed1.epoch - parsed2.epoch); } - const upstreamVersionDifference = this._compare_string( + + // Greater upstream version wins + console.log('upstream...'); + const upstreamVersionDifference = this._compare_glob( parsed1.upstreamVersion, parsed2.upstreamVersion, ); + if (upstreamVersionDifference !== 0) { return upstreamVersionDifference; } - return this._compare_string(parsed1.rpmRelease, parsed2.rpmRelease); + + // Greater release version wins + const releaseVersionDifference = this._compare_glob( + parsed1.rpmRelease, + parsed2.rpmRelease, + ); + + if (releaseVersionDifference !== 0) { + return releaseVersionDifference; + } + + // No Prerelease wins + if (parsed1.rpmPreRelease === '' && parsed2.rpmPreRelease !== '') { + return 1; + } else if (parsed1.rpmPreRelease !== '' && parsed2.rpmPreRelease === '') { + return -1; + } + + const preReleaseDifference = this._compare_glob( + parsed1.rpmPreRelease, + parsed2.rpmPreRelease, + ); + + if (preReleaseDifference !== 0) { + return releaseVersionDifference; + } + + // Greater Snapshot wins + return this._compare_glob(parsed1.snapshot, parsed2.snapshot); } }