nautilus_core/python/
uuid.rs1use std::{
19 collections::hash_map::DefaultHasher,
20 ffi::CStr,
21 hash::{Hash, Hasher},
22 str::FromStr,
23};
24
25use pyo3::{
26 IntoPyObjectExt, Py,
27 prelude::*,
28 pyclass::CompareOp,
29 types::{PyBytes, PyTuple},
30};
31
32use super::{IntoPyObjectNautilusExt, to_pyvalue_err};
33use crate::uuid::{UUID4, UUID4_LEN};
34
35#[pymethods]
36impl UUID4 {
37 #[new]
42 fn py_new() -> Self {
43 Self::new()
44 }
45
46 #[allow(clippy::needless_pass_by_value)]
48 fn __setstate__(&mut self, py: Python<'_>, state: Py<PyAny>) -> PyResult<()> {
49 let bytes: &Bound<'_, PyBytes> = state.downcast_bound::<PyBytes>(py)?;
50 let slice = bytes.as_bytes();
51
52 if slice.len() != UUID4_LEN {
53 return Err(to_pyvalue_err(
54 "Invalid state for deserializing, incorrect bytes length",
55 ));
56 }
57
58 if slice[UUID4_LEN - 1] != 0 {
59 return Err(to_pyvalue_err(
60 "Invalid state for deserializing, missing null terminator",
61 ));
62 }
63
64 let cstr = CStr::from_bytes_with_nul(slice).map_err(|_| {
65 to_pyvalue_err("Invalid state for deserializing, bytes must be null-terminated UTF-8")
66 })?;
67
68 let value = cstr.to_str().map_err(|_| {
69 to_pyvalue_err("Invalid state for deserializing, bytes must be valid UTF-8")
70 })?;
71
72 let parsed = UUID4::from_str(value).map_err(|e| {
73 to_pyvalue_err(format!(
74 "Invalid state for deserializing, unable to parse UUID: {e}"
75 ))
76 })?;
77
78 self.value.copy_from_slice(&parsed.value);
79 Ok(())
80 }
81
82 fn __getstate__(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
84 PyBytes::new(py, &self.value).into_py_any(py)
85 }
86
87 fn __reduce__(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
89 let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
90 let state = self.__getstate__(py)?;
91 (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
92 }
93
94 #[staticmethod]
96 #[allow(clippy::unnecessary_wraps)]
97 fn _safe_constructor() -> PyResult<Self> {
98 Ok(Self::new()) }
100
101 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
103 match op {
104 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
105 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
106 _ => py.NotImplemented(),
107 }
108 }
109
110 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
112 fn __hash__(&self) -> isize {
113 let mut h = DefaultHasher::new();
114 self.hash(&mut h);
115 h.finish() as isize
116 }
117
118 fn __repr__(&self) -> String {
120 format!("{self:?}")
121 }
122
123 fn __str__(&self) -> String {
125 self.to_string()
126 }
127
128 #[getter]
130 #[pyo3(name = "value")]
131 fn py_value(&self) -> String {
132 self.to_string()
133 }
134
135 #[staticmethod]
137 #[pyo3(name = "from_str")]
138 fn py_from_str(value: &str) -> PyResult<Self> {
139 Self::from_str(value).map_err(to_pyvalue_err)
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use std::sync::Once;
146
147 use pyo3::Python;
148
149 use super::*;
150
151 fn ensure_python_initialized() {
152 static INIT: Once = Once::new();
153 INIT.call_once(|| {
154 pyo3::prepare_freethreaded_python();
155 });
156 }
157
158 #[test]
159 fn test_setstate_rejects_invalid_uuid_bytes() {
160 ensure_python_initialized();
161 Python::with_gil(|py| {
162 let mut uuid = UUID4::new();
163 let mut invalid = [b'a'; UUID4_LEN];
164 invalid[UUID4_LEN - 1] = 0;
165 let py_bytes = PyBytes::new(py, &invalid);
166 let err = uuid
167 .__setstate__(py, py_bytes.into_py_any_unwrap(py))
168 .expect_err("expected invalid state to error");
169 assert!(err.to_string().contains("Invalid state for deserializing"));
170 });
171 }
172
173 #[test]
174 fn test_setstate_rejects_missing_null_terminator() {
175 ensure_python_initialized();
176 Python::with_gil(|py| {
177 let mut uuid = UUID4::new();
178 let mut bytes = uuid.value;
179 bytes[UUID4_LEN - 1] = b'0';
180 let py_bytes = PyBytes::new(py, &bytes);
181 let err = uuid
182 .__setstate__(py, py_bytes.into_py_any_unwrap(py))
183 .expect_err("expected missing NUL terminator to error");
184 assert!(
185 err.to_string()
186 .contains("Invalid state for deserializing, missing null terminator")
187 );
188 });
189 }
190
191 #[test]
192 fn test_setstate_accepts_valid_state() {
193 ensure_python_initialized();
194 Python::with_gil(|py| {
195 let source = UUID4::new();
196 let mut target = UUID4::new();
197 let py_bytes = PyBytes::new(py, &source.value);
198 target
199 .__setstate__(py, py_bytes.into_py_any_unwrap(py))
200 .expect("valid state should succeed");
201 assert_eq!(target.to_string(), source.to_string());
202 });
203 }
204}