Skip to main content

ironwork_exec/
cics.rs

1//! The CICS region a harness run stands in for: one task, its files, its temporary-storage and
2//! transient-data queues, and the time services. The interpreter runs the commands; this holds
3//! what they act on.
4
5use crate::files::{Dd, Format, KeySpan, Keying};
6use crate::unit;
7use std::collections::{BTreeMap, HashMap};
8use std::fs::OpenOptions;
9use std::io::Write;
10use std::path::PathBuf;
11
12/// The EXEC interface block's length: EIBRLDBK, its last field, is at X'54'.
13pub const EIB_LEN: usize = 85;
14
15/// What kind of VSAM data set a CICS file is.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum DataSet {
18    Ksds { key: KeySpan },
19    Rrds,
20}
21
22/// A file-control table entry: a CICS FILE name's data set, given with --file.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct FileDef {
25    pub dd: Dd,
26    pub data_set: DataSet,
27    pub record_len: usize,
28}
29
30impl FileDef {
31    /// How files::open_keyed keys the data set.
32    pub fn keying(&self) -> Keying {
33        match &self.data_set {
34            DataSet::Ksds { key } => Keying::Indexed { prime: *key, alternates: vec![] },
35            DataSet::Rrds => Keying::Relative,
36        }
37    }
38
39    pub fn format(&self) -> Format {
40        self.dd.format.unwrap_or(Format::Fixed)
41    }
42}
43
44/// Parses `NAME=path,KSDS,key=OFFSET:LENGTH,len=RECLEN[,text|,variable]` or
45/// `NAME=path,RRDS,len=RECLEN[,text|,variable]`.
46pub fn parse_file(spec: &str) -> Result<(String, FileDef), String> {
47    let parts: Vec<&str> = spec.split(',').collect();
48    if parts.len() < 3 {
49        return Err(format!("{spec}: expected NAME=path,KIND,…"));
50    }
51
52    let (name, path) = parts[0]
53        .split_once('=')
54        .ok_or_else(|| format!("{spec}: expected NAME=path"))?;
55    let name = name.to_ascii_uppercase();
56    let kind = parts[1].to_ascii_uppercase();
57
58    let mut key: Option<KeySpan> = None;
59    let mut record_len: Option<usize> = None;
60    let mut format: Option<Format> = None;
61
62    for part in &parts[2..] {
63        let part = part.trim();
64        if let Some(val) = part.strip_prefix("key=") {
65            let (off, len) = val
66                .split_once(':')
67                .ok_or_else(|| format!("{spec}: bad key spec {val}"))?;
68            let off: usize = off.parse().map_err(|_| format!("{spec}: bad key offset {off}"))?;
69            let len: usize = len.parse().map_err(|_| format!("{spec}: bad key length {len}"))?;
70            key = Some(KeySpan { offset: off, len });
71        } else if let Some(val) = part.strip_prefix("len=") {
72            record_len = Some(
73                val.parse()
74                    .map_err(|_| format!("{spec}: bad record length {val}"))?,
75            );
76        } else if part.eq_ignore_ascii_case("text") {
77            format = Some(Format::Text);
78        } else if part.eq_ignore_ascii_case("variable") {
79            format = Some(Format::Variable);
80        } else {
81            return Err(format!("{spec}: unknown field {part}"));
82        }
83    }
84
85    let data_set = match kind.as_str() {
86        "KSDS" => {
87            let key = key.ok_or_else(|| format!("{spec}: KSDS requires key=OFFSET:LENGTH"))?;
88            DataSet::Ksds { key }
89        }
90        "RRDS" => DataSet::Rrds,
91        _ => return Err(format!("{spec}: unknown data set kind {kind}")),
92    };
93
94    let record_len = record_len.ok_or_else(|| format!("{spec}: missing len=RECLEN"))?;
95    let dd = Dd { path: PathBuf::from(path), format };
96    Ok((name, FileDef { dd, data_set, record_len }))
97}
98
99/// A temporary-storage queue: its items (item numbers start at 1) and where READQ TS NEXT is.
100#[derive(Clone, Debug, Default, PartialEq, Eq)]
101pub struct TsQueue {
102    pub items: Vec<Vec<u8>>,
103    pub next: usize,
104}
105
106/// The task's terminal: SEND writes a 3270 data stream to it, and RECEIVE reads the stream the
107/// operator's next AID key sends back.
108pub trait Terminal: std::fmt::Debug {
109    fn size(&self) -> (usize, usize);
110    fn send(&mut self, stream: &[u8]) -> Result<(), String>;
111    /// None when the operator has nothing more to send.
112    fn receive(&mut self) -> Result<Option<Vec<u8>>, String>;
113}
114
115/// A browse's position: the key it is at, and whether the record there is itself next (after
116/// STARTBR or RESETBR) or was the last returned.
117#[derive(Clone, Debug, Default, PartialEq, Eq)]
118pub struct Browse {
119    pub at: Vec<u8>,
120    pub inclusive: bool,
121}
122
123/// The task: who and what started it, and the resources it has touched.
124#[derive(Debug, Default)]
125pub struct Task {
126    pub transid: String,
127    pub termid: String,
128    pub userid: String,
129    pub applid: String,
130    pub sysid: String,
131    pub number: u32,
132    /// The COMMAREA the task starts with; EIBCALEN is its length.
133    pub commarea: Option<Vec<u8>>,
134    pub files: HashMap<String, FileDef>,
135    pub ts: BTreeMap<String, TsQueue>,
136    pub td: BTreeMap<String, Vec<Vec<u8>>>,
137    /// TD queues whose items are appended to a host file as text lines when the task ends.
138    pub td_files: HashMap<String, PathBuf>,
139    /// The key each file's READ UPDATE holds, for REWRITE, DELETE and UNLOCK.
140    pub held: HashMap<String, Vec<u8>>,
141    /// Open browses by file and REQID.
142    pub browses: HashMap<(String, i64), Browse>,
143    pub terminal: Option<Box<dyn Terminal>>,
144    /// The AID key whose input started the task, as EIBAID shows it before any RECEIVE.
145    pub initial_aid: Option<u8>,
146    /// Mapsets already read from the copy libraries, by name.
147    pub mapsets: HashMap<String, syntax::bms::Mapset>,
148    /// RETURN TRANSID and COMMAREA, when the task ended that way.
149    pub next_transid: Option<String>,
150    pub returned_commarea: Option<Vec<u8>>,
151}
152
153impl Task {
154    /// WRITEQ TS: appends and returns the new item's number; with Some(item) (REWRITE) replaces
155    /// that item and returns it.
156    pub fn writeq_ts(
157        &mut self,
158        queue: &str,
159        rewrite: Option<usize>,
160        data: &[u8],
161    ) -> Result<usize, &'static str> {
162        let queue = queue.trim_end();
163        match rewrite {
164            None => {
165                let q = self
166                    .ts
167                    .entry(queue.to_string())
168                    .or_default();
169                q.items.push(data.to_vec());
170                Ok(q.items.len())
171            }
172            Some(item) => {
173                let q = self.ts.get_mut(queue).ok_or("QIDERR")?;
174                if item == 0 || item > q.items.len() {
175                    return Err("ITEMERR");
176                }
177                q.items[item - 1] = data.to_vec();
178                Ok(item)
179            }
180        }
181    }
182
183    /// READQ TS: Some(item) reads that item; None reads the next item after the last read.
184    pub fn readq_ts(
185        &mut self,
186        queue: &str,
187        item: Option<usize>,
188    ) -> Result<(Vec<u8>, usize), &'static str> {
189        let queue = queue.trim_end();
190        let q = self.ts.get_mut(queue).ok_or("QIDERR")?;
191        let item_num = match item {
192            Some(n) => {
193                if n == 0 || n > q.items.len() {
194                    return Err("ITEMERR");
195                }
196                n
197            }
198            None => {
199                let n = if q.next == 0 { 1 } else { q.next + 1 };
200                if n > q.items.len() {
201                    return Err("ITEMERR");
202                }
203                n
204            }
205        };
206        let data = q.items[item_num - 1].clone();
207        q.next = item_num;
208        Ok((data, q.items.len()))
209    }
210
211    /// DELETEQ TS.
212    pub fn deleteq_ts(&mut self, queue: &str) -> Result<(), &'static str> {
213        let queue = queue.trim_end();
214        self.ts.remove(queue).map(|_| ()).ok_or("QIDERR")
215    }
216
217    /// WRITEQ TD: appends (creating the queue).
218    pub fn writeq_td(&mut self, queue: &str, data: &[u8]) {
219        let queue = queue.trim_end();
220        self.td.entry(queue.to_string()).or_default().push(data.to_vec());
221    }
222
223    /// READQ TD: removes and returns the oldest item.
224    pub fn readq_td(&mut self, queue: &str) -> Result<Vec<u8>, &'static str> {
225        let queue = queue.trim_end();
226        let items = self.td.get_mut(queue).ok_or("QZERO")?;
227        if items.is_empty() {
228            return Err("QZERO");
229        }
230        Ok(items.remove(0))
231    }
232
233    /// DELETEQ TD: empties the queue (no error when absent).
234    pub fn deleteq_td(&mut self, queue: &str) {
235        let queue = queue.trim_end();
236        if let Some(items) = self.td.get_mut(queue) {
237            items.clear();
238        }
239    }
240
241    /// Appends each TD queue that has a td_files entry to its file, one line per item, each item
242    /// decoded through `page` with trailing spaces trimmed; then empties those queues.
243    pub fn flush_td(&mut self, page: &zarch::ebcdic::CodePage) -> std::io::Result<()> {
244        let queues: Vec<String> = self.td_files.keys().cloned().collect();
245        for queue in &queues {
246            let Some(path) = self.td_files.get(queue) else { continue };
247            let items: Vec<Vec<u8>> = self
248                .td
249                .get(queue)
250                .cloned()
251                .unwrap_or_default();
252            if items.is_empty() {
253                continue;
254            }
255            let mut file = OpenOptions::new().create(true).append(true).open(path)?;
256            for item in &items {
257                let text = page.decode(item);
258                let line = format!("{}\n", text.trim_end());
259                file.write_all(line.as_bytes())?;
260            }
261            if let Some(q) = self.td.get_mut(queue) {
262                q.clear();
263            }
264        }
265        Ok(())
266    }
267}
268
269/// Milliseconds from 1900-01-01T00:00:00 to a Unix time given as seconds and hundredths.
270pub fn abstime(seconds: i64, hundredths: u32) -> i64 {
271    2_208_988_800_000 + seconds * 1000 + hundredths as i64 * 10
272}
273
274/// Unix seconds (rounded down) of an ABSTIME.
275pub fn unix_seconds(abstime: i64) -> i64 {
276    (abstime - 2_208_988_800_000).div_euclid(1000)
277}
278
279/// EIBDATE's value for an ABSTIME: 0CYYDDD as a decimal number.
280pub fn eib_date(abstime: i64) -> i64 {
281    let secs = unix_seconds(abstime);
282    let (year, _, _, _, _, _, doy, _) = unit::civil(secs);
283    let c = if year >= 2000 { 1 } else { 0 };
284    let yy = year % 100;
285    c * 100_000 + yy * 1_000 + doy as i64
286}
287
288/// EIBTIME's value for an ABSTIME: 0HHMMSS as a decimal number.
289pub fn eib_time(abstime: i64) -> i64 {
290    let secs = unix_seconds(abstime);
291    let (_, _, _, hour, minute, second, _, _) = unit::civil(secs);
292    hour as i64 * 10_000 + minute as i64 * 100 + second as i64
293}
294
295/// A FORMATTIME output: text for the date and time forms, a number for the counts.
296#[derive(Clone, Debug, PartialEq, Eq)]
297pub enum FormatValue {
298    Text(String),
299    Number(i64),
300}
301
302/// One FORMATTIME option's value for an ABSTIME, or None for an option this does not produce.
303/// `datesep` and `timesep` are the separators when DATESEP / TIMESEP were given.
304pub fn format_time(abstime: i64, option: &str, datesep: Option<char>, timesep: Option<char>) -> Option<FormatValue> {
305    let secs = unix_seconds(abstime);
306    let (year, month, day, hour, minute, second, doy, weekday) = unit::civil(secs);
307
308    let yy = format!("{:02}", year % 100);
309    let yyyy = format!("{:04}", year);
310    let mm = format!("{:02}", month);
311    let dd = format!("{:02}", day);
312    let hh = format!("{:02}", hour);
313    let mi = format!("{:02}", minute);
314    let ss = format!("{:02}", second);
315    let ddd = format!("{:03}", doy);
316
317    let join = |parts: &[&str], sep: Option<char>| match sep {
318        Some(c) => {
319            let mut s = String::new();
320            for (i, p) in parts.iter().enumerate() {
321                if i > 0 {
322                    s.push(c);
323                }
324                s.push_str(p);
325            }
326            s
327        }
328        None => parts.concat(),
329    };
330
331    let option = option.to_ascii_uppercase();
332
333    match option.as_str() {
334        "YYYYMMDD" => Some(FormatValue::Text(join(&[&yyyy, &mm, &dd], datesep))),
335        "YYMMDD" => Some(FormatValue::Text(join(&[&yy, &mm, &dd], datesep))),
336        "YYDDMM" => Some(FormatValue::Text(join(&[&yy, &dd, &mm], datesep))),
337        "YYYYDDMM" => Some(FormatValue::Text(join(&[&yyyy, &dd, &mm], datesep))),
338        "DDMMYY" => Some(FormatValue::Text(join(&[&dd, &mm, &yy], datesep))),
339        "DDMMYYYY" => Some(FormatValue::Text(join(&[&dd, &mm, &yyyy], datesep))),
340        "MMDDYY" | "DATE" => Some(FormatValue::Text(join(&[&mm, &dd, &yy], datesep))),
341        "MMDDYYYY" | "FULLDATE" => Some(FormatValue::Text(join(&[&mm, &dd, &yyyy], datesep))),
342        "YYDDD" => Some(FormatValue::Text(join(&[&yy, &ddd], datesep))),
343        "YYYYDDD" => Some(FormatValue::Text(join(&[&yyyy, &ddd], datesep))),
344        "DATEFORM" => Some(FormatValue::Text("MMDDYY".to_string())),
345        "TIME" => Some(FormatValue::Text(join(&[&hh, &mi, &ss], timesep))),
346        "DAYCOUNT" => Some(FormatValue::Number((secs + 2_208_988_800) / 86_400)),
347        "DAYOFWEEK" => {
348            let dow = if weekday == 7 { 0 } else { weekday as i64 - 1 };
349            Some(FormatValue::Number(dow))
350        }
351        "DAYOFMONTH" => Some(FormatValue::Number(day as i64)),
352        "MONTHOFYEAR" => Some(FormatValue::Number(month as i64)),
353        "YEAR" => Some(FormatValue::Number(year)),
354        "MILLISECONDS" => Some(FormatValue::Number(abstime.rem_euclid(1000))),
355        _ => None,
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn parse_file_ksds() {
365        let (name, def) = parse_file("MYFILE=/tmp/data,KSDS,key=0:8,len=80").unwrap();
366        assert_eq!(name, "MYFILE");
367        assert_eq!(def.record_len, 80);
368        assert!(matches!(&def.data_set, DataSet::Ksds { key } if key.offset == 0 && key.len == 8));
369    }
370
371    #[test]
372    fn parse_file_rrds_text() {
373        let (name, def) = parse_file("myfile=/tmp/data,rrds,len=120,text").unwrap();
374        assert_eq!(name, "MYFILE");
375        assert_eq!(def.record_len, 120);
376        assert!(matches!(def.data_set, DataSet::Rrds));
377        assert_eq!(def.dd.format, Some(Format::Text));
378    }
379
380    #[test]
381    fn parse_file_errors() {
382        assert!(parse_file("F=/tmp/d,KSDS,len=80").is_err());
383        assert!(parse_file("F=/tmp/d,RRDS,len=abc").is_err());
384        assert!(parse_file("F=/tmp/d,FOO,len=80").is_err());
385    }
386
387    #[test]
388    fn ts_queue() {
389        let mut task = Task::default();
390        task.writeq_ts("Q1", None, b"one").unwrap();
391        task.writeq_ts("Q1", None, b"two").unwrap();
392        task.writeq_ts("Q1", None, b"three").unwrap();
393
394        let (data, count) = task.readq_ts("Q1", Some(2)).unwrap();
395        assert_eq!(data, b"two");
396        assert_eq!(count, 3);
397
398        let (data, count) = task.readq_ts("Q1", None).unwrap();
399        assert_eq!(data, b"three");
400        assert_eq!(count, 3);
401
402        assert_eq!(task.readq_ts("Q1", None), Err("ITEMERR"));
403
404        task.writeq_ts("Q1", Some(1), b"ONE").unwrap();
405        let (data, _) = task.readq_ts("Q1", Some(1)).unwrap();
406        assert_eq!(data, b"ONE");
407
408        assert_eq!(task.writeq_ts("Q1", Some(9), b"x"), Err("ITEMERR"));
409        assert_eq!(task.readq_ts("NOPE", None), Err("QIDERR"));
410
411        task.deleteq_ts("Q1").unwrap();
412        assert_eq!(task.readq_ts("Q1", None), Err("QIDERR"));
413    }
414
415    #[test]
416    fn td_queue() {
417        let mut task = Task::default();
418        task.writeq_td("Q1", b"first");
419        task.writeq_td("Q1", b"second");
420        task.writeq_td("Q1", b"third");
421
422        assert_eq!(task.readq_td("Q1").unwrap(), b"first");
423        assert_eq!(task.readq_td("Q1").unwrap(), b"second");
424        assert_eq!(task.readq_td("Q1").unwrap(), b"third");
425        assert_eq!(task.readq_td("Q1"), Err("QZERO"));
426    }
427
428    #[test]
429    fn abstime_and_unix() {
430        assert_eq!(abstime(0, 0), 2_208_988_800_000);
431        assert_eq!(abstime(1, 50), 2_208_988_801_500);
432        assert_eq!(unix_seconds(abstime(1, 50)), 1);
433        assert_eq!(unix_seconds(abstime(0, 0)), 0);
434    }
435
436    #[test]
437    fn eib_date_and_time() {
438        // 2026-09-27 13:05:09 UTC
439        let at = abstime(1_790_514_309, 0);
440        assert_eq!(eib_date(at), 126_270);
441        assert_eq!(eib_time(at), 130_509);
442
443        // 1999-12-31 00:00:00 UTC
444        let at = abstime(946_598_400, 0);
445        assert_eq!(eib_date(at), 99_365);
446        assert_eq!(eib_time(at), 0);
447    }
448
449    #[test]
450    fn format_time_options() {
451        let at = abstime(1_790_514_309, 25);
452
453        assert_eq!(format_time(at, "YYYYMMDD", Some('/'), None), Some(FormatValue::Text("2026/09/27".into())));
454        assert_eq!(format_time(at, "MMDDYY", None, None), Some(FormatValue::Text("092726".into())));
455        assert_eq!(format_time(at, "YYDDD", Some('-'), None), Some(FormatValue::Text("26-270".into())));
456        assert_eq!(format_time(at, "TIME", None, Some(':')), Some(FormatValue::Text("13:05:09".into())));
457        assert_eq!(format_time(at, "DAYOFWEEK", None, None), Some(FormatValue::Number(0)));
458        assert_eq!(format_time(at, "DAYOFMONTH", None, None), Some(FormatValue::Number(27)));
459        assert_eq!(format_time(at, "MONTHOFYEAR", None, None), Some(FormatValue::Number(9)));
460        assert_eq!(format_time(at, "YEAR", None, None), Some(FormatValue::Number(2026)));
461        assert_eq!(format_time(at, "MILLISECONDS", None, None), Some(FormatValue::Number(250)));
462        assert_eq!(format_time(at, "DAYCOUNT", None, None), Some(FormatValue::Number(46_290)));
463        assert_eq!(format_time(at, "STRINGFORMAT", None, None), None);
464    }
465}