1use std::cell::Cell;
18use std::collections::{BTreeMap, HashMap, VecDeque};
19
20use crate::charset;
21use crate::names::NameTable;
22use crate::object::Memory;
23use crate::prolog::{ObjectType, SEMI, object_size, read_field};
24
25pub const MAX_STEPS: usize = 1 << 18;
28pub const MAX_NESTING: usize = 12;
30const MAX_ELEMENTS: usize = 4096;
32const MAX_CODE_DEPTH: usize = 4;
34const MIN_MENUS: usize = 20;
36
37#[derive(Clone, Debug, Default, PartialEq, Eq)]
39pub struct MenuDef {
40 pub commands: Vec<String>,
42 pub submenus: Vec<(String, u32)>,
44}
45
46#[derive(Clone, Debug, Default, PartialEq, Eq)]
48pub struct Menus {
49 menus: Vec<Option<MenuDef>>,
51 library: Option<u16>,
53 addresses: HashMap<u32, u32>,
55 truncated: bool,
58}
59
60impl Menus {
61 pub fn len(&self) -> usize {
63 self.menus.len()
64 }
65
66 pub fn truncated(&self) -> bool {
69 self.truncated
70 }
71
72 pub fn is_empty(&self) -> bool {
74 self.menus.is_empty()
75 }
76
77 pub fn get(&self, n: u32) -> Option<&MenuDef> {
79 self.menus.get(n as usize)?.as_ref()
80 }
81
82 pub fn number(&self, pointer: u32, mem: &dyn Memory) -> Option<u32> {
87 if let Some(n) = self.addresses.get(&pointer) {
88 return Some(*n);
89 }
90 let lib = self.library?;
91 let field = |at: u32, w: u32| -> Option<u32> {
92 (0..w).rev().try_fold(0u32, |v, i| {
93 Some((v << 4) | u32::from(mem.nibble(at.checked_add(i)?)?))
94 })
95 };
96 if field(pointer, 5)? != ObjectType::XlibName.prolog() {
97 return None;
98 }
99 (field(pointer + 5, 3)? == u32::from(lib)).then(|| field(pointer + 8, 3))?
100 }
101
102 pub fn iter(&self) -> impl Iterator<Item = (u32, &MenuDef)> {
104 self.menus
105 .iter()
106 .enumerate()
107 .filter_map(|(n, d)| Some((u32::try_from(n).ok()?, d.as_ref()?)))
108 }
109
110 pub fn paths(&self, roots: &[(u32, String)]) -> BTreeMap<u32, String> {
116 let mut out: BTreeMap<u32, String> = BTreeMap::new();
117 let mut queue: VecDeque<u32> = VecDeque::new();
118 for (n, name) in roots {
119 if self.get(*n).is_some() && !out.contains_key(n) {
120 out.insert(*n, name.clone());
121 queue.push_back(*n);
122 }
123 }
124 self.descend(&mut out, &mut queue);
125 let unnamed: Vec<u32> = self
126 .iter()
127 .map(|(n, _)| n)
128 .filter(|n| !out.contains_key(n))
129 .collect();
130 for n in unnamed {
131 if let std::collections::btree_map::Entry::Vacant(e) = out.entry(n) {
132 e.insert(format!("MENU {n}"));
133 queue.push_back(n);
134 self.descend(&mut out, &mut queue);
135 }
136 }
137 out
138 }
139
140 fn descend(&self, out: &mut BTreeMap<u32, String>, queue: &mut VecDeque<u32>) {
142 while let Some(n) = queue.pop_front() {
143 let (Some(def), Some(parent)) = (self.get(n), out.get(&n).cloned()) else {
144 continue;
145 };
146 for (label, sub) in &def.submenus {
147 if out.contains_key(sub) || self.get(*sub).is_none() {
148 continue;
149 }
150 out.insert(*sub, format!("{parent} {label}"));
151 queue.push_back(*sub);
152 }
153 }
154 }
155
156 pub fn placement(&self, paths: &BTreeMap<u32, String>) -> BTreeMap<String, Vec<String>> {
159 let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
160 for (n, def) in self.iter() {
161 let name = paths
162 .get(&n)
163 .cloned()
164 .unwrap_or_else(|| format!("MENU {n}"));
165 for c in &def.commands {
166 let list = out.entry(c.clone()).or_default();
167 if !list.contains(&name) {
168 list.push(name.clone());
169 }
170 }
171 }
172 out
173 }
174}
175
176fn f5(n: &[u8], at: usize) -> Option<u32> {
178 read_field(n, at, 5)
179}
180
181#[derive(Clone, Copy, Debug)]
183enum El {
184 Embedded(usize, u32),
186 Pointer(u32),
188}
189
190#[derive(Clone, Debug)]
192enum Source {
193 List(Vec<El>),
195 Library(u16),
197}
198
199#[derive(Clone, Copy)]
203struct View<'a> {
204 rom: &'a [u8],
205 bank: Option<usize>,
206 low: usize,
207 high: usize,
208}
209
210impl View<'_> {
211 fn index(&self, a: u32) -> Option<usize> {
213 let a = a as usize;
214 let i = match self.bank {
215 None => a,
216 Some(b) if a < b => self.low * b + a,
217 Some(b) if a < 2 * b => self.high * b + (a - b),
218 Some(_) => return None,
219 };
220 (i < self.rom.len()).then_some(i)
221 }
222
223 fn at(&self, i: usize) -> Self {
226 match self.bank {
227 Some(b) => View {
228 high: i / b,
229 ..*self
230 },
231 None => *self,
232 }
233 }
234}
235
236impl Memory for View<'_> {
237 fn nibble(&self, addr: u32) -> Option<u8> {
238 self.rom.get(self.index(addr)?).copied()
239 }
240}
241
242struct Reader<'a> {
243 rom: &'a [u8],
244 names: &'a NameTable,
245 view: View<'a>,
246 steps: Cell<usize>,
247 max_steps: usize,
249}
250
251impl<'a> Reader<'a> {
252 fn step(&self) -> bool {
254 let s = self.steps.get();
255 if s >= self.max_steps {
256 return false;
257 }
258 self.steps.set(s + 1);
259 true
260 }
261
262 fn prolog(&self, i: usize) -> Option<u32> {
263 f5(self.rom, i)
264 }
265
266 fn elements(&self, i: usize) -> Vec<El> {
268 let mut out = Vec::new();
269 let mut at = i + 5;
270 while out.len() < MAX_ELEMENTS && self.step() {
271 let Some(p) = f5(self.rom, at) else { break };
272 if p == SEMI {
273 break;
274 }
275 if ObjectType::from_prolog(p).is_some() {
276 let Ok(size) = object_size(self.rom, at) else {
277 break;
278 };
279 out.push(El::Embedded(at, p));
280 at += size;
281 } else if (0x02600..=0x02FFF).contains(&p) {
282 break;
283 } else {
284 out.push(El::Pointer(p));
285 at += 5;
286 }
287 }
288 out
289 }
290
291 fn xlib_at(&self, i: usize) -> Option<(u16, u16)> {
293 Some((
294 read_field(self.rom, i, 3)? as u16,
295 read_field(self.rom, i + 3, 3)? as u16,
296 ))
297 }
298
299 fn command(&self, e: El, view: View<'_>) -> Option<String> {
301 match e {
302 El::Embedded(i, p) if p == ObjectType::XlibName.prolog() => {
303 let (l, c) = self.xlib_at(i + 5)?;
304 self.names.xlib(l, c)?.name.map(str::to_string)
305 }
306 El::Pointer(a) => self.names.command_at(a, &view)?.name.map(str::to_string),
307 El::Embedded(..) => None,
308 }
309 }
310
311 fn target(&self, e: El, view: View<'a>) -> Option<(usize, View<'a>)> {
314 match e {
315 El::Embedded(i, p) if p == ObjectType::XlibName.prolog() => {
316 let (l, c) = self.xlib_at(i + 5)?;
317 let t = self.names.object_index(l, c)?;
318 Some((t, view.at(t)))
319 }
320 El::Embedded(i, _) => Some((i, view)),
321 El::Pointer(a) => Some((view.index(a)?, view)),
322 }
323 }
324
325 fn xlib_array(&self, i: usize) -> Option<Vec<(u16, u16)>> {
327 if self.prolog(i)? != ObjectType::Array.prolog()
328 || f5(self.rom, i + 10)? != ObjectType::XlibName.prolog()
329 || f5(self.rom, i + 15)? != 1
330 {
331 return None;
332 }
333 let count = f5(self.rom, i + 20)? as usize;
334 if count > MAX_ELEMENTS {
335 return None;
336 }
337 (0..count).map(|k| self.xlib_at(i + 25 + 6 * k)).collect()
338 }
339}
340
341struct Collect<'r, 'a> {
343 r: &'r Reader<'a>,
344 source: &'r Source,
345 defs: HashMap<usize, u32>,
347 def: MenuDef,
348}
349
350impl<'a> Collect<'_, 'a> {
351 fn add_command(&mut self, name: String) {
352 if !self.def.commands.contains(&name) {
353 self.def.commands.push(name);
354 }
355 }
356
357 fn menu_ref(&self, e: El, view: View<'a>) -> Option<u32> {
361 match (e, self.source) {
362 (El::Embedded(i, p), Source::Library(lib)) if p == ObjectType::XlibName.prolog() => {
363 let (l, c) = self.r.xlib_at(i + 5)?;
364 (l == *lib).then_some(u32::from(c))
365 }
366 (El::Pointer(a), Source::List(_)) => self.defs.get(&view.index(a)?).copied(),
367 (El::Embedded(i, _), Source::List(_)) => self.defs.get(&i).copied(),
368 _ => None,
369 }
370 }
371
372 fn find_menu(&self, i: usize, view: View<'a>, depth: usize) -> Option<u32> {
375 if depth > MAX_NESTING || !composite(self.r.prolog(i)?) {
376 return None;
377 }
378 for e in self.r.elements(i) {
379 if let Some(n) = self.menu_ref(e, view) {
380 return Some(n);
381 }
382 if let El::Embedded(j, p) = e
383 && composite(p)
384 && let Some(n) = self.find_menu(j, view, depth + 1)
385 {
386 return Some(n);
387 }
388 }
389 None
390 }
391
392 fn commands_in(&mut self, i: usize, view: View<'a>, depth: usize) {
395 if depth > MAX_NESTING {
396 return;
397 }
398 let Some(p) = self.r.prolog(i) else { return };
399 if p == ObjectType::XlibName.prolog() {
400 if let Some(name) = self.r.command(El::Embedded(i, p), view) {
401 self.add_command(name);
402 }
403 return;
404 }
405 if !composite(p) {
406 return;
407 }
408 for e in self.r.elements(i) {
409 if let Some(name) = self.r.command(e, view) {
410 self.add_command(name);
411 } else if let El::Embedded(j, q) = e
412 && composite(q)
413 {
414 self.commands_in(j, view, depth + 1);
415 }
416 }
417 }
418
419 fn label(&self, e: El, view: View<'a>, depth: usize) -> Option<String> {
422 if let Some(name) = self.r.command(e, view) {
423 return Some(name);
424 }
425 let (i, view) = self.r.target(e, view)?;
426 let p = self.r.prolog(i)?;
427 if p == ObjectType::String.prolog() {
428 let len = (f5(self.r.rom, i + 5)? as usize).checked_sub(5)? / 2;
429 let bytes: Option<Vec<u8>> = (0..len)
430 .map(|k| read_field(self.r.rom, i + 10 + 2 * k, 2).map(|b| b as u8))
431 .collect();
432 let text: String = charset::decode(&bytes?)
435 .chars()
436 .filter(|c| !c.is_control())
437 .collect();
438 return Some(text.trim().to_string()).filter(|t| !t.is_empty());
439 }
440 if p == ObjectType::Program.prolog() && depth < 2 {
441 return self
442 .r
443 .elements(i)
444 .into_iter()
445 .find_map(|x| self.label(x, view, depth + 1));
446 }
447 None
448 }
449
450 fn key(&mut self, e: El, view: View<'a>, depth: usize) {
452 if depth > MAX_NESTING || !self.r.step() {
453 return;
454 }
455 if let Some(name) = self.r.command(e, view) {
456 self.add_command(name);
457 return;
458 }
459 let Some((i, view)) = self.r.target(e, view) else {
460 return;
461 };
462 let Some(p) = self.r.prolog(i) else { return };
463 if p == ObjectType::List.prolog() {
464 let els = self.r.elements(i);
465 if let [only] = els.as_slice() {
471 self.key(*only, view, depth + 1);
472 return;
473 }
474 let labelled = els
475 .split_first()
476 .and_then(|(&first, rest)| Some((self.label(first, view, 0)?, rest)));
477 if let Some((label, rest)) = labelled {
478 let submenu = rest.iter().find_map(|&a| {
482 self.menu_ref(a, view).or_else(|| {
483 let (j, v) = self.r.target(a, view)?;
484 self.find_menu(j, v, depth + 1)
485 })
486 });
487 if let Some(n) = submenu {
488 self.def.submenus.push((label, n));
489 return;
490 }
491 for &a in rest {
492 if let Some(name) = self.r.command(a, view) {
493 self.add_command(name);
494 } else if let Some((j, v)) = self.r.target(a, view) {
495 self.commands_in(j, v, depth + 1);
496 }
497 }
498 return;
499 }
500 self.commands_in(i, view, depth + 1);
501 } else if p == ObjectType::Program.prolog() {
502 self.commands_in(i, view, depth + 1);
503 }
504 }
505
506 fn definition(&mut self, i: usize, view: View<'a>, depth: usize) {
508 if depth > 3 {
509 return;
510 }
511 let Some(p) = self.r.prolog(i) else { return };
512 if let Some(keys) = self.r.xlib_array(i) {
513 for (l, c) in keys {
514 match self.r.names.xlib(l, c).and_then(|x| x.name) {
516 Some(name) => self.add_command(name.to_string()),
517 None => {
518 if let Some(t) = self.r.names.object_index(l, c) {
519 let v = view.at(t);
520 self.key_object(t, v, depth + 1);
521 }
522 }
523 }
524 }
525 } else if p == ObjectType::XlibName.prolog() {
526 if let Some((t, v)) = self.r.target(El::Embedded(i, p), view) {
527 self.definition(t, v, depth + 1);
528 }
529 } else if p == ObjectType::List.prolog() {
530 for e in self.r.elements(i) {
531 self.key(e, view, 1);
532 }
533 } else if p == ObjectType::Program.prolog() {
534 if let Some(El::Embedded(j, _)) = self
536 .r
537 .elements(i)
538 .into_iter()
539 .find(|e| matches!(e, El::Embedded(_, q) if *q == ObjectType::List.prolog()))
540 {
541 self.definition(j, view, depth + 1);
542 }
543 }
544 }
545
546 fn key_object(&mut self, t: usize, view: View<'a>, depth: usize) {
549 let p = self.r.prolog(t);
550 if p == Some(ObjectType::List.prolog()) || p == Some(ObjectType::Program.prolog()) {
551 self.key(El::Embedded(t, p.unwrap_or(0)), view, depth);
552 }
553 }
554}
555
556fn composite(p: u32) -> bool {
557 p == ObjectType::List.prolog() || p == ObjectType::Program.prolog()
558}
559
560pub fn read(rom: &[u8], names: &NameTable) -> Option<Menus> {
566 read_within(rom, names, MAX_STEPS)
567}
568
569fn read_within(rom: &[u8], names: &NameTable, max_steps: usize) -> Option<Menus> {
571 let (lib, cmd) = names.find("MENU")?;
572 let menu = names.object_index(lib, cmd)?;
573 let bank = names.bank();
574 let low = bank.map_or(0, |b| menu / b);
575 let view = View {
576 rom,
577 bank,
578 low,
579 high: low,
580 };
581 let r = Reader {
582 rom,
583 names,
584 view,
585 steps: Cell::new(0),
586 max_steps,
587 };
588 let source = locate(&r, menu)?;
589 let count = match &source {
590 Source::List(els) => els.len() + 1,
591 Source::Library(l) => names.library_size(*l)?.0,
592 };
593 let mut c = Collect {
594 r: &r,
595 source: &source,
596 defs: HashMap::new(),
597 def: MenuDef::default(),
598 };
599 if let Source::List(els) = &source {
600 for (k, &e) in els.iter().enumerate() {
601 if let Some((i, _)) = r.target(e, view) {
602 c.defs.insert(i, u32::try_from(k + 1).ok()?);
603 }
604 }
605 }
606 let mut addresses = HashMap::new();
607 if let Source::List(els) = &source {
608 for (k, &e) in els.iter().enumerate() {
609 let a = match e {
610 El::Pointer(a) => Some(a),
611 El::Embedded(i, _) => u32::try_from(i).ok().filter(|_| bank.is_none()),
614 };
615 if let (Some(a), Ok(n)) = (a, u32::try_from(k + 1)) {
616 addresses.insert(a, n);
617 }
618 }
619 }
620 let library = match &source {
621 Source::Library(l) => Some(*l),
622 Source::List(_) => None,
623 };
624 let mut menus = vec![None; count];
625 for (n, slot) in menus.iter_mut().enumerate() {
626 let at = match &source {
627 Source::List(els) => n.checked_sub(1).and_then(|k| els.get(k).copied()),
629 Source::Library(l) => names
630 .object_index(*l, u16::try_from(n).ok()?)
631 .and_then(|t| f5(rom, t).map(|p| El::Embedded(t, p))),
632 };
633 let Some(at) = at else { continue };
634 let Some((i, v)) = r.target(at, view) else {
635 continue;
636 };
637 c.def = MenuDef::default();
638 c.definition(i, v.at(i), 0);
639 *slot = Some(std::mem::take(&mut c.def));
640 }
641 Some(Menus {
642 menus,
643 library,
644 addresses,
645 truncated: r.steps.get() >= max_steps,
646 })
647}
648
649fn locate(r: &Reader<'_>, menu: usize) -> Option<Source> {
654 let mut best: Option<(usize, Source)> = None;
655 let mut seen = std::collections::HashSet::new();
656 let mut queue: VecDeque<(usize, usize)> = VecDeque::from([(menu, 0)]);
657 let view = r.view;
658 while let Some((i, depth)) = queue.pop_front() {
659 if depth > MAX_CODE_DEPTH || !seen.insert(i) || !r.step() {
660 continue;
661 }
662 if r.prolog(i) != Some(ObjectType::Program.prolog()) {
663 continue;
664 }
665 for e in r.elements(i) {
666 let (t, embedded) = match e {
667 El::Embedded(j, _) => (j, true),
668 El::Pointer(a) => {
669 if r.names.command_at(a, &view).is_some() {
670 continue;
671 }
672 let Some(t) = view.index(a) else { continue };
673 (t, false)
674 }
675 };
676 let Some(p) = r.prolog(t) else { continue };
677 if p == ObjectType::Program.prolog() {
678 queue.push_back((t, if embedded { depth } else { depth + 1 }));
679 } else if p == ObjectType::List.prolog() {
680 let els = r.elements(t);
681 let menus = els
682 .iter()
683 .filter(|&&x| {
684 r.target(x, view)
685 .and_then(|(j, _)| r.prolog(j))
686 .is_some_and(composite)
687 })
688 .count();
689 if els.len() >= MIN_MENUS && menus * 5 >= els.len() * 4 {
690 consider(&mut best, menus, Source::List(els));
691 }
692 } else if p == ObjectType::SystemBinary.prolog() {
693 let Some(v) = f5(r.rom, t + 5).and_then(|v| u16::try_from(v).ok()) else {
694 continue;
695 };
696 let Some((len, named)) = r.names.library_size(v) else {
697 continue;
698 };
699 if len < MIN_MENUS || named > 0 {
700 continue;
701 }
702 let menus = (0..len)
703 .filter(|&c| {
704 u16::try_from(c)
705 .ok()
706 .and_then(|c| r.names.object_index(v, c))
707 .is_some_and(|j| {
708 r.xlib_array(j).is_some()
709 || r.prolog(j) == Some(ObjectType::List.prolog())
710 })
711 })
712 .count();
713 if menus * 2 >= len {
714 consider(&mut best, menus, Source::Library(v));
715 }
716 }
717 }
718 }
719 best.map(|(_, s)| s)
720}
721
722fn consider(best: &mut Option<(usize, Source)>, score: usize, s: Source) {
723 if best.as_ref().is_none_or(|(b, _)| score > *b) {
724 *best = Some((score, s));
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731 use saturnus::Model;
732
733 fn put(n: &mut [u8], at: usize, value: usize, width: usize) {
734 for i in 0..width {
735 n[at + i] = ((value >> (4 * i)) & 0xF) as u8;
736 }
737 }
738
739 fn put_rel(n: &mut [u8], at: usize, target: usize) {
740 put(n, at, (target + 0x100000 - at) & 0xFFFFF, 5);
741 }
742
743 fn put_hex(n: &mut [u8], at: usize, hex: &str) -> usize {
744 for (i, c) in hex.chars().enumerate() {
745 n[at + i] = c.to_digit(16).unwrap() as u8;
746 }
747 at + hex.len()
748 }
749
750 fn ptr(a: usize) -> String {
751 (0..5)
752 .map(|i| format!("{:X}", (a >> (4 * i)) & 0xF))
753 .collect()
754 }
755
756 fn xlib(lib: usize, cmd: usize) -> String {
757 format!("29E20{}", body(lib, cmd))
758 }
759
760 fn body(lib: usize, cmd: usize) -> String {
761 (0..3)
762 .map(|i| format!("{:X}", (lib >> (4 * i)) & 0xF))
763 .chain((0..3).map(|i| format!("{:X}", (cmd >> (4 * i)) & 0xF)))
764 .collect()
765 }
766
767 fn string(s: &str) -> String {
768 let mut h = format!("C2A20{}", ptr(5 + 2 * s.len()));
769 for b in s.bytes() {
770 h.push_str(&format!("{:X}{:X}", b & 0xF, b >> 4));
771 }
772 h
773 }
774
775 fn library(
779 n: &mut [u8],
780 base: usize,
781 id: usize,
782 names: &[Option<&str>],
783 objects: &[String],
784 ) -> Vec<usize> {
785 let (hash, link) = (base + 0x30, base + 0x300);
786 let body_at = hash + 10;
787 let mut at = body_at + 85;
788 let mut entries = vec![0; names.len()];
789 for len in 1..=16 {
790 for (c, name) in names.iter().enumerate() {
791 let Some(name) = name else { continue };
792 if name.len() != len {
793 continue;
794 }
795 if read_field(n, body_at + 5 * (len - 1), 5) == Some(0) {
796 put_rel(n, body_at + 5 * (len - 1), at);
797 }
798 entries[c] = at;
799 put(n, at, len, 2);
800 for (i, b) in name.bytes().enumerate() {
801 put(n, at + 2 + 2 * i, usize::from(b), 2);
802 }
803 put(n, at + 2 + 2 * len, c, 3);
804 at += 2 + 2 * len + 3;
805 }
806 }
807 put_rel(n, body_at + 80, at);
808 for (c, &e) in entries.iter().enumerate() {
809 let f = at + 5 * c;
810 put(n, f, if e == 0 { 0 } else { f - e }, 5);
811 }
812 let end = at + 5 * names.len();
813 assert!(end < link);
814 put(n, hash, 0x02A4E, 5);
815 put(n, hash + 5, end - hash - 5, 5);
816 put(n, link, 0x02A4E, 5);
817 put(n, link + 5, 5 + 5 * names.len(), 5);
818 let mut addrs = Vec::new();
819 for (c, o) in objects.iter().enumerate() {
820 let obj = base + 0x400 + 0x100 * c;
821 put_hex(n, obj - 6, &body(id, c));
822 assert!(o.len() < 0xF0);
823 put_hex(n, obj, o);
824 put_rel(n, link + 10 + 5 * c, obj);
825 addrs.push(obj);
826 }
827 put(n, base, id, 3);
828 put_rel(n, base + 3, hash);
829 put_rel(n, base + 13, link);
830 addrs
831 }
832
833 fn builtins(n: &mut [u8], menu_body: &str) -> Vec<usize> {
836 let prog = "D9D20B2130".to_string();
837 library(
838 n,
839 0x100,
840 2,
841 &[Some("MENU"), Some("SIN"), Some("COS"), Some("TAN")],
842 &[
843 format!("D9D20{menu_body}B2130"),
844 prog.clone(),
845 prog.clone(),
846 prog,
847 ],
848 )
849 }
850
851 #[test]
852 fn a_list_of_definitions_as_on_the_48sx() {
853 let mut n = vec![0u8; 0x8000];
854 let list = 0x4000;
855 let a = builtins(&mut n, &ptr(list));
856 let (sin, cos, tan) = (a[1], a[2], a[3]);
857 let mut at = put_hex(&mut n, list, "47A20");
860 let def1 = |m2: usize| {
861 format!(
862 "47A20{}47A20{}{}B2130B2130",
863 ptr(sin),
864 string("TRIG"),
865 ptr(m2)
866 )
867 };
868 let m2_at = at + def1(0).len();
869 at = put_hex(&mut n, at, &def1(m2_at));
870 at = put_hex(
871 &mut n,
872 at,
873 &format!(
874 "47A20{}{}47A20{}{}B2130B2130",
875 ptr(cos),
876 ptr(tan),
877 string("T"),
878 ptr(tan)
879 ),
880 );
881 for _ in 3..=24 {
882 at = put_hex(&mut n, at, "47A20B2130");
883 }
884 put_hex(&mut n, at, "B2130");
885 let names = NameTable::build(Model::Hp48sx, &n);
886 let m = read(&n, &names).unwrap();
887 assert_eq!(m.len(), 25);
888 assert_eq!(m.get(1).unwrap().commands, ["SIN"]);
889 assert_eq!(m.get(1).unwrap().submenus, [("TRIG".to_string(), 2)]);
890 assert_eq!(m.get(2).unwrap().commands, ["COS", "TAN"]);
891 let paths = m.paths(&[(1, "MATH".into())]);
892 assert_eq!(paths[&2], "MATH TRIG");
893 assert_eq!(paths[&3], "MENU 3");
894 let p = m.placement(&paths);
895 assert_eq!(p["TAN"], ["MATH TRIG"]);
896 assert_eq!(p["SIN"], ["MATH"]);
897 struct Image<'a>(&'a [u8]);
899 impl Memory for Image<'_> {
900 fn nibble(&self, a: u32) -> Option<u8> {
901 self.0.get(a as usize).copied()
902 }
903 }
904 assert_eq!(m.number(m2_at as u32, &Image(&n)), Some(2));
905 }
906
907 #[test]
908 fn a_menu_library_as_on_the_48gx() {
909 let mut n = vec![0u8; 0x10000];
910 builtins(&mut n, "119209A000");
912 let keys = library(
914 &mut n,
915 0x8000,
916 0xA8,
917 &[None, None],
918 &[
919 format!("47A20{}D9D20{}B2130B2130", string("SUBM"), xlib(0xA9, 2)),
920 format!("47A20{}{}B2130", string("C"), xlib(2, 2)),
921 ],
922 );
923 let _ = keys;
924 let array = |items: &[String]| {
925 let body: String = items.iter().map(|x| x[5..].to_string()).collect();
926 format!(
927 "8E920{}29E2010000{}{}",
928 ptr(25 + 6 * items.len() - 5),
929 ptr(items.len()),
930 body
931 )
932 };
933 let mut defs = vec!["47A20B2130".to_string(); 22];
934 defs[1] = array(&[xlib(2, 1), xlib(0xA8, 0)]);
935 defs[2] = array(&[xlib(0xA8, 1), xlib(2, 3)]);
936 library(&mut n, 0xA000, 0xA9, &vec![None; 22], &defs);
937 let names = NameTable::build(Model::Hp48gx, &n);
938 let m = read(&n, &names).unwrap();
939 assert_eq!(m.len(), 22);
940 assert_eq!(m.get(1).unwrap().commands, ["SIN"]);
941 assert_eq!(m.get(1).unwrap().submenus, [("SUBM".to_string(), 2)]);
942 assert_eq!(m.get(2).unwrap().commands, ["COS", "TAN"]);
943 let mut ram = n.clone();
945 put_hex(&mut ram, 0xF000, &xlib(0xA9, 2));
946 struct Image<'a>(&'a [u8]);
947 impl Memory for Image<'_> {
948 fn nibble(&self, a: u32) -> Option<u8> {
949 self.0.get(a as usize).copied()
950 }
951 }
952 assert_eq!(m.number(0xF000, &Image(&ram)), Some(2));
953 }
954
955 #[test]
956 fn single_object_keys_and_labels() {
957 let mut n = vec![0u8; 0x8000];
958 let list = 0x4000;
959 let a = builtins(&mut n, &ptr(list));
960 let (sin, cos, tan) = (a[1], a[2], a[3]);
961 let mut at = put_hex(&mut n, list, "47A20");
963 at = put_hex(
964 &mut n,
965 at,
966 &format!(
967 "47A2047A20{}{}B213047A20{}B2130B2130",
968 ptr(sin),
969 ptr(cos),
970 ptr(tan)
971 ),
972 );
973 for _ in 2..=24 {
974 at = put_hex(&mut n, at, "47A20B2130");
975 }
976 put_hex(&mut n, at, "B2130");
977 let names = NameTable::build(Model::Hp48sx, &n);
978 let m = read(&n, &names).unwrap();
979 assert_eq!(m.get(1).unwrap().commands, ["COS", "TAN"]);
980 assert!(!m.truncated());
981 let short = read_within(&n, &names, 30).unwrap();
983 assert!(short.truncated());
984 }
985
986 #[test]
987 fn garbage_finds_no_menus() {
988 let mut n = vec![0u8; 0x8000];
989 for (i, x) in n.iter_mut().enumerate() {
990 *x = ((i * 7919) >> 3) as u8 & 0xF;
991 }
992 let names = NameTable::build(Model::Hp48sx, &n);
993 assert!(read(&n, &names).is_none());
994 let mut n = vec![0u8; 0x8000];
996 let a = builtins(&mut n, &format!("{}{}", ptr(0x500), ptr(0x7FF0)));
997 let _ = a;
998 let names = NameTable::build(Model::Hp48sx, &n);
999 assert!(read(&n, &names).is_none());
1000 assert!(read(&n, &NameTable::default()).is_none());
1001 }
1002}