1use crate::vocab::OpenMode;
9use std::cmp::Ordering;
10use std::collections::{BTreeMap, BTreeSet, HashMap};
11use std::fs::{File, OpenOptions};
12use std::io::{self, BufRead, BufReader, BufWriter, Read, Write};
13use std::ops::Bound;
14use std::path::PathBuf;
15use zarch::ebcdic::{self, CodePage};
16
17pub use crate::abend::FileStatus;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum Format {
21 Fixed,
22 Variable,
23 Text,
24}
25
26impl Format {
27 pub fn from_keyword(word: &str) -> Option<Format> {
28 match word.to_ascii_lowercase().as_str() {
29 "text" => Some(Format::Text),
30 "fixed" | "f" => Some(Format::Fixed),
31 "variable" | "v" => Some(Format::Variable),
32 _ => None,
33 }
34 }
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct Dd {
39 pub path: PathBuf,
40 pub format: Option<Format>,
41}
42
43#[derive(Clone, Debug, Default)]
45pub struct Dds {
46 given: HashMap<String, Dd>,
47 environment: bool,
48}
49
50fn dd_value(value: &str) -> Result<Dd, String> {
51 let (path, format) = value.rsplit_once(':').and_then(|(p, word)| Some((p, Some(Format::from_keyword(word)?)))).unwrap_or((value, None));
52 if path.is_empty() {
53 return Err(format!("DD {value}: no path"));
54 }
55 Ok(Dd { path: PathBuf::from(path), format })
56}
57
58impl Dds {
59 pub fn new(specs: &[String], environment: bool) -> Result<Self, String> {
61 let mut given = HashMap::new();
62 for spec in specs {
63 let (name, value) = spec.split_once('=').ok_or_else(|| format!("--dd {spec}: expected NAME=path"))?;
64 given.insert(name.to_ascii_uppercase(), dd_value(value)?);
65 }
66 Ok(Self { given, environment })
67 }
68
69 pub fn get(&self, name: &str) -> Option<Dd> {
70 if let Some(dd) = self.given.get(name) {
71 return Some(dd.clone());
72 }
73 if !self.environment {
74 return None;
75 }
76 std::env::var(format!("DD_{name}")).ok().and_then(|v| dd_value(&v).ok())
77 }
78}
79
80enum Handle {
81 Reader(BufReader<File>),
82 Writer(BufWriter<File>),
83 Keyed(Box<Keyed>),
84 Empty,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub struct KeySpan {
90 pub offset: usize,
91 pub len: usize,
92}
93
94impl KeySpan {
95 pub fn of(&self, record: &[u8]) -> Vec<u8> {
96 let mut key: Vec<u8> = record.iter().skip(self.offset).take(self.len).copied().collect();
97 key.resize(self.len, ebcdic::SPACE);
98 key
99 }
100}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
105pub enum Keying {
106 Position,
108 Relative,
111 Indexed { prime: KeySpan, alternates: Vec<(KeySpan, bool)> },
113}
114
115pub const MAX_RELATIVE: u64 = 16_777_215;
119
120pub fn record_number(n: u64) -> Vec<u8> {
121 n.to_be_bytes().to_vec()
122}
123
124pub fn number_of(key: &[u8]) -> u64 {
125 u64::from_be_bytes(key.try_into().unwrap_or([0; 8]))
126}
127
128type Entry = (Vec<u8>, u64, Vec<u8>);
132
133struct Alternate {
134 span: KeySpan,
135 duplicates: bool,
136 index: BTreeSet<Entry>,
137 arrival: HashMap<Vec<u8>, u64>,
138}
139
140impl Alternate {
141 fn holders<'a>(&'a self, value: &'a [u8]) -> impl Iterator<Item = &'a Entry> {
142 self.index.range((value.to_vec(), 0, Vec::new())..).take_while(move |(v, ..)| v == value)
143 }
144}
145
146#[derive(Clone, Debug)]
148enum Cursor {
149 First,
151 At { which: usize, at: Entry, inclusive: bool },
154 Undefined,
156}
157
158pub struct Found {
160 pub key: Vec<u8>,
161 pub record: Vec<u8>,
162 pub duplicate: bool,
164}
165
166pub struct Keyed {
170 pub keying: Keying,
171 records: BTreeMap<Vec<u8>, Vec<u8>>,
172 alternates: Vec<Alternate>,
173 arrivals: u64,
174 cursor: Cursor,
175 pub last_read: Option<Vec<u8>>,
178 dirty: bool,
179 path: Option<PathBuf>,
180 record_len: usize,
181 page: &'static CodePage,
182}
183
184impl Keyed {
185 fn new(keying: Keying, path: Option<PathBuf>, record_len: usize, page: &'static CodePage) -> Self {
186 let alternates = match &keying {
187 Keying::Indexed { alternates, .. } => {
188 alternates.iter().map(|&(span, duplicates)| Alternate { span, duplicates, index: BTreeSet::new(), arrival: HashMap::new() }).collect()
189 }
190 _ => Vec::new(),
191 };
192 Self { keying, records: BTreeMap::new(), alternates, arrivals: 0, cursor: Cursor::First, last_read: None, dirty: true, path, record_len, page }
193 }
194
195 pub fn prime_key(&self, record: &[u8]) -> Option<Vec<u8>> {
196 match &self.keying {
197 Keying::Indexed { prime, .. } => Some(prime.of(record)),
198 _ => None,
199 }
200 }
201
202 pub fn highest_key(&self) -> Option<&Vec<u8>> {
203 self.records.keys().next_back()
204 }
205
206 pub fn record(&self, key: &[u8]) -> Option<&Vec<u8>> {
207 self.records.get(key)
208 }
209
210 fn key_len(&self, which: usize) -> usize {
211 match (&self.keying, which) {
212 (Keying::Indexed { prime, .. }, 0) => prime.len,
213 (Keying::Indexed { .. }, n) => self.alternates[n - 1].span.len,
214 _ => 8,
215 }
216 }
217
218 pub fn get(&self, which: usize, value: &[u8]) -> Option<Found> {
220 if which == 0 {
221 return self.records.get(value).map(|r| Found { key: value.to_vec(), record: r.clone(), duplicate: false });
222 }
223 let mut hits = self.alternates[which - 1].holders(value);
224 let key = hits.next()?.2.clone();
225 Some(Found { record: self.records[&key].clone(), key, duplicate: hits.next().is_some() })
226 }
227
228 fn put(&mut self, key: Vec<u8>, record: Vec<u8>) -> Result<bool, FileStatus> {
232 let mut shared = false;
233 for alt in &self.alternates {
234 let value = alt.span.of(&record);
235 let others = alt.holders(&value).any(|(_, _, k)| *k != key);
236 if others && !alt.duplicates {
237 return Err(FileStatus::DuplicateKey);
238 }
239 shared |= others;
240 }
241 let old = self.records.get(&key).cloned();
242 let mut arrivals = self.arrivals;
243 for alt in &mut self.alternates {
244 let now = alt.span.of(&record);
245 if let Some(old) = &old {
246 let was = alt.span.of(old);
247 if was == now {
248 continue;
249 }
250 let seq = alt.arrival[&key];
251 alt.index.remove(&(was, seq, key.clone()));
252 }
253 alt.index.insert((now, arrivals, key.clone()));
254 alt.arrival.insert(key.clone(), arrivals);
255 arrivals += 1;
256 }
257 self.arrivals = arrivals;
258 self.records.insert(key, record);
259 self.dirty = true;
260 Ok(shared)
261 }
262
263 pub fn insert(&mut self, key: Vec<u8>, record: Vec<u8>) -> Result<bool, FileStatus> {
266 if self.records.contains_key(&key) {
267 return Err(FileStatus::DuplicateKey);
268 }
269 self.put(key, record)
270 }
271
272 pub fn replace(&mut self, key: Vec<u8>, record: Vec<u8>) -> Result<bool, FileStatus> {
274 if !self.records.contains_key(&key) {
275 return Err(FileStatus::NotFound);
276 }
277 self.put(key, record)
278 }
279
280 pub fn remove(&mut self, key: &[u8]) -> Option<Vec<u8>> {
281 let record = self.records.remove(key)?;
282 for alt in &mut self.alternates {
283 if let Some(seq) = alt.arrival.remove(key) {
284 alt.index.remove(&(alt.span.of(&record), seq, key.to_vec()));
285 }
286 }
287 self.dirty = true;
288 Some(record)
289 }
290
291 pub fn start(&mut self, which: usize, wanted: Ordering, or_equal: bool, value: &[u8]) -> bool {
295 let value = &value[..value.len().min(self.key_len(which))];
296 let fits = |k: &[u8]| {
297 let o = k[..value.len().min(k.len())].cmp(value);
298 o == wanted || (or_equal && o == Ordering::Equal)
299 };
300 let from = (value.to_vec(), 0, Vec::new());
301 let found = match which {
302 0 => self.records.range(from.0..).map(|(k, _)| k).find(|k| fits(k)).map(|k| (k.clone(), 0, Vec::new())),
303 n => self.alternates[n - 1].index.range(from..).find(|(k, ..)| fits(k)).cloned(),
304 };
305 self.last_read = None;
306 match found {
307 Some(at) => {
308 self.cursor = Cursor::At { which, at, inclusive: true };
309 true
310 }
311 None => {
312 self.lose_position();
313 false
314 }
315 }
316 }
317
318 pub fn lose_position(&mut self) {
320 self.cursor = Cursor::Undefined;
321 self.last_read = None;
322 }
323
324 pub fn read_at(&mut self, which: usize, key: &[u8]) {
326 let at = match which {
327 0 => (key.to_vec(), 0, Vec::new()),
328 n => {
329 let alt = &self.alternates[n - 1];
330 (alt.span.of(&self.records[key]), alt.arrival[key], key.to_vec())
331 }
332 };
333 self.cursor = Cursor::At { which, at, inclusive: false };
334 self.last_read = Some(key.to_vec());
335 }
336
337 pub fn seek(&self, from: &[u8], inclusive: bool, backward: bool) -> Option<(Vec<u8>, Vec<u8>)> {
340 let edge = if inclusive { Bound::Included(from.to_vec()) } else { Bound::Excluded(from.to_vec()) };
341 let mut hits = if backward { self.records.range((Bound::Unbounded, edge)) } else { self.records.range((edge, Bound::Unbounded)) };
342 let hit = if backward { hits.next_back() } else { hits.next() };
343 hit.map(|(k, r)| (k.clone(), r.clone()))
344 }
345
346 pub fn step(&mut self, backward: bool) -> Result<Option<Found>, &'static str> {
348 self.read_next(backward).map_err(FileStatus::as_str)
349 }
350
351 pub fn read_next(&mut self, backward: bool) -> Result<Option<Found>, FileStatus> {
353 let (which, bound) = match &self.cursor {
354 Cursor::Undefined => return Err(FileStatus::NoNextRecord),
355 Cursor::First => (0, Bound::Unbounded),
356 Cursor::At { which, at, inclusive: true } => (*which, Bound::Included(at.clone())),
357 Cursor::At { which, at, inclusive: false } => (*which, Bound::Excluded(at.clone())),
358 };
359 let range = if backward { (Bound::Unbounded, bound) } else { (bound, Bound::Unbounded) };
360 let hit = if which == 0 {
361 let mut keys = self.records.range((range.0.map(|(k, ..)| k), range.1.map(|(k, ..)| k))).map(|(k, _)| k);
362 let key = if backward { keys.next_back() } else { keys.next() };
363 key.map(|k| (k.clone(), false))
364 } else {
365 let mut entries = self.alternates[which - 1].index.range(range);
366 let entry = if backward { entries.next_back() } else { entries.next() };
367 entry.map(|(value, _, key)| {
368 let after = if backward { entries.next_back() } else { entries.next() };
369 (key.clone(), after.is_some_and(|(v, ..)| v == value))
370 })
371 };
372 let Some((key, duplicate)) = hit else {
373 self.lose_position();
374 return Ok(None);
375 };
376 self.read_at(which, &key);
377 Ok(Some(Found { record: self.records[&key].clone(), key, duplicate }))
378 }
379
380 fn inbound(&self, raw: Vec<u8>, format: Format) -> Vec<u8> {
381 let mut record = match format {
382 Format::Text => self.page.encode_lossy(&String::from_utf8_lossy(&raw)),
383 _ => raw,
384 };
385 if format != Format::Variable {
386 record.resize(self.record_len.max(record.len()), ebcdic::SPACE);
387 }
388 record
389 }
390
391 fn outbound(&self, record: &[u8], format: Format) -> Vec<u8> {
392 match format {
393 Format::Text => format!("{}\n", self.page.decode(record).trim_end()).into_bytes(),
394 Format::Fixed => {
395 let mut r = record.to_vec();
396 r.resize(self.record_len, ebcdic::SPACE);
397 r
398 }
399 Format::Variable => record.to_vec(),
400 }
401 }
402
403 fn save(&self, format: Format) -> io::Result<()> {
404 let Some(path) = self.path.as_ref().filter(|_| self.dirty) else { return Ok(()) };
405 let mut out = Open { mode: OpenMode::Output, format, handle: Handle::Writer(BufWriter::new(File::create(path)?)), head: Head::Start, page: None };
406 let empty = match format {
407 Format::Fixed => vec![0; self.record_len],
408 Format::Variable => Vec::new(),
409 Format::Text => b"\n".to_vec(),
410 };
411 let mut next = 1;
412 for (key, record) in &self.records {
413 if self.keying == Keying::Relative {
414 for _ in next..number_of(key) {
415 out.write(&empty)?;
416 }
417 next = number_of(key) + 1;
418 }
419 out.write(&self.outbound(record, format))?;
420 }
421 out.close()
422 }
423}
424
425pub struct Open {
426 pub mode: OpenMode,
427 pub format: Format,
428 handle: Handle,
429 head: Head,
430 pub page: Option<crate::linage::Page>,
432}
433
434#[derive(Clone, Copy, Debug, PartialEq, Eq)]
436pub enum Move {
437 Lines(u64),
438 Page,
439}
440
441#[derive(Clone, Copy, Debug, PartialEq, Eq)]
444enum Head {
445 Start,
446 OnLine(bool),
447 Fresh,
448}
449
450pub enum Record {
452 Data(Vec<u8>),
453 WrongLength(Vec<u8>),
455 End,
456}
457
458pub fn open_keyed(dd: Option<&Dd>, mode: OpenMode, format: Format, keying: Keying, record_len: usize, page: &'static CodePage) -> io::Result<Open> {
461 let path = dd.filter(|_| mode != OpenMode::Input).map(|d| d.path.clone());
462 let mut keyed = Keyed::new(keying, path, record_len, page);
463 if let Some(dd) = dd.filter(|d| mode != OpenMode::Output && d.path.exists()) {
464 let mut reader = open(dd, OpenMode::Input, format)?;
465 let mut number = 0u64;
466 loop {
467 let raw = match reader.read(record_len)? {
468 Record::End => break,
469 Record::Data(r) | Record::WrongLength(r) => r,
470 };
471 number += 1;
472 if keyed.keying == Keying::Relative && raw.iter().all(|&b| b == 0) {
473 continue;
474 }
475 let record = keyed.inbound(raw, format);
476 let key = keyed.prime_key(&record).unwrap_or_else(|| record_number(number));
477 keyed.insert(key, record).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, format!("{}: two records with one key", dd.path.display())))?;
478 }
479 keyed.dirty = false;
480 }
481 Ok(Open { mode, format, handle: Handle::Keyed(Box::new(keyed)), head: Head::Start, page: None })
482}
483
484pub fn open(dd: &Dd, mode: OpenMode, format: Format) -> io::Result<Open> {
485 let handle = match mode {
486 OpenMode::Input => Handle::Reader(BufReader::new(File::open(&dd.path)?)),
487 OpenMode::Output => Handle::Writer(BufWriter::new(File::create(&dd.path)?)),
488 OpenMode::Extend => Handle::Writer(BufWriter::new(OpenOptions::new().append(true).create(true).open(&dd.path)?)),
489 OpenMode::InputOutput => return Err(io::Error::new(io::ErrorKind::Unsupported, "OPEN I-O of a line-sequential file")),
490 };
491 Ok(Open { mode, format, handle, head: Head::Start, page: None })
492}
493
494pub fn absent() -> Open {
496 Open { mode: OpenMode::Input, format: Format::Fixed, handle: Handle::Empty, head: Head::Start, page: None }
497}
498
499impl Open {
500 pub fn keyed(&mut self) -> Option<&mut Keyed> {
501 match &mut self.handle {
502 Handle::Keyed(k) => Some(k),
503 _ => None,
504 }
505 }
506
507 pub fn is_keyed(&self) -> bool {
508 matches!(self.handle, Handle::Keyed(_))
509 }
510
511 pub fn read(&mut self, fixed_len: usize) -> io::Result<Record> {
512 let reader = match &mut self.handle {
513 Handle::Reader(r) => r,
514 Handle::Empty => return Ok(Record::End),
515 Handle::Writer(_) | Handle::Keyed(_) => return Err(io::Error::other("a sequential READ of a file not open for it")),
516 };
517 match self.format {
518 Format::Fixed => {
519 let mut buf = Vec::with_capacity(fixed_len);
520 reader.by_ref().take(fixed_len as u64).read_to_end(&mut buf)?;
521 Ok(match buf.len() {
522 0 => Record::End,
523 n if n == fixed_len => Record::Data(buf),
524 _ => Record::WrongLength(buf),
525 })
526 }
527 Format::Variable => {
528 let mut rdw = [0u8; 4];
529 match reader.read_exact(&mut rdw) {
530 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(Record::End),
531 other => other?,
532 }
533 let len = u16::from_be_bytes([rdw[0], rdw[1]]) as usize;
534 if len < 4 {
535 return Err(io::Error::new(io::ErrorKind::InvalidData, format!("an RDW of length {len}")));
536 }
537 let mut buf = vec![0u8; len - 4];
538 reader.read_exact(&mut buf)?;
539 Ok(Record::Data(buf))
540 }
541 Format::Text => {
542 let mut line = Vec::new();
543 if reader.read_until(b'\n', &mut line)? == 0 {
544 return Ok(Record::End);
545 }
546 while matches!(line.last(), Some(b'\n' | b'\r')) {
547 line.pop();
548 }
549 Ok(Record::Data(line))
550 }
551 }
552 }
553
554 pub fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
555 let Handle::Writer(w) = &mut self.handle else {
556 return Err(io::Error::other("WRITE to a file not opened for output"));
557 };
558 match self.format {
559 Format::Fixed | Format::Text => w.write_all(bytes),
560 Format::Variable => {
561 let len = u16::try_from(bytes.len() + 4).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "a record over 32,760 bytes"))?;
562 w.write_all(&len.to_be_bytes())?;
563 w.write_all(&[0, 0])?;
564 w.write_all(bytes)
565 }
566 }
567 }
568
569 pub fn print(&mut self, before: Option<Move>, line: &str, after: Option<Move>) -> io::Result<()> {
574 if let Some(m) = before {
575 self.feed(m)?;
576 }
577 let Handle::Writer(w) = &mut self.handle else {
578 return Err(io::Error::other("WRITE to a file not opened for output"));
579 };
580 let shown = matches!(self.head, Head::OnLine(true));
581 if shown && !line.is_empty() {
582 w.write_all(b"\r")?;
583 }
584 w.write_all(line.as_bytes())?;
585 self.head = Head::OnLine(shown || !line.is_empty());
586 match after {
587 Some(m) => self.feed(m),
588 None => Ok(()),
589 }
590 }
591
592 fn feed(&mut self, m: Move) -> io::Result<()> {
593 let Handle::Writer(w) = &mut self.handle else {
594 return Err(io::Error::other("WRITE to a file not opened for output"));
595 };
596 match (m, self.head) {
597 (Move::Lines(0), _) => return Ok(()),
598 (Move::Lines(n), head) => {
599 for _ in 0..n - u64::from(head == Head::Start) {
600 w.write_all(b"\n")?;
601 }
602 }
603 (Move::Page, Head::OnLine(_)) => w.write_all(b"\n\x0c")?,
604 (Move::Page, _) => w.write_all(b"\x0c")?,
605 }
606 self.head = Head::Fresh;
607 Ok(())
608 }
609
610 pub fn close(self) -> io::Result<()> {
611 match self.handle {
612 Handle::Writer(mut w) => {
613 if matches!(self.head, Head::OnLine(_)) {
614 w.write_all(b"\n")?;
615 }
616 w.flush()
617 }
618 Handle::Keyed(k) => k.save(self.format),
619 _ => Ok(()),
620 }
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn dd_specs() {
630 let dds = Dds::new(&["IN=/tmp/a.txt:text".into(), "out=/tmp/b.dat".into()], false).unwrap();
631 assert_eq!(dds.get("IN"), Some(Dd { path: "/tmp/a.txt".into(), format: Some(Format::Text) }));
632 assert_eq!(dds.get("OUT").unwrap().format, None);
633 assert!(dds.get("NONE").is_none());
634 assert!(Dds::new(&["NOEQUALS".into()], false).is_err());
635 }
636
637 #[test]
638 fn variable_records_round_trip_through_rdws() {
639 let path = std::env::temp_dir().join(format!("ironwork-vb-{}", std::process::id()));
640 let dd = Dd { path: path.clone(), format: None };
641 let mut out = open(&dd, OpenMode::Output, Format::Variable).unwrap();
642 out.write(b"AB").unwrap();
643 out.write(b"CDEF").unwrap();
644 out.close().unwrap();
645 assert_eq!(std::fs::read(&path).unwrap(), [0, 6, 0, 0, b'A', b'B', 0, 8, 0, 0, b'C', b'D', b'E', b'F']);
646 let mut input = open(&dd, OpenMode::Input, Format::Variable).unwrap();
647 assert!(matches!(input.read(0).unwrap(), Record::Data(d) if d == b"AB"));
648 assert!(matches!(input.read(0).unwrap(), Record::Data(d) if d == b"CDEF"));
649 assert!(matches!(input.read(0).unwrap(), Record::End));
650 }
651}