From 4f6df44c2a8529dadfa82586dec0840e61d10b34 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 16 Jun 2026 11:06:06 -0300 Subject: [PATCH] fix(Platform): make arm64 nil-variant and v8 hash identically (#764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `Platform.==` treats `arm64` with `nil` variant as equal to `arm64/v8`, but `hash(into:)` used `description` which serializes them differently (`linux/arm64` vs `linux/arm64/v8`). This violates the `Hashable` contract — equal values must produce the same hash. ### Root cause ```swift // == returns true for these two let a = Platform(arch: "arm64", os: "linux", variant: nil) let b = Platform(arch: "arm64", os: "linux", variant: "v8") a == b // true ✓ // but hash was different — broken a.hashValue == b.hashValue // false ✗ (before this fix) ``` This mismatch caused `Set` and `Dictionary` lookups to silently miss entries when one platform was decoded from JSON (no `variant` field in the manifest) and another was created via `Platform(from:)` or `Platform.current` (which both set `variant = "v8"`). ### Practical consequence In `apple/container`, this manifests as inconsistent platform-string normalization across stages of a single `container build` — some stages log `linux/arm64`, others `linux/arm64/v8` — which can cause `COPY --from=` to fail to resolve the source stage under concurrent builds. See apple/container#1542. ### Fix `hash(into:)` now normalizes `arm64` with `nil` variant to `"v8"` before hashing, matching the existing `==` behavior. --- Sources/ContainerizationOCI/Platform.swift | 9 ++++++++- .../OCIPlatformTests.swift | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Sources/ContainerizationOCI/Platform.swift b/Sources/ContainerizationOCI/Platform.swift index 3a54fbc9..7455e011 100644 --- a/Sources/ContainerizationOCI/Platform.swift +++ b/Sources/ContainerizationOCI/Platform.swift @@ -272,7 +272,14 @@ extension Platform: Hashable { } public func hash(into hasher: inout Swift.Hasher) { - hasher.combine(description) + hasher.combine(os) + hasher.combine(architecture) + // arm64 with no variant is equivalent to arm64/v8 per the == implementation + if architecture == "arm64" { + hasher.combine(variant ?? "v8") + } else { + hasher.combine(variant) + } } } diff --git a/Tests/ContainerizationOCITests/OCIPlatformTests.swift b/Tests/ContainerizationOCITests/OCIPlatformTests.swift index 8977a526..7ffebedc 100644 --- a/Tests/ContainerizationOCITests/OCIPlatformTests.swift +++ b/Tests/ContainerizationOCITests/OCIPlatformTests.swift @@ -66,4 +66,19 @@ struct OCIPlatformTests { let rhs = Platform(arch: "arm64", os: "linux", variant: nil) #expect(lhs == rhs, "Both nil variants => variantEqual is true => overall equal") } + + @Test func arm64_nilAndV8_sameHashValue() { + let withoutVariant = Platform(arch: "arm64", os: "linux", variant: nil) + let withV8 = Platform(arch: "arm64", os: "linux", variant: "v8") + // Equal platforms must produce the same hash — violating this breaks Set/Dictionary lookups + #expect(withoutVariant.hashValue == withV8.hashValue, "arm64 nil variant and v8 must hash identically") + } + + @Test func arm64_nilAndV8_setLookup() { + let withoutVariant = Platform(arch: "arm64", os: "linux", variant: nil) + let withV8 = Platform(arch: "arm64", os: "linux", variant: "v8") + var set = Set() + set.insert(withoutVariant) + #expect(set.contains(withV8), "arm64/v8 must be found in a Set that contains arm64 with nil variant") + } }