Skip to main content

copybook_safe_index/
lib.rs

1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Panic-safe helpers for low-level indexing and arithmetic operations.
4//!
5//! This crate isolates small, single-responsibility helpers that previously lived in
6//! other panic-safe utility layers. All errors are translated into
7//! `copybook-error` values.
8
9use copybook_error::{Error, ErrorCode};
10
11/// Result type alias using `copybook-error`.
12pub type Result<T> = std::result::Result<T, Error>;
13
14/// Safely divide two numbers, checking for division by zero.
15///
16/// # Errors
17///
18/// Returns `CBKP001_SYNTAX` if `denominator` is zero.
19#[inline]
20#[must_use = "Handle the Result or propagate the error"]
21pub fn safe_divide(numerator: usize, denominator: usize, context: &str) -> Result<usize> {
22    if denominator == 0 {
23        return Err(Error::new(
24            ErrorCode::CBKP001_SYNTAX,
25            format!("Division by zero in {context}"),
26        ));
27    }
28    Ok(numerator / denominator)
29}
30
31/// Access a slice index with explicit bounds checking.
32///
33/// # Errors
34///
35/// Returns `CBKP001_SYNTAX` if `index` is out of bounds.
36#[inline]
37#[must_use = "Handle the Result or propagate the error"]
38pub fn safe_slice_get<T>(slice: &[T], index: usize, context: &str) -> Result<T>
39where
40    T: Copy,
41{
42    if index < slice.len() {
43        Ok(slice[index])
44    } else {
45        Err(Error::new(
46            ErrorCode::CBKP001_SYNTAX,
47            format!(
48                "Slice bounds violation in {context}: index {index} >= length {}",
49                slice.len()
50            ),
51        ))
52    }
53}
54
55#[cfg(test)]
56#[allow(clippy::expect_used, clippy::unwrap_used)]
57mod tests {
58    use super::*;
59    use proptest::prelude::*;
60
61    #[test]
62    fn safe_divide_ok() {
63        assert_eq!(safe_divide(10, 2, "test").expect("divide"), 5);
64    }
65
66    #[test]
67    fn safe_divide_by_zero_is_error() {
68        assert!(matches!(
69            safe_divide(10, 0, "test"),
70            Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
71        ));
72    }
73
74    #[test]
75    fn safe_slice_get_ok() {
76        let data = [1u8, 2u8, 3u8];
77        assert_eq!(safe_slice_get(&data, 1, "test").expect("index"), 2u8);
78    }
79
80    #[test]
81    fn safe_slice_get_out_of_range_is_error() {
82        let data = [1u8, 2u8, 3u8];
83        assert!(matches!(
84            safe_slice_get(&data, 99, "test"),
85            Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
86        ));
87    }
88
89    // --- safe_divide edge cases ---
90
91    #[test]
92    fn safe_divide_zero_numerator() {
93        assert_eq!(safe_divide(0, 5, "test").expect("0/5"), 0);
94    }
95
96    #[test]
97    fn safe_divide_same_values() {
98        assert_eq!(safe_divide(7, 7, "test").expect("7/7"), 1);
99    }
100
101    #[test]
102    fn safe_divide_integer_truncation() {
103        assert_eq!(safe_divide(7, 2, "test").expect("7/2"), 3);
104    }
105
106    #[test]
107    fn safe_divide_large_values() {
108        assert_eq!(
109            safe_divide(usize::MAX, 1, "test").expect("max/1"),
110            usize::MAX
111        );
112    }
113
114    #[test]
115    fn safe_divide_error_message_contains_context() {
116        let err = safe_divide(1, 0, "my-context").unwrap_err();
117        assert!(
118            err.message.contains("my-context"),
119            "Error message should contain context"
120        );
121    }
122
123    // --- safe_slice_get edge cases ---
124
125    #[test]
126    fn safe_slice_get_empty_slice() {
127        let data: &[u8] = &[];
128        assert!(safe_slice_get(data, 0, "test").is_err());
129    }
130
131    #[test]
132    fn safe_slice_get_first_element() {
133        let data = [42u8];
134        assert_eq!(safe_slice_get(&data, 0, "test").expect("first"), 42);
135    }
136
137    #[test]
138    fn safe_slice_get_last_element() {
139        let data = [1u8, 2, 3, 4, 5];
140        assert_eq!(safe_slice_get(&data, 4, "test").expect("last"), 5);
141    }
142
143    #[test]
144    fn safe_slice_get_exactly_out_of_bounds() {
145        let data = [1u8, 2, 3];
146        assert!(safe_slice_get(&data, 3, "test").is_err());
147    }
148
149    #[test]
150    fn safe_slice_get_with_i32_type() {
151        let data = [10i32, 20, 30];
152        assert_eq!(safe_slice_get(&data, 2, "test").expect("i32"), 30);
153    }
154
155    proptest! {
156        #[test]
157        fn safe_divide_round_trip(
158            numerator in 0usize..1_000_000usize,
159            denominator in 1usize..1000usize,
160        ) {
161            prop_assert_eq!(
162                safe_divide(numerator, denominator, "prop").expect("safe divide"),
163                numerator / denominator
164            );
165        }
166
167        #[test]
168        fn safe_slice_get_round_trip(values in prop::collection::vec(0u8..=255u8, 1..200), index in 0usize..220usize) {
169            let normalized_index = index % (values.len() + 1);
170            let result = safe_slice_get(&values, normalized_index, "prop");
171
172            if normalized_index < values.len() {
173                prop_assert_eq!(result.expect("index in bounds"), values[normalized_index]);
174            } else {
175                prop_assert!(result.is_err());
176            }
177        }
178    }
179}