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