From 9715442a7005ce110b7102d79e6a889a6dec5dda Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 11 May 2026 14:17:15 +0200 Subject: [PATCH 1/4] Fix order-dependent hashing in OpenAPIObjectContainer --- Sources/OpenAPIRuntime/Base/OpenAPIValue.swift | 10 ++++++++-- .../OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift | 11 +++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift index 0ed8ff1b..6cda5c55 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift @@ -421,10 +421,16 @@ public struct OpenAPIObjectContainer: Codable, Hashable, Sendable { // swift-format-ignore: AllPublicDeclarationsHaveDocumentation public func hash(into hasher: inout Hasher) { + var commutativeHash = 0 for (key, itemValue) in value { - hasher.combine(key) - hasher.combine(OpenAPIValueContainer(validatedValue: itemValue)) + // Note that we use a copy of our own hasher here. This makes hash values + // dependent on its state, eliminating static collision patterns. + var elementHasher = hasher + elementHasher.combine(key) + elementHasher.combine(OpenAPIValueContainer(validatedValue: itemValue)) + commutativeHash ^= elementHasher.finalize() } + hasher.combine(commutativeHash) } } diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift index fb4b51e7..bafa772b 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -444,6 +444,17 @@ final class Test_OpenAPIValue: Test_Runtime { encodedData ) } + + func testHashing_objectOrderIndependence_success() throws { + let container1 = try OpenAPIObjectContainer( + unvalidatedValue: ["foo": 0, "bar": 1] + ) + let container2 = try OpenAPIObjectContainer( + unvalidatedValue: ["bar": 1, "foo": 0] + ) + XCTAssertEqual(container1, container2) + XCTAssertEqual(container1.hashValue, container2.hashValue) + } } struct MyAnyOf2: Codable, Hashable, From a05bfb78606266522ac6e050f28533761e033fab Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 12 Aug 2026 17:38:05 +0200 Subject: [PATCH 2/4] Sort keys before hashing --- Sources/OpenAPIRuntime/Base/OpenAPIValue.swift | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift index 6cda5c55..5cbc7455 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift @@ -281,9 +281,9 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable { case let value as [(any Sendable)?]: for item in value { hasher.combine(OpenAPIValueContainer(validatedValue: item)) } case let value as [String: (any Sendable)?]: - for (key, itemValue) in value { + for key in value.keys.sorted() { hasher.combine(key) - hasher.combine(OpenAPIValueContainer(validatedValue: itemValue)) + hasher.combine(OpenAPIValueContainer(validatedValue: value[key])) } default: break } @@ -421,16 +421,10 @@ public struct OpenAPIObjectContainer: Codable, Hashable, Sendable { // swift-format-ignore: AllPublicDeclarationsHaveDocumentation public func hash(into hasher: inout Hasher) { - var commutativeHash = 0 - for (key, itemValue) in value { - // Note that we use a copy of our own hasher here. This makes hash values - // dependent on its state, eliminating static collision patterns. - var elementHasher = hasher - elementHasher.combine(key) - elementHasher.combine(OpenAPIValueContainer(validatedValue: itemValue)) - commutativeHash ^= elementHasher.finalize() + for key in value.keys.sorted() { + hasher.combine(key) + hasher.combine(OpenAPIValueContainer(validatedValue: value[key])) } - hasher.combine(commutativeHash) } } From acbfd82c2e3e2a7760e066e35a8108c0c1dc753a Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 13 Aug 2026 10:39:17 +0200 Subject: [PATCH 3/4] Update tests --- .../OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift index bafa772b..94ad6228 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -455,6 +455,17 @@ final class Test_OpenAPIValue: Test_Runtime { XCTAssertEqual(container1, container2) XCTAssertEqual(container1.hashValue, container2.hashValue) } + + func testHashing_containerOrderIndependence_success() throws { + let container1 = try OpenAPIValueContainer( + unvalidatedValue: ["foo": 0, "bar": 1] + ) + let container2 = try OpenAPIValueContainer( + unvalidatedValue: ["bar": 1, "foo": 0] + ) + XCTAssertEqual(container1, container2) + XCTAssertEqual(container1.hashValue, container2.hashValue) + } } struct MyAnyOf2: Codable, Hashable, From 682420628c92661689ef7d0d93f9ac2c7b243756 Mon Sep 17 00:00:00 2001 From: Si Beaumont Date: Thu, 20 Aug 2026 16:26:02 +0100 Subject: [PATCH 4/4] Run swift-format --- .../Base/Test_OpenAPIValue.swift | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift index 94ad6228..3f578d58 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -446,23 +446,15 @@ final class Test_OpenAPIValue: Test_Runtime { } func testHashing_objectOrderIndependence_success() throws { - let container1 = try OpenAPIObjectContainer( - unvalidatedValue: ["foo": 0, "bar": 1] - ) - let container2 = try OpenAPIObjectContainer( - unvalidatedValue: ["bar": 1, "foo": 0] - ) + let container1 = try OpenAPIObjectContainer(unvalidatedValue: ["foo": 0, "bar": 1]) + let container2 = try OpenAPIObjectContainer(unvalidatedValue: ["bar": 1, "foo": 0]) XCTAssertEqual(container1, container2) XCTAssertEqual(container1.hashValue, container2.hashValue) } func testHashing_containerOrderIndependence_success() throws { - let container1 = try OpenAPIValueContainer( - unvalidatedValue: ["foo": 0, "bar": 1] - ) - let container2 = try OpenAPIValueContainer( - unvalidatedValue: ["bar": 1, "foo": 0] - ) + let container1 = try OpenAPIValueContainer(unvalidatedValue: ["foo": 0, "bar": 1]) + let container2 = try OpenAPIValueContainer(unvalidatedValue: ["bar": 1, "foo": 0]) XCTAssertEqual(container1, container2) XCTAssertEqual(container1.hashValue, container2.hashValue) }