Skip to main content

serializer/
safety.rs

1//! # Safety Validation Utilities
2//!
3//! This module provides zero-cost abstractions for common safety checks
4//! required when working with unsafe code. It is designed to provide
5//! consistent validation patterns for zero-copy operations.
6//!
7//! ## Attribution
8//!
9//! This code is inlined from the `dx-safety` crate to enable standalone
10//! publishability of `dx-serializer` without path dependencies.
11//! Original source: `crates/dx/dx-safety/src/lib.rs`
12//!
13//! ## Usage
14//!
15//! ```rust
16//! use serializer::safety::{check_bounds, check_alignment, check_cast, SafetyError};
17//!
18//! fn safe_read<T: Copy>(buffer: &[u8]) -> Result<&T, SafetyError> {
19//!     // Validate before any unsafe operation
20//!     check_cast::<T>(buffer)?;
21//!     
22//!     // SAFETY: We verified size >= size_of::<T>() and alignment matches
23//!     Ok(unsafe { &*(buffer.as_ptr() as *const T) })
24//! }
25//!
26//! // Example usage
27//! let data = [1u8, 0, 0, 0]; // Little-endian 1
28//! let value: &u32 = safe_read(&data).unwrap();
29//! assert_eq!(*value, 1);
30//! ```
31
32use core::fmt;
33use core::mem::{align_of, size_of};
34
35// ============================================================================
36// ERROR TYPES
37// ============================================================================
38
39/// Error type for safety validation failures.
40///
41/// All variants include context information to aid debugging.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum SafetyError {
44    /// Buffer is too small for the requested type.
45    ///
46    /// Contains the number of bytes needed and the actual buffer size.
47    BufferTooSmall {
48        /// Minimum bytes required
49        needed: usize,
50        /// Actual buffer size
51        actual: usize,
52    },
53
54    /// Pointer is not properly aligned for the requested type.
55    ///
56    /// Contains the required alignment and the actual misalignment offset.
57    Misaligned {
58        /// Required alignment in bytes
59        needed: usize,
60        /// Actual offset from aligned address (ptr % needed)
61        actual: usize,
62    },
63
64    /// Offset would exceed buffer bounds.
65    ///
66    /// Contains the requested offset and the buffer length.
67    OffsetOutOfBounds {
68        /// Requested offset
69        offset: usize,
70        /// Buffer length
71        length: usize,
72    },
73
74    /// Integer overflow occurred during size calculation.
75    ///
76    /// This typically happens when multiplying `count * size_of::<T>()`.
77    Overflow,
78}
79
80impl fmt::Display for SafetyError {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            Self::BufferTooSmall { needed, actual } => {
84                write!(
85                    f,
86                    "buffer too small: needed {} bytes, got {}",
87                    needed, actual
88                )
89            }
90            Self::Misaligned { needed, actual } => {
91                write!(
92                    f,
93                    "pointer misaligned: needed {} byte alignment, offset is {}",
94                    needed, actual
95                )
96            }
97            Self::OffsetOutOfBounds { offset, length } => {
98                write!(f, "offset {} out of bounds for length {}", offset, length)
99            }
100            Self::Overflow => write!(f, "integer overflow in size calculation"),
101        }
102    }
103}
104
105impl std::error::Error for SafetyError {}
106
107// ============================================================================
108// BOUNDS CHECKING UTILITIES
109// ============================================================================
110
111/// Check that a slice has sufficient length for type T.
112///
113/// This is a zero-cost check when the slice is large enough.
114///
115/// # Example
116///
117/// ```rust
118/// use serializer::safety::{check_size, SafetyError};
119///
120/// let buffer = [0u8; 8];
121/// assert!(check_size::<u64>(&buffer).is_ok());
122/// assert!(check_size::<u128>(&buffer).is_err());
123/// ```
124#[inline(always)]
125pub fn check_size<T>(slice: &[u8]) -> Result<(), SafetyError> {
126    let needed = size_of::<T>();
127    if slice.len() < needed {
128        Err(SafetyError::BufferTooSmall {
129            needed,
130            actual: slice.len(),
131        })
132    } else {
133        Ok(())
134    }
135}
136
137/// Check that offset + size doesn't exceed buffer length.
138///
139/// This function also checks for integer overflow when adding offset + size.
140///
141/// # Example
142///
143/// ```rust
144/// use serializer::safety::{check_bounds, SafetyError};
145///
146/// // Valid: offset 0, size 4, length 8
147/// assert!(check_bounds(0, 4, 8).is_ok());
148///
149/// // Invalid: offset 6, size 4, length 8 (6 + 4 = 10 > 8)
150/// assert!(matches!(
151///     check_bounds(6, 4, 8),
152///     Err(SafetyError::OffsetOutOfBounds { .. })
153/// ));
154///
155/// // Overflow: very large values
156/// assert!(matches!(
157///     check_bounds(usize::MAX, 1, 100),
158///     Err(SafetyError::Overflow)
159/// ));
160/// ```
161#[inline(always)]
162pub fn check_bounds(offset: usize, size: usize, length: usize) -> Result<(), SafetyError> {
163    match offset.checked_add(size) {
164        Some(end) if end <= length => Ok(()),
165        Some(_) => Err(SafetyError::OffsetOutOfBounds { offset, length }),
166        None => Err(SafetyError::Overflow),
167    }
168}
169
170/// Check bounds for reading `count` elements of type T starting at `offset`.
171///
172/// This combines overflow checking for `count * size_of::<T>()` with bounds checking.
173///
174/// # Example
175///
176/// ```rust
177/// use serializer::safety::{check_slice_bounds, SafetyError};
178///
179/// let buffer = [0u8; 32];
180///
181/// // Valid: 4 u64s starting at offset 0 = 32 bytes
182/// assert!(check_slice_bounds::<u64>(0, 4, buffer.len()).is_ok());
183///
184/// // Invalid: 5 u64s = 40 bytes > 32
185/// assert!(check_slice_bounds::<u64>(0, 5, buffer.len()).is_err());
186/// ```
187#[inline(always)]
188pub fn check_slice_bounds<T>(
189    offset: usize,
190    count: usize,
191    length: usize,
192) -> Result<(), SafetyError> {
193    let size = size_of::<T>()
194        .checked_mul(count)
195        .ok_or(SafetyError::Overflow)?;
196    check_bounds(offset, size, length)
197}
198
199// ============================================================================
200// ALIGNMENT CHECKING UTILITIES
201// ============================================================================
202
203/// Check that a pointer is properly aligned for type T.
204///
205/// # Example
206///
207/// ```rust
208/// use serializer::safety::{check_alignment, SafetyError};
209///
210/// let aligned: [u64; 2] = [0, 0];
211/// let ptr = aligned.as_ptr() as *const u8;
212///
213/// // u64 requires 8-byte alignment
214/// assert!(check_alignment::<u64>(ptr).is_ok());
215///
216/// // Offset by 1 byte - now misaligned for u64
217/// let misaligned = unsafe { ptr.add(1) };
218/// assert!(matches!(
219///     check_alignment::<u64>(misaligned),
220///     Err(SafetyError::Misaligned { needed: 8, actual: 1 })
221/// ));
222/// ```
223#[inline(always)]
224pub fn check_alignment<T>(ptr: *const u8) -> Result<(), SafetyError> {
225    let needed = align_of::<T>();
226    let actual = ptr as usize % needed;
227    if actual != 0 {
228        Err(SafetyError::Misaligned { needed, actual })
229    } else {
230        Ok(())
231    }
232}
233
234/// Combined check for size and alignment before casting a slice to type T.
235///
236/// This is the primary validation function for zero-copy deserialization.
237///
238/// # Example
239///
240/// ```rust
241/// use serializer::safety::{check_cast, SafetyError};
242///
243/// #[repr(C)]
244/// struct Header {
245///     magic: u32,
246///     version: u32,
247/// }
248///
249/// fn read_header(buffer: &[u8]) -> Result<&Header, SafetyError> {
250///     check_cast::<Header>(buffer)?;
251///     // SAFETY: We verified size and alignment
252///     Ok(unsafe { &*(buffer.as_ptr() as *const Header) })
253/// }
254///
255/// // Example usage with properly aligned buffer
256/// let data = [0x44u8, 0x58, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00];
257/// let header = read_header(&data).unwrap();
258/// ```
259#[inline(always)]
260pub fn check_cast<T>(slice: &[u8]) -> Result<(), SafetyError> {
261    check_size::<T>(slice)?;
262    check_alignment::<T>(slice.as_ptr())?;
263    Ok(())
264}
265
266// ============================================================================
267// SAFE POINTER UTILITIES
268// ============================================================================
269
270/// Safely read a value of type T from a byte slice with full validation.
271///
272/// This is a convenience function that combines validation and reading.
273///
274/// # Safety
275///
276/// This helper borrows a `T` directly from caller-provided bytes. The caller
277/// must only use types that can safely be viewed from raw bytes, such as
278/// integer primitives or carefully audited `#[repr(C)]` POD structs. The
279/// function validates length and alignment before the cast, but it cannot prove
280/// that every possible bit pattern is valid for `T`.
281///
282/// # Example
283///
284/// ```rust
285/// use serializer::safety::safe_read;
286///
287/// let buffer = 42u64.to_le_bytes();
288/// let value: &u64 = safe_read(&buffer).unwrap();
289/// assert_eq!(*value, 42);
290/// ```
291#[inline]
292#[allow(unsafe_code)]
293pub fn safe_read<T: Copy>(slice: &[u8]) -> Result<&T, SafetyError> {
294    check_cast::<T>(slice)?;
295    // SAFETY: length and alignment were checked immediately above. Validity of
296    // the `T` bit pattern is part of this helper's documented contract.
297    Ok(unsafe { &*(slice.as_ptr() as *const T) })
298}
299
300/// Safely read a slice of values from a byte buffer with full validation.
301///
302/// # Safety
303///
304/// This helper borrows a `[T]` directly from caller-provided bytes. The caller
305/// must only use types that can safely be viewed from raw bytes. The function
306/// validates length and alignment, but validity of each `T` value remains part
307/// of the caller contract.
308///
309/// # Example
310///
311/// ```rust
312/// use serializer::safety::{safe_read_slice, SafetyError};
313///
314/// // Create a properly aligned buffer
315/// let values: [u32; 4] = [1, 2, 3, 4];
316/// let bytes: &[u8] = bytemuck::cast_slice(&values);
317/// let read_values: &[u32] = safe_read_slice(bytes, 4).unwrap();
318/// assert_eq!(read_values, &[1, 2, 3, 4]);
319/// ```
320#[inline]
321#[allow(unsafe_code)]
322pub fn safe_read_slice<T: Copy>(slice: &[u8], count: usize) -> Result<&[T], SafetyError> {
323    check_slice_bounds::<T>(0, count, slice.len())?;
324    check_alignment::<T>(slice.as_ptr())?;
325    // SAFETY: bounds and alignment were checked immediately above. Validity of
326    // each `T` bit pattern is part of this helper's documented contract.
327    Ok(unsafe { core::slice::from_raw_parts(slice.as_ptr() as *const T, count) })
328}
329
330// ============================================================================
331// TESTS
332// ============================================================================
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn test_check_size_success() {
340        let buffer = [0u8; 16];
341        assert!(check_size::<u8>(&buffer).is_ok());
342        assert!(check_size::<u16>(&buffer).is_ok());
343        assert!(check_size::<u32>(&buffer).is_ok());
344        assert!(check_size::<u64>(&buffer).is_ok());
345        assert!(check_size::<u128>(&buffer).is_ok());
346    }
347
348    #[test]
349    fn test_check_size_failure() {
350        let buffer = [0u8; 4];
351        assert!(matches!(
352            check_size::<u64>(&buffer),
353            Err(SafetyError::BufferTooSmall {
354                needed: 8,
355                actual: 4
356            })
357        ));
358    }
359
360    #[test]
361    fn test_check_bounds_success() {
362        assert!(check_bounds(0, 4, 8).is_ok());
363        assert!(check_bounds(4, 4, 8).is_ok());
364        assert!(check_bounds(0, 8, 8).is_ok());
365        assert!(check_bounds(0, 0, 0).is_ok());
366    }
367
368    #[test]
369    fn test_check_bounds_failure() {
370        assert!(matches!(
371            check_bounds(5, 4, 8),
372            Err(SafetyError::OffsetOutOfBounds {
373                offset: 5,
374                length: 8
375            })
376        ));
377    }
378
379    #[test]
380    fn test_check_bounds_overflow() {
381        assert!(matches!(
382            check_bounds(usize::MAX, 1, 100),
383            Err(SafetyError::Overflow)
384        ));
385    }
386
387    #[test]
388    fn test_check_alignment_success() {
389        let aligned: [u64; 2] = [0, 0];
390        let ptr = aligned.as_ptr() as *const u8;
391        assert!(check_alignment::<u64>(ptr).is_ok());
392        assert!(check_alignment::<u32>(ptr).is_ok());
393        assert!(check_alignment::<u16>(ptr).is_ok());
394        assert!(check_alignment::<u8>(ptr).is_ok());
395    }
396
397    #[test]
398    fn test_check_alignment_failure() {
399        let aligned: [u64; 2] = [0, 0];
400        let ptr = aligned.as_ptr() as *const u8;
401        let misaligned = ptr.wrapping_add(1);
402
403        let result = check_alignment::<u64>(misaligned);
404        assert!(matches!(
405            result,
406            Err(SafetyError::Misaligned {
407                needed: 8,
408                actual: 1
409            })
410        ));
411    }
412
413    #[test]
414    fn test_safe_read() {
415        let value: u64 = 0x1234567890ABCDEF;
416        let bytes = value.to_le_bytes();
417
418        let read_value: &u64 = safe_read(&bytes).unwrap();
419        assert_eq!(*read_value, value);
420    }
421
422    #[test]
423    fn test_safe_read_slice() {
424        let values: [u32; 4] = [1, 2, 3, 4];
425        #[repr(align(4))]
426        struct AlignedBytes([u8; 16]);
427
428        let mut bytes = AlignedBytes([0; 16]);
429        for (chunk, value) in bytes.0.chunks_exact_mut(4).zip(values) {
430            chunk.copy_from_slice(&value.to_ne_bytes());
431        }
432
433        let read_values: &[u32] = safe_read_slice(&bytes.0, 4).unwrap();
434        assert_eq!(read_values, &[1, 2, 3, 4]);
435    }
436
437    #[test]
438    fn test_error_display() {
439        let err = SafetyError::BufferTooSmall {
440            needed: 8,
441            actual: 4,
442        };
443        assert_eq!(err.to_string(), "buffer too small: needed 8 bytes, got 4");
444
445        let err = SafetyError::Misaligned {
446            needed: 8,
447            actual: 3,
448        };
449        assert_eq!(
450            err.to_string(),
451            "pointer misaligned: needed 8 byte alignment, offset is 3"
452        );
453
454        let err = SafetyError::OffsetOutOfBounds {
455            offset: 10,
456            length: 8,
457        };
458        assert_eq!(err.to_string(), "offset 10 out of bounds for length 8");
459
460        let err = SafetyError::Overflow;
461        assert_eq!(err.to_string(), "integer overflow in size calculation");
462    }
463}