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