1#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum FieldKind {
11 Alphanumeric,
13 Numeric { scale: usize, signed: bool },
15 Group(Vec<FieldDecl>),
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct FieldDecl {
24 pub name: String,
26 pub pic: String,
28 pub offset: usize,
30 pub length: usize,
32 pub kind: FieldKind,
34}
35
36impl FieldDecl {
37 pub fn alnum(name: impl Into<String>, pic: impl Into<String>, offset: usize, length: usize) -> Self {
39 FieldDecl { name: name.into(), pic: pic.into(), offset, length, kind: FieldKind::Alphanumeric }
40 }
41
42 pub fn numeric(
44 name: impl Into<String>,
45 pic: impl Into<String>,
46 offset: usize,
47 length: usize,
48 scale: usize,
49 signed: bool,
50 ) -> Self {
51 FieldDecl {
52 name: name.into(),
53 pic: pic.into(),
54 offset,
55 length,
56 kind: FieldKind::Numeric { scale, signed },
57 }
58 }
59
60 pub fn group(name: impl Into<String>, offset: usize, children: Vec<FieldDecl>) -> Self {
62 let length = children.iter().map(|c| c.length).sum();
63 FieldDecl { name: name.into(), pic: String::new(), offset, length, kind: FieldKind::Group(children) }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct Copybook {
71 pub record_name: String,
73 pub encoding: String,
76 pub fields: Vec<FieldDecl>,
78}
79
80impl Copybook {
81 pub fn record_length(&self) -> usize {
83 self.fields.iter().map(|f| f.length).sum()
84 }
85
86 pub fn canonical_bytes(&self) -> Vec<u8> {
90 let mut out = Vec::new();
91 out.extend_from_slice(b"COPYBOOK\x1f");
92 out.extend_from_slice(self.record_name.as_bytes());
93 out.push(0x1f);
94 out.extend_from_slice(self.encoding.as_bytes());
95 out.push(0x1e);
96 for f in &self.fields {
97 canon_field(f, &mut out);
98 }
99 out
100 }
101}
102
103fn canon_field(f: &FieldDecl, out: &mut Vec<u8>) {
104 out.extend_from_slice(f.name.as_bytes());
105 out.push(0x1f);
106 out.extend_from_slice(f.pic.as_bytes());
107 out.push(0x1f);
108 out.extend_from_slice(f.offset.to_string().as_bytes());
109 out.push(0x1f);
110 out.extend_from_slice(f.length.to_string().as_bytes());
111 out.push(0x1f);
112 match &f.kind {
113 FieldKind::Alphanumeric => out.extend_from_slice(b"A"),
114 FieldKind::Numeric { scale, signed } => {
115 out.extend_from_slice(b"N");
116 out.extend_from_slice(scale.to_string().as_bytes());
117 out.push(if *signed { b'S' } else { b'U' });
118 }
119 FieldKind::Group(children) => {
120 out.extend_from_slice(b"G{");
121 for c in children {
122 canon_field(c, out);
123 }
124 out.push(b'}');
125 }
126 }
127 out.push(0x1e);
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct Finding {
133 pub code: String,
135 pub message: String,
137}
138
139impl Finding {
140 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
142 Finding { code: code.into(), message: message.into() }
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct DecodedField {
150 pub name: String,
152 pub value: String,
154 pub raw: Vec<u8>,
156 pub decl: FieldDecl,
158 pub findings: Vec<Finding>,
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn record_length_sums_children() {
168 let cb = Copybook {
169 record_name: "REC".into(),
170 encoding: "ascii".into(),
171 fields: vec![
172 FieldDecl::alnum("NAME", "X(4)", 0, 4),
173 FieldDecl::numeric("AMT", "9(3)V99", 4, 5, 2, false),
174 ],
175 };
176 assert_eq!(cb.record_length(), 9);
177 }
178
179 #[test]
180 fn group_length_is_sum() {
181 let g = FieldDecl::group(
182 "G",
183 0,
184 vec![FieldDecl::alnum("A", "X(2)", 0, 2), FieldDecl::alnum("B", "X(3)", 2, 3)],
185 );
186 assert_eq!(g.length, 5);
187 }
188
189 #[test]
190 fn canonical_bytes_deterministic() {
191 let cb = Copybook {
192 record_name: "REC".into(),
193 encoding: "ascii".into(),
194 fields: vec![FieldDecl::alnum("NAME", "X(4)", 0, 4)],
195 };
196 assert_eq!(cb.canonical_bytes(), cb.canonical_bytes());
197 assert!(!cb.canonical_bytes().is_empty());
198 }
199}