Skip to main content

copybook_codec/numeric/
binary.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Binary integer codecs for COBOL COMP/BINARY fields.
3
4use copybook_core::{Error, ErrorCode, Result};
5
6/// Decode binary integer field (COMP-4, COMP-5, BINARY)
7///
8/// Decodes big-endian binary integer fields. Supports 16-bit, 32-bit, and 64-bit
9/// widths. All binary integers use big-endian byte order as per COBOL specification.
10///
11/// # Arguments
12/// * `data` - Raw byte data containing the binary integer
13/// * `bits` - Bit width of the field (16, 32, or 64)
14/// * `signed` - Whether the field is signed (true) or unsigned (false)
15///
16/// # Returns
17/// The decoded integer value as `i64`
18///
19/// # Errors
20/// Returns an error if the binary data is invalid or the field size is unsupported.
21///
22/// # Examples
23///
24/// ## 16-bit Signed Integer
25///
26/// ```no_run
27/// use copybook_codec::numeric::{decode_binary_int};
28///
29/// // 16-bit signed: -12345 = [0xCF, 0xC7] (big-endian)
30/// let data = [0xCF, 0xC7];
31/// let result = decode_binary_int(&data, 16, true)?;
32/// assert_eq!(result, -12345);
33/// # Ok::<(), copybook_core::Error>(())
34/// ```
35///
36/// ## 16-bit Unsigned Integer
37///
38/// ```no_run
39/// use copybook_codec::numeric::{decode_binary_int};
40///
41/// // 16-bit unsigned: 54321 = [0xD4, 0x31]
42/// let data = [0xD4, 0x31];
43/// let result = decode_binary_int(&data, 16, false)?;
44/// assert_eq!(result, 54321);
45/// # Ok::<(), copybook_core::Error>(())
46/// ```
47///
48/// ## 32-bit Signed Integer
49///
50/// ```no_run
51/// use copybook_codec::numeric::{decode_binary_int};
52///
53/// // 32-bit signed: -987654321 = [0xC5, 0x7D, 0x3C, 0x21]
54/// let data = [0xC5, 0x7D, 0x3C, 0x21];
55/// let result = decode_binary_int(&data, 32, true)?;
56/// assert_eq!(result, -987654321);
57/// # Ok::<(), copybook_core::Error>(())
58/// ```
59///
60/// ## 64-bit Signed Integer
61///
62/// ```no_run
63/// use copybook_codec::numeric::{decode_binary_int};
64///
65/// // 64-bit signed: 9223372036854775807 = [0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07]
66/// let data = [0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07];
67/// let result = decode_binary_int(&data, 64, true)?;
68/// assert_eq!(result, 9223372036854775807);
69/// # Ok::<(), copybook_core::Error>(())
70/// ```
71///
72/// # See Also
73/// * [`encode_binary_int`] - For encoding binary integers
74/// * [`get_binary_width_from_digits`] - For mapping digit count to width
75/// * [`validate_explicit_binary_width`] - For validating explicit BINARY(n) widths
76#[inline]
77#[must_use = "Handle the Result or propagate the error"]
78pub fn decode_binary_int(data: &[u8], bits: u16, signed: bool) -> Result<i64> {
79    let expected_bytes = usize::from(bits / 8);
80    if data.len() != expected_bytes {
81        return Err(Error::new(
82            ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, // Reusing error code for binary validation
83            format!(
84                "Binary data length {} doesn't match expected {} bytes for {} bits",
85                data.len(),
86                expected_bytes,
87                bits
88            ),
89        ));
90    }
91
92    match bits {
93        16 => {
94            if data.len() != 2 {
95                return Err(Error::new(
96                    ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
97                    "16-bit binary field requires exactly 2 bytes",
98                ));
99            }
100            let value = u16::from_be_bytes([data[0], data[1]]);
101            if signed {
102                Ok(i64::from(i16::from_be_bytes([data[0], data[1]])))
103            } else {
104                Ok(i64::from(value))
105            }
106        }
107        32 => {
108            if data.len() != 4 {
109                return Err(Error::new(
110                    ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
111                    "32-bit binary field requires exactly 4 bytes",
112                ));
113            }
114            let value = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
115            if signed {
116                Ok(i64::from(i32::from_be_bytes([
117                    data[0], data[1], data[2], data[3],
118                ])))
119            } else {
120                Ok(i64::from(value))
121            }
122        }
123        64 => {
124            if data.len() != 8 {
125                return Err(Error::new(
126                    ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
127                    "64-bit binary field requires exactly 8 bytes",
128                ));
129            }
130            let bytes: [u8; 8] = data.try_into().map_err(|_| {
131                Error::new(
132                    ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
133                    "Failed to convert data to 8-byte array",
134                )
135            })?;
136            if signed {
137                Ok(i64::from_be_bytes(bytes))
138            } else {
139                // For unsigned 64-bit, we need to be careful about overflow
140                let value = u64::from_be_bytes(bytes);
141                let max_i64 = u64::try_from(i64::MAX).unwrap_or(u64::MAX);
142                if value > max_i64 {
143                    return Err(Error::new(
144                        ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
145                        format!("Unsigned 64-bit value {value} exceeds i64::MAX"),
146                    ));
147                }
148                i64::try_from(value).map_err(|_| {
149                    Error::new(
150                        ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
151                        format!("Unsigned 64-bit value {value} exceeds i64::MAX"),
152                    )
153                })
154            }
155        }
156        _ => Err(Error::new(
157            ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
158            format!("Unsupported binary field width: {bits} bits"),
159        )),
160    }
161}
162
163/// Encode binary integer field (COMP-4, COMP-5, BINARY)
164///
165/// Encodes integer values to big-endian binary format. Supports 16-bit, 32-bit, and 64-bit
166/// widths. All binary integers use big-endian byte order as per COBOL specification.
167///
168/// # Arguments
169/// * `value` - Integer value to encode
170/// * `bits` - Bit width of field (16, 32, or 64)
171/// * `signed` - Whether the field is signed (true) or unsigned (false)
172///
173/// # Returns
174/// A vector of bytes containing the encoded binary integer
175///
176/// # Errors
177/// Returns an error if the value is out of range for the specified bit width.
178///
179/// # Examples
180///
181/// ## 16-bit Signed Integer
182///
183/// ```no_run
184/// use copybook_codec::numeric::{encode_binary_int};
185///
186/// // Encode -12345 as 16-bit signed
187/// let encoded = encode_binary_int(-12345, 16, true)?;
188/// assert_eq!(encoded, [0xCF, 0xC7]); // Big-endian
189/// # Ok::<(), copybook_core::Error>(())
190/// ```
191///
192/// ## 16-bit Unsigned Integer
193///
194/// ```no_run
195/// use copybook_codec::numeric::{encode_binary_int};
196///
197/// // Encode 54321 as 16-bit unsigned
198/// let encoded = encode_binary_int(54321, 16, false)?;
199/// assert_eq!(encoded, [0xD4, 0x31]);
200/// # Ok::<(), copybook_core::Error>(())
201/// ```
202///
203/// ## 32-bit Signed Integer
204///
205/// ```no_run
206/// use copybook_codec::numeric::{encode_binary_int};
207///
208/// // Encode -987654321 as 32-bit signed
209/// let encoded = encode_binary_int(-987654321, 32, true)?;
210/// assert_eq!(encoded, [0xC5, 0x7D, 0x3C, 0x21]);
211/// # Ok::<(), copybook_core::Error>(())
212/// ```
213///
214/// ## 64-bit Signed Integer
215///
216/// ```no_run
217/// use copybook_codec::numeric::{encode_binary_int};
218///
219/// // Encode 9223372036854775807 as 64-bit signed
220/// let encoded = encode_binary_int(9223372036854775807, 64, true)?;
221/// assert_eq!(encoded, [0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07]);
222/// # Ok::<(), copybook_core::Error>(())
223/// ```
224///
225/// # See Also
226/// * [`decode_binary_int`] - For decoding binary integers
227/// * [`get_binary_width_from_digits`] - For mapping digit count to width
228/// * [`validate_explicit_binary_width`] - For validating explicit BINARY(n) widths
229#[inline]
230#[must_use = "Handle the Result or propagate the error"]
231pub fn encode_binary_int(value: i64, bits: u16, signed: bool) -> Result<Vec<u8>> {
232    match bits {
233        16 => {
234            if signed {
235                let int_value = i16::try_from(value).map_err(|_| {
236                    Error::new(
237                        ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
238                        format!("Value {value} out of range for signed 16-bit integer"),
239                    )
240                })?;
241                Ok(int_value.to_be_bytes().to_vec())
242            } else {
243                let int_value = u16::try_from(value).map_err(|_| {
244                    Error::new(
245                        ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
246                        format!("Value {value} out of range for unsigned 16-bit integer"),
247                    )
248                })?;
249                Ok(int_value.to_be_bytes().to_vec())
250            }
251        }
252        32 => {
253            if signed {
254                let int_value = i32::try_from(value).map_err(|_| {
255                    Error::new(
256                        ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
257                        format!("Value {value} out of range for signed 32-bit integer"),
258                    )
259                })?;
260                Ok(int_value.to_be_bytes().to_vec())
261            } else {
262                let int_value = u32::try_from(value).map_err(|_| {
263                    Error::new(
264                        ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
265                        format!("Value {value} out of range for unsigned 32-bit integer"),
266                    )
267                })?;
268                Ok(int_value.to_be_bytes().to_vec())
269            }
270        }
271        64 => {
272            if signed {
273                Ok(value.to_be_bytes().to_vec())
274            } else {
275                let int_value = u64::try_from(value).map_err(|_| {
276                    Error::new(
277                        ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
278                        format!("Value {value} cannot be negative for unsigned 64-bit integer"),
279                    )
280                })?;
281                Ok(int_value.to_be_bytes().to_vec())
282            }
283        }
284        _ => Err(Error::new(
285            ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
286            format!("Unsupported binary field width: {bits} bits"),
287        )),
288    }
289}
290
291/// Map a COBOL PIC digit count to the corresponding USAGE BINARY storage width
292/// in bits (NORMATIVE).
293///
294/// Follows the COBOL standard mapping:
295/// - 1-4 digits  -> 16 bits (2 bytes, halfword)
296/// - 5-9 digits  -> 32 bits (4 bytes, fullword)
297/// - 10-18 digits -> 64 bits (8 bytes, doubleword)
298///
299/// # Arguments
300/// * `digits` - Number of digit positions declared in the PIC clause
301///
302/// # Returns
303/// The storage width in bits: 16, 32, or 64.
304///
305/// # See Also
306/// * [`validate_explicit_binary_width`] - For explicit `USAGE BINARY(n)` declarations
307/// * [`decode_binary_int`] - Decodes binary integer data at the determined width
308#[inline]
309#[must_use]
310pub fn get_binary_width_from_digits(digits: u16) -> u16 {
311    match digits {
312        1..=4 => 16, // 2 bytes
313        5..=9 => 32, // 4 bytes
314        _ => 64,     // 8 bytes for larger values
315    }
316}
317
318/// Validate an explicit `USAGE BINARY(n)` byte-width declaration and return
319/// the equivalent bit width (NORMATIVE).
320///
321/// Only the widths 1, 2, 4, and 8 bytes are valid.  Each is converted to
322/// the corresponding bit count (8, 16, 32, 64) for downstream codec use.
323///
324/// # Arguments
325/// * `width_bytes` - The explicit byte width from the copybook (1, 2, 4, or 8)
326///
327/// # Returns
328/// The equivalent width in bits: 8, 16, 32, or 64.
329///
330/// # Errors
331/// * `CBKE501_JSON_TYPE_MISMATCH` - if `width_bytes` is not 1, 2, 4, or 8
332///
333/// # See Also
334/// * [`get_binary_width_from_digits`] - Infers width from PIC digit count
335#[inline]
336#[must_use = "Handle the Result or propagate the error"]
337pub fn validate_explicit_binary_width(width_bytes: u8) -> Result<u16> {
338    match width_bytes {
339        1 => Ok(8),  // 1 byte = 8 bits
340        2 => Ok(16), // 2 bytes = 16 bits
341        4 => Ok(32), // 4 bytes = 32 bits
342        8 => Ok(64), // 8 bytes = 64 bits
343        _ => Err(Error::new(
344            ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
345            format!("Invalid explicit binary width: {width_bytes} bytes. Must be 1, 2, 4, or 8"),
346        )),
347    }
348}
349
350/// Decode a COBOL binary integer (USAGE BINARY / COMP) with optimized fast
351/// paths for 16-, 32-, and 64-bit widths.
352///
353/// For the three common widths this function reads the big-endian bytes
354/// directly into the native integer type, avoiding the generic loop in
355/// [`decode_binary_int`].  Uncommon widths fall back to that generic
356/// implementation.
357///
358/// # Arguments
359/// * `data` - Raw big-endian byte data containing the binary integer
360/// * `bits` - Expected field width in bits (16, 32, or 64 for fast path)
361/// * `signed` - Whether the field is signed (PIC S9 COMP) or unsigned (PIC 9 COMP)
362///
363/// # Returns
364/// The decoded integer value as `i64`.
365///
366/// # Errors
367/// * `CBKD401_COMP3_INVALID_NIBBLE` - if an unsigned 64-bit value exceeds `i64::MAX`
368/// * Delegates to [`decode_binary_int`] for unsupported widths, which may
369///   return its own errors.
370///
371/// # See Also
372/// * [`decode_binary_int`] - General-purpose binary integer decoder
373/// * [`encode_binary_int`] - Binary integer encoder
374/// * [`super::format_binary_int_to_string_with_scratch`] - Formats the decoded value to a string
375#[inline]
376#[must_use = "Handle the Result or propagate the error"]
377pub fn decode_binary_int_fast(data: &[u8], bits: u16, signed: bool) -> Result<i64> {
378    // Optimized paths for common binary widths
379    match (bits, data.len()) {
380        (16, 2) => {
381            // 16-bit integer - most common case
382            let bytes = [data[0], data[1]];
383            if signed {
384                Ok(i64::from(i16::from_be_bytes(bytes)))
385            } else {
386                Ok(i64::from(u16::from_be_bytes(bytes)))
387            }
388        }
389        (32, 4) => {
390            // 32-bit integer - common case
391            let bytes = [data[0], data[1], data[2], data[3]];
392            if signed {
393                Ok(i64::from(i32::from_be_bytes(bytes)))
394            } else {
395                Ok(i64::from(u32::from_be_bytes(bytes)))
396            }
397        }
398        (64, 8) => {
399            // 64-bit integer
400            let bytes = [
401                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
402            ];
403            if signed {
404                Ok(i64::from_be_bytes(bytes))
405            } else {
406                let value = u64::from_be_bytes(bytes);
407                let max_i64 = u64::try_from(i64::MAX).unwrap_or(u64::MAX);
408                if value > max_i64 {
409                    return Err(Error::new(
410                        ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
411                        format!("Unsigned 64-bit value {value} exceeds i64::MAX"),
412                    ));
413                }
414                i64::try_from(value).map_err(|_| {
415                    Error::new(
416                        ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
417                        format!("Unsigned 64-bit value {value} exceeds i64::MAX"),
418                    )
419                })
420            }
421        }
422        _ => {
423            // Fallback to general implementation
424            decode_binary_int(data, bits, signed)
425        }
426    }
427}