1use 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
12pub const EIB_LEN: usize = 85;
14
15#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum DataSet {
18 Ksds { key: KeySpan },
19 Rrds,
20}
21
22#[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 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
44pub 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
101pub struct TsQueue {
102 pub items: Vec<Vec<u8>>,
103 pub next: usize,
104}
105
106pub trait Terminal: std::fmt::Debug {
109 fn size(&self) -> (usize, usize);
110 fn send(&mut self, stream: &[u8]) -> Result<(), String>;
111 fn receive(&mut self) -> Result<Option<Vec<u8>>, String>;
113}
114
115#[derive(Clone, Debug, Default, PartialEq, Eq)]
118pub struct Browse {
119 pub at: Vec<u8>,
120 pub inclusive: bool,
121}
122
123#[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 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 pub td_files: HashMap<String, PathBuf>,
139 pub held: HashMap<String, Vec<u8>>,
141 pub browses: HashMap<(String, i64), Browse>,
143 pub terminal: Option<Box<dyn Terminal>>,
144 pub initial_aid: Option<u8>,
146 pub mapsets: HashMap<String, syntax::bms::Mapset>,
148 pub next_transid: Option<String>,
150 pub returned_commarea: Option<Vec<u8>>,
151}
152
153impl Task {
154 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 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 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 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 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 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 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
269pub fn abstime(seconds: i64, hundredths: u32) -> i64 {
271 2_208_988_800_000 + seconds * 1000 + hundredths as i64 * 10
272}
273
274pub fn unix_seconds(abstime: i64) -> i64 {
276 (abstime - 2_208_988_800_000).div_euclid(1000)
277}
278
279pub 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
288pub 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#[derive(Clone, Debug, PartialEq, Eq)]
297pub enum FormatValue {
298 Text(String),
299 Number(i64),
300}
301
302pub 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 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 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}