1use crate::{Error, Pos};
5use std::collections::BTreeMap;
6
7#[derive(Clone, Debug, Default, PartialEq, Eq)]
8pub struct Csd {
9 pub transactions: BTreeMap<String, Transaction>,
10 pub programs: BTreeMap<String, Program>,
11 pub files: BTreeMap<String, File>,
12 pub tdqueues: BTreeMap<String, TdQueue>,
13 pub urimaps: BTreeMap<String, Urimap>,
14}
15
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct Transaction {
18 pub program: Option<String>,
19 pub group: Option<String>,
20 pub line: u32,
21}
22
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct Program {
25 pub group: Option<String>,
26 pub line: u32,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct File {
31 pub dsname: Option<String>,
32 pub group: Option<String>,
33 pub line: u32,
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct TdQueue {
38 pub kind: Option<String>,
39 pub ddname: Option<String>,
40 pub indirect: Option<String>,
41 pub group: Option<String>,
42 pub line: u32,
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct Urimap {
47 pub usage: Option<String>,
48 pub path: Option<String>,
49 pub program: Option<String>,
50 pub transaction: Option<String>,
51 pub group: Option<String>,
52 pub line: u32,
53}
54
55pub fn parse(text: &str) -> Result<Csd, Error> {
58 let mut csd = Csd::default();
59 for (line, block) in blocks(text) {
60 if !starts_with_word(&block, "DEFINE") {
61 continue;
62 }
63 if block.bytes().filter(|&b| b == b'(').count() > block.bytes().filter(|&b| b == b')').count() {
64 return Err(fail(line, "unbalanced '(' in DEFINE"));
65 }
66 let Some((kind, name, rest)) = parse_head(&block) else {
67 return Err(fail(line, "DEFINE names no KIND(NAME)"));
68 };
69 let limit = match kind.as_str() {
70 "TRANSACTION" => 4,
71 "PROGRAM" | "FILE" | "TDQUEUE" | "URIMAP" => 8,
72 _ => continue,
73 };
74 if name.len() > limit {
75 return Err(fail(line, format!("{kind}({name}): a {kind} name is at most {limit} characters")));
76 }
77 let attrs = parse_attrs(rest, line)?;
78 let get = |keyword: &str| attrs.get(keyword).cloned();
79 match kind.as_str() {
80 "TRANSACTION" => {
81 csd.transactions.insert(name, Transaction { program: get("PROGRAM"), group: get("GROUP"), line });
82 }
83 "PROGRAM" => {
84 csd.programs.insert(name, Program { group: get("GROUP"), line });
85 }
86 "FILE" => {
87 csd.files.insert(name, File { dsname: get("DSNAME"), group: get("GROUP"), line });
88 }
89 "TDQUEUE" => {
90 csd.tdqueues.insert(name, TdQueue { kind: get("TYPE"), ddname: get("DDNAME"), indirect: get("INDIRECTNAME"), group: get("GROUP"), line });
91 }
92 _ => {
93 let urimap = Urimap { usage: get("USAGE"), path: get("PATH"), program: get("PROGRAM"), transaction: get("TRANSACTION"), group: get("GROUP"), line };
94 csd.urimaps.insert(name, urimap);
95 }
96 }
97 }
98 Ok(csd)
99}
100
101impl Csd {
102 pub fn dd_of_queue(&self, queue: &str) -> Option<&str> {
105 let q = self.tdqueues.get(queue)?;
106 match q.kind.as_deref() {
107 Some("EXTRA") => q.ddname.as_deref(),
108 Some("INDIRECT") => {
109 let target = q.indirect.as_deref()?;
110 let target_q = self.tdqueues.get(target)?;
111 if target_q.kind.as_deref() == Some("EXTRA") {
112 target_q.ddname.as_deref()
113 } else {
114 None
115 }
116 }
117 _ => None,
118 }
119 }
120}
121
122fn fail(line: u32, message: impl Into<String>) -> Error {
123 Error::at(Pos { file: 0, line, col: 1 }, message)
124}
125
126const COMMANDS: [&str; 14] = ["DEFINE", "ADD", "ALTER", "APPEND", "COPY", "DELETE", "INITIALIZE", "LIST", "REMOVE", "SCAN", "SERVICE", "UPGRADE", "USERDEFINE", "VERIFY"];
127
128fn starts_with_word(text: &str, word: &str) -> bool {
130 let text = text.trim_start().as_bytes();
131 text.len() >= word.len() && text[..word.len()].eq_ignore_ascii_case(word.as_bytes()) && !text.get(word.len()).is_some_and(u8::is_ascii_alphanumeric)
132}
133
134fn blocks(text: &str) -> Vec<(u32, String)> {
137 let mut blocks: Vec<(u32, String)> = Vec::new();
138 for (line, n) in text.lines().zip(1u32..) {
139 if line.trim_start().starts_with('*') {
140 continue;
141 }
142 if COMMANDS.iter().any(|c| starts_with_word(line, c)) {
143 blocks.push((n, line.to_owned()));
144 } else if let Some((_, block)) = blocks.last_mut() {
145 block.push(' ');
146 block.push_str(line);
147 }
148 }
149 blocks
150}
151
152fn parse_head(text: &str) -> Option<(String, String, &str)> {
153 let bytes = text.as_bytes();
154 let mut i = 0;
155
156 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
157 i += 1;
158 }
159
160 if !text[i..].to_ascii_uppercase().starts_with("DEFINE") {
161 return None;
162 }
163 i += 6;
164
165 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
166 i += 1;
167 }
168
169 let kind_start = i;
170 while i < bytes.len() && bytes[i].is_ascii_alphanumeric() {
171 i += 1;
172 }
173 let kind = text[kind_start..i].to_uppercase();
174
175 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
176 i += 1;
177 }
178
179 if i >= bytes.len() || bytes[i] != b'(' {
180 return None;
181 }
182 i += 1;
183
184 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
185 i += 1;
186 }
187
188 let name_start = i;
189 while i < bytes.len() && !bytes[i].is_ascii_whitespace() && bytes[i] != b')' {
190 i += 1;
191 }
192 let name = text[name_start..i].to_uppercase();
193 if kind.is_empty() || name.is_empty() {
194 return None;
195 }
196
197 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
198 i += 1;
199 }
200
201 if i >= bytes.len() || bytes[i] != b')' {
202 return None;
203 }
204 i += 1;
205
206 Some((kind, name, &text[i..]))
207}
208
209fn parse_attrs(rest: &str, line: u32) -> Result<BTreeMap<String, String>, Error> {
210 let mut attrs = BTreeMap::new();
211 let bytes = rest.as_bytes();
212 let mut i = 0;
213
214 while i < bytes.len() {
215 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
216 i += 1;
217 }
218 if i >= bytes.len() {
219 break;
220 }
221
222 let key_start = i;
223 while i < bytes.len() && bytes[i].is_ascii_alphanumeric() {
224 i += 1;
225 }
226 let key = &rest[key_start..i];
227 if key.is_empty() {
228 i += 1;
229 continue;
230 }
231
232 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
233 i += 1;
234 }
235
236 if i >= bytes.len() || bytes[i] != b'(' {
237 continue;
238 }
239 i += 1;
240
241 let val_start = i;
242 let mut val_len = 0;
243 while i < bytes.len() && bytes[i] != b')' {
244 i += 1;
245 val_len += 1;
246 if val_len > 256 {
247 return Err(fail(
248 line,
249 format!("attribute value for '{}' exceeds 256 characters", key),
250 ));
251 }
252 }
253
254 if i >= bytes.len() {
255 return Err(fail(line, "unbalanced '(' in attribute value"));
256 }
257 i += 1;
258
259 let val = rest[val_start..i - 1].trim();
260 let val = if key.eq_ignore_ascii_case("PATH") {
261 val.to_string()
262 } else {
263 val.to_uppercase()
264 };
265
266 attrs.insert(key.to_uppercase(), val);
267 }
268
269 Ok(attrs)
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn parse_basic_definitions() {
278 let input = "DEFINE TRANSACTION(CECI) GROUP(LOCAL)\n PROGRAM(DFHECIP) TASKDATALOC(ANY)\n DEFINE TRANSACTION(PAY1) GROUP(PAYROLL)\n PROGRAM(PAYADM) CMDSEC(NO) RESSEC(NO)\n DEFINE TDQUEUE(JOBS) GROUP(APP)\n DESCRIPTION(SUBMIT JOBS FROM CICS)\n TYPE(EXTRA) DDNAME(INREADER)\n DEFINE TDQUEUE(LOGQ) GROUP(APP) TYPE(INDIRECT) INDIRECTNAME(JOBS)\n DEFINE PROGRAM(PAYADM) GROUP(PAYROLL) LANGUAGE(COBOL)\n DEFINE FILE(ACCTDAT) GROUP(APP) DSNAME(AWS.M2.CARDDEMO.ACCTDATA.VSAM.KSDS)\n DEFINE URIMAP(ECHO) GROUP(WEB) USAGE(SERVER) PATH(/cics/echo) PROGRAM(WEBECHO)\n ADD GROUP(PAYROLL) LIST(REGLIST)";
279 let csd = parse(input).unwrap();
280
281 assert_eq!(csd.transactions.len(), 2);
282 let ceci = csd.transactions.get("CECI").unwrap();
283 assert_eq!(ceci.program.as_deref(), Some("DFHECIP"));
284 assert_eq!(ceci.group.as_deref(), Some("LOCAL"));
285 assert_eq!(ceci.line, 1);
286
287 let pay1 = csd.transactions.get("PAY1").unwrap();
288 assert_eq!(pay1.program.as_deref(), Some("PAYADM"));
289 assert_eq!(pay1.group.as_deref(), Some("PAYROLL"));
290 assert_eq!(pay1.line, 3);
291
292 assert_eq!(csd.tdqueues.len(), 2);
293 let jobs = csd.tdqueues.get("JOBS").unwrap();
294 assert_eq!(jobs.kind.as_deref(), Some("EXTRA"));
295 assert_eq!(jobs.ddname.as_deref(), Some("INREADER"));
296 assert_eq!(jobs.indirect, None);
297 assert_eq!(jobs.group.as_deref(), Some("APP"));
298 assert_eq!(jobs.line, 5);
299
300 let logq = csd.tdqueues.get("LOGQ").unwrap();
301 assert_eq!(logq.kind.as_deref(), Some("INDIRECT"));
302 assert_eq!(logq.indirect.as_deref(), Some("JOBS"));
303 assert_eq!(logq.ddname, None);
304 assert_eq!(logq.line, 8);
305
306 assert_eq!(csd.programs.len(), 1);
307 let payadm = csd.programs.get("PAYADM").unwrap();
308 assert_eq!(payadm.group.as_deref(), Some("PAYROLL"));
309 assert_eq!(payadm.line, 9);
310
311 assert_eq!(csd.files.len(), 1);
312 let acct = csd.files.get("ACCTDAT").unwrap();
313 assert_eq!(acct.dsname.as_deref(), Some("AWS.M2.CARDDEMO.ACCTDATA.VSAM.KSDS"));
314 assert_eq!(acct.group.as_deref(), Some("APP"));
315 assert_eq!(acct.line, 10);
316
317 assert_eq!(csd.urimaps.len(), 1);
318 let echo = csd.urimaps.get("ECHO").unwrap();
319 assert_eq!(echo.usage.as_deref(), Some("SERVER"));
320 assert_eq!(echo.path.as_deref(), Some("/cics/echo"));
321 assert_eq!(echo.program.as_deref(), Some("WEBECHO"));
322 assert_eq!(echo.group.as_deref(), Some("WEB"));
323 assert_eq!(echo.line, 11);
324 }
325
326 #[test]
327 fn continuation_lines_and_blanks() {
328 let input = " DEFINE TRANSACTION(CECI)\n GROUP(LOCAL)\n PROGRAM(DFHECIP)";
329 let csd = parse(input).unwrap();
330 let t = csd.transactions.get("CECI").unwrap();
331 assert_eq!(t.program.as_deref(), Some("DFHECIP"));
332 assert_eq!(t.group.as_deref(), Some("LOCAL"));
333 assert_eq!(t.line, 1);
334 }
335
336 #[test]
337 fn comments_and_non_define_skipped() {
338 let input = "* comment\nDEFINE TRANSACTION(CECI) GROUP(LOCAL)\nADD GROUP(LOCAL)\nLIST ALL\nDEFINE TRANSACTION(PAY1) GROUP(PAYROLL)";
339 let csd = parse(input).unwrap();
340 assert_eq!(csd.transactions.len(), 2);
341 assert!(csd.transactions.contains_key("CECI"));
342 assert!(csd.transactions.contains_key("PAY1"));
343 }
344
345 #[test]
346 fn later_define_replaces_earlier() {
347 let input = "DEFINE TRANSACTION(CECI) GROUP(LOCAL)\nDEFINE TRANSACTION(CECI) GROUP(NEW)";
348 let csd = parse(input).unwrap();
349 assert_eq!(csd.transactions.len(), 1);
350 let t = csd.transactions.get("CECI").unwrap();
351 assert_eq!(t.group.as_deref(), Some("NEW"));
352 assert_eq!(t.line, 2);
353 }
354
355 #[test]
356 fn path_keeps_case() {
357 let input = "DEFINE URIMAP(ECHO) PATH(/Cics/Echo)";
358 let csd = parse(input).unwrap();
359 let u = csd.urimaps.get("ECHO").unwrap();
360 assert_eq!(u.path.as_deref(), Some("/Cics/Echo"));
361 }
362
363 #[test]
364 fn dd_of_queue_extra() {
365 let input = "DEFINE TDQUEUE(JOBS) TYPE(EXTRA) DDNAME(INREADER)";
366 let csd = parse(input).unwrap();
367 assert_eq!(csd.dd_of_queue("JOBS"), Some("INREADER"));
368 }
369
370 #[test]
371 fn dd_of_queue_indirect() {
372 let input = "DEFINE TDQUEUE(JOBS) TYPE(EXTRA) DDNAME(INREADER)\nDEFINE TDQUEUE(LOGQ) TYPE(INDIRECT) INDIRECTNAME(JOBS)";
373 let csd = parse(input).unwrap();
374 assert_eq!(csd.dd_of_queue("LOGQ"), Some("INREADER"));
375 }
376
377 #[test]
378 fn dd_of_queue_intra() {
379 let input = "DEFINE TDQUEUE(JOBS) TYPE(INTRA)";
380 let csd = parse(input).unwrap();
381 assert_eq!(csd.dd_of_queue("JOBS"), None);
382 }
383
384 #[test]
385 fn dd_of_queue_indirect_to_intra() {
386 let input = "DEFINE TDQUEUE(JOBS) TYPE(INTRA)\nDEFINE TDQUEUE(LOGQ) TYPE(INDIRECT) INDIRECTNAME(JOBS)";
387 let csd = parse(input).unwrap();
388 assert_eq!(csd.dd_of_queue("LOGQ"), None);
389 }
390
391 #[test]
392 fn error_no_kind_name() {
393 let input = "DEFINE TRANSACTION";
394 let err = parse(input).unwrap_err();
395 assert_eq!(err.pos.line, 1);
396 assert!(err.message.contains("names no KIND(NAME)"));
397 }
398
399 #[test]
400 fn error_name_too_long_transaction() {
401 let input = "DEFINE TRANSACTION(CECIX) GROUP(LOCAL)";
402 let err = parse(input).unwrap_err();
403 assert_eq!(err.pos.line, 1);
404 assert!(err.message.contains("at most 4 characters"));
405 }
406
407 #[test]
408 fn error_name_too_long_program() {
409 let input = "DEFINE PROGRAM(PAYADMINX) GROUP(LOCAL)";
410 let err = parse(input).unwrap_err();
411 assert_eq!(err.pos.line, 1);
412 assert!(err.message.contains("at most 8 characters"));
413 }
414
415 #[test]
416 fn skip_unknown_kind_long_name() {
417 let input = "DEFINE TCPIPSERVICE(SECUREWEB) GROUP(LOCAL)";
418 let csd = parse(input).unwrap();
419 assert!(csd.transactions.is_empty());
420 assert!(csd.programs.is_empty());
421 assert!(csd.files.is_empty());
422 assert!(csd.tdqueues.is_empty());
423 assert!(csd.urimaps.is_empty());
424 }
425
426 #[test]
427 fn error_value_too_long() {
428 let long_val = "A".repeat(300);
429 let input = format!("DEFINE TRANSACTION(CECI) GROUP({})", long_val);
430 let err = parse(&input).unwrap_err();
431 assert_eq!(err.pos.line, 1);
432 assert!(err.message.contains("exceeds 256 characters"));
433 }
434
435 #[test]
436 fn crlf_same_as_lf() {
437 let lf_input = "DEFINE TRANSACTION(CECI) GROUP(LOCAL)\nPROGRAM(DFHECIP)";
438 let crlf_input = "DEFINE TRANSACTION(CECI) GROUP(LOCAL)\r\nPROGRAM(DFHECIP)";
439 let csd_lf = parse(lf_input).unwrap();
440 let csd_crlf = parse(crlf_input).unwrap();
441 assert_eq!(csd_lf, csd_crlf);
442 }
443
444 #[test]
445 fn unbalanced_paren() {
446 let input = "DEFINE TRANSACTION(CECI GROUP(LOCAL)";
447 let err = parse(input).unwrap_err();
448 assert_eq!(err.pos.line, 1);
449 assert!(err.message.contains("unbalanced '('"));
450 }
451}