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}