Skip to main content

copybook_safe_ops/
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 conversion and arithmetic helper functions.
4//!
5//! This crate centralizes arithmetic-focused helpers and re-exports text-oriented
6//! panic-safe operations from `copybook_safe_text`.
7
8use copybook_error::Error;
9pub use copybook_safe_index::{safe_divide, safe_slice_get};
10pub use copybook_safe_text::{
11    parse_isize, parse_usize, safe_parse_u16, safe_string_char_at, safe_write, safe_write_str,
12};
13
14/// Result type alias using `copybook-error`.
15pub type Result<T> = std::result::Result<T, Error>;
16
17/// Safely calculate COBOL array bounds with overflow protection.
18///
19/// # Errors
20///
21/// Returns an error if the computation overflows.
22#[inline]
23#[must_use = "Handle the Result or propagate the error"]
24pub fn safe_array_bound(
25    base: usize,
26    count: usize,
27    item_size: usize,
28    context: &str,
29) -> Result<usize> {
30    copybook_overflow::safe_array_bound(base, count, item_size, context)
31}
32
33/// Safely convert `u64` to `u32` with overflow checking.
34///
35/// # Errors
36///
37/// Returns an error if `value` exceeds `u32::MAX`.
38#[inline]
39#[must_use = "Handle the Result or propagate the error"]
40pub fn safe_u64_to_u32(value: u64, context: &str) -> Result<u32> {
41    copybook_overflow::safe_u64_to_u32(value, context)
42}
43
44/// Safely convert `u64` to `u16` with overflow checking.
45///
46/// # Errors
47///
48/// Returns an error if `value` exceeds `u16::MAX`.
49#[inline]
50#[must_use = "Handle the Result or propagate the error"]
51pub fn safe_u64_to_u16(value: u64, context: &str) -> Result<u16> {
52    copybook_overflow::safe_u64_to_u16(value, context)
53}
54
55/// Safely convert `usize` to `u32` with overflow checking.
56///
57/// # Errors
58///
59/// Returns an error if `value` exceeds `u32::MAX`.
60#[inline]
61#[must_use = "Handle the Result or propagate the error"]
62pub fn safe_usize_to_u32(value: usize, context: &str) -> Result<u32> {
63    copybook_overflow::safe_usize_to_u32(value, context)
64}
65
66#[cfg(test)]
67#[allow(clippy::expect_used, clippy::unwrap_used)]
68mod tests {
69    use super::*;
70    use copybook_error::ErrorCode;
71    use proptest::prelude::*;
72
73    #[test]
74    fn safe_parse_usize() {
75        assert_eq!(parse_usize("123", "test").expect("parse usize"), 123);
76    }
77
78    #[test]
79    fn safe_parse_usize_invalid() {
80        assert!(matches!(
81            parse_usize("invalid", "test"),
82            Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
83        ));
84    }
85
86    #[test]
87    fn safe_parse_isize() {
88        assert_eq!(parse_isize("-42", "test").expect("parse isize"), -42);
89    }
90
91    #[test]
92    fn safe_divide_returns_quotient_or_syntax_error() {
93        assert_eq!(safe_divide(10, 2, "test").expect("divide"), 5);
94        assert!(matches!(
95            safe_divide(10, 0, "test"),
96            Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
97        ));
98    }
99
100    #[test]
101    fn safe_array_bound_behavior() {
102        assert_eq!(safe_array_bound(10, 3, 4, "test").expect("array bound"), 22);
103
104        assert!(matches!(
105            safe_array_bound(0, usize::MAX, 2, "overflow"),
106            Err(error) if error.code == ErrorCode::CBKP021_ODO_NOT_TAIL
107        ));
108    }
109
110    #[test]
111    fn safe_parse_u16_valid_and_invalid() {
112        assert_eq!(safe_parse_u16("42", "test").expect("parse u16"), 42);
113        assert!(matches!(
114            safe_parse_u16("99999", "test"),
115            Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
116        ));
117    }
118
119    #[test]
120    fn safe_string_char_at_behavior() {
121        assert_eq!(
122            safe_string_char_at("abc", 1, "test").expect("char index"),
123            'b'
124        );
125        assert!(matches!(
126            safe_string_char_at("abc", 3, "test"),
127            Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
128        ));
129    }
130
131    proptest! {
132        #[test]
133        fn safe_parse_usize_round_trip(value in 0u64..1_000_000u64) {
134            let value = value as usize;
135            let text = value.to_string();
136            let parsed = parse_usize(&text, "prop").expect("roundtrip parse");
137            prop_assert_eq!(parsed, value);
138        }
139
140        #[test]
141        fn safe_slice_get_round_trip(value in 1u32..200u32) {
142            let vec: Vec<u32> = (0..value).collect();
143            let index = (value as usize) / 2;
144            let got = safe_slice_get(&vec, index, "prop").expect("slice index");
145            prop_assert_eq!(got, index as u32);
146        }
147
148        #[test]
149        fn safe_array_bound_matches_reference(
150            base in 0u32..1000u32,
151            count in 0u32..1000u32,
152            item in 1u32..100u32,
153        ) {
154            let base = base as usize;
155            let count = count as usize;
156            let item = item as usize;
157
158            let expected = (count as u128)
159                .checked_mul(item as u128)
160                .and_then(|total| (base as u128).checked_add(total));
161
162            match expected.and_then(|total| usize::try_from(total).ok()) {
163                Some(expected) => {
164                    prop_assert_eq!(safe_array_bound(base, count, item, "prop").expect("bounded"), expected);
165                }
166                None => {
167                    prop_assert!(safe_array_bound(base, count, item, "prop").is_err());
168                }
169            }
170        }
171    }
172
173    proptest! {
174        #[test]
175        fn safe_u64_to_u32_round_trip(value in 0u64..=u32::MAX as u64) {
176            prop_assert_eq!(
177                safe_u64_to_u32(value, "prop").expect("u64->u32"),
178                value as u32
179            );
180        }
181
182        #[test]
183        fn safe_u64_to_u16_round_trip(value in 0u64..=u16::MAX as u64) {
184            prop_assert_eq!(
185                safe_u64_to_u16(value, "prop").expect("u64->u16"),
186                value as u16
187            );
188        }
189    }
190}