Skip to main content

ironwork_syntax/
csd.rs

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