dry4rust/stable_hasher.rs
1// Copyright (c) 2026 Matjaz Domen Pecan
2// Copyright 2026 Umberto Gotti <umberto.gotti@umbertogotti.dev>
3// Licensed under the MIT License
4// SPDX-License-Identifier: MIT
5
6/// FNV-1a, 64-bit, specified here rather than borrowed.
7///
8/// Fingerprints are written into `.dry4rust-ignore.toml` and are expected to
9/// still match the next time the tool runs -- possibly under a different
10/// toolchain. `DefaultHasher` cannot promise that: the standard library says
11/// its algorithm is unspecified and not to be relied upon across releases. A
12/// suppression that silently stops matching is the worst kind of failure,
13/// because the duplicate simply reappears with no explanation.
14///
15/// So the algorithm lives here, in eight lines anyone can check against the
16/// published FNV specification, and every integer is written big-endian at a
17/// fixed width so that a 32-bit machine and a 64-bit one agree.
18pub struct StableHasher {
19 state: u64,
20}
21
22/// The FNV-1a 64-bit offset basis.
23const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
24
25/// The FNV-1a 64-bit prime.
26const PRIME: u64 = 0x0000_0100_0000_01b3;
27
28impl StableHasher {
29 #[must_use]
30 pub const fn new() -> Self {
31 Self {
32 state: OFFSET_BASIS,
33 }
34 }
35
36 /// Absorb raw bytes.
37 pub const fn write_bytes(&mut self, bytes: &[u8]) {
38 let mut index = 0;
39 while index < bytes.len() {
40 self.state ^= bytes[index] as u64;
41 self.state = self.state.wrapping_mul(PRIME);
42 index += 1;
43 }
44 }
45
46 /// Absorb a single byte.
47 pub const fn write_u8(&mut self, value: u8) {
48 self.write_bytes(&[value]);
49 }
50
51 /// Absorb an integer, big-endian and eight bytes wide regardless of the
52 /// platform's word size.
53 pub const fn write_u64(&mut self, value: u64) {
54 self.write_bytes(&value.to_be_bytes());
55 }
56
57 /// Absorb a string, length first.
58 ///
59 /// The length prefix is what keeps `("ab", "c")` from hashing the same as
60 /// `("a", "bc")`, which matters because variant names are written one
61 /// after another with nothing between them.
62 pub const fn write_str(&mut self, value: &str) {
63 self.write_u64(value.len() as u64);
64 self.write_bytes(value.as_bytes());
65 }
66
67 /// The hash of everything absorbed so far.
68 #[must_use]
69 pub const fn finish(&self) -> u64 {
70 self.state
71 }
72}
73
74impl Default for StableHasher {
75 fn default() -> Self {
76 Self::new()
77 }
78}