1use 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
56pub 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 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
129fn 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
135fn 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}