use crate::lib_api::decode_record_with_raw_data;
use crate::options::{DecodeOptions, RecordFormat};
use copybook_core::{Error, ErrorCode, ErrorContext, Result, Schema};
use copybook_rdw::RdwHeader;
use serde_json::Value;
use std::io::{BufReader, Read};
enum BoundaryRead {
Complete,
CleanEof,
Partial(usize),
}
fn fill_at_record_boundary<R: Read>(
reader: &mut R,
buffer: &mut [u8],
) -> std::io::Result<BoundaryRead> {
let mut filled = 0;
while filled < buffer.len() {
match reader.read(&mut buffer[filled..]) {
Ok(0) => break,
Ok(read) => filled += read,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
if filled == buffer.len() {
Ok(BoundaryRead::Complete)
} else if filled == 0 {
Ok(BoundaryRead::CleanEof)
} else {
Ok(BoundaryRead::Partial(filled))
}
}
pub struct RecordIterator<R: Read> {
reader: BufReader<R>,
schema: Schema,
options: DecodeOptions,
record_index: u64,
eof_reached: bool,
buffer: Vec<u8>,
raw_data_with_header: Option<Vec<u8>>,
}
impl<R: Read> RecordIterator<R> {
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn new(reader: R, schema: &Schema, options: &DecodeOptions) -> Result<Self> {
Ok(Self {
reader: BufReader::new(reader),
schema: schema.clone(),
options: options.clone(),
record_index: 0,
eof_reached: false,
buffer: Vec::new(),
raw_data_with_header: None,
})
}
#[inline]
#[must_use]
pub fn current_record_index(&self) -> u64 {
self.record_index
}
#[inline]
#[must_use]
pub fn is_eof(&self) -> bool {
self.eof_reached
}
#[inline]
#[must_use]
pub fn schema(&self) -> &Schema {
&self.schema
}
#[inline]
#[must_use]
pub fn options(&self) -> &DecodeOptions {
&self.options
}
fn next_record_context(&self, details: String) -> ErrorContext {
ErrorContext {
record_index: Some(self.record_index + 1),
field_path: None,
byte_offset: None,
line_number: None,
details: Some(details),
}
}
fn rdw_header_read_error(&self, error: &std::io::Error) -> Error {
Error::new(
ErrorCode::CBKR201_RDW_READ_ERROR,
format!("Failed to read RDW header: {error}"),
)
.with_context(self.next_record_context("I/O failure while reading RDW header".to_string()))
}
fn rdw_payload_read_error(&self, error: &std::io::Error, length: usize) -> Error {
let (code, details) = if error.kind() == std::io::ErrorKind::UnexpectedEof {
(
ErrorCode::CBKF221_RDW_UNDERFLOW,
format!("File ends before the declared {length}-byte RDW payload"),
)
} else {
(
ErrorCode::CBKR201_RDW_READ_ERROR,
"I/O failure while reading RDW payload".to_string(),
)
};
Error::new(code, format!("Failed to read RDW payload: {error}"))
.with_context(self.next_record_context(details))
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn read_raw_record(&mut self) -> Result<Option<Vec<u8>>> {
if self.eof_reached {
return Ok(None);
}
self.buffer.clear();
self.raw_data_with_header = None;
let record_data = match self.options.format {
RecordFormat::Fixed => {
let lrecl = crate::file::fixed::lrecl(&self.schema)? as usize;
self.buffer.resize(lrecl, 0);
match fill_at_record_boundary(&mut self.reader, &mut self.buffer) {
Ok(BoundaryRead::Complete) => {
self.record_index += 1;
Some(self.buffer.clone())
}
Ok(BoundaryRead::CleanEof) => {
self.eof_reached = true;
return Ok(None);
}
Ok(BoundaryRead::Partial(read)) => {
self.eof_reached = true;
return Err(Error::new(
ErrorCode::CBKR101_FIXED_RECORD_ERROR,
format!("Incomplete record at end of file: expected {lrecl} bytes"),
)
.with_context(ErrorContext {
record_index: Some(self.record_index + 1),
field_path: None,
byte_offset: None,
line_number: None,
details: Some(format!(
"File ends with partial record ({read} of {lrecl} bytes)"
)),
}));
}
Err(e) => {
return Err(Error::new(
ErrorCode::CBKR101_FIXED_RECORD_ERROR,
format!("Failed to read fixed record: {e}"),
));
}
}
}
RecordFormat::RDW => {
let mut rdw_header = [0u8; 4];
match fill_at_record_boundary(&mut self.reader, &mut rdw_header) {
Ok(BoundaryRead::Complete) => {}
Ok(BoundaryRead::CleanEof) => {
self.eof_reached = true;
return Ok(None);
}
Ok(BoundaryRead::Partial(read)) => {
self.eof_reached = true;
return Err(Error::new(
ErrorCode::CBKF221_RDW_UNDERFLOW,
"Incomplete RDW header at end of file: expected 4 bytes".to_string(),
)
.with_context(ErrorContext {
record_index: Some(self.record_index + 1),
field_path: None,
byte_offset: None,
line_number: None,
details: Some(format!(
"File ends with partial RDW header ({read} of 4 bytes)"
)),
}));
}
Err(e) => {
return Err(self.rdw_header_read_error(&e));
}
}
let length = usize::from(RdwHeader::from_bytes(rdw_header).length());
self.buffer.resize(length, 0);
match self.reader.read_exact(&mut self.buffer) {
Ok(()) => {
let mut raw_data_with_header = Vec::with_capacity(4 + length);
raw_data_with_header.extend_from_slice(&rdw_header);
raw_data_with_header.extend_from_slice(&self.buffer);
self.raw_data_with_header = Some(raw_data_with_header);
self.record_index += 1;
Some(self.buffer.clone())
}
Err(e) => {
return Err(self.rdw_payload_read_error(&e, length));
}
}
}
};
Ok(record_data)
}
#[inline]
fn decode_next_record(&mut self) -> Result<Option<Value>> {
match self.read_raw_record()? {
Some(record_bytes) => {
let raw_data_with_header = self.raw_data_with_header.take();
let json_value = decode_record_with_raw_data(
&self.schema,
&record_bytes,
&self.options,
raw_data_with_header.as_deref(),
self.record_index,
)?;
Ok(Some(json_value))
}
None => Ok(None),
}
}
}
impl<R: Read> Iterator for RecordIterator<R> {
type Item = Result<Value>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
if self.eof_reached {
return None;
}
match self.decode_next_record() {
Ok(Some(value)) => Some(Ok(value)),
Ok(None) => {
self.eof_reached = true;
None
}
Err(error) => {
Some(Err(error))
}
}
}
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn iter_records_from_file<P: AsRef<std::path::Path>>(
file_path: P,
schema: &Schema,
options: &DecodeOptions,
) -> Result<RecordIterator<std::fs::File>> {
let file = std::fs::File::open(file_path).map_err(|e| {
Error::new(
ErrorCode::CBKR201_RDW_READ_ERROR,
format!("failed to open input file: {e}"),
)
})?;
RecordIterator::new(file, schema, options)
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn iter_records<R: Read>(
reader: R,
schema: &Schema,
options: &DecodeOptions,
) -> Result<RecordIterator<R>> {
RecordIterator::new(reader, schema, options)
}
#[cfg(test)]
#[allow(clippy::expect_used)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::Codepage;
use copybook_core::parse_copybook;
use std::collections::VecDeque;
use std::io::{self, Cursor, Read};
#[test]
fn test_record_iterator_basic() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let schema = parse_copybook(copybook_text).unwrap();
let test_data = b"001ALICE002BOB ";
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::Fixed,
..DecodeOptions::default()
};
let iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
assert_eq!(iterator.current_record_index(), 0);
assert!(!iterator.is_eof());
}
#[test]
fn test_record_iterator_rdw() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let schema = parse_copybook(copybook_text).unwrap();
let test_data = vec![
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', ];
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::RDW,
..DecodeOptions::default()
};
let iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
assert_eq!(iterator.current_record_index(), 0);
assert!(!iterator.is_eof());
}
#[test]
fn test_raw_record_reading() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let schema = parse_copybook(copybook_text).unwrap();
let test_data = b"001ALICE";
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::Fixed,
..DecodeOptions::default()
};
let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
let raw_record = iterator.read_raw_record().unwrap().unwrap();
assert_eq!(raw_record, b"001ALICE");
assert_eq!(iterator.current_record_index(), 1);
assert!(iterator.read_raw_record().unwrap().is_none());
}
#[test]
fn test_iterator_error_handling() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let schema = parse_copybook(copybook_text).unwrap();
let test_data = b"001A";
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::Fixed,
..DecodeOptions::default()
};
let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
let error = iterator
.next()
.expect("truncated data yields an item")
.expect_err("truncated data is an error");
assert_eq!(error.code, ErrorCode::CBKR101_FIXED_RECORD_ERROR);
assert!(iterator.next().is_none());
}
#[test]
fn test_iterator_fixed_format_missing_lrecl_errors_on_next() {
let copybook_text = "01 SOME-GROUP. 05 SOME-FIELD PIC X(1).";
let mut schema = parse_copybook(copybook_text).unwrap();
schema.lrecl_fixed = None;
let test_data = b"";
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::Fixed,
..DecodeOptions::default()
};
let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
let first = iterator.next().unwrap();
assert!(first.is_err());
if let Err(e) = first {
assert_eq!(e.code, ErrorCode::CBKI001_INVALID_STATE);
assert_eq!(e.message, crate::file::fixed::FIXED_FORMAT_LRECL_MISSING);
}
}
#[test]
fn test_iterator_fixed_format_zero_lrecl_errors_on_next() {
let copybook_text = "01 SOME-GROUP. 05 SOME-FIELD PIC X(1).";
let mut schema = parse_copybook(copybook_text).unwrap();
schema.lrecl_fixed = Some(0);
let options = DecodeOptions {
format: RecordFormat::Fixed,
..DecodeOptions::default()
};
let mut iterator = RecordIterator::new(Cursor::new(b"DATA"), &schema, &options).unwrap();
let error = iterator.next().unwrap().unwrap_err();
assert_eq!(error.code, ErrorCode::CBKI001_INVALID_STATE);
assert_eq!(error.message, "LRECL must be greater than zero");
}
#[test]
fn test_iterator_schema_and_options_accessors() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let mut schema = parse_copybook(copybook_text).unwrap();
schema.lrecl_fixed = Some(8);
let test_data = b"001ALICE";
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::Fixed,
codepage: Codepage::ASCII,
..DecodeOptions::default()
};
let iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
assert_eq!(iterator.schema().fields[0].name, "RECORD");
assert_eq!(iterator.options().format, RecordFormat::Fixed);
}
#[test]
fn test_iterator_multiple_fixed_records() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let mut schema = parse_copybook(copybook_text).unwrap();
schema.lrecl_fixed = Some(8);
let test_data = b"001ALICE002BOB 003CAROL";
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::Fixed,
codepage: Codepage::ASCII,
..DecodeOptions::default()
};
let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
let mut count = 0;
for result in iterator.by_ref() {
assert!(result.is_ok(), "Record {count} should decode successfully");
count += 1;
}
assert_eq!(count, 3);
assert_eq!(iterator.current_record_index(), 3);
assert!(iterator.is_eof());
}
#[test]
fn test_iterator_rdw_multiple_records() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let schema = parse_copybook(copybook_text).unwrap();
let test_data = vec![
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',
];
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::RDW,
codepage: Codepage::ASCII,
..DecodeOptions::default()
};
let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
let mut count = 0;
for result in iterator.by_ref() {
assert!(result.is_ok(), "Record {count} should decode successfully");
count += 1;
}
assert_eq!(count, 3);
assert_eq!(iterator.current_record_index(), 3);
assert!(iterator.is_eof());
}
#[test]
fn test_iter_records_convenience() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let schema = parse_copybook(copybook_text).unwrap();
let test_data = b"001ALICE002BOB ";
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::Fixed,
..DecodeOptions::default()
};
let iterator = iter_records(cursor, &schema, &options).unwrap();
assert_eq!(iterator.current_record_index(), 0);
assert!(!iterator.is_eof());
}
#[test]
fn test_iterator_with_empty_data() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let mut schema = parse_copybook(copybook_text).unwrap();
schema.lrecl_fixed = Some(8);
let test_data = b"";
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::Fixed,
..DecodeOptions::default()
};
let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
assert!(iterator.next().is_none());
assert!(iterator.is_eof());
assert_eq!(iterator.current_record_index(), 0);
}
#[test]
fn test_iterator_raw_record_eof() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let schema = parse_copybook(copybook_text).unwrap();
let test_data = b"001ALICE";
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::Fixed,
..DecodeOptions::default()
};
let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
assert!(iterator.read_raw_record().unwrap().is_some());
assert_eq!(iterator.current_record_index(), 1);
assert!(iterator.read_raw_record().unwrap().is_none());
assert!(iterator.is_eof());
}
#[test]
fn test_iterator_collect_results() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let mut schema = parse_copybook(copybook_text).unwrap();
schema.lrecl_fixed = Some(8);
let test_data = b"001ALICE002BOB 003CAROL";
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::Fixed,
codepage: Codepage::ASCII,
..DecodeOptions::default()
};
let iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
let results: Vec<Result<Value>> = iterator.collect();
assert_eq!(results.len(), 3);
for result in results {
assert!(result.is_ok());
}
}
#[test]
fn test_iterator_with_decode_error() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let mut schema = parse_copybook(copybook_text).unwrap();
schema.lrecl_fixed = Some(8);
let test_data = b"001ALICE";
let cursor = Cursor::new(test_data);
let options = DecodeOptions {
format: RecordFormat::Fixed,
codepage: Codepage::ASCII,
..DecodeOptions::default()
};
let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
let first = iterator.next();
assert!(first.is_some());
assert!(first.unwrap().is_ok());
assert!(iterator.next().is_none());
}
#[derive(Default)]
struct FailingReader {
fail: bool,
}
impl Read for FailingReader {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
if self.fail {
Ok(0)
} else {
self.fail = true;
Err(io::Error::other("forced read error"))
}
}
}
#[test]
fn test_iterator_fixed_format_read_error_code() {
let copybook_text = r"
01 RECORD.
05 ID PIC 9(3).
05 NAME PIC X(5).
";
let schema = parse_copybook(copybook_text).unwrap();
let mut schema = schema;
schema.lrecl_fixed = Some(8);
let mut iterator =
RecordIterator::new(FailingReader::default(), &schema, &DecodeOptions::default())
.unwrap();
let error = iterator.read_raw_record().unwrap_err();
assert_eq!(error.code, ErrorCode::CBKR101_FIXED_RECORD_ERROR);
}
enum ReadStep {
Bytes(Vec<u8>),
Error(io::ErrorKind),
Eof,
}
struct ScriptedReader {
steps: VecDeque<ReadStep>,
}
impl ScriptedReader {
fn new(steps: impl IntoIterator<Item = ReadStep>) -> Self {
Self {
steps: steps.into_iter().collect(),
}
}
}
impl Read for ScriptedReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self.steps.pop_front() {
Some(ReadStep::Bytes(mut bytes)) => {
let read = bytes.len().min(buf.len());
buf[..read].copy_from_slice(&bytes[..read]);
if read < bytes.len() {
bytes.drain(..read);
self.steps.push_front(ReadStep::Bytes(bytes));
}
Ok(read)
}
Some(ReadStep::Error(kind)) => Err(io::Error::new(kind, "scripted read error")),
Some(ReadStep::Eof) | None => Ok(0),
}
}
}
fn rdw_iterator(reader: ScriptedReader) -> RecordIterator<ScriptedReader> {
let schema = eight_byte_schema();
let options = DecodeOptions::default()
.with_format(RecordFormat::RDW)
.with_codepage(Codepage::ASCII);
RecordIterator::new(reader, &schema, &options).unwrap()
}
fn complete_rdw_record() -> Vec<u8> {
let mut record = vec![0x00, 0x08, 0x00, 0x00];
record.extend_from_slice(b"RECORD01");
record
}
fn assert_rdw_error(error: Error, code: ErrorCode, record_index: u64) {
assert_eq!(error.code, code);
assert_eq!(
error.context.and_then(|context| context.record_index),
Some(record_index)
);
}
#[test]
fn rdw_clean_boundary_eof_returns_none() {
let mut iterator = rdw_iterator(ScriptedReader::new([ReadStep::Eof]));
assert!(iterator.read_raw_record().unwrap().is_none());
assert!(iterator.is_eof());
}
#[test]
fn rdw_partial_next_header_keeps_underflow_and_context() {
let mut iterator = rdw_iterator(ScriptedReader::new([
ReadStep::Bytes(complete_rdw_record()),
ReadStep::Bytes(vec![0x00, 0x08]),
ReadStep::Eof,
]));
assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
assert_rdw_error(
iterator.read_raw_record().unwrap_err(),
ErrorCode::CBKF221_RDW_UNDERFLOW,
2,
);
}
#[test]
fn rdw_next_header_io_error_maps_to_read_error_with_context() {
let mut iterator = rdw_iterator(ScriptedReader::new([
ReadStep::Bytes(complete_rdw_record()),
ReadStep::Error(io::ErrorKind::Other),
]));
assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
assert_rdw_error(
iterator.read_raw_record().unwrap_err(),
ErrorCode::CBKR201_RDW_READ_ERROR,
2,
);
}
#[test]
fn rdw_partial_next_payload_keeps_underflow_and_context() {
let mut iterator = rdw_iterator(ScriptedReader::new([
ReadStep::Bytes(complete_rdw_record()),
ReadStep::Bytes(vec![0x00, 0x08, 0x00, 0x00]),
ReadStep::Bytes(b"ABC".to_vec()),
ReadStep::Eof,
]));
assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
assert_rdw_error(
iterator.read_raw_record().unwrap_err(),
ErrorCode::CBKF221_RDW_UNDERFLOW,
2,
);
}
#[test]
fn rdw_next_payload_io_error_maps_to_read_error_with_context() {
let mut iterator = rdw_iterator(ScriptedReader::new([
ReadStep::Bytes(complete_rdw_record()),
ReadStep::Bytes(vec![0x00, 0x08, 0x00, 0x00]),
ReadStep::Bytes(b"ABC".to_vec()),
ReadStep::Error(io::ErrorKind::Other),
]));
assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
assert_rdw_error(
iterator.read_raw_record().unwrap_err(),
ErrorCode::CBKR201_RDW_READ_ERROR,
2,
);
}
struct DribbleReader {
data: Vec<u8>,
position: usize,
}
impl Read for DribbleReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.position >= self.data.len() || buf.is_empty() {
return Ok(0);
}
buf[0] = self.data[self.position];
self.position += 1;
Ok(1)
}
}
fn eight_byte_schema() -> Schema {
let mut schema = parse_copybook("01 RECORD.\n 05 NAME PIC X(8).").unwrap();
schema.lrecl_fixed = Some(8);
schema
}
#[test]
fn fixed_trailing_partial_record_is_reported_not_discarded() {
let data = b"RECORD01HALF".to_vec();
let schema = eight_byte_schema();
let options = DecodeOptions::default().with_codepage(Codepage::ASCII);
let mut iterator = RecordIterator::new(Cursor::new(data), &schema, &options).unwrap();
assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
let error = iterator.read_raw_record().unwrap_err();
assert_eq!(error.code, ErrorCode::CBKR101_FIXED_RECORD_ERROR);
let context = error.context.expect("partial record reports context");
assert_eq!(context.record_index, Some(2));
assert!(
context.details.unwrap_or_default().contains("4 of 8 bytes"),
"details should name the short count"
);
}
#[test]
fn fixed_partial_record_terminates_iteration_after_one_error() {
let schema = eight_byte_schema();
let options = DecodeOptions::default().with_codepage(Codepage::ASCII);
let mut iterator =
RecordIterator::new(Cursor::new(b"RECORD01HALF".to_vec()), &schema, &options).unwrap();
assert!(iterator.next().unwrap().is_ok());
assert!(iterator.next().unwrap().is_err());
assert!(
iterator.next().is_none(),
"iteration must stop after the partial-record error"
);
}
#[test]
fn fixed_file_ending_on_a_record_boundary_is_still_clean_eof() {
let schema = eight_byte_schema();
let options = DecodeOptions::default().with_codepage(Codepage::ASCII);
let mut iterator =
RecordIterator::new(Cursor::new(b"RECORD01".to_vec()), &schema, &options).unwrap();
assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
assert!(iterator.read_raw_record().unwrap().is_none());
assert!(iterator.is_eof());
}
#[test]
fn fixed_partial_record_detected_when_reads_return_one_byte_at_a_time() {
let schema = eight_byte_schema();
let options = DecodeOptions::default().with_codepage(Codepage::ASCII);
let reader = DribbleReader {
data: b"RECORD01HALF".to_vec(),
position: 0,
};
let mut iterator = RecordIterator::new(reader, &schema, &options).unwrap();
assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
assert_eq!(
iterator.read_raw_record().unwrap_err().code,
ErrorCode::CBKR101_FIXED_RECORD_ERROR
);
}
#[test]
fn rdw_trailing_partial_header_is_reported_not_discarded() {
let mut data = vec![0x00, 0x08, 0x00, 0x00];
data.extend_from_slice(b"RECORD01");
data.extend_from_slice(&[0x00, 0x08]);
let schema = eight_byte_schema();
let options = DecodeOptions::default()
.with_format(RecordFormat::RDW)
.with_codepage(Codepage::ASCII);
let mut iterator = RecordIterator::new(Cursor::new(data), &schema, &options).unwrap();
assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
let error = iterator.read_raw_record().unwrap_err();
assert_eq!(error.code, ErrorCode::CBKF221_RDW_UNDERFLOW);
}
#[test]
fn rdw_file_ending_on_a_record_boundary_is_still_clean_eof() {
let mut data = vec![0x00, 0x08, 0x00, 0x00];
data.extend_from_slice(b"RECORD01");
let schema = eight_byte_schema();
let options = DecodeOptions::default()
.with_format(RecordFormat::RDW)
.with_codepage(Codepage::ASCII);
let mut iterator = RecordIterator::new(Cursor::new(data), &schema, &options).unwrap();
assert_eq!(iterator.read_raw_record().unwrap().unwrap(), b"RECORD01");
assert!(iterator.read_raw_record().unwrap().is_none());
assert!(iterator.is_eof());
}
}