diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index c54b726d..da73322d 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -675,11 +675,12 @@ def _parse_build_number(build: str) -> int | None: """Parse a raw build identifier (e.g. CFBundleVersion) into a sortable int. Plain integer builds (the common case, e.g. Android versionCode) pass through - unchanged. Apple's CFBundleVersion also allows up to three dot-separated - non-negative integers (e.g. "1.2.3"); those are packed into a single int by + unchanged. Apple's CFBundleVersion also allows dot-separated non-negative + integers (e.g. "1.2.3"); those are packed into a single int by zero-padding each component, which preserves correct ordering as long as no - component reaches 10**_BUILD_NUMBER_COMPONENT_WIDTH. Anything else - (non-numeric, malformed) returns None, same as today. + component reaches 10**_BUILD_NUMBER_COMPONENT_WIDTH. Per the CFBundleVersion + spec, more than three groups are accepted and groups beyond the third are + dropped. Anything else (non-numeric, malformed) returns None, same as today. Known limitation: plain-integer values are left small while packed dotted values are much larger, so if the same app_id/build_version ever has builds @@ -695,8 +696,12 @@ def _parse_build_number(build: str) -> int | None: return int(build) parts = build.split(".") - if 2 <= len(parts) <= 3 and all(p.isdigit() and len(p) <= _BUILD_NUMBER_COMPONENT_WIDTH for p in parts): - parts += ["0"] * (3 - len(parts)) - return sum(int(part) * 10 ** (_BUILD_NUMBER_COMPONENT_WIDTH * (2 - i)) for i, part in enumerate(parts)) - - return None + if not all(p.isdigit() for p in parts): + return None + # CFBundleVersion allows more than three groups; groups beyond the third + # are dropped. + parts = parts[:3] + if not (2 <= len(parts) and all(len(p) <= _BUILD_NUMBER_COMPONENT_WIDTH for p in parts)): + return None + parts += ["0"] * (3 - len(parts)) + return sum(int(part) * 10 ** (_BUILD_NUMBER_COMPONENT_WIDTH * (2 - i)) for i, part in enumerate(parts)) diff --git a/tests/unit/artifacts/test_artifact_processor.py b/tests/unit/artifacts/test_artifact_processor.py index 8e7aa42d..82cfdf8c 100644 --- a/tests/unit/artifacts/test_artifact_processor.py +++ b/tests/unit/artifacts/test_artifact_processor.py @@ -413,13 +413,21 @@ class TestParseBuildNumber: ("9999", 9999), ("0", 0), ("1", 1), - # Apple CFBundleVersion: up to three dot-separated non-negative integers + # Apple CFBundleVersion: two or three dot-separated non-negative integers ("1.2.3", 1_000_002_000_003), ("1.2", 1_000_002_000_000), + # CFBundleVersion allows more than three groups; groups beyond the + # third are dropped + ("1.2.3.4", 1_000_002_000_003), + ("1.2.3.4.5", 1_000_002_000_003), + ("1.2.3.0", 1_000_002_000_003), + # Dropped groups are not width-checked, but must still be numeric + ("1.2.3.1234567", 1_000_002_000_003), + ("1.2.3.beta", None), + ("1.2.3.", None), # Malformed or unsupported shapes fall back to None, same as before ("1.2.a", None), ("abc", None), - ("1.2.3.4", None), ("", None), # A component too wide for the padding width is refused rather than # silently corrupting the ordering of adjacent components