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("scores") {
598 let scores: Result<Vec<LastGamePlayer>, io::Error> = match scores {
599 StateOrStateList::StateList(sl) => sl
600 .iter()
601 .map(|d| read_last_game_player(&mut nvram_file, d, endian, nibble, offset))
602 .collect(),
603 _other => {
604 return Err(io::Error::new(
605 io::ErrorKind::InvalidData,
606 "Scores is not a StateList",
607 ));
608 }
609 };
610 Ok(Some(scores?))
611 } else {
612 Ok(None)
613 }
614 } else {
615 Ok(None)
616 }
617}
618
619fn read_mode_champions<T: Read + Seek>(
620 mut nvram_file: &mut T,
621 endian: Endian,
622 nibble: Nibble,
623 offset: u64,
624 map: &NvramMap,
625) -> io::Result<Option<Vec<ModeChampion>>> {
626 if let Some(mode_champions) = &map.mode_champions {
627 let champions: Result<Vec<ModeChampion>, io::Error> = mode_champions
628 .iter()
629 .map(|mc| read_mode_champion(&mut nvram_file, mc, endian, nibble, offset, map))
630 .collect();
631 Ok(Some(champions?))
632 } else {
633 Ok(None)
634 }
635}
636
637fn read_replay_score<T: Read + Seek>(
638 mut nvram_file: &mut T,
639 endian: Endian,
640 nibble: Nibble,
641 offset: u64,
642 map: &NvramMap,
643) -> io::Result<Option<u64>> {
644 if let Some(descriptor) = &map.replay_score {
645 let value = read_descriptor_to_u64(&mut nvram_file, descriptor, endian, nibble, offset)?;
646 Ok(Some(value))
647 } else {
648 Ok(None)
649 }
650}
651
652fn read_game_state<T: Read + Seek>(
653 mut nvram_file: &mut T,
654 endian: Endian,
655 nibble: Nibble,
656 offset: u64,
657 map: &NvramMap,
658) -> io::Result<Option<HashMap<String, String>>> {
659 if let Some(game_state) = &map.game_state {
660 let state: Result<HashMap<String, String>, io::Error> = game_state
662 .iter()
663 .flat_map(|(key, v)| match v {
664 StateOrStateList::State(s) => {
665 let r =
666 read_descriptor_to_string(&mut nvram_file, s, endian, nibble, offset, map)
667 .map(|r| (key.clone(), r));
668 vec![r]
669 }
670 StateOrStateList::StateList(sl) => sl
671 .iter()
672 .enumerate()
673 .map(|(index, s)| {
674 let compund_key = format!("{key}.{index}");
675 read_descriptor_to_string(&mut nvram_file, s, endian, nibble, offset, map)
676 .map(|r| (compund_key, r))
677 })
678 .collect(),
679 StateOrStateList::Notes(_) => {
680 vec![]
681 }
682 })
683 .collect();
684
685 Ok(Some(state?))
686 } else {
687 Ok(None)
688 }
689}
690
691fn read_descriptor_to_string<T: Read + Seek, S: GlobalSettings>(
692 mut nvram_file: &mut T,
693 descriptor: &Descriptor,
694 endian: Endian,
695 nibble: Nibble,
696 offset: u64,
697 global_settings: &S,
698) -> io::Result<String> {
699 match descriptor.encoding {
700 Encoding::Ch => match &descriptor.start {
701 Some(start) => read_ch(
702 &mut nvram_file,
703 u64::from(start) - offset,
704 descriptor.length.unwrap_or(DEFAULT_LENGTH),
705 descriptor.mask.as_ref().map(|m| m.into()),
706 global_settings.char_map(),
707 descriptor.nibble.unwrap_or(nibble),
708 None,
709 ),
710 None => Err(io::Error::new(
711 io::ErrorKind::InvalidData,
712 "Ch descriptor requires start",
713 )),
714 },
715 Encoding::Int => match &descriptor.start {
716 Some(start) => {
717 let start = u64::from(start);
718 if start < offset {
719 return Ok("Value is stored outside the NVRAM".to_string());
720 }
721 let score = read_int(
722 &mut nvram_file,
723 endian,
724 nibble,
725 start - offset,
726 descriptor.length.unwrap_or(DEFAULT_LENGTH),
727 descriptor
728 .scale
729 .as_ref()
730 .unwrap_or(&Number::from(DEFAULT_SCALE)),
731 )?;
732 Ok(score.to_string())
733 }
734 None => Err(io::Error::new(
735 io::ErrorKind::InvalidData,
736 "Int descriptor requires start",
737 )),
738 },
739 Encoding::Bcd => {
740 let location = match location_for(descriptor, offset)? {
741 LocateResult::OutsideNVRAM => {
742 return Ok("Value is stored outside the NVRAM".to_string());
743 }
744 LocateResult::Located(loc) => loc,
745 };
746 let score = read_bcd(
747 &mut nvram_file,
748 location,
749 descriptor.nibble.unwrap_or(nibble),
750 descriptor
751 .scale
752 .as_ref()
753 .unwrap_or(&Number::from(DEFAULT_SCALE)),
754 endian,
755 )?;
756 Ok(score.to_string())
757 }
758 Encoding::Bits => Ok("Bits encoding not implemented".to_string()),
759 Encoding::Bool => match &descriptor.start {
760 Some(start) => {
761 let bool = read_bool(
762 nvram_file,
763 start.into(),
764 nibble,
765 endian,
766 descriptor.length.unwrap_or(DEFAULT_LENGTH),
767 descriptor.invert.unwrap_or(DEFAULT_INVERT),
768 )?;
769 Ok(bool.to_string())
770 }
771 None => Err(io::Error::new(
772 io::ErrorKind::InvalidData,
773 "Bool descriptor requires start",
774 )),
775 },
776 other => todo!("Encoding not implemented: {:?}", other),
777 }
778}
779
780fn read_descriptor_to_u64<T: Read + Seek>(
781 mut nvram_file: &mut T,
782 descriptor: &Descriptor,
783 endian: Endian,
784 nibble: Nibble,
785 offset: u64,
786) -> io::Result<u64> {
787 match descriptor.encoding {
788 Encoding::Bcd => {
789 let location = match location_for(descriptor, offset)? {
790 LocateResult::OutsideNVRAM => {
791 return Err(io::Error::new(
792 io::ErrorKind::InvalidData,
793 format!(
794 "Descriptor '{}' points outside NVRAM",
795 descriptor.label.as_deref().unwrap_or("unknown")
796 ),
797 ));
798 }
799 LocateResult::Located(loc) => loc,
800 };
801 read_bcd(
802 &mut nvram_file,
803 location,
804 descriptor.nibble.unwrap_or(nibble),
805 descriptor
806 .scale
807 .as_ref()
808 .unwrap_or(&Number::from(DEFAULT_SCALE)),
809 endian,
810 )
811 }
812 Encoding::Int => {
813 if let Some(start) = &descriptor.start {
814 read_int(
815 &mut nvram_file,
816 endian,
817 descriptor.nibble.unwrap_or(nibble),
818 u64::from(start) - offset,
819 descriptor.length.unwrap_or(DEFAULT_LENGTH),
820 descriptor
821 .scale
822 .as_ref()
823 .unwrap_or(&Number::from(DEFAULT_SCALE)),
824 )
825 } else {
826 Err(io::Error::new(
827 io::ErrorKind::InvalidData,
828 "Int descriptor requires start",
829 ))
830 }
831 }
832 other => todo!("Encoding not implemented: {:?}", other),
833 }
834}
835
836fn read_descriptor_to_rtc_string<T: Read + Seek>(
837 mut nvram_file: &mut T,
838 ts: &Descriptor,
839) -> io::Result<String> {
840 match &ts.encoding {
841 Encoding::WpcRtc => read_wpc_rtc(
842 &mut nvram_file,
843 ts.start
844 .as_ref()
845 .expect("missing start for wpc_rtc encoding")
846 .into(),
847 ts.length.expect("missing length for wpc_rtc encoding"),
848 ),
849 other => todo!("Timestamp encoding not implemented: {:?}", other),
850 }
851}
852
853enum LocateResult {
854 OutsideNVRAM,
855 Located(Location),
856}
857
858fn location_for(descriptor: &Descriptor, offset: u64) -> io::Result<LocateResult> {
859 match &descriptor.offsets {
860 None => match &descriptor.start {
861 Some(start) => {
862 let start = u64::from(start);
863 if start < offset {
864 return Ok(LocateResult::OutsideNVRAM);
865 }
866 Ok(LocateResult::Located(Location::Continuous {
867 start: start - offset,
868 length: descriptor.length.unwrap_or(DEFAULT_LENGTH),
869 }))
870 }
871 _ => Err(io::Error::new(
872 io::ErrorKind::InvalidData,
873 "Descriptor without offsets requires start",
874 )),
875 },
876 Some(offsets) => Ok(LocateResult::Located(Location::Scattered {
877 offsets: offsets.iter().map(|o| u64::from(o) - offset).collect(),
878 })),
879 }
880}
881
882#[cfg(test)]
883mod tests {
884 use super::*;
885 use pretty_assertions::assert_eq;
886 use serde_json::Value;
887 use std::fs::File;
888 use testdir::testdir;
889
890 #[test]
891 fn test_not_found() {
892 let nvram = Nvram::open(Path::new("does_not_exist.nv"));
893 assert!(matches!(
894 nvram,
895 Err(ref e) if e.kind() == io::ErrorKind::NotFound && e.to_string() == "File not found: \"does_not_exist.nv\""
896 ));
897 }
898
899 #[test]
900 fn test_no_map() -> io::Result<()> {
901 let dir = testdir!();
902 let test_file = dir.join("unknown_rom.nv");
903 let _ = File::create(&test_file)?;
904 let nvram = Nvram::open(&test_file)?;
905 assert_eq!(true, nvram.is_none());
906 Ok(())
907 }
908
909 #[test]
910 fn test_find_map() -> io::Result<()> {
911 let map: Option<Value> = find_map(&"afm_113b".to_string())?;
912 assert_eq!(true, map.is_some());
913 Ok(())
914 }
915}