1use crate::lib_api::decode_record_with_raw_data;
215use crate::options::{DecodeOptions, RecordFormat};
216use copybook_core::{Error, ErrorCode, ErrorContext, Result, Schema};
217use copybook_rdw::{RdwHeader, VbBlockReader};
218use serde_json::Value;
219use std::io::{BufReader, Read};
220
221enum BoundaryRead {
223 Complete,
225 CleanEof,
227 Partial(usize),
229}
230
231fn fill_at_record_boundary<R: Read>(
239 reader: &mut R,
240 buffer: &mut [u8],
241) -> std::io::Result<BoundaryRead> {
242 let mut filled = 0;
243 while filled < buffer.len() {
244 match reader.read(&mut buffer[filled..]) {
245 Ok(0) => break,
246 Ok(read) => filled += read,
247 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
248 Err(e) => return Err(e),
249 }
250 }
251
252 if filled == buffer.len() {
253 Ok(BoundaryRead::Complete)
254 } else if filled == 0 {
255 Ok(BoundaryRead::CleanEof)
256 } else {
257 Ok(BoundaryRead::Partial(filled))
258 }
259}
260
261#[derive(Debug)]
300enum FramingInput<R: Read> {
301 Stream(BufReader<R>),
302 Blocks(VbBlockReader<R>),
303}
304
305pub struct RecordIterator<R: Read> {
306 input: FramingInput<R>,
308 schema: Schema,
310 options: DecodeOptions,
312 record_index: u64,
314 eof_reached: bool,
316 buffer: Vec<u8>,
318 raw_data_with_header: Option<Vec<u8>>,
320}
321
322impl<R: Read> RecordIterator<R> {
323 #[inline]
334 #[must_use = "Handle the Result or propagate the error"]
335 pub fn new(reader: R, schema: &Schema, options: &DecodeOptions) -> Result<Self> {
336 let input = if options.format == RecordFormat::Vb {
337 FramingInput::Blocks(VbBlockReader::new(reader, options.strict_mode))
338 } else {
339 FramingInput::Stream(BufReader::new(reader))
340 };
341 Ok(Self {
342 input,
343 schema: schema.clone(),
344 options: options.clone(),
345 record_index: 0,
346 eof_reached: false,
347 buffer: Vec::new(),
348 raw_data_with_header: None,
349 })
350 }
351
352 #[inline]
357 #[must_use]
358 pub fn current_record_index(&self) -> u64 {
359 self.record_index
360 }
361
362 #[inline]
364 #[must_use]
365 pub fn is_eof(&self) -> bool {
366 self.eof_reached
367 }
368
369 #[inline]
371 #[must_use]
372 pub fn schema(&self) -> &Schema {
373 &self.schema
374 }
375
376 #[inline]
378 #[must_use]
379 pub fn options(&self) -> &DecodeOptions {
380 &self.options
381 }
382
383 fn record_context_for(record_index: u64, details: String) -> ErrorContext {
384 ErrorContext {
385 record_index: Some(record_index + 1),
386 field_path: None,
387 byte_offset: None,
388 line_number: None,
389 details: Some(details),
390 }
391 }
392
393 fn rdw_header_read_error_for(record_index: u64, error: &std::io::Error) -> Error {
394 Error::new(
395 ErrorCode::CBKR201_RDW_READ_ERROR,
396 format!("Failed to read RDW header: {error}"),
397 )
398 .with_context(Self::record_context_for(
399 record_index,
400 "I/O failure while reading RDW header".to_string(),
401 ))
402 }
403
404 fn rdw_payload_read_error_for(
405 record_index: u64,
406 error: &std::io::Error,
407 length: usize,
408 ) -> Error {
409 let (code, details) = if error.kind() == std::io::ErrorKind::UnexpectedEof {
410 (
411 ErrorCode::CBKF221_RDW_UNDERFLOW,
412 format!("File ends before the declared {length}-byte RDW payload"),
413 )
414 } else {
415 (
416 ErrorCode::CBKR201_RDW_READ_ERROR,
417 "I/O failure while reading RDW payload".to_string(),
418 )
419 };
420 Error::new(code, format!("Failed to read RDW payload: {error}"))
421 .with_context(Self::record_context_for(record_index, details))
422 }
423
424 #[inline]
474 #[must_use = "Handle the Result or propagate the error"]
475 pub fn read_raw_record(&mut self) -> Result<Option<Vec<u8>>> {
476 if self.eof_reached {
477 return Ok(None);
478 }
479
480 self.buffer.clear();
481 self.raw_data_with_header = None;
482
483 if self.options.format == RecordFormat::Vb {
484 return self.read_vb_record();
485 }
486 let reader = match &mut self.input {
487 FramingInput::Stream(reader) => reader,
488 FramingInput::Blocks(_) => {
489 return Err(Error::new(
490 ErrorCode::CBKI001_INVALID_STATE,
491 "VB block reader active for fixed/RDW decode",
492 ));
493 }
494 };
495
496 let record_data = match self.options.format {
497 RecordFormat::Fixed => {
498 let lrecl = crate::file::fixed::lrecl(&self.schema)? as usize;
499 self.buffer.resize(lrecl, 0);
500
501 match fill_at_record_boundary(reader, &mut self.buffer) {
502 Ok(BoundaryRead::Complete) => {
503 self.record_index += 1;
504 Some(self.buffer.clone())
505 }
506 Ok(BoundaryRead::CleanEof) => {
507 self.eof_reached = true;
508 return Ok(None);
509 }
510 Ok(BoundaryRead::Partial(read)) => {
511 self.eof_reached = true;
512 return Err(Error::new(
513 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
514 format!("Incomplete record at end of file: expected {lrecl} bytes"),
515 )
516 .with_context(ErrorContext {
517 record_index: Some(self.record_index + 1),
518 field_path: None,
519 byte_offset: None,
520 line_number: None,
521 details: Some(format!(
522 "File ends with partial record ({read} of {lrecl} bytes)"
523 )),
524 }));
525 }
526 Err(e) => {
527 return Err(Error::new(
528 ErrorCode::CBKR101_FIXED_RECORD_ERROR,
529 format!("Failed to read fixed record: {e}"),
530 ));
531 }
532 }
533 }
534 RecordFormat::RDW => Self::read_rdw_raw_record(
535 reader,
536 &mut self.buffer,
537 &mut self.raw_data_with_header,
538 &mut self.record_index,
539 &mut self.eof_reached,
540 )?,
541 RecordFormat::Vb => {
542 return Err(Error::new(
543 ErrorCode::CBKI001_INVALID_STATE,
544 "VB format handled by the block reader path",
545 ));
546 }
547 };
548
549 Ok(record_data)
550 }
551
552 #[inline]
560 #[must_use = "Handle the Result or propagate the error"]
561 fn read_rdw_raw_record(
562 reader: &mut std::io::BufReader<R>,
563 buffer: &mut Vec<u8>,
564 raw_data_with_header: &mut Option<Vec<u8>>,
565 record_index: &mut u64,
566 eof_reached: &mut bool,
567 ) -> Result<Option<Vec<u8>>> {
568 let mut rdw_header = [0u8; 4];
570 match fill_at_record_boundary(reader, &mut rdw_header) {
571 Ok(BoundaryRead::Complete) => {}
572 Ok(BoundaryRead::CleanEof) => {
573 *eof_reached = true;
574 return Ok(None);
575 }
576 Ok(BoundaryRead::Partial(read)) => {
577 *eof_reached = true;
578 return Err(Error::new(
579 ErrorCode::CBKF221_RDW_UNDERFLOW,
580 "Incomplete RDW header at end of file: expected 4 bytes".to_string(),
581 )
582 .with_context(Self::record_context_for(
583 *record_index,
584 format!("File ends with partial RDW header ({read} of 4 bytes)"),
585 )));
586 }
587 Err(error) => {
588 return Err(Self::rdw_header_read_error_for(*record_index, &error));
589 }
590 }
591
592 let length = usize::from(RdwHeader::from_bytes(rdw_header).length());
594
595 buffer.resize(length, 0);
597 match reader.read_exact(buffer) {
598 Ok(()) => {
599 let mut framed = Vec::with_capacity(4 + length);
600 framed.extend_from_slice(&rdw_header);
601 framed.extend_from_slice(buffer);
602 *raw_data_with_header = Some(framed);
603 *record_index += 1;
604 Ok(Some(buffer.clone()))
605 }
606 Err(error) => Err(Self::rdw_payload_read_error_for(
607 *record_index,
608 &error,
609 length,
610 )),
611 }
612 }
613
614 fn read_vb_record(&mut self) -> Result<Option<Vec<u8>>> {
619 let FramingInput::Blocks(blocks) = &mut self.input else {
620 return Err(Error::new(
621 ErrorCode::CBKI001_INVALID_STATE,
622 "stream reader active for VB decode",
623 ));
624 };
625 match blocks.read_record()? {
626 None => {
627 self.eof_reached = true;
628 Ok(None)
629 }
630 Some(record) => {
631 let mut raw_data_with_header =
632 Vec::with_capacity(record.rdw.len() + record.payload.len());
633 raw_data_with_header.extend_from_slice(&record.rdw);
634 raw_data_with_header.extend_from_slice(&record.payload);
635 self.raw_data_with_header = Some(raw_data_with_header);
636 self.record_index += 1;
637 Ok(Some(record.payload))
638 }
639 }
640 }
641
642 #[inline]
647 fn decode_next_record(&mut self) -> Result<Option<Value>> {
648 match self.read_raw_record()? {
649 Some(record_bytes) => {
650 let raw_data_with_header = self.raw_data_with_header.take();
651 let json_value = decode_record_with_raw_data(
652 &self.schema,
653 &record_bytes,
654 &self.options,
655 raw_data_with_header.as_deref(),
656 self.record_index,
657 )?;
658 Ok(Some(json_value))
659 }
660 None => Ok(None),
661 }
662 }
663}
664
665impl<R: Read> Iterator for RecordIterator<R> {
666 type Item = Result<Value>;
667
668 #[inline]
669 fn next(&mut self) -> Option<Self::Item> {
670 if self.eof_reached {
671 return None;
672 }
673
674 match self.decode_next_record() {
675 Ok(Some(value)) => Some(Ok(value)),
676 Ok(None) => {
677 self.eof_reached = true;
678 None
679 }
680 Err(error) => {
681 Some(Err(error))
683 }
684 }
685 }
686}
687
688#[inline]
770#[must_use = "Handle the Result or propagate the error"]
771pub fn iter_records_from_file<P: AsRef<std::path::Path>>(
772 file_path: P,
773 schema: &Schema,
774 options: &DecodeOptions,
775) -> Result<RecordIterator<std::fs::File>> {
776 let file = std::fs::File::open(file_path).map_err(|e| {
777 Error::new(
778 ErrorCode::CBKR201_RDW_READ_ERROR,
779 format!("failed to open input file: {e}"),
780 )
781 })?;
782
783 RecordIterator::new(file, schema, options)
784}
785
786#[inline]
873#[must_use = "Handle the Result or propagate the error"]
874pub fn iter_records<R: Read>(
875 reader: R,
876 schema: &Schema,
877 options: &DecodeOptions,
878) -> Result<RecordIterator<R>> {
879 RecordIterator::new(reader, schema, options)
880}
881
882#[cfg(test)]
883#[allow(clippy::expect_used)]
884#[allow(clippy::unwrap_used)]
885#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
886mod tests {
887 use super::*;
888 use crate::Codepage;
889 use copybook_core::parse_copybook;
890 use std::collections::VecDeque;
891 use std::io::{self, Cursor, Read};
892
893 #[test]
894 fn test_record_iterator_basic() {
895 let copybook_text = r"
896 01 RECORD.
897 05 ID PIC 9(3).
898 05 NAME PIC X(5).
899 ";
900
901 let schema = parse_copybook(copybook_text).unwrap();
902
903 let test_data = b"001ALICE002BOB ";
905 let cursor = Cursor::new(test_data);
906
907 let options = DecodeOptions {
908 format: RecordFormat::Fixed,
909 ..DecodeOptions::default()
910 };
911
912 let iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
913
914 assert_eq!(iterator.current_record_index(), 0);
916 assert!(!iterator.is_eof());
917 }
918
919 #[test]
920 fn test_record_iterator_rdw() {
921 let copybook_text = r"
922 01 RECORD.
923 05 ID PIC 9(3).
924 05 NAME PIC X(5).
925 ";
926
927 let schema = parse_copybook(copybook_text).unwrap();
928
929 let test_data = vec![
933 0x00, 0x08, 0x00, 0x00, b'0', b'0', b'1', b'A', b'L', b'I', b'C', b'E', 0x00, 0x06, 0x00, 0x00, b'0', b'0', b'2', b'B', b'O', b'B', ];
938
939 let cursor = Cursor::new(test_data);
940
941 let options = DecodeOptions {
942 format: RecordFormat::RDW,
943 ..DecodeOptions::default()
944 };
945
946 let iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
947
948 assert_eq!(iterator.current_record_index(), 0);
950 assert!(!iterator.is_eof());
951 }
952
953 #[test]
954 fn test_raw_record_reading() {
955 let copybook_text = r"
956 01 RECORD.
957 05 ID PIC 9(3).
958 05 NAME PIC X(5).
959 ";
960
961 let schema = parse_copybook(copybook_text).unwrap();
962
963 let test_data = b"001ALICE";
964 let cursor = Cursor::new(test_data);
965
966 let options = DecodeOptions {
967 format: RecordFormat::Fixed,
968 ..DecodeOptions::default()
969 };
970
971 let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
972
973 let raw_record = iterator.read_raw_record().unwrap().unwrap();
975 assert_eq!(raw_record, b"001ALICE");
976 assert_eq!(iterator.current_record_index(), 1);
977
978 assert!(iterator.read_raw_record().unwrap().is_none());
980 }
981
982 #[test]
983 fn test_iterator_error_handling() {
984 let copybook_text = r"
985 01 RECORD.
986 05 ID PIC 9(3).
987 05 NAME PIC X(5).
988 ";
989
990 let schema = parse_copybook(copybook_text).unwrap();
991
992 let test_data = b"001A";
994 let cursor = Cursor::new(test_data);
995
996 let options = DecodeOptions {
997 format: RecordFormat::Fixed,
998 ..DecodeOptions::default()
999 };
1000
1001 let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
1002
1003 let error = iterator
1007 .next()
1008 .expect("truncated data yields an item")
1009 .expect_err("truncated data is an error");
1010 assert_eq!(error.code, ErrorCode::CBKR101_FIXED_RECORD_ERROR);
1011 assert!(iterator.next().is_none());
1012 }
1013
1014 #[test]
1015 fn test_iterator_fixed_format_missing_lrecl_errors_on_next() {
1016 let copybook_text = "01 SOME-GROUP. 05 SOME-FIELD PIC X(1).";
1018 let mut schema = parse_copybook(copybook_text).unwrap();
1019 schema.lrecl_fixed = None; let test_data = b"";
1022 let cursor = Cursor::new(test_data);
1023
1024 let options = DecodeOptions {
1025 format: RecordFormat::Fixed,
1026 ..DecodeOptions::default()
1027 };
1028
1029 let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
1030
1031 let first = iterator.next().unwrap();
1032 assert!(first.is_err());
1033 if let Err(e) = first {
1034 assert_eq!(e.code, ErrorCode::CBKI001_INVALID_STATE);
1035 assert_eq!(e.message, crate::file::fixed::FIXED_FORMAT_LRECL_MISSING);
1036 }
1037 }
1038
1039 #[test]
1040 fn test_iterator_fixed_format_zero_lrecl_errors_on_next() {
1041 let copybook_text = "01 SOME-GROUP. 05 SOME-FIELD PIC X(1).";
1042 let mut schema = parse_copybook(copybook_text).unwrap();
1043 schema.lrecl_fixed = Some(0);
1044
1045 let options = DecodeOptions {
1046 format: RecordFormat::Fixed,
1047 ..DecodeOptions::default()
1048 };
1049
1050 let mut iterator = RecordIterator::new(Cursor::new(b"DATA"), &schema, &options).unwrap();
1051
1052 let error = iterator.next().unwrap().unwrap_err();
1053 assert_eq!(error.code, ErrorCode::CBKI001_INVALID_STATE);
1054 assert_eq!(error.message, "LRECL must be greater than zero");
1055 }
1056
1057 #[test]
1058 fn test_iterator_schema_and_options_accessors() {
1059 let copybook_text = r"
1060 01 RECORD.
1061 05 ID PIC 9(3).
1062 05 NAME PIC X(5).
1063 ";
1064
1065 let mut schema = parse_copybook(copybook_text).unwrap();
1066 schema.lrecl_fixed = Some(8);
1067 let test_data = b"001ALICE";
1068 let cursor = Cursor::new(test_data);
1069
1070 let options = DecodeOptions {
1071 format: RecordFormat::Fixed,
1072 codepage: Codepage::ASCII,
1073 ..DecodeOptions::default()
1074 };
1075
1076 let iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
1077
1078 assert_eq!(iterator.schema().fields[0].name, "RECORD");
1080
1081 assert_eq!(iterator.options().format, RecordFormat::Fixed);
1083 }
1084
1085 #[test]
1086 fn test_iterator_multiple_fixed_records() {
1087 let copybook_text = r"
1088 01 RECORD.
1089 05 ID PIC 9(3).
1090 05 NAME PIC X(5).
1091 ";
1092
1093 let mut schema = parse_copybook(copybook_text).unwrap();
1094 schema.lrecl_fixed = Some(8);
1095
1096 let test_data = b"001ALICE002BOB 003CAROL";
1098 let cursor = Cursor::new(test_data);
1099
1100 let options = DecodeOptions {
1101 format: RecordFormat::Fixed,
1102 codepage: Codepage::ASCII,
1103 ..DecodeOptions::default()
1104 };
1105
1106 let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
1107
1108 let mut count = 0;
1110 for result in iterator.by_ref() {
1111 assert!(result.is_ok(), "Record {count} should decode successfully");
1112 count += 1;
1113 }
1114
1115 assert_eq!(count, 3);
1116 assert_eq!(iterator.current_record_index(), 3);
1117 assert!(iterator.is_eof());
1118 }
1119
1120 #[test]
1121 fn test_iterator_rdw_multiple_records() {
1122 let copybook_text = r"
1123 01 RECORD.
1124 05 ID PIC 9(3).
1125 05 NAME PIC X(5).
1126 ";
1127
1128 let schema = parse_copybook(copybook_text).unwrap();
1129
1130 let test_data = vec![
1132 0x00, 0x08, 0x00, 0x00, b'0', b'0', b'1', b'A', b'L', b'I', b'C', b'E', 0x00, 0x06, 0x00, 0x00, b'0', b'0', b'2', b'B', b'O', b'B', 0x00, 0x08, 0x00, 0x00, b'0', b'0', b'3', b'C', b'A', b'R', b'O', b'L',
1139 ];
1140
1141 let cursor = Cursor::new(test_data);
1142
1143 let options = DecodeOptions {
1144 format: RecordFormat::RDW,
1145 codepage: Codepage::ASCII,
1146 ..DecodeOptions::default()
1147 };
1148
1149 let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
1150
1151 let mut count = 0;
1153 for result in iterator.by_ref() {
1154 assert!(result.is_ok(), "Record {count} should decode successfully");
1155 count += 1;
1156 }
1157
1158 assert_eq!(count, 3);
1159 assert_eq!(iterator.current_record_index(), 3);
1160 assert!(iterator.is_eof());
1161 }
1162
1163 #[test]
1164 fn test_iter_records_convenience() {
1165 let copybook_text = r"
1166 01 RECORD.
1167 05 ID PIC 9(3).
1168 05 NAME PIC X(5).
1169 ";
1170
1171 let schema = parse_copybook(copybook_text).unwrap();
1172
1173 let test_data = b"001ALICE002BOB ";
1174 let cursor = Cursor::new(test_data);
1175
1176 let options = DecodeOptions {
1177 format: RecordFormat::Fixed,
1178 ..DecodeOptions::default()
1179 };
1180
1181 let iterator = iter_records(cursor, &schema, &options).unwrap();
1182
1183 assert_eq!(iterator.current_record_index(), 0);
1184 assert!(!iterator.is_eof());
1185 }
1186
1187 #[test]
1188 fn test_iterator_with_empty_data() {
1189 let copybook_text = r"
1190 01 RECORD.
1191 05 ID PIC 9(3).
1192 05 NAME PIC X(5).
1193 ";
1194
1195 let mut schema = parse_copybook(copybook_text).unwrap();
1196 schema.lrecl_fixed = Some(8);
1197
1198 let test_data = b"";
1199 let cursor = Cursor::new(test_data);
1200
1201 let options = DecodeOptions {
1202 format: RecordFormat::Fixed,
1203 ..DecodeOptions::default()
1204 };
1205
1206 let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
1207
1208 assert!(iterator.next().is_none());
1210 assert!(iterator.is_eof());
1211 assert_eq!(iterator.current_record_index(), 0);
1212 }
1213
1214 #[test]
1215 fn test_iterator_raw_record_eof() {
1216 let copybook_text = r"
1217 01 RECORD.
1218 05 ID PIC 9(3).
1219 05 NAME PIC X(5).
1220 ";
1221
1222 let schema = parse_copybook(copybook_text).unwrap();
1223
1224 let test_data = b"001ALICE";
1225 let cursor = Cursor::new(test_data);
1226
1227 let options = DecodeOptions {
1228 format: RecordFormat::Fixed,
1229 ..DecodeOptions::default()
1230 };
1231
1232 let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
1233
1234 assert!(iterator.read_raw_record().unwrap().is_some());
1236 assert_eq!(iterator.current_record_index(), 1);
1237
1238 assert!(iterator.read_raw_record().unwrap().is_none());
1240 assert!(iterator.is_eof());
1241 }
1242
1243 #[test]
1244 fn test_iterator_collect_results() {
1245 let copybook_text = r"
1246 01 RECORD.
1247 05 ID PIC 9(3).
1248 05 NAME PIC X(5).
1249 ";
1250
1251 let mut schema = parse_copybook(copybook_text).unwrap();
1252 schema.lrecl_fixed = Some(8);
1253
1254 let test_data = b"001ALICE002BOB 003CAROL";
1255 let cursor = Cursor::new(test_data);
1256
1257 let options = DecodeOptions {
1258 format: RecordFormat::Fixed,
1259 codepage: Codepage::ASCII,
1260 ..DecodeOptions::default()
1261 };
1262
1263 let iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
1264
1265 let results: Vec<Result<Value>> = iterator.collect();
1267
1268 assert_eq!(results.len(), 3);
1269 for result in results {
1270 assert!(result.is_ok());
1271 }
1272 }
1273
1274 #[test]
1275 fn test_iterator_with_decode_error() {
1276 let copybook_text = r"
1277 01 RECORD.
1278 05 ID PIC 9(3).
1279 05 NAME PIC X(5).
1280 ";
1281
1282 let mut schema = parse_copybook(copybook_text).unwrap();
1283 schema.lrecl_fixed = Some(8);
1284
1285 let test_data = b"001ALICE";
1287 let cursor = Cursor::new(test_data);
1288
1289 let options = DecodeOptions {
1290 format: RecordFormat::Fixed,
1291 codepage: Codepage::ASCII,
1292 ..DecodeOptions::default()
1293 };
1294
1295 let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
1296
1297 let first = iterator.next();
1299 assert!(first.is_some());
1300 assert!(first.unwrap().is_ok());
1301
1302 assert!(iterator.next().is_none());
1304 }
1305
1306 #[derive(Default)]
1307 struct FailingReader {
1308 fail: bool,
1309 }
1310
1311 impl Read for FailingReader {
1312 fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
1313 if self.fail {
1314 Ok(0)
1315 } else {
1316 self.fail = true;
1317 Err(io::Error::other("forced read error"))
1318 }
1319 }
1320 }
1321
1322 #[test]
1323 fn test_iterator_fixed_format_read_error_code() {
1324 let copybook_text = r"
1325 01 RECORD.
1326 05 ID PIC 9(3).
1327 05 NAME PIC X(5).
1328 ";
1329
1330 let schema = parse_copybook(copybook_text).unwrap();
1331
1332 let mut schema = schema;
1333 schema.lrecl_fixed = Some(8);
1334
1335 let mut iterator =
1336 RecordIterator::new(FailingReader::default(), &schema, &DecodeOptions::default())
1337 .unwrap();
1338
1339 let error = iterator.read_raw_record().unwrap_err();
1340 assert_eq!(error.code, ErrorCode::CBKR101_FIXED_RECORD_ERROR);
1341 }
1342
1343 enum ReadStep {
1344 Bytes(Vec<u8>),
1345 Error(io::ErrorKind),
1346 Eof,
1347 }
1348
1349 struct ScriptedReader {
1350 steps: VecDeque<ReadStep>,
1351 }
1352
1353 impl ScriptedReader {
1354 fn new(steps: impl IntoIterator<Item = ReadStep>) -> Self {
1355 Self {
1356 steps: steps.into_iter().collect(),
1357 }
1358 }
1359 }
1360
1361 impl Read for ScriptedReader {
1362 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1363 match self.steps.pop_front() {
1364 Some(ReadStep::Bytes(mut bytes)) => {
1365 let read = bytes.len().min(buf.len());
1366 buf[..read].copy_from_slice(&bytes[..read]);
1367 if read < bytes.len() {
1368 bytes.drain(..read);
1369 self.steps.push_front(ReadStep::Bytes(bytes));
1370 }
1371 Ok(read)
1372 }
1373 Some(ReadStep::Error(kind)) => Err(io::Error::new(kind, "scripted read error")),
1374 Some(ReadStep::Eof) | None => Ok(0),
1375 }
1376 }
1377 }
1378
1379 fn rdw_iterator(reader: ScriptedReader) -> RecordIterator<ScriptedReader> {
1380 let schema = eight_byte_schema();
1381 let options = DecodeOptions::default()
1382 .with_format(RecordFormat::RDW)
1383 .with_codepage(Codepage::ASCII);
1384 RecordIterator::new(reader, &schema, &options).unwrap()
1385 }
1386
1387 fn complete_rdw_record() -> Vec<u8> {
1388 let mut record = vec![0x00, 0x08, 0x00, 0x00];
1389 record.extend_from_slice(b"RECORD01");
1390 record
1391 }
1392
1393 fn assert_rdw_error(error: Error, code: ErrorCode, record_index: u64) {
1394 assert_eq!(error.code, code);
1395 assert_eq!(
1396 error.context.and_then(|context| context.record_index),
1397 Some(record_index)
1398 );
1399 }
1400
1401 #[test]
1402 fn rdw_clean_boundary_eof_returns_none() {
1403 let mut iterator = rdw_iterator(ScriptedReader::new([ReadStep::Eof]));
1404
1405 assert!(iterator.read_raw_record().unwrap().is_none());
1406 assert!(iterator.is_eof());
1407 }
1408
1409 #[test]
1410 fn rdw_partial_next_header_keeps_underflow_and_context() {
1411 let mut iterator = rdw_iterator(ScriptedReader::new([
1412 ReadStep::Bytes(complete_rdw_record()),
1413 ReadStep::Bytes(vec![0x00, 0x08]),
1414 ReadStep::Eof,
1415 ]));
1416
1417 assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
1418 assert_rdw_error(
1419 iterator.read_raw_record().unwrap_err(),
1420 ErrorCode::CBKF221_RDW_UNDERFLOW,
1421 2,
1422 );
1423 }
1424
1425 #[test]
1426 fn rdw_next_header_io_error_maps_to_read_error_with_context() {
1427 let mut iterator = rdw_iterator(ScriptedReader::new([
1428 ReadStep::Bytes(complete_rdw_record()),
1429 ReadStep::Error(io::ErrorKind::Other),
1430 ]));
1431
1432 assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
1433 assert_rdw_error(
1434 iterator.read_raw_record().unwrap_err(),
1435 ErrorCode::CBKR201_RDW_READ_ERROR,
1436 2,
1437 );
1438 }
1439
1440 #[test]
1441 fn rdw_partial_next_payload_keeps_underflow_and_context() {
1442 let mut iterator = rdw_iterator(ScriptedReader::new([
1443 ReadStep::Bytes(complete_rdw_record()),
1444 ReadStep::Bytes(vec![0x00, 0x08, 0x00, 0x00]),
1445 ReadStep::Bytes(b"ABC".to_vec()),
1446 ReadStep::Eof,
1447 ]));
1448
1449 assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
1450 assert_rdw_error(
1451 iterator.read_raw_record().unwrap_err(),
1452 ErrorCode::CBKF221_RDW_UNDERFLOW,
1453 2,
1454 );
1455 }
1456
1457 #[test]
1458 fn rdw_next_payload_io_error_maps_to_read_error_with_context() {
1459 let mut iterator = rdw_iterator(ScriptedReader::new([
1460 ReadStep::Bytes(complete_rdw_record()),
1461 ReadStep::Bytes(vec![0x00, 0x08, 0x00, 0x00]),
1462 ReadStep::Bytes(b"ABC".to_vec()),
1463 ReadStep::Error(io::ErrorKind::Other),
1464 ]));
1465
1466 assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
1467 assert_rdw_error(
1468 iterator.read_raw_record().unwrap_err(),
1469 ErrorCode::CBKR201_RDW_READ_ERROR,
1470 2,
1471 );
1472 }
1473
1474 struct DribbleReader {
1477 data: Vec<u8>,
1478 position: usize,
1479 }
1480
1481 impl Read for DribbleReader {
1482 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1483 if self.position >= self.data.len() || buf.is_empty() {
1484 return Ok(0);
1485 }
1486 buf[0] = self.data[self.position];
1487 self.position += 1;
1488 Ok(1)
1489 }
1490 }
1491
1492 fn eight_byte_schema() -> Schema {
1493 let mut schema = parse_copybook("01 RECORD.\n 05 NAME PIC X(8).").unwrap();
1494 schema.lrecl_fixed = Some(8);
1495 schema
1496 }
1497
1498 #[test]
1499 fn fixed_trailing_partial_record_is_reported_not_discarded() {
1500 let data = b"RECORD01HALF".to_vec();
1502 let schema = eight_byte_schema();
1503 let options = DecodeOptions::default().with_codepage(Codepage::ASCII);
1504 let mut iterator = RecordIterator::new(Cursor::new(data), &schema, &options).unwrap();
1505
1506 assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
1507
1508 let error = iterator.read_raw_record().unwrap_err();
1509 assert_eq!(error.code, ErrorCode::CBKR101_FIXED_RECORD_ERROR);
1510 let context = error.context.expect("partial record reports context");
1511 assert_eq!(context.record_index, Some(2));
1512 assert!(
1513 context.details.unwrap_or_default().contains("4 of 8 bytes"),
1514 "details should name the short count"
1515 );
1516 }
1517
1518 #[test]
1519 fn fixed_partial_record_terminates_iteration_after_one_error() {
1520 let schema = eight_byte_schema();
1521 let options = DecodeOptions::default().with_codepage(Codepage::ASCII);
1522 let mut iterator =
1523 RecordIterator::new(Cursor::new(b"RECORD01HALF".to_vec()), &schema, &options).unwrap();
1524
1525 assert!(iterator.next().unwrap().is_ok());
1526 assert!(iterator.next().unwrap().is_err());
1527 assert!(
1528 iterator.next().is_none(),
1529 "iteration must stop after the partial-record error"
1530 );
1531 }
1532
1533 #[test]
1534 fn fixed_file_ending_on_a_record_boundary_is_still_clean_eof() {
1535 let schema = eight_byte_schema();
1536 let options = DecodeOptions::default().with_codepage(Codepage::ASCII);
1537 let mut iterator =
1538 RecordIterator::new(Cursor::new(b"RECORD01".to_vec()), &schema, &options).unwrap();
1539
1540 assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
1541 assert!(iterator.read_raw_record().unwrap().is_none());
1542 assert!(iterator.is_eof());
1543 }
1544
1545 #[test]
1546 fn fixed_partial_record_detected_when_reads_return_one_byte_at_a_time() {
1547 let schema = eight_byte_schema();
1550 let options = DecodeOptions::default().with_codepage(Codepage::ASCII);
1551 let reader = DribbleReader {
1552 data: b"RECORD01HALF".to_vec(),
1553 position: 0,
1554 };
1555 let mut iterator = RecordIterator::new(reader, &schema, &options).unwrap();
1556
1557 assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
1558 assert_eq!(
1559 iterator.read_raw_record().unwrap_err().code,
1560 ErrorCode::CBKR101_FIXED_RECORD_ERROR
1561 );
1562 }
1563
1564 #[test]
1565 fn rdw_trailing_partial_header_is_reported_not_discarded() {
1566 let mut data = vec![0x00, 0x08, 0x00, 0x00];
1568 data.extend_from_slice(b"RECORD01");
1569 data.extend_from_slice(&[0x00, 0x08]);
1570
1571 let schema = eight_byte_schema();
1572 let options = DecodeOptions::default()
1573 .with_format(RecordFormat::RDW)
1574 .with_codepage(Codepage::ASCII);
1575 let mut iterator = RecordIterator::new(Cursor::new(data), &schema, &options).unwrap();
1576
1577 assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
1578
1579 let error = iterator.read_raw_record().unwrap_err();
1580 assert_eq!(error.code, ErrorCode::CBKF221_RDW_UNDERFLOW);
1581 }
1582
1583 #[test]
1584 fn rdw_file_ending_on_a_record_boundary_is_still_clean_eof() {
1585 let mut data = vec![0x00, 0x08, 0x00, 0x00];
1586 data.extend_from_slice(b"RECORD01");
1587
1588 let schema = eight_byte_schema();
1589 let options = DecodeOptions::default()
1590 .with_format(RecordFormat::RDW)
1591 .with_codepage(Codepage::ASCII);
1592 let mut iterator = RecordIterator::new(Cursor::new(data), &schema, &options).unwrap();
1593
1594 assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
1595 assert!(iterator.read_raw_record().unwrap().is_none());
1596 assert!(iterator.is_eof());
1597 }
1598}