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    /// Whether the stack has no levels.
663    pub fn is_empty(&self) -> bool {
664        self.levels.is_empty()
665    }
666}
667
668/// The level number of a line that starts with `N:`, and the rest.
669fn level_prefix(line: &str) -> Option<(usize, &str)> {
670    let (num, rest) = line.split_once(':')?;
671    if num.is_empty() || !num.bytes().all(|b| b.is_ascii_digit()) {
672        return None;
673    }
674    let n = num.parse().ok()?;
675    (n >= 1).then_some((n, rest))
676}
677
678/// Parse the stack display of a host command: an optional first line
679/// `Error: X`, then `Empty Stack` or the levels from the highest down,
680/// each starting with `N:`; any other line continues the level before it.
681/// Never fails: text before the first level is ignored.
682pub fn parse_stack(text: &str) -> StackReply {
683    let text = text.trim_end_matches(['\r', '\n']);
684    let mut reply = StackReply::default();
685    // Highest level first; reversed at the end.
686    let mut values: Vec<String> = Vec::new();
687    // The level the next level line must carry.
688    let mut expected = 0usize;
689    for (i, line) in text.split('\n').enumerate() {
690        let line = line.strip_suffix('\r').unwrap_or(line);
691        if i == 0
692            && let Some(msg) = line.strip_prefix("Error:")
693        {
694            reply.error = Some(msg.trim().to_string());
695            continue;
696        }
697        if values.is_empty() {
698            if line.trim() == "Empty Stack" {
699                break;
700            }
701            if let Some((n, rest)) = level_prefix(line) {
702                values.push(rest.trim_start_matches(' ').to_string());
703                expected = n - 1;
704            }
705            continue;
706        }
707        if expected >= 1
708            && let Some(rest) = line.strip_prefix(format!("{expected}:").as_str())
709        {
710            values.push(rest.trim_start_matches(' ').to_string());
711            expected -= 1;
712            continue;
713        }
714        if let Some(last) = values.last_mut() {
715            last.push('\n');
716            last.push_str(line);
717        }
718    }
719    values.reverse();
720    reply.levels = values;
721    reply
722}
723
724#[cfg(test)]
725mod tests {
726    use super::*;
727
728    #[test]
729    fn a_picture_travels_as_a_binary_grob() {
730        let g = Graphic::from_packed(131, 64, &[0x5A; 17 * 64]).unwrap();
731        for (family, head) in [(Family::Hp48, b"HPHP48-X"), (Family::Hp49, b"HPHP49-X")] {
732            let file = graphic_file(&g, family).unwrap();
733            assert_eq!(&file[..HEADER_LEN], head);
734            assert_eq!(file.len(), HEADER_LEN + 2196 / 2);
735            assert!(is_binary_file(&file));
736            let Object::Unknown {
737                kind,
738                graphic: Some(back),
739                ..
740            } = decode_file(&file, &crate::NoMemory).unwrap()
741            else {
742                panic!("not a picture");
743            };
744            assert_eq!(kind.as_deref(), Some("Graphic"));
745            assert_eq!(back, g);
746        }
747    }
748
749    #[test]
750    fn stacks() {
751        let r = parse_stack("2:                   42\r\n1: \"a\r\nb\"\r\n");
752        assert_eq!(r.levels, ["\"a\nb\"", "42"]);
753        assert_eq!(r.error, None);
754        assert_eq!(r.level(2), Some("42"));
755        assert_eq!(r.level(0), None);
756        let r = parse_stack("Error: Too Few Arguments\r\nEmpty Stack\r\n");
757        assert_eq!(r.error.as_deref(), Some("Too Few Arguments"));
758        assert!(r.levels.is_empty());
759        // A level whose text holds `1:` is not taken for level 1 twice.
760        let r = parse_stack("1: { 1:2 }\n1: x");
761        assert_eq!(r.levels, ["{ 1:2 }\n1: x"]);
762    }
763    use crate::object::{Integer, NoMemory, Real};
764    use serde_json::json;
765
766    #[test]
767    fn headers_and_packing() {
768        assert_eq!(&binary_header(Family::Hp49), b"HPHP49-X");
769        assert!(is_binary_file(b"HPHP48-J\x01"));
770        assert!(!is_binary_file(b"HPHP47-J") && !is_binary_file(b"HPHP48-"));
771        assert_eq!(Family::of(Model::Hp49g), Family::Hp49);
772        assert_eq!(Family::of(Model::Hp48sx), Family::Hp48);
773        assert_eq!(pack(&[1, 2, 3]), [0x21, 0x03]);
774        assert_eq!(unpack(&[0x21, 0x03]), [1, 2, 3, 0]);
775        assert_eq!(ascii_body("%%HP: T(1)A(D)F(.);\r\n{ 1 }\r\n"), "{ 1 }\r\n");
776        assert_eq!(ascii_body("%%HP: T(3)A(R)F(,);\n« »"), "« »");
777        assert_eq!(ascii_body("{ 1 }"), "{ 1 }");
778    }
779
780    #[test]
781    fn plain_names() {
782        assert!(is_plain_name("X") && is_plain_name("ΣDAT") && is_plain_name("A.1"));
783        for bad in [
784            "",
785            "A\\GS",
786            "1A",
787            ".A",
788            "A B",
789            "X'",
790            "A+B",
791            "a:b",
792            "€",
793            &"A".repeat(128),
794        ] {
795            assert!(!is_plain_name(bad), "{bad:?}");
796        }
797    }
798
799    /// Nibbles from a hex string in memory order.
800    fn nib(s: &str) -> Vec<u8> {
801        s.chars().map(|c| c.to_digit(16).unwrap() as u8).collect()
802    }
803
804    fn dec(s: &str) -> Object {
805        decode(&nib(s), &NoMemory).unwrap()
806    }
807
808    fn real(s: &str) -> Real {
809        Real::parse(s).unwrap()
810    }
811
812    /// A memory holding the real 1 at #2A2C9 (48SX ROM J).
813    struct Rom;
814    impl Memory for Rom {
815        fn nibble(&self, addr: u32) -> Option<u8> {
816            let i = addr.checked_sub(0x2A2C9)? as usize;
817            nib("339200000000000000010").get(i).copied()
818        }
819    }
820
821    /// A list holding an XLIB name of a library the tables do not know:
822    /// no name, its numbers kept, its text wanted from the calculator,
823    /// and it goes back in binary unchanged.
824    #[test]
825    fn unresolved_xlib_names_round_trip() {
826        // { XLIB 1234 5 } with 1234 = #4D2, 5 = #005.
827        let file = {
828            let mut d = binary_header(Family::Hp48).to_vec();
829            d.extend(pack(&nib("47A2029E202D4500B2130")));
830            d
831        };
832        let obj = decode_file(&file, &NoMemory).unwrap();
833        let Object::List { items } = &obj else {
834            panic!("{obj:?}")
835        };
836        assert_eq!(
837            serde_json::to_value(&items[0]).unwrap(),
838            json!({"type": "command", "library": 1234, "command": 5})
839        );
840        assert!(obj.needs_source());
841        assert_eq!(to_source(&obj, Family::Hp48).unwrap(), None);
842        assert_eq!(encode_file(&obj, Family::Hp48).unwrap().unwrap(), file);
843        // With the calculator's text filled in it still travels in binary.
844        let mut filled = obj.clone();
845        fill_sources(&mut filled, "{ XLIB 1234 5 }");
846        let Object::List { items } = &filled else {
847            panic!()
848        };
849        assert!(matches!(&items[0], Object::Command { source: Some(s), .. } if s == "XLIB 1234 5"));
850        assert_eq!(encode_file(&filled, Family::Hp48).unwrap().unwrap(), file);
851    }
852
853    #[test]
854    fn sources_come_from_the_ascii_text() {
855        // Program, algebraic, unit (48SX), each at top level.
856        let mut p = dec("D9D20E16329C2A2ED2A276BA193632B21300");
857        assert!(p.needs_source());
858        fill_sources(&mut p, "%%HP: T(1)A(D)F(.);\r\n« 1 2 +\r\n»\r\n");
859        assert_eq!(
860            p,
861            Object::Program {
862                source: Some("« 1 2 +\n»".into())
863            }
864        );
865        let mut a = dec("8BA2084E201085ED2A2D20B1B21300");
866        fill_sources(&mut a, "%%HP: T(1)A(D)F(.);\r\n'X^2'\r\n");
867        assert_eq!(
868            a,
869            Object::Algebraic {
870                source: Some("'X^2'".into())
871            }
872        );
873        // The unit's leading 1 is a ROM pointer: without memory the unit
874        // cannot be decoded and stays unknown, with it the number is exact.
875        let unit = "ADA209C2A2C2A2070000D6C2A207000037ED2A227B0186B0168B01B21300";
876        assert!(matches!(dec(unit), Object::Unknown { .. }));
877        let mut u = decode(&nib(unit), &Rom).unwrap();
878        assert_eq!(
879            u,
880            Object::Unit {
881                value: real("1"),
882                unit: None
883            }
884        );
885        fill_sources(&mut u, "%%HP: T(1)A(D)F(.);\r\n'1_m/s^2'\r\n");
886        assert_eq!(
887            u,
888            Object::Unit {
889                value: real("1"),
890                unit: Some("m/s^2".into())
891            }
892        );
893    }
894
895    #[test]
896    fn nested_sources_follow_the_tree() {
897        let mut list = Object::List {
898            items: vec![
899                Object::Real { value: real("1") },
900                Object::Program { source: None },
901                Object::String {
902                    value: "a » b".into(),
903                },
904                Object::Tagged {
905                    tag: "T".into(),
906                    object: Box::new(Object::Command {
907                        name: None,
908                        address: None,
909                        library: None,
910                        command: None,
911                        source: None,
912                    }),
913                },
914                Object::Binary {
915                    value: 42,
916                    base: None,
917                    text: None,
918                },
919                Object::List {
920                    items: vec![Object::Algebraic { source: None }],
921                },
922                Object::Complex {
923                    re: real("1"),
924                    im: real("2"),
925                },
926            ],
927        };
928        fill_sources(
929            &mut list,
930            "{ 1 « 2 \"x»\" + » \"a » b\" :T: + # 42d { 'X+1' } (1,2) }",
931        );
932        let Object::List { items } = &list else {
933            panic!()
934        };
935        assert_eq!(
936            items[1],
937            Object::Program {
938                source: Some("« 2 \"x»\" + »".into())
939            }
940        );
941        assert_eq!(
942            items[3],
943            Object::Tagged {
944                tag: "T".into(),
945                object: Box::new(Object::Command {
946                    name: None,
947                    address: None,
948                    library: None,
949                    command: None,
950                    source: Some("+".into())
951                })
952            }
953        );
954        assert_eq!(
955            items[5],
956            Object::List {
957                items: vec![Object::Algebraic {
958                    source: Some("'X+1'".into())
959                }]
960            }
961        );
962        // A mismatch fills nothing nested.
963        let mut l = Object::List {
964            items: vec![Object::Program { source: None }],
965        };
966        fill_sources(&mut l, "{ 1 2 }");
967        assert_eq!(
968            l,
969            Object::List {
970                items: vec![Object::Program { source: None }]
971            }
972        );
973        // ... but a top-level program takes the whole text.
974        let mut p = Object::Program { source: None };
975        fill_sources(&mut p, "« garbled");
976        assert_eq!(
977            p,
978            Object::Program {
979                source: Some("« garbled".into())
980            }
981        );
982    }
983
984    #[test]
985    fn encode_round_trips_through_decode() {
986        let objs = [
987            Object::Real {
988                value: real("-4.79425538604E-1"),
989            },
990            Object::Complex {
991                re: real("1.5"),
992                im: real("-2"),
993            },
994            Object::String {
995                value: "Hi «x» \"q\"".into(),
996            },
997            Object::Name {
998                value: "ABC".into(),
999            },
1000            Object::LocalName { value: "x".into() },
1001            Object::Character { value: "A".into() },
1002            Object::Binary {
1003                value: u64::MAX,
1004                base: None,
1005                text: None,
1006            },
1007            Object::List {
1008                items: vec![
1009                    Object::Real { value: real("1") },
1010                    Object::List { items: vec![] },
1011                    Object::Tagged {
1012                        tag: "T".into(),
1013                        object: Box::new(Object::Real { value: real("5") }),
1014                    },
1015                ],
1016            },
1017        ];
1018        for o in &objs {
1019            let file = encode_file(o, Family::Hp48).unwrap().unwrap();
1020            assert_eq!(&file[..8], b"HPHP48-X");
1021            assert_eq!(decode_file(&file, &NoMemory).unwrap(), *o, "{o:?}");
1022            // Exactly the object's bytes: the 48SX rejects a trailing byte.
1023            let size = crate::prolog::object_size(&unpack(&file[8..]), 0).unwrap();
1024            assert_eq!(file.len(), 8 + size.div_ceil(2));
1025        }
1026        // The known 48SX encoding of 8.72653549837E-3.
1027        let file = encode_file(
1028            &Object::Real {
1029                value: real("8.72653549837E-3"),
1030            },
1031            Family::Hp48,
1032        )
1033        .unwrap()
1034        .unwrap();
1035        assert_eq!(unpack(&file[8..])[..21], nib("339207997389453562780")[..]);
1036        let i = Object::Integer {
1037            value: Integer::parse("-1234567890123456789").unwrap(),
1038        };
1039        let file = encode_file(&i, Family::Hp49).unwrap().unwrap();
1040        assert_eq!(
1041            unpack(&file[8..])[..30],
1042            nib("416209100098765432109876543219")[..]
1043        );
1044        assert!(encode_file(&i, Family::Hp48).is_err());
1045        assert_eq!(
1046            encode_file(
1047                &Object::Program {
1048                    source: Some("« »".into())
1049                },
1050                Family::Hp48
1051            )
1052            .unwrap(),
1053            None
1054        );
1055        // An unknown object travels back as its hex.
1056        let u = dec("E1B20A0000FFFFF");
1057        assert!(matches!(u, Object::Unknown { .. }));
1058        assert_eq!(
1059            encode_file(&u, Family::Hp48).unwrap().unwrap()[8..],
1060            pack(&nib("E1B20A0000FFFFF"))[..]
1061        );
1062    }
1063
1064    #[test]
1065    fn sources_for_text_transfers() {
1066        let src = |o: Object| to_source(&o, Family::Hp48).unwrap();
1067        assert_eq!(src(Object::Real { value: real("0.5") }).unwrap(), "5.E-1");
1068        assert_eq!(
1069            src(Object::Complex {
1070                re: real("1"),
1071                im: real("-2")
1072            })
1073            .unwrap(),
1074            "(1.,-2.)"
1075        );
1076        assert_eq!(
1077            src(Object::String {
1078                value: "a b".into()
1079            })
1080            .unwrap(),
1081            "\"a b\""
1082        );
1083        assert_eq!(
1084            src(Object::String {
1085                value: "a\"b".into()
1086            }),
1087            None
1088        );
1089        assert_eq!(
1090            src(Object::Binary {
1091                value: 255,
1092                base: None,
1093                text: None
1094            })
1095            .unwrap(),
1096            "#FFh"
1097        );
1098        assert_eq!(
1099            src(Object::List {
1100                items: vec![
1101                    Object::Name { value: "X".into() },
1102                    Object::Unit {
1103                        value: real("9.81"),
1104                        unit: Some("m/s^2".into())
1105                    }
1106                ]
1107            })
1108            .unwrap(),
1109            "{ 'X' 9.81_m/s^2 }"
1110        );
1111        assert_eq!(src(Object::LocalName { value: "x".into() }), None);
1112        assert!(to_source(&Object::Program { source: None }, Family::Hp48).is_err());
1113    }
1114
1115    #[test]
1116    fn text_cannot_break_out_of_its_quotes() {
1117        let src = |o: Object| to_source(&o, Family::Hp48);
1118        // Names and tags that are not plain tokens go binary instead.
1119        assert_eq!(
1120            src(Object::Name {
1121                value: "X' 11. 'QA' STO 'Y".into()
1122            })
1123            .unwrap(),
1124            None
1125        );
1126        let odd = Object::Tagged {
1127            tag: "a:b".into(),
1128            object: Box::new(Object::Real { value: Real::ZERO }),
1129        };
1130        assert_eq!(src(odd.clone()).unwrap(), None);
1131        assert!(encode_file(&odd, Family::Hp48).unwrap().is_some());
1132        let spaced = Object::Tagged {
1133            tag: "my tag".into(),
1134            object: Box::new(Object::Real { value: Real::ZERO }),
1135        };
1136        assert_eq!(src(spaced).unwrap(), None);
1137        // Programs, algebraics, units and commands are refused.
1138        let p = |s: &str| Object::Program {
1139            source: Some(s.into()),
1140        };
1141        assert!(src(p("« 1 » 22. 'QB' STO « 2 »")).is_err());
1142        assert!(src(p("1 2 +")).is_err());
1143        assert!(src(p("« 1 \"»\" »")).is_ok());
1144        assert!(src(p("\\<< 1 \\>>")).is_ok());
1145        assert!(src(p("« « 1 » »")).is_ok());
1146        let a = |s: &str| Object::Algebraic {
1147            source: Some(s.into()),
1148        };
1149        assert!(src(a("'X' 'Y' 44. 'QD' STO 'Z'")).is_err());
1150        assert!(src(a("X+1")).is_err());
1151        assert!(src(a("''")).is_err());
1152        assert_eq!(src(a("'X^2+1'")).unwrap().unwrap(), "'X^2+1'");
1153        let u = |s: &str| Object::Unit {
1154            value: Real::ZERO,
1155            unit: Some(s.into()),
1156        };
1157        assert!(src(u("m 33. 'QC' STO")).is_err());
1158        assert!(src(u("m'")).is_err());
1159        assert!(src(u("(m/s)^2")).is_ok());
1160        let c = |s: &str| Object::Command {
1161            name: None,
1162            address: None,
1163            library: None,
1164            command: None,
1165            source: Some(s.into()),
1166        };
1167        assert!(src(c("+ 1. 'QE' STO")).is_err());
1168        assert_eq!(src(c("\u{2192}LIST")).unwrap().unwrap(), "\u{2192}LIST");
1169    }
1170
1171    #[test]
1172    fn json_shapes_parse() {
1173        let o: Object = serde_json::from_value(json!({"type": "list", "items": [
1174            {"type": "real", "value": 0.5},
1175            {"type": "string", "value": "s"},
1176            {"type": "program", "source": "« 1 »"},
1177            {"type": "binary", "value": 42},
1178            {"type": "array", "dims": [2], "items": [{"type": "real", "value": 1}, {"type": "real", "value": 2}]}
1179        ]}))
1180        .unwrap();
1181        let Object::List { items } = o else { panic!() };
1182        assert_eq!(items.len(), 5);
1183        assert!(serde_json::from_value::<Object>(json!({"type": "real", "value": "x"})).is_err());
1184        assert!(serde_json::from_value::<Object>(json!({"type": "nope"})).is_err());
1185    }
1186}