1use crate::files::{Dd, Format, KeySpan, Keying};
6use crate::calendar::{civil, EPOCH_1900_TO_1970_MILLIS, EPOCH_1900_TO_1970_SECONDS, SECONDS_PER_DAY};
7pub use crate::terminal::Terminal;
8use std::collections::{BTreeMap, HashMap};
9use std::fs::OpenOptions;
10use std::io::Write;
11use std::path::PathBuf;
12
13pub const EIB_LEN: usize = 85;
15
16macro_rules! conditions {
17 ($($name:ident)*) => {
18 #[allow(clippy::upper_case_acronyms)]
20 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21 pub enum Condition {
22 $($name,)*
23 }
24
25 impl Condition {
26 pub const ALL: &[Self] = &[$(Self::$name,)*];
27
28 pub fn name(self) -> &'static str {
29 match self {
30 $(Self::$name => stringify!($name),)*
31 }
32 }
33 }
34 };
35}
36
37conditions! {
38 NORMAL ERROR RDATT WRBRK EOF EODS EOC INBFMH ENDINPT NONVAL NOSTART TERMIDERR FILENOTFOUND
39 NOTFND DUPREC DUPKEY INVREQ IOERR NOSPACE NOTOPEN ENDFILE ILLOGIC LENGERR QZERO SIGNAL QBUSY
40 ITEMERR PGMIDERR TRANSIDERR ENDDATA INVTSREQ EXPIRED RETPAGE RTEFAIL RTESOME TSIOERR MAPFAIL
41 INVERRTERM INVMPSZ IGREQID OVERFLOW INVLDC NOSTG JIDERR QIDERR NOJBUFSP DSSTAT SELNERR FUNCERR
42 UNEXPIN NOPASSBKRD NOPASSBKWR SEGIDERR SYSIDERR ISCINVREQ ENQBUSY ENVDEFERR IGREQCD SESSIONERR
43 SYSBUSY SESSBUSY NOTALLOC CBIDERR INVEXITREQ INVPARTNSET INVPARTN PARTNFAIL USERIDERR NOTAUTH
44 VOLIDERR SUPPRESSED RESIDERR NOSPOOL TERMERR ROLLEDBACK END DISABLED ALLOCERR STRELERR OPENERR
45 SPOLBUSY SPOLERR NODEIDERR TASKIDERR TCIDERR DSNNOTFOUND LOADING MODELIDERR OUTDESCRERR
46 PARTNERIDERR PROFILEIDERR NETNAMEIDERR LOCKED RECORDBUSY UOWNOTFOUND UOWLNOTFOUND LINKABEND
47 CHANGED PROCESSBUSY ACTIVITYBUSY PROCESSERR ACTIVITYERR CONTAINERERR EVENTERR TOKENERR
48 NOTFINISHED POOLERR TIMERERR SYMBOLERR TEMPLATERR NOTSUPERUSER CSDERR DUPRES RESUNAVAIL
49 CHANNELERR CCSIDERR TIMEDOUT CODEPAGEERR INCOMPLETE APPNOTFOUND BUSY
50}
51
52const ALIASES: &[(&str, Condition)] = &[("DSIDERR", Condition::FILENOTFOUND)];
54
55const DEFAULT_ABENDS: &[(Condition, &str)] = &[
58 (Condition::NOTFND, "AEIM"),
59 (Condition::DUPREC, "AEIN"),
60 (Condition::DUPKEY, "AEIO"),
61 (Condition::IOERR, "AEIQ"),
62 (Condition::NOSPACE, "AEIR"),
63 (Condition::NOTOPEN, "AEIS"),
64 (Condition::ENDFILE, "AEIT"),
65 (Condition::ILLOGIC, "AEIU"),
66 (Condition::LENGERR, "AEIV"),
67 (Condition::QZERO, "AEIW"),
68 (Condition::ITEMERR, "AEIZ"),
69 (Condition::PGMIDERR, "AEI0"),
70 (Condition::TRANSIDERR, "AEI1"),
71 (Condition::ENDDATA, "AEI2"),
72 (Condition::INVTSREQ, "AEI3"),
73 (Condition::EXPIRED, "AEI4"),
74 (Condition::TSIOERR, "AEI8"),
75 (Condition::MAPFAIL, "AEI9"),
76 (Condition::ERROR, "AEIA"),
77 (Condition::EOF, "AEID"),
78 (Condition::EODS, "AEIE"),
79 (Condition::INBFMH, "AEIG"),
80 (Condition::ENDINPT, "AEIH"),
81 (Condition::NONVAL, "AEII"),
82 (Condition::NOSTART, "AEIJ"),
83 (Condition::TERMIDERR, "AEIK"),
84 (Condition::FILENOTFOUND, "AEIL"),
85 (Condition::DISABLED, "AEXL"),
86 (Condition::ROLLEDBACK, "AEXJ"),
87 (Condition::LOCKED, "AEX8"),
88 (Condition::RECORDBUSY, "AEX9"),
89 (Condition::QIDERR, "AEYH"),
90 (Condition::SYSIDERR, "AEYQ"),
91 (Condition::NOTAUTH, "AEY7"),
92 (Condition::USERIDERR, "AEYX"),
93 (Condition::CONTAINERERR, "AEZJ"),
94 (Condition::CHANNELERR, "AEZV"),
95];
96
97impl Condition {
98 pub fn from_name(name: &str) -> Option<Self> {
100 let alias = ALIASES.iter().find(|(a, _)| a.eq_ignore_ascii_case(name)).map(|&(_, c)| c);
101 alias.or_else(|| Self::ALL.iter().copied().find(|c| c.name().eq_ignore_ascii_case(name)))
102 }
103
104 pub fn resp(self) -> i32 {
106 crate::cics_tables::resp(self.name()).unwrap_or_else(|| panic!("DFHRESP has no {}", self.name()))
107 }
108
109 pub fn default_abend(self) -> &'static str {
110 DEFAULT_ABENDS.iter().find(|&&(c, _)| c == self).map_or("AEIP", |&(_, abend)| abend)
111 }
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
116pub enum DataSet {
117 Ksds { key: KeySpan },
118 Rrds,
119}
120
121#[derive(Clone, Debug, PartialEq, Eq)]
123pub struct FileDef {
124 pub dd: Dd,
125 pub data_set: DataSet,
126 pub record_len: usize,
127}
128
129impl FileDef {
130 pub fn keying(&self) -> Keying {
132 match &self.data_set {
133 DataSet::Ksds { key } => Keying::Indexed { prime: *key, alternates: vec![] },
134 DataSet::Rrds => Keying::Relative,
135 }
136 }
137
138 pub fn format(&self) -> Format {
139 self.dd.format.unwrap_or(Format::Fixed)
140 }
141}
142
143pub fn parse_file(spec: &str) -> Result<(String, FileDef), String> {
146 let parts: Vec<&str> = spec.split(',').collect();
147 if parts.len() < 3 {
148 return Err(format!("{spec}: expected NAME=path,KIND,…"));
149 }
150
151 let (name, path) = parts[0]
152 .split_once('=')
153 .ok_or_else(|| format!("{spec}: expected NAME=path"))?;
154 let name = name.to_ascii_uppercase();
155 let kind = parts[1].to_ascii_uppercase();
156
157 let mut key: Option<KeySpan> = None;
158 let mut record_len: Option<usize> = None;
159 let mut format: Option<Format> = None;
160
161 for part in &parts[2..] {
162 let part = part.trim();
163 if let Some(val) = part.strip_prefix("key=") {
164 let (off, len) = val
165 .split_once(':')
166 .ok_or_else(|| format!("{spec}: bad key spec {val}"))?;
167 let off: usize = off.parse().map_err(|_| format!("{spec}: bad key offset {off}"))?;
168 let len: usize = len.parse().map_err(|_| format!("{spec}: bad key length {len}"))?;
169 key = Some(KeySpan { offset: off, len });
170 } else if let Some(val) = part.strip_prefix("len=") {
171 record_len = Some(
172 val.parse()
173 .map_err(|_| format!("{spec}: bad record length {val}"))?,
174 );
175 } else if let Some(f) = Format::from_keyword(part) {
176 format = Some(f);
177 } else {
178 return Err(format!("{spec}: unknown field {part}"));
179 }
180 }
181
182 let data_set = match kind.as_str() {
183 "KSDS" => {
184 let key = key.ok_or_else(|| format!("{spec}: KSDS requires key=OFFSET:LENGTH"))?;
185 DataSet::Ksds { key }
186 }
187 "RRDS" => DataSet::Rrds,
188 _ => return Err(format!("{spec}: unknown data set kind {kind}")),
189 };
190
191 let record_len = record_len.ok_or_else(|| format!("{spec}: missing len=RECLEN"))?;
192 let dd = Dd { path: PathBuf::from(path), format };
193 Ok((name, FileDef { dd, data_set, record_len }))
194}
195
196#[derive(Clone, Debug, Default, PartialEq, Eq)]
198pub struct TsQueue {
199 pub items: Vec<Vec<u8>>,
200 pub next: usize,
201}
202
203#[derive(Clone, Debug, Default, PartialEq, Eq)]
206pub struct Browse {
207 pub at: Vec<u8>,
208 pub inclusive: bool,
209}
210
211#[derive(Debug, Default)]
213pub struct Task {
214 pub transid: String,
215 pub termid: String,
216 pub userid: String,
217 pub applid: String,
218 pub sysid: String,
219 pub number: u32,
220 pub commarea: Option<Vec<u8>>,
222 pub files: HashMap<String, FileDef>,
223 pub ts: BTreeMap<String, TsQueue>,
224 pub td: BTreeMap<String, Vec<Vec<u8>>>,
225 pub td_files: HashMap<String, PathBuf>,
227 pub held: HashMap<String, Vec<u8>>,
229 pub browses: HashMap<(String, i64), Browse>,
231 pub terminal: Option<Box<dyn Terminal>>,
232 pub initial_aid: Option<u8>,
234 pub mapsets: HashMap<String, crate::bms::Mapset>,
236 pub next_transid: Option<String>,
238 pub returned_commarea: Option<Vec<u8>>,
239}
240
241impl Task {
242 pub fn writeq_ts(
245 &mut self,
246 queue: &str,
247 rewrite: Option<usize>,
248 data: &[u8],
249 ) -> Result<usize, Condition> {
250 let queue = queue.trim_end();
251 match rewrite {
252 None => {
253 let q = self
254 .ts
255 .entry(queue.to_string())
256 .or_default();
257 q.items.push(data.to_vec());
258 Ok(q.items.len())
259 }
260 Some(item) => {
261 let q = self.ts.get_mut(queue).ok_or(Condition::QIDERR)?;
262 if item == 0 || item > q.items.len() {
263 return Err(Condition::ITEMERR);
264 }
265 q.items[item - 1] = data.to_vec();
266 Ok(item)
267 }
268 }
269 }
270
271 pub fn readq_ts(
273 &mut self,
274 queue: &str,
275 item: Option<usize>,
276 ) -> Result<(Vec<u8>, usize), Condition> {
277 let queue = queue.trim_end();
278 let q = self.ts.get_mut(queue).ok_or(Condition::QIDERR)?;
279 let item_num = match item {
280 Some(n) => {
281 if n == 0 || n > q.items.len() {
282 return Err(Condition::ITEMERR);
283 }
284 n
285 }
286 None => {
287 let n = if q.next == 0 { 1 } else { q.next + 1 };
288 if n > q.items.len() {
289 return Err(Condition::ITEMERR);
290 }
291 n
292 }
293 };
294 let data = q.items[item_num - 1].clone();
295 q.next = item_num;
296 Ok((data, q.items.len()))
297 }
298
299 pub fn deleteq_ts(&mut self, queue: &str) -> Result<(), Condition> {
301 let queue = queue.trim_end();
302 self.ts.remove(queue).map(|_| ()).ok_or(Condition::QIDERR)
303 }
304
305 pub fn writeq_td(&mut self, queue: &str, data: &[u8]) {
307 let queue = queue.trim_end();
308 self.td.entry(queue.to_string()).or_default().push(data.to_vec());
309 }
310
311 pub fn readq_td(&mut self, queue: &str) -> Result<Vec<u8>, Condition> {
313 let queue = queue.trim_end();
314 let items = self.td.get_mut(queue).ok_or(Condition::QZERO)?;
315 if items.is_empty() {
316 return Err(Condition::QZERO);
317 }
318 Ok(items.remove(0))
319 }
320
321 pub fn deleteq_td(&mut self, queue: &str) {
323 let queue = queue.trim_end();
324 if let Some(items) = self.td.get_mut(queue) {
325 items.clear();
326 }
327 }
328
329 pub fn flush_td(&mut self, page: &zarch::ebcdic::CodePage) -> std::io::Result<()> {
332 let queues: Vec<String> = self.td_files.keys().cloned().collect();
333 for queue in &queues {
334 let Some(path) = self.td_files.get(queue) else { continue };
335 let items: Vec<Vec<u8>> = self
336 .td
337 .get(queue)
338 .cloned()
339 .unwrap_or_default();
340 if items.is_empty() {
341 continue;
342 }
343 let mut file = OpenOptions::new().create(true).append(true).open(path)?;
344 for item in &items {
345 let text = page.decode(item);
346 let line = format!("{}\n", text.trim_end());
347 file.write_all(line.as_bytes())?;
348 }
349 if let Some(q) = self.td.get_mut(queue) {
350 q.clear();
351 }
352 }
353 Ok(())
354 }
355}
356
357pub fn abstime(seconds: i64, hundredths: u32) -> i64 {
359 EPOCH_1900_TO_1970_MILLIS + seconds * 1000 + hundredths as i64 * 10
360}
361
362pub fn unix_seconds(abstime: i64) -> i64 {
364 (abstime - EPOCH_1900_TO_1970_MILLIS).div_euclid(1000)
365}
366
367pub fn eib_date(abstime: i64) -> i64 {
369 let secs = unix_seconds(abstime);
370 let date = civil(secs);
371 let c = if date.year >= 2000 { 1 } else { 0 };
372 let yy = date.year % 100;
373 c * 100_000 + yy * 1_000 + i64::from(date.day_of_year)
374}
375
376pub fn eib_time(abstime: i64) -> i64 {
378 let secs = unix_seconds(abstime);
379 let time = civil(secs);
380 i64::from(time.hour) * 10_000 + i64::from(time.minute) * 100 + i64::from(time.second)
381}
382
383#[derive(Clone, Debug, PartialEq, Eq)]
385pub enum FormatValue {
386 Text(String),
387 Number(i64),
388}
389
390pub fn format_time(abstime: i64, option: &str, datesep: Option<char>, timesep: Option<char>) -> Option<FormatValue> {
393 let secs = unix_seconds(abstime);
394 let c = civil(secs);
395 let (year, month, day, hour, minute, second, doy) = (c.year, c.month, c.day, c.hour, c.minute, c.second, c.day_of_year);
396
397 let yy = format!("{:02}", year % 100);
398 let yyyy = format!("{:04}", year);
399 let mm = format!("{:02}", month);
400 let dd = format!("{:02}", day);
401 let hh = format!("{:02}", hour);
402 let mi = format!("{:02}", minute);
403 let ss = format!("{:02}", second);
404 let ddd = format!("{:03}", doy);
405
406 let join = |parts: &[&str], sep: Option<char>| match sep {
407 Some(c) => {
408 let mut s = String::new();
409 for (i, p) in parts.iter().enumerate() {
410 if i > 0 {
411 s.push(c);
412 }
413 s.push_str(p);
414 }
415 s
416 }
417 None => parts.concat(),
418 };
419
420 let option = option.to_ascii_uppercase();
421
422 match option.as_str() {
423 "YYYYMMDD" => Some(FormatValue::Text(join(&[&yyyy, &mm, &dd], datesep))),
424 "YYMMDD" => Some(FormatValue::Text(join(&[&yy, &mm, &dd], datesep))),
425 "YYDDMM" => Some(FormatValue::Text(join(&[&yy, &dd, &mm], datesep))),
426 "YYYYDDMM" => Some(FormatValue::Text(join(&[&yyyy, &dd, &mm], datesep))),
427 "DDMMYY" => Some(FormatValue::Text(join(&[&dd, &mm, &yy], datesep))),
428 "DDMMYYYY" => Some(FormatValue::Text(join(&[&dd, &mm, &yyyy], datesep))),
429 "MMDDYY" | "DATE" => Some(FormatValue::Text(join(&[&mm, &dd, &yy], datesep))),
430 "MMDDYYYY" | "FULLDATE" => Some(FormatValue::Text(join(&[&mm, &dd, &yyyy], datesep))),
431 "YYDDD" => Some(FormatValue::Text(join(&[&yy, &ddd], datesep))),
432 "YYYYDDD" => Some(FormatValue::Text(join(&[&yyyy, &ddd], datesep))),
433 "DATEFORM" => Some(FormatValue::Text("MMDDYY".to_string())),
434 "TIME" => Some(FormatValue::Text(join(&[&hh, &mi, &ss], timesep))),
435 "DAYCOUNT" => Some(FormatValue::Number((secs + EPOCH_1900_TO_1970_SECONDS) / SECONDS_PER_DAY)),
436 "DAYOFWEEK" => Some(FormatValue::Number(c.cics_weekday())),
437 "DAYOFMONTH" => Some(FormatValue::Number(day as i64)),
438 "MONTHOFYEAR" => Some(FormatValue::Number(month as i64)),
439 "YEAR" => Some(FormatValue::Number(year)),
440 "MILLISECONDS" => Some(FormatValue::Number(abstime.rem_euclid(1000))),
441 _ => None,
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
450 fn every_condition_is_a_dfhresp_row_with_its_number() {
451 for &c in Condition::ALL {
452 assert_eq!(Some(c.resp()), crate::cics_tables::resp(c.name()), "{c:?}");
453 assert_eq!(Condition::from_name(c.name()), Some(c));
454 }
455 let rows: Vec<&str> = crate::cics_tables::resp_table().iter().map(|r| r.condition).collect();
456 let names: Vec<&str> = Condition::ALL.iter().map(|c| c.name()).collect();
457 assert_eq!(names, rows, "one variant per DFHRESP row, in its order");
458 }
459
460 #[test]
461 fn every_default_abend_is_for_a_dfhresp_condition_and_listed_once() {
462 for (i, &(c, abend)) in DEFAULT_ABENDS.iter().enumerate() {
463 assert!(crate::cics_tables::resp(c.name()).is_some(), "{c:?}");
464 assert!(DEFAULT_ABENDS[..i].iter().all(|&(d, _)| d != c), "{c:?} is listed twice");
465 assert_eq!(c.default_abend(), abend);
466 }
467 assert_eq!(DEFAULT_ABENDS.len(), 37);
468 let codes = [Condition::QIDERR, Condition::ROLLEDBACK, Condition::FILENOTFOUND, Condition::INVREQ, Condition::INVMPSZ].map(Condition::default_abend);
469 assert_eq!(codes, ["AEYH", "AEXJ", "AEIL", "AEIP", "AEIP"]);
470 }
471
472 #[test]
473 fn dsiderr_is_filenotfound_by_its_older_name() {
474 let c = Condition::from_name("dsiderr").unwrap();
475 assert_eq!((c, c.name(), c.resp()), (Condition::FILENOTFOUND, "FILENOTFOUND", 12));
476 assert_eq!(Condition::from_name("NOSUCH"), None);
477 }
478
479 #[test]
480 fn parse_file_ksds() {
481 let (name, def) = parse_file("MYFILE=/tmp/data,KSDS,key=0:8,len=80").unwrap();
482 assert_eq!(name, "MYFILE");
483 assert_eq!(def.record_len, 80);
484 assert!(matches!(&def.data_set, DataSet::Ksds { key } if key.offset == 0 && key.len == 8));
485 }
486
487 #[test]
488 fn parse_file_rrds_text() {
489 let (name, def) = parse_file("myfile=/tmp/data,rrds,len=120,text").unwrap();
490 assert_eq!(name, "MYFILE");
491 assert_eq!(def.record_len, 120);
492 assert!(matches!(def.data_set, DataSet::Rrds));
493 assert_eq!(def.dd.format, Some(Format::Text));
494 }
495
496 #[test]
497 fn parse_file_errors() {
498 assert!(parse_file("F=/tmp/d,KSDS,len=80").is_err());
499 assert!(parse_file("F=/tmp/d,RRDS,len=abc").is_err());
500 assert!(parse_file("F=/tmp/d,FOO,len=80").is_err());
501 }
502
503 #[test]
504 fn ts_queue() {
505 let mut task = Task::default();
506 task.writeq_ts("Q1", None, b"one").unwrap();
507 task.writeq_ts("Q1", None, b"two").unwrap();
508 task.writeq_ts("Q1", None, b"three").unwrap();
509
510 let (data, count) = task.readq_ts("Q1", Some(2)).unwrap();
511 assert_eq!(data, b"two");
512 assert_eq!(count, 3);
513
514 let (data, count) = task.readq_ts("Q1", None).unwrap();
515 assert_eq!(data, b"three");
516 assert_eq!(count, 3);
517
518 assert_eq!(task.readq_ts("Q1", None), Err(Condition::ITEMERR));
519
520 task.writeq_ts("Q1", Some(1), b"ONE").unwrap();
521 let (data, _) = task.readq_ts("Q1", Some(1)).unwrap();
522 assert_eq!(data, b"ONE");
523
524 assert_eq!(task.writeq_ts("Q1", Some(9), b"x"), Err(Condition::ITEMERR));
525 assert_eq!(task.readq_ts("NOPE", None), Err(Condition::QIDERR));
526
527 task.deleteq_ts("Q1").unwrap();
528 assert_eq!(task.readq_ts("Q1", None), Err(Condition::QIDERR));
529 }
530
531 #[test]
532 fn td_queue() {
533 let mut task = Task::default();
534 task.writeq_td("Q1", b"first");
535 task.writeq_td("Q1", b"second");
536 task.writeq_td("Q1", b"third");
537
538 assert_eq!(task.readq_td("Q1").unwrap(), b"first");
539 assert_eq!(task.readq_td("Q1").unwrap(), b"second");
540 assert_eq!(task.readq_td("Q1").unwrap(), b"third");
541 assert_eq!(task.readq_td("Q1"), Err(Condition::QZERO));
542 }
543
544 #[test]
545 fn abstime_and_unix() {
546 assert_eq!(abstime(0, 0), 2_208_988_800_000);
547 assert_eq!(abstime(1, 50), 2_208_988_801_500);
548 assert_eq!(unix_seconds(abstime(1, 50)), 1);
549 assert_eq!(unix_seconds(abstime(0, 0)), 0);
550 }
551
552 #[test]
553 fn eib_date_and_time() {
554 let at = abstime(1_790_514_309, 0);
556 assert_eq!(eib_date(at), 126_270);
557 assert_eq!(eib_time(at), 130_509);
558
559 let at = abstime(946_598_400, 0);
561 assert_eq!(eib_date(at), 99_365);
562 assert_eq!(eib_time(at), 0);
563 }
564
565 #[test]
566 fn format_time_options() {
567 let at = abstime(1_790_514_309, 25);
568
569 assert_eq!(format_time(at, "YYYYMMDD", Some('/'), None), Some(FormatValue::Text("2026/09/27".into())));
570 assert_eq!(format_time(at, "MMDDYY", None, None), Some(FormatValue::Text("092726".into())));
571 assert_eq!(format_time(at, "YYDDD", Some('-'), None), Some(FormatValue::Text("26-270".into())));
572 assert_eq!(format_time(at, "TIME", None, Some(':')), Some(FormatValue::Text("13:05:09".into())));
573 assert_eq!(format_time(at, "DAYOFWEEK", None, None), Some(FormatValue::Number(0)));
574 assert_eq!(format_time(at, "DAYOFMONTH", None, None), Some(FormatValue::Number(27)));
575 assert_eq!(format_time(at, "MONTHOFYEAR", None, None), Some(FormatValue::Number(9)));
576 assert_eq!(format_time(at, "YEAR", None, None), Some(FormatValue::Number(2026)));
577 assert_eq!(format_time(at, "MILLISECONDS", None, None), Some(FormatValue::Number(250)));
578 assert_eq!(format_time(at, "DAYCOUNT", None, None), Some(FormatValue::Number(46_290)));
579 assert_eq!(format_time(at, "STRINGFORMAT", None, None), None);
580 }
581}