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