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}