1pub mod checksum;
2pub mod dips;
3mod encoding;
4mod index;
5mod model;
6pub mod resolve;
7
8use crate::checksum::{ChecksumMismatch, update_all_checksum16, verify_all_checksum16};
9use crate::dips::{MAX_SWITCH_COUNT, get_dip_switch, set_dip_switch, validate_dip_switch_range};
10use crate::encoding::{
11 Location, read_bcd, read_bool, read_ch, read_int, read_wpc_rtc, write_bcd, write_ch,
12};
13use crate::index::get_index_map;
14use crate::model::{
15 DEFAULT_INVERT, DEFAULT_LENGTH, DEFAULT_SCALE, Descriptor, Encoding, Endian, GlobalSettings,
16 HexOrInteger, MemoryLayoutType, Nibble, NvramMap, Platform, StateOrStateList,
17};
18use include_dir::{Dir, File, include_dir};
19use serde::de;
20use serde::de::DeserializeOwned;
21use serde_json::{Number, Value};
22use std::collections::HashMap;
23use std::fs::OpenOptions;
24use std::io;
25use std::io::{Read, Seek, Write};
26use std::path::{Path, PathBuf};
27
28static MAPS: Dir = include_dir!("$OUT_DIR/maps.brotli");
29
30#[derive(Debug, PartialEq)]
31pub struct HighScore {
32 pub label: Option<String>,
33 pub short_label: Option<String>,
34 pub initials: String,
35 pub score: u64,
36}
37
38#[derive(Debug, PartialEq)]
39pub struct ModeChampion {
41 pub label: Option<String>,
42 pub short_label: Option<String>,
43 pub initials: Option<String>,
44 pub score: Option<u64>,
45 pub suffix: Option<String>,
46 pub timestamp: Option<String>,
47}
48
49#[derive(Debug, PartialEq)]
52pub struct LastGamePlayer {
53 pub score: u64,
54 pub label: Option<String>,
55}
56
57#[derive(Debug, PartialEq)]
58pub struct DipSwitchInfo {
59 pub nr: usize,
60 pub name: Option<String>,
61}
62
63pub struct Nvram {
65 pub map: NvramMap,
66 pub platform: Platform,
67 pub nv_path: PathBuf,
68}
69
70impl Nvram {}
71
72impl Nvram {
73 pub fn open(nv_path: &Path) -> io::Result<Option<Nvram>> {
80 let map_opt: Option<NvramMap> = open_nvram(nv_path)?;
81 if let Some(map) = map_opt {
82 let platform = read_platform(&map._metadata.platform)?;
84 Ok(Some(Nvram {
85 map,
86 platform,
87 nv_path: nv_path.to_path_buf(),
88 }))
89 } else {
90 Ok(None)
91 }
92 }
93
94 pub fn open_local(nv_path: &Path) -> io::Result<Option<Nvram>> {
102 let map_opt: Option<NvramMap> = open_nvram_local(nv_path)?;
103 if let Some(map) = map_opt {
105 let platform = read_platform_local(map.platform())?;
106 Ok(Some(Nvram {
107 map,
108 platform,
109 nv_path: nv_path.to_path_buf(),
110 }))
111 } else {
112 Ok(None)
113 }
114 }
115
116 pub fn read_highscores(&mut self) -> io::Result<Vec<HighScore>> {
117 let mut file = OpenOptions::new().read(true).open(&self.nv_path)?;
118 read_highscores(
119 self.platform.endian,
120 self.platform.nibble(MemoryLayoutType::NVRam),
121 self.platform.offset(MemoryLayoutType::NVRam),
122 &self.map,
123 &mut file,
124 )
125 }
126
127 pub fn clear_highscores(&mut self) -> io::Result<()> {
128 let mut rw_file = OpenOptions::new()
130 .read(true)
131 .write(true)
132 .open(&self.nv_path)?;
133 clear_highscores(
134 &mut rw_file,
135 self.platform.nibble(MemoryLayoutType::NVRam),
136 self.platform.offset(MemoryLayoutType::NVRam),
137 &self.map,
138 )?;
139 update_all_checksum16(&mut rw_file, &self.map, &self.platform)
140 }
141
142 pub fn read_mode_champions(&mut self) -> io::Result<Option<Vec<ModeChampion>>> {
143 let mut file = OpenOptions::new().read(true).open(&self.nv_path)?;
144 read_mode_champions(
145 &mut file,
146 self.platform.endian,
147 self.platform.nibble(MemoryLayoutType::NVRam),
148 self.platform.offset(MemoryLayoutType::NVRam),
149 &self.map,
150 )
151 }
152
153 pub fn read_last_game(&mut self) -> io::Result<Option<Vec<LastGamePlayer>>> {
154 let mut file = OpenOptions::new().read(true).open(&self.nv_path)?;
155 read_last_game(
156 &mut file,
157 self.platform.endian,
158 self.platform.nibble(MemoryLayoutType::NVRam),
159 self.platform.offset(MemoryLayoutType::NVRam),
160 &self.map,
161 )
162 }
163
164 pub fn verify_all_checksum16(&mut self) -> io::Result<Vec<ChecksumMismatch<u16>>> {
165 let mut file = OpenOptions::new().read(true).open(&self.nv_path)?;
166 verify_all_checksum16(&mut file, &self.map, &self.platform)
167 }
168
169 pub fn read_replay_score(&mut self) -> io::Result<Option<u64>> {
171 let mut file = OpenOptions::new().read(true).open(&self.nv_path)?;
172 read_replay_score(
173 &mut file,
174 self.platform.endian,
175 self.platform.nibble(MemoryLayoutType::NVRam),
176 self.platform.offset(MemoryLayoutType::NVRam),
177 &self.map,
178 )
179 }
180
181 pub fn read_game_state(&mut self) -> io::Result<Option<HashMap<String, String>>> {
182 let mut file = OpenOptions::new().read(true).open(&self.nv_path)?;
183 read_game_state(
184 &mut file,
185 self.platform.endian,
186 self.platform.nibble(MemoryLayoutType::NVRam),
187 self.platform.offset(MemoryLayoutType::NVRam),
188 &self.map,
189 )
190 }
191
192 pub fn dip_switches_len(&self) -> io::Result<usize> {
193 if let Some(dip_switches) = &self.map.dip_switches {
194 let mut highest_offset = 0;
195 for (name, ds) in dip_switches {
196 if let Some(offsets_vec) = &ds.offsets {
197 for offset in offsets_vec {
198 let offest_u64 = u64::from(offset);
199 if offest_u64 > highest_offset {
200 highest_offset = offest_u64;
201 }
202 }
203 } else {
204 return Err(io::Error::new(
205 io::ErrorKind::InvalidData,
206 format!("Dip switch '{name}' is missing offsets"),
207 ));
208 }
209 }
210 if highest_offset as usize > MAX_SWITCH_COUNT {
211 return Err(io::Error::new(
212 io::ErrorKind::InvalidData,
213 format!(
214 "Dip switch offset {highest_offset} out of range, expected 1-{MAX_SWITCH_COUNT}"
215 ),
216 ));
217 }
218 Ok(highest_offset as usize)
219 } else {
220 Ok(32 + 3)
224 }
225 }
226
227 pub fn dip_switches_info(&self) -> io::Result<Vec<DipSwitchInfo>> {
228 let len = self.dip_switches_len()?;
229 let mut info = Vec::with_capacity(len);
230 if let Some(dip_switches) = &self.map.dip_switches {
231 let mut offsets = std::collections::HashSet::new();
232 for (name, ds) in dip_switches {
233 if let Some(offsets_vec) = &ds.offsets {
234 for offset in offsets_vec {
235 offsets.insert(u64::from(offset));
236 }
237 } else {
238 return Err(io::Error::new(
239 io::ErrorKind::InvalidData,
240 format!("Dip switch '{name}' is missing offsets"),
241 ));
242 }
243 }
244 let mut sorted_offsets: Vec<u64> = offsets.into_iter().collect();
245 sorted_offsets.sort_unstable();
246 for (i, offset) in sorted_offsets.iter().enumerate() {
247 let name = dip_switches
248 .iter()
249 .find_map(|(_section, ds)| {
250 if let Some(offsets_vec) = &ds.offsets
251 && offsets_vec.contains(&HexOrInteger::from(*offset))
252 {
253 return ds.label.clone();
254 }
255 None
256 })
257 .unwrap_or_else(|| "".to_string());
258 info.push(DipSwitchInfo {
259 nr: i + 1,
260 name: if name.is_empty() { None } else { Some(name) },
261 });
262 }
263 Ok(info)
264 } else {
265 for i in 0..len {
266 info.push(DipSwitchInfo {
267 nr: i + 1,
268 name: None,
269 });
270 }
271 Ok(info)
272 }
273 }
274
275 pub fn get_dip_switch(&self, number: usize) -> io::Result<bool> {
283 validate_dip_switch_range(self.dip_switches_len()?, number)?;
284 let mut file = OpenOptions::new().read(true).open(&self.nv_path)?;
285 get_dip_switch(&mut file, number)
286 }
287
288 pub fn set_dip_switch(&self, number: usize, on: bool) -> io::Result<()> {
296 validate_dip_switch_range(self.dip_switches_len()?, number)?;
297 let mut file = OpenOptions::new()
298 .read(true)
299 .write(true)
300 .open(&self.nv_path)?;
301 set_dip_switch(&mut file, number, on)
302 }
303}
304
305fn open_nvram<T: DeserializeOwned>(nv_path: &Path) -> io::Result<Option<T>> {
306 let rom_name = nv_path
308 .file_name()
309 .unwrap()
310 .to_str()
311 .unwrap()
312 .split('.')
313 .next()
314 .unwrap()
315 .to_string();
316 if !nv_path.exists() {
318 return Err(io::Error::new(
319 io::ErrorKind::NotFound,
320 format!("File not found: {nv_path:?}"),
321 ));
322 }
323 find_map(&rom_name)
324}
325
326fn open_nvram_local<T: DeserializeOwned>(nv_path: &Path) -> io::Result<Option<T>> {
327 let rom_name = nv_path
329 .file_name()
330 .unwrap()
331 .to_str()
332 .unwrap()
333 .split('.')
334 .next()
335 .unwrap()
336 .to_string();
337 if !nv_path.exists() {
339 return Err(io::Error::new(
340 io::ErrorKind::NotFound,
341 format!("File not found: {nv_path:?}"),
342 ));
343 }
344 find_map_local(&rom_name)
345}
346
347fn read_platform<T: DeserializeOwned>(platform_name: &str) -> io::Result<T> {
348 let platform_file_name = format!("{platform_name}.json.brotli");
349 let compressed_platform_path = Path::new("platforms").join(platform_file_name);
350
351 let map_file = MAPS.get_file(&compressed_platform_path).ok_or_else(|| {
352 io::Error::new(
353 io::ErrorKind::NotFound,
354 format!(
355 "File not found: {}",
356 compressed_platform_path.to_string_lossy()
357 ),
358 )
359 })?;
360 read_compressed_json(map_file)
361}
362
363fn read_platform_local<T: DeserializeOwned>(platform_name: &str) -> io::Result<T> {
364 let platform_file_name = format!("{platform_name}.json");
365 let platform_path = Path::new("pinmame-nvram-maps")
366 .join("platforms")
367 .join(platform_file_name);
368 if !platform_path.exists() {
369 return Err(io::Error::new(
370 io::ErrorKind::NotFound,
371 format!("File not found: {platform_path:?}"),
372 ));
373 }
374 let platform_file = OpenOptions::new().read(true).open(&platform_path)?;
375 let platform = serde_json::from_reader(platform_file)?;
376 Ok(platform)
377}
378
379fn find_map<T: DeserializeOwned>(rom_name: &String) -> io::Result<Option<T>> {
380 match get_index_map()?.get(rom_name) {
381 Some(map_path) => {
382 let compressed_map_path = format!("{}.brotli", map_path.as_str().unwrap());
383 let map_file = MAPS.get_file(&compressed_map_path).ok_or_else(|| {
384 io::Error::new(
385 io::ErrorKind::NotFound,
386 format!("File not found: {compressed_map_path}"),
387 )
388 })?;
389 let map: T = read_compressed_json(map_file)?;
390 Ok(Some(map))
391 }
392 None => Ok(None),
393 }
394}
395
396fn find_map_local<T: DeserializeOwned>(rom_name: &String) -> io::Result<Option<T>> {
397 let index_file = Path::new("pinmame-nvram-maps").join("index.json");
398 if !index_file.exists() {
399 return Err(io::Error::new(
400 io::ErrorKind::NotFound,
401 format!("File not found: {index_file:?}"),
402 ));
403 }
404 let index_file = OpenOptions::new().read(true).open(&index_file)?;
405 let map: Value = serde_json::from_reader(index_file)?;
406 match map.get(rom_name) {
407 Some(map_path) => {
408 let map_file = Path::new("pinmame-nvram-maps").join(map_path.as_str().unwrap());
409 if !map_file.exists() {
410 return Err(io::Error::new(
411 io::ErrorKind::NotFound,
412 format!("File not found: {map_file:?}"),
413 ));
414 }
415 let map_file = OpenOptions::new().read(true).open(&map_file)?;
416 let map: T = serde_json::from_reader(map_file)?;
417 Ok(Some(map))
418 }
419 None => Ok(None),
420 }
421}
422
423fn read_compressed_json<T: de::DeserializeOwned>(map_file: &File) -> io::Result<T> {
424 let mut cursor = io::Cursor::new(map_file.contents());
425 let reader = brotli::Decompressor::new(&mut cursor, 4096);
426 let data = serde_json::from_reader(reader)?;
427 Ok(data)
428}
429
430fn read_highscores<T: Read + Seek>(
431 endian: Endian,
432 nibble: Nibble,
433 offset: u64,
434 map: &NvramMap,
435 mut nvram_file: &mut T,
436) -> io::Result<Vec<HighScore>> {
437 let scores: Result<Vec<HighScore>, io::Error> = map
438 .high_scores
439 .iter()
440 .map(|hs| read_highscore(&mut nvram_file, hs, endian, nibble, offset, map))
441 .collect();
442 scores
443}
444
445fn read_highscore<T: Read + Seek, S: GlobalSettings>(
446 mut nvram_file: &mut T,
447 hs: &model::HighScore,
448 endian: Endian,
449 nibble: Nibble,
450 offset: u64,
451 global_settings: &S,
452) -> io::Result<HighScore> {
453 let mut initials = "".to_string();
454 if let Some(map_initials) = &hs.initials {
455 initials = read_ch(
456 &mut nvram_file,
457 u64::from(
458 map_initials
459 .start
460 .as_ref()
461 .expect("missing start for ch encoding"),
462 ) - offset,
463 map_initials.length.expect("missing length for ch encoding"),
464 map_initials.mask.as_ref().map(|m| m.into()),
465 global_settings.char_map(),
466 map_initials.nibble.unwrap_or(nibble),
467 map_initials.null,
468 )?;
469 }
470
471 let score = read_descriptor_to_u64(&mut nvram_file, &hs.score, endian, nibble, offset)?;
472
473 Ok(HighScore {
474 label: hs.label.clone(),
475 short_label: hs.short_label.clone(),
476 initials,
477 score,
478 })
479}
480
481fn clear_highscores<T: Write + Seek>(
482 mut nvram_file: &mut T,
483 nibble: Nibble,
484 offset: u64,
485 map: &NvramMap,
486) -> io::Result<()> {
487 for hs in &map.high_scores {
488 if let Some(map_initials) = &hs.initials {
489 write_ch(
490 &mut nvram_file,
491 u64::from(
492 map_initials
493 .start
494 .as_ref()
495 .expect("missing start for ch encoding"),
496 ) - offset,
497 map_initials.length.expect("missing length for ch encoding"),
498 "AAA".to_string(),
499 map.char_map(),
500 &map_initials.nibble.or(Some(nibble)),
501 )?;
502 }
503 if let Some(map_score_start) = &hs.score.start {
504 write_bcd(
505 &mut nvram_file,
506 u64::from(map_score_start) - offset,
507 hs.score.length.unwrap_or(0),
508 hs.score.nibble.unwrap_or(nibble),
509 0,
510 )?;
511 }
512 }
513 Ok(())
514}
515
516fn read_mode_champion<T: Read + Seek, S: GlobalSettings>(
517 mut nvram_file: &mut T,
518 mc: &model::ModeChampion,
519 endian: Endian,
520 nibble: Nibble,
521 offset: u64,
522 global_settings: &S,
523) -> io::Result<ModeChampion> {
524 let initials = mc
525 .initials
526 .as_ref()
527 .map(|initials| {
528 read_ch(
529 &mut nvram_file,
530 u64::from(
531 initials
532 .start
533 .as_ref()
534 .expect("missing start for ch encoding"),
535 ) - offset,
536 initials.length.expect("missing start for ch encoding"),
537 initials.mask.as_ref().map(|m| m.into()),
538 global_settings.char_map(),
539 initials.nibble.unwrap_or(nibble),
540 initials.null,
541 )
542 })
543 .transpose()?;
544 let score = if let Some(score) = &mc.score {
545 let result = read_descriptor_to_u64(&mut nvram_file, score, endian, nibble, offset)?;
546 Some(result)
547 } else {
548 None
549 };
550
551 let timestamp = mc
552 .timestamp
553 .as_ref()
554 .map(|ts| read_descriptor_to_rtc_string(&mut nvram_file, ts))
555 .transpose()?;
556
557 Ok(ModeChampion {
558 label: Some(mc.label.clone()),
559 short_label: mc.short_label.clone(),
560 initials,
561 score,
562 suffix: mc.score.as_ref().and_then(|s| s.suffix.clone()),
563 timestamp,
564 })
565}
566
567fn read_last_game_player<T: Read + Seek>(
568 mut nvram_file: &mut T,
569 descriptor: &Descriptor,
570 endian: Endian,
571 nibble: Nibble,
572 offset: u64,
573) -> io::Result<LastGamePlayer> {
574 let score = read_descriptor_to_u64(&mut nvram_file, descriptor, endian, nibble, offset)?;
575 Ok(LastGamePlayer {
576 score,
577 label: descriptor.label.clone(),
578 })
579}
580
581fn read_last_game<T: Read + Seek>(
582 mut nvram_file: &mut T,
583 endian: Endian,
584 nibble: Nibble,
585 offset: u64,
586 map: &NvramMap,
587) -> io::Result<Option<Vec<LastGamePlayer>>> {
588 if let Some(lg) = &map.last_game {
589 let last_games: Result<Vec<LastGamePlayer>, io::Error> = lg
592 .iter()
593 .map(|lg| read_last_game_player(&mut nvram_file, lg, endian, nibble, offset))
594 .collect();
595 Ok(Some(last_games?))
596 } else if let Some(game_state) = &map.game_state {
597 if let Some(scores) = game_state.get("final_scores") {
599 let scores: Result<Vec<LastGamePlayer>, io::Error> = match scores {
600 StateOrStateList::StateList(sl) => sl
601 .iter()
602 .map(|d| read_last_game_player(&mut nvram_file, d, endian, nibble, offset))
607 .collect(),
608 _other => {
609 return Err(io::Error::new(
610 io::ErrorKind::InvalidData,
611 "Scores is not a StateList",
612 ));
613 }
614 };
615 Ok(Some(scores?))
616 } else if let Some(scores) = game_state.get("scores") {
617 let scores: Result<Vec<LastGamePlayer>, io::Error> = match scores {
618 StateOrStateList::StateList(sl) => sl
619 .iter()
620 .map(|d| read_last_game_player(&mut nvram_file, d, endian, nibble, offset))
621 .collect(),
622 _other => {
623 return Err(io::Error::new(
624 io::ErrorKind::InvalidData,
625 "Scores is not a StateList",
626 ));
627 }
628 };
629 Ok(Some(scores?))
630 } else {
631 Ok(None)
632 }
633 } else {
634 Ok(None)
635 }
636}
637
638fn read_mode_champions<T: Read + Seek>(
639 mut nvram_file: &mut T,
640 endian: Endian,
641 nibble: Nibble,
642 offset: u64,
643 map: &NvramMap,
644) -> io::Result<Option<Vec<ModeChampion>>> {
645 if let Some(mode_champions) = &map.mode_champions {
646 let champions: Result<Vec<ModeChampion>, io::Error> = mode_champions
647 .iter()
648 .map(|mc| read_mode_champion(&mut nvram_file, mc, endian, nibble, offset, map))
649 .collect();
650 Ok(Some(champions?))
651 } else {
652 Ok(None)
653 }
654}
655
656fn read_replay_score<T: Read + Seek>(
657 mut nvram_file: &mut T,
658 endian: Endian,
659 nibble: Nibble,
660 offset: u64,
661 map: &NvramMap,
662) -> io::Result<Option<u64>> {
663 if let Some(descriptor) = &map.replay_score {
664 let value = read_descriptor_to_u64(&mut nvram_file, descriptor, endian, nibble, offset)?;
665 Ok(Some(value))
666 } else {
667 Ok(None)
668 }
669}
670
671fn read_game_state<T: Read + Seek>(
672 mut nvram_file: &mut T,
673 endian: Endian,
674 nibble: Nibble,
675 offset: u64,
676 map: &NvramMap,
677) -> io::Result<Option<HashMap<String, String>>> {
678 if let Some(game_state) = &map.game_state {
679 let state: Result<HashMap<String, String>, io::Error> = game_state
681 .iter()
682 .flat_map(|(key, v)| match v {
683 StateOrStateList::State(s) => {
684 if let Ok(LocateResult::OutsideNVRAM) = location_for(s, offset) {
686 return vec![];
687 }
688 let r =
689 read_descriptor_to_string(&mut nvram_file, s, endian, nibble, offset, map)
690 .map(|r| (key.clone(), r));
691 vec![r]
692 }
693 StateOrStateList::StateList(sl) => sl
694 .iter()
695 .enumerate()
696 .filter_map(|(index, s)| {
697 if let Ok(LocateResult::OutsideNVRAM) = location_for(s, offset) {
699 return None;
700 }
701 Some((index, s))
702 })
703 .map(|(index, s)| {
704 let compund_key = format!("{key}.{index}");
705 read_descriptor_to_string(&mut nvram_file, s, endian, nibble, offset, map)
706 .map(|r| (compund_key, r))
707 })
708 .collect(),
709 StateOrStateList::Notes(_) => {
710 vec![]
711 }
712 })
713 .collect();
714
715 Ok(Some(state?))
716 } else {
717 Ok(None)
718 }
719}
720
721fn read_descriptor_to_string<T: Read + Seek, S: GlobalSettings>(
722 mut nvram_file: &mut T,
723 descriptor: &Descriptor,
724 endian: Endian,
725 nibble: Nibble,
726 offset: u64,
727 global_settings: &S,
728) -> io::Result<String> {
729 match descriptor.encoding {
730 Encoding::Ch => match &descriptor.start {
731 Some(start) => read_ch(
732 &mut nvram_file,
733 u64::from(start) - offset,
734 descriptor.length.unwrap_or(DEFAULT_LENGTH),
735 descriptor.mask.as_ref().map(|m| m.into()),
736 global_settings.char_map(),
737 descriptor.nibble.unwrap_or(nibble),
738 None,
739 ),
740 None => Err(io::Error::new(
741 io::ErrorKind::InvalidData,
742 "Ch descriptor requires start",
743 )),
744 },
745 Encoding::Int => match &descriptor.start {
746 Some(start) => {
747 let start = u64::from(start);
748 if start < offset {
749 return Ok("Value is stored outside the NVRAM".to_string());
750 }
751 let score = read_int(
752 &mut nvram_file,
753 endian,
754 nibble,
755 start - offset,
756 descriptor.length.unwrap_or(DEFAULT_LENGTH),
757 descriptor
758 .scale
759 .as_ref()
760 .unwrap_or(&Number::from(DEFAULT_SCALE)),
761 )?;
762 Ok(score.to_string())
763 }
764 None => Err(io::Error::new(
765 io::ErrorKind::InvalidData,
766 "Int descriptor requires start",
767 )),
768 },
769 Encoding::Bcd => {
770 let location = match location_for(descriptor, offset)? {
771 LocateResult::OutsideNVRAM => {
772 return Ok("Value is stored outside the NVRAM".to_string());
773 }
774 LocateResult::Located(loc) => loc,
775 };
776 let score = read_bcd(
777 &mut nvram_file,
778 location,
779 descriptor.nibble.unwrap_or(nibble),
780 descriptor
781 .scale
782 .as_ref()
783 .unwrap_or(&Number::from(DEFAULT_SCALE)),
784 endian,
785 )?;
786 Ok(score.to_string())
787 }
788 Encoding::Bits => Ok("Bits encoding not implemented".to_string()),
789 Encoding::Bool => match &descriptor.start {
790 Some(start) => {
791 println!("Reading bool at start {:?}", start);
792 let bool = read_bool(
793 nvram_file,
794 u64::from(start) - offset,
795 nibble,
796 endian,
797 descriptor.length.unwrap_or(DEFAULT_LENGTH),
798 descriptor.invert.unwrap_or(DEFAULT_INVERT),
799 )?;
800 Ok(bool.to_string())
801 }
802 None => Err(io::Error::new(
803 io::ErrorKind::InvalidData,
804 "Bool descriptor requires start",
805 )),
806 },
807 Encoding::Dipsw => Ok("Dipsw encoding not implemented".to_string()),
808 other => todo!("Encoding not implemented: {:?}", other),
809 }
810}
811
812fn read_descriptor_to_u64<T: Read + Seek>(
813 mut nvram_file: &mut T,
814 descriptor: &Descriptor,
815 endian: Endian,
816 nibble: Nibble,
817 offset: u64,
818) -> io::Result<u64> {
819 match descriptor.encoding {
820 Encoding::Bcd => {
821 let location = match location_for(descriptor, offset)? {
822 LocateResult::OutsideNVRAM => {
823 return Err(io::Error::new(
824 io::ErrorKind::InvalidData,
825 format!(
826 "Descriptor '{}' points outside NVRAM",
827 descriptor.label.as_deref().unwrap_or("unknown")
828 ),
829 ));
830 }
831 LocateResult::Located(loc) => loc,
832 };
833 read_bcd(
834 &mut nvram_file,
835 location,
836 descriptor.nibble.unwrap_or(nibble),
837 descriptor
838 .scale
839 .as_ref()
840 .unwrap_or(&Number::from(DEFAULT_SCALE)),
841 endian,
842 )
843 }
844 Encoding::Int => {
845 if let Some(start) = &descriptor.start {
846 read_int(
847 &mut nvram_file,
848 endian,
849 descriptor.nibble.unwrap_or(nibble),
850 u64::from(start) - offset,
851 descriptor.length.unwrap_or(DEFAULT_LENGTH),
852 descriptor
853 .scale
854 .as_ref()
855 .unwrap_or(&Number::from(DEFAULT_SCALE)),
856 )
857 } else {
858 Err(io::Error::new(
859 io::ErrorKind::InvalidData,
860 "Int descriptor requires start",
861 ))
862 }
863 }
864 other => todo!("Encoding not implemented: {:?}", other),
865 }
866}
867
868fn read_descriptor_to_rtc_string<T: Read + Seek>(
869 mut nvram_file: &mut T,
870 ts: &Descriptor,
871) -> io::Result<String> {
872 match &ts.encoding {
873 Encoding::WpcRtc => read_wpc_rtc(
874 &mut nvram_file,
875 ts.start
876 .as_ref()
877 .expect("missing start for wpc_rtc encoding")
878 .into(),
879 ts.length.expect("missing length for wpc_rtc encoding"),
880 ),
881 other => todo!("Timestamp encoding not implemented: {:?}", other),
882 }
883}
884
885enum LocateResult {
886 OutsideNVRAM,
887 Located(Location),
888}
889
890fn location_for(descriptor: &Descriptor, offset: u64) -> io::Result<LocateResult> {
891 match &descriptor.offsets {
892 None => match &descriptor.start {
893 Some(start) => {
894 let start = u64::from(start);
895 if start < offset {
896 return Ok(LocateResult::OutsideNVRAM);
897 }
898 Ok(LocateResult::Located(Location::Continuous {
899 start: start - offset,
900 length: descriptor.length.unwrap_or(DEFAULT_LENGTH),
901 }))
902 }
903 _ => Err(io::Error::new(
904 io::ErrorKind::InvalidData,
905 "Descriptor without offsets requires start",
906 )),
907 },
908 Some(offsets) => {
909 for o in offsets {
911 let o_u64 = u64::from(o);
912 if o_u64 < offset {
913 return Ok(LocateResult::OutsideNVRAM);
914 }
915 }
916 Ok(LocateResult::Located(Location::Scattered {
917 offsets: offsets.iter().map(|o| u64::from(o) - offset).collect(),
918 }))
919 }
920 }
921}
922
923#[cfg(test)]
924mod tests {
925 use super::*;
926 use pretty_assertions::assert_eq;
927 use serde_json::Value;
928 use std::fs::File;
929 use testdir::testdir;
930
931 #[test]
932 fn test_not_found() {
933 let nvram = Nvram::open(Path::new("does_not_exist.nv"));
934 assert!(matches!(
935 nvram,
936 Err(ref e) if e.kind() == io::ErrorKind::NotFound && e.to_string() == "File not found: \"does_not_exist.nv\""
937 ));
938 }
939
940 #[test]
941 fn test_no_map() -> io::Result<()> {
942 let dir = testdir!();
943 let test_file = dir.join("unknown_rom.nv");
944 let _ = File::create(&test_file)?;
945 let nvram = Nvram::open(&test_file)?;
946 assert_eq!(true, nvram.is_none());
947 Ok(())
948 }
949
950 #[test]
951 fn test_find_map() -> io::Result<()> {
952 let map: Option<Value> = find_map(&"afm_113b".to_string())?;
953 assert_eq!(true, map.is_some());
954 Ok(())
955 }
956}