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::Model;
12
13use crate::charset;
14use crate::object::{ArrayItem, 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
401fn prolog(out: &mut Vec<u8>, ty: ObjectType) {
402    put_field(out, u64::from(ty.prolog()), 5);
403}
404
405fn encode_into(obj: &Object, family: Family, out: &mut Vec<u8>) -> Result<bool> {
406    match obj {
407        Object::Real { value } => {
408            prolog(out, ObjectType::Real);
409            out.extend(value.to_body());
410        }
411        Object::Complex { re, im } => {
412            prolog(out, ObjectType::Complex);
413            out.extend(re.to_body());
414            out.extend(im.to_body());
415        }
416        Object::String { value } => {
417            prolog(out, ObjectType::String);
418            let mut body = Vec::new();
419            let n = put_chars(&mut body, value)?;
420            put_field(out, 5 + 2 * n as u64, 5);
421            out.extend(body);
422        }
423        Object::Name { value } | Object::LocalName { value } => {
424            let ty = if matches!(obj, Object::Name { .. }) {
425                ObjectType::GlobalName
426            } else {
427                ObjectType::LocalName
428            };
429            prolog(out, ty);
430            let mut body = Vec::new();
431            let n = put_chars(&mut body, value)?;
432            if n == 0 || n > 255 {
433                bail!("a name has 1 to 255 characters: {value:?}");
434            }
435            put_field(out, n as u64, 2);
436            out.extend(body);
437        }
438        Object::Character { value } => {
439            prolog(out, ObjectType::Character);
440            if put_chars(out, value)? != 1 {
441                bail!("a character object holds one character: {value:?}");
442            }
443        }
444        Object::Binary { value, .. } => {
445            prolog(out, ObjectType::BinaryInteger);
446            put_field(out, 21, 5);
447            put_field(out, *value, 16);
448        }
449        Object::Integer { value } => {
450            if family != Family::Hp49 {
451                bail!("exact integers exist on the 49G only; send a real instead");
452            }
453            prolog(out, ObjectType::Integer);
454            let n = value.to_nibbles();
455            put_field(out, 5 + n.len() as u64, 5);
456            out.extend(n);
457        }
458        Object::List { items } => {
459            prolog(out, ObjectType::List);
460            for item in items {
461                if !encode_into(item, family, out)? {
462                    return Ok(false);
463                }
464            }
465            put_field(out, u64::from(SEMI), 5);
466        }
467        Object::Tagged { tag, object } => {
468            prolog(out, ObjectType::Tagged);
469            let mut body = Vec::new();
470            let n = put_chars(&mut body, tag)?;
471            if n > 255 {
472                bail!("tag longer than 255 characters");
473            }
474            put_field(out, n as u64, 2);
475            out.extend(body);
476            return encode_into(object, family, out);
477        }
478        Object::Unknown { hex, truncated, .. } => {
479            if *truncated {
480                return Ok(false);
481            }
482            for c in hex.chars() {
483                let d = c
484                    .to_digit(16)
485                    .with_context(|| format!("hex holds {c:?}, not a hex digit"))?;
486                out.push(d as u8);
487            }
488        }
489        // An XLIB name: prolog, library and command (3 nibbles each).
490        Object::Command {
491            address: None,
492            library: Some(lib),
493            command: Some(cmd),
494            ..
495        } => {
496            prolog(out, ObjectType::XlibName);
497            put_field(out, u64::from(*lib), 3);
498            put_field(out, u64::from(*cmd), 3);
499        }
500        Object::Unit { .. }
501        | Object::Array { .. }
502        | Object::Program { .. }
503        | Object::Algebraic { .. }
504        | Object::Command { .. } => return Ok(false),
505    }
506    Ok(true)
507}
508
509/// RPL source that compiles to `obj`, or `None` when it has none (a string
510/// holding `"`, a local name, a character, an unknown object).
511pub fn to_source(obj: &Object, family: Family) -> Result<Option<String>> {
512    Ok(match obj {
513        Object::Real { value } => Some(value.to_source()),
514        Object::Integer { value } => {
515            if family != Family::Hp49 {
516                bail!("exact integers exist on the 49G only; send a real instead");
517            }
518            Some(value.to_source())
519        }
520        Object::Complex { re, im } => Some(format!("({},{})", re.to_source(), im.to_source())),
521        Object::String { value } => (!value.contains(['"', '\\'])).then(|| format!("\"{value}\"")),
522        // A name or tag that is not a plain token could close its quotes
523        // and run commands: those travel in binary only.
524        Object::Name { value } => is_plain_name(value).then(|| format!("'{value}'")),
525        Object::Binary { value, .. } => Some(format!("#{value:X}h")),
526        Object::List { items } => {
527            let mut parts = Vec::new();
528            for item in items {
529                match to_source(item, family)? {
530                    Some(s) => parts.push(s),
531                    None => return Ok(None),
532                }
533            }
534            Some(format!("{{ {} }}", parts.join(" ")))
535        }
536        Object::Tagged { tag, object } => {
537            if !plain_token(tag, false) {
538                return Ok(None);
539            }
540            to_source(object, family)?.map(|s| format!(":{tag}:{s}"))
541        }
542        Object::Unit { value, unit } => {
543            let unit = unit.as_deref().context("a unit object needs its unit")?;
544            if !plain_token(unit, true) || unit.contains('_') {
545                bail!(
546                    "not a unit expression (spaces and RPL delimiters are not allowed): {unit:?}"
547                );
548            }
549            Some(format!("{}_{unit}", value.to_source()))
550        }
551        Object::Array { items, .. } => array_source(items, family)?,
552        Object::Program { source } | Object::Algebraic { source } => {
553            let source = source.as_deref().context("this object needs its source")?;
554            Some(one_object_source(obj, source)?)
555        }
556        // An XLIB name the ROM's tables do not name: its text (`XLIB l n`)
557        // does not compile back; it travels in binary.
558        Object::Command {
559            name: None,
560            address: None,
561            library: Some(_),
562            command: Some(_),
563            ..
564        } => None,
565        Object::Command { name, source, .. } => {
566            let source = source
567                .as_deref()
568                .or(name.as_deref())
569                .context("this object needs its source")?;
570            Some(one_object_source(obj, source)?)
571        }
572        Object::LocalName { .. } | Object::Character { .. } | Object::Unknown { .. } => None,
573    })
574}
575
576/// Whether `s` is one RPL token that cannot close a quote or a group:
577/// no whitespace, control characters, quotes or delimiters (parentheses
578/// only with `parens`, for unit expressions).
579fn plain_token(s: &str, parens: bool) -> bool {
580    !s.is_empty()
581        && s.chars().all(|c| {
582            !c.is_whitespace()
583                && !c.is_control()
584                && !matches!(
585                    c,
586                    '\'' | '"' | '«' | '»' | '{' | '}' | '[' | ']' | ':' | '#' | ',' | ';' | '\\'
587                )
588                && (parens || !matches!(c, '(' | ')'))
589        })
590}
591
592/// `source` checked to be exactly one program (one balanced `« »`), one
593/// algebraic (one `' '` group) or one command (a plain token), with
594/// nothing before or after it: a host command must push it, not run
595/// anything.
596fn one_object_source(obj: &Object, source: &str) -> Result<String> {
597    // The calculator reads the trigraphs as the delimiters themselves.
598    let text = source.trim().replace("\\<<", "«").replace("\\>>", "»");
599    let mut scan = Scanner { s: &text, pos: 0 };
600    let ok = match obj {
601        Object::Program { .. } => scan.balanced('«', '»').is_some(),
602        Object::Algebraic { .. } => scan.quoted().is_some_and(|q| q.len() > 2),
603        _ => plain_token(&text, true) && scan.token().is_some(),
604    };
605    if !ok || !scan.rest().trim().is_empty() {
606        let what = match obj {
607            Object::Program { .. } => "a program's source must be one « ... » group",
608            Object::Algebraic { .. } => "an algebraic's source must be one '...' group",
609            _ => "a command's source must be one token",
610        };
611        bail!("{what} with nothing after it: {source:?}");
612    }
613    Ok(source.trim().to_string())
614}
615
616fn array_source(items: &[ArrayItem], family: Family) -> Result<Option<String>> {
617    let mut parts = Vec::new();
618    for item in items {
619        let s = match item {
620            ArrayItem::Row(row) => array_source(row, family)?,
621            ArrayItem::Item(obj) => {
622                if !matches!(**obj, Object::Real { .. } | Object::Complex { .. }) {
623                    bail!("array elements must be reals or complex numbers");
624                }
625                to_source(obj, family)?
626            }
627        };
628        match s {
629            Some(s) => parts.push(s),
630            None => return Ok(None),
631        }
632    }
633    Ok(Some(format!("[ {} ]", parts.join(" "))))
634}
635
636/// The stack text the ROM's Kermit server returns for a host command
637/// (`C`), already translated from the HP character set.
638#[derive(Clone, Debug, Default, PartialEq)]
639pub struct StackReply {
640    /// The text after `Error:` when the command failed.
641    pub error: Option<String>,
642    /// The levels as display text, level 1 first.
643    pub levels: Vec<String>,
644}
645
646impl StackReply {
647    /// Level `n` (1-based), if there is one.
648    pub fn level(&self, n: usize) -> Option<&str> {
649        n.checked_sub(1)
650            .and_then(|i| self.levels.get(i))
651            .map(String::as_str)
652    }
653}
654
655/// The level number of a line that starts with `N:`, and the rest.
656fn level_prefix(line: &str) -> Option<(usize, &str)> {
657    let (num, rest) = line.split_once(':')?;
658    if num.is_empty() || !num.bytes().all(|b| b.is_ascii_digit()) {
659        return None;
660    }
661    let n = num.parse().ok()?;
662    (n >= 1).then_some((n, rest))
663}
664
665/// Parse the stack display of a host command: an optional first line
666/// `Error: X`, then `Empty Stack` or the levels from the highest down,
667/// each starting with `N:`; any other line continues the level before it.
668/// Never fails: text before the first level is ignored.
669pub fn parse_stack(text: &str) -> StackReply {
670    let text = text.trim_end_matches(['\r', '\n']);
671    let mut reply = StackReply::default();
672    // Highest level first; reversed at the end.
673    let mut values: Vec<String> = Vec::new();
674    // The level the next level line must carry.
675    let mut expected = 0usize;
676    for (i, line) in text.split('\n').enumerate() {
677        let line = line.strip_suffix('\r').unwrap_or(line);
678        if i == 0
679            && let Some(msg) = line.strip_prefix("Error:")
680        {
681            reply.error = Some(msg.trim().to_string());
682            continue;
683        }
684        if values.is_empty() {
685            if line.trim() == "Empty Stack" {
686                break;
687            }
688            if let Some((n, rest)) = level_prefix(line) {
689                values.push(rest.trim_start_matches(' ').to_string());
690                expected = n - 1;
691            }
692            continue;
693        }
694        if expected >= 1
695            && let Some(rest) = line.strip_prefix(format!("{expected}:").as_str())
696        {
697            values.push(rest.trim_start_matches(' ').to_string());
698            expected -= 1;
699            continue;
700        }
701        if let Some(last) = values.last_mut() {
702            last.push('\n');
703            last.push_str(line);
704        }
705    }
706    values.reverse();
707    reply.levels = values;
708    reply
709}
710
711#[cfg(test)]
712mod tests {
713    use super::*;
714
715    #[test]
716    fn stacks() {
717        let r = parse_stack("2:                   42\r\n1: \"a\r\nb\"\r\n");
718        assert_eq!(r.levels, ["\"a\nb\"", "42"]);
719        assert_eq!(r.error, None);
720        assert_eq!(r.level(2), Some("42"));
721        assert_eq!(r.level(0), None);
722        let r = parse_stack("Error: Too Few Arguments\r\nEmpty Stack\r\n");
723        assert_eq!(r.error.as_deref(), Some("Too Few Arguments"));
724        assert!(r.levels.is_empty());
725        // A level whose text holds `1:` is not taken for level 1 twice.
726        let r = parse_stack("1: { 1:2 }\n1: x");
727        assert_eq!(r.levels, ["{ 1:2 }\n1: x"]);
728    }
729    use crate::object::{Integer, NoMemory, Real};
730    use serde_json::json;
731
732    #[test]
733    fn headers_and_packing() {
734        assert_eq!(&binary_header(Family::Hp49), b"HPHP49-X");
735        assert!(is_binary_file(b"HPHP48-J\x01"));
736        assert!(!is_binary_file(b"HPHP47-J") && !is_binary_file(b"HPHP48-"));
737        assert_eq!(Family::of(Model::Hp49g), Family::Hp49);
738        assert_eq!(Family::of(Model::Hp48sx), Family::Hp48);
739        assert_eq!(pack(&[1, 2, 3]), [0x21, 0x03]);
740        assert_eq!(unpack(&[0x21, 0x03]), [1, 2, 3, 0]);
741        assert_eq!(ascii_body("%%HP: T(1)A(D)F(.);\r\n{ 1 }\r\n"), "{ 1 }\r\n");
742        assert_eq!(ascii_body("%%HP: T(3)A(R)F(,);\n« »"), "« »");
743        assert_eq!(ascii_body("{ 1 }"), "{ 1 }");
744    }
745
746    #[test]
747    fn plain_names() {
748        assert!(is_plain_name("X") && is_plain_name("ΣDAT") && is_plain_name("A.1"));
749        for bad in [
750            "",
751            "A\\GS",
752            "1A",
753            ".A",
754            "A B",
755            "X'",
756            "A+B",
757            "a:b",
758            "€",
759            &"A".repeat(128),
760        ] {
761            assert!(!is_plain_name(bad), "{bad:?}");
762        }
763    }
764
765    /// Nibbles from a hex string in memory order.
766    fn nib(s: &str) -> Vec<u8> {
767        s.chars().map(|c| c.to_digit(16).unwrap() as u8).collect()
768    }
769
770    fn dec(s: &str) -> Object {
771        decode(&nib(s), &NoMemory).unwrap()
772    }
773
774    fn real(s: &str) -> Real {
775        Real::parse(s).unwrap()
776    }
777
778    /// A memory holding the real 1 at #2A2C9 (48SX ROM J).
779    struct Rom;
780    impl Memory for Rom {
781        fn nibble(&self, addr: u32) -> Option<u8> {
782            let i = addr.checked_sub(0x2A2C9)? as usize;
783            nib("339200000000000000010").get(i).copied()
784        }
785    }
786
787    /// A list holding an XLIB name of a library the tables do not know:
788    /// no name, its numbers kept, its text wanted from the calculator,
789    /// and it goes back in binary unchanged.
790    #[test]
791    fn unresolved_xlib_names_round_trip() {
792        // { XLIB 1234 5 } with 1234 = #4D2, 5 = #005.
793        let file = {
794            let mut d = binary_header(Family::Hp48).to_vec();
795            d.extend(pack(&nib("47A2029E202D4500B2130")));
796            d
797        };
798        let obj = decode_file(&file, &NoMemory).unwrap();
799        let Object::List { items } = &obj else {
800            panic!("{obj:?}")
801        };
802        assert_eq!(
803            serde_json::to_value(&items[0]).unwrap(),
804            json!({"type": "command", "library": 1234, "command": 5})
805        );
806        assert!(obj.needs_source());
807        assert_eq!(to_source(&obj, Family::Hp48).unwrap(), None);
808        assert_eq!(encode_file(&obj, Family::Hp48).unwrap().unwrap(), file);
809        // With the calculator's text filled in it still travels in binary.
810        let mut filled = obj.clone();
811        fill_sources(&mut filled, "{ XLIB 1234 5 }");
812        let Object::List { items } = &filled else {
813            panic!()
814        };
815        assert!(matches!(&items[0], Object::Command { source: Some(s), .. } if s == "XLIB 1234 5"));
816        assert_eq!(encode_file(&filled, Family::Hp48).unwrap().unwrap(), file);
817    }
818
819    #[test]
820    fn sources_come_from_the_ascii_text() {
821        // Program, algebraic, unit (48SX), each at top level.
822        let mut p = dec("D9D20E16329C2A2ED2A276BA193632B21300");
823        assert!(p.needs_source());
824        fill_sources(&mut p, "%%HP: T(1)A(D)F(.);\r\n« 1 2 +\r\n»\r\n");
825        assert_eq!(
826            p,
827            Object::Program {
828                source: Some("« 1 2 +\n»".into())
829            }
830        );
831        let mut a = dec("8BA2084E201085ED2A2D20B1B21300");
832        fill_sources(&mut a, "%%HP: T(1)A(D)F(.);\r\n'X^2'\r\n");
833        assert_eq!(
834            a,
835            Object::Algebraic {
836                source: Some("'X^2'".into())
837            }
838        );
839        // The unit's leading 1 is a ROM pointer: without memory the unit
840        // cannot be decoded and stays unknown, with it the number is exact.
841        let unit = "ADA209C2A2C2A2070000D6C2A207000037ED2A227B0186B0168B01B21300";
842        assert!(matches!(dec(unit), Object::Unknown { .. }));
843        let mut u = decode(&nib(unit), &Rom).unwrap();
844        assert_eq!(
845            u,
846            Object::Unit {
847                value: real("1"),
848                unit: None
849            }
850        );
851        fill_sources(&mut u, "%%HP: T(1)A(D)F(.);\r\n'1_m/s^2'\r\n");
852        assert_eq!(
853            u,
854            Object::Unit {
855                value: real("1"),
856                unit: Some("m/s^2".into())
857            }
858        );
859    }
860
861    #[test]
862    fn nested_sources_follow_the_tree() {
863        let mut list = Object::List {
864            items: vec![
865                Object::Real { value: real("1") },
866                Object::Program { source: None },
867                Object::String {
868                    value: "a » b".into(),
869                },
870                Object::Tagged {
871                    tag: "T".into(),
872                    object: Box::new(Object::Command {
873                        name: None,
874                        address: None,
875                        library: None,
876                        command: None,
877                        source: None,
878                    }),
879                },
880                Object::Binary {
881                    value: 42,
882                    base: None,
883                    text: None,
884                },
885                Object::List {
886                    items: vec![Object::Algebraic { source: None }],
887                },
888                Object::Complex {
889                    re: real("1"),
890                    im: real("2"),
891                },
892            ],
893        };
894        fill_sources(
895            &mut list,
896            "{ 1 « 2 \"x»\" + » \"a » b\" :T: + # 42d { 'X+1' } (1,2) }",
897        );
898        let Object::List { items } = &list else {
899            panic!()
900        };
901        assert_eq!(
902            items[1],
903            Object::Program {
904                source: Some("« 2 \"x»\" + »".into())
905            }
906        );
907        assert_eq!(
908            items[3],
909            Object::Tagged {
910                tag: "T".into(),
911                object: Box::new(Object::Command {
912                    name: None,
913                    address: None,
914                    library: None,
915                    command: None,
916                    source: Some("+".into())
917                })
918            }
919        );
920        assert_eq!(
921            items[5],
922            Object::List {
923                items: vec![Object::Algebraic {
924                    source: Some("'X+1'".into())
925                }]
926            }
927        );
928        // A mismatch fills nothing nested.
929        let mut l = Object::List {
930            items: vec![Object::Program { source: None }],
931        };
932        fill_sources(&mut l, "{ 1 2 }");
933        assert_eq!(
934            l,
935            Object::List {
936                items: vec![Object::Program { source: None }]
937            }
938        );
939        // ... but a top-level program takes the whole text.
940        let mut p = Object::Program { source: None };
941        fill_sources(&mut p, "« garbled");
942        assert_eq!(
943            p,
944            Object::Program {
945                source: Some("« garbled".into())
946            }
947        );
948    }
949
950    #[test]
951    fn encode_round_trips_through_decode() {
952        let objs = [
953            Object::Real {
954                value: real("-4.79425538604E-1"),
955            },
956            Object::Complex {
957                re: real("1.5"),
958                im: real("-2"),
959            },
960            Object::String {
961                value: "Hi «x» \"q\"".into(),
962            },
963            Object::Name {
964                value: "ABC".into(),
965            },
966            Object::LocalName { value: "x".into() },
967            Object::Character { value: "A".into() },
968            Object::Binary {
969                value: u64::MAX,
970                base: None,
971                text: None,
972            },
973            Object::List {
974                items: vec![
975                    Object::Real { value: real("1") },
976                    Object::List { items: vec![] },
977                    Object::Tagged {
978                        tag: "T".into(),
979                        object: Box::new(Object::Real { value: real("5") }),
980                    },
981                ],
982            },
983        ];
984        for o in &objs {
985            let file = encode_file(o, Family::Hp48).unwrap().unwrap();
986            assert_eq!(&file[..8], b"HPHP48-X");
987            assert_eq!(decode_file(&file, &NoMemory).unwrap(), *o, "{o:?}");
988            // Exactly the object's bytes: the 48SX rejects a trailing byte.
989            let size = crate::prolog::object_size(&unpack(&file[8..]), 0).unwrap();
990            assert_eq!(file.len(), 8 + size.div_ceil(2));
991        }
992        // The known 48SX encoding of 8.72653549837E-3.
993        let file = encode_file(
994            &Object::Real {
995                value: real("8.72653549837E-3"),
996            },
997            Family::Hp48,
998        )
999        .unwrap()
1000        .unwrap();
1001        assert_eq!(unpack(&file[8..])[..21], nib("339207997389453562780")[..]);
1002        let i = Object::Integer {
1003            value: Integer::parse("-1234567890123456789").unwrap(),
1004        };
1005        let file = encode_file(&i, Family::Hp49).unwrap().unwrap();
1006        assert_eq!(
1007            unpack(&file[8..])[..30],
1008            nib("416209100098765432109876543219")[..]
1009        );
1010        assert!(encode_file(&i, Family::Hp48).is_err());
1011        assert_eq!(
1012            encode_file(
1013                &Object::Program {
1014                    source: Some("« »".into())
1015                },
1016                Family::Hp48
1017            )
1018            .unwrap(),
1019            None
1020        );
1021        // An unknown object travels back as its hex.
1022        let u = dec("E1B20A0000FFFFF");
1023        assert!(matches!(u, Object::Unknown { .. }));
1024        assert_eq!(
1025            encode_file(&u, Family::Hp48).unwrap().unwrap()[8..],
1026            pack(&nib("E1B20A0000FFFFF"))[..]
1027        );
1028    }
1029
1030    #[test]
1031    fn sources_for_text_transfers() {
1032        let src = |o: Object| to_source(&o, Family::Hp48).unwrap();
1033        assert_eq!(src(Object::Real { value: real("0.5") }).unwrap(), "5.E-1");
1034        assert_eq!(
1035            src(Object::Complex {
1036                re: real("1"),
1037                im: real("-2")
1038            })
1039            .unwrap(),
1040            "(1.,-2.)"
1041        );
1042        assert_eq!(
1043            src(Object::String {
1044                value: "a b".into()
1045            })
1046            .unwrap(),
1047            "\"a b\""
1048        );
1049        assert_eq!(
1050            src(Object::String {
1051                value: "a\"b".into()
1052            }),
1053            None
1054        );
1055        assert_eq!(
1056            src(Object::Binary {
1057                value: 255,
1058                base: None,
1059                text: None
1060            })
1061            .unwrap(),
1062            "#FFh"
1063        );
1064        assert_eq!(
1065            src(Object::List {
1066                items: vec![
1067                    Object::Name { value: "X".into() },
1068                    Object::Unit {
1069                        value: real("9.81"),
1070                        unit: Some("m/s^2".into())
1071                    }
1072                ]
1073            })
1074            .unwrap(),
1075            "{ 'X' 9.81_m/s^2 }"
1076        );
1077        assert_eq!(src(Object::LocalName { value: "x".into() }), None);
1078        assert!(to_source(&Object::Program { source: None }, Family::Hp48).is_err());
1079    }
1080
1081    #[test]
1082    fn text_cannot_break_out_of_its_quotes() {
1083        let src = |o: Object| to_source(&o, Family::Hp48);
1084        // Names and tags that are not plain tokens go binary instead.
1085        assert_eq!(
1086            src(Object::Name {
1087                value: "X' 11. 'QA' STO 'Y".into()
1088            })
1089            .unwrap(),
1090            None
1091        );
1092        let odd = Object::Tagged {
1093            tag: "a:b".into(),
1094            object: Box::new(Object::Real { value: Real::ZERO }),
1095        };
1096        assert_eq!(src(odd.clone()).unwrap(), None);
1097        assert!(encode_file(&odd, Family::Hp48).unwrap().is_some());
1098        let spaced = Object::Tagged {
1099            tag: "my tag".into(),
1100            object: Box::new(Object::Real { value: Real::ZERO }),
1101        };
1102        assert_eq!(src(spaced).unwrap(), None);
1103        // Programs, algebraics, units and commands are refused.
1104        let p = |s: &str| Object::Program {
1105            source: Some(s.into()),
1106        };
1107        assert!(src(p("« 1 » 22. 'QB' STO « 2 »")).is_err());
1108        assert!(src(p("1 2 +")).is_err());
1109        assert!(src(p("« 1 \"»\" »")).is_ok());
1110        assert!(src(p("\\<< 1 \\>>")).is_ok());
1111        assert!(src(p("« « 1 » »")).is_ok());
1112        let a = |s: &str| Object::Algebraic {
1113            source: Some(s.into()),
1114        };
1115        assert!(src(a("'X' 'Y' 44. 'QD' STO 'Z'")).is_err());
1116        assert!(src(a("X+1")).is_err());
1117        assert!(src(a("''")).is_err());
1118        assert_eq!(src(a("'X^2+1'")).unwrap().unwrap(), "'X^2+1'");
1119        let u = |s: &str| Object::Unit {
1120            value: Real::ZERO,
1121            unit: Some(s.into()),
1122        };
1123        assert!(src(u("m 33. 'QC' STO")).is_err());
1124        assert!(src(u("m'")).is_err());
1125        assert!(src(u("(m/s)^2")).is_ok());
1126        let c = |s: &str| Object::Command {
1127            name: None,
1128            address: None,
1129            library: None,
1130            command: None,
1131            source: Some(s.into()),
1132        };
1133        assert!(src(c("+ 1. 'QE' STO")).is_err());
1134        assert_eq!(src(c("\u{2192}LIST")).unwrap().unwrap(), "\u{2192}LIST");
1135    }
1136
1137    #[test]
1138    fn json_shapes_parse() {
1139        let o: Object = serde_json::from_value(json!({"type": "list", "items": [
1140            {"type": "real", "value": 0.5},
1141            {"type": "string", "value": "s"},
1142            {"type": "program", "source": "« 1 »"},
1143            {"type": "binary", "value": 42},
1144            {"type": "array", "dims": [2], "items": [{"type": "real", "value": 1}, {"type": "real", "value": 2}]}
1145        ]}))
1146        .unwrap();
1147        let Object::List { items } = o else { panic!() };
1148        assert_eq!(items.len(), 5);
1149        assert!(serde_json::from_value::<Object>(json!({"type": "real", "value": "x"})).is_err());
1150        assert!(serde_json::from_value::<Object>(json!({"type": "nope"})).is_err());
1151    }
1152}