unity_pack/
guid.rs

1//! Unity GUID generation and handling
2//!
3//! Unity uses 128-bit GUIDs represented as 32 lowercase hex characters (no dashes).
4
5use crate::{Error, Result};
6use std::fmt;
7
8/// A Unity-style GUID (32 lowercase hex characters, no dashes)
9#[derive(Clone, Copy, PartialEq, Eq, Hash)]
10pub struct UnityGuid([u8; 16]);
11
12impl UnityGuid {
13    /// Generate a new random GUID
14    pub fn new() -> Self {
15        let uuid = uuid::Uuid::new_v4();
16        Self(*uuid.as_bytes())
17    }
18
19    /// Create a GUID from a 32-character hex string
20    pub fn from_hex(s: &str) -> Result<Self> {
21        if s.len() != 32 {
22            return Err(Error::InvalidGuid(format!(
23                "GUID must be 32 hex characters, got {}",
24                s.len()
25            )));
26        }
27
28        let mut bytes = [0u8; 16];
29        for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
30            let hex_str = std::str::from_utf8(chunk)
31                .map_err(|_| Error::InvalidGuid("Invalid UTF-8".to_string()))?;
32            bytes[i] = u8::from_str_radix(hex_str, 16)
33                .map_err(|_| Error::InvalidGuid(format!("Invalid hex: {}", hex_str)))?;
34        }
35
36        Ok(Self(bytes))
37    }
38
39    /// Create a deterministic GUID from a path (useful for reproducible builds)
40    pub fn from_path(path: &str) -> Self {
41        use std::collections::hash_map::DefaultHasher;
42        use std::hash::{Hash, Hasher};
43
44        let mut hasher = DefaultHasher::new();
45        path.hash(&mut hasher);
46        let hash1 = hasher.finish();
47
48        // Hash again for second 64 bits
49        hash1.hash(&mut hasher);
50        let hash2 = hasher.finish();
51
52        let mut bytes = [0u8; 16];
53        bytes[0..8].copy_from_slice(&hash1.to_le_bytes());
54        bytes[8..16].copy_from_slice(&hash2.to_le_bytes());
55
56        Self(bytes)
57    }
58
59    /// Get the GUID as a 32-character lowercase hex string
60    pub fn to_hex(&self) -> String {
61        self.0.iter().map(|b| format!("{:02x}", b)).collect()
62    }
63
64    /// Get the raw bytes
65    pub fn as_bytes(&self) -> &[u8; 16] {
66        &self.0
67    }
68}
69
70impl Default for UnityGuid {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl fmt::Display for UnityGuid {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(f, "{}", self.to_hex())
79    }
80}
81
82impl fmt::Debug for UnityGuid {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "UnityGuid({})", self.to_hex())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_guid_generation() {
94        let guid1 = UnityGuid::new();
95        let guid2 = UnityGuid::new();
96        assert_ne!(guid1, guid2);
97        assert_eq!(guid1.to_hex().len(), 32);
98    }
99
100    #[test]
101    fn test_guid_from_hex() {
102        let hex = "0123456789abcdef0123456789abcdef";
103        let guid = UnityGuid::from_hex(hex).unwrap();
104        assert_eq!(guid.to_hex(), hex);
105    }
106
107    #[test]
108    fn test_deterministic_guid() {
109        let guid1 = UnityGuid::from_path("Assets/Terrain/heightmap.raw");
110        let guid2 = UnityGuid::from_path("Assets/Terrain/heightmap.raw");
111        let guid3 = UnityGuid::from_path("Assets/Terrain/other.raw");
112
113        assert_eq!(guid1, guid2);
114        assert_ne!(guid1, guid3);
115    }
116}