1#![allow(clippy::missing_inline_in_public_items)]
4use crate::lib_api::{decode_record, encode_record};
9use crate::options::{DecodeOptions, EncodeOptions};
10use copybook_core::{Error, ErrorCode, Result, Schema};
11use copybook_determinism::compare_outputs;
12use copybook_rdw::RdwHeader;
13
14pub use copybook_determinism::{ByteDiff, DeterminismMode, DeterminismResult};
15
16fn serialize_json(value: &serde_json::Value, context: &str) -> Result<Vec<u8>> {
17 serde_json::to_vec(value).map_err(|e| {
18 Error::new(
19 ErrorCode::CBKC201_JSON_WRITE_ERROR,
20 format!("Failed to serialize {context}: {e}"),
21 )
22 })
23}
24
25#[inline]
31#[must_use = "Handle the Result or propagate the error"]
32pub fn check_decode_determinism(
33 schema: &Schema,
34 data: &[u8],
35 options: &DecodeOptions,
36) -> Result<DeterminismResult> {
37 let payload = payload_for_format(data, options.format)?;
38 let value1 = decode_record(schema, payload, options)?;
39 let value2 = decode_record(schema, payload, options)?;
40
41 let json1 = serialize_json(&value1, "first decode result")?;
42 let json2 = serialize_json(&value2, "second decode result")?;
43
44 Ok(compare_outputs(DeterminismMode::DecodeOnly, &json1, &json2))
45}
46
47#[inline]
53#[must_use = "Handle the Result or propagate the error"]
54pub fn check_encode_determinism(
55 schema: &Schema,
56 json_data: &serde_json::Value,
57 options: &EncodeOptions,
58) -> Result<DeterminismResult> {
59 let binary1 = encode_record(schema, json_data, options)?;
60 let binary2 = encode_record(schema, json_data, options)?;
61
62 Ok(compare_outputs(
63 DeterminismMode::EncodeOnly,
64 &binary1,
65 &binary2,
66 ))
67}
68
69#[inline]
75#[must_use = "Handle the Result or propagate the error"]
76pub fn check_round_trip_determinism(
77 schema: &Schema,
78 data: &[u8],
79 decode_opts: &DecodeOptions,
80 encode_opts: &EncodeOptions,
81) -> Result<DeterminismResult> {
82 let decoded_payload = payload_for_format(data, decode_opts.format)?;
83 let json1 = decode_record(schema, decoded_payload, decode_opts)?;
84 let binary = encode_record(schema, &json1, encode_opts)?;
85 let encoded_payload = payload_for_format(&binary, decode_opts.format)?;
86 let json2 = decode_record(schema, encoded_payload, decode_opts)?;
87
88 let serialized1 = serialize_json(&json1, "first round-trip decode result")?;
89 let serialized2 = serialize_json(&json2, "second round-trip decode result")?;
90
91 Ok(compare_outputs(
92 DeterminismMode::RoundTrip,
93 &serialized1,
94 &serialized2,
95 ))
96}
97
98#[inline]
99fn payload_for_format(data: &[u8], format: crate::options::RecordFormat) -> Result<&[u8]> {
100 if format != crate::options::RecordFormat::RDW {
101 return Ok(data);
102 }
103
104 if data.len() < copybook_rdw::RDW_HEADER_LEN {
105 return Err(Error::new(
106 ErrorCode::CBKF221_RDW_UNDERFLOW,
107 "RDW data is shorter than the 4-byte RDW header",
108 ));
109 }
110
111 let header_slice = data.get(..copybook_rdw::RDW_HEADER_LEN).ok_or_else(|| {
112 Error::new(
113 ErrorCode::CBKF221_RDW_UNDERFLOW,
114 "RDW data is shorter than the 4-byte RDW header",
115 )
116 })?;
117
118 let header_bytes: [u8; copybook_rdw::RDW_HEADER_LEN] =
119 header_slice.try_into().map_err(|_| {
120 Error::new(
121 ErrorCode::CBKF221_RDW_UNDERFLOW,
122 "RDW header must be exactly 4 bytes",
123 )
124 })?;
125
126 let header = RdwHeader::from_bytes(header_bytes);
127
128 let payload_len = usize::from(header.length());
129 let expected_len = copybook_rdw::RDW_HEADER_LEN.saturating_add(payload_len);
130 if data.len() != expected_len {
131 return Err(Error::new(
132 ErrorCode::CBKF221_RDW_UNDERFLOW,
133 format!(
134 "RDW payload mismatch: expected {expected_len} bytes, got {}",
135 data.len()
136 ),
137 ));
138 }
139
140 Ok(&data[copybook_rdw::RDW_HEADER_LEN..expected_len])
141}
142
143#[cfg(test)]
144#[allow(clippy::expect_used)]
145#[allow(clippy::unwrap_used)]
146mod tests {
147 use super::*;
148 use crate::options::{Codepage, RecordFormat};
149 use copybook_core::parse_copybook;
150
151 fn decode_opts() -> DecodeOptions {
152 DecodeOptions::new().with_codepage(Codepage::CP037)
153 }
154
155 fn encode_opts() -> EncodeOptions {
156 EncodeOptions::new()
157 .with_codepage(Codepage::CP037)
158 .with_format(RecordFormat::Fixed)
159 }
160
161 #[test]
162 fn decode_deterministic_for_display_schema() {
163 let copybook = r"
164 01 RECORD.
165 05 FIELD-A PIC X(10).
166 ";
167 let schema = parse_copybook(copybook).expect("parse copybook");
168
169 let data: Vec<u8> = vec![0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xD1];
170
171 let result =
172 check_decode_determinism(&schema, &data, &decode_opts()).expect("determinism check");
173
174 assert!(
175 result.is_deterministic,
176 "Expected deterministic decode for DISPLAY-only schema"
177 );
178 assert_eq!(result.mode, DeterminismMode::DecodeOnly);
179 assert!(result.byte_differences.is_none());
180 assert_eq!(result.diff_count(), 0);
181 assert!(result.passed());
182 }
183
184 #[test]
185 fn decode_deterministic_for_comp3_schema() {
186 let copybook = r"
187 01 RECORD.
188 05 AMOUNT PIC S9(7)V99 COMP-3.
189 ";
190 let schema = parse_copybook(copybook).expect("parse copybook");
191
192 let data = vec![0x12, 0x34, 0x56, 0x78, 0x9C];
193
194 let result =
195 check_decode_determinism(&schema, &data, &decode_opts()).expect("determinism check");
196
197 assert!(
198 result.is_deterministic,
199 "Expected deterministic decode for COMP-3 schema"
200 );
201 assert!(result.passed());
202 }
203
204 #[test]
205 fn encode_deterministic_for_display_schema() {
206 let copybook = r"
207 01 RECORD.
208 05 FIELD-A PIC X(5).
209 ";
210 let schema = parse_copybook(copybook).expect("parse copybook");
211 let json = serde_json::json!({"FIELD-A": "HELLO"});
212
213 let result =
214 check_encode_determinism(&schema, &json, &encode_opts()).expect("determinism check");
215
216 assert!(
217 result.is_deterministic,
218 "Expected deterministic encode for DISPLAY-only schema"
219 );
220 assert_eq!(result.mode, DeterminismMode::EncodeOnly);
221 assert!(result.byte_differences.is_none());
222 }
223
224 #[test]
225 fn round_trip_deterministic() {
226 let copybook = r"
227 01 RECORD.
228 05 NAME PIC X(10).
229 05 AGE PIC 9(3).
230 ";
231 let schema = parse_copybook(copybook).expect("parse copybook");
232
233 let data: Vec<u8> = vec![
234 0xD1, 0xD6, 0xC8, 0xD5, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0xF1, 0xF2, 0xF3,
235 ];
236
237 let result = check_round_trip_determinism(&schema, &data, &decode_opts(), &encode_opts())
238 .expect("round-trip check");
239
240 assert!(result.is_deterministic, "Expected deterministic round-trip");
241 assert_eq!(result.mode, DeterminismMode::RoundTrip);
242 }
243
244 #[test]
245 fn detect_json_serialization_nondeterminism() {
246 let json1 = serde_json::json!({"FIELD": "VALUE1"});
247 let json2 = serde_json::json!({"FIELD": "VALUE2"});
248
249 let bytes1 = serde_json::to_vec(&json1).expect("serialize json1");
250 let bytes2 = serde_json::to_vec(&json2).expect("serialize json2");
251
252 let result = compare_outputs(DeterminismMode::DecodeOnly, &bytes1, &bytes2);
253 assert!(!result.is_deterministic);
254 assert!(result.diff_count() > 0);
255 }
256
257 #[test]
258 fn decode_error_propagates_correctly() {
259 let copybook = r"
260 01 RECORD.
261 05 AMOUNT PIC S9(7)V99 COMP-3.
262 ";
263 let schema = parse_copybook(copybook).expect("parse copybook");
264
265 let truncated_data = vec![0x12, 0x34];
266
267 let result = check_decode_determinism(&schema, &truncated_data, &decode_opts());
268
269 assert!(
270 result.is_err(),
271 "Should return error for truncated COMP-3 data"
272 );
273 }
274
275 #[test]
276 fn encode_error_propagates_correctly() {
277 let copybook = r"
278 01 RECORD.
279 05 FIELD PIC 9(5).
280 ";
281 let schema = parse_copybook(copybook).expect("parse copybook");
282
283 let invalid_json = serde_json::json!({"FIELD": "NOT_A_NUMBER"});
284
285 let result = check_encode_determinism(&schema, &invalid_json, &encode_opts());
286
287 assert!(
288 result.is_err(),
289 "Should return error for type mismatch in encoding"
290 );
291 }
292
293 #[test]
294 fn round_trip_error_propagates() {
295 let copybook = r"
296 01 RECORD.
297 05 AMOUNT PIC S9(7)V99 COMP-3.
298 ";
299 let schema = parse_copybook(copybook).expect("parse copybook");
300
301 let bad_data = vec![0x12, 0x34];
302
303 let result =
304 check_round_trip_determinism(&schema, &bad_data, &decode_opts(), &encode_opts());
305
306 assert!(
307 result.is_err(),
308 "Should return error for truncated data in round-trip"
309 );
310 }
311
312 #[test]
313 fn insufficient_data_handling_is_stable() {
314 let copybook = r"
315 01 RECORD.
316 05 FIELD PIC X(5).
317 ";
318 let schema = parse_copybook(copybook).expect("parse copybook");
319
320 let insufficient_data = vec![0x40, 0x40, 0x40];
321
322 let result = check_decode_determinism(&schema, &insufficient_data, &decode_opts());
323
324 if let Ok(det_result) = result {
325 assert!(
326 det_result.is_deterministic,
327 "If insufficient data is handled, it must be deterministic"
328 );
329 }
330 }
331}