Skip to content

Commit

Permalink
Merge pull request #22 from crichez/optional-hashing-fix
Browse files Browse the repository at this point in the history
Fix `Optional` hashing
  • Loading branch information
crichez authored Jan 18, 2022
2 parents 724812d + ba339f4 commit 901f35f
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 3 deletions.
12 changes: 11 additions & 1 deletion Sources/FowlerNollVo/FNV1Hasher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,17 @@ public struct FNV1Hasher<Digest: FNVDigest>: FNVHasher {

/// Feeds the provided data to the hasher.
public mutating func combine<Data>(_ 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
}
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/FowlerNollVo/FNV1aHasher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ public struct FNV1aHasher<Digest: FNVDigest>: FNVHasher {

/// Feeds the provided data to the hasher.
public mutating func combine<Data>(_ 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
}
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/FowlerNollVo/FNVHashable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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([])
}
}
}
Expand Down
116 changes: 116 additions & 0 deletions Tests/FowlerNollVoTests/FNVHashableTests.swift
Original file line number Diff line number Diff line change
@@ -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<UInt64>()
let sequence1 = [nil, 1, 2, 3]
sequence1.hash(into: &hasher1)

// Hash a sequence of three elements
var hasher2 = FNV1aHasher<UInt64>()
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<UInt64>()
let sequence1 = [nil, 1, 2, 3]
sequence1.hash(into: &hasher1)

// Hash a sequence of three elements
var hasher2 = FNV1Hasher<UInt64>()
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<UInt64>()
let sequence1: [Float?] = [nil, nil, nil, nil]
sequence1.hash(into: &hasher1)

// Hash a sequence of three nils
var hasher2 = FNV1aHasher<UInt64>()
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<UInt64>()
let sequence1: [Float?] = [nil, nil, nil, nil]
sequence1.hash(into: &hasher1)

// Hash a sequence of three nils
var hasher2 = FNV1Hasher<UInt64>()
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<UInt64>()
let sequence1: [String?] = []
sequence1.hash(into: &hasher1)

// Hash a sequence with a single nil element
var hasher2 = FNV1aHasher<UInt64>()
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<UInt64>()
let sequence1: [String?] = []
sequence1.hash(into: &hasher1)

// Hash a sequence with a single nil element
var hasher2 = FNV1Hasher<UInt64>()
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")
}
}

0 comments on commit 901f35f

Please sign in to comment.