Skip to content
Snippets Groups Projects
Unverified Commit 75cf8f1e authored by Malte Swart's avatar Malte Swart Committed by GitHub
Browse files

feat(versioning/deb): New module to compare deb package versions like dpkg (#20291)

parent 2ed30c07
No related merge requests found
......@@ -49,5 +49,5 @@ When the operating system package for `gcc` of `Alpine Linux 3.12` is updated, R
<!-- prettier-ignore -->
!!! tip
We recommend you try `loose` versioning for distribution packages first.
We recommend you try `loose` or `deb` versioning for distribution packages first.
This is because the version number usually doesn't match Renovate's default `semver` specification.
......@@ -2,6 +2,7 @@ import * as amazonMachineImage from './aws-machine-image';
import * as cargo from './cargo';
import * as composer from './composer';
import * as conan from './conan';
import * as deb from './deb';
import * as debian from './debian';
import * as docker from './docker';
import * as git from './git';
......@@ -40,6 +41,7 @@ api.set(amazonMachineImage.id, amazonMachineImage.api);
api.set(cargo.id, cargo.api);
api.set(composer.id, composer.api);
api.set(conan.id, conan.api);
api.set(deb.id, deb.api);
api.set(debian.id, debian.api);
api.set(docker.id, docker.api);
api.set(git.id, git.api);
......
import deb from '.';
describe('modules/versioning/deb/index', () => {
test.each`
version | expected
${'1.1'} | ${true}
${'1.3.RC2'} | ${true}
${'0:1.1-1'} | ${true}
${'a:1.1-1'} | ${false}
${'1.1:1.3-1'} | ${false}
${'1.1a:1.3-1'} | ${false}
${'1a:1.3-1'} | ${false}
${'-1:1.3-1'} | ${false}
${'1:1:1:2-1'} | ${true}
${'1:a:b:c:2-1'} | ${true}
${'1:3_3.2-1'} | ${false}
${'1:3!3.2-1'} | ${false}
${'1:3/3.2-1'} | ${false}
${'1.0-3_2'} | ${false}
${'1.0-3!3'} | ${false}
${'1.0-3/3'} | ${false}
${'1.0+ä1-1'} | ${false}
${'1,0-1'} | ${false}
${'2:1.1-1'} | ${true}
${'1.1.1-0debian1'} | ${true}
${'1.1.1+really1.1.2-0debian1'} | ${true}
${'2.31-13+deb11u5'} | ${true}
${'1:4.14+20190211-1ubuntu1'} | ${true}
${'2.7.7+dfsg-12'} | ${true}
${'9.5.0-1ubuntu1~22.04'} | ${true}
${'5:20.10.17~3-0~ubuntu-focal'} | ${true}
${'1:6.0.1r16-1.1build1'} | ${true}
${'2:102.12+LibO7.3.7-0ubuntu0.22.04.1'} | ${true}
${'1:2.20.1-1~bpo9+1'} | ${true}
${'v1.4'} | ${true}
${'3.5.0'} | ${true}
${'4.2.21.Final'} | ${true}
${'0.6.5.1'} | ${true}
${'20100527'} | ${true}
${'2.1.0-M3'} | ${true}
${'4.3.20.RELEASE'} | ${true}
${'1.1-groovy-2.4'} | ${true}
${'0.8a'} | ${true}
${'3.1.0.GA'} | ${true}
${'3.0.0-beta.3'} | ${true}
${'foo'} | ${true}
${'1.2.3.4.5.6.7'} | ${true}
${'0a1b2c3'} | ${true}
${'0a1b2c3d'} | ${true}
${'0a1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d'} | ${true}
${'0a1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d0'} | ${true}
${'0a1b2C3'} | ${true}
${'0z1b2c3'} | ${true}
${'0A1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d'} | ${true}
${'123098140293'} | ${true}
`('isValid("$version") === $expected', ({ version, expected }) => {
expect(deb.isValid(version)).toBe(expected);
});
test.each`
a | b | expected
${'2.4'} | ${'2.4'} | ${true}
${'2.4.0'} | ${'2.4.0'} | ${true}
${'2.4.0'} | ${'2.4'} | ${false}
${'2.4.1'} | ${'2.4'} | ${false}
${'2.4.2'} | ${'2.4.1'} | ${false}
${'0.8a'} | ${'0.8a'} | ${true}
${'9.5.0-1ubuntu1~22.04'} | ${'9.5.0-1ubuntu1'} | ${false}
${'9.5.0-1ubuntu1~22.04'} | ${'9.5.0-1ubuntu1~20.04'} | ${false}
${'9.5.0-1ubuntu1~22.04'} | ${'9.5.0-1ubuntu1~22.04'} | ${true}
${'2.31-13+deb11u5'} | ${'2.31-13+deb11u5'} | ${true}
${'2.31-13+deb11u5'} | ${'2.31-13+deb11u4'} | ${false}
${'1.4-'} | ${'1.4'} | ${false}
${'v1.4'} | ${'1.4'} | ${false}
${'0:1.4'} | ${'1.4'} | ${true}
${'1:1.4'} | ${'1.4'} | ${false}
${'1.4-1'} | ${'1.4-2'} | ${false}
${'0:1.4'} | ${'a:1.4'} | ${false}
${'a:1.4'} | ${'0:1.4'} | ${false}
`('equals("$a", "$b") === $expected', ({ a, b, expected }) => {
expect(deb.equals(a, b)).toBe(expected);
});
test.each`
a | b | expected
${'2.4.0'} | ${'2.4'} | ${true}
${'2.4.2'} | ${'2.4.1'} | ${true}
${'2.4.beta'} | ${'2.4.alpha'} | ${true}
${'1.9'} | ${'2'} | ${false}
${'1.9'} | ${'1.9.1'} | ${false}
${'2.4'} | ${'2.4.beta'} | ${false}
${'2.4.0'} | ${'2.4.beta'} | ${false}
${'2.4.beta'} | ${'2.4'} | ${true}
${'2.4.beta'} | ${'2.4.0'} | ${true}
${'2.4~'} | ${'2.4~~'} | ${true}
${'2.4'} | ${'2.4~'} | ${true}
${'2.4a'} | ${'2.4'} | ${true}
${'2.31-13+deb11u5'} | ${'2.31-9'} | ${true}
${'2.31-13+deb11u5'} | ${'2.31-13+deb10u5'} | ${true}
${'2.31-13+deb11u5'} | ${'2.31-13+deb11u4'} | ${true}
${'1.9'} | ${'1:1.7'} | ${false}
${'1.9'} | ${'1.12'} | ${false}
${'1.12'} | ${'1.9'} | ${true}
${'1:1.9'} | ${'1:1.7'} | ${true}
${'2.4.0.beta1'} | ${'2.4.0.Beta1'} | ${true}
${'1:1.0'} | ${'1:1.0~'} | ${true}
${'1:1.0Z0-0'} | ${'1:1.0'} | ${true}
${'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'} | ${true}
${'1:1.0-0-0'} | ${'1:1.0+0-0'} | ${true}
${'1:1.0.0-0'} | ${'1:1.0-0-0'} | ${true}
${'1:1.0:0-0'} | ${'1:1.0.0-0'} | ${true}
${'a:1.4'} | ${'0:1.4'} | ${true}
${'0:1.4'} | ${'a:1.4'} | ${true}
${'a:1.4'} | ${'a:1.4'} | ${true}
${'a1'} | ${'a~'} | ${true}
${'a0'} | ${'a~'} | ${true}
${'aa'} | ${'a1'} | ${true}
${'ab'} | ${'a0'} | ${true}
${'10'} | ${'1.'} | ${true}
${'10'} | ${'1a'} | ${true}
`('isGreaterThan("$a", "$b") === $expected', ({ a, b, expected }) => {
expect(deb.isGreaterThan(a, b)).toBe(expected);
});
test.each`
version | expected
${'1.2.0'} | ${true}
${'^1.2.0'} | ${false}
`('isSingleVersion("$version") === $expected', ({ version, expected }) => {
expect(deb.isSingleVersion(version)).toBe(expected);
});
test.each`
version | expected
${'v1.3.0'} | ${1}
${'2-0-1'} | ${2}
${'2.31-13+deb11u5'} | ${2}
${'1:2.3.1'} | ${2}
${'foo'} | ${null}
${'8'} | ${8}
${'1.0'} | ${1}
`('getMajor("$version") === $expected', ({ version, expected }) => {
expect(deb.getMajor(version)).toBe(expected);
});
test.each`
version | expected
${'v1.3.0'} | ${3}
${'2-0-1'} | ${0}
${'2.31-13+deb11u5'} | ${31}
${'1:2.3.1'} | ${3}
${'foo'} | ${null}
${'8'} | ${null}
${'1.0'} | ${0}
`('getMinor("$version") === $expected', ({ version, expected }) => {
expect(deb.getMinor(version)).toBe(expected);
});
test.each`
version | expected
${'v1.3.0'} | ${0}
${'2-0-1'} | ${1}
${'2.31-13+deb11u5'} | ${13}
${'1:2.3.1'} | ${1}
${'foo'} | ${null}
${'8'} | ${null}
${'1.0'} | ${null}
`('getPatch("$version") === $expected', ({ version, expected }) => {
expect(deb.getPatch(version)).toBe(expected);
});
});
import { regEx } from '../../../util/regex';
import { GenericVersion, GenericVersioningApi } from '../generic';
import type { VersioningApi } from '../types';
export const id = 'deb';
export const displayName = 'Deb version';
export const urls = [
'https://www.debian.org/doc/debian-policy/ch-controlfields.html#version',
'https://manpages.debian.org/unstable/dpkg-dev/deb-version.7.en.html',
];
export const supportsRanges = false;
const epochPattern = regEx(/^\d+$/);
const upstreamVersionPattern = regEx(/^[-+.:~A-Za-z\d]+$/);
const debianRevisionPattern = regEx(/^[+.~A-Za-z\d]*$/);
const numericPattern = regEx(/\d+/g);
const characterOrder =
'~ ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-.:';
const numericChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
export interface DebVersion extends GenericVersion {
/**
* epoch, defaults to 0 if not present, are used to leave version mistakes and previous
* versioning schemes behind.
*/
epoch: number;
/**
* upstreamVersion is the main version part: it defines the version of origin software
* that was packaged.
*/
upstreamVersion: string;
/**
* debianRevision is used to distinguish between different versions of packaging for the
* same upstream version.
*/
debianRevision: string;
}
class DebVersioningApi extends GenericVersioningApi {
protected _parse(version: string): DebVersion | null {
/* Splitting version into "[epoch:]upstream-version[-debian-revision]"
All found numbers are exported as release info */
// split of first element by `:` as epoch:
const epochSplit = version.split(':');
const epochStr = epochSplit.length > 1 ? epochSplit.shift()! : '0';
const remainingVersion = epochSplit.join(':');
// split of last element by `-`
if (remainingVersion.endsWith('-')) {
// Forbid debian revision (it would result in `2.4.0-` == `2.4.0`)
return null;
}
const debianSplit = remainingVersion.split('-');
const debianRevision = debianSplit.length > 1 ? debianSplit.pop()! : '';
const upstreamVersion = debianSplit.join('-');
if (
!epochPattern.test(epochStr) ||
!upstreamVersionPattern.test(upstreamVersion) ||
!debianRevisionPattern.test(debianRevision)
) {
return null;
}
const release = [...remainingVersion.matchAll(numericPattern)].map((m) =>
parseInt(m[0], 10)
);
return {
epoch: parseInt(epochStr, 10),
upstreamVersion,
debianRevision,
release,
suffix: debianRevision,
};
}
protected _compare_string(a: string, b: string): number {
/* Special string sorting based on official specification:
* https://manpages.debian.org/unstable/dpkg-dev/deb-version.7.en.html#Sorting_algorithm
* The string is compared by continuous blocks of a) non-digit and b) digit characters.
* Non-digit blocks are compared lexicographically with a custom character order.
* Digit blocks are compared numerically.
* We are alternating between both modes until a difference is found.
*/
let charPos = 0;
while (charPos < a.length || charPos < b.length) {
const aChar = a.charAt(charPos);
const bChar = b.charAt(charPos);
if (numericChars.includes(aChar) && numericChars.includes(bChar)) {
// numeric comparison of the whole block
let aNumericEnd = charPos + 1;
while (numericChars.includes(a.charAt(aNumericEnd))) {
aNumericEnd += 1;
}
let bNumericEnd = charPos + 1;
while (numericChars.includes(b.charAt(bNumericEnd))) {
bNumericEnd += 1;
}
const numericCmp = a
.substring(charPos, aNumericEnd)
.localeCompare(b.substring(charPos, bNumericEnd), undefined, {
numeric: true,
});
if (numericCmp !== 0) {
return numericCmp;
}
charPos = aNumericEnd; // the same as bNumericEnd as both are the same
continue;
}
if (aChar !== bChar) {
// Lexicographical comparison
// numeric character is treated like end of string (they are part of a new block)
const aPriority = characterOrder.indexOf(
numericChars.includes(aChar) || aChar === '' ? ' ' : aChar
);
const bPriority = characterOrder.indexOf(
numericChars.includes(bChar) || bChar === '' ? ' ' : bChar
);
return Math.sign(aPriority - bPriority);
}
charPos += 1;
}
return 0;
}
protected override _compare(version: string, other: string): number {
const parsed1 = this._parse(version);
const parsed2 = this._parse(other);
if (!(parsed1 && parsed2)) {
return 1;
}
if (parsed1.epoch !== parsed2.epoch) {
return Math.sign(parsed1.epoch - parsed2.epoch);
}
const upstreamVersionDifference = this._compare_string(
parsed1.upstreamVersion,
parsed2.upstreamVersion
);
if (upstreamVersionDifference !== 0) {
return upstreamVersionDifference;
}
return this._compare_string(parsed1.debianRevision, parsed2.debianRevision);
}
}
export const api: VersioningApi = new DebVersioningApi();
export default api;
Deb versioning compares versions like package managers on Debian-based Linux distributions compare packages (`dpkg`, `apt`).
Deb versioning supports complicated version numbers, including alphabetical characters almost everywhere.
Similar to our "loose" versioning, deb versioning is a "best effort" attempt to convert deb versions to SemVer fields like `major`, `minor`, `patch`.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment