Skip to content

feat(version): add a CPAN/perl version comparator - #3614

Draft
wagoodman wants to merge 1 commit into
mainfrom
add-perl-capabilities/perl-version-comparator
Draft

feat(version): add a CPAN/perl version comparator#3614
wagoodman wants to merge 1 commit into
mainfrom
add-perl-capabilities/perl-version-comparator

Conversation

@wagoodman

@wagoodman wagoodman commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Adds CpanFormat and a comparator implementing perl's own version.pm semantics. ParseFormat accepts both cpan and perl.

Semver is wrong for the most common version shape on CPAN. Under perl's rules a decimal version is a fraction, so 1.2 > 1.10 and 1.23 > 1.2.3, and a packed decimal equals its dotted form, so 5.008001 and 5.8.1 are the same version. Comparing those as semver gets the ordering backwards.

Follows Perl_prescan_version, Perl_scan_version and Perl_vcmp: qv detection, three-digit fractional grouping, dotted-decimal padding, underscore alpha, and VERSION_MAX clamping. Alpha components take no part in ordering, and trailing zeros compare equal.

Correctness was established against real perl rather than by inspection: version->parse($s) and <=> inside perl:5.40-slim, over every distinct version string in the CPANSA database. That turned up three things reading the source alone did not, all of which are handled here:

  • the $LAX regex rejects a trailing . that prescan_version accepts
  • 1. really does parse as [1, 0]
  • overflow stops the scan, so 99999999999.1 is [2147483647] rather than clamping and continuing

Unparseable input degrades to a no-match rather than dropping the package.

Perl orders versions unlike anything else grype knows: `1.2 > 1.10`, `1.23 > 1.2.3`,
`1.23 == 1.230`, and `5.008001 == 5.8.1`. None of the 13 existing formats get any of
those right, so a dedicated comparator is the only way perl/CPAN matching lands correct
answers.

`CpanFormat` is a direct port of `Perl_prescan_version`, `Perl_scan_version` and
`Perl_vcmp` from perl's `vutil.c` (the same code the CPAN `version` distribution ships,
so it's what every CPAN toolchain bottoms out in). The whole surprise reduces to one
branch: a leading `v` or a second `.` makes a version dotted-decimal, everything else is
decimal and its fractional part is chopped into three-digit groups.

Two things a naive port gets wrong, both pinned by tests:

- the alpha/underscore flag never participates in ordering. `1.23_01 > 1.23` because its
  underscore digits fold into the numeric groups, not because of any pre-release rule
- trailing zeros are equality, not inequality, so `v1.2 == v1.2.0 == v1.2.0.0`

`ParseFormat` accepts both `cpan` and `perl`. Unparseable input errors the way other
formats do, which the search layer already treats as "skip this vulnerability" rather
than dropping the package.

The test vectors were measured against perl 5.34.1 and are shared with a Python port of
the same algorithm; the two implementations have to agree pair for pair.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
@wagoodman wagoodman changed the title add perl capabilities/perl version comparator feat(version): add a CPAN/perl version comparator Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant