diff --git a/Sources/FowlerNollVo/FNV1Hasher.swift b/Sources/FowlerNollVo/FNV1Hasher.swift index 28b56c6..b27a460 100644 --- a/Sources/FowlerNollVo/FNV1Hasher.swift +++ b/Sources/FowlerNollVo/FNV1Hasher.swift @@ -20,7 +20,17 @@ public struct FNV1Hasher: FNVHasher { /// Feeds the provided data to the hasher. public mutating func combine(_ data: Data) where Data : Sequence, Data.Element == UInt8 { - for byte in data { + // Get an iterator for the data sequence + var iterator = data.makeIterator() + // Get the first byte of the sequence + guard let firstByte = iterator.next() else { + // If the sequence is empty, multiply by fnv_prime + digest = digest &* .fnvPrime; return + } + // Combine the first byte manually + digest = (digest &* .fnvPrime) ^ firstByte + // Iterate over the rest of the bytes in the sequence + while let byte = iterator.next() { digest = (digest &* .fnvPrime) ^ byte } } diff --git a/Sources/FowlerNollVo/FNV1aHasher.swift b/Sources/FowlerNollVo/FNV1aHasher.swift index 60796e9..1469e96 100644 --- a/Sources/FowlerNollVo/FNV1aHasher.swift +++ b/Sources/FowlerNollVo/FNV1aHasher.swift @@ -20,7 +20,12 @@ public struct FNV1aHasher: FNVHasher { /// Feeds the provided data to the hasher. public mutating func combine(_ data: Data) where Data : Sequence, Data.Element == UInt8 { - for byte in data { + var iterator = data.makeIterator() + guard let firstByte = iterator.next() else { + digest = digest &* .fnvPrime; return + } + digest = (digest ^ firstByte) &* .fnvPrime + while let byte = iterator.next() { digest = (digest ^ byte) &* .fnvPrime } } diff --git a/Sources/FowlerNollVo/FNVHashable.swift b/Sources/FowlerNollVo/FNVHashable.swift index 539e24b..e7d1023 100644 --- a/Sources/FowlerNollVo/FNVHashable.swift +++ b/Sources/FowlerNollVo/FNVHashable.swift @@ -157,7 +157,8 @@ extension Optional: FNVHashable where Wrapped: FNVHashable { case .some(let value): hasher.combine(value) case .none: - return + // Pass an empty array to mutate the digest without data + hasher.combine([]) } } } diff --git a/Tests/FowlerNollVoTests/FNVHashableTests.swift b/Tests/FowlerNollVoTests/FNVHashableTests.swift new file mode 100644 index 0000000..337f4ab --- /dev/null +++ b/Tests/FowlerNollVoTests/FNVHashableTests.swift @@ -0,0 +1,116 @@ +// +// FNVHashableTests.swift +// FNVHashableTests +// +// Created by Christopher Richez on January 15 2022 +// + +import FowlerNollVo +import XCTest + +class FNVHashableTests: XCTestCase { + /// Asserts `nil` values affect the hash value of their parent optional sequence + /// when using the `FNV-1a` hash function. + /// + /// See issue #21 for details. + func testOptionalSequenceHash1a() { + // Hash a sequence of four elements including one nil + var hasher1 = FNV1aHasher() + let sequence1 = [nil, 1, 2, 3] + sequence1.hash(into: &hasher1) + + // Hash a sequence of three elements + var hasher2 = FNV1aHasher() + let sequence2 = [1, 2, 3] + sequence2.hash(into: &hasher2) + + // Assert the hash values are not equal + XCTAssertNotEqual(hasher1.digest, hasher2.digest, "nil element didn't affect hash") + } + + /// Asserts `nil` values affect the hash value of their parent optional sequence + /// when using the `FNV-1` hash function. + /// + /// See issue #21 for details. + func testOptionalSequenceHash1() { + // Hash a sequence of four elements including one nil + var hasher1 = FNV1Hasher() + let sequence1 = [nil, 1, 2, 3] + sequence1.hash(into: &hasher1) + + // Hash a sequence of three elements + var hasher2 = FNV1Hasher() + let sequence2 = [1, 2, 3] + sequence2.hash(into: &hasher2) + + // Assert the hash values are not equal + XCTAssertNotEqual(hasher1.digest, hasher2.digest, "nil element didn't affect hash") + } + + /// Asserts two sequences that contain different number of nil elements have different + /// hash values using the FNV-1a hash function. + func testNilSequenceHash1a() { + // Hash a sequence of four nils + var hasher1 = FNV1aHasher() + let sequence1: [Float?] = [nil, nil, nil, nil] + sequence1.hash(into: &hasher1) + + // Hash a sequence of three nils + var hasher2 = FNV1aHasher() + let sequence2: [Float?] = [nil, nil, nil] + sequence2.hash(into: &hasher2) + + // Assert the hash values are not equal + XCTAssertNotEqual(hasher1.digest, hasher2.digest, "nil sequences with different counts are equal") + } + /// Asserts two sequences that contain different number of nil elements have different + /// hash values using the FNV-1 hash function. + func testNilSequenceHash1() { + // Hash a sequence of four nils + var hasher1 = FNV1Hasher() + let sequence1: [Float?] = [nil, nil, nil, nil] + sequence1.hash(into: &hasher1) + + // Hash a sequence of three nils + var hasher2 = FNV1Hasher() + let sequence2: [Float?] = [nil, nil, nil] + sequence2.hash(into: &hasher2) + + // Assert the hash values are not equal + XCTAssertNotEqual(hasher1.digest, hasher2.digest, "nil sequences with different counts are equal") + } + + /// Asserts an empty sequence and a sequence with a single nil element have different hash values + /// using the FNV-1a hash function. + func testEmptySequenceDifferentFromNil1a() throws { + // Hash an empty sequence + var hasher1 = FNV1aHasher() + let sequence1: [String?] = [] + sequence1.hash(into: &hasher1) + + // Hash a sequence with a single nil element + var hasher2 = FNV1aHasher() + let sequence2: [String?] = [nil] + sequence2.hash(into: &hasher2) + + // Assert the hash values for these sequences are different + XCTAssertNotEqual(hasher1.digest, hasher2.digest, "sequences unexpectedly match") + } + + /// Asserts an empty sequence and a sequence with a single nil element have different hash values + /// using the FNV-1 hash function. + func testEmptySequenceDifferentFromNil1() throws { + // Hash an empty sequence + var hasher1 = FNV1Hasher() + let sequence1: [String?] = [] + sequence1.hash(into: &hasher1) + + // Hash a sequence with a single nil element + var hasher2 = FNV1Hasher() + let sequence2: [String?] = [nil] + sequence2.hash(into: &hasher2) + + // Assert the hash values for these sequences are different + XCTAssertNotEqual(hasher1.digest, hasher2.digest, "sequences unexpectedly match") + } +} \ No newline at end of file