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(
48 clippy::needless_pass_by_value,
49 reason = "Python FFI requires owned types"
50 )]
51 fn __setstate__(&mut self, py: Python<'_>, state: Py<PyAny>) -> PyResult<()> {
52 let bytes: &Bound<'_, PyBytes> = state.cast_bound::<PyBytes>(py)?;
53 let slice = bytes.as_bytes();
54
55 if slice.len() != UUID4_LEN {
56 return Err(to_pyvalue_err(
57 "Invalid state for deserializing, incorrect bytes length",
58 ));
59 }
60
61 if slice[UUID4_LEN - 1] != 0 {
62 return Err(to_pyvalue_err(
63 "Invalid state for deserializing, missing null terminator",
64 ));
65 }
66
67 let cstr = CStr::from_bytes_with_nul(slice).map_err(|_| {
68 to_pyvalue_err("Invalid state for deserializing, bytes must be null-terminated UTF-8")
69 })?;
70
71 let value = cstr.to_str().map_err(|_| {
72 to_pyvalue_err("Invalid state for deserializing, bytes must be valid UTF-8")
73 })?;
74
75 let parsed = Self::from_str(value).map_err(|e| {
76 to_pyvalue_err(format!(
77 "Invalid state for deserializing, unable to parse UUID: {e}"
78 ))
79 })?;
80
81 self.value.copy_from_slice(&parsed.value);
82 Ok(())
83 }
84
85 fn __getstate__(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
87 PyBytes::new(py, &self.value).into_py_any(py)
88 }
89
90 fn __reduce__(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
92 let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
93 let state = self.__getstate__(py)?;
94 (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
95 }
96
97 #[staticmethod]
99 #[allow(
100 clippy::unnecessary_wraps,
101 reason = "Python FFI requires Result return type"
102 )]
103 fn _safe_constructor() -> PyResult<Self> {
104 Ok(Self::new()) }
106
107 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
109 match op {
110 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
111 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
112 _ => py.NotImplemented(),
113 }
114 }
115
116 #[allow(
118 clippy::cast_possible_truncation,
119 clippy::cast_possible_wrap,
120 reason = "Intentional cast for Python interop"
121 )]
122 fn __hash__(&self) -> isize {
123 let mut h = DefaultHasher::new();
124 self.hash(&mut h);
125 h.finish() as isize
126 }
127
128 fn __repr__(&self) -> String {
130 format!("{self:?}")
131 }
132
133 fn __str__(&self) -> String {
135 self.to_string()
136 }
137
138 #[getter]
140 #[pyo3(name = "value")]
141 fn py_value(&self) -> String {
142 self.to_string()
143 }
144
145 #[staticmethod]
147 #[pyo3(name = "from_str")]
148 fn py_from_str(value: &str) -> PyResult<Self> {
149 Self::from_str(value).map_err(to_pyvalue_err)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use std::sync::Once;
156
157 use pyo3::Python;
158 use rstest::rstest;
159
160 use super::*;
161
162 fn ensure_python_initialized() {
163 static INIT: Once = Once::new();
164 INIT.call_once(|| {
165 pyo3::prepare_freethreaded_python();
166 });
167 }
168
169 #[rstest]
170 fn test_setstate_rejects_invalid_uuid_bytes() {
171 ensure_python_initialized();
172 Python::with_gil(|py| {
173 let mut uuid = UUID4::new();
174 let mut invalid = [b'a'; UUID4_LEN];
175 invalid[UUID4_LEN - 1] = 0;
176 let py_bytes = PyBytes::new(py, &invalid);
177 let err = uuid
178 .__setstate__(py, py_bytes.into_py_any_unwrap(py))
179 .expect_err("expected invalid state to error");
180 assert!(err.to_string().contains("Invalid state for deserializing"));
181 });
182 }
183
184 #[rstest]
185 fn test_setstate_rejects_missing_null_terminator() {
186 ensure_python_initialized();
187 Python::with_gil(|py| {
188 let mut uuid = UUID4::new();
189 let mut bytes = uuid.value;
190 bytes[UUID4_LEN - 1] = b'0';
191 let py_bytes = PyBytes::new(py, &bytes);
192 let err = uuid
193 .__setstate__(py, py_bytes.into_py_any_unwrap(py))
194 .expect_err("expected missing NUL terminator to error");
195 assert!(
196 err.to_string()
197 .contains("Invalid state for deserializing, missing null terminator")
198 );
199 });
200 }
201
202 #[rstest]
203 fn test_setstate_accepts_valid_state() {
204 ensure_python_initialized();
205 Python::with_gil(|py| {
206 let source = UUID4::new();
207 let mut target = UUID4::new();
208 let py_bytes = PyBytes::new(py, &source.value);
209 target
210 .__setstate__(py, py_bytes.into_py_any_unwrap(py))
211 .expect("valid state should succeed");
212 assert_eq!(target.to_string(), source.to_string());
213 });
214 }
215}