1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2use copybook_error::{Error, ErrorCode, ErrorContext, Result};
13use std::convert::TryFrom;
14use std::io::{ErrorKind, Read, Write};
15use tracing::debug;
16
17#[derive(Debug)]
19pub struct FixedRecordReader<R: Read> {
20 input: R,
21 lrecl: u32,
22 record_count: u64,
23}
24
25impl<R: Read> FixedRecordReader<R> {
26 #[inline]
35 #[must_use = "Handle the Result or propagate the error"]
36 pub fn with_lrecl(input: R, lrecl: u32) -> Result<Self> {
37 Self::new(input, Some(lrecl))
38 }
39
40 #[inline]
45 #[must_use = "Handle the Result or propagate the error"]
46 pub fn new(input: R, lrecl: Option<u32>) -> Result<Self> {
47 let lrecl = lrecl.ok_or_else(|| {
48 Error::new(
49 ErrorCode::CBKI001_INVALID_STATE,
50 "Fixed format requires LRECL",
51 )
52 })?;
53
54 if lrecl == 0 {
55 return Err(Error::new(
56 ErrorCode::CBKI001_INVALID_STATE,
57 "LRECL must be greater than zero",
58 ));
59 }
60
61 Ok(Self {
62 input,
63 lrecl,
64 record_count: 0,
65 })
66 }
67
68 #[inline]
73 #[must_use = "Handle the Result or propagate the error"]
74 pub fn read_record(&mut self) -> Result<Option<Vec<u8>>> {
75 let mut first_byte = [0u8; 1];
77 match self.input.read_exact(&mut first_byte) {
78 Ok(()) => {
79 let lrecl_len = self.lrecl_usize()?;
80 let mut buffer = vec![0u8; lrecl_len];
81 buffer[0] = first_byte[0];
82
83 if lrecl_len > 1 {
84 match self.input.read_exact(&mut buffer[1..]) {
85 Ok(()) => {
86 self.record_count += 1;
87 debug!(
88 "Read fixed record {} of {} bytes",
89 self.record_count, self.lrecl
90 );
91 Ok(Some(buffer))
92 }
93 Err(e) if e.kind() == ErrorKind::UnexpectedEof => Err(Error::new(
94 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
95 format!(
96 "Incomplete record at end of file: expected {} bytes",
97 self.lrecl
98 ),
99 )
100 .with_context(ErrorContext {
101 record_index: Some(self.record_count + 1),
102 field_path: None,
103 byte_offset: None,
104 line_number: None,
105 details: Some("File ends with partial record".to_string()),
106 })),
107 Err(e) => Err(Error::new(
108 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
109 format!("I/O error reading record: {e}"),
110 )
111 .with_context(ErrorContext {
112 record_index: Some(self.record_count + 1),
113 field_path: None,
114 byte_offset: None,
115 line_number: None,
116 details: None,
117 })),
118 }
119 } else {
120 self.record_count += 1;
121 debug!(
122 "Read fixed record {} of {} bytes",
123 self.record_count, self.lrecl
124 );
125 Ok(Some(buffer))
126 }
127 }
128 Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
129 debug!("Reached EOF after {} records", self.record_count);
130 Ok(None)
131 }
132 Err(e) => Err(Error::new(
133 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
134 format!("I/O error reading record: {e}"),
135 )
136 .with_context(ErrorContext {
137 record_index: Some(self.record_count + 1),
138 field_path: None,
139 byte_offset: None,
140 line_number: None,
141 details: None,
142 })),
143 }
144 }
145
146 #[must_use]
148 #[inline]
149 pub fn record_count(&self) -> u64 {
150 self.record_count
151 }
152
153 #[must_use]
155 #[inline]
156 pub fn lrecl(&self) -> u32 {
157 self.lrecl
158 }
159
160 #[inline]
161 fn lrecl_usize(&self) -> Result<usize> {
162 usize::try_from(self.lrecl).map_err(|_| {
163 Error::new(
164 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
165 "LRECL exceeds platform addressable size",
166 )
167 })
168 }
169}
170
171#[derive(Debug)]
173pub struct FixedRecordWriter<W: Write> {
174 output: W,
175 lrecl: u32,
176 record_count: u64,
177}
178
179impl<W: Write> FixedRecordWriter<W> {
180 #[inline]
189 #[must_use = "Handle the Result or propagate the error"]
190 pub fn with_lrecl(output: W, lrecl: u32) -> Result<Self> {
191 Self::new(output, Some(lrecl))
192 }
193
194 #[inline]
199 #[must_use = "Handle the Result or propagate the error"]
200 pub fn new(output: W, lrecl: Option<u32>) -> Result<Self> {
201 let lrecl = lrecl.ok_or_else(|| {
202 Error::new(
203 ErrorCode::CBKI001_INVALID_STATE,
204 "Fixed format requires LRECL",
205 )
206 })?;
207
208 if lrecl == 0 {
209 return Err(Error::new(
210 ErrorCode::CBKI001_INVALID_STATE,
211 "LRECL must be greater than zero",
212 ));
213 }
214
215 Ok(Self {
216 output,
217 lrecl,
218 record_count: 0,
219 })
220 }
221
222 #[inline]
227 #[must_use = "Handle the Result or propagate the error"]
228 pub fn write_record(&mut self, data: &[u8]) -> Result<()> {
229 let data_len = data.len();
230 let lrecl = self.lrecl_usize()?;
231
232 if data_len > lrecl {
233 return Err(Error::new(
234 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
235 format!("Record too long: {data_len} bytes exceeds LRECL of {lrecl}"),
236 )
237 .with_context(ErrorContext {
238 record_index: Some(self.record_count + 1),
239 field_path: None,
240 byte_offset: None,
241 line_number: None,
242 details: Some("Record exceeds fixed length".to_string()),
243 }));
244 }
245
246 self.output.write_all(data).map_err(|e| {
247 Error::new(
248 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
249 format!("I/O error writing record: {e}"),
250 )
251 .with_context(ErrorContext {
252 record_index: Some(self.record_count + 1),
253 field_path: None,
254 byte_offset: None,
255 line_number: None,
256 details: None,
257 })
258 })?;
259
260 if data_len < lrecl {
261 let padding = vec![0u8; lrecl - data_len];
262 self.output.write_all(&padding).map_err(|e| {
263 Error::new(
264 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
265 format!("I/O error writing padding: {e}"),
266 )
267 .with_context(ErrorContext {
268 record_index: Some(self.record_count + 1),
269 field_path: None,
270 byte_offset: Some(u64::try_from(data_len).unwrap_or(u64::MAX)),
271 line_number: None,
272 details: Some("Error writing record padding".to_string()),
273 })
274 })?;
275 }
276
277 self.record_count += 1;
278 debug!(
279 "Wrote fixed record {} of {} bytes (data: {}, padding: {})",
280 self.record_count,
281 lrecl,
282 data_len,
283 lrecl - data_len
284 );
285 Ok(())
286 }
287
288 #[inline]
293 #[must_use = "Handle the Result or propagate the error"]
294 pub fn flush(&mut self) -> Result<()> {
295 self.output.flush().map_err(|e| {
296 Error::new(
297 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
298 format!("I/O error flushing output: {e}"),
299 )
300 })
301 }
302
303 #[must_use]
305 #[inline]
306 pub fn record_count(&self) -> u64 {
307 self.record_count
308 }
309
310 #[must_use]
312 #[inline]
313 pub fn lrecl(&self) -> u32 {
314 self.lrecl
315 }
316
317 #[inline]
318 fn lrecl_usize(&self) -> Result<usize> {
319 usize::try_from(self.lrecl).map_err(|_| {
320 Error::new(
321 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
322 "LRECL exceeds platform addressable size",
323 )
324 })
325 }
326}
327
328#[cfg(test)]
329#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
330mod tests {
331 use super::*;
332 use proptest::collection::vec;
333 use proptest::prelude::*;
334 use std::io::Cursor;
335
336 #[test]
337 fn fixed_record_reader_basic() {
338 let data = b"ABCD1234EFGH5678";
339 let mut reader = FixedRecordReader::new(Cursor::new(data), Some(8)).unwrap();
340
341 let record1 = reader.read_record().unwrap().unwrap();
342 assert_eq!(record1, b"ABCD1234");
343 assert_eq!(reader.record_count(), 1);
344
345 let record2 = reader.read_record().unwrap().unwrap();
346 assert_eq!(record2, b"EFGH5678");
347 assert_eq!(reader.record_count(), 2);
348
349 let record3 = reader.read_record().unwrap();
350 assert!(record3.is_none());
351 }
352
353 #[test]
354 fn explicit_lrecl_reader_constructor() {
355 let mut reader = FixedRecordReader::with_lrecl(Cursor::new(b"ABCD"), 4).unwrap();
356
357 assert_eq!(reader.read_record().unwrap().unwrap(), b"ABCD");
358 }
359
360 #[test]
361 fn explicit_lrecl_reader_constructor_rejects_zero() {
362 let error = FixedRecordReader::with_lrecl(Cursor::new(b"ABCD"), 0).unwrap_err();
363
364 assert_eq!(error.code, ErrorCode::CBKI001_INVALID_STATE);
365 }
366
367 #[test]
368 fn fixed_record_reader_partial_record_is_fixed_record_error() {
369 let data = b"ABCD123";
370 let mut reader = FixedRecordReader::new(Cursor::new(data), Some(8)).unwrap();
371
372 let error = reader.read_record().unwrap_err();
373 assert_eq!(error.code, ErrorCode::CBKR101_FIXED_RECORD_ERROR);
374 }
375
376 #[test]
377 fn fixed_record_reader_zero_lrecl_is_invalid_state() {
378 let data = b"test";
379 let error = FixedRecordReader::new(Cursor::new(data), Some(0)).unwrap_err();
380 assert_eq!(error.code, ErrorCode::CBKI001_INVALID_STATE);
381 }
382
383 #[test]
384 fn fixed_record_reader_missing_lrecl_is_invalid_state() {
385 let data = b"test";
386 let error = FixedRecordReader::new(Cursor::new(data), None).unwrap_err();
387 assert_eq!(error.code, ErrorCode::CBKI001_INVALID_STATE);
388 }
389
390 #[test]
391 fn fixed_record_writer_basic() {
392 let mut output = Vec::new();
393 let mut writer = FixedRecordWriter::new(&mut output, Some(8)).unwrap();
394
395 writer.write_record(b"ABCD1234").unwrap();
396 writer.write_record(b"XYZ").unwrap();
397 writer.flush().unwrap();
398
399 assert_eq!(writer.record_count(), 2);
400 assert_eq!(output, b"ABCD1234XYZ\x00\x00\x00\x00\x00");
401 }
402
403 #[test]
404 fn explicit_lrecl_writer_constructor() {
405 let mut output = Vec::new();
406 let mut writer = FixedRecordWriter::with_lrecl(&mut output, 4).unwrap();
407
408 writer.write_record(b"AB").unwrap();
409 assert_eq!(output, b"AB\x00\x00");
410 }
411
412 #[test]
413 fn explicit_lrecl_writer_constructor_rejects_zero() {
414 let mut output = Vec::new();
415 let error = FixedRecordWriter::with_lrecl(&mut output, 0).unwrap_err();
416
417 assert_eq!(error.code, ErrorCode::CBKI001_INVALID_STATE);
418 }
419
420 #[test]
421 fn fixed_record_writer_too_long_is_fixed_record_error() {
422 let mut output = Vec::new();
423 let mut writer = FixedRecordWriter::new(&mut output, Some(4)).unwrap();
424
425 let error = writer.write_record(b"ABCDEFGH").unwrap_err();
426 assert_eq!(error.code, ErrorCode::CBKR101_FIXED_RECORD_ERROR);
427 }
428
429 #[test]
430 fn fixed_record_writer_zero_lrecl_is_invalid_state() {
431 let mut output = Vec::new();
432 let error = FixedRecordWriter::new(&mut output, Some(0)).unwrap_err();
433 assert_eq!(error.code, ErrorCode::CBKI001_INVALID_STATE);
434 }
435
436 #[test]
437 fn fixed_record_writer_missing_lrecl_is_invalid_state() {
438 let mut output = Vec::new();
439 let error = FixedRecordWriter::new(&mut output, None).unwrap_err();
440 assert_eq!(error.code, ErrorCode::CBKI001_INVALID_STATE);
441 }
442
443 proptest! {
444 #[test]
445 fn prop_fixed_writer_reader_roundtrip(
446 lrecl in 1u16..=512u16,
447 payload in vec(any::<u8>(), 0..=512),
448 ) {
449 prop_assume!(payload.len() <= usize::from(lrecl));
450 let mut encoded = Vec::new();
451 let mut writer = FixedRecordWriter::new(&mut encoded, Some(u32::from(lrecl))).unwrap();
452 writer.write_record(&payload).unwrap();
453 writer.flush().unwrap();
454 prop_assert_eq!(encoded.len(), usize::from(lrecl));
455 prop_assert_eq!(&encoded[..payload.len()], payload.as_slice());
456
457 let mut reader = FixedRecordReader::new(Cursor::new(&encoded), Some(u32::from(lrecl))).unwrap();
458 let decoded = reader.read_record().unwrap().unwrap();
459 prop_assert_eq!(decoded, encoded.as_slice());
460 prop_assert_eq!(reader.read_record().unwrap(), None);
461 }
462
463 #[test]
464 fn prop_fixed_writer_rejects_oversize_payload(
465 lrecl in 1u16..=128u16,
466 extra in 1usize..=64usize,
467 ) {
468 let mut output = Vec::new();
469 let mut writer = FixedRecordWriter::new(&mut output, Some(u32::from(lrecl))).unwrap();
470 let payload = vec![0x41; usize::from(lrecl) + extra];
471 let error = writer.write_record(&payload).unwrap_err();
472 prop_assert_eq!(error.code, ErrorCode::CBKR101_FIXED_RECORD_ERROR);
473 }
474 }
475
476 #[test]
479 fn fixed_reader_empty_file_returns_none() {
480 let mut reader = FixedRecordReader::new(Cursor::new(Vec::<u8>::new()), Some(8)).unwrap();
481 assert!(reader.read_record().unwrap().is_none());
482 assert_eq!(reader.record_count(), 0);
483 }
484
485 #[test]
486 fn fixed_reader_single_byte_lrecl() {
487 let data = b"ABCDE";
488 let mut reader = FixedRecordReader::new(Cursor::new(data.as_slice()), Some(1)).unwrap();
489 for expected in b"ABCDE" {
490 let record = reader.read_record().unwrap().unwrap();
491 assert_eq!(record, vec![*expected]);
492 }
493 assert!(reader.read_record().unwrap().is_none());
494 assert_eq!(reader.record_count(), 5);
495 }
496
497 #[test]
498 fn fixed_reader_lrecl_accessor() {
499 let reader = FixedRecordReader::new(Cursor::new(Vec::<u8>::new()), Some(42)).unwrap();
500 assert_eq!(reader.lrecl(), 42);
501 }
502
503 #[test]
504 fn fixed_writer_lrecl_accessor() {
505 let mut output = Vec::new();
506 let writer = FixedRecordWriter::new(&mut output, Some(42)).unwrap();
507 assert_eq!(writer.lrecl(), 42);
508 }
509
510 #[test]
511 fn fixed_writer_exact_lrecl_no_padding() {
512 let mut output = Vec::new();
513 let mut writer = FixedRecordWriter::new(&mut output, Some(4)).unwrap();
514 writer.write_record(b"ABCD").unwrap();
515 writer.flush().unwrap();
516 assert_eq!(output, b"ABCD");
517 }
518
519 #[test]
520 fn fixed_writer_empty_payload_full_padding() {
521 let mut output = Vec::new();
522 let mut writer = FixedRecordWriter::new(&mut output, Some(4)).unwrap();
523 writer.write_record(b"").unwrap();
524 writer.flush().unwrap();
525 assert_eq!(output, vec![0u8; 4]);
526 }
527
528 #[test]
529 fn fixed_multi_record_write_read_roundtrip() {
530 let lrecl = 10u32;
531 let payloads: Vec<&[u8]> = vec![b"AAAAAAAAAA", b"BB", b"CCCCCCCCCC"];
532 let mut encoded = Vec::new();
533 {
534 let mut writer = FixedRecordWriter::new(&mut encoded, Some(lrecl)).unwrap();
535 for p in &payloads {
536 writer.write_record(p).unwrap();
537 }
538 writer.flush().unwrap();
539 assert_eq!(writer.record_count(), 3);
540 }
541 assert_eq!(encoded.len(), 30);
542
543 let mut reader = FixedRecordReader::new(Cursor::new(&encoded), Some(lrecl)).unwrap();
544 for (i, expected) in payloads.iter().enumerate() {
545 let record = reader.read_record().unwrap().unwrap();
546 assert_eq!(
547 &record[..expected.len()],
548 *expected,
549 "record {i} data mismatch"
550 );
551 assert!(
553 record[expected.len()..].iter().all(|&b| b == 0),
554 "record {i} padding mismatch"
555 );
556 }
557 assert!(reader.read_record().unwrap().is_none());
558 assert_eq!(reader.record_count(), 3);
559 }
560
561 #[test]
562 fn fixed_streaming_many_records() {
563 let lrecl = 16u32;
564 let record_count = 500u64;
565 let payload = b"STREAMING_FIXED_";
566 assert_eq!(payload.len(), lrecl as usize);
567
568 let mut encoded = Vec::new();
569 {
570 let mut writer = FixedRecordWriter::new(&mut encoded, Some(lrecl)).unwrap();
571 for _ in 0..record_count {
572 writer.write_record(payload).unwrap();
573 }
574 writer.flush().unwrap();
575 }
576
577 let mut reader = FixedRecordReader::new(Cursor::new(&encoded), Some(lrecl)).unwrap();
578 let mut count = 0u64;
579 while let Some(record) = reader.read_record().unwrap() {
580 assert_eq!(record.as_slice(), payload.as_slice());
581 count += 1;
582 }
583 assert_eq!(count, record_count);
584 assert_eq!(reader.record_count(), record_count);
585 }
586}