Skip to main content

saturnus_objects/
transfer.rs

1//! What rides on the calculators' Kermit transfers (wiki:
2//! protocols/hp-object-format), for hosts that talk to the ROM's server:
3//! the binary transfer file (`HPHP48-x` / `HPHP49-x` header and packed
4//! nibbles) decoded to an [`Object`] and encoded from one, the text of
5//! programs, algebraics, units and commands filled in from the ASCII
6//! transfer of the same object, and RPL source text for objects sent as a
7//! host command. No I/O and no protocol: the Kermit exchange is the
8//! host's.
9
10use crate::error::Error;
11
12type Result<T, E = Error> = std::result::Result<T, E>;
13
14/// The error `message` for an object or value this cannot take.
15fn invalid(message: impl Into<String>) -> Error {
16    Error::Invalid(message.into())
17}
18use saturnus_core::Model;
19
20use crate::charset;
21use crate::object::{ArrayItem, Graphic, Memory, Object, decode};
22use crate::prolog::{ObjectType, SEMI};
23
24/// Length of the binary transfer header (`HPHP48-x`).
25pub const HEADER_LEN: usize = 8;
26
27/// The calculator family a binary transfer file names.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum Family {
30    /// HP 48SX and 48GX: `HPHP48-x`.
31    Hp48,
32    /// HP 49G: `HPHP49-x`; exact integers exist only here.
33    Hp49,
34}
35
36impl Family {
37    /// The family of `model`'s transfer files (the 49G's, else the 48's).
38    pub fn of(model: Model) -> Family {
39        if model == Model::Hp49g {
40            Family::Hp49
41        } else {
42            Family::Hp48
43        }
44    }
45}
46
47/// The header of a binary file sent to `family`. Its ROM letter (`X`) is
48/// not checked by the calculators.
49pub fn binary_header(family: Family) -> [u8; HEADER_LEN] {
50    let digit = match family {
51        Family::Hp48 => b'8',
52        Family::Hp49 => b'9',
53    };
54    [b'H', b'P', b'H', b'P', b'4', digit, b'-', b'X']
55}
56
57/// Whether `data` starts with a binary transfer header (`HPHP48-` or
58/// `HPHP49-` and a printable ROM letter).
59pub fn is_binary_file(data: &[u8]) -> bool {
60    data.get(..HEADER_LEN).is_some_and(|h| {
61        (h.starts_with(b"HPHP48-") || h.starts_with(b"HPHP49-")) && h[7].is_ascii_graphic()
62    })
63}
64
65/// Bytes as nibbles, low nibble first (the transfer file's packing).
66pub fn unpack(bytes: &[u8]) -> Vec<u8> {
67    bytes.iter().flat_map(|b| [b & 0x0F, b >> 4]).collect()
68}
69
70/// Nibbles packed two per byte, low nibble first; an odd count ends with
71/// a 0 nibble.
72pub fn pack(nibbles: &[u8]) -> Vec<u8> {
73    nibbles
74        .chunks(2)
75        .map(|c| (c[0] & 0x0F) | (c.get(1).copied().unwrap_or(0) & 0x0F) << 4)
76        .collect()
77}
78
79/// The text of an ASCII transfer after its `%%HP: T(t)A(a)F(f);` header
80/// line, or all of it when it has none.
81pub fn ascii_body(text: &str) -> &str {
82    let Some(rest) = text.strip_prefix("%%HP:") else {
83        return text;
84    };
85    let Some(end) = rest.find(';') else {
86        return text;
87    };
88    let fields = rest[..end]
89        .chars()
90        .all(|c| c.is_ascii_alphanumeric() || matches!(c, '(' | ')' | ' ' | '\t' | '.' | ','));
91    if !fields {
92        return text;
93    }
94    let rest = &rest[end + 1..];
95    rest.strip_prefix("\r\n")
96        .or_else(|| rest.strip_prefix('\n'))
97        .unwrap_or(rest)
98}
99
100/// Whether `name` is a plain global name that, quoted as `'name'`, cannot
101/// close its quotes: 1 to 127 characters, not starting with a digit or a
102/// point, without whitespace, control characters, RPL delimiters,
103/// operators or a backslash (a host command reads `A\GS` as `AΣ`, a
104/// transfer as the bytes), all in the HP character set.
105pub fn is_plain_name(name: &str) -> bool {
106    let Some(first) = name.chars().next() else {
107        return false;
108    };
109    name.chars().count() <= 127
110        && !first.is_ascii_digit()
111        && first != '.'
112        && !name.chars().any(|c| {
113            c.is_whitespace()
114                || c.is_control()
115                || matches!(
116                    c,
117                    '\'' | '"'
118                        | '«'
119                        | '»'
120                        | '{'
121                        | '}'
122                        | '['
123                        | ']'
124                        | '('
125                        | ')'
126                        | '#'
127                        | ':'
128                        | ','
129                        | ';'
130                        | '+'
131                        | '-'
132                        | '*'
133                        | '/'
134                        | '^'
135                        | '='
136                        | '<'
137                        | '>'
138                        | '\\'
139                )
140        })
141        && charset::encode(name).is_ok()
142}
143
144/// Decode a binary transfer file (`HPHP48-x` / `HPHP49-x` header).
145pub fn decode_file(data: &[u8], mem: &dyn Memory) -> Result<Object> {
146    if !is_binary_file(data) {
147        return Err(invalid(
148            "not an HP binary object (no HPHP48-x / HPHP49-x header)",
149        ));
150    }
151    let nibbles = unpack(&data[HEADER_LEN..]);
152    Ok(decode(&nibbles, mem)?)
153}
154
155/// Fill the text of programs, algebraics, units, commands and unknown
156/// objects in `obj` from `text`, the ASCII transfer of the same object (the
157/// `%%HP:` header is skipped). Walks the text in step with the decoded
158/// tree; if they disagree, nested texts stay unset and a top-level
159/// program, algebraic or unknown object gets the whole text.
160pub fn fill_sources(obj: &mut Object, text: &str) {
161    let body = ascii_body(text);
162    let body = body.replace("\r\n", "\n");
163    let mut filled = obj.clone();
164    let mut scan = Scanner { s: &body, pos: 0 };
165    let ok = scan.fill(&mut filled).is_some() && scan.rest().trim().is_empty();
166    if ok {
167        *obj = filled;
168        return;
169    }
170    let whole = Some(body.trim().to_string());
171    match obj {
172        Object::Program { source }
173        | Object::Algebraic { source }
174        | Object::Command { source, .. }
175        | Object::Unknown { source, .. } => *source = whole,
176        _ => {}
177    }
178}
179
180/// A cursor over RPL text.
181struct Scanner<'a> {
182    s: &'a str,
183    pos: usize,
184}
185
186impl<'a> Scanner<'a> {
187    fn rest(&self) -> &'a str {
188        &self.s[self.pos..]
189    }
190
191    fn skip_ws(&mut self) {
192        let r = self.rest();
193        self.pos += r.len() - r.trim_start().len();
194    }
195
196    fn peek(&self) -> Option<char> {
197        self.rest().chars().next()
198    }
199
200    fn expect(&mut self, lit: &str) -> Option<()> {
201        self.skip_ws();
202        if self.rest().starts_with(lit) {
203            self.pos += lit.len();
204            Some(())
205        } else {
206            None
207        }
208    }
209
210    /// Up to the next whitespace.
211    fn token(&mut self) -> Option<&'a str> {
212        self.skip_ws();
213        let r = self.rest();
214        let end = r.find(char::is_whitespace).unwrap_or(r.len());
215        if end == 0 {
216            return None;
217        }
218        self.pos += end;
219        Some(&r[..end])
220    }
221
222    /// From `open` to its matching `close`, skipping string literals.
223    fn balanced(&mut self, open: char, close: char) -> Option<&'a str> {
224        self.skip_ws();
225        let r = self.rest();
226        if !r.starts_with(open) {
227            return None;
228        }
229        let mut depth = 0usize;
230        let mut in_string = false;
231        let mut escaped = false;
232        for (i, c) in r.char_indices() {
233            if in_string {
234                match c {
235                    _ if escaped => escaped = false,
236                    '\\' => escaped = true,
237                    '"' => in_string = false,
238                    _ => {}
239                }
240                continue;
241            }
242            if c == '"' {
243                in_string = true;
244            } else if c == open && (open != close || depth == 0) {
245                depth += 1;
246            } else if c == close {
247                depth -= 1;
248                if depth == 0 {
249                    let end = i + c.len_utf8();
250                    self.pos += end;
251                    return Some(&r[..end]);
252                }
253            }
254        }
255        None
256    }
257
258    /// A string literal (49G escapes `\"` and `\\`).
259    fn string(&mut self) -> Option<&'a str> {
260        self.skip_ws();
261        let r = self.rest();
262        if !r.starts_with('"') {
263            return None;
264        }
265        let mut escaped = false;
266        for (i, c) in r.char_indices().skip(1) {
267            match c {
268                _ if escaped => escaped = false,
269                '\\' => escaped = true,
270                '"' => {
271                    self.pos += i + 1;
272                    return Some(&r[..=i]);
273                }
274                _ => {}
275            }
276        }
277        None
278    }
279
280    /// Text quoted with `'`.
281    fn quoted(&mut self) -> Option<&'a str> {
282        self.skip_ws();
283        let r = self.rest();
284        let inner = r.strip_prefix('\'')?;
285        let end = inner.find('\'')? + 2;
286        self.pos += end;
287        Some(&r[..end])
288    }
289
290    /// Any one object's text: a delimited group or a token.
291    fn any(&mut self) -> Option<&'a str> {
292        self.skip_ws();
293        match self.peek()? {
294            '{' => self.balanced('{', '}'),
295            '[' => self.balanced('[', ']'),
296            '«' => self.balanced('«', '»'),
297            '(' => self.balanced('(', ')'),
298            '"' => self.string(),
299            '\'' => self.quoted(),
300            _ => self.token(),
301        }
302    }
303
304    /// Consume the text of `obj`, filling its source fields.
305    fn fill(&mut self, obj: &mut Object) -> Option<()> {
306        self.skip_ws();
307        match obj {
308            Object::Real { .. } | Object::Integer { .. } => {
309                self.token()?;
310            }
311            Object::Name { .. } | Object::LocalName { .. } => {
312                if self.peek() == Some('\'') {
313                    self.quoted()?;
314                } else {
315                    self.token()?;
316                }
317            }
318            Object::Character { .. } => {
319                self.any()?;
320            }
321            Object::Binary { .. } => {
322                if self.token()? == "#" {
323                    self.token()?;
324                }
325            }
326            Object::String { .. } => {
327                self.string()?;
328            }
329            Object::Complex { .. } => {
330                self.balanced('(', ')')?;
331            }
332            Object::Array { .. } => {
333                self.balanced('[', ']')?;
334            }
335            Object::List { items } => {
336                self.expect("{")?;
337                for item in items.iter_mut() {
338                    self.fill(item)?;
339                }
340                self.expect("}")?;
341            }
342            Object::Tagged { tag, object } => {
343                self.expect(&format!(":{tag}:"))?;
344                self.fill(object)?;
345            }
346            Object::Program { source } => {
347                *source = Some(self.balanced('«', '»')?.to_string());
348            }
349            Object::Algebraic { source } => {
350                *source = Some(self.quoted()?.to_string());
351            }
352            Object::Unit { unit, .. } => {
353                let t = if self.peek() == Some('\'') {
354                    let q = self.quoted()?;
355                    &q[1..q.len() - 1]
356                } else {
357                    self.token()?
358                };
359                *unit = Some(t.split_once('_')?.1.to_string());
360            }
361            // An XLIB name the tables do not name reads `XLIB l n`.
362            Object::Command {
363                name: None,
364                address: None,
365                library: Some(_),
366                source,
367                ..
368            } if self.rest().trim_start().starts_with("XLIB ") => {
369                let start = self.pos;
370                for _ in 0..3 {
371                    self.token()?;
372                }
373                *source = Some(self.s[start..self.pos].trim().to_string());
374            }
375            Object::Command { source, .. } | Object::Unknown { source, .. } => {
376                *source = Some(self.any()?.to_string());
377            }
378        }
379        Some(())
380    }
381}
382
383/// Nibbles of a little-endian `width`-nibble field.
384fn put_field(out: &mut Vec<u8>, value: u64, width: usize) {
385    out.extend((0..width).map(|i| ((value >> (4 * i)) & 0xF) as u8));
386}
387
388fn put_chars(out: &mut Vec<u8>, text: &str) -> Result<usize> {
389    let bytes = charset::encode(text)
390        .map_err(|c| invalid(format!("{c:?} is not in the HP character set")))?;
391    for b in &bytes {
392        put_field(out, u64::from(*b), 2);
393    }
394    Ok(bytes.len())
395}
396
397/// The binary transfer file for `obj` (header and packed nibbles, exactly
398/// as long as the object), or `None` for objects that only travel as text
399/// (programs, algebraics, units, arrays, commands other than XLIB names).
400pub fn encode_file(obj: &Object, family: Family) -> Result<Option<Vec<u8>>> {
401    let mut nibbles = Vec::new();
402    if !encode_into(obj, family, &mut nibbles)? {
403        return Ok(None);
404    }
405    let mut data = binary_header(family).to_vec();
406    data.extend(pack(&nibbles));
407    Ok(Some(data))
408}
409
410/// The binary transfer file of the GROB that pictures `g`
411/// ([`Graphic::to_grob`]).
412pub fn graphic_file(g: &Graphic, family: Family) -> Result<Vec<u8>> {
413    let mut data = binary_header(family).to_vec();
414    data.extend(pack(&g.to_grob()?));
415    Ok(data)
416}
417
418fn prolog(out: &mut Vec<u8>, ty: ObjectType) {
419    put_field(out, u64::from(ty.prolog()), 5);
420}
421
422fn encode_into(obj: &Object, family: Family, out: &mut Vec<u8>) -> Result<bool> {
423    match obj {
424        Object::Real { value } => {
425            prolog(out, ObjectType::Real);
426            out.extend(value.to_body());
427        }
428        Object::Complex { re, im } => {
429            prolog(out, ObjectType::Complex);
430            out.extend(re.to_body());
431            out.extend(im.to_body());
432        }
433        Object::String { value } => {
434            prolog(out, ObjectType::String);
435            let mut body = Vec::new();
436            let n = put_chars(&mut body, value)?;
437            put_field(out, 5 + 2 * n as u64, 5);
438            out.extend(body);
439        }
440        Object::Name { value } | Object::LocalName { value } => {
441            let ty = if matches!(obj, Object::Name { .. }) {
442                ObjectType::GlobalName
443            } else {
444                ObjectType::LocalName
445            };
446            prolog(out, ty);
447            let mut body = Vec::new();
448            let n = put_chars(&mut body, value)?;
449            if n == 0 || n > 255 {
450                return Err(invalid(format!(
451                    "a name has 1 to 255 characters: {value:?}"
452                )));
453            }
454            put_field(out, n as u64, 2);
455            out.extend(body);
456        }
457        Object::Character { value } => {
458            prolog(out, ObjectType::Character);
459            if put_chars(out, value)? != 1 {
460                return Err(invalid(format!(
461                    "a character object holds one character: {value:?}"
462                )));
463            }
464        }
465        Object::Binary { value, .. } => {
466            prolog(out, ObjectType::BinaryInteger);
467            put_field(out, 21, 5);
468            put_field(out, *value, 16);
469        }
470        Object::Integer { value } => {
471            if family != Family::Hp49 {
472                return Err(invalid(
473                    "exact integers exist on the 49G only; send a real instead",
474                ));
475            }
476            prolog(out, ObjectType::Integer);
477            let n = value.to_nibbles();
478            put_field(out, 5 + n.len() as u64, 5);
479            out.extend(n);
480        }
481        Object::List { items } => {
482            prolog(out, ObjectType::List);
483            for item in items {
484                if !encode_into(item, family, out)? {
485                    return Ok(false);
486                }
487            }
488            put_field(out, u64::from(SEMI), 5);
489        }
490        Object::Tagged { tag, object } => {
491            prolog(out, ObjectType::Tagged);
492            let mut body = Vec::new();
493            let n = put_chars(&mut body, tag)?;
494            if n > 255 {
495                return Err(invalid("tag longer than 255 characters"));
496            }
497            put_field(out, n as u64, 2);
498            out.extend(body);
499            return encode_into(object, family, out);
500        }
501        Object::Unknown { hex, truncated, .. } => {
502            if *truncated {
503                return Ok(false);
504            }
505            for c in hex.chars() {
506                let d = c
507                    .to_digit(16)
508                    .ok_or_else(|| invalid(format!("hex holds {c:?}, not a hex digit")))?;
509                out.push(d as u8);
510            }
511        }
512        // An XLIB name: prolog, library and command (3 nibbles each).
513        Object::Command {
514            address: None,
515            library: Some(lib),
516            command: Some(cmd),
517            ..
518        } => {
519            prolog(out, ObjectType::XlibName);
520            put_field(out, u64::from(*lib), 3);
521            put_field(out, u64::from(*cmd), 3);
522        }
523        Object::Unit { .. }
524        | Object::Array { .. }
525        | Object::Program { .. }
526        | Object::Algebraic { .. }
527        | Object::Command { .. } => return Ok(false),
528    }
529    Ok(true)
530}
531
532/// RPL source that compiles to `obj`, or `None` when it has none (a string
533/// holding `"`, a local name, a character, an unknown object).
534pub fn to_source(obj: &Object, family: Family) -> Result<Option<String>> {
535    Ok(match obj {
536        Object::Real { value } => Some(value.to_source()),
537        Object::Integer { value } => {
538            if family != Family::Hp49 {
539                return Err(invalid(
540                    "exact integers exist on the 49G only; send a real instead",
541                ));
542            }
543            Some(value.to_source())
544        }
545        Object::Complex { re, im } => Some(format!("({},{})", re.to_source(), im.to_source())),
546        Object::String { value } => (!value.contains(['"', '\\'])).then(|| format!("\"{value}\"")),
547        // A name or tag that is not a plain token could close its quotes
548        // and run commands: those travel in binary only.
549        Object::Name { value } => is_plain_name(value).then(|| format!("'{value}'")),
550        Object::Binary { value, .. } => Some(format!("#{value:X}h")),
551        Object::List { items } => {
552            let mut parts = Vec::new();
553            for item in items {
554                match to_source(item, family)? {
555                    Some(s) => parts.push(s),
556                    None => return Ok(None),
557                }
558            }
559            Some(format!("{{ {} }}", parts.join(" ")))
560        }
561        Object::Tagged { tag, object } => {
562            if !plain_token(tag, false) {
563                return Ok(None);
564            }
565            to_source(object, family)?.map(|s| format!(":{tag}:{s}"))
566        }
567        Object::Unit { value, unit } => {
568            let unit = unit
569                .as_deref()
570                .ok_or_else(|| invalid("a unit object needs its unit"))?;
571            if !plain_token(unit, true) || unit.contains('_') {
572                return Err(invalid(format!(
573                    "not a unit expression (spaces and RPL delimiters are not allowed): {unit:?}"
574                )));
575            }
576            Some(format!("{}_{unit}", value.to_source()))
577        }
578        Object::Array { items, .. } => array_source(items, family)?,
579        Object::Program { source } | Object::Algebraic { source } => {
580            let source = source
581                .as_deref()
582                .ok_or_else(|| invalid("this object needs its source"))?;
583            Some(one_object_source(obj, source)?)
584        }
585        // An XLIB name the ROM's tables do not name: its text (`XLIB l n`)
586        // does not compile back; it travels in binary.
587        Object::Command {
588            name: None,
589            address: None,
590            library: Some(_),
591            command: Some(_),
592            ..
593        } => None,
594        Object::Command { name, source, .. } => {
595            let source = source
596                .as_deref()
597                .or(name.as_deref())
598                .ok_or_else(|| invalid("this object needs its source"))?;
599            Some(one_object_source(obj, source)?)
600        }
601        Object::LocalName { .. } | Object::Character { .. } | Object::Unknown { .. } => None,
602    })
603}
604
605/// Whether `s` is one RPL token that cannot close a quote or a group:
606/// no whitespace, control characters, quotes or delimiters (parentheses
607/// only with `parens`, for unit expressions).
608fn plain_token(s: &str, parens: bool) -> bool {
609    !s.is_empty()
610        && s.chars().all(|c| {
611            !c.is_whitespace()
612                && !c.is_control()
613                && !matches!(
614                    c,
615                    '\'' | '"' | '«' | '»' | '{' | '}' | '[' | ']' | ':' | '#' | ',' | ';' | '\\'
616                )
617                && (parens || !matches!(c, '(' | ')'))
618        })
619}
620
621/// `source` checked to be exactly one program (one balanced `« »`), one
622/// algebraic (one `' '` group) or one command (a plain token), with
623/// nothing before or after it: a host command must push it, not run
624/// anything.
625fn one_object_source(obj: &Object, source: &str) -> Result<String> {
626    // The calculator reads the trigraphs as the delimiters themselves.
627    let text = source.trim().replace("\\<<", "«").replace("\\>>", "»");
628    let mut scan = Scanner { s: &text, pos: 0 };
629    let ok = match obj {
630        Object::Program { .. } => scan.balanced('«', '»').is_some(),
631        Object::Algebraic { .. } => scan.quoted().is_some_and(|q| q.len() > 2),
632        _ => plain_token(&text, true) && scan.token().is_some(),
633    };
634    if !ok || !scan.rest().trim().is_empty() {
635        let what = match obj {
636            Object::Program { .. } => "a program's source must be one « ... » group",
637            Object::Algebraic { .. } => "an algebraic's source must be one '...' group",
638            _ => "a command's source must be one token",
639        };
640        return Err(invalid(format!("{what} with nothing after it: {source:?}")));
641    }
642    Ok(source.trim().to_string())
643}
644
645fn array_source(items: &[ArrayItem], family: Family) -> Result<Option<String>> {
646    let mut parts = Vec::new();
647    for item in items {
648        let s = match item {
649            ArrayItem::Row(row) => array_source(row, family)?,
650            ArrayItem::Item(obj) => {
651                if !matches!(**obj, Object::Real { .. } | Object::Complex { .. }) {
652                    return Err(invalid("array elements must be reals or complex numbers"));
653                }
654                to_source(obj, family)?
655            }
656        };
657        match s {
658            Some(s) => parts.push(s),
659            None => return Ok(None),
660        }
661    }
662    Ok(Some(format!("[ {} ]", parts.join(" "))))
663}
664
665/// The stack text the ROM's Kermit server returns for a host command
666/// (`C`), already translated from the HP character set.
667#[derive(Clone, Debug, Default, PartialEq)]
668pub struct StackReply {
669    /// The text after `Error:` when the command failed.
670    pub error: Option<String>,
671    /// The levels as display text, level 1 first.
672    pub levels: Vec<String>,
673}
674
675impl StackReply {
676    /// Level `n` (1-based), if there is one.
677    pub fn level(&self, n: usize) -> Option<&str> {
678        n.checked_sub(1)
679            .and_then(|i| self.levels.get(i))
680            .map(String::as_str)
681    }
682
683    /// Whether the stack has no levels.
684    pub fn is_empty(&self) -> bool {
685        self.levels.is_empty()
686    }
687}
688
689/// The level number of a line that starts with `N:`, and the rest.
690fn level_prefix(line: &str) -> Option<(usize, &str)> {
691    let (num, rest) = line.split_once(':')?;
692    if num.is_empty() || !num.bytes().all(|b| b.is_ascii_digit()) {
693        return None;
694    }
695    let n = num.parse().ok()?;
696    (n >= 1).then_some((n, rest))
697}
698
699/// Parse the stack display of a host command: an optional first line
700/// `Error: X`, then `Empty Stack` or the levels from the highest down,
701/// each starting with `N:`; any other line continues the level before it.
702/// Never fails: text before the first level is ignored.
703pub fn parse_stack(text: &str) -> StackReply {
704    let text = text.trim_end_matches(['\r', '\n']);
705    let mut reply = StackReply::default();
706    // Highest level first; reversed at the end.
707    let mut values: Vec<String> = Vec::new();
708    // The level the next level line must carry.
709    let mut expected = 0usize;
710    for (i, line) in text.split('\n').enumerate() {
711        let line = line.strip_suffix('\r').unwrap_or(line);
712        if i == 0
713            && let Some(msg) = line.strip_prefix("Error:")
714        {
715            reply.error = Some(msg.trim().to_string());
716            continue;
717        }
718        if values.is_empty() {
719            if line.trim() == "Empty Stack" {
720                break;
721            }
722            if let Some((n, rest)) = level_prefix(line) {
723                values.push(rest.trim_start_matches(' ').to_string());
724                expected = n - 1;
725            }
726            continue;
727        }
728        if expected >= 1
729            && let Some(rest) = line.strip_prefix(format!("{expected}:").as_str())
730        {
731            values.push(rest.trim_start_matches(' ').to_string());
732            expected -= 1;
733            continue;
734        }
735        if let Some(last) = values.last_mut() {
736            last.push('\n');
737            last.push_str(line);
738        }
739    }
740    values.reverse();
741    reply.levels = values;
742    reply
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748
749    #[test]
750    fn a_picture_travels_as_a_binary_grob() {
751        let g = Graphic::from_packed(131, 64, &[0x5A; 17 * 64]).unwrap();
752        for (family, head) in [(Family::Hp48, b"HPHP48-X"), (Family::Hp49, b"HPHP49-X")] {
753            let file = graphic_file(&g, family).unwrap();
754            assert_eq!(&file[..HEADER_LEN], head);
755            assert_eq!(file.len(), HEADER_LEN + 2196 / 2);
756            assert!(is_binary_file(&file));
757            let Object::Unknown {
758                kind,
759                graphic: Some(back),
760                ..
761            } = decode_file(&file, &crate::NoMemory).unwrap()
762            else {
763                panic!("not a picture");
764            };
765            assert_eq!(kind.as_deref(), Some("Graphic"));
766            assert_eq!(back, g);
767        }
768    }
769
770    #[test]
771    fn stacks() {
772        let r = parse_stack("2:                   42\r\n1: \"a\r\nb\"\r\n");
773        assert_eq!(r.levels, ["\"a\nb\"", "42"]);
774        assert_eq!(r.error, None);
775        assert_eq!(r.level(2), Some("42"));
776        assert_eq!(r.level(0), None);
777        let r = parse_stack("Error: Too Few Arguments\r\nEmpty Stack\r\n");
778        assert_eq!(r.error.as_deref(), Some("Too Few Arguments"));
779        assert!(r.levels.is_empty());
780        // A level whose text holds `1:` is not taken for level 1 twice.
781        let r = parse_stack("1: { 1:2 }\n1: x");
782        assert_eq!(r.levels, ["{ 1:2 }\n1: x"]);
783    }
784    use crate::object::{Integer, NoMemory, Real};
785    use serde_json::json;
786
787    #[test]
788    fn headers_and_packing() {
789        assert_eq!(&binary_header(Family::Hp49), b"HPHP49-X");
790        assert!(is_binary_file(b"HPHP48-J\x01"));
791        assert!(!is_binary_file(b"HPHP47-J") && !is_binary_file(b"HPHP48-"));
792        assert_eq!(Family::of(Model::Hp49g), Family::Hp49);
793        assert_eq!(Family::of(Model::Hp48sx), Family::Hp48);
794        assert_eq!(pack(&[1, 2, 3]), [0x21, 0x03]);
795        assert_eq!(unpack(&[0x21, 0x03]), [1, 2, 3, 0]);
796        assert_eq!(ascii_body("%%HP: T(1)A(D)F(.);\r\n{ 1 }\r\n"), "{ 1 }\r\n");
797        assert_eq!(ascii_body("%%HP: T(3)A(R)F(,);\n« »"), "« »");
798        assert_eq!(ascii_body("{ 1 }"), "{ 1 }");
799    }
800
801    #[test]
802    fn plain_names() {
803        assert!(is_plain_name("X") && is_plain_name("ΣDAT") && is_plain_name("A.1"));
804        for bad in [
805            "",
806            "A\\GS",
807            "1A",
808            ".A",
809            "A B",
810            "X'",
811            "A+B",
812            "a:b",
813            "€",
814            &"A".repeat(128),
815        ] {
816            assert!(!is_plain_name(bad), "{bad:?}");
817        }
818    }
819
820    /// Nibbles from a hex string in memory order.
821    fn nib(s: &str) -> Vec<u8> {
822        s.chars().map(|c| c.to_digit(16).unwrap() as u8).collect()
823    }
824
825    fn dec(s: &str) -> Object {
826        decode(&nib(s), &NoMemory).unwrap()
827    }
828
829    fn real(s: &str) -> Real {
830        Real::parse(s).unwrap()
831    }
832
833    /// A memory holding the real 1 at #2A2C9 (48SX ROM J).
834    struct Rom;
835    impl Memory for Rom {
836        fn nibble(&self, addr: u32) -> Option<u8> {
837            let i = addr.checked_sub(0x2A2C9)? as usize;
838            nib("339200000000000000010").get(i).copied()
839        }
840    }
841
842    /// A list holding an XLIB name of a library the tables do not know:
843    /// no name, its numbers kept, its text wanted from the calculator,
844    /// and it goes back in binary unchanged.
845    #[test]
846    fn unresolved_xlib_names_round_trip() {
847        // { XLIB 1234 5 } with 1234 = #4D2, 5 = #005.
848        let file = {
849            let mut d = binary_header(Family::Hp48).to_vec();
850            d.extend(pack(&nib("47A2029E202D4500B2130")));
851            d
852        };
853        let obj = decode_file(&file, &NoMemory).unwrap();
854        let Object::List { items } = &obj else {
855            panic!("{obj:?}")
856        };
857        assert_eq!(
858            serde_json::to_value(&items[0]).unwrap(),
859            json!({"type": "command", "library": 1234, "command": 5})
860        );
861        assert!(obj.needs_source());
862        assert_eq!(to_source(&obj, Family::Hp48).unwrap(), None);
863        assert_eq!(encode_file(&obj, Family::Hp48).unwrap().unwrap(), file);
864        // With the calculator's text filled in it still travels in binary.
865        let mut filled = obj.clone();
866        fill_sources(&mut filled, "{ XLIB 1234 5 }");
867        let Object::List { items } = &filled else {
868            panic!()
869        };
870        assert!(matches!(&items[0], Object::Command { source: Some(s), .. } if s == "XLIB 1234 5"));
871        assert_eq!(encode_file(&filled, Family::Hp48).unwrap().unwrap(), file);
872    }
873
874    #[test]
875    fn sources_come_from_the_ascii_text() {
876        // Program, algebraic, unit (48SX), each at top level.
877        let mut p = dec("D9D20E16329C2A2ED2A276BA193632B21300");
878        assert!(p.needs_source());
879        fill_sources(&mut p, "%%HP: T(1)A(D)F(.);\r\n« 1 2 +\r\n»\r\n");
880        assert_eq!(
881            p,
882            Object::Program {
883                source: Some("« 1 2 +\n»".into())
884            }
885        );
886        let mut a = dec("8BA2084E201085ED2A2D20B1B21300");
887        fill_sources(&mut a, "%%HP: T(1)A(D)F(.);\r\n'X^2'\r\n");
888        assert_eq!(
889            a,
890            Object::Algebraic {
891                source: Some("'X^2'".into())
892            }
893        );
894        // The unit's leading 1 is a ROM pointer: without memory the unit
895        // cannot be decoded and stays unknown, with it the number is exact.
896        let unit = "ADA209C2A2C2A2070000D6C2A207000037ED2A227B0186B0168B01B21300";
897        assert!(matches!(dec(unit), Object::Unknown { .. }));
898        let mut u = decode(&nib(unit), &Rom).unwrap();
899        assert_eq!(
900            u,
901            Object::Unit {
902                value: real("1"),
903                unit: None
904            }
905        );
906        fill_sources(&mut u, "%%HP: T(1)A(D)F(.);\r\n'1_m/s^2'\r\n");
907        assert_eq!(
908            u,
909            Object::Unit {
910                value: real("1"),
911                unit: Some("m/s^2".into())
912            }
913        );
914    }
915
916    #[test]
917    fn nested_sources_follow_the_tree() {
918        let mut list = Object::List {
919            items: vec![
920                Object::Real { value: real("1") },
921                Object::Program { source: None },
922                Object::String {
923                    value: "a » b".into(),
924                },
925                Object::Tagged {
926                    tag: "T".into(),
927                    object: Box::new(Object::Command {
928                        name: None,
929                        address: None,
930                        library: None,
931                        command: None,
932                        source: None,
933                    }),
934                },
935                Object::Binary {
936                    value: 42,
937                    base: None,
938                    text: None,
939                },
940                Object::List {
941                    items: vec![Object::Algebraic { source: None }],
942                },
943                Object::Complex {
944                    re: real("1"),
945                    im: real("2"),
946                },
947            ],
948        };
949        fill_sources(
950            &mut list,
951            "{ 1 « 2 \"x»\" + » \"a » b\" :T: + # 42d { 'X+1' } (1,2) }",
952        );
953        let Object::List { items } = &list else {
954            panic!()
955        };
956        assert_eq!(
957            items[1],
958            Object::Program {
959                source: Some("« 2 \"x»\" + »".into())
960            }
961        );
962        assert_eq!(
963            items[3],
964            Object::Tagged {
965                tag: "T".into(),
966                object: Box::new(Object::Command {
967                    name: None,
968                    address: None,
969                    library: None,
970                    command: None,
971                    source: Some("+".into())
972                })
973            }
974        );
975        assert_eq!(
976            items[5],
977            Object::List {
978                items: vec![Object::Algebraic {
979                    source: Some("'X+1'".into())
980                }]
981            }
982        );
983        // A mismatch fills nothing nested.
984        let mut l = Object::List {
985            items: vec![Object::Program { source: None }],
986        };
987        fill_sources(&mut l, "{ 1 2 }");
988        assert_eq!(
989            l,
990            Object::List {
991                items: vec![Object::Program { source: None }]
992            }
993        );
994        // ... but a top-level program takes the whole text.
995        let mut p = Object::Program { source: None };
996        fill_sources(&mut p, "« garbled");
997        assert_eq!(
998            p,
999            Object::Program {
1000                source: Some("« garbled".into())
1001            }
1002        );
1003    }
1004
1005    #[test]
1006    fn encode_round_trips_through_decode() {
1007        let objs = [
1008            Object::Real {
1009                value: real("-4.79425538604E-1"),
1010            },
1011            Object::Complex {
1012                re: real("1.5"),
1013                im: real("-2"),
1014            },
1015            Object::String {
1016                value: "Hi «x» \"q\"".into(),
1017            },
1018            Object::Name {
1019                value: "ABC".into(),
1020            },
1021            Object::LocalName { value: "x".into() },
1022            Object::Character { value: "A".into() },
1023            Object::Binary {
1024                value: u64::MAX,
1025                base: None,
1026                text: None,
1027            },
1028            Object::List {
1029                items: vec![
1030                    Object::Real { value: real("1") },
1031                    Object::List { items: vec![] },
1032                    Object::Tagged {
1033                        tag: "T".into(),
1034                        object: Box::new(Object::Real { value: real("5") }),
1035                    },
1036                ],
1037            },
1038        ];
1039        for o in &objs {
1040            let file = encode_file(o, Family::Hp48).unwrap().unwrap();
1041            assert_eq!(&file[..8], b"HPHP48-X");
1042            assert_eq!(decode_file(&file, &NoMemory).unwrap(), *o, "{o:?}");
1043            // Exactly the object's bytes: the 48SX rejects a trailing byte.
1044            let size = crate::prolog::object_size(&unpack(&file[8..]), 0).unwrap();
1045            assert_eq!(file.len(), 8 + size.div_ceil(2));
1046        }
1047        // The known 48SX encoding of 8.72653549837E-3.
1048        let file = encode_file(
1049            &Object::Real {
1050                value: real("8.72653549837E-3"),
1051            },
1052            Family::Hp48,
1053        )
1054        .unwrap()
1055        .unwrap();
1056        assert_eq!(unpack(&file[8..])[..21], nib("339207997389453562780")[..]);
1057        let i = Object::Integer {
1058            value: Integer::parse("-1234567890123456789").unwrap(),
1059        };
1060        let file = encode_file(&i, Family::Hp49).unwrap().unwrap();
1061        assert_eq!(
1062            unpack(&file[8..])[..30],
1063            nib("416209100098765432109876543219")[..]
1064        );
1065        assert!(encode_file(&i, Family::Hp48).is_err());
1066        assert_eq!(
1067            encode_file(
1068                &Object::Program {
1069                    source: Some("« »".into())
1070                },
1071                Family::Hp48
1072            )
1073            .unwrap(),
1074            None
1075        );
1076        // An unknown object travels back as its hex.
1077        let u = dec("E1B20A0000FFFFF");
1078        assert!(matches!(u, Object::Unknown { .. }));
1079        assert_eq!(
1080            encode_file(&u, Family::Hp48).unwrap().unwrap()[8..],
1081            pack(&nib("E1B20A0000FFFFF"))[..]
1082        );
1083    }
1084
1085    #[test]
1086    fn sources_for_text_transfers() {
1087        let src = |o: Object| to_source(&o, Family::Hp48).unwrap();
1088        assert_eq!(src(Object::Real { value: real("0.5") }).unwrap(), "5.E-1");
1089        assert_eq!(
1090            src(Object::Complex {
1091                re: real("1"),
1092                im: real("-2")
1093            })
1094            .unwrap(),
1095            "(1.,-2.)"
1096        );
1097        assert_eq!(
1098            src(Object::String {
1099                value: "a b".into()
1100            })
1101            .unwrap(),
1102            "\"a b\""
1103        );
1104        assert_eq!(
1105            src(Object::String {
1106                value: "a\"b".into()
1107            }),
1108            None
1109        );
1110        assert_eq!(
1111            src(Object::Binary {
1112                value: 255,
1113                base: None,
1114                text: None
1115            })
1116            .unwrap(),
1117            "#FFh"
1118        );
1119        assert_eq!(
1120            src(Object::List {
1121                items: vec![
1122                    Object::Name { value: "X".into() },
1123                    Object::Unit {
1124                        value: real("9.81"),
1125                        unit: Some("m/s^2".into())
1126                    }
1127                ]
1128            })
1129            .unwrap(),
1130            "{ 'X' 9.81_m/s^2 }"
1131        );
1132        assert_eq!(src(Object::LocalName { value: "x".into() }), None);
1133        assert!(to_source(&Object::Program { source: None }, Family::Hp48).is_err());
1134    }
1135
1136    #[test]
1137    fn text_cannot_break_out_of_its_quotes() {
1138        let src = |o: Object| to_source(&o, Family::Hp48);
1139        // Names and tags that are not plain tokens go binary instead.
1140        assert_eq!(
1141            src(Object::Name {
1142                value: "X' 11. 'QA' STO 'Y".into()
1143            })
1144            .unwrap(),
1145            None
1146        );
1147        let odd = Object::Tagged {
1148            tag: "a:b".into(),
1149            object: Box::new(Object::Real { value: Real::ZERO }),
1150        };
1151        assert_eq!(src(odd.clone()).unwrap(), None);
1152        assert!(encode_file(&odd, Family::Hp48).unwrap().is_some());
1153        let spaced = Object::Tagged {
1154            tag: "my tag".into(),
1155            object: Box::new(Object::Real { value: Real::ZERO }),
1156        };
1157        assert_eq!(src(spaced).unwrap(), None);
1158        // Programs, algebraics, units and commands are refused.
1159        let p = |s: &str| Object::Program {
1160            source: Some(s.into()),
1161        };
1162        assert!(src(p("« 1 » 22. 'QB' STO « 2 »")).is_err());
1163        assert!(src(p("1 2 +")).is_err());
1164        assert!(src(p("« 1 \"»\" »")).is_ok());
1165        assert!(src(p("\\<< 1 \\>>")).is_ok());
1166        assert!(src(p("« « 1 » »")).is_ok());
1167        let a = |s: &str| Object::Algebraic {
1168            source: Some(s.into()),
1169        };
1170        assert!(src(a("'X' 'Y' 44. 'QD' STO 'Z'")).is_err());
1171        assert!(src(a("X+1")).is_err());
1172        assert!(src(a("''")).is_err());
1173        assert_eq!(src(a("'X^2+1'")).unwrap().unwrap(), "'X^2+1'");
1174        let u = |s: &str| Object::Unit {
1175            value: Real::ZERO,
1176            unit: Some(s.into()),
1177        };
1178        assert!(src(u("m 33. 'QC' STO")).is_err());
1179        assert!(src(u("m'")).is_err());
1180        assert!(src(u("(m/s)^2")).is_ok());
1181        let c = |s: &str| Object::Command {
1182            name: None,
1183            address: None,
1184            library: None,
1185            command: None,
1186            source: Some(s.into()),
1187        };
1188        assert!(src(c("+ 1. 'QE' STO")).is_err());
1189        assert_eq!(src(c("\u{2192}LIST")).unwrap().unwrap(), "\u{2192}LIST");
1190    }
1191
1192    #[test]
1193    fn json_shapes_parse() {
1194        let o: Object = serde_json::from_value(json!({"type": "list", "items": [
1195            {"type": "real", "value": 0.5},
1196            {"type": "string", "value": "s"},
1197            {"type": "program", "source": "« 1 »"},
1198            {"type": "binary", "value": 42},
1199            {"type": "array", "dims": [2], "items": [{"type": "real", "value": 1}, {"type": "real", "value": 2}]}
1200        ]}))
1201        .unwrap();
1202        let Object::List { items } = o else { panic!() };
1203        assert_eq!(items.len(), 5);
1204        assert!(serde_json::from_value::<Object>(json!({"type": "real", "value": "x"})).is_err());
1205        assert!(serde_json::from_value::<Object>(json!({"type": "nope"})).is_err());
1206    }
1207}