Skip to main content

extendable_assets/asset/
id.rs

1use rand::Rng;
2use rand::RngExt;
3use rand::distr::{Distribution, StandardUniform};
4
5use xxhash_rust::const_xxh3::const_custom_default_secret;
6use xxhash_rust::xxh3::xxh3_64_with_secret;
7
8use std::hash::Hash;
9
10#[derive(Default, Clone, Copy)]
11#[derive(PartialEq, PartialOrd, Ord, Eq)]
12#[derive(serde::Deserialize, serde::Serialize)]
13#[serde(transparent)]
14#[repr(transparent)]
15pub struct AssetId(u64);
16
17impl Hash for AssetId {
18    #[inline]
19    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
20        state.write_u64(self.0)
21    }
22}
23
24/// Generates a deterministic asset ID from an asset path.
25///
26/// Encodes the asset path using percent-encoding for URI safety.
27/// Applies RFC 3986 percent-encoding to asset paths, preserving forward slashes
28/// and unreserved characters while encoding everything else. This ensures asset
29/// paths are safe for use in URIs and filesystem operations.
30///
31/// Uses XXH3 hash with a custom secret to generate consistent IDs
32/// for the same asset path across application restarts.
33impl From<&str> for AssetId {
34    fn from(value: &str) -> Self {
35        let value: String = value
36            .chars()
37            .map(|c| {
38                match c {
39                    // Unreserved characters (RFC 3986)
40                    'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => c.to_string(),
41                    // Preserve forward slashes
42                    '/' => c.to_string(),
43                    // Everything else gets percent-encoded
44                    _ => {
45                        let mut buf = [0; 4];
46                        let encoded = c.encode_utf8(&mut buf);
47                        let mut strs = encoded.bytes().map(|b| format!("%{:02X}", b));
48                        if c.is_ascii() {
49                            strs.next_back().unwrap()
50                        } else {
51                            strs.collect::<String>()
52                        }
53                    }
54                }
55            })
56            .collect();
57        const SECRET: [u8; 192] = const_custom_default_secret(1111);
58        xxh3_64_with_secret(value.as_bytes(), &SECRET).into()
59    }
60}
61
62impl From<AssetId> for u64 {
63    #[inline]
64    fn from(value: AssetId) -> Self {
65        value.0
66    }
67}
68impl From<u64> for AssetId {
69    #[inline]
70    fn from(value: u64) -> Self {
71        Self(value)
72    }
73}
74
75impl std::fmt::Debug for AssetId {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        struct HexWrapper(u64);
78        impl std::fmt::Debug for HexWrapper {
79            #[inline]
80            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81                f.write_fmt(format_args!("0x{:#x}", self.0))
82            }
83        }
84        f.debug_tuple("AssetId").field(&HexWrapper(self.0)).finish()
85    }
86}
87
88impl Distribution<AssetId> for StandardUniform {
89    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> AssetId {
90        AssetId(rng.random())
91    }
92}
93
94#[cfg(test)]
95mod test {
96    use super::*;
97    use serde_test::{Token, assert_tokens};
98
99    #[test]
100    fn asset_id_serde() {
101        let id: AssetId = rand::rng().random();
102        let id_value: u64 = id.into();
103        assert_tokens(&id, &[Token::U64(id_value)]);
104    }
105}