Skip to main content

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}