Skip to main content

ironwork_rt/le/
parm.rs

1//! The parameter Language Environment gives a COBOL main program that EXEC PGM=,PARM= starts.
2
3use zarch::ebcdic::CodePage;
4
5/// Language Environment's runtime options, as the CEEXOPT sample in the Programming Guide
6/// (SA38-0682) spells them, with the NO forms it gives.
7const RUNTIME_OPTIONS: &[&str] = &[
8    "ABPERC", "ABTERMENC", "AIXBLD", "ALL31", "ANYHEAP", "BELOWHEAP", "CBLOPTS", "CBLPSHPOP", "CBLQDA", "CEEDUMP", "CHECK", "COUNTRY", "DEBUG", "DEPTHCONDLMT", "DYNDUMP", "ENVAR", "ERRCOUNT", "ERRUNIT", "FILEHIST", "FILETAG", "HEAP", "HEAPCHK",
9    "HEAPPOOLS", "HEAPZONES", "INFOMSGFILTER", "INQPCOPN", "INTERRUPT", "LIBSTACK", "MSGFILE", "MSGQ", "NATLANG", "NOTEST", "NOUSRHDLR", "OCSTATUS", "PAGEFRAMESIZE", "PC", "PLITASKCOUNT", "POSIX", "PROFILE", "PRTUNIT", "PUNUNIT", "RDRUNIT",
10    "RECPAD", "RPTOPTS", "RPTSTG", "RTEREUS", "SIMVRD", "STACK", "STORAGE", "TERMTHDACT", "TEST", "THREADHEAP", "THREADSTACK", "TRACE", "TRAP", "UPSI", "USRHDLR", "VCTRSAVE", "XPLINK", "XUFLOW",
11];
12
13/// The longest PARM= value JCL allows, in characters.
14pub const PARM_LIMIT: usize = 100;
15
16/// The program arguments in `parm` under CBLOPTS(ON), Language Environment's default outside
17/// CICS: what precedes the last slash, unless no word after it is a runtime option, when the
18/// whole string is (assumption C250).
19pub fn program_arguments(parm: &str) -> &str {
20    match parm.rfind('/') {
21        Some(at) if names_runtime_options(&parm[at + 1..]) => &parm[..at],
22        _ => parm,
23    }
24}
25
26/// Whether the runtime options in `parm` turn TRAP off, the last TRAP among them deciding
27/// ([`numeric::assumptions::TRAP_OFF_LEAVES_FILES_OPEN`]).
28pub fn trap_off(parm: &str) -> bool {
29    let trap = words(runtime_options(parm)).into_iter().rev().find_map(|w| {
30        let (name, value) = w.split_once('(')?;
31        name.eq_ignore_ascii_case("TRAP").then(|| value.trim_end_matches(')').split(',').next().unwrap_or("").trim().to_ascii_uppercase())
32    });
33    trap.as_deref() == Some("OFF")
34}
35
36/// The UPSI switches the runtime options in `parm` set, the last UPSI among them deciding: UPSI-0
37/// to UPSI-7, from the leftmost of its eight digits, each 1 for on and 0 for off (Language
38/// Environment Programming Reference, UPSI). None without one, and the message for one that is
39/// not eight such digits, which leaves the switches off (assumption C411).
40pub fn upsi(parm: &str) -> Option<Result<[bool; 8], String>> {
41    let option = words(runtime_options(parm)).into_iter().rev().find(|w| w.split('(').next().is_some_and(|name| name.trim().eq_ignore_ascii_case("UPSI")))?;
42    let digits = option.split_once('(').map(|(_, v)| v.trim_end_matches(')').trim());
43    let switches = digits.filter(|d| d.len() == 8 && d.bytes().all(|b| matches!(b, b'0' | b'1')));
44    Some(match switches {
45        Some(d) => Ok(std::array::from_fn(|n| d.as_bytes()[n] == b'1')),
46        None => Err(format!("runtime option {option}: UPSI takes eight digits, each 0 or 1, so the UPSI switches stay off")),
47    })
48}
49
50/// What `--compliance extended` gives ACCEPT ... FROM COMMAND-LINE, ARGUMENT-NUMBER and
51/// ARGUMENT-VALUE: the job step's program arguments as written, their blank-separated words, and
52/// the word the next ARGUMENT-VALUE takes (assumption C442).
53#[derive(Clone, Debug, Default, PartialEq, Eq)]
54pub struct Arguments {
55    pub text: String,
56    pub words: Vec<String>,
57    pub next: usize,
58}
59
60impl Arguments {
61    pub fn of(parm: &str) -> Self {
62        let text = program_arguments(parm).to_owned();
63        let words = text.split_whitespace().map(str::to_owned).collect();
64        Arguments { text, words, next: 0 }
65    }
66
67    /// The next word, None once there is none left.
68    pub fn take(&mut self) -> Option<&str> {
69        let word = self.words.get(self.next)?;
70        self.next += 1;
71        Some(word)
72    }
73
74    /// DISPLAY `n` UPON ARGUMENT-NUMBER: word `n` comes next, the last word when `n` is past them, as
75    /// GnuCOBOL gives it, and none when `n` is below 1, a PARM having no word 0.
76    pub fn position(&mut self, n: i64) {
77        self.next = match usize::try_from(n) {
78            Ok(n) if n >= 1 => n.min(self.words.len()).saturating_sub(1),
79            _ => self.words.len(),
80        };
81    }
82}
83
84/// The runtime options of `parm`: what follows its last slash, if that names any.
85fn runtime_options(parm: &str) -> &str {
86    match parm.rfind('/') {
87        Some(at) if names_runtime_options(&parm[at + 1..]) => &parm[at + 1..],
88        _ => "",
89    }
90}
91
92/// Nothing at all, or at least one runtime option among the words, each NAME or NAME(...),
93/// separated by commas or blanks.
94fn names_runtime_options(text: &str) -> bool {
95    let words = words(text);
96    words.is_empty() || words.iter().any(|w| RUNTIME_OPTIONS.iter().any(|o| o.eq_ignore_ascii_case(w.split('(').next().unwrap_or(w))))
97}
98
99/// The words of runtime options, each NAME or NAME(...), separated by commas or blanks.
100fn words(text: &str) -> Vec<String> {
101    let (mut words, mut word, mut depth) = (Vec::new(), String::new(), 0i32);
102    for c in text.chars() {
103        match c {
104            '(' => depth += 1,
105            ')' => depth -= 1,
106            ',' | ' ' if depth <= 0 => {
107                words.push(std::mem::take(&mut word));
108                continue;
109            }
110            _ => {}
111        }
112        word.push(c);
113    }
114    words.push(word);
115    words.retain(|w| !w.is_empty());
116    words
117}
118
119/// The storage the main program's first USING item addresses: a halfword length and the
120/// arguments in EBCDIC, then X'00' up to the longest PARM (assumption C251).
121pub fn parameter_area(arguments: &str, page: &CodePage) -> Vec<u8> {
122    let text = page.encode_lossy(arguments);
123    let mut area = (text.len() as u16).to_be_bytes().to_vec();
124    area.extend(&text);
125    area.resize(2 + PARM_LIMIT.max(text.len()), 0);
126    area
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn the_last_slash_ends_the_arguments_when_runtime_options_follow_it() {
135        assert_eq!(program_arguments("RUN=1,MODE=X"), "RUN=1,MODE=X");
136        assert_eq!(program_arguments("A/B/RPTOPTS(ON)"), "A/B");
137        assert_eq!(program_arguments("ABC/RPTOPTS(ON),MSGFILE(SYSOUT,FBA,121,0)"), "ABC");
138        assert_eq!(program_arguments("ABC/ rptstg(on) NOSUCH"), "ABC");
139        assert_eq!(program_arguments("ABC/"), "ABC");
140        assert_eq!(program_arguments("/TRAP(OFF)"), "");
141        assert_eq!(program_arguments("11/16/1967"), "11/16/1967", "the manual's example: 1967 is no runtime option");
142    }
143
144    #[test]
145    fn trap_is_off_when_the_last_trap_among_the_runtime_options_says_so() {
146        assert!(trap_off("/TRAP(OFF)"));
147        assert!(trap_off("ARGS/RPTOPTS(ON) trap(off,nospie)"));
148        assert!(!trap_off("/TRAP(OFF),TRAP(ON)"));
149        assert!(!trap_off("/TRAP(,NOSPIE)"));
150        assert!(!trap_off("TRAP(OFF)"), "with no slash the PARM is all program arguments");
151        assert!(!trap_off(""));
152    }
153
154    #[test]
155    fn the_program_arguments_are_the_command_line_and_its_words_are_taken_in_turn_from_a_position() {
156        let taken = |a: &mut Arguments, n: usize| (0..n).map(|_| a.take().map(str::to_owned)).collect::<Vec<_>>();
157        let word = |w: &str| Some(w.to_owned());
158        let mut a = Arguments::of("alpha  be 7/RPTOPTS(ON)");
159        assert_eq!((a.text.as_str(), a.words.len()), ("alpha  be 7", 3));
160        assert_eq!(taken(&mut a, 4), [word("alpha"), word("be"), word("7"), None]);
161        a.position(2);
162        assert_eq!(taken(&mut a, 1), [word("be")]);
163        a.position(9);
164        assert_eq!(taken(&mut a, 2), [word("7"), None]);
165        a.position(0);
166        assert_eq!(taken(&mut a, 1), [None]);
167        let mut empty = Arguments::of("");
168        empty.position(1);
169        assert_eq!((empty.text.clone(), taken(&mut empty, 1)), (String::new(), vec![None]));
170    }
171
172    #[test]
173    fn the_last_upsi_among_the_runtime_options_sets_the_switches_from_its_leftmost_digit() {
174        let on = |digits: &str| -> [bool; 8] { std::array::from_fn(|n| digits.as_bytes()[n] == b'1') };
175        assert_eq!(upsi("/UPSI(10000001)"), Some(Ok(on("10000001"))));
176        assert_eq!(upsi("ARGS/upsi(00000000) RPTOPTS(ON),UPSI( 01000000 )"), Some(Ok(on("01000000"))));
177        assert_eq!(upsi("UPSI(10000000)"), None, "with no slash the PARM is all program arguments");
178        assert_eq!(upsi("/TRAP(OFF)"), None);
179        for malformed in ["/UPSI(1000000)", "/UPSI(100000002)", "/UPSI", "/UPSI(1000000X)"] {
180            assert!(upsi(malformed).is_some_and(|r| r.is_err()), "{malformed}");
181        }
182    }
183
184    #[test]
185    fn the_area_holds_a_halfword_length_and_the_arguments_in_ebcdic() {
186        let page = numeric::options::Options::default().code_page();
187        let area = parameter_area("AB 1", page);
188        assert_eq!(&area[..6], [0, 4, 0xC1, 0xC2, 0x40, 0xF1]);
189        assert_eq!(area.len(), 2 + PARM_LIMIT);
190        assert!(area[6..].iter().all(|&b| b == 0));
191    }
192}