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 pub keys: Vec<MenuKey>,
51}
52
53#[derive(Clone, Debug, Default, PartialEq, Eq)]
55pub struct MenuKey {
56 pub label: Option<String>,
59 pub command: Option<String>,
61 pub submenu: Option<u32>,
63}
64
65impl MenuKey {
66 pub fn text(&self) -> Option<&str> {
68 self.label.as_deref().or(self.command.as_deref())
69 }
70}
71
72#[derive(Clone, Debug, Default, PartialEq, Eq)]
74pub struct Menus {
75 menus: Vec<Option<MenuDef>>,
77 library: Option<u16>,
79 addresses: HashMap<u32, u32>,
81 truncated: bool,
84}
85
86impl Menus {
87 pub fn len(&self) -> usize {
89 self.menus.len()
90 }
91
92 pub fn truncated(&self) -> bool {
95 self.truncated
96 }
97
98 pub fn is_empty(&self) -> bool {
100 self.menus.is_empty()
101 }
102
103 pub fn get(&self, n: u32) -> Option<&MenuDef> {
105 self.menus.get(n as usize)?.as_ref()
106 }
107
108 pub fn number(&self, pointer: u32, mem: &dyn Memory) -> Option<u32> {
113 if let Some(n) = self.addresses.get(&pointer) {
114 return Some(*n);
115 }
116 let lib = self.library?;
117 let field = |at: u32, w: u32| -> Option<u32> {
118 (0..w).rev().try_fold(0u32, |v, i| {
119 Some((v << 4) | u32::from(mem.nibble(at.checked_add(i)?)?))
120 })
121 };
122 if field(pointer, 5)? != ObjectType::XlibName.prolog() {
123 return None;
124 }
125 (field(pointer + 5, 3)? == u32::from(lib)).then(|| field(pointer + 8, 3))?
126 }
127
128 pub fn iter(&self) -> impl Iterator<Item = (u32, &MenuDef)> {
130 self.menus
131 .iter()
132 .enumerate()
133 .filter_map(|(n, d)| Some((u32::try_from(n).ok()?, d.as_ref()?)))
134 }
135
136 pub fn paths(&self, roots: &[(u32, String)]) -> BTreeMap<u32, String> {
142 let mut out: BTreeMap<u32, String> = BTreeMap::new();
143 let mut queue: VecDeque<u32> = VecDeque::new();
144 for (n, name) in roots {
145 if self.get(*n).is_some() && !out.contains_key(n) {
146 out.insert(*n, name.clone());
147 queue.push_back(*n);
148 }
149 }
150 self.descend(&mut out, &mut queue);
151 let unnamed: Vec<u32> = self
152 .iter()
153 .map(|(n, _)| n)
154 .filter(|n| !out.contains_key(n))
155 .collect();
156 for n in unnamed {
157 if let std::collections::btree_map::Entry::Vacant(e) = out.entry(n) {
158 e.insert(format!("MENU {n}"));
159 queue.push_back(n);
160 self.descend(&mut out, &mut queue);
161 }
162 }
163 out
164 }
165
166 fn descend(&self, out: &mut BTreeMap<u32, String>, queue: &mut VecDeque<u32>) {
168 while let Some(n) = queue.pop_front() {
169 let (Some(def), Some(parent)) = (self.get(n), out.get(&n).cloned()) else {
170 continue;
171 };
172 for (label, sub) in &def.submenus {
173 if out.contains_key(sub) || self.get(*sub).is_none() {
174 continue;
175 }
176 out.insert(*sub, format!("{parent} {label}"));
177 queue.push_back(*sub);
178 }
179 }
180 }
181
182 pub fn placement(&self, paths: &BTreeMap<u32, String>) -> BTreeMap<String, Vec<String>> {
185 let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
186 for (n, def) in self.iter() {
187 let name = paths
188 .get(&n)
189 .cloned()
190 .unwrap_or_else(|| format!("MENU {n}"));
191 for c in &def.commands {
192 let list = out.entry(c.clone()).or_default();
193 if !list.contains(&name) {
194 list.push(name.clone());
195 }
196 }
197 }
198 out
199 }
200}
201
202fn f5(n: &[u8], at: usize) -> Option<u32> {
204 read_field(n, at, 5)
205}
206
207#[derive(Clone, Copy, Debug)]
209enum El {
210 Embedded(usize, u32),
212 Pointer(u32),
214}
215
216#[derive(Clone, Debug)]
218enum Source {
219 List(Vec<El>),
221 Library(u16),
223}
224
225#[derive(Clone, Copy)]
229struct View<'a> {
230 rom: &'a [u8],
231 bank: Option<usize>,
232 low: usize,
233 high: usize,
234}
235
236impl View<'_> {
237 fn index(&self, a: u32) -> Option<usize> {
239 let a = a as usize;
240 let i = match self.bank {
241 None => a,
242 Some(b) if a < b => self.low * b + a,
243 Some(b) if a < 2 * b => self.high * b + (a - b),
244 Some(_) => return None,
245 };
246 (i < self.rom.len()).then_some(i)
247 }
248
249 fn at(&self, i: usize) -> Self {
252 match self.bank {
253 Some(b) => View {
254 high: i / b,
255 ..*self
256 },
257 None => *self,
258 }
259 }
260}
261
262impl Memory for View<'_> {
263 fn nibble(&self, addr: u32) -> Option<u8> {
264 self.rom.get(self.index(addr)?).copied()
265 }
266}
267
268struct Reader<'a> {
269 rom: &'a [u8],
270 names: &'a NameTable,
271 view: View<'a>,
272 steps: Cell<usize>,
273 max_steps: usize,
275}
276
277impl<'a> Reader<'a> {
278 fn step(&self) -> bool {
280 let s = self.steps.get();
281 if s >= self.max_steps {
282 return false;
283 }
284 self.steps.set(s + 1);
285 true
286 }
287
288 fn prolog(&self, i: usize) -> Option<u32> {
289 f5(self.rom, i)
290 }
291
292 fn elements(&self, i: usize) -> Vec<El> {
294 let mut out = Vec::new();
295 let mut at = i + 5;
296 while out.len() < MAX_ELEMENTS && self.step() {
297 let Some(p) = f5(self.rom, at) else { break };
298 if p == SEMI {
299 break;
300 }
301 if ObjectType::from_prolog(p).is_some() {
302 let Ok(size) = object_size(self.rom, at) else {
303 break;
304 };
305 out.push(El::Embedded(at, p));
306 at += size;
307 } else if (0x02600..=0x02FFF).contains(&p) {
308 break;
309 } else {
310 out.push(El::Pointer(p));
311 at += 5;
312 }
313 }
314 out
315 }
316
317 fn xlib_at(&self, i: usize) -> Option<(u16, u16)> {
319 Some((
320 read_field(self.rom, i, 3)? as u16,
321 read_field(self.rom, i + 3, 3)? as u16,
322 ))
323 }
324
325 fn command(&self, e: El, view: View<'_>) -> Option<String> {
327 match e {
328 El::Embedded(i, p) if p == ObjectType::XlibName.prolog() => {
329 let (l, c) = self.xlib_at(i + 5)?;
330 self.names.xlib(l, c)?.name.map(str::to_string)
331 }
332 El::Pointer(a) => self.names.command_at(a, &view)?.name.map(str::to_string),
333 El::Embedded(..) => None,
334 }
335 }
336
337 fn target(&self, e: El, view: View<'a>) -> Option<(usize, View<'a>)> {
340 match e {
341 El::Embedded(i, p) if p == ObjectType::XlibName.prolog() => {
342 let (l, c) = self.xlib_at(i + 5)?;
343 let t = self.names.object_index(l, c)?;
344 Some((t, view.at(t)))
345 }
346 El::Embedded(i, _) => Some((i, view)),
347 El::Pointer(a) => Some((view.index(a)?, view)),
348 }
349 }
350
351 fn xlib_array(&self, i: usize) -> Option<Vec<(u16, u16)>> {
353 if self.prolog(i)? != ObjectType::Array.prolog()
354 || f5(self.rom, i + 10)? != ObjectType::XlibName.prolog()
355 || f5(self.rom, i + 15)? != 1
356 {
357 return None;
358 }
359 let count = f5(self.rom, i + 20)? as usize;
360 if count > MAX_ELEMENTS {
361 return None;
362 }
363 (0..count).map(|k| self.xlib_at(i + 25 + 6 * k)).collect()
364 }
365}
366
367struct Collect<'r, 'a> {
369 r: &'r Reader<'a>,
370 source: &'r Source,
371 defs: HashMap<usize, u32>,
373 def: MenuDef,
374 slot: Option<Slot>,
377}
378
379#[derive(Default)]
381struct Slot {
382 label: Option<String>,
383 commands: Vec<String>,
384 submenu: Option<u32>,
385}
386
387impl<'a> Collect<'_, 'a> {
388 fn add_command(&mut self, name: String) {
389 if let Some(slot) = self.slot.as_mut()
390 && !slot.commands.contains(&name)
391 {
392 slot.commands.push(name.clone());
393 }
394 if !self.def.commands.contains(&name) {
395 self.def.commands.push(name);
396 }
397 }
398
399 fn menu_ref(&self, e: El, view: View<'a>) -> Option<u32> {
403 match (e, self.source) {
404 (El::Embedded(i, p), Source::Library(lib)) if p == ObjectType::XlibName.prolog() => {
405 let (l, c) = self.r.xlib_at(i + 5)?;
406 (l == *lib).then_some(u32::from(c))
407 }
408 (El::Pointer(a), Source::List(_)) => self.defs.get(&view.index(a)?).copied(),
409 (El::Embedded(i, _), Source::List(_)) => self.defs.get(&i).copied(),
410 _ => None,
411 }
412 }
413
414 fn find_menu(&self, i: usize, view: View<'a>, depth: usize) -> Option<u32> {
417 if depth > MAX_NESTING || !composite(self.r.prolog(i)?) {
418 return None;
419 }
420 for e in self.r.elements(i) {
421 if let Some(n) = self.menu_ref(e, view) {
422 return Some(n);
423 }
424 if let El::Embedded(j, p) = e
425 && composite(p)
426 && let Some(n) = self.find_menu(j, view, depth + 1)
427 {
428 return Some(n);
429 }
430 }
431 None
432 }
433
434 fn commands_in(&mut self, i: usize, view: View<'a>, depth: usize) {
437 if depth > MAX_NESTING {
438 return;
439 }
440 let Some(p) = self.r.prolog(i) else { return };
441 if p == ObjectType::XlibName.prolog() {
442 if let Some(name) = self.r.command(El::Embedded(i, p), view) {
443 self.add_command(name);
444 }
445 return;
446 }
447 if !composite(p) {
448 return;
449 }
450 for e in self.r.elements(i) {
451 if let Some(name) = self.r.command(e, view) {
452 self.add_command(name);
453 } else if let El::Embedded(j, q) = e
454 && composite(q)
455 {
456 self.commands_in(j, view, depth + 1);
457 }
458 }
459 }
460
461 fn label(&self, e: El, view: View<'a>, depth: usize) -> Option<String> {
464 if let Some(name) = self.r.command(e, view) {
465 return Some(name);
466 }
467 let (i, view) = self.r.target(e, view)?;
468 let p = self.r.prolog(i)?;
469 if p == ObjectType::String.prolog() {
470 let len = (f5(self.r.rom, i + 5)? as usize).checked_sub(5)? / 2;
471 let bytes: Option<Vec<u8>> = (0..len)
472 .map(|k| read_field(self.r.rom, i + 10 + 2 * k, 2).map(|b| b as u8))
473 .collect();
474 let text: String = charset::decode(&bytes?)
477 .chars()
478 .filter(|c| !c.is_control())
479 .collect();
480 return Some(text.trim().to_string()).filter(|t| !t.is_empty());
481 }
482 if p == ObjectType::Program.prolog() && depth < 2 {
483 return self
484 .r
485 .elements(i)
486 .into_iter()
487 .find_map(|x| self.label(x, view, depth + 1));
488 }
489 None
490 }
491
492 fn key(&mut self, e: El, view: View<'a>, depth: usize) {
494 if depth > MAX_NESTING || !self.r.step() {
495 return;
496 }
497 if let Some(name) = self.r.command(e, view) {
498 self.add_command(name);
499 return;
500 }
501 let Some((i, view)) = self.r.target(e, view) else {
502 return;
503 };
504 let Some(p) = self.r.prolog(i) else { return };
505 if p == ObjectType::List.prolog() {
506 let els = self.r.elements(i);
507 if let [only] = els.as_slice() {
513 self.key(*only, view, depth + 1);
514 return;
515 }
516 let labelled = els
517 .split_first()
518 .and_then(|(&first, rest)| Some((self.label(first, view, 0)?, rest)));
519 if let Some((label, rest)) = labelled {
520 let submenu = rest.iter().find_map(|&a| {
524 self.menu_ref(a, view).or_else(|| {
525 let (j, v) = self.r.target(a, view)?;
526 self.find_menu(j, v, depth + 1)
527 })
528 });
529 if let Some(slot) = self.slot.as_mut()
530 && slot.label.is_none()
531 && slot.submenu.is_none()
532 {
533 slot.label = Some(label.clone());
534 slot.submenu = submenu;
535 }
536 if let Some(n) = submenu {
537 self.def.submenus.push((label, n));
538 return;
539 }
540 for &a in rest {
541 if let Some(name) = self.r.command(a, view) {
542 self.add_command(name);
543 } else if let Some((j, v)) = self.r.target(a, view) {
544 self.commands_in(j, v, depth + 1);
545 }
546 }
547 return;
548 }
549 self.commands_in(i, view, depth + 1);
550 } else if p == ObjectType::Program.prolog() {
551 self.commands_in(i, view, depth + 1);
552 }
553 }
554
555 fn definition(&mut self, i: usize, view: View<'a>, depth: usize) {
557 if depth > 3 {
558 return;
559 }
560 let Some(p) = self.r.prolog(i) else { return };
561 if let Some(keys) = self.r.xlib_array(i) {
562 for (l, c) in keys {
563 self.begin_key();
564 match self.r.names.xlib(l, c).and_then(|x| x.name) {
566 Some(name) => self.add_command(name.to_string()),
567 None => {
568 if let Some(t) = self.r.names.object_index(l, c) {
569 let v = view.at(t);
570 self.key_object(t, v, depth + 1);
571 }
572 }
573 }
574 self.end_key();
575 }
576 } else if p == ObjectType::XlibName.prolog() {
577 if let Some((t, v)) = self.r.target(El::Embedded(i, p), view) {
578 self.definition(t, v, depth + 1);
579 }
580 } else if p == ObjectType::List.prolog() {
581 for e in self.r.elements(i) {
582 self.begin_key();
583 self.key(e, view, 1);
584 self.end_key();
585 }
586 } else if p == ObjectType::Program.prolog() {
587 if let Some(El::Embedded(j, _)) = self
590 .r
591 .elements(i)
592 .into_iter()
593 .find(|e| matches!(e, El::Embedded(_, q) if *q == ObjectType::List.prolog()))
594 {
595 self.definition(j, view, depth + 1);
596 }
597 }
598 }
599
600 fn key_object(&mut self, t: usize, view: View<'a>, depth: usize) {
603 let p = self.r.prolog(t);
604 if p == Some(ObjectType::List.prolog()) || p == Some(ObjectType::Program.prolog()) {
605 self.key(El::Embedded(t, p.unwrap_or(0)), view, depth);
606 }
607 }
608}
609
610impl Collect<'_, '_> {
611 fn begin_key(&mut self) {
613 self.slot = Some(Slot::default());
614 }
615
616 fn end_key(&mut self) {
618 let Some(slot) = self.slot.take() else { return };
619 let command = match slot.commands.as_slice() {
620 [one] => Some(one.clone()),
621 _ => None,
622 };
623 let label = slot.label.filter(|l| Some(l) != command.as_ref());
625 self.def.keys.push(MenuKey {
626 label,
627 command,
628 submenu: slot.submenu,
629 });
630 }
631}
632
633fn composite(p: u32) -> bool {
634 p == ObjectType::List.prolog() || p == ObjectType::Program.prolog()
635}
636
637pub fn read(rom: &[u8], names: &NameTable) -> Option<Menus> {
643 read_within(rom, names, MAX_STEPS)
644}
645
646fn read_within(rom: &[u8], names: &NameTable, max_steps: usize) -> Option<Menus> {
648 let (lib, cmd) = names.find("MENU")?;
649 let menu = names.object_index(lib, cmd)?;
650 let bank = names.bank();
651 let low = bank.map_or(0, |b| menu / b);
652 let view = View {
653 rom,
654 bank,
655 low,
656 high: low,
657 };
658 let r = Reader {
659 rom,
660 names,
661 view,
662 steps: Cell::new(0),
663 max_steps,
664 };
665 let source = locate(&r, menu)?;
666 let count = match &source {
667 Source::List(els) => els.len() + 1,
668 Source::Library(l) => names.library_size(*l)?.0,
669 };
670 let mut c = Collect {
671 r: &r,
672 source: &source,
673 defs: HashMap::new(),
674 def: MenuDef::default(),
675 slot: None,
676 };
677 if let Source::List(els) = &source {
678 for (k, &e) in els.iter().enumerate() {
679 if let Some((i, _)) = r.target(e, view) {
680 c.defs.insert(i, u32::try_from(k + 1).ok()?);
681 }
682 }
683 }
684 let mut addresses = HashMap::new();
685 if let Source::List(els) = &source {
686 for (k, &e) in els.iter().enumerate() {
687 let a = match e {
688 El::Pointer(a) => Some(a),
689 El::Embedded(i, _) => u32::try_from(i).ok().filter(|_| bank.is_none()),
692 };
693 if let (Some(a), Ok(n)) = (a, u32::try_from(k + 1)) {
694 addresses.insert(a, n);
695 }
696 }
697 }
698 let library = match &source {
699 Source::Library(l) => Some(*l),
700 Source::List(_) => None,
701 };
702 let mut menus = vec![None; count];
703 for (n, slot) in menus.iter_mut().enumerate() {
704 let at = match &source {
705 Source::List(els) => n.checked_sub(1).and_then(|k| els.get(k).copied()),
707 Source::Library(l) => names
708 .object_index(*l, u16::try_from(n).ok()?)
709 .and_then(|t| f5(rom, t).map(|p| El::Embedded(t, p))),
710 };
711 let Some(at) = at else { continue };
712 let Some((i, v)) = r.target(at, view) else {
713 continue;
714 };
715 c.def = MenuDef::default();
716 c.definition(i, v.at(i), 0);
717 *slot = Some(std::mem::take(&mut c.def));
718 }
719 Some(Menus {
720 menus,
721 library,
722 addresses,
723 truncated: r.steps.get() >= max_steps,
724 })
725}
726
727fn locate(r: &Reader<'_>, menu: usize) -> Option<Source> {
732 let mut best: Option<(usize, Source)> = None;
733 let mut seen = std::collections::HashSet::new();
734 let mut queue: VecDeque<(usize, usize)> = VecDeque::from([(menu, 0)]);
735 let view = r.view;
736 while let Some((i, depth)) = queue.pop_front() {
737 if depth > MAX_CODE_DEPTH || !seen.insert(i) || !r.step() {
738 continue;
739 }
740 if r.prolog(i) != Some(ObjectType::Program.prolog()) {
741 continue;
742 }
743 for e in r.elements(i) {
744 let (t, embedded) = match e {
745 El::Embedded(j, _) => (j, true),
746 El::Pointer(a) => {
747 if r.names.command_at(a, &view).is_some() {
748 continue;
749 }
750 let Some(t) = view.index(a) else { continue };
751 (t, false)
752 }
753 };
754 let Some(p) = r.prolog(t) else { continue };
755 if p == ObjectType::Program.prolog() {
756 queue.push_back((t, if embedded { depth } else { depth + 1 }));
757 } else if p == ObjectType::List.prolog() {
758 let els = r.elements(t);
759 let menus = els
760 .iter()
761 .filter(|&&x| {
762 r.target(x, view)
763 .and_then(|(j, _)| r.prolog(j))
764 .is_some_and(composite)
765 })
766 .count();
767 if els.len() >= MIN_MENUS && menus * 5 >= els.len() * 4 {
768 consider(&mut best, menus, Source::List(els));
769 }
770 } else if p == ObjectType::SystemBinary.prolog() {
771 let Some(v) = f5(r.rom, t + 5).and_then(|v| u16::try_from(v).ok()) else {
772 continue;
773 };
774 let Some((len, named)) = r.names.library_size(v) else {
775 continue;
776 };
777 if len < MIN_MENUS || named > 0 {
778 continue;
779 }
780 let menus = (0..len)
781 .filter(|&c| {
782 u16::try_from(c)
783 .ok()
784 .and_then(|c| r.names.object_index(v, c))
785 .is_some_and(|j| {
786 r.xlib_array(j).is_some()
787 || r.prolog(j) == Some(ObjectType::List.prolog())
788 })
789 })
790 .count();
791 if menus * 2 >= len {
792 consider(&mut best, menus, Source::Library(v));
793 }
794 }
795 }
796 }
797 best.map(|(_, s)| s)
798}
799
800fn consider(best: &mut Option<(usize, Source)>, score: usize, s: Source) {
801 if best.as_ref().is_none_or(|(b, _)| score > *b) {
802 *best = Some((score, s));
803 }
804}
805
806#[cfg(test)]
807mod tests {
808 use super::*;
809 use saturnus_core::Model;
810
811 fn put(n: &mut [u8], at: usize, value: usize, width: usize) {
812 for i in 0..width {
813 n[at + i] = ((value >> (4 * i)) & 0xF) as u8;
814 }
815 }
816
817 fn put_rel(n: &mut [u8], at: usize, target: usize) {
818 put(n, at, (target + 0x100000 - at) & 0xFFFFF, 5);
819 }
820
821 fn put_hex(n: &mut [u8], at: usize, hex: &str) -> usize {
822 for (i, c) in hex.chars().enumerate() {
823 n[at + i] = c.to_digit(16).unwrap() as u8;
824 }
825 at + hex.len()
826 }
827
828 fn ptr(a: usize) -> String {
829 (0..5)
830 .map(|i| format!("{:X}", (a >> (4 * i)) & 0xF))
831 .collect()
832 }
833
834 fn xlib(lib: usize, cmd: usize) -> String {
835 format!("29E20{}", body(lib, cmd))
836 }
837
838 fn body(lib: usize, cmd: usize) -> String {
839 (0..3)
840 .map(|i| format!("{:X}", (lib >> (4 * i)) & 0xF))
841 .chain((0..3).map(|i| format!("{:X}", (cmd >> (4 * i)) & 0xF)))
842 .collect()
843 }
844
845 fn string(s: &str) -> String {
846 let mut h = format!("C2A20{}", ptr(5 + 2 * s.len()));
847 for b in s.bytes() {
848 h.push_str(&format!("{:X}{:X}", b & 0xF, b >> 4));
849 }
850 h
851 }
852
853 fn library(
857 n: &mut [u8],
858 base: usize,
859 id: usize,
860 names: &[Option<&str>],
861 objects: &[String],
862 ) -> Vec<usize> {
863 let (hash, link) = (base + 0x30, base + 0x300);
864 let body_at = hash + 10;
865 let mut at = body_at + 85;
866 let mut entries = vec![0; names.len()];
867 for len in 1..=16 {
868 for (c, name) in names.iter().enumerate() {
869 let Some(name) = name else { continue };
870 if name.len() != len {
871 continue;
872 }
873 if read_field(n, body_at + 5 * (len - 1), 5) == Some(0) {
874 put_rel(n, body_at + 5 * (len - 1), at);
875 }
876 entries[c] = at;
877 put(n, at, len, 2);
878 for (i, b) in name.bytes().enumerate() {
879 put(n, at + 2 + 2 * i, usize::from(b), 2);
880 }
881 put(n, at + 2 + 2 * len, c, 3);
882 at += 2 + 2 * len + 3;
883 }
884 }
885 put_rel(n, body_at + 80, at);
886 for (c, &e) in entries.iter().enumerate() {
887 let f = at + 5 * c;
888 put(n, f, if e == 0 { 0 } else { f - e }, 5);
889 }
890 let end = at + 5 * names.len();
891 assert!(end < link);
892 put(n, hash, 0x02A4E, 5);
893 put(n, hash + 5, end - hash - 5, 5);
894 put(n, link, 0x02A4E, 5);
895 put(n, link + 5, 5 + 5 * names.len(), 5);
896 let mut addrs = Vec::new();
897 for (c, o) in objects.iter().enumerate() {
898 let obj = base + 0x400 + 0x100 * c;
899 put_hex(n, obj - 6, &body(id, c));
900 assert!(o.len() < 0xF0);
901 put_hex(n, obj, o);
902 put_rel(n, link + 10 + 5 * c, obj);
903 addrs.push(obj);
904 }
905 put(n, base, id, 3);
906 put_rel(n, base + 3, hash);
907 put_rel(n, base + 13, link);
908 addrs
909 }
910
911 fn builtins(n: &mut [u8], menu_body: &str) -> Vec<usize> {
914 let prog = "D9D20B2130".to_string();
915 library(
916 n,
917 0x100,
918 2,
919 &[Some("MENU"), Some("SIN"), Some("COS"), Some("TAN")],
920 &[
921 format!("D9D20{menu_body}B2130"),
922 prog.clone(),
923 prog.clone(),
924 prog,
925 ],
926 )
927 }
928
929 #[test]
930 fn a_list_of_definitions_as_on_the_48sx() {
931 let mut n = vec![0u8; 0x8000];
932 let list = 0x4000;
933 let a = builtins(&mut n, &ptr(list));
934 let (sin, cos, tan) = (a[1], a[2], a[3]);
935 let mut at = put_hex(&mut n, list, "47A20");
938 let def1 = |m2: usize| {
939 format!(
940 "47A20{}47A20{}{}B2130B2130",
941 ptr(sin),
942 string("TRIG"),
943 ptr(m2)
944 )
945 };
946 let m2_at = at + def1(0).len();
947 at = put_hex(&mut n, at, &def1(m2_at));
948 at = put_hex(
949 &mut n,
950 at,
951 &format!(
952 "47A20{}{}47A20{}{}B2130B2130",
953 ptr(cos),
954 ptr(tan),
955 string("T"),
956 ptr(tan)
957 ),
958 );
959 for _ in 3..=24 {
960 at = put_hex(&mut n, at, "47A20B2130");
961 }
962 put_hex(&mut n, at, "B2130");
963 let names = NameTable::build(Model::Hp48sx, &n);
964 let m = read(&n, &names).unwrap();
965 assert_eq!(m.len(), 25);
966 assert_eq!(m.get(1).unwrap().commands, ["SIN"]);
967 assert_eq!(m.get(1).unwrap().submenus, [("TRIG".to_string(), 2)]);
968 assert_eq!(m.get(2).unwrap().commands, ["COS", "TAN"]);
969 let key = |label: Option<&str>, command: Option<&str>, submenu: Option<u32>| MenuKey {
972 label: label.map(str::to_string),
973 command: command.map(str::to_string),
974 submenu,
975 };
976 assert_eq!(
977 m.get(1).unwrap().keys,
978 [
979 key(None, Some("SIN"), None),
980 key(Some("TRIG"), None, Some(2))
981 ]
982 );
983 assert_eq!(
984 m.get(2).unwrap().keys,
985 [
986 key(None, Some("COS"), None),
987 key(None, Some("TAN"), None),
988 key(Some("T"), Some("TAN"), None)
989 ]
990 );
991 assert_eq!(m.get(2).unwrap().keys[2].text(), Some("T"));
992 let paths = m.paths(&[(1, "MATH".into())]);
993 assert_eq!(paths[&2], "MATH TRIG");
994 assert_eq!(paths[&3], "MENU 3");
995 let p = m.placement(&paths);
996 assert_eq!(p["TAN"], ["MATH TRIG"]);
997 assert_eq!(p["SIN"], ["MATH"]);
998 struct Image<'a>(&'a [u8]);
1000 impl Memory for Image<'_> {
1001 fn nibble(&self, a: u32) -> Option<u8> {
1002 self.0.get(a as usize).copied()
1003 }
1004 }
1005 assert_eq!(m.number(m2_at as u32, &Image(&n)), Some(2));
1006 }
1007
1008 #[test]
1009 fn a_menu_library_as_on_the_48gx() {
1010 let mut n = vec![0u8; 0x10000];
1011 builtins(&mut n, "119209A000");
1013 let keys = library(
1015 &mut n,
1016 0x8000,
1017 0xA8,
1018 &[None, None],
1019 &[
1020 format!("47A20{}D9D20{}B2130B2130", string("SUBM"), xlib(0xA9, 2)),
1021 format!("47A20{}{}B2130", string("C"), xlib(2, 2)),
1022 ],
1023 );
1024 let _ = keys;
1025 let array = |items: &[String]| {
1026 let body: String = items.iter().map(|x| x[5..].to_string()).collect();
1027 format!(
1028 "8E920{}29E2010000{}{}",
1029 ptr(25 + 6 * items.len() - 5),
1030 ptr(items.len()),
1031 body
1032 )
1033 };
1034 let mut defs = vec!["47A20B2130".to_string(); 22];
1035 defs[1] = array(&[xlib(2, 1), xlib(0xA8, 0)]);
1036 defs[2] = array(&[xlib(0xA8, 1), xlib(2, 3)]);
1037 library(&mut n, 0xA000, 0xA9, &vec![None; 22], &defs);
1038 let names = NameTable::build(Model::Hp48gx, &n);
1039 let m = read(&n, &names).unwrap();
1040 assert_eq!(m.len(), 22);
1041 assert_eq!(m.get(1).unwrap().commands, ["SIN"]);
1042 assert_eq!(m.get(1).unwrap().submenus, [("SUBM".to_string(), 2)]);
1043 assert_eq!(m.get(2).unwrap().commands, ["COS", "TAN"]);
1044 let texts: Vec<_> = m.get(1).unwrap().keys.iter().map(MenuKey::text).collect();
1045 assert_eq!(texts, [Some("SIN"), Some("SUBM")]);
1046 let texts: Vec<_> = m.get(2).unwrap().keys.iter().map(MenuKey::text).collect();
1047 assert_eq!(texts, [Some("C"), Some("TAN")]);
1048 let mut ram = n.clone();
1050 put_hex(&mut ram, 0xF000, &xlib(0xA9, 2));
1051 struct Image<'a>(&'a [u8]);
1052 impl Memory for Image<'_> {
1053 fn nibble(&self, a: u32) -> Option<u8> {
1054 self.0.get(a as usize).copied()
1055 }
1056 }
1057 assert_eq!(m.number(0xF000, &Image(&ram)), Some(2));
1058 }
1059
1060 #[test]
1061 fn single_object_keys_and_labels() {
1062 let mut n = vec![0u8; 0x8000];
1063 let list = 0x4000;
1064 let a = builtins(&mut n, &ptr(list));
1065 let (sin, cos, tan) = (a[1], a[2], a[3]);
1066 let mut at = put_hex(&mut n, list, "47A20");
1068 at = put_hex(
1069 &mut n,
1070 at,
1071 &format!(
1072 "47A2047A20{}{}B213047A20{}B2130B2130",
1073 ptr(sin),
1074 ptr(cos),
1075 ptr(tan)
1076 ),
1077 );
1078 for _ in 2..=24 {
1079 at = put_hex(&mut n, at, "47A20B2130");
1080 }
1081 put_hex(&mut n, at, "B2130");
1082 let names = NameTable::build(Model::Hp48sx, &n);
1083 let m = read(&n, &names).unwrap();
1084 assert_eq!(m.get(1).unwrap().commands, ["COS", "TAN"]);
1085 let k = &m.get(1).unwrap().keys;
1086 assert_eq!(k.len(), 2);
1087 assert_eq!(
1088 (k[0].text(), k[0].command.as_deref()),
1089 (Some("SIN"), Some("COS"))
1090 );
1091 assert_eq!(
1092 (k[1].text(), k[1].command.as_deref()),
1093 (Some("TAN"), Some("TAN"))
1094 );
1095 assert!(!m.truncated());
1096 let short = read_within(&n, &names, 30).unwrap();
1098 assert!(short.truncated());
1099 }
1100
1101 #[test]
1102 fn garbage_finds_no_menus() {
1103 let mut n = vec![0u8; 0x8000];
1104 for (i, x) in n.iter_mut().enumerate() {
1105 *x = ((i * 7919) >> 3) as u8 & 0xF;
1106 }
1107 let names = NameTable::build(Model::Hp48sx, &n);
1108 assert!(read(&n, &names).is_none());
1109 let mut n = vec![0u8; 0x8000];
1111 let a = builtins(&mut n, &format!("{}{}", ptr(0x500), ptr(0x7FF0)));
1112 let _ = a;
1113 let names = NameTable::build(Model::Hp48sx, &n);
1114 assert!(read(&n, &names).is_none());
1115 assert!(read(&n, &NameTable::default()).is_none());
1116 }
1117}