Skip to content

fix: compare large numeric prerelease ids without precision loss - #880

Open
spokodev wants to merge 1 commit into
npm:mainfrom
spokodev:fix/compare-large-numeric-prerelease
Open

fix: compare large numeric prerelease ids without precision loss#880
spokodev wants to merge 1 commit into
npm:mainfrom
spokodev:fix/compare-large-numeric-prerelease

Conversation

@spokodev

Copy link
Copy Markdown

Problem

Two valid versions whose prerelease numeric identifiers are >= 2^53 are reported equal and mis-sorted:

semver.compare('1.0.0-9007199254740992', '1.0.0-9007199254740993') // returns 0, should be -1
semver.eq('1.0.0-9007199254740992', '1.0.0-9007199254740993')      // returns true, should be false
semver.gt('1.0.0-9007199254740993', '1.0.0-9007199254740992')      // returns false, should be true

Both versions are valid SemVer (there is no magnitude cap on numeric prerelease identifiers in §9), so this violates SemVer 2.0.0 §11.4.1:

Identifiers consisting of only digits are compared numerically.

9007199254740992 and 9007199254740993 are distinct integers and must order accordingly.

Root cause

classes/semver.js deliberately keeps numeric prerelease ids >= MAX_SAFE_INTEGER as strings to avoid IEEE-754 precision loss in storage:

if (num >= 0 && num < MAX_SAFE_INTEGER) {
  return num
}
return id // kept as string for large ids

But internal/identifiers.js compareIdentifiers re-introduced that loss for any all-digit identifier:

if (anum && bnum) {
  a = +a
  b = +b
}

+'9007199254740992' and +'9007199254740993' both evaluate to the same double (9007199254740992), so the comparison sees them as equal.

Fix

Compare all-digit identifiers with BigInt instead of Number, preserving full integer precision:

if (anum && bnum) {
  a = BigInt(a)
  b = BigInt(b)
}

The already-numberified fast path (typeof a === 'number' && typeof b === 'number', for ids < MAX_SAFE_INTEGER) is untouched, and mixed alphanumeric ordering is unchanged.

Tests

Added a unit case in test/internal/identifiers.js for compareIdentifiers/rcompareIdentifiers with ids beyond 2^53, and a fixture entry in test/fixtures/comparisons.js exercising compare/gt/eq end to end. Both fail on the unpatched code and pass with the fix. Full npm test suite (including lint and 100% coverage) is green.