Skip to main content

boox_note_parser/
id.rs

1use std::{
2    collections::{HashMap, HashSet},
3    sync::{Mutex, OnceLock},
4};
5
6trait CheckUuid {
7    fn id(&self) -> &uuid::Uuid;
8    fn to_string(&self) -> String;
9}
10static KNOWN_UUIDS: OnceLock<Mutex<HashMap<uuid::Uuid, Vec<Box<dyn CheckUuid + Send + Sync>>>>> =
11    OnceLock::new();
12
13fn normalized_uuid_input(s: &str) -> &str {
14    s.trim_matches(|c: char| c.is_ascii_whitespace() || c == '\0')
15}
16
17fn check_uuid(id: impl CheckUuid + Send + Sync + 'static) {
18    // Lock the mutex and clear the HashMap
19    let mut map = KNOWN_UUIDS
20        .get_or_init(|| Mutex::new(HashMap::new()))
21        .lock()
22        .unwrap();
23    // Check if the id already exists in the HashMap
24    if let Some(existing) = map.get(id.id()) {
25        // log::warn!("Duplicate UUID found: {}. Already registered as:", id.to_string());
26        let mut types = HashSet::new();
27        for existing_id in existing {
28            types.insert(existing_id.to_string());
29        }
30
31        if types.len() > 1 {
32            log::warn!(
33                "UUIDs with different types found: {}. Already registered as: {}",
34                id.to_string(),
35                types.into_iter().collect::<Vec<_>>().join(", ")
36            );
37        }
38    }
39
40    // Insert the id into the HashMap
41    map.entry(id.id().clone()).or_default().push(Box::new(id));
42}
43
44macro_rules! implement_uuid {
45    ($name:ident) => {
46        #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
47        pub struct $name(uuid::Uuid);
48
49        impl $name {
50            pub fn new(id: uuid::Uuid) -> Self {
51                let id = Self(id);
52                check_uuid(id.clone());
53                id
54            }
55
56            pub fn from_str(s: &str) -> crate::error::Result<Self> {
57                let normalized = normalized_uuid_input(s);
58                let parsed = uuid::Uuid::parse_str(normalized).map_err(|e| {
59                    log::error!(
60                        "Failed to parse UUID from string '{}'(normalized '{}'): {}",
61                        s.escape_debug(),
62                        normalized.escape_debug(),
63                        e
64                    );
65                    e
66                })?;
67                let id = Self(parsed);
68                check_uuid(id.clone());
69                Ok(id)
70            }
71
72            pub fn from_byte_str(s: &[u8]) -> crate::error::Result<Self> {
73                let s =
74                    std::str::from_utf8(s).map_err(|e| crate::error::Error::UuidInvalidUtf8(e))?;
75                Self::from_str(s)
76            }
77
78            pub fn to_simple_string(&self) -> String {
79                self.0.simple().to_string()
80            }
81
82            pub fn to_hyphenated_string(&self) -> String {
83                self.0.hyphenated().to_string()
84            }
85        }
86
87        impl std::fmt::Display for $name {
88            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89                write!(f, "{}({})", stringify!($name), self.0.to_string())
90            }
91        }
92
93        impl<'de> serde::Deserialize<'de> for $name {
94            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95            where
96                D: serde::Deserializer<'de>,
97            {
98                let id: uuid::Uuid = serde::Deserialize::deserialize(deserializer)?;
99                Ok(Self(id))
100            }
101        }
102
103        impl CheckUuid for $name {
104            fn id(&self) -> &uuid::Uuid {
105                &self.0
106            }
107
108            fn to_string(&self) -> String {
109                format!("{}({})", stringify!($name), self.0.to_string())
110            }
111        }
112    };
113}
114
115implement_uuid!(NoteUuid);
116implement_uuid!(VirtualPageUuid);
117implement_uuid!(VirtualDocUuid);
118implement_uuid!(StrokeUuid);
119implement_uuid!(PageUuid);
120implement_uuid!(PageModelUuid);
121implement_uuid!(PenUuid);
122implement_uuid!(ShapeGroupUuid);
123implement_uuid!(PointsUuid);
124implement_uuid!(ResourceUuid);
125
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub enum PenId {
128    Uuid(PenUuid),
129    Id(u32),
130}
131
132impl PenId {
133    pub fn from_uuid(uuid: PenUuid) -> Self {
134        Self::Uuid(uuid)
135    }
136
137    pub fn from_id(id: u32) -> Self {
138        Self::Id(id)
139    }
140}
141
142impl std::fmt::Display for PenId {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        match self {
145            Self::Uuid(uuid) => write!(f, "{}", uuid),
146            Self::Id(id) => write!(f, "PenId({})", id),
147        }
148    }
149}
150
151impl<'de> serde::Deserialize<'de> for PenId {
152    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
153    where
154        D: serde::Deserializer<'de>,
155    {
156        let id: String = serde::Deserialize::deserialize(deserializer)?;
157        let trimmed = id.trim();
158
159        // Try numeric first to avoid logging an error for the common short-int case.
160        if let Ok(num) = trimmed.parse::<u32>() {
161            return Ok(Self::from_id(num));
162        }
163
164        let uuid = PenUuid::from_str(trimmed).map_err(serde::de::Error::custom)?;
165        Ok(Self::from_uuid(uuid))
166    }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub struct LayerId(u32);
171
172impl LayerId {
173    pub fn new(id: u32) -> Self {
174        Self(id)
175    }
176
177    pub fn value(&self) -> u32 {
178        self.0
179    }
180}
181
182impl std::fmt::Display for LayerId {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        write!(f, "LayerId({})", self.0)
185    }
186}
187
188impl<'de> serde::Deserialize<'de> for LayerId {
189    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
190    where
191        D: serde::Deserializer<'de>,
192    {
193        let id: u32 = serde::Deserialize::deserialize(deserializer)?;
194        Ok(Self(id))
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::{NoteUuid, PenId, StrokeUuid};
201
202    #[test]
203    fn uuid_from_str_accepts_whitespace_padding() {
204        let id = NoteUuid::from_str("  7a960ca753b0420ea2d5b88d57f7bf62  ").unwrap();
205        assert_eq!(id.to_simple_string(), "7a960ca753b0420ea2d5b88d57f7bf62");
206    }
207
208    #[test]
209    fn uuid_from_str_accepts_hyphenated_uuid() {
210        let id = NoteUuid::from_str("7a960ca7-53b0-420e-a2d5-b88d57f7bf62").unwrap();
211        assert_eq!(
212            id.to_hyphenated_string(),
213            "7a960ca7-53b0-420e-a2d5-b88d57f7bf62"
214        );
215    }
216
217    #[test]
218    fn uuid_from_byte_str_accepts_null_and_whitespace_padding() {
219        let raw = b"92c1ab73-4ec1-4f70-907a-dc11dcb0806d\0\0  ";
220        let id = StrokeUuid::from_byte_str(raw).unwrap();
221        assert_eq!(
222            id.to_hyphenated_string(),
223            "92c1ab73-4ec1-4f70-907a-dc11dcb0806d"
224        );
225    }
226
227    #[test]
228    fn pen_id_deserializes_uuid_forms_and_numeric_ids() {
229        let simple: PenId = serde_json::from_str("\"abb5f970105848349b5dfa80d7b0fa5b\"").unwrap();
230        assert!(matches!(simple, PenId::Uuid(_)));
231
232        let hyphenated: PenId =
233            serde_json::from_str("\" abb5f970-1058-4834-9b5d-fa80d7b0fa5b \"").unwrap();
234        assert!(matches!(hyphenated, PenId::Uuid(_)));
235
236        let numeric: PenId = serde_json::from_str("\" 1 \"").unwrap();
237        assert_eq!(numeric, PenId::Id(1));
238    }
239}