Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to Celerity are documented here. This project follows [Keep

## [Unreleased]

### Fixed

- `StringFnV1AHasher.Hash(null)` now throws `ArgumentNullException` (parameter name `"key"`) instead of `NullReferenceException`. Public APIs should signal a null argument as an explicit contract violation, not as an unchecked dereference. The Celerity dictionaries store the out-of-band `null` / `default(TKey)` key entry without ever calling the hasher, so the surface area of this change is limited to direct `StringFnV1AHasher` usage and to consumers that plug the hasher into custom `IHashProvider<string>` callers that do not handle the null-key slot themselves. The XML doc comment on `Hash` now declares the exception, and `StringFnV1AHasherTests.Hash_NullString_*` is updated to assert `ArgumentNullException` rather than pinning the previous wart. Closes #71.

## [1.2.0] - 2026-05-10

### Added
Expand Down
15 changes: 10 additions & 5 deletions src/Celerity.Tests/Hashing/StringFnV1AHasherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,18 @@ public void Hash_DifferentCaseStrings_ProduceDifferentValues()
}

/// <summary>
/// Demonstrates what happens if you pass null into the current implementation.
/// The current code will throw NullReferenceException (or potentially an NRE).
/// If you'd prefer an explicit ArgumentNullException, modify the struct.
/// Hashing a null reference must throw <see cref="ArgumentNullException"/>
/// (with the parameter name "key"), not the NullReferenceException the
/// implicit dereference would otherwise produce. Celerity dictionaries
/// route the out-of-band null/default-key entry around the hasher, so the
/// check only ever fires for direct hasher usage — but when it fires, the
/// thrown exception must reflect a contract violation, not an unchecked
/// dereference. Closes issue #71.
/// </summary>
[Fact]
public void Hash_NullString_ThrowsNullReferenceException()
public void Hash_NullString_ThrowsArgumentNullException()
{
Assert.Throws<NullReferenceException>(() => _hasher.Hash(null));
ArgumentNullException ex = Assert.Throws<ArgumentNullException>(() => _hasher.Hash(null!));
Assert.Equal("key", ex.ParamName);
}
}
14 changes: 13 additions & 1 deletion src/Celerity/Hashing/StringFnV1AHasher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,22 @@ namespace Celerity.Hashing;
/// </remarks>
public struct StringFnV1AHasher : IHashProvider<string>
{
/// <inheritdoc/>
/// <summary>
/// Computes the FNV-1a 32-bit hash of the specified string.
/// </summary>
/// <param name="key">The string to hash. Must not be <c>null</c>.</param>
/// <returns>The signed 32-bit FNV-1a hash of <paramref name="key"/>.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="key"/> is <c>null</c>. Celerity dictionaries store the
/// out-of-band <c>null</c>-key entry without calling the hasher, so this
/// check only surfaces when the hasher is used directly or plugged into a
/// consumer that does not handle <c>null</c> keys out-of-band.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Hash(string key)
{
ArgumentNullException.ThrowIfNull(key);

// The FNV-1a 32-bit parameters
const uint fnvPrime = 16777619;
const uint offsetBasis = 2166136261;
Expand Down