copybook_codec/file/
fixed.rs1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2use copybook_core::Schema;
10use copybook_error::{Error, ErrorCode, ErrorContext, Result};
11use copybook_fixed::FixedRecordReader;
12use std::convert::TryFrom;
13use std::io::Read;
14use tracing::{debug, warn};
15
16pub(crate) const FIXED_FORMAT_LRECL_MISSING: &str = "Fixed format requires a fixed record length (LRECL). Set schema.lrecl_fixed or use RecordFormat::Variable.";
17
18#[inline]
24#[must_use = "Handle the Result or propagate the error"]
25pub fn lrecl(schema: &Schema) -> Result<u32> {
26 let lrecl = schema
27 .lrecl_fixed
28 .ok_or_else(|| Error::new(ErrorCode::CBKI001_INVALID_STATE, FIXED_FORMAT_LRECL_MISSING))?;
29
30 if lrecl == 0 {
31 return Err(Error::new(
32 ErrorCode::CBKI001_INVALID_STATE,
33 "LRECL must be greater than zero",
34 ));
35 }
36
37 Ok(lrecl)
38}
39
40#[inline]
48#[must_use = "Handle the Result or propagate the error"]
49pub fn reader<R: Read>(input: R, schema: &Schema) -> Result<FixedRecordReader<R>> {
50 FixedRecordReader::with_lrecl(input, lrecl(schema)?)
51}
52
53#[inline]
62#[must_use = "Handle the Result or propagate the error"]
63pub fn validate_record_length(
64 schema: &Schema,
65 configured_lrecl: u32,
66 record_index: u64,
67 record_data: &[u8],
68) -> Result<()> {
69 let lrecl_len = usize::try_from(configured_lrecl).map_err(|_| {
70 Error::new(
71 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
72 "LRECL exceeds platform addressable size",
73 )
74 })?;
75
76 if record_data.len() != lrecl_len {
77 return Err(Error::new(
78 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
79 format!(
80 "Record length mismatch: expected {}, got {}",
81 configured_lrecl,
82 record_data.len()
83 ),
84 )
85 .with_context(ErrorContext {
86 record_index: Some(record_index),
87 field_path: None,
88 byte_offset: None,
89 line_number: None,
90 details: Some("Fixed record length validation failed".to_string()),
91 }));
92 }
93
94 if let Some(schema_lrecl) = schema.lrecl_fixed
95 && configured_lrecl != schema_lrecl
96 {
97 warn!(
98 "LRECL mismatch: codec configured for {}, schema expects {}",
99 configured_lrecl, schema_lrecl
100 );
101 }
102
103 if schema.tail_odo.is_some() {
104 debug!("Record has ODO tail, variable length within fixed LRECL is expected");
105 }
106
107 Ok(())
108}
109
110#[cfg(test)]
111#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
112mod tests {
113 use super::*;
114 use std::io::Cursor;
115
116 #[test]
117 fn resolves_schema_lrecl_at_codec_boundary() {
118 let mut schema = Schema::new();
119 schema.lrecl_fixed = Some(8);
120
121 assert_eq!(lrecl(&schema).unwrap(), 8);
122 let mut reader = reader(Cursor::new(b"ABCDEFGH"), &schema).unwrap();
123 assert_eq!(reader.read_record().unwrap().unwrap(), b"ABCDEFGH");
124 }
125
126 #[test]
127 fn rejects_schema_without_fixed_lrecl_at_codec_boundary() {
128 let schema = Schema::new();
129
130 let error = lrecl(&schema).unwrap_err();
131 assert_eq!(error.code, ErrorCode::CBKI001_INVALID_STATE);
132 }
133
134 #[test]
135 fn rejects_zero_schema_lrecl_at_codec_boundary() {
136 let mut schema = Schema::new();
137 schema.lrecl_fixed = Some(0);
138
139 let error = lrecl(&schema).unwrap_err();
140 assert_eq!(error.code, ErrorCode::CBKI001_INVALID_STATE);
141 assert_eq!(error.message, "LRECL must be greater than zero");
142 }
143
144 #[test]
145 fn validates_record_length_at_codec_boundary() {
146 let mut schema = Schema::new();
147 schema.lrecl_fixed = Some(8);
148
149 validate_record_length(&schema, 8, 1, b"ABCDEFGH").unwrap();
150 let error = validate_record_length(&schema, 8, 2, b"SHORT").unwrap_err();
151 assert_eq!(error.code, ErrorCode::CBKR101_FIXED_RECORD_ERROR);
152 }
153}