Skip to main content

ironwork_rt/
fileio.rs

1//! The file statements (lir.md ยง9.4): OPEN, CLOSE, READ, WRITE, REWRITE, DELETE and START on one
2//! file, FILE STATUS, LINAGE and the print carriage. A sequential file streams through its DD; an
3//! indexed or relative file, or a sequential file opened I-O, is held in memory ([`Keyed`]). Each
4//! verb returns the status its phrases or the file's error path read; the executor runs those.
5
6use crate::abend::Abend;
7use crate::files::{self, Dd, FileStatus, Format, KeySpan, Keyed, Keying, Move, Open, Record};
8use crate::host::{self, Host};
9use crate::linage::{Geometry, Motion, Page};
10use crate::lir::{Access, Carriage, Organization, Spacing, StartRel};
11use crate::printer::{self, Controls};
12use crate::storage::{Loc, Val};
13use crate::store::{self, ProgramFacts};
14use crate::unit::Event;
15use crate::vocab::{Closing, OpenMode, Pos};
16use numeric::VsamOpenFs;
17use numeric::precision::{Fixed, Places};
18use std::cmp::Ordering;
19use zarch::ebcdic;
20
21type R<T> = Result<T, Abend>;
22
23/// A file as SELECT and FD declare it ([`crate::lir::FileDesc`]), with the executor's handles: `P`
24/// a data item, `X` an integer it evaluates. Its keys are the executor's to resolve
25/// ([`Files::keying`]).
26#[derive(Clone, Copy)]
27pub struct File<'a, P, X> {
28    pub index: usize,
29    pub name: &'a str,
30    /// The DD name ASSIGN gives.
31    pub assign: &'a str,
32    /// The data item ASSIGN names, whose value is the DD name at each OPEN, and where the SELECT
33    /// is (`--compliance extended`).
34    pub assign_item: Option<(P, Pos)>,
35    pub organization: Organization,
36    pub access: Access,
37    pub optional: bool,
38    /// How records are held when the DD does not say.
39    pub format: Format,
40    pub status: Option<P>,
41    pub relative: Option<P>,
42    pub linage: Option<Linage<X>>,
43    pub carriage: Option<Carriage>,
44    /// Where the record area is in run-unit memory, and its length.
45    pub area: (usize, usize),
46    /// The shortest and longest variable-length record a READ takes without a record length
47    /// conflict ([`crate::lir::FileDesc::read_lengths`]).
48    pub read_lengths: (usize, usize),
49    pub depending: Option<Depending<P>>,
50}
51
52/// RECORD IS VARYING DEPENDING ON: the item holding a record's length, and the shortest and longest
53/// record the clause allows.
54#[derive(Clone, Copy)]
55pub struct Depending<P> {
56    pub item: P,
57    pub lengths: (usize, usize),
58}
59
60/// LINAGE's values, evaluated in this order whenever the page's geometry is taken, and
61/// LINAGE-COUNTER.
62#[derive(Clone, Copy)]
63pub struct Linage<X> {
64    pub lines: X,
65    pub footing: Option<X>,
66    pub top: Option<X>,
67    pub bottom: Option<X>,
68    pub counter: Option<Loc>,
69}
70
71/// READ ([`crate::lir::FileVerb::Read`]). `key` is READ ... KEY IS.
72#[derive(Clone, Copy)]
73pub struct Read<P> {
74    pub sequential: bool,
75    pub previous: bool,
76    pub into: Option<P>,
77    pub key: Option<P>,
78}
79
80/// WRITE's ADVANCING phrase ([`crate::lir::Advance`]). A mnemonic-name's `space` is None when its
81/// environment-name is no printer channel.
82#[derive(Clone, Copy)]
83pub enum Advance<'a, X> {
84    Lines { before: bool, count: X },
85    Page { before: bool },
86    Mnemonic { before: bool, space: Option<Spacing>, name: &'a str, environment: &'a str },
87}
88
89/// How a verb ended.
90pub enum Outcome {
91    /// FILE STATUS holds its status and nothing is left to run.
92    Done,
93    /// The status the statement's AT END (`at_end`) or INVALID KEY phrase, or else the file's
94    /// error path, reads.
95    Status { status: FileStatus, at_end: bool },
96    /// A WRITE to a LINAGE file, with FILE STATUS set: END-OF-PAGE or NOT END-OF-PAGE runs.
97    Page { end_of_page: bool },
98    /// A failing status that takes the file's error path at once.
99    Failed(Failure),
100}
101
102/// `mode` is the mode the file is open in, or being opened in; `open_or_close` whether the
103/// statement that failed is an OPEN or a CLOSE.
104pub struct Failure {
105    pub status: FileStatus,
106    pub mode: Option<OpenMode>,
107    pub open_or_close: bool,
108    pub message: String,
109}
110
111/// Whether `abend` is the ending `unhandled` gives a failing status.
112pub fn is_unhandled_io(abend: &Abend) -> bool {
113    use crate::abend::{AbendCode, LeCondition};
114    matches!(abend.code, AbendCode::Io(_) | AbendCode::Le(LeCondition::FileStatus))
115}
116
117/// How a failing status ends the run when no FILE STATUS holds it and no EXCEPTION/ERROR
118/// procedure takes it (assumption C451); None where control returns to the program. An OPEN or
119/// CLOSE of a VSAM file (indexed or relative, C220) returns control whatever its status (Programming
120/// Guide, 'Handling errors in VSAM files'). Any other failure is a severity-3 condition nothing
121/// handles, which ends the run U4038 with a message (Programming Guide, 'Handling errors in input and
122/// output operations' and its two flow figures): IGZ0035S for an OPEN or CLOSE, IGZ0020S for a logic
123/// error (4x), IGZ0197S for a line-sequential (z/OS UNIX) file's permanent error and IGZ0002S, QSAM's
124/// SYNAD message, for a sequential file's. IBM names no message for an AT END or INVALID KEY no
125/// phrase takes, or a VSAM file's permanent error; ironwork says which phrase was missing.
126pub fn unhandled(status: FileStatus, organization: Organization, open_or_close: bool, file: &str, program: &str, detail: String, pos: Pos) -> Option<Abend> {
127    use crate::abend::{AbendCode, LeCondition};
128    if !status.ends_the_run() || open_or_close && matches!(organization, Organization::Indexed | Organization::Relative) {
129        return None;
130    }
131    let code = status.as_str();
132    let message = if open_or_close {
133        format!("IGZ0035S There was an unsuccessful OPEN or CLOSE of file {file} in program {program}. Neither FILE STATUS nor an ERROR declarative were specified. The status code was {code}. ({detail})")
134    } else if status.covers('4') {
135        format!("IGZ0020S A logic error occurred. Neither FILE STATUS nor a declarative was specified for file {file} in program {program}. The status code was {code}. ({detail})")
136    } else if status.covers('3') && organization == Organization::LineSequential {
137        format!("IGZ0197S There was an unsuccessful READ or WRITE of file {file} in program {program}. Neither FILE STATUS nor an ERROR declarative were specified. The file status code was {code}. ({detail})")
138    } else if status.covers('3') && organization == Organization::Sequential {
139        format!("IGZ0002S {detail}")
140    } else {
141        let phrase = if status.covers('1') {
142            "an AT END phrase, "
143        } else if status.covers('2') {
144            "an INVALID KEY phrase, "
145        } else {
146            ""
147        };
148        format!("Neither {phrase}FILE STATUS nor a declarative was specified for file {file} in program {program}. The status code was {code}. ({detail})")
149    };
150    Some(Abend { code: AbendCode::Le(LeCondition::FileStatus), message, pos, file: None })
151}
152
153/// What the file verbs ask of the executor beyond [`Host`].
154pub trait Files<P: Copy, X: Copy>: Host<P> {
155    /// The program's file `k` while it is open.
156    fn slot(&mut self, k: usize) -> &mut Option<Open>;
157    /// Whether CLOSE WITH LOCK has closed file `k`.
158    fn locked(&mut self, k: usize) -> &mut bool;
159    fn dd(&self, assign: &str) -> Option<Dd>;
160    fn notify(&mut self, event: Event<'_>);
161    /// Tells the input trace the operand of an operation an input could steer.
162    fn sink(&mut self, kind: &'static str, pos: Pos, operand: &str);
163    fn int(&mut self, value: X, pos: Pos) -> R<i64>;
164    /// How file `k` finds its records by key.
165    fn keying(&mut self, k: usize, pos: Pos) -> R<Keying>;
166    /// Which key of indexed file `k` the item names (0 the prime key, then each alternate), and
167    /// its value in the record area. With `partial` (START) it may be a leading part of the key.
168    fn key_value(&mut self, k: usize, keying: &Keying, key: P, partial: bool, pos: Pos) -> R<(usize, Vec<u8>)>;
169}
170
171pub fn set_status<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, status: FileStatus, pos: Pos) -> R<()> {
172    if let Some(p) = file.status {
173        let loc = receiver(x, p)?;
174        let bytes = x.facts().page().encode(status.as_str()).map_err(|e| Abend::ironwork(e.to_string(), pos))?;
175        x.assign(loc, Val::Bytes(bytes), None, pos)?;
176    }
177    Ok(())
178}
179
180/// Locates a receiver the verb only writes, FILE STATUS: its old bytes reach nothing, so they are
181/// not read.
182fn receiver<P: Copy>(x: &mut impl Host<P>, p: P) -> R<Loc> {
183    let was = x.taint().is_some_and(|t| t.writing(true));
184    let loc = x.locate(p, false);
185    if let Some(t) = x.taint() {
186        t.writing(was);
187    }
188    loc
189}
190
191/// A failure in the mode file `k` is open in now.
192fn failed<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, status: FileStatus, open_or_close: bool, message: String) -> Outcome {
193    let mode = x.slot(file.index).as_ref().map(|f| f.mode);
194    Outcome::Failed(Failure { status, mode, open_or_close, message })
195}
196
197pub fn sequential<P, X>(file: &File<'_, P, X>) -> bool {
198    file.access == Access::Sequential || file.organization == Organization::Sequential
199}
200
201/// Whether the file's DD holds a byte before each record for the printer control character: a
202/// print file under ADV, unless the DD is text, which shows the character as line spacing.
203pub fn adds_control_byte<P, X>(file: &File<'_, P, X>, format: Format) -> bool {
204    file.carriage.is_some_and(|c| !c.reserved) && format != Format::Text
205}
206
207/// How the file's DD holds its records.
208pub fn dd_format<P: Copy, X: Copy>(x: &impl Files<P, X>, file: &File<'_, P, X>) -> Format {
209    x.dd(file.assign).and_then(|d| d.format).unwrap_or(file.format)
210}
211
212fn record_area<P: Copy>(x: &mut impl Host<P>, (offset, size): (usize, usize)) -> &[u8] {
213    &x.mem()[offset..offset + size]
214}
215
216/// The prime key's value in the record area, which a random READ, START or DELETE reads.
217fn read_prime_key<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, prime: &KeySpan) -> Vec<u8> {
218    if let Some(taint) = x.taint() {
219        taint.read(file.area.0 + prime.offset, prime.len);
220    }
221    prime.of(record_area(x, file.area))
222}
223
224fn relative_value<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, pos: Pos) -> R<i64> {
225    let r = file.relative.ok_or_else(|| Abend::ironwork(format!("{} has no RELATIVE KEY", file.name), pos))?;
226    x.integer(r, pos)
227}
228
229/// The RELATIVE KEY's record number as a key, or None when it is below 1 (assumption C331).
230fn relative_number<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, pos: Pos) -> R<Option<Vec<u8>>> {
231    let n = relative_value(x, file, pos)?;
232    Ok((n >= 1).then(|| files::record_number(n as u64)))
233}
234
235/// Whether record number `n` fits the RELATIVE KEY item, as sequential READ and WRITE store it
236/// there.
237fn relative_fits<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, n: u64) -> R<bool> {
238    let Some(r) = file.relative else { return Ok(true) };
239    let loc = x.locate(r, false)?;
240    Ok(match loc.kind.digits_scale() {
241        Some((digits, scale)) => digits.saturating_sub(scale) >= 19 || n < 10u64.pow(digits.saturating_sub(scale)),
242        None => true,
243    })
244}
245
246/// Runs `op` on file `k` when it is open and held in memory.
247fn held<P: Copy, X: Copy, F: Files<P, X>, T>(x: &mut F, k: usize, op: impl FnOnce(&mut F, OpenMode, Format, &mut Keyed) -> R<T>) -> R<Option<T>> {
248    let Some(mut f) = x.slot(k).take() else { return Ok(None) };
249    let (mode, format) = (f.mode, f.format);
250    let result = match f.keyed() {
251        Some(keyed) => op(x, mode, format, keyed).map(Some),
252        None => Ok(None),
253    };
254    *x.slot(k) = Some(f);
255    result
256}
257
258fn is_held<P: Copy, X: Copy>(x: &mut impl Files<P, X>, k: usize) -> bool {
259    x.slot(k).as_ref().is_some_and(|f| f.is_keyed())
260}
261
262/// Whether the file is a print file opened I-O whose records hold the byte ADV adds
263/// ([`numeric::assumptions::PRINT_FILE_UPDATE`]).
264fn held_control_byte<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>) -> bool {
265    x.slot(file.index).as_ref().is_some_and(|f| f.is_keyed() && adds_control_byte(file, f.format))
266}
267
268/// A record as WRITE or REWRITE puts it in the file: a fixed record at the length of the area.
269fn record_bytes<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, loc: Loc, format: Format) -> Vec<u8> {
270    let mut bytes = store::bytes(x.mem(), loc).to_vec();
271    if format != Format::Variable {
272        bytes.resize(file.area.1.max(bytes.len()), ebcdic::SPACE);
273    }
274    bytes
275}
276
277/// The record WRITE, REWRITE or RELEASE puts out: under DEPENDING ON the record area's first n
278/// bytes, n the item's value, or status 44 when n is outside the clause's lengths (Language
279/// Reference SC27-8713-03, pp. 188, 302).
280pub fn record_length<P: Copy, X: Copy>(x: &mut impl Host<P>, file: &File<'_, P, X>, loc: Loc, pos: Pos) -> R<Result<Loc, FileStatus>> {
281    let Some(d) = file.depending else { return Ok(Ok(loc)) };
282    let n = x.integer(d.item, pos)?;
283    let (shortest, longest) = d.lengths;
284    if n < shortest as i64 || n > longest.min(file.area.1) as i64 {
285        return Ok(Err(FileStatus::RecordLengthChanged));
286    }
287    Ok(Ok(Loc { offset: file.area.0, len: n as usize, ..loc }))
288}
289
290/// Moves a record into the file's area, and to INTO's item; a variable-length record fills only
291/// its own length, which DEPENDING ON's item receives. True when the record was longer than the
292/// area.
293fn deliver<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, record: &[u8], variable: bool, into: Option<P>, pos: Pos) -> R<bool> {
294    let (offset, size) = file.area;
295    let n = record.len().min(size);
296    let mem = x.mem();
297    mem[offset..offset + n].copy_from_slice(&record[..n]);
298    if !variable {
299        mem[offset + n..offset + size].fill(ebcdic::SPACE);
300    }
301    if let Some(t) = x.taint() {
302        t.set(offset, n, true);
303        t.set(offset + n, if variable { 0 } else { size - n }, false);
304    }
305    if let Some(d) = file.depending {
306        host::set_integer(x, d.item, n as i64, pos)?;
307    }
308    if let Some(r) = into {
309        let dest = x.locate(r, true)?;
310        let moved = if variable { n } else { size };
311        if let Some(t) = x.taint() {
312            t.read(offset, moved);
313        }
314        let bytes = x.mem()[offset..offset + moved].to_vec();
315        x.assign(dest, Val::Bytes(bytes), None, pos)?;
316    }
317    Ok(record.len() > size)
318}
319
320/// Whether a record READ delivered has a record length conflict, status 04: it was longer than the
321/// record area, or it is a variable-length record outside the file's read lengths
322/// ([`numeric::assumptions::VLR_RECORDS_CHECKED`]).
323fn length_conflict<P, X>(file: &File<'_, P, X>, len: usize, variable: bool, long: bool) -> bool {
324    let (shortest, longest) = file.read_lengths;
325    long || variable && !(shortest..=longest).contains(&len)
326}
327
328/// OPEN: a file whose data set is unavailable is status 35, or 05 when it is OPTIONAL, which
329/// OPEN EXTEND then creates (Language Reference SC27-8713-03, pp. 300-301). A VSAM data set a run
330/// left open for output is verified, status 97 under VSAMOPENFS(COMPAT), which takes the error
331/// path but never ends the run, or 00 under SUCC ([`numeric::assumptions::VSAM_DATA_SET_LEFT_OPEN`]).
332pub fn open<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, mode: OpenMode, pos: Pos) -> R<Outcome> {
333    let assigned = match file.assign_item {
334        Some((item, select)) => Some(assigned_dd(x, item, select)?),
335        None => None,
336    };
337    let closed = x.slot(file.index).is_none();
338    let outcome = open_on(x, file, mode, pos, assigned.as_ref())?;
339    if let Some((dd, _)) = assigned
340        && closed
341        && let Some(f) = x.slot(file.index).as_mut()
342    {
343        f.assigned = Some(dd);
344    }
345    Ok(outcome)
346}
347
348/// The DD the data item ASSIGN names holds, read as OPEN reads it, and the item's value. Micro
349/// Focus and GnuCOBOL take a value with no directory as the name DD_name maps to a file, as a DD
350/// maps one here: the value's blanks go, and one that cannot be a DD name names no DD, so a
351/// program never opens a file it names itself (C360). The input trace has the value at the
352/// SELECT, where cobolwork's finding is, and whether input reached it is the item's alone, not
353/// what the statement read for a file before it.
354fn assigned_dd<P: Copy, X: Copy>(x: &mut impl Files<P, X>, item: P, select: Pos) -> R<(String, String)> {
355    let read_before = x.taint().map(|t| {
356        let pending = t.pending();
357        t.start_statement();
358        pending
359    });
360    let loc = x.locate(item, false)?;
361    if let Some(taint) = x.taint() {
362        taint.read(loc.offset, loc.len);
363    }
364    let bytes = x.mem()[loc.offset..loc.offset + loc.len].to_vec();
365    let value = x.facts().page().decode(&bytes).trim().to_string();
366    x.sink("dynamic-file-path", select, &value);
367    if let (Some(before), Some(taint)) = (read_before, x.taint()) {
368        taint.resume_statement(before);
369    }
370    let named = value.len() <= 8 && value.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || "@#$".contains(c)) && value.chars().all(|c| c.is_ascii_alphanumeric() || "@#$".contains(c));
371    Ok((if named { value.to_ascii_uppercase() } else { String::new() }, value))
372}
373
374fn open_on<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, mode: OpenMode, pos: Pos, assigned: Option<&(String, String)>) -> R<Outcome> {
375    let (k, name) = (file.index, file.name);
376    let assign = assigned.map_or(file.assign, |(dd, _)| dd.as_str());
377    let failure = |status, message| Ok(Outcome::Failed(Failure { status, mode: Some(mode), open_or_close: true, message }));
378    if *x.locked(k) {
379        return failure(FileStatus::ClosedWithLock, format!("{name} was closed WITH LOCK"));
380    }
381    if x.slot(k).is_some() {
382        return failure(FileStatus::AlreadyOpen, format!("{name} is already open"));
383    }
384    let page = match (&file.linage, mode) {
385        (Some(_), OpenMode::Output | OpenMode::Extend) => Some(Page::opened(geometry(x, file, pos)?)),
386        _ => None,
387    };
388    let default = file.format;
389    let dd = if assign.is_empty() { None } else { x.dd(assign) };
390    if let Some(d) = &dd {
391        x.notify(Event::Open { dd: assign, mode, path: &d.path });
392    }
393    let no_dd = match assigned {
394        Some((dd, value)) if dd.is_empty() => format!("{name}: ASSIGN's data item holds {value:?}, which is not a DD name; a run opens only the files its DDs give"),
395        _ => format!("{name}: no DD {assign} was given (--dd {assign}=path)"),
396    };
397    let held = match file.organization {
398        Organization::Indexed | Organization::Relative => true,
399        Organization::Sequential => mode == OpenMode::InputOutput,
400        Organization::LineSequential => false,
401    };
402    if held {
403        let status = match &dd {
404            Some(d) if mode == OpenMode::Output || d.path.exists() => FileStatus::Success,
405            _ if file.optional && mode != OpenMode::Output => FileStatus::SuccessOptional,
406            None => return failure(FileStatus::FileNotFound, no_dd),
407            Some(d) => return failure(FileStatus::FileNotFound, format!("{name}: {}: no such file", d.path.display())),
408        };
409        let keying = x.keying(k, pos)?;
410        let format = dd.as_ref().and_then(|d| d.format).unwrap_or(default);
411        let record_len = file.area.1 + usize::from(adds_control_byte(file, format));
412        let vsam = dd.as_ref().filter(|_| matches!(file.organization, Organization::Indexed | Organization::Relative));
413        let verified = vsam.is_some_and(|d| files::left_open(&d.path));
414        let mut f = match files::open_keyed(dd.as_ref(), mode, format, keying, record_len, x.facts().page()) {
415            Ok(f) => f,
416            Err(e) => return failure(FileStatus::PermanentError, format!("{name}: {e}")),
417        };
418        if let Some(d) = vsam.filter(|_| mode != OpenMode::Input)
419            && let Err(e) = f.mark_open(&d.path)
420        {
421            return failure(FileStatus::PermanentError, format!("{name}: {}: {e}", files::open_mark(&d.path).display()));
422        }
423        let status = match x.facts().options().vsamopenfs {
424            _ if !verified => status,
425            VsamOpenFs::Compat => FileStatus::SuccessVerified,
426            VsamOpenFs::Succ => FileStatus::Success,
427        };
428        opened(x, file, f, status, pos)?;
429        return Ok(match (status, vsam) {
430            (FileStatus::SuccessVerified, Some(d)) => failed(x, file, status, true, format!("{name}: {} was left open for output, and OPEN verified it", d.path.display())),
431            _ => Outcome::Done,
432        });
433    }
434    match dd {
435        _ if file.optional && mode == OpenMode::Input && dd.as_ref().is_none_or(|d| !d.path.exists()) => opened(x, file, files::absent(), FileStatus::SuccessOptional, pos),
436        None => failure(FileStatus::FileNotFound, no_dd),
437        Some(dd) if mode == OpenMode::Extend && !file.optional && !dd.path.exists() => failure(FileStatus::FileNotFound, format!("{name}: {}: no such file", dd.path.display())),
438        Some(dd) => {
439            let created = mode == OpenMode::Extend && !dd.path.exists();
440            match files::open(&dd, mode, dd.format.unwrap_or(default)) {
441                Ok(mut f) => {
442                    f.page = page;
443                    opened(x, file, f, if created { FileStatus::SuccessOptional } else { FileStatus::Success }, pos)
444                }
445                Err(e) => {
446                    let status = match e.kind() {
447                        std::io::ErrorKind::NotFound => FileStatus::FileNotFound,
448                        std::io::ErrorKind::Unsupported => FileStatus::OpenModeUnsupported,
449                        _ => FileStatus::PermanentError,
450                    };
451                    failure(status, format!("{name}: {}: {e}", dd.path.display()))
452                }
453            }
454        }
455    }
456}
457
458/// Holds the file open; OPEN sets a LINAGE file's LINAGE-COUNTER to 1 (Language Reference
459/// SC27-8713-03, p. 24).
460fn opened<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, f: Open, status: FileStatus, pos: Pos) -> R<Outcome> {
461    *x.slot(file.index) = Some(f);
462    set_linage_counter(x, file, 1, pos)?;
463    set_status(x, file, status, pos)?;
464    Ok(Outcome::Done)
465}
466
467/// The page the file's LINAGE clause gives as its data items stand now.
468fn geometry<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, pos: Pos) -> R<Geometry> {
469    let Some(linage) = file.linage else { return Err(Abend::ironwork(format!("{} has no LINAGE clause", file.name), pos)) };
470    let body = x.int(linage.lines, pos)?;
471    let footing = match linage.footing {
472        Some(v) => Some(x.int(v, pos)?),
473        None => None,
474    };
475    let top = match linage.top {
476        Some(v) => x.int(v, pos)?,
477        None => 0,
478    };
479    let bottom = match linage.bottom {
480        Some(v) => x.int(v, pos)?,
481        None => 0,
482    };
483    Geometry::new(body, footing, top, bottom).map_err(|why| Abend::ironwork(format!("{}: {why} ({})", file.name, numeric::assumptions::LINAGE_VALUES), pos))
484}
485
486fn set_linage_counter<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, value: u64, pos: Pos) -> R<()> {
487    let Some(loc) = file.linage.and_then(|l| l.counter) else { return Ok(()) };
488    x.store_fixed(loc, &Fixed::new(value as i128, Places::new(19, 0)), pos)
489}
490
491/// CLOSE, and its phrases as IBM's table has them for a file on a medium without reels or units
492/// (Language Reference for Enterprise COBOL 6.4, 'Effect of CLOSE statement on file types'): REEL
493/// or UNIT leaves the file open and sets status 07, NO REWIND closes it with 07, and LOCK closes it
494/// so that OPEN refuses it with 38.
495pub fn close<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, closing: Option<Closing>, pos: Pos) -> R<Outcome> {
496    let name = file.name;
497    if closing == Some(Closing::Volume) && x.slot(file.index).is_some() {
498        set_status(x, file, FileStatus::SuccessNonReel, pos)?;
499        return Ok(Outcome::Done);
500    }
501    match x.slot(file.index).take() {
502        None => Ok(failed(x, file, FileStatus::NotOpen, true, format!("{name} is not open"))),
503        Some(f) => {
504            let (mode, assigned) = (Some(f.mode), f.assigned.clone());
505            match f.close() {
506                Ok(()) => {
507                    let assign = assigned.as_deref().unwrap_or(file.assign);
508                    if !assign.is_empty()
509                        && let Some(d) = x.dd(assign)
510                    {
511                        x.notify(Event::Close { dd: assign, path: &d.path });
512                    }
513                    *x.locked(file.index) |= closing == Some(Closing::Lock);
514                    let status = if closing == Some(Closing::NoRewind) { FileStatus::SuccessNonReel } else { FileStatus::Success };
515                    set_status(x, file, status, pos)?;
516                    Ok(Outcome::Done)
517                }
518                Err(e) => Ok(Outcome::Failed(Failure { status: FileStatus::PermanentError, mode, open_or_close: true, message: format!("{name}: {e}") })),
519            }
520        }
521    }
522}
523
524/// READ of a held file by key or in sequence, else of a stream, whose phrase is AT END.
525pub fn read<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, r: Read<P>, pos: Pos) -> R<Outcome> {
526    let k = file.index;
527    if !is_held(x, k) {
528        return read_stream(x, file, r.into, pos);
529    }
530    let sequential = r.sequential;
531    let added = held_control_byte(x, file);
532    let (status, found, variable) = held(x, k, |x, mode, format, keyed| {
533        let variable = format == Format::Variable;
534        if !matches!(mode, OpenMode::Input | OpenMode::InputOutput) {
535            return Ok((FileStatus::NotOpenInput, None, variable));
536        }
537        if sequential {
538            return Ok(match keyed.read_next(r.previous) {
539                Err(status) => (status, None, variable),
540                Ok(None) => (FileStatus::AtEnd, None, variable),
541                Ok(Some(found)) if keyed.keying == Keying::Relative && !relative_fits(x, file, files::number_of(&found.key))? => (FileStatus::RelativeKeyOverflow, None, variable),
542                Ok(Some(found)) => (if found.duplicate { FileStatus::SuccessDuplicate } else { FileStatus::Success }, Some(found), variable),
543            });
544        }
545        let keying = keyed.keying.clone();
546        let (which, value) = match (&keying, r.key) {
547            (Keying::Indexed { .. }, Some(key)) => x.key_value(k, &keying, key, false, pos)?,
548            (Keying::Indexed { prime, .. }, None) => (0, read_prime_key(x, file, prime)),
549            _ => match relative_number(x, file, pos)? {
550                Some(key) => (0, key),
551                None => {
552                    keyed.lose_position();
553                    return Ok((FileStatus::NotFound, None, variable));
554                }
555            },
556        };
557        Ok(match keyed.get(which, &value) {
558            None => {
559                keyed.lose_position();
560                (FileStatus::NotFound, None, variable)
561            }
562            Some(found) => {
563                keyed.read_at(which, &found.key);
564                (if found.duplicate { FileStatus::SuccessDuplicate } else { FileStatus::Success }, Some(found), variable)
565            }
566        })
567    })?
568    .unwrap_or((FileStatus::NotOpenInput, None, false));
569    let mut status = status;
570    if let Some(found) = found {
571        if sequential && file.organization == Organization::Relative
572            && let Some(rk) = file.relative
573        {
574            host::set_integer(x, rk, files::number_of(&found.key) as i64, pos)?;
575        }
576        let record = found.record.get(usize::from(added)..).unwrap_or_default();
577        let long = deliver(x, file, record, variable, r.into, pos)?;
578        if length_conflict(file, record.len(), variable, long) && status == FileStatus::Success {
579            status = FileStatus::SuccessWrongLength;
580        }
581    }
582    Ok(Outcome::Status { status, at_end: sequential })
583}
584
585fn read_stream<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, into: Option<P>, pos: Pos) -> R<Outcome> {
586    let k = file.index;
587    let at_end = |status| Ok(Outcome::Status { status, at_end: true });
588    let size = file.area.1;
589    let Some(mut f) = x.slot(k).take() else {
590        return at_end(FileStatus::NotOpenInput);
591    };
592    let added = adds_control_byte(file, f.format);
593    let read = f.read(size + usize::from(added));
594    let format = f.format;
595    let input = f.mode == OpenMode::Input;
596    *x.slot(k) = Some(f);
597    if !input {
598        return at_end(FileStatus::NotOpenInput);
599    }
600    let (record, wrong_length) = match read {
601        Err(e) => return Ok(failed(x, file, FileStatus::PermanentError, false, format!("READ {}: {e}", file.name))),
602        Ok(Record::End) => return at_end(FileStatus::AtEnd),
603        Ok(Record::Data(bytes)) => (bytes, false),
604        Ok(Record::WrongLength(bytes)) => (bytes, true),
605    };
606    let record = if added { record.get(1..).unwrap_or_default().to_vec() } else { record };
607    let record = if format == Format::Text {
608        let page = x.facts().page();
609        let unknown = page.encode_char('?').unwrap_or(0x6F);
610        String::from_utf8_lossy(&record).chars().map(|c| page.encode_char(c).unwrap_or(unknown)).collect()
611    } else {
612        record
613    };
614    let variable = format == Format::Variable;
615    let long = deliver(x, file, &record, variable, into, pos)?;
616    at_end(if wrong_length || length_conflict(file, record.len(), variable, long) { FileStatus::SuccessWrongLength } else { FileStatus::Success })
617}
618
619/// WRITE of the record at `loc`: to a LINAGE file's page, a stream, or a held file.
620pub fn write<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, loc: Loc, advancing: Option<Advance<'_, X>>, pos: Pos) -> R<Outcome> {
621    let k = file.index;
622    let loc = match record_length(x, file, loc, pos)? {
623        Ok(loc) => loc,
624        Err(status) => return Ok(Outcome::Status { status, at_end: false }),
625    };
626    if paged(x, k) {
627        return write_page(x, file, loc, advancing, pos);
628    }
629    if !is_held(x, k) {
630        let (before, space) = match advancing {
631            Some(a) => advance(x, a, pos)?,
632            None => (false, Spacing::Lines(1)),
633        };
634        return write_stream(x, file, loc, before, space, pos);
635    }
636    let sequential = sequential(file);
637    let status = held(x, k, |x, mode, format, keyed| {
638        let allowed = match mode {
639            OpenMode::Output => true,
640            OpenMode::Extend => sequential,
641            OpenMode::InputOutput => !sequential,
642            OpenMode::Input => false,
643        };
644        if !allowed || keyed.keying == Keying::Position {
645            return Ok(FileStatus::NotOpenOutput);
646        }
647        keyed.last_read = None;
648        let bytes = record_bytes(x, file, loc, format);
649        let key = match &keyed.keying {
650            Keying::Indexed { prime, .. } => {
651                let key = prime.of(&bytes);
652                if sequential && keyed.highest_key().is_some_and(|h| key <= *h) {
653                    return Ok(FileStatus::SequenceError);
654                }
655                key
656            }
657            _ if sequential => {
658                let n = keyed.highest_key().map_or(0, |h| files::number_of(h)) + 1;
659                if n > files::MAX_RELATIVE || !relative_fits(x, file, n)? {
660                    return Ok(FileStatus::BoundaryViolation);
661                }
662                if let Some(rk) = file.relative {
663                    host::set_integer(x, rk, n as i64, pos)?;
664                }
665                files::record_number(n)
666            }
667            _ => match relative_number(x, file, pos)? {
668                Some(key) if files::number_of(&key) <= files::MAX_RELATIVE => key,
669                _ => return Ok(FileStatus::BoundaryViolation),
670            },
671        };
672        Ok(match keyed.insert(key, bytes) {
673            Err(status) => status,
674            Ok(true) => FileStatus::SuccessDuplicate,
675            Ok(false) => FileStatus::Success,
676        })
677    })?
678    .unwrap_or(FileStatus::NotOpenOutput);
679    Ok(Outcome::Status { status, at_end: false })
680}
681
682fn paged<P: Copy, X: Copy>(x: &mut impl Files<P, X>, k: usize) -> bool {
683    x.slot(k).as_ref().is_some_and(|f| f.page.is_some())
684}
685
686/// A WRITE to a LINAGE file: the page decides how far the paper moves, and once the record is
687/// written LINAGE-COUNTER changes and an END-OF-PAGE phrase runs (Language Reference
688/// SC27-8713-03, pp. 474-475; Programming Guide SC27-8714-03, p. 178).
689fn write_page<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, loc: Loc, advancing: Option<Advance<'_, X>>, pos: Pos) -> R<Outcome> {
690    let k = file.index;
691    let (before, motion) = match advancing {
692        None => (false, Motion::Lines(1)),
693        Some(Advance::Lines { before, count }) => (before, Motion::Lines(x.int(count, pos)?.max(0) as u64)),
694        Some(Advance::Page { before }) => (before, Motion::Page),
695        Some(Advance::Mnemonic { name, .. }) => {
696            return Err(crate::refusal::IWR0068.abend(format_args!("ADVANCING {name} on {}, whose FD has LINAGE, is not supported yet", file.name), pos));
697        }
698    };
699    let Some(mut page) = x.slot(k).as_ref().and_then(|f| f.page) else { return Ok(Outcome::Done) };
700    let step = page.write(before, motion, || geometry(x, file, pos))?;
701    let controls = file.carriage.map(|c| printer::moving(c.machine, step.ahead, step.behind));
702    let text = (Some(Move::Lines(step.ahead)), before.then_some(Move::Lines(step.behind)));
703    if let failure @ Outcome::Failed(_) = put_line(x, file, loc, controls, text, pos)? {
704        return Ok(failure);
705    }
706    if let Some(f) = x.slot(k).as_mut() {
707        f.page = Some(page);
708    }
709    set_linage_counter(x, file, page.counter, pos)?;
710    Ok(Outcome::Page { end_of_page: step.end_of_page })
711}
712
713/// A WRITE's ADVANCING phrase as a movement, BEFORE or AFTER the line; a count below zero moves as
714/// zero ([`numeric::assumptions::PRINT_CONTROL_RUN_TIME`]).
715fn advance<P: Copy, X: Copy>(x: &mut impl Files<P, X>, a: Advance<'_, X>, pos: Pos) -> R<(bool, Spacing)> {
716    Ok(match a {
717        Advance::Lines { before, count } => (before, Spacing::Lines(x.int(count, pos)?.max(0) as u64)),
718        Advance::Page { before } => (before, Spacing::Channel(1)),
719        Advance::Mnemonic { before, space, name, environment } => {
720            let space = space.ok_or_else(|| Abend::ironwork(format!("ADVANCING {name}: {environment} is not a printer channel"), pos))?;
721            (before, space)
722        }
723    })
724}
725
726/// A WRITE to a sequential file: a print file's records carry the control character, a text DD
727/// shows it as line and form feeds.
728pub fn write_stream<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, loc: Loc, before: bool, space: Spacing, pos: Pos) -> R<Outcome> {
729    let controls = file.carriage.map(|c| printer::controls(c.machine, before, space));
730    put_line(x, file, loc, controls, printer::text_motion(before, space), pos)
731}
732
733/// Writes the record at `loc` to the sequential file behind `controls`, or to a text DD with the
734/// paper moved as `text` says before and after its line.
735fn put_line<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, loc: Loc, controls: Option<Controls>, text: (Option<Move>, Option<Move>), pos: Pos) -> R<Outcome> {
736    let (k, name) = (file.index, file.name);
737    let Some(mut f) = x.slot(k).take() else {
738        return Ok(failed(x, file, FileStatus::NotOpenOutput, false, format!("WRITE {name}: {}", FileStatus::NotOpenOutput.meaning())));
739    };
740    if f.mode == OpenMode::Input {
741        *x.slot(k) = Some(f);
742        return Ok(failed(x, file, FileStatus::NotOpenOutput, false, format!("WRITE {name}: {}", FileStatus::NotOpenOutput.meaning())));
743    }
744    let reserved = usize::from(file.carriage.is_some_and(|c| c.reserved));
745    if let Some(c) = controls.filter(|_| reserved == 1 && loc.len > 0) {
746        x.mem()[loc.offset] = c.data;
747        host::mark(x, loc.offset, 1);
748    }
749    let bytes = record_bytes(x, file, loc, f.format);
750    let written = match (f.format, controls) {
751        (Format::Text, _) => {
752            let line = x.facts().page().decode(bytes.get(reserved..).unwrap_or_default()).trim_end().to_owned();
753            f.print(text.0, &line, text.1)
754        }
755        (_, None) => f.write(&bytes),
756        (_, Some(c)) => c.records().try_for_each(|(control, line)| {
757            let mut record = Vec::with_capacity(bytes.len() + 1);
758            record.push(control);
759            if line {
760                record.extend_from_slice(&bytes[reserved.min(bytes.len())..]);
761            } else {
762                record.resize(bytes.len() + 1 - reserved.min(bytes.len()), ebcdic::SPACE);
763            }
764            f.write(&record)
765        }),
766    };
767    *x.slot(k) = Some(f);
768    match written {
769        Ok(()) => set_status(x, file, FileStatus::Success, pos).map(|()| Outcome::Done),
770        Err(e) => Ok(failed(x, file, FileStatus::PermanentError, false, format!("WRITE {name}: {e}"))),
771    }
772}
773
774/// REWRITE of the record at `loc`; its phrase is INVALID KEY.
775pub fn rewrite<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, loc: Loc, pos: Pos) -> R<FileStatus> {
776    let loc = match record_length(x, file, loc, pos)? {
777        Ok(loc) => loc,
778        Err(status) => return Ok(status),
779    };
780    let sequential = sequential(file);
781    let added = held_control_byte(x, file);
782    Ok(held(x, file.index, |x, mode, format, keyed| {
783        if mode != OpenMode::InputOutput {
784            return Ok(FileStatus::NotOpenInputOutput);
785        }
786        let mut bytes = record_bytes(x, file, loc, format);
787        if added {
788            let control = keyed.last_read.as_ref().and_then(|key| keyed.record(key)).and_then(|old| old.first().copied()).unwrap_or(ebcdic::SPACE);
789            bytes.insert(0, control);
790        }
791        let prior = keyed.last_read.take();
792        let key = if sequential {
793            let Some(prior) = prior else { return Ok(FileStatus::NoPriorRead) };
794            if keyed.prime_key(&bytes).is_some_and(|key| key != prior) {
795                return Ok(FileStatus::SequenceError);
796            }
797            prior
798        } else {
799            match keyed.prime_key(&bytes) {
800                Some(key) => key,
801                None => match relative_number(x, file, pos)? {
802                    Some(key) => key,
803                    None => return Ok(FileStatus::NotFound),
804                },
805            }
806        };
807        if keyed.keying == Keying::Position && keyed.record(&key).is_some_and(|old| old.len() != bytes.len()) {
808            return Ok(FileStatus::RecordLengthChanged);
809        }
810        Ok(match keyed.replace(key, bytes) {
811            Err(status) => status,
812            Ok(true) => FileStatus::SuccessDuplicate,
813            Ok(false) => FileStatus::Success,
814        })
815    })?
816    .unwrap_or(FileStatus::NotOpenInputOutput))
817}
818
819/// DELETE; its phrase is INVALID KEY.
820pub fn delete<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, pos: Pos) -> R<FileStatus> {
821    let sequential = sequential(file);
822    Ok(held(x, file.index, |x, mode, _, keyed| {
823        if mode != OpenMode::InputOutput || keyed.keying == Keying::Position {
824            return Ok(FileStatus::NotOpenInputOutput);
825        }
826        let prior = keyed.last_read.take();
827        let key = match (&keyed.keying, sequential) {
828            (_, true) => match prior {
829                Some(key) => key,
830                None => return Ok(FileStatus::NoPriorRead),
831            },
832            (Keying::Indexed { prime, .. }, false) => read_prime_key(x, file, prime),
833            _ => match relative_number(x, file, pos)? {
834                Some(key) => key,
835                None => return Ok(FileStatus::NotFound),
836            },
837        };
838        Ok(if keyed.remove(&key).is_some() { FileStatus::Success } else { FileStatus::NotFound })
839    })?
840    .unwrap_or(FileStatus::NotOpenInputOutput))
841}
842
843/// DELETE FILE under `--compliance extended`: the data set of the DD the file is assigned to removed
844/// while the file is closed (assumption C485). Open, status 41; no DD or no data set, 35.
845pub fn delete_file<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, pos: Pos) -> R<Outcome> {
846    let name = file.name;
847    if x.slot(file.index).is_some() {
848        return Ok(failed(x, file, FileStatus::AlreadyOpen, true, format!("DELETE FILE {name}: the file is open")));
849    }
850    let assigned = match file.assign_item {
851        Some((item, select)) => Some(assigned_dd(x, item, select)?),
852        None => None,
853    };
854    let assign = assigned.as_ref().map_or(file.assign, |(dd, _)| dd.as_str());
855    let path = if assign.is_empty() { None } else { x.dd(assign).map(|d| d.path.clone()) };
856    match path.map(|p| std::fs::remove_file(&p).map(|()| p)) {
857        Some(Ok(_)) => {
858            set_status(x, file, FileStatus::Success, pos)?;
859            Ok(Outcome::Done)
860        }
861        Some(Err(e)) => Ok(failed(x, file, FileStatus::FileNotFound, true, format!("DELETE FILE {name}: {e}"))),
862        None => Ok(failed(x, file, FileStatus::FileNotFound, true, format!("DELETE FILE {name}: no DD {assign} was given (--dd {assign}=path)"))),
863    }
864}
865
866/// START; `key` is START ... KEY, and its phrase is INVALID KEY.
867pub fn start<P: Copy, X: Copy>(x: &mut impl Files<P, X>, file: &File<'_, P, X>, rel: StartRel, key: Option<P>, pos: Pos) -> R<FileStatus> {
868    let (wanted, or_equal) = match rel {
869        StartRel::Equal => (Ordering::Equal, false),
870        StartRel::Greater => (Ordering::Greater, false),
871        StartRel::NotLess => (Ordering::Greater, true),
872        StartRel::Less => (Ordering::Less, false),
873        StartRel::NotGreater => (Ordering::Less, true),
874    };
875    let k = file.index;
876    Ok(held(x, k, |x, mode, _, keyed| {
877        if !matches!(mode, OpenMode::Input | OpenMode::InputOutput) || keyed.keying == Keying::Position {
878            return Ok(FileStatus::NotOpenInput);
879        }
880        let keying = keyed.keying.clone();
881        let (which, value) = match (&keying, key) {
882            (Keying::Indexed { .. }, Some(r)) => x.key_value(k, &keying, r, true, pos)?,
883            (Keying::Indexed { prime, .. }, None) => (0, read_prime_key(x, file, prime)),
884            (_, Some(r)) => (0, files::record_number(x.integer(r, pos)?.max(0) as u64)),
885            (_, None) => (0, files::record_number(relative_value(x, file, pos)?.max(0) as u64)),
886        };
887        Ok(if keyed.start(which, wanted, or_equal, &value) { FileStatus::Success } else { FileStatus::NotFound })
888    })?
889    .unwrap_or(FileStatus::NotOpenInput))
890}