Skip to main content

pinmame_nvram/
lib.rs

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)]
39// probably one of both, score or timestamp
40pub 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/// Score of the last game played
50/// For each player that played during the last game, the score is stored.
51#[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
63/// Main interface to read and write data from a NVRAM file
64pub struct Nvram {
65    pub map: NvramMap,
66    pub platform: Platform,
67    pub nv_path: PathBuf,
68}
69
70impl Nvram {}
71
72impl Nvram {
73    /// Open a NVRAM file from the embedded maps
74    ///
75    /// # Returns
76    ///
77    /// * `Ok(Some(Nvram))` if the file was found and a map was found for the ROM
78    /// * `Ok(None)` if the file was found but no map was found for the ROM
79    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            // find the platform from the map
83            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    /// Open a NVRAM file from the file system using the local maps
95    /// Expects the `pinmame-nvram-maps` folder to exist in the current working directory
96    ///
97    /// # Returns
98    ///
99    /// * `Ok(Some(Nvram))` if the file was found and a map was found for the ROM
100    /// * `Ok(None)` if the file was found but no map was found for the ROM
101    pub fn open_local(nv_path: &Path) -> io::Result<Option<Nvram>> {
102        let map_opt: Option<NvramMap> = open_nvram_local(nv_path)?;
103        //let platform = todo!("Determine platform from map");
104        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        // re-open the file in write mode
129        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    // TODO we probably want to remove this
170    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            // centaur
221            // 32 default switches
222            // 3 additional switches for the sound board reverb effect
223            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    /// Get the value of a dip switch
276    /// # Arguments
277    /// * `number` - The number of the dip switch to get, 1-based!
278    /// # Returns
279    /// * `Ok(true)` if the dip switch is ON
280    /// * `Ok(false)` if the dip switch is OFF
281    /// * `Err(io::Error)` if the dip switch number is out of range or an IO error occurred
282    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    /// Set a dip switch to on or off
289    /// # Arguments
290    /// * `number` - The number of the dip switch to set, 1-based!
291    /// * `on` - `true` to set the dip switch to ON, `false` to set it to OFF
292    /// # Returns
293    /// * `Ok(())` if the dip switch was set successfully
294    /// * `Err(io::Error)` if the dip switch number is out of range or an IO error occurred
295    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    // get the rom name from the file name
307    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    // check if file exists
317    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    // get the rom name from the file name
328    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    // check if file exists
338    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        // this is the old location of the last game scores
590        // TODO remove once all maps have been updated
591        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        // sometimes scores is outside nvram and we need to read final_scores instead
598        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                    // .filter(|d| match location_for(d, offset) {
603                    //     Ok(LocateResult::Located(_)) => true,
604                    //     _ => false,
605                    // })
606                    .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        // map the hashmap values to a new hashmap with the values read from the nvram file
680        let state: Result<HashMap<String, String>, io::Error> = game_state
681            .iter()
682            .flat_map(|(key, v)| match v {
683                StateOrStateList::State(s) => {
684                    // skip values outside nvram
685                    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                        // skip values outside nvram
698                        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            // check if any offset is outside nvram
910            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}