Skip to content

Commit 517595d

Browse files
committed
Compare decoded integers numerically across signed and unsigned
1 parent c8c17e3 commit 517595d

1 file changed

Lines changed: 48 additions & 1 deletion

File tree

Sources/BluetoothExplorerPluginEngine/DecodedField.swift

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public struct DecodedField: Equatable, Hashable, Sendable, Identifiable {
3434
}
3535

3636
/// A typed decoded value. Mirrors the value types carried across the WASM ABI (CBOR).
37-
public enum DecodedValue: Equatable, Hashable, Sendable {
37+
public enum DecodedValue: Sendable {
3838
case string(String)
3939
case int(Int64)
4040
case uint(UInt64)
@@ -45,6 +45,53 @@ public enum DecodedValue: Equatable, Hashable, Sendable {
4545
case uuid(UUID)
4646
}
4747

48+
extension DecodedValue: Equatable, Hashable {
49+
50+
/// Integers compare numerically across `int` and `uint`.
51+
///
52+
/// CBOR has no notion of signedness: major type 0 encodes *any* non-negative integer, so a
53+
/// plugin field that is semantically signed arrives as `uint` whenever its value happens to be
54+
/// non-negative. The cases therefore mean "negative" and "non-negative", not "signed" and
55+
/// "unsigned", and two producers of the same number — a native parser and a WASM plugin — must
56+
/// compare equal. The UI renders both identically.
57+
public static func == (lhs: DecodedValue, rhs: DecodedValue) -> Bool {
58+
switch (lhs, rhs) {
59+
case let (.string(a), .string(b)): return a == b
60+
case let (.int(a), .int(b)): return a == b
61+
case let (.uint(a), .uint(b)): return a == b
62+
case let (.int(a), .uint(b)): return a >= 0 && UInt64(a) == b
63+
case let (.uint(a), .int(b)): return b >= 0 && a == UInt64(b)
64+
case let (.double(a), .double(b)): return a == b
65+
case let (.bool(a), .bool(b)): return a == b
66+
case let (.bytes(a), .bytes(b)): return a == b
67+
case let (.uuid(a), .uuid(b)): return a == b
68+
default: return false
69+
}
70+
}
71+
72+
public func hash(into hasher: inout Hasher) {
73+
switch self {
74+
case let .string(value):
75+
hasher.combine(0); hasher.combine(value)
76+
// Both integer cases share a discriminator, and non-negative values hash through the same
77+
// UInt64 path, so numerically equal values hash equally as Hashable requires.
78+
case let .int(value):
79+
hasher.combine(1)
80+
if value >= 0 { hasher.combine(UInt64(value)) } else { hasher.combine(value) }
81+
case let .uint(value):
82+
hasher.combine(1); hasher.combine(value)
83+
case let .double(value):
84+
hasher.combine(2); hasher.combine(value)
85+
case let .bool(value):
86+
hasher.combine(3); hasher.combine(value)
87+
case let .bytes(value):
88+
hasher.combine(4); hasher.combine(value)
89+
case let .uuid(value):
90+
hasher.combine(5); hasher.combine(value)
91+
}
92+
}
93+
}
94+
4895
/// The result of a plugin decoding one advertisement field or attribute value.
4996
public struct DecodedResult: Equatable, Hashable, Sendable {
5097

0 commit comments

Comments
 (0)