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::{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 pub append: bool,
43}
44
45#[derive(Clone, Debug, Default)]
47pub struct Dds {
48 given: HashMap<String, Dd>,
49 environment: bool,
50}
51
52fn dd_value(value: &str) -> Result<Dd, String> {
53 let (rest, append) = match value.rsplit_once(':') {
54 Some((rest, word)) if word.eq_ignore_ascii_case("mod") => (rest, true),
55 _ => (value, false),
56 };
57 let (path, format) = rest.rsplit_once(':').and_then(|(p, word)| Some((p, Some(Format::from_keyword(word)?)))).unwrap_or((rest, None));
58 if path.is_empty() {
59 return Err(format!("DD {value}: no path"));
60 }
61 Ok(Dd { path: PathBuf::from(path), format, append })
62}
63
64impl Dds {
65 pub fn new(specs: &[String], environment: bool) -> Result<Self, String> {
67 let mut given = HashMap::new();
68 for spec in specs {
69 let (name, value) = spec.split_once('=').ok_or_else(|| format!("--dd {spec}: expected NAME=path"))?;
70 given.insert(name.to_ascii_uppercase(), dd_value(value)?);
71 }
72 Ok(Self { given, environment })
73 }
74
75 pub fn get(&self, name: &str) -> Option<Dd> {
76 if let Some(dd) = self.given.get(name) {
77 return Some(dd.clone());
78 }
79 if !self.environment {
80 return None;
81 }
82 std::env::var(format!("DD_{name}")).ok().and_then(|v| dd_value(&v).ok())
83 }
84
85 pub fn assigned(&self, assign: &str) -> Option<Dd> {
88 self.get(assign).or_else(|| {
89 let (_, name) = assign.rsplit_once('-')?;
90 let b = name.as_bytes();
91 let dd_name = !b.is_empty() && b.len() <= 8 && matches!(b[0], b'A'..=b'Z' | b'#' | b'$' | b'@') && b[1..].iter().all(|c| matches!(c, b'A'..=b'Z' | b'0'..=b'9' | b'#' | b'$' | b'@'));
92 if dd_name { self.get(name) } else { None }
93 })
94 }
95}
96
97enum Handle {
98 Reader(BufReader<File>),
99 Writer(BufWriter<File>),
100 Keyed(Box<Keyed>),
101 Empty,
102}
103
104#[derive(Clone, Debug, Default, PartialEq, Eq)]
107pub struct KeySpan {
108 pub offset: usize,
109 pub len: usize,
110 pub split: Vec<(usize, usize)>,
111}
112
113impl KeySpan {
114 pub fn new(offset: usize, len: usize) -> Self {
115 Self { offset, len, split: Vec::new() }
116 }
117
118 pub fn joined(pieces: Vec<(usize, usize)>) -> Self {
120 Self { offset: pieces.first().map_or(0, |p| p.0), len: pieces.iter().map(|p| p.1).sum(), split: pieces }
121 }
122
123 pub fn of(&self, record: &[u8]) -> Vec<u8> {
124 let piece = |offset: usize, len: usize| {
125 let mut bytes: Vec<u8> = record.iter().skip(offset).take(len).copied().collect();
126 bytes.resize(len, ebcdic::SPACE);
127 bytes
128 };
129 if self.split.is_empty() {
130 return piece(self.offset, self.len);
131 }
132 self.split.iter().flat_map(|&(offset, len)| piece(offset, len)).collect()
133 }
134
135}
136
137#[derive(Clone, Debug, PartialEq, Eq)]
140pub enum Keying {
141 Position,
143 Relative,
146 Indexed { prime: KeySpan, alternates: Vec<(KeySpan, bool)> },
148}
149
150impl Keying {
151 pub fn keys(&self) -> Vec<&KeySpan> {
153 match self {
154 Self::Indexed { prime, alternates } => std::iter::once(prime).chain(alternates.iter().map(|(span, _)| span)).collect(),
155 _ => Vec::new(),
156 }
157 }
158}
159
160pub const MAX_RELATIVE: u64 = 16_777_215;
164
165pub fn record_number(n: u64) -> Vec<u8> {
166 n.to_be_bytes().to_vec()
167}
168
169pub fn number_of(key: &[u8]) -> u64 {
170 u64::from_be_bytes(key.try_into().unwrap_or([0; 8]))
171}
172
173type Entry = (Vec<u8>, u64, Vec<u8>);
177
178struct Alternate {
179 span: KeySpan,
180 duplicates: bool,
181 index: BTreeSet<Entry>,
182 arrival: HashMap<Vec<u8>, u64>,
183}
184
185impl Alternate {
186 fn holders<'a>(&'a self, value: &'a [u8]) -> impl Iterator<Item = &'a Entry> {
187 self.index.range((value.to_vec(), 0, Vec::new())..).take_while(move |(v, ..)| v == value)
188 }
189}
190
191#[derive(Clone, Debug)]
193enum Cursor {
194 First,
196 At { which: usize, at: Entry, inclusive: bool },
199 Undefined,
201}
202
203pub struct Found {
205 pub key: Vec<u8>,
206 pub record: Vec<u8>,
207 pub duplicate: bool,
209}
210
211pub struct Keyed {
215 pub keying: Keying,
216 records: BTreeMap<Vec<u8>, Vec<u8>>,
217 alternates: Vec<Alternate>,
218 arrivals: u64,
219 cursor: Cursor,
220 pub last_read: Option<Vec<u8>>,
223 dirty: bool,
224 path: Option<PathBuf>,
225 record_len: usize,
226 page: &'static CodePage,
227 mark: Option<PathBuf>,
229}
230
231impl Keyed {
232 fn new(keying: Keying, path: Option<PathBuf>, record_len: usize, page: &'static CodePage) -> Self {
233 let alternates = match &keying {
234 Keying::Indexed { alternates, .. } => {
235 alternates.iter().map(|(span, duplicates)| Alternate { span: span.clone(), duplicates: *duplicates, index: BTreeSet::new(), arrival: HashMap::new() }).collect()
236 }
237 _ => Vec::new(),
238 };
239 Self { keying, records: BTreeMap::new(), alternates, arrivals: 0, cursor: Cursor::First, last_read: None, dirty: true, path, record_len, page, mark: None }
240 }
241
242 pub fn prime_key(&self, record: &[u8]) -> Option<Vec<u8>> {
243 match &self.keying {
244 Keying::Indexed { prime, .. } => Some(prime.of(record)),
245 _ => None,
246 }
247 }
248
249 pub fn highest_key(&self) -> Option<&Vec<u8>> {
250 self.records.keys().next_back()
251 }
252
253 pub fn record(&self, key: &[u8]) -> Option<&Vec<u8>> {
254 self.records.get(key)
255 }
256
257 fn key_len(&self, which: usize) -> usize {
258 match (&self.keying, which) {
259 (Keying::Indexed { prime, .. }, 0) => prime.len,
260 (Keying::Indexed { .. }, n) => self.alternates[n - 1].span.len,
261 _ => 8,
262 }
263 }
264
265 pub fn get(&self, which: usize, value: &[u8]) -> Option<Found> {
267 if which == 0 {
268 return self.records.get(value).map(|r| Found { key: value.to_vec(), record: r.clone(), duplicate: false });
269 }
270 let mut hits = self.alternates[which - 1].holders(value);
271 let key = hits.next()?.2.clone();
272 Some(Found { record: self.records[&key].clone(), key, duplicate: hits.next().is_some() })
273 }
274
275 fn put(&mut self, key: Vec<u8>, record: Vec<u8>) -> Result<bool, FileStatus> {
279 let mut shared = false;
280 for alt in &self.alternates {
281 let value = alt.span.of(&record);
282 let others = alt.holders(&value).any(|(_, _, k)| *k != key);
283 if others && !alt.duplicates {
284 return Err(FileStatus::DuplicateKey);
285 }
286 shared |= others;
287 }
288 let old = self.records.get(&key).cloned();
289 let mut arrivals = self.arrivals;
290 for alt in &mut self.alternates {
291 let now = alt.span.of(&record);
292 if let Some(old) = &old {
293 let was = alt.span.of(old);
294 if was == now {
295 continue;
296 }
297 let seq = alt.arrival[&key];
298 alt.index.remove(&(was, seq, key.clone()));
299 }
300 alt.index.insert((now, arrivals, key.clone()));
301 alt.arrival.insert(key.clone(), arrivals);
302 arrivals += 1;
303 }
304 self.arrivals = arrivals;
305 self.records.insert(key, record);
306 self.dirty = true;
307 Ok(shared)
308 }
309
310 pub fn insert(&mut self, key: Vec<u8>, record: Vec<u8>) -> Result<bool, FileStatus> {
313 if self.records.contains_key(&key) {
314 return Err(FileStatus::DuplicateKey);
315 }
316 self.put(key, record)
317 }
318
319 pub fn replace(&mut self, key: Vec<u8>, record: Vec<u8>) -> Result<bool, FileStatus> {
321 if !self.records.contains_key(&key) {
322 return Err(FileStatus::NotFound);
323 }
324 self.put(key, record)
325 }
326
327 pub fn remove(&mut self, key: &[u8]) -> Option<Vec<u8>> {
328 let record = self.records.remove(key)?;
329 for alt in &mut self.alternates {
330 if let Some(seq) = alt.arrival.remove(key) {
331 alt.index.remove(&(alt.span.of(&record), seq, key.to_vec()));
332 }
333 }
334 self.dirty = true;
335 Some(record)
336 }
337
338 pub fn start(&mut self, which: usize, wanted: Ordering, or_equal: bool, value: &[u8]) -> bool {
343 let value = &value[..value.len().min(self.key_len(which))];
344 let fits = |k: &[u8]| {
345 let o = k[..value.len().min(k.len())].cmp(value);
346 o == wanted || (or_equal && o == Ordering::Equal)
347 };
348 let from = (value.to_vec(), 0, Vec::new());
349 let found = match (which, wanted) {
350 (0, Ordering::Less) => self.records.keys().rev().find(|k| fits(k)).map(|k| (k.clone(), 0, Vec::new())),
351 (0, _) => self.records.range(from.0..).map(|(k, _)| k).find(|k| fits(k)).map(|k| (k.clone(), 0, Vec::new())),
352 (n, Ordering::Less) => self.alternates[n - 1].index.iter().rev().find(|(k, ..)| fits(k)).cloned(),
353 (n, _) => self.alternates[n - 1].index.range(from..).find(|(k, ..)| fits(k)).cloned(),
354 };
355 self.last_read = None;
356 match found {
357 Some(at) => {
358 self.cursor = Cursor::At { which, at, inclusive: true };
359 true
360 }
361 None => {
362 self.lose_position();
363 false
364 }
365 }
366 }
367
368 pub fn lose_position(&mut self) {
370 self.cursor = Cursor::Undefined;
371 self.last_read = None;
372 }
373
374 pub fn read_at(&mut self, which: usize, key: &[u8]) {
376 let at = match which {
377 0 => (key.to_vec(), 0, Vec::new()),
378 n => {
379 let alt = &self.alternates[n - 1];
380 (alt.span.of(&self.records[key]), alt.arrival[key], key.to_vec())
381 }
382 };
383 self.cursor = Cursor::At { which, at, inclusive: false };
384 self.last_read = Some(key.to_vec());
385 }
386
387 pub fn seek(&self, from: &[u8], inclusive: bool, backward: bool) -> Option<(Vec<u8>, Vec<u8>)> {
390 let edge = if inclusive { Bound::Included(from.to_vec()) } else { Bound::Excluded(from.to_vec()) };
391 let mut hits = if backward { self.records.range((Bound::Unbounded, edge)) } else { self.records.range((edge, Bound::Unbounded)) };
392 let hit = if backward { hits.next_back() } else { hits.next() };
393 hit.map(|(k, r)| (k.clone(), r.clone()))
394 }
395
396 pub fn step(&mut self, backward: bool) -> Result<Option<Found>, &'static str> {
398 self.read_next(backward).map_err(FileStatus::as_str)
399 }
400
401 pub fn read_next(&mut self, backward: bool) -> Result<Option<Found>, FileStatus> {
403 let (which, bound) = match &self.cursor {
404 Cursor::Undefined => return Err(FileStatus::NoNextRecord),
405 Cursor::First => (0, Bound::Unbounded),
406 Cursor::At { which, at, inclusive: true } => (*which, Bound::Included(at.clone())),
407 Cursor::At { which, at, inclusive: false } => (*which, Bound::Excluded(at.clone())),
408 };
409 let range = if backward { (Bound::Unbounded, bound) } else { (bound, Bound::Unbounded) };
410 let hit = if which == 0 {
411 let mut keys = self.records.range((range.0.map(|(k, ..)| k), range.1.map(|(k, ..)| k))).map(|(k, _)| k);
412 let key = if backward { keys.next_back() } else { keys.next() };
413 key.map(|k| (k.clone(), false))
414 } else {
415 let mut entries = self.alternates[which - 1].index.range(range);
416 let entry = if backward { entries.next_back() } else { entries.next() };
417 entry.map(|(value, _, key)| {
418 let after = if backward { entries.next_back() } else { entries.next() };
419 (key.clone(), after.is_some_and(|(v, ..)| v == value))
420 })
421 };
422 let Some((key, duplicate)) = hit else {
423 self.lose_position();
424 return Ok(None);
425 };
426 self.read_at(which, &key);
427 Ok(Some(Found { record: self.records[&key].clone(), key, duplicate }))
428 }
429
430 fn inbound(&self, raw: Vec<u8>, format: Format) -> Vec<u8> {
431 let mut record = match format {
432 Format::Text => self.page.encode_lossy(&String::from_utf8_lossy(&raw)),
433 _ => raw,
434 };
435 if format != Format::Variable {
436 record.resize(self.record_len.max(record.len()), ebcdic::SPACE);
437 }
438 record
439 }
440
441 fn outbound(&self, record: &[u8], format: Format) -> Vec<u8> {
442 match format {
443 Format::Text => format!("{}\n", self.page.decode(record).trim_end()).into_bytes(),
444 Format::Fixed => {
445 let mut r = record.to_vec();
446 r.resize(self.record_len, ebcdic::SPACE);
447 r
448 }
449 Format::Variable => record.to_vec(),
450 }
451 }
452
453 fn save(&self, format: Format) -> io::Result<()> {
454 let Some(path) = self.path.as_ref().filter(|_| self.dirty) else { return Ok(()) };
455 let mut out = Open { mode: OpenMode::Output, format, handle: Handle::Writer(BufWriter::new(File::create(path)?)), head: Head::Start, page: None, assigned: None };
456 let empty = match format {
457 Format::Fixed => vec![0; self.record_len],
458 Format::Variable => Vec::new(),
459 Format::Text => b"\n".to_vec(),
460 };
461 let mut next = 1;
462 for (key, record) in &self.records {
463 if self.keying == Keying::Relative {
464 for _ in next..number_of(key) {
465 out.write(&empty)?;
466 }
467 next = number_of(key) + 1;
468 }
469 out.write(&self.outbound(record, format))?;
470 }
471 out.close()
472 }
473}
474
475pub fn open_mark(data_set: &Path) -> PathBuf {
480 let mut name = data_set.as_os_str().to_owned();
481 name.push(".open-for-output");
482 PathBuf::from(name)
483}
484
485pub fn left_open(data_set: &Path) -> bool {
487 data_set.is_file() && open_mark(data_set).is_file()
488}
489
490pub fn clear_open_mark(data_set: &Path) -> io::Result<()> {
492 remove_mark(&open_mark(data_set))
493}
494
495fn remove_mark(mark: &Path) -> io::Result<()> {
496 match std::fs::remove_file(mark) {
497 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
498 other => other,
499 }
500}
501
502pub struct Open {
503 pub mode: OpenMode,
504 pub format: Format,
505 handle: Handle,
506 head: Head,
507 pub page: Option<crate::linage::Page>,
509 pub assigned: Option<String>,
511}
512
513#[derive(Clone, Copy, Debug, PartialEq, Eq)]
515pub enum Move {
516 Lines(u64),
517 Page,
518}
519
520#[derive(Clone, Copy, Debug, PartialEq, Eq)]
523enum Head {
524 Start,
525 OnLine(bool),
526 Fresh,
527}
528
529pub enum Record {
531 Data(Vec<u8>),
532 WrongLength(Vec<u8>),
534 End,
535}
536
537pub fn open_keyed(dd: Option<&Dd>, mode: OpenMode, format: Format, keying: Keying, record_len: usize, page: &'static CodePage) -> io::Result<Open> {
540 let path = dd.filter(|_| mode != OpenMode::Input).map(|d| d.path.clone());
541 let mut keyed = Keyed::new(keying, path, record_len, page);
542 if let Some(dd) = dd.filter(|d| mode != OpenMode::Output && d.path.exists()) {
543 let mut reader = open(dd, OpenMode::Input, format)?;
544 let mut number = 0u64;
545 loop {
546 let raw = match reader.read(record_len)? {
547 Record::End => break,
548 Record::Data(r) | Record::WrongLength(r) => r,
549 };
550 number += 1;
551 if keyed.keying == Keying::Relative && raw.iter().all(|&b| b == 0) {
552 continue;
553 }
554 let record = keyed.inbound(raw, format);
555 let key = keyed.prime_key(&record).unwrap_or_else(|| record_number(number));
556 keyed.insert(key, record).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, format!("{}: two records with one key", dd.path.display())))?;
557 }
558 keyed.dirty = false;
559 }
560 Ok(Open { mode, format, handle: Handle::Keyed(Box::new(keyed)), head: Head::Start, page: None, assigned: None })
561}
562
563pub fn open(dd: &Dd, mode: OpenMode, format: Format) -> io::Result<Open> {
564 let handle = match mode {
565 OpenMode::Input => Handle::Reader(BufReader::new(File::open(&dd.path)?)),
567 OpenMode::Output if !dd.append => Handle::Writer(BufWriter::new(File::create(&dd.path)?)),
569 OpenMode::Output | OpenMode::Extend => Handle::Writer(BufWriter::new(OpenOptions::new().append(true).create(true).open(&dd.path)?)),
570 OpenMode::InputOutput => return Err(io::Error::new(io::ErrorKind::Unsupported, "OPEN I-O of a line-sequential file")),
571 };
572 Ok(Open { mode, format, handle, head: Head::Start, page: None, assigned: None })
573}
574
575pub fn absent() -> Open {
577 Open { mode: OpenMode::Input, format: Format::Fixed, handle: Handle::Empty, head: Head::Start, page: None, assigned: None }
578}
579
580impl Open {
581 pub fn keyed(&mut self) -> Option<&mut Keyed> {
582 match &mut self.handle {
583 Handle::Keyed(k) => Some(k),
584 _ => None,
585 }
586 }
587
588 pub fn is_keyed(&self) -> bool {
589 matches!(self.handle, Handle::Keyed(_))
590 }
591
592 pub fn mark_open(&mut self, data_set: &Path) -> io::Result<()> {
594 let Handle::Keyed(k) = &mut self.handle else { return Ok(()) };
595 let mark = open_mark(data_set);
596 std::fs::write(&mark, b"")?;
597 k.mark = Some(mark);
598 Ok(())
599 }
600
601 pub fn read(&mut self, fixed_len: usize) -> io::Result<Record> {
602 let reader = match &mut self.handle {
603 Handle::Reader(r) => r,
604 Handle::Empty => return Ok(Record::End),
605 Handle::Writer(_) | Handle::Keyed(_) => return Err(io::Error::other("a sequential READ of a file not open for it")),
606 };
607 match self.format {
608 Format::Fixed => {
609 let mut buf = Vec::with_capacity(fixed_len);
610 reader.by_ref().take(fixed_len as u64).read_to_end(&mut buf)?;
611 Ok(match buf.len() {
612 0 => Record::End,
613 n if n == fixed_len => Record::Data(buf),
614 _ => Record::WrongLength(buf),
615 })
616 }
617 Format::Variable => {
618 let mut rdw = [0u8; 4];
619 match reader.read_exact(&mut rdw) {
620 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(Record::End),
621 other => other?,
622 }
623 let len = u16::from_be_bytes([rdw[0], rdw[1]]) as usize;
624 if len < 4 {
625 return Err(io::Error::new(io::ErrorKind::InvalidData, format!("an RDW of length {len}")));
626 }
627 let mut buf = vec![0u8; len - 4];
628 reader.read_exact(&mut buf)?;
629 Ok(Record::Data(buf))
630 }
631 Format::Text => {
632 let mut line = Vec::new();
633 if reader.read_until(b'\n', &mut line)? == 0 {
634 return Ok(Record::End);
635 }
636 while matches!(line.last(), Some(b'\n' | b'\r')) {
637 line.pop();
638 }
639 Ok(Record::Data(line))
640 }
641 }
642 }
643
644 pub fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
645 let Handle::Writer(w) = &mut self.handle else {
646 return Err(io::Error::other("WRITE to a file not opened for output"));
647 };
648 match self.format {
649 Format::Fixed | Format::Text => w.write_all(bytes),
650 Format::Variable => {
651 let len = u16::try_from(bytes.len() + 4).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "a record over 32,760 bytes"))?;
652 w.write_all(&len.to_be_bytes())?;
653 w.write_all(&[0, 0])?;
654 w.write_all(bytes)
655 }
656 }
657 }
658
659 pub fn print(&mut self, before: Option<Move>, line: &str, after: Option<Move>) -> io::Result<()> {
664 if let Some(m) = before {
665 self.feed(m)?;
666 }
667 let Handle::Writer(w) = &mut self.handle else {
668 return Err(io::Error::other("WRITE to a file not opened for output"));
669 };
670 let shown = matches!(self.head, Head::OnLine(true));
671 if shown && !line.is_empty() {
672 w.write_all(b"\r")?;
673 }
674 w.write_all(line.as_bytes())?;
675 self.head = Head::OnLine(shown || !line.is_empty());
676 match after {
677 Some(m) => self.feed(m),
678 None => Ok(()),
679 }
680 }
681
682 fn feed(&mut self, m: Move) -> io::Result<()> {
683 let Handle::Writer(w) = &mut self.handle else {
684 return Err(io::Error::other("WRITE to a file not opened for output"));
685 };
686 match (m, self.head) {
687 (Move::Lines(0), _) => return Ok(()),
688 (Move::Lines(n), head) => {
689 for _ in 0..n - u64::from(head == Head::Start) {
690 w.write_all(b"\n")?;
691 }
692 }
693 (Move::Page, Head::OnLine(_)) => w.write_all(b"\n\x0c")?,
694 (Move::Page, _) => w.write_all(b"\x0c")?,
695 }
696 self.head = Head::Fresh;
697 Ok(())
698 }
699
700 pub fn close(self) -> io::Result<()> {
701 match self.handle {
702 Handle::Writer(mut w) => {
703 if matches!(self.head, Head::OnLine(_)) {
704 w.write_all(b"\n")?;
705 }
706 w.flush()
707 }
708 Handle::Keyed(k) => {
709 k.save(self.format)?;
710 k.mark.as_deref().map_or(Ok(()), remove_mark)
711 }
712 _ => Ok(()),
713 }
714 }
715
716 pub fn abandon(mut self) -> io::Result<()> {
720 if let Handle::Keyed(k) = &mut self.handle {
721 k.mark = None;
722 }
723 self.close()
724 }
725}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730
731 #[test]
732 fn dd_specs() {
733 let dds = Dds::new(&["IN=/tmp/a.txt:text".into(), "out=/tmp/b.dat".into()], false).unwrap();
734 assert_eq!(dds.get("IN"), Some(Dd { path: "/tmp/a.txt".into(), format: Some(Format::Text), append: false }));
735 assert_eq!(dds.get("OUT").unwrap().format, None);
736 assert!(dds.get("NONE").is_none());
737 let labelled = Dds::new(&["MASTER=/tmp/m".into(), "UT-S-X=/tmp/x".into()], false).unwrap();
738 assert_eq!(labelled.assigned("DA-MASTER").map(|d| d.path), Some("/tmp/m".into()));
739 assert_eq!(labelled.assigned("UT-S-X").map(|d| d.path), Some("/tmp/x".into()));
740 assert!(labelled.assigned("DA-NOTHERE").is_none() && labelled.assigned("A-TOOLONGNAME").is_none() && labelled.assigned("DA-9BAD").is_none());
741 assert!(Dds::new(&["NOEQUALS".into()], false).is_err());
742 }
743
744 #[test]
745 fn mod_suffix_follows_the_format_or_stands_alone() {
746 let dd = |value: &str| dd_value(value).unwrap();
747 assert_eq!(dd("a.dat:mod"), Dd { path: "a.dat".into(), format: None, append: true });
748 assert_eq!(dd("a.dat:text:mod"), Dd { path: "a.dat".into(), format: Some(Format::Text), append: true });
749 assert_eq!(dd("A.DAT:TEXT:MOD"), Dd { path: "A.DAT".into(), format: Some(Format::Text), append: true });
750 assert_eq!(dd("a.dat:text"), Dd { path: "a.dat".into(), format: Some(Format::Text), append: false });
751 assert_eq!(dd("c:\\a.dat"), Dd { path: "c:\\a.dat".into(), format: None, append: false });
752 assert!(dd_value(":mod").is_err());
753 }
754
755 #[test]
756 fn open_output_on_a_mod_dd_writes_after_the_records_there() {
757 let path = std::env::temp_dir().join(format!("ironwork-mod-{}", std::process::id()));
758 let write = |append: bool, records: &[&[u8]]| {
759 let mut out = open(&Dd { path: path.clone(), format: None, append }, OpenMode::Output, Format::Fixed).unwrap();
760 for record in records {
761 out.write(record).unwrap();
762 }
763 out.close().unwrap();
764 };
765 write(false, &[b"AA", b"BB"]);
766 write(true, &[b"CC"]);
767 assert_eq!(std::fs::read(&path).unwrap(), b"AABBCC");
768 write(false, &[b"DD"]);
769 assert_eq!(std::fs::read(&path).unwrap(), b"DD");
770 std::fs::remove_file(&path).unwrap();
771 }
772
773 #[test]
774 fn variable_records_round_trip_through_rdws() {
775 let path = std::env::temp_dir().join(format!("ironwork-vb-{}", std::process::id()));
776 let dd = Dd { path: path.clone(), format: None, append: false };
777 let mut out = open(&dd, OpenMode::Output, Format::Variable).unwrap();
778 out.write(b"AB").unwrap();
779 out.write(b"CDEF").unwrap();
780 out.close().unwrap();
781 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']);
782 let mut input = open(&dd, OpenMode::Input, Format::Variable).unwrap();
783 assert!(matches!(input.read(0).unwrap(), Record::Data(d) if d == b"AB"));
784 assert!(matches!(input.read(0).unwrap(), Record::Data(d) if d == b"CDEF"));
785 assert!(matches!(input.read(0).unwrap(), Record::End));
786 }
787}