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::{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
55/// Only DEFINE commands of the kinds above are kept, and a later definition of a kind and name
56/// replaces an earlier one.
57pub 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    /// The DD an extrapartition queue is written to, following one INDIRECT hop; None for an
103    /// intrapartition queue or one nobody defined.
104    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
128/// Whether `text`, after leading blanks, starts with `word` as a whole word, in any case.
129fn 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
134/// Each command with the 1-based line it starts on: comment lines dropped, and the lines after a
135/// command joined to it until the next one starts.
136fn 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}