1use crate::copy::Libraries;
5use crate::{Error, Pos};
6use std::fmt::Write as _;
7use std::path::PathBuf;
8
9pub use rt::bms::{Attrb, Field, Initial, Intensity, Map, Mapset, Mode, Protection};
10
11pub fn find_mapset(libraries: &Libraries, name: &str) -> Option<Result<Mapset, Error>> {
14 load(libraries, name, None).map(|(_, mapset)| mapset)
15}
16
17pub(crate) fn load(libraries: &Libraries, name: &str, library: Option<&str>) -> Option<(PathBuf, Result<Mapset, Error>)> {
18 let path = libraries.find_bms(name, library)?;
19 let mapset = std::fs::read(&path)
20 .map_err(|e| Error::at(Pos::default(), e.to_string()))
21 .map(|bytes| crate::copy::decode(&bytes))
22 .and_then(|text| parse(&text))
23 .and_then(|mut sets| {
24 match sets.iter().position(|s| s.name.eq_ignore_ascii_case(name)) {
25 Some(i) => Ok(sets.swap_remove(i)),
26 None if sets.len() == 1 => Ok(sets.remove(0)),
27 None => Err(Error::at(Pos::default(), format!("no mapset {} among the {} in the file", name.to_ascii_uppercase(), sets.len()))),
28 }
29 })
30 .map_err(|mut e| {
31 e.file = Some(path.display().to_string());
32 e
33 });
34 Some((path, mapset))
35}
36
37#[derive(Clone, Debug)]
38enum Val {
39 Word(String),
40 Quoted(String),
41 List(Vec<String>),
42}
43
44struct Statement {
45 line: u32,
46 label: Option<String>,
47 macro_name: String,
48 operands: Vec<(String, Val)>,
49}
50
51fn fail(line: u32, message: impl Into<String>) -> Error {
52 Error::at(Pos { file: 0, line, col: 1 }, message)
53}
54
55fn statements(text: &str) -> Result<Vec<Statement>, Error> {
57 let mut out = Vec::new();
58 let mut lines = text.lines().enumerate();
59 while let Some((n, raw)) = lines.next() {
60 let start = n as u32 + 1;
61 let line: Vec<char> = raw.chars().take(72).collect();
62 if line.first() == Some(&'*') || line.starts_with(&['.', '*']) || line.iter().all(|c| c.is_whitespace()) {
63 continue;
64 }
65 let mut joined = String::new();
66 let mut from = 0;
67 let mut line = line;
68 loop {
69 let continued = line.len() == 72 && line[71] != ' ';
70 let end = if continued { 71 } else { line.len() };
71 joined.extend(line.get(from..end).unwrap_or_default());
72 if !continued {
73 break;
74 }
75 let (_, next) = lines.next().ok_or_else(|| fail(start, "a continuation line is missing"))?;
76 line = next.chars().take(72).collect();
77 from = 15;
78 }
79 out.push(statement(&joined, start)?);
80 }
81 Ok(out)
82}
83
84fn statement(text: &str, line: u32) -> Result<Statement, Error> {
85 let token = |s: &str| s.split(char::is_whitespace).next().unwrap_or_default().len();
86 let (label, rest) = if text.starts_with(char::is_whitespace) {
87 (None, text.trim_start())
88 } else {
89 let n = token(text);
90 (Some(text[..n].to_ascii_uppercase()), text[n..].trim_start())
91 };
92 let n = token(rest);
93 let macro_name = rest[..n].to_ascii_uppercase();
94 let rest = rest[n..].trim_start();
95 if macro_name.is_empty() {
96 return Err(fail(line, "a label with no macro"));
97 }
98 Ok(Statement { line, label, macro_name, operands: operands(rest, line)? })
99}
100
101fn operands(text: &str, line: u32) -> Result<Vec<(String, Val)>, Error> {
104 let chars: Vec<char> = text.chars().collect();
105 let mut i = 0;
106 let mut out = Vec::new();
107 while i < chars.len() {
108 let key_start = i;
109 while i < chars.len() && !matches!(chars[i], '=' | ',') && !chars[i].is_whitespace() {
110 i += 1;
111 }
112 let key: String = chars[key_start..i].iter().collect::<String>().to_ascii_uppercase();
113 let mut value = Val::Word(String::new());
114 if chars.get(i) == Some(&'=') {
115 i += 1;
116 value = match chars.get(i) {
117 Some('\'') => {
118 let (s, next) = quoted(&chars, i, line)?;
119 i = next;
120 Val::Quoted(s)
121 }
122 Some('(') => {
123 let (items, next) = list(&chars, i, line)?;
124 i = next;
125 Val::List(items)
126 }
127 _ => {
128 let start = i;
129 let mut in_quote = false;
130 while i < chars.len() && (in_quote || !(chars[i] == ',' || chars[i].is_whitespace())) {
131 in_quote ^= chars[i] == '\'';
132 i += 1;
133 }
134 Val::Word(chars[start..i].iter().collect())
135 }
136 };
137 }
138 if !key.is_empty() {
139 out.push((key, value));
140 }
141 if chars.get(i) == Some(&',') {
142 i += 1;
143 while chars.get(i).is_some_and(|c| c.is_whitespace()) {
144 i += 1;
145 }
146 } else {
147 break;
148 }
149 }
150 Ok(out)
151}
152
153fn quoted(chars: &[char], at: usize, line: u32) -> Result<(String, usize), Error> {
155 let mut s = String::new();
156 let mut i = at + 1;
157 while i < chars.len() {
158 if chars[i] == '\'' {
159 if chars.get(i + 1) == Some(&'\'') {
160 s.push('\'');
161 i += 2;
162 continue;
163 }
164 return Ok((s, i + 1));
165 }
166 s.push(chars[i]);
167 i += 1;
168 }
169 Err(fail(line, "a quoted string is not closed"))
170}
171
172fn list(chars: &[char], at: usize, line: u32) -> Result<(Vec<String>, usize), Error> {
173 let mut items = Vec::new();
174 let mut current = String::new();
175 let mut i = at + 1;
176 while i < chars.len() {
177 match chars[i] {
178 '\'' => {
179 let (s, next) = quoted(chars, i, line)?;
180 current.push_str(&s);
181 i = next;
182 continue;
183 }
184 ')' => {
185 items.push(current.trim().to_owned());
186 return Ok((items.into_iter().filter(|s| !s.is_empty()).collect(), i + 1));
187 }
188 ',' => items.push(std::mem::take(&mut current).trim().to_owned()),
189 c => current.push(c),
190 }
191 i += 1;
192 }
193 Err(fail(line, "a parenthesised list is not closed"))
194}
195
196struct Operands<'a> {
197 ops: &'a [(String, Val)],
198 line: u32,
199}
200
201impl Operands<'_> {
202 fn get(&self, key: &str) -> Option<&Val> {
203 self.ops.iter().find(|(k, _)| k == key).map(|(_, v)| v)
204 }
205
206 fn words(&self, key: &str) -> Option<Vec<String>> {
207 self.get(key).map(|v| match v {
208 Val::List(items) => items.iter().map(|s| s.to_ascii_uppercase()).collect(),
209 Val::Word(w) | Val::Quoted(w) => vec![w.to_ascii_uppercase()],
210 })
211 }
212
213 fn word(&self, key: &str) -> Result<Option<String>, Error> {
214 match self.words(key) {
215 None => Ok(None),
216 Some(mut w) if w.len() == 1 => Ok(w.pop()),
217 Some(_) => Err(fail(self.line, format!("{key} takes one value"))),
218 }
219 }
220
221 fn text(&self, key: &str) -> Result<Option<String>, Error> {
222 match self.get(key) {
223 None => Ok(None),
224 Some(Val::Word(s) | Val::Quoted(s)) => Ok(Some(s.clone())),
225 Some(Val::List(_)) => Err(fail(self.line, format!("{key} takes a quoted string"))),
226 }
227 }
228
229 fn number(&self, key: &str, low: u32, high: u32) -> Result<Option<u32>, Error> {
230 let Some(w) = self.word(key)? else { return Ok(None) };
231 match w.parse::<u32>() {
232 Ok(n) if (low..=high).contains(&n) => Ok(Some(n)),
233 _ => Err(fail(self.line, format!("{key}={w} is not a number from {low} to {high}"))),
234 }
235 }
236
237 fn yes_no(&self, key: &str) -> Result<Option<bool>, Error> {
238 match self.word(key)?.as_deref() {
239 None => Ok(None),
240 Some("YES") => Ok(Some(true)),
241 Some("NO") => Ok(Some(false)),
242 Some(other) => Err(fail(self.line, format!("{key}={other}: YES or NO"))),
243 }
244 }
245
246 fn dsatts(&self) -> Result<Option<Vec<String>>, Error> {
248 if let Some(list) = self.words("DSATTS") {
249 let mut out: Vec<String> = Vec::new();
250 for a in list {
251 if !EXTENDED.iter().any(|(name, _, _)| *name == a) {
252 return Err(fail(self.line, format!("DSATTS={a} is not an extended attribute")));
253 }
254 if !out.contains(&a) {
255 out.push(a);
256 }
257 }
258 return Ok(Some(out));
259 }
260 Ok(match self.word("EXTATT")?.as_deref() {
261 None => None,
262 Some("YES") => Some(EXTENDED.iter().filter(|(_, _, implied)| *implied).map(|(a, _, _)| a.to_string()).collect()),
263 Some("NO" | "MAPONLY") => Some(Vec::new()),
264 Some(other) => return Err(fail(self.line, format!("EXTATT={other}: NO, MAPONLY or YES"))),
265 })
266 }
267}
268
269const MAX_NAME: usize = 30;
270
271struct OpenSet {
272 set: Mapset,
273 tioapfx: bool,
274 dsatts: Vec<String>,
275}
276
277pub fn parse(text: &str) -> Result<Vec<Mapset>, Error> {
279 let mut done = Vec::new();
280 let mut open: Option<OpenSet> = None;
281 for st in statements(text)? {
282 let ops = Operands { ops: &st.operands, line: st.line };
283 match st.macro_name.as_str() {
284 "PRINT" | "TITLE" | "EJECT" | "SPACE" | "END" => {}
285 "DFHMSD" => {
286 if ops.word("TYPE")?.as_deref() == Some("FINAL") {
287 done.push(open.take().ok_or_else(|| fail(st.line, "DFHMSD TYPE=FINAL with no mapset open"))?.set);
288 continue;
289 }
290 done.extend(open.take().map(|o| o.set));
291 open = Some(mapset_header(&st, &ops)?);
292 }
293 "DFHMDI" => {
294 let o = open.as_mut().ok_or_else(|| fail(st.line, "DFHMDI outside a DFHMSD"))?;
295 let map = map_header(&st, &ops, o)?;
296 o.set.maps.push(map);
297 }
298 "DFHMDF" => {
299 let map = open.as_mut().and_then(|o| o.set.maps.last_mut()).ok_or_else(|| fail(st.line, "DFHMDF outside a DFHMDI map"))?;
300 let field = field(&st, &ops, map)?;
301 map.fields.push(field);
302 }
303 other => return Err(fail(st.line, format!("unknown macro {other}"))),
304 }
305 }
306 done.extend(open.map(|o| o.set));
307 Ok(done)
308}
309
310fn name_of(st: &Statement, what: &str, max: usize) -> Result<String, Error> {
311 let name = st.label.clone().ok_or_else(|| fail(st.line, format!("{what} needs a name")))?;
312 if name.len() > max {
313 return Err(fail(st.line, format!("{what} name {name} is longer than {max} characters")));
314 }
315 Ok(name)
316}
317
318fn mapset_header(st: &Statement, ops: &Operands) -> Result<OpenSet, Error> {
319 let name = name_of(st, "DFHMSD", MAX_NAME)?;
320 if let Some(t) = ops.word("TYPE")?
322 && !matches!(t.as_str(), "DSECT" | "MAP" | "&SYSPARM" | "&&SYSPARM")
323 {
324 return Err(fail(st.line, format!("TYPE={t}: DSECT, MAP, FINAL or &SYSPARM")));
325 }
326 let mode = match ops.word("MODE")?.as_deref() {
327 None | Some("OUT") => Mode::Out,
328 Some("IN") => Mode::In,
329 Some("INOUT") => Mode::InOut,
330 Some(other) => return Err(fail(st.line, format!("MODE={other}: IN, OUT or INOUT"))),
331 };
332 let auto = ops.word("STORAGE")?.as_deref() == Some("AUTO");
333 let tioapfx = ops.yes_no("TIOAPFX")?.unwrap_or(auto);
334 Ok(OpenSet {
335 set: Mapset { name, mode, ctrl: ops.words("CTRL").unwrap_or_default(), maps: Vec::new() },
336 tioapfx,
337 dsatts: ops.dsatts()?.unwrap_or_default(),
338 })
339}
340
341fn map_header(st: &Statement, ops: &Operands, open: &OpenSet) -> Result<Map, Error> {
342 let name = name_of(st, "DFHMDI", 7)?;
343 let (lines, columns) = match ops.words("SIZE") {
344 None => (24, 80),
345 Some(v) => {
346 let dims: Vec<u16> = v.iter().map(|s| s.parse().ok().filter(|n| (1..=240).contains(n)).ok_or(())).collect::<Result<_, _>>().map_err(|()| fail(st.line, "SIZE=(lines,columns), each 1 to 240"))?;
347 match dims[..] {
348 [l, c] => (l, c),
349 _ => return Err(fail(st.line, "SIZE=(lines,columns), each 1 to 240")),
350 }
351 }
352 };
353 let origin = |key: &str| -> Result<u16, Error> {
354 match ops.word(key)?.as_deref() {
355 None | Some("NEXT" | "SAME") => Ok(1),
356 Some(w) => w.parse().ok().filter(|n| (1..=240).contains(n)).ok_or_else(|| fail(st.line, format!("{key}={w} is not a number from 1 to 240"))),
357 }
358 };
359 Ok(Map {
360 name,
361 lines,
362 columns,
363 line: origin("LINE")?,
364 column: origin("COLUMN")?,
365 ctrl: ops.words("CTRL").unwrap_or_else(|| open.set.ctrl.clone()),
366 tioapfx: ops.yes_no("TIOAPFX")?.unwrap_or(open.tioapfx),
367 dsatts: ops.dsatts()?.unwrap_or_else(|| open.dsatts.clone()),
368 fields: Vec::new(),
369 })
370}
371
372fn attrb(ops: &Operands) -> Result<Attrb, Error> {
373 let mut a = Attrb { protection: Protection::Askip, numeric: false, intensity: Intensity::Norm, detectable: false, cursor: false, fset: false };
374 let Some(words) = ops.words("ATTRB") else { return Ok(a) };
375 a.protection = Protection::Unprot;
376 for w in words {
377 match w.as_str() {
378 "ASKIP" => a.protection = Protection::Askip,
379 "PROT" => a.protection = Protection::Prot,
380 "UNPROT" => a.protection = Protection::Unprot,
381 "NUM" => a.numeric = true,
382 "BRT" => a.intensity = Intensity::Brt,
383 "NORM" => a.intensity = Intensity::Norm,
384 "DRK" => a.intensity = Intensity::Drk,
385 "DET" => a.detectable = true,
386 "IC" => a.cursor = true,
387 "FSET" => a.fset = true,
388 other => return Err(fail(ops.line, format!("ATTRB={other} is not an attribute"))),
389 }
390 }
391 Ok(a)
392}
393
394pub fn picture_size(pic: &str) -> u32 {
397 let chars: Vec<char> = pic.chars().collect();
398 let (mut size, mut i) = (0, 0);
399 while i < chars.len() {
400 let c = chars[i].to_ascii_uppercase();
401 i += 1;
402 let mut repeat = 1;
403 if chars.get(i) == Some(&'(')
404 && let Some(close) = chars[i..].iter().position(|&c| c == ')')
405 {
406 repeat = chars[i + 1..i + close].iter().collect::<String>().parse().unwrap_or(1);
407 i += close + 1;
408 }
409 if !matches!(c, 'S' | 'V') {
410 size += repeat;
411 }
412 }
413 size
414}
415
416fn hex_bytes(s: &str, line: u32) -> Result<Vec<u8>, Error> {
417 if !s.len().is_multiple_of(2) || !s.is_ascii() {
418 return Err(fail(line, "XINIT takes an even number of hexadecimal digits"));
419 }
420 (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| fail(line, "XINIT takes hexadecimal digits"))).collect()
421}
422
423fn field(st: &Statement, ops: &Operands, map: &Map) -> Result<Field, Error> {
424 let name = st.label.as_ref().map(|_| name_of(st, "DFHMDF", MAX_NAME)).transpose()?;
425 let line = st.line;
426 let attrb = attrb(ops)?;
427 let occurs = ops.number("OCCURS", 1, u16::MAX.into())?.unwrap_or(1) as u16;
428 let group = ops.word("GRPNAME")?;
429 if group.is_some() && (ops.get("OCCURS").is_some() || name.is_none()) {
430 return Err(fail(line, "GRPNAME needs a labelled field and does not go with OCCURS"));
431 }
432 if group.as_ref().is_some_and(|g| g.len() > MAX_NAME) {
433 return Err(fail(line, "GRPNAME is longer than 30 characters"));
434 }
435 let initial = match (ops.text("INITIAL")?.or(ops.text("GINIT")?), ops.text("XINIT")?) {
436 (_, Some(x)) => Some(Initial::Bytes(hex_bytes(&x, line)?)),
437 (Some(t), None) => Some(Initial::Text(t)),
438 (None, None) => None,
439 };
440 let (picin, picout) = (ops.text("PICIN")?, ops.text("PICOUT")?);
441 let from_data = match &initial {
442 Some(Initial::Text(t)) => t.chars().count() as u32,
443 Some(Initial::Bytes(b)) => b.len() as u32,
444 None => 0,
445 };
446 let length = match ops.number("LENGTH", 0, 256)? {
447 Some(0) if name.is_some() => return Err(fail(line, "LENGTH=0 is allowed only on an unlabelled field, where it delimits an input field")),
448 Some(n) => n,
449 None => picin.iter().chain(&picout).map(|p| picture_size(p)).max().unwrap_or(from_data),
450 };
451 if length > 256 || (length == 0 && name.is_some()) {
452 return Err(fail(line, "LENGTH is missing, or is not from 1 to 256"));
453 }
454 let (line_no, column) = position(ops, map)?;
455 let justify = ops.words("JUSTIFY").unwrap_or_default();
456 for w in &justify {
457 if !matches!(w.as_str(), "LEFT" | "RIGHT" | "BLANK" | "ZERO") {
458 return Err(fail(line, format!("JUSTIFY={w}: LEFT or RIGHT, BLANK or ZERO")));
459 }
460 }
461 let has = |w: &str| justify.iter().any(|j| j == w);
462 let color = ops.word("COLOR")?;
463 if let Some(c) = &color
464 && !matches!(c.as_str(), "BLUE" | "RED" | "PINK" | "GREEN" | "TURQUOISE" | "YELLOW" | "NEUTRAL" | "DEFAULT")
465 {
466 return Err(fail(line, format!("COLOR={c} is not a colour")));
467 }
468 let hilight = ops.word("HILIGHT")?;
469 if let Some(h) = &hilight
470 && !matches!(h.as_str(), "OFF" | "BLINK" | "REVERSE" | "UNDERLINE")
471 {
472 return Err(fail(line, format!("HILIGHT={h} is not a highlight")));
473 }
474 Ok(Field {
475 name,
476 line: line_no,
477 column,
478 length: length as u16,
479 attrb,
480 initial,
481 picin,
482 picout,
483 occurs,
484 group,
485 justify_right: if has("RIGHT") || has("LEFT") { has("RIGHT") } else { attrb.numeric },
486 fill_zero: if has("ZERO") || has("BLANK") { has("ZERO") } else { attrb.numeric },
487 color,
488 hilight,
489 })
490}
491
492fn position(ops: &Operands, map: &Map) ->Result<(u16, u16), Error> {
494 let bad = || fail(ops.line, "POS is an offset within the map, or (line,column) inside it");
495 let (lines, columns) = (u32::from(map.lines), u32::from(map.columns));
496 let offset = match ops.words("POS") {
497 None => map.fields.last().map_or(0, |f| (u32::from(f.line) - 1) * columns + u32::from(f.column) - 1 + u32::from(f.occurs) * (u32::from(f.length) + 1)),
498 Some(v) => match v.iter().map(|s| s.parse::<u32>()).collect::<Result<Vec<_>, _>>().map_err(|_| bad())?[..] {
499 [n] => n,
500 [l, c] if (1..=lines).contains(&l) && (1..=columns).contains(&c) => (l - 1) * columns + c - 1,
501 _ => return Err(bad()),
502 },
503 };
504 if offset >= lines * columns {
505 return Err(bad());
506 }
507 Ok(((offset / columns + 1) as u16, (offset % columns + 1) as u16))
508}
509
510const EXTENDED: &[(&str, char, bool)] = &[
512 ("COLOR", 'C', true),
513 ("PS", 'P', true),
514 ("HILIGHT", 'H', true),
515 ("VALIDN", 'V', true),
516 ("OUTLINE", 'U', false),
517 ("SOSI", 'M', false),
518 ("TRANSP", 'T', false),
519];
520
521#[derive(Clone, Copy, Debug, PartialEq, Eq)]
526pub struct Slot {
527 pub field: usize,
528 pub occurrence: u16,
529 pub control: Option<usize>,
530 pub extended: usize,
531 pub data: usize,
532 pub size: usize,
533}
534
535impl Slot {
536 pub fn length_at(&self) -> Option<usize> {
538 self.control
539 }
540
541 pub fn attribute_at(&self) -> Option<usize> {
542 self.control.map(|c| c + 2)
543 }
544}
545
546pub fn extended_attributes(map: &Map) -> Vec<&'static str> {
548 EXTENDED.iter().filter(|(a, _, _)| map.dsatts.iter().any(|d| d == a)).map(|(a, _, _)| *a).collect()
549}
550
551pub fn slots(map: &Map, input: bool) -> Vec<Slot> {
554 let k = extended_attributes(map).len();
555 let mut at = if map.tioapfx { 12 } else { 0 };
556 let mut out = Vec::new();
557 let mut group: Option<&str> = None;
558 for (i, f) in map.fields.iter().enumerate().filter(|(_, f)| f.name.is_some()) {
559 let lead = match &f.group {
560 Some(g) => {
561 let first = group != Some(g.as_str());
562 group = Some(g);
563 first
564 }
565 None => {
566 group = None;
567 true
568 }
569 };
570 let picture = if input { &f.picin } else { &f.picout };
571 let size = picture.as_deref().map_or(usize::from(f.length), |p| picture_size(p) as usize);
572 let copies = if f.group.is_none() { f.occurs.max(1) } else { 1 };
573 for occurrence in 0..copies {
574 let control = lead.then_some(at);
575 if lead {
576 at += 3 + k;
577 }
578 out.push(Slot { field: i, occurrence, control, extended: at - k, data: at, size });
579 at += size;
580 }
581 }
582 out
583}
584
585pub fn symbolic_map(mapset: &Mapset) -> String {
586 let mut out = String::new();
587 for map in &mapset.maps {
588 if mapset.mode != Mode::Out {
589 structure(&mut out, map, true, mapset.mode);
590 }
591 if mapset.mode != Mode::In {
592 structure(&mut out, map, false, mapset.mode);
593 }
594 }
595 out
596}
597
598fn entry(out: &mut String, level: usize, name: &str, clauses: &str) {
600 let indent = if level == 1 { 7 } else { 11 + 3 * (level - 2) };
601 let sep = if level == 1 { " " } else { " " };
602 let head = format!("{:indent$}{level:02}{sep}{name}", "");
603 let line = if clauses.is_empty() { format!("{head}.") } else { format!("{head} {clauses}.") };
604 if line.len() <= 72 || clauses.is_empty() {
605 let _ = writeln!(out, "{line}");
606 } else {
607 let _ = writeln!(out, "{head}\n{:width$}{clauses}.", "", width = indent + 4);
608 }
609}
610
611fn structure(out: &mut String, map: &Map, input: bool, mode: Mode) {
612 let side = if input { 'I' } else { 'O' };
613 if input || mode == Mode::Out {
614 entry(out, 1, &format!("{}{side}", map.name), "");
615 } else {
616 entry(out, 1, &format!("{}O", map.name), &format!("REDEFINES {}I", map.name));
617 }
618 if map.tioapfx {
619 entry(out, 2, "FILLER", "PIC X(12)");
620 }
621 let mut group: Option<&str> = None;
622 for f in map.fields.iter().filter(|f| f.name.is_some()) {
623 let (level, lead) = match &f.group {
624 Some(g) => {
625 let first = group != Some(g.as_str());
626 if first {
627 entry(out, 2, g, "");
628 }
629 group = Some(g);
630 (3, first)
631 }
632 None => {
633 group = None;
634 if f.occurs > 1 {
635 let name = format!("{}{}", f.name.as_deref().unwrap_or_default(), if input { 'D' } else { 'G' });
636 entry(out, 2, &name, &format!("OCCURS {}", f.occurs));
637 (3, true)
638 } else {
639 (2, true)
640 }
641 }
642 };
643 if input {
644 input_items(out, map, f, level, lead);
645 } else {
646 output_items(out, map, f, level, lead, mode);
647 }
648 }
649}
650
651fn input_items(out: &mut String, map: &Map, f: &Field, level: usize, lead: bool) {
652 let n = f.name.as_deref().unwrap_or_default();
653 if lead {
654 entry(out, level, &format!("{n}L"), "COMP PIC S9(4)");
655 entry(out, level, &format!("{n}F"), "PICTURE X");
656 entry(out, level, "FILLER", &format!("REDEFINES {n}F"));
657 entry(out, level + 1, &format!("{n}A"), "PICTURE X");
658 let k = extended_attributes(map).len();
659 if k > 0 {
660 entry(out, level, "FILLER", &format!("PICTURE X({k})"));
661 }
662 }
663 let pic = f.picin.clone().unwrap_or_else(|| format!("X({})", f.length));
664 entry(out, level, &format!("{n}I"), &format!("PIC {pic}"));
665}
666
667fn output_items(out: &mut String, map: &Map, f: &Field, level: usize, lead: bool, mode: Mode) {
668 let n = f.name.as_deref().unwrap_or_default();
669 if lead {
670 if mode == Mode::InOut {
671 entry(out, level, "FILLER", "PICTURE X(3)");
672 } else {
673 entry(out, level, "FILLER", "PICTURE X(2)");
674 entry(out, level, &format!("{n}A"), "PICTURE X");
675 }
676 for (a, s, _) in EXTENDED {
677 if map.dsatts.iter().any(|d| d == a) {
678 entry(out, level, &format!("{n}{s}"), "PICTURE X");
679 }
680 }
681 }
682 let pic = f.picout.clone().unwrap_or_else(|| format!("X({})", f.length));
683 entry(out, level, &format!("{n}O"), &format!("PIC {pic}"));
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689
690 #[test]
691 fn slots_match_the_symbolic_map() {
692 let sets = parse(&realistic()).unwrap();
693 let map = &sets[0].maps[0];
694 let slots = slots(map, true);
695 let k = extended_attributes(map).len();
696 let first = slots[0];
697 assert_eq!(first.length_at(), Some(if map.tioapfx { 12 } else { 0 }));
698 assert_eq!(first.data, first.length_at().unwrap() + 3 + k);
699 for pair in slots.windows(2) {
700 let (a, b) = (pair[0], pair[1]);
701 let gap = if b.control.is_some() { 3 + k } else { 0 };
702 assert_eq!(b.data, a.data + a.size + gap);
703 }
704 }
705
706 fn cont(line: &str) -> String {
708 format!("{line:<71}X\n")
709 }
710
711 fn realistic() -> String {
712 let mut s = String::new();
713 s.push_str("* Demo mapset\n PRINT NOGEN\n");
714 s.push_str(&cont("DEMOSET DFHMSD TYPE=&SYSPARM,MODE=INOUT,LANG=COBOL,STORAGE=AUTO,"));
715 s.push_str(" CTRL=(FREEKB,FRSET),DSATTS=(COLOR,HILIGHT) SEQ00010\n");
716 s.push_str("DEMO DFHMDI SIZE=(24,80),LINE=1,COLUMN=1\n");
717 s.push_str(&cont(" DFHMDF POS=(1,30),LENGTH=15,ATTRB=(ASKIP,BRT),"));
718 s.push_str(" INITIAL='ACCOUNT ENQUIRY'\n");
719 s.push_str(&cont("NAME DFHMDF POS=(3,10),LENGTH=20,ATTRB=(UNPROT,IC,FSET),COLOR=RED,"));
720 s.push_str(" HILIGHT=UNDERLINE,INITIAL='O''NEIL'\n");
721 s.push_str(&cont("AMT DFHMDF POS=165,ATTRB=(NUM,BRT),PICIN='9(5)V99',"));
722 s.push_str(" PICOUT='ZZ,ZZ9.99',JUSTIFY=(RIGHT,ZERO)\n");
723 s.push_str("ROW DFHMDF POS=(6,2),LENGTH=8,OCCURS=3\n");
724 s.push_str("ADDR1 DFHMDF POS=(10,2),LENGTH=20,GRPNAME=ADDR,ATTRB=(UNPROT)\n");
725 s.push_str("ADDR2 DFHMDF POS=(11,2),LENGTH=20,GRPNAME=ADDR\n");
726 s.push_str(" DFHMSD TYPE=FINAL\n END\n");
727 s
728 }
729
730 #[test]
731 fn a_realistic_mapset_parses() {
732 let sets = parse(&realistic()).unwrap();
733 assert_eq!(sets.len(), 1);
734 let set = &sets[0];
735 assert_eq!((set.name.as_str(), set.mode), ("DEMOSET", Mode::InOut));
736 assert_eq!(set.ctrl, ["FREEKB", "FRSET"]);
737 let map = &set.maps[0];
738 assert_eq!((map.name.as_str(), map.lines, map.columns), ("DEMO", 24, 80));
739 assert!(map.tioapfx);
740 assert_eq!(map.dsatts, ["COLOR", "HILIGHT"]);
741 assert_eq!(map.fields.len(), 6);
742
743 let title = &map.fields[0];
744 assert_eq!((title.name.clone(), title.line, title.column, title.length), (None, 1, 30, 15));
745 assert_eq!(title.initial, Some(Initial::Text("ACCOUNT ENQUIRY".into())));
746 assert_eq!((title.attrb.protection, title.attrb.intensity), (Protection::Askip, Intensity::Brt));
747
748 let name = &map.fields[1];
749 assert_eq!(name.name.as_deref(), Some("NAME"));
750 assert_eq!((name.line, name.column), (3, 10));
751 assert!(name.attrb.cursor && name.attrb.fset && name.attrb.protection == Protection::Unprot);
752 assert_eq!((name.color.as_deref(), name.hilight.as_deref()), (Some("RED"), Some("UNDERLINE")));
753 assert_eq!(name.initial, Some(Initial::Text("O'NEIL".into())));
754 assert!(!name.justify_right && !name.fill_zero);
755
756 let amt = &map.fields[2];
757 assert_eq!((amt.line, amt.column), (3, 6));
758 assert_eq!((amt.picin.as_deref(), amt.picout.as_deref()), (Some("9(5)V99"), Some("ZZ,ZZ9.99")));
759 assert_eq!(amt.length, 9);
760 assert!(amt.attrb.numeric && amt.justify_right && amt.fill_zero);
761
762 assert_eq!(map.fields[3].occurs, 3);
763 assert_eq!(map.fields[4].group.as_deref(), Some("ADDR"));
764 assert_eq!(map.fields[5].group.as_deref(), Some("ADDR"));
765 }
766
767 #[test]
768 fn zero_length_is_for_an_unlabelled_field_only() {
769 let src = |label: &str| format!("M DFHMSD TYPE=MAP\nM1 DFHMDI SIZE=(24,80)\n{label:<9}DFHMDF POS=(6,33),LENGTH=0,ATTRB=ASKIP\n");
770 let map = &parse(&src("")).unwrap()[0].maps[0];
771 assert_eq!((map.fields[0].name.clone(), map.fields[0].length), (None, 0));
772 let err = parse(&src("AMT")).unwrap_err();
773 assert!(err.to_string().contains("LENGTH=0 is allowed only on an unlabelled field"), "{err}");
774 }
775
776 #[test]
777 fn a_quoted_string_continues_on_the_next_line() {
778 let head = " DFHMDF POS=1,LENGTH=20,INITIAL='";
779 let fill = "A".repeat(71 - head.len());
780 let src = format!("M DFHMSD TYPE=MAP\nM1 DFHMDI\n{}{}", cont(&format!("{head}{fill}")), " BC'\n");
781 let map = &parse(&src).unwrap()[0].maps[0];
782 assert_eq!(map.fields[0].initial, Some(Initial::Text(format!("{fill}BC"))));
783 }
784
785 #[test]
786 fn xinit_gives_bytes_and_several_mapsets_are_kept() {
787 let src = "S1 DFHMSD TYPE=MAP\nM1 DFHMDI\n DFHMDF POS=1,LENGTH=2,XINIT=C1C2\n DFHMSD TYPE=FINAL\nS2 DFHMSD TYPE=MAP,MODE=IN\n DFHMSD TYPE=FINAL\n";
788 let sets = parse(src).unwrap();
789 assert_eq!(sets.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), ["S1", "S2"]);
790 assert_eq!(sets[0].maps[0].fields[0].initial, Some(Initial::Bytes(vec![0xC1, 0xC2])));
791 assert_eq!(sets[1].mode, Mode::In);
792 assert_eq!(sets[0].mode, Mode::Out);
793 }
794
795 fn two_field_map() -> Mapset {
796 let src = "MAPS DFHMSD TYPE=MAP,MODE=INOUT,STORAGE=AUTO,DSATTS=(COLOR)\nMAP1 DFHMDI SIZE=(24,80)\nNAME DFHMDF POS=(1,1),LENGTH=10\nAMT DFHMDF POS=(2,1),PICIN='9(5)',PICOUT='ZZZZ9'\n DFHMSD TYPE=FINAL\n";
797 parse(src).unwrap().remove(0)
798 }
799
800 #[test]
801 fn the_symbolic_map_of_a_two_field_map() {
802 let expected = "\n 01 MAP1I.
803 02 FILLER PIC X(12).
804 02 NAMEL COMP PIC S9(4).
805 02 NAMEF PICTURE X.
806 02 FILLER REDEFINES NAMEF.
807 03 NAMEA PICTURE X.
808 02 FILLER PICTURE X(1).
809 02 NAMEI PIC X(10).
810 02 AMTL COMP PIC S9(4).
811 02 AMTF PICTURE X.
812 02 FILLER REDEFINES AMTF.
813 03 AMTA PICTURE X.
814 02 FILLER PICTURE X(1).
815 02 AMTI PIC 9(5).
816 01 MAP1O REDEFINES MAP1I.
817 02 FILLER PIC X(12).
818 02 FILLER PICTURE X(3).
819 02 NAMEC PICTURE X.
820 02 NAMEO PIC X(10).
821 02 FILLER PICTURE X(3).
822 02 AMTC PICTURE X.
823 02 AMTO PIC ZZZZ9.
824";
825 assert_eq!(symbolic_map(&two_field_map()).lines().collect::<Vec<_>>(), expected[1..].lines().collect::<Vec<_>>());
826 }
827
828 #[test]
829 fn output_only_occurs_and_groups() {
830 let src = "MAPS DFHMSD TYPE=MAP,TIOAPFX=NO\nM1 DFHMDI\nROW DFHMDF POS=1,LENGTH=4,OCCURS=2\nA1 DFHMDF POS=100,LENGTH=3,GRPNAME=G\nA2 DFHMDF POS=120,LENGTH=5,GRPNAME=G\n DFHMSD TYPE=FINAL\n";
831 let mapset = parse(src).unwrap().remove(0);
832 let expected = "\n 01 M1O.
833 02 ROWG OCCURS 2.
834 03 FILLER PICTURE X(2).
835 03 ROWA PICTURE X.
836 03 ROWO PIC X(4).
837 02 G.
838 03 FILLER PICTURE X(2).
839 03 A1A PICTURE X.
840 03 A1O PIC X(3).
841 03 A2O PIC X(5).
842";
843 assert_eq!(symbolic_map(&mapset), &expected[1..]);
844 let input = symbolic_map(&Mapset { mode: Mode::In, ..mapset });
845 assert!(input.contains(" 02 ROWD OCCURS 2.\n 03 ROWL COMP PIC S9(4).\n"), "{input}");
846 assert!(input.contains(" 02 G.\n 03 A1L COMP PIC S9(4).") && input.contains(" 03 A2I PIC X(5).\n"), "{input}");
847 assert!(!input.contains("A2L") && input.lines().all(|l| l.len() <= 72));
848 }
849
850 fn library(name: &str, files: &[(&str, String)]) -> Libraries {
851 let dir = std::env::temp_dir().join(format!("ironwork-bms-{}-{name}", std::process::id()));
852 std::fs::create_dir_all(&dir).unwrap();
853 for (file, text) in files {
854 std::fs::write(dir.join(file), text).unwrap();
855 }
856 Libraries::new(vec![dir])
857 }
858
859 fn offsets(program: &crate::ast::Program, record: &str) -> Vec<(String, u32)> {
861 let mut at = 0;
862 let mut inside = false;
863 let mut parent = 0;
864 let mut out: Vec<(String, u32)> = Vec::new();
865 for e in &program.working_storage {
866 match e.level {
867 1 => inside = e.name.as_deref() == Some(record),
868 2 if inside => {
869 let size = if e.usage.is_some() { 2 } else { e.picture.as_deref().map_or(0, picture_size) };
870 let name = e.name.clone().unwrap_or_default();
871 match &e.redefines {
872 Some(target) => {
873 parent = out.iter().find(|(n, _)| n == target).unwrap().1;
874 out.push((name, parent));
875 }
876 None => {
877 parent = at;
878 out.push((name, at));
879 at += size;
880 }
881 }
882 }
883 3 if inside => out.push((e.name.clone().unwrap_or_default(), parent)),
884 _ => {}
885 }
886 }
887 out
888 }
889
890 #[test]
891 fn a_program_copies_a_mapset_and_gets_its_layout() {
892 let bms = "DEMOMAP DFHMSD TYPE=&SYSPARM,MODE=INOUT,STORAGE=AUTO\nM1 DFHMDI SIZE=(24,80)\nNAME DFHMDF POS=(1,1),LENGTH=10\n DFHMSD TYPE=FINAL\n".to_owned();
893 let libs = library("layout", &[("DEMOMAP.bms", bms)]);
894 let program = " IDENTIFICATION DIVISION.\n PROGRAM-ID. P.\n DATA DIVISION.\n WORKING-STORAGE SECTION.\n COPY DEMOMAP.\n PROCEDURE DIVISION.\n GOBACK.\n";
895 let p = crate::parse_with(program, &libs).unwrap();
896 let at = |n: &str| offsets(&p, "M1I").into_iter().find(|(name, _)| name == n).unwrap().1;
897 assert_eq!((at("NAMEL"), at("NAMEF"), at("NAMEA"), at("NAMEI")), (12, 14, 14, 15));
898 assert!(p.working_storage.iter().any(|e| e.name.as_deref() == Some("M1O") && e.redefines.as_deref() == Some("M1I")));
899 assert_eq!(find_mapset(&libs, "DEMOMAP").unwrap().unwrap().maps[0].fields[0].name.as_deref(), Some("NAME"));
900 assert!(find_mapset(&libs, "NOPE").is_none());
901 }
902
903 #[test]
904 fn copy_of_a_broken_mapset_names_the_file() {
905 let libs = library("broken", &[("BAD.bms", "BAD DFHMSD TYPE=MAP\n FROBNICATE X=1\n".to_owned())]);
906 let err = crate::parse_with(" COPY BAD.\n", &libs).unwrap_err();
907 assert!(err.message.contains("unknown macro FROBNICATE") && err.message.contains("BAD.bms"), "{err}");
908 }
909
910 #[test]
911 fn dfhaid_has_values() {
912 let src = " IDENTIFICATION DIVISION.\n PROGRAM-ID. P.\n DATA DIVISION.\n WORKING-STORAGE SECTION.\n COPY DFHAID.\n COPY DFHBMSCA.\n PROCEDURE DIVISION.\n GOBACK.\n";
913 let p = crate::parse_with(src, &Libraries::default()).unwrap();
914 let entry = |n: &str| p.working_storage.iter().find(|e| e.name.as_deref() == Some(n)).unwrap();
915 assert_eq!(entry("DFHENTER").value, Some(crate::ast::Literal::Hex(vec![0x7D])));
916 assert_eq!(entry("DFHPF24").value, Some(crate::ast::Literal::Hex(vec![0x4C])));
917 assert_eq!(entry("DFHBMASK").value, Some(crate::ast::Literal::Hex(vec![0xF0])));
918 let erase = entry("DFHERASE");
919 assert_eq!((erase.level, erase.condition_values.len()), (88, 2));
920 }
921
922 #[test]
923 fn errors_point_at_the_line() {
924 let e = |src: &str| parse(src).unwrap_err();
925 assert!(e("S DFHMSD TYPE=MAP\n WIBBLE\n").message.contains("unknown macro WIBBLE"));
926 let outside = e("* c\nF DFHMDF POS=1,LENGTH=1\n");
927 assert_eq!(outside.pos.line, 2);
928 assert!(outside.message.contains("outside"));
929 let bad = e("S DFHMSD TYPE=MAP\nM DFHMDI SIZE=(2,10)\nF DFHMDF POS=(3,1),LENGTH=1\n");
930 assert_eq!(bad.pos.line, 3);
931 assert!(bad.message.contains("POS"));
932 assert!(e("S DFHMSD TYPE=MAP\nM DFHMDI\nF DFHMDF POS=ABC,LENGTH=1\n").message.contains("POS"));
933 assert!(e("S DFHMSD TYPE=MAP\nTOOLONGNAME DFHMDI\n").message.contains("longer than 7"));
934 assert!(e("S DFHMSD TYPE=MAP\nM DFHMDI\nF DFHMDF POS=1,LENGTH=1,OCCURS=2,GRPNAME=G\n").message.contains("GRPNAME"));
935 }
936}