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