Skip to main content

copybook_overflow/
lib.rs

1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Overflow-safe numeric guards for copybook-rs.
4//!
5//! This crate isolates checked arithmetic and checked narrowing conversions
6//! that are performance-sensitive and correctness-critical.
7//!
8//! All functions return structured [`copybook_error::Error`] values with
9//! domain-specific error codes on overflow.
10
11use copybook_error::{Error, ErrorCode, Result};
12
13/// Safely calculate COBOL array bounds with overflow protection.
14///
15/// # Errors
16/// Returns `CBKP021_ODO_NOT_TAIL` for multiplication/addition overflow.
17#[inline]
18#[must_use = "Handle the Result or propagate the error"]
19pub fn safe_array_bound(
20    base: usize,
21    count: usize,
22    item_size: usize,
23    context: &str,
24) -> Result<usize> {
25    let total_size = count.checked_mul(item_size).ok_or_else(|| {
26        Error::new(
27            ErrorCode::CBKP021_ODO_NOT_TAIL,
28            format!("Array size overflow in {context}: {count} * {item_size} would overflow"),
29        )
30    })?;
31
32    base.checked_add(total_size).ok_or_else(|| {
33        Error::new(
34            ErrorCode::CBKP021_ODO_NOT_TAIL,
35            format!("Array offset overflow in {context}: {base} + {total_size} would overflow"),
36        )
37    })
38}
39
40/// Safely convert `u64` to `u32` with overflow checking.
41///
42/// # Errors
43/// Returns `CBKS141_RECORD_TOO_LARGE` when `value > u32::MAX`.
44#[inline]
45#[must_use = "Handle the Result or propagate the error"]
46pub fn safe_u64_to_u32(value: u64, context: &str) -> Result<u32> {
47    u32::try_from(value).map_err(|_| {
48        Error::new(
49            ErrorCode::CBKS141_RECORD_TOO_LARGE,
50            format!(
51                "Integer overflow converting u64 to u32 in {context}: {value} exceeds u32::MAX"
52            ),
53        )
54    })
55}
56
57/// Safely convert `u64` to `u16` with overflow checking.
58///
59/// # Errors
60/// Returns `CBKS141_RECORD_TOO_LARGE` when `value > u16::MAX`.
61#[inline]
62#[must_use = "Handle the Result or propagate the error"]
63pub fn safe_u64_to_u16(value: u64, context: &str) -> Result<u16> {
64    u16::try_from(value).map_err(|_| {
65        Error::new(
66            ErrorCode::CBKS141_RECORD_TOO_LARGE,
67            format!(
68                "Integer overflow converting u64 to u16 in {context}: {value} exceeds u16::MAX"
69            ),
70        )
71    })
72}
73
74/// Safely convert `usize` to `u32` with overflow checking.
75///
76/// # Errors
77/// Returns `CBKS141_RECORD_TOO_LARGE` when `value > u32::MAX`.
78#[inline]
79#[must_use = "Handle the Result or propagate the error"]
80pub fn safe_usize_to_u32(value: usize, context: &str) -> Result<u32> {
81    u32::try_from(value).map_err(|_| {
82        Error::new(
83            ErrorCode::CBKS141_RECORD_TOO_LARGE,
84            format!(
85                "Integer overflow converting usize to u32 in {context}: {value} exceeds u32::MAX"
86            ),
87        )
88    })
89}
90
91#[cfg(test)]
92#[allow(clippy::expect_used, clippy::unwrap_used)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn safe_array_bound_ok() {
98        let value = safe_array_bound(10, 3, 4, "test");
99        assert_eq!(value.unwrap(), 22);
100    }
101
102    #[test]
103    fn safe_array_bound_mul_overflow() {
104        let err = safe_array_bound(0, usize::MAX, 2, "mul-overflow").unwrap_err();
105        assert_eq!(err.code, ErrorCode::CBKP021_ODO_NOT_TAIL);
106    }
107
108    #[test]
109    fn safe_array_bound_add_overflow() {
110        let err = safe_array_bound(usize::MAX - 1, 1, 2, "add-overflow").unwrap_err();
111        assert_eq!(err.code, ErrorCode::CBKP021_ODO_NOT_TAIL);
112    }
113
114    #[test]
115    fn safe_u64_to_u32_ok() {
116        let value = safe_u64_to_u32(123, "test");
117        assert_eq!(value.unwrap(), 123);
118    }
119
120    #[test]
121    fn safe_u64_to_u32_overflow() {
122        let err = safe_u64_to_u32(u64::from(u32::MAX) + 1, "u64->u32").unwrap_err();
123        assert_eq!(err.code, ErrorCode::CBKS141_RECORD_TOO_LARGE);
124    }
125
126    #[test]
127    fn safe_u64_to_u16_ok() {
128        let value = safe_u64_to_u16(123, "test");
129        assert_eq!(value.unwrap(), 123);
130    }
131
132    #[test]
133    fn safe_u64_to_u16_overflow() {
134        let err = safe_u64_to_u16(u64::from(u16::MAX) + 1, "u64->u16").unwrap_err();
135        assert_eq!(err.code, ErrorCode::CBKS141_RECORD_TOO_LARGE);
136    }
137
138    #[test]
139    fn safe_usize_to_u32_ok() {
140        let value = safe_usize_to_u32(123, "test");
141        assert_eq!(value.unwrap(), 123);
142    }
143
144    #[cfg(target_pointer_width = "64")]
145    #[test]
146    fn safe_usize_to_u32_overflow() {
147        let err = safe_usize_to_u32(u32::MAX as usize + 1, "usize->u32").unwrap_err();
148        assert_eq!(err.code, ErrorCode::CBKS141_RECORD_TOO_LARGE);
149    }
150
151    // --- Boundary value tests ---
152
153    #[test]
154    fn safe_u64_to_u32_at_max_boundary() {
155        assert_eq!(
156            safe_u64_to_u32(u64::from(u32::MAX), "boundary").unwrap(),
157            u32::MAX
158        );
159    }
160
161    #[test]
162    fn safe_u64_to_u32_zero() {
163        assert_eq!(safe_u64_to_u32(0, "zero").unwrap(), 0);
164    }
165
166    #[test]
167    fn safe_u64_to_u16_at_max_boundary() {
168        assert_eq!(
169            safe_u64_to_u16(u64::from(u16::MAX), "boundary").unwrap(),
170            u16::MAX
171        );
172    }
173
174    #[test]
175    fn safe_u64_to_u16_zero() {
176        assert_eq!(safe_u64_to_u16(0, "zero").unwrap(), 0);
177    }
178
179    #[test]
180    fn safe_u64_to_u16_just_over_max() {
181        let err = safe_u64_to_u16(u64::from(u16::MAX) + 1, "over-max").unwrap_err();
182        assert_eq!(err.code, ErrorCode::CBKS141_RECORD_TOO_LARGE);
183    }
184
185    #[test]
186    fn safe_u64_to_u32_just_over_max() {
187        let err = safe_u64_to_u32(u64::from(u32::MAX) + 1, "over-max").unwrap_err();
188        assert_eq!(err.code, ErrorCode::CBKS141_RECORD_TOO_LARGE);
189    }
190
191    #[test]
192    fn safe_u64_to_u32_u64_max() {
193        let err = safe_u64_to_u32(u64::MAX, "u64-max").unwrap_err();
194        assert_eq!(err.code, ErrorCode::CBKS141_RECORD_TOO_LARGE);
195    }
196
197    #[test]
198    fn safe_u64_to_u16_u64_max() {
199        let err = safe_u64_to_u16(u64::MAX, "u64-max").unwrap_err();
200        assert_eq!(err.code, ErrorCode::CBKS141_RECORD_TOO_LARGE);
201    }
202
203    #[test]
204    fn safe_usize_to_u32_zero() {
205        assert_eq!(safe_usize_to_u32(0, "zero").unwrap(), 0);
206    }
207
208    #[test]
209    fn safe_usize_to_u32_at_max_boundary() {
210        assert_eq!(
211            safe_usize_to_u32(u32::MAX as usize, "boundary").unwrap(),
212            u32::MAX
213        );
214    }
215
216    // --- safe_array_bound boundary tests ---
217
218    #[test]
219    fn safe_array_bound_zero_count() {
220        assert_eq!(safe_array_bound(10, 0, 4, "zero-count").unwrap(), 10);
221    }
222
223    #[test]
224    fn safe_array_bound_zero_base() {
225        assert_eq!(safe_array_bound(0, 5, 3, "zero-base").unwrap(), 15);
226    }
227
228    #[test]
229    fn safe_array_bound_zero_item_size() {
230        assert_eq!(safe_array_bound(10, 1000, 0, "zero-item").unwrap(), 10);
231    }
232
233    #[test]
234    fn safe_array_bound_all_zeros() {
235        assert_eq!(safe_array_bound(0, 0, 0, "all-zero").unwrap(), 0);
236    }
237
238    #[test]
239    fn safe_array_bound_error_message_contains_context() {
240        let err = safe_array_bound(0, usize::MAX, 2, "my-context").unwrap_err();
241        assert!(
242            err.message.contains("my-context"),
243            "Error message should contain context"
244        );
245    }
246
247    // --- Additional coverage ---
248
249    #[test]
250    fn safe_array_bound_large_non_overflowing() {
251        // 1000 * 1000 + 500 = 1_000_500
252        assert_eq!(
253            safe_array_bound(500, 1000, 1000, "large").unwrap(),
254            1_000_500
255        );
256    }
257
258    #[test]
259    fn safe_array_bound_max_base_zero_product() {
260        assert_eq!(
261            safe_array_bound(usize::MAX, 0, 100, "max-base").unwrap(),
262            usize::MAX
263        );
264    }
265
266    #[test]
267    fn safe_array_bound_mul_overflow_message() {
268        let err = safe_array_bound(0, usize::MAX, 2, "ctx").unwrap_err();
269        assert!(err.message.contains("Array size overflow"));
270        assert!(err.message.contains("would overflow"));
271    }
272
273    #[test]
274    fn safe_array_bound_add_overflow_message() {
275        let err = safe_array_bound(usize::MAX, 1, 1, "ctx").unwrap_err();
276        assert!(err.message.contains("Array offset overflow"));
277        assert!(err.message.contains("would overflow"));
278    }
279
280    #[test]
281    fn safe_u64_to_u32_midrange() {
282        assert_eq!(safe_u64_to_u32(1_000_000, "mid").unwrap(), 1_000_000);
283    }
284
285    #[test]
286    fn safe_u64_to_u32_error_message_contains_context() {
287        let err = safe_u64_to_u32(u64::from(u32::MAX) + 1, "my-ctx").unwrap_err();
288        assert!(err.message.contains("my-ctx"));
289        assert!(err.message.contains("exceeds u32::MAX"));
290    }
291
292    #[test]
293    fn safe_u64_to_u16_midrange() {
294        assert_eq!(safe_u64_to_u16(30_000, "mid").unwrap(), 30_000);
295    }
296
297    #[test]
298    fn safe_u64_to_u16_error_message_contains_context() {
299        let err = safe_u64_to_u16(70_000, "my-ctx").unwrap_err();
300        assert!(err.message.contains("my-ctx"));
301        assert!(err.message.contains("exceeds u16::MAX"));
302    }
303
304    #[test]
305    fn safe_u64_to_u16_at_u32_max() {
306        let err = safe_u64_to_u16(u64::from(u32::MAX), "u32-max").unwrap_err();
307        assert_eq!(err.code, ErrorCode::CBKS141_RECORD_TOO_LARGE);
308    }
309
310    #[test]
311    fn safe_usize_to_u32_midrange() {
312        assert_eq!(safe_usize_to_u32(50_000, "mid").unwrap(), 50_000);
313    }
314
315    #[test]
316    fn safe_usize_to_u32_error_message_contains_context() {
317        #[cfg(target_pointer_width = "64")]
318        {
319            let err = safe_usize_to_u32(u32::MAX as usize + 1, "my-ctx").unwrap_err();
320            assert!(err.message.contains("my-ctx"));
321            assert!(err.message.contains("exceeds u32::MAX"));
322        }
323    }
324
325    #[test]
326    fn safe_u64_to_u32_one() {
327        assert_eq!(safe_u64_to_u32(1, "one").unwrap(), 1);
328    }
329
330    #[test]
331    fn safe_u64_to_u16_one() {
332        assert_eq!(safe_u64_to_u16(1, "one").unwrap(), 1);
333    }
334
335    #[test]
336    fn safe_usize_to_u32_one() {
337        assert_eq!(safe_usize_to_u32(1, "one").unwrap(), 1);
338    }
339
340    #[test]
341    fn safe_array_bound_single_element() {
342        assert_eq!(safe_array_bound(0, 1, 1, "single").unwrap(), 1);
343    }
344
345    #[test]
346    fn safe_array_bound_base_at_max_minus_product() {
347        // Exactly at limit: usize::MAX - 10 + 10 = usize::MAX
348        assert_eq!(
349            safe_array_bound(usize::MAX - 10, 1, 10, "exact").unwrap(),
350            usize::MAX
351        );
352    }
353}