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