copybook_record_io/
lib.rs1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2use copybook_error::{Error, ErrorCode, Result};
12use copybook_options::RecordFormat;
13use std::io::{Read, Write};
14
15pub use copybook_fixed::FixedRecordReader;
17pub use copybook_fixed::FixedRecordWriter;
19pub use copybook_rdw::RDWRecord;
21pub use copybook_rdw::RDWRecordReader;
23pub use copybook_rdw::RDWRecordWriter;
25
26#[inline]
31#[must_use = "Handle the Result or propagate the error"]
32pub fn read_record(
33 input: &mut impl Read,
34 format: RecordFormat,
35 lrecl: Option<u32>,
36) -> Result<Option<Vec<u8>>> {
37 match format {
38 RecordFormat::Fixed => read_fixed_record(input, lrecl),
39 RecordFormat::RDW => read_rdw_record(input),
40 }
41}
42
43#[inline]
44fn read_fixed_record(input: &mut impl Read, lrecl: Option<u32>) -> Result<Option<Vec<u8>>> {
45 let mut reader = FixedRecordReader::new(input, lrecl)?;
46 reader.read_record()
47}
48
49#[inline]
50fn read_rdw_record(input: &mut impl Read) -> Result<Option<Vec<u8>>> {
51 let mut reader = RDWRecordReader::new(input, false);
52 match reader.read_record()? {
53 Some(record) => Ok(Some(record.payload)),
54 None => Ok(None),
55 }
56}
57
58#[inline]
63#[must_use = "Handle the Result or propagate the error"]
64pub fn write_record(output: &mut impl Write, data: &[u8], format: RecordFormat) -> Result<()> {
65 match format {
66 RecordFormat::Fixed => {
67 output.write_all(data).map_err(|e| {
68 Error::new(
69 ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
70 format!("Write error: {e}"),
71 )
72 })?;
73 Ok(())
74 }
75 RecordFormat::RDW => {
76 let mut writer = RDWRecordWriter::new(output);
77 writer.write_record_from_payload(data, None)
78 }
79 }
80}
81
82#[cfg(test)]
83#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
84mod tests {
85 use super::*;
86 use proptest::collection::vec;
87 use proptest::prelude::*;
88 use std::io::Cursor;
89
90 #[test]
91 fn read_fixed_record_delegates_to_fixed_microcrate() {
92 let mut cursor = Cursor::new(b"LOCKTEST".to_vec());
93 let record = read_record(&mut cursor, RecordFormat::Fixed, Some(8))
94 .unwrap()
95 .unwrap();
96 assert_eq!(record, b"LOCKTEST");
97 }
98
99 #[test]
100 fn read_record_fixed_requires_lrecl() {
101 let mut cursor = Cursor::new(b"LOCKTEST".to_vec());
102 let err = read_record(&mut cursor, RecordFormat::Fixed, None).unwrap_err();
103 assert_eq!(err.code, ErrorCode::CBKI001_INVALID_STATE);
104 }
105
106 #[test]
107 fn read_rdw_record_returns_payload_only() {
108 let mut cursor = Cursor::new(vec![0x00, 0x04, 0x00, 0x00, b'T', b'E', b'S', b'T']);
109 let record = read_record(&mut cursor, RecordFormat::RDW, None)
110 .unwrap()
111 .unwrap();
112 assert_eq!(record, b"TEST");
113 }
114
115 #[test]
116 fn write_fixed_record_passthrough() {
117 let mut output = Vec::new();
118 write_record(&mut output, b"LOCK", RecordFormat::Fixed).unwrap();
119 assert_eq!(output, b"LOCK");
120 }
121
122 #[test]
123 fn write_rdw_record_emits_header_and_payload() {
124 let mut output = Vec::new();
125 write_record(&mut output, b"ABCD", RecordFormat::RDW).unwrap();
126 assert_eq!(output, vec![0x00, 0x04, 0x00, 0x00, b'A', b'B', b'C', b'D']);
127 }
128
129 #[test]
130 fn write_rdw_record_rejects_oversized_payload() {
131 let mut output = Vec::new();
132 let oversized = vec![0u8; usize::from(u16::MAX) + 1];
133 let err = write_record(&mut output, &oversized, RecordFormat::RDW).unwrap_err();
134 assert_eq!(err.code, ErrorCode::CBKE501_JSON_TYPE_MISMATCH);
135 }
136
137 proptest! {
138 #[test]
139 fn prop_fixed_roundtrip_when_lrecl_equals_payload_len(payload in vec(any::<u8>(), 1..=512)) {
140 let mut encoded = Vec::new();
141 write_record(&mut encoded, &payload, RecordFormat::Fixed).unwrap();
142
143 let mut cursor = Cursor::new(encoded);
144 let decoded = read_record(
145 &mut cursor,
146 RecordFormat::Fixed,
147 Some(u32::try_from(payload.len()).unwrap()),
148 )
149 .unwrap()
150 .unwrap();
151
152 prop_assert_eq!(decoded.as_slice(), payload.as_slice());
153 prop_assert!(read_record(
154 &mut cursor,
155 RecordFormat::Fixed,
156 Some(u32::try_from(payload.len()).unwrap()),
157 ).unwrap().is_none());
158 }
159
160 #[test]
161 fn prop_rdw_roundtrip_preserves_payload(payload in vec(any::<u8>(), 0..=1024)) {
162 let mut encoded = Vec::new();
163 write_record(&mut encoded, &payload, RecordFormat::RDW).unwrap();
164
165 let mut cursor = Cursor::new(encoded);
166 let decoded = read_record(&mut cursor, RecordFormat::RDW, None).unwrap().unwrap();
167 prop_assert_eq!(decoded, payload);
168 prop_assert!(read_record(&mut cursor, RecordFormat::RDW, None).unwrap().is_none());
169 }
170 }
171}