Skip to main content

use_stable_id/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Deterministic stable ID helpers.
5
6use core::fmt;
7use std::path::Path;
8
9pub mod prelude;
10
11const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
12const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
13
14#[must_use]
15pub fn stable_hash64(bytes: &[u8]) -> u64 {
16    bytes.iter().fold(FNV_OFFSET_BASIS, |hash, byte| {
17        (hash ^ u64::from(*byte)).wrapping_mul(FNV_PRIME)
18    })
19}
20
21#[must_use]
22pub fn stable_hex(bytes: &[u8]) -> String {
23    format!("{:016x}", stable_hash64(bytes))
24}
25
26#[must_use]
27pub fn stable_name_id(name: &str) -> StableId {
28    StableId::from_bytes(name.as_bytes())
29}
30
31#[must_use]
32pub fn stable_content_id(content: &[u8]) -> StableId {
33    StableId::from_bytes(content)
34}
35
36#[must_use]
37pub fn stable_reference(namespace: &str, name: &str) -> StableId {
38    StableId::from_bytes(format!("{namespace}:{name}").as_bytes())
39}
40
41#[must_use]
42pub fn stable_path_id(path: impl AsRef<Path>) -> StableId {
43    StableId::from_path(path)
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
47pub struct StableId(String);
48
49impl StableId {
50    #[must_use]
51    pub fn from_bytes(bytes: &[u8]) -> Self {
52        Self(stable_hex(bytes))
53    }
54
55    #[must_use]
56    pub fn from_name(name: &str) -> Self {
57        stable_name_id(name)
58    }
59
60    #[must_use]
61    pub fn from_path(path: impl AsRef<Path>) -> Self {
62        let normalized = path.as_ref().to_string_lossy().replace('\\', "/");
63        Self::from_bytes(normalized.as_bytes())
64    }
65
66    #[must_use]
67    pub fn as_str(&self) -> &str {
68        &self.0
69    }
70
71    #[must_use]
72    pub fn into_inner(self) -> String {
73        self.0
74    }
75}
76
77impl fmt::Display for StableId {
78    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79        formatter.write_str(self.as_str())
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::{StableId, stable_name_id, stable_path_id, stable_reference};
86
87    #[test]
88    fn name_ids_are_deterministic() {
89        assert_eq!(stable_name_id("rustuse"), stable_name_id("rustuse"));
90    }
91
92    #[test]
93    fn path_ids_normalize_separators() {
94        assert_eq!(
95            stable_path_id("docs\\intro.md"),
96            stable_path_id("docs/intro.md")
97        );
98    }
99
100    #[test]
101    fn namespace_changes_the_output() {
102        assert_ne!(
103            stable_reference("docs", "intro"),
104            StableId::from_name("intro")
105        );
106    }
107}