Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions src/launchpad/artifact_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
12 changes: 10 additions & 2 deletions tests/unit/artifacts/test_artifact_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading