Skip to main content

ithmb_core/
profile_db.rs

1//! Profile database — loads profiles from embedded or external JSON.
2//!
3//! The built-in profile data is embedded at compile time via `include_str!`.
4//! An external `profiles.json` can override entries at runtime.
5
6use std::collections::HashMap;
7use std::fs;
8use std::path::Path;
9
10use crate::error::DecodeError;
11use crate::profile::Profile;
12use crate::profile_parser::parse_profiles_json;
13
14/// An in-memory profile database keyed by format prefix.
15#[derive(Debug, Clone)]
16pub struct ProfileDb {
17    profiles: HashMap<i32, Profile>,
18}
19
20impl ProfileDb {
21    /// Load built-in profiles from the embedded `data/profiles.json`.
22    ///
23    /// This is the canonical source of the 53 active (+1 speculative) format
24    /// profiles derived from the C# reference implementation.
25    ///
26    /// # Errors
27    /// Returns `DecodeError::Profile` if the embedded JSON cannot be parsed.
28    pub fn load_builtin() -> Result<Self, DecodeError> {
29        let json = include_str!("../data/profiles.json");
30        let profiles = parse_profiles_json(json)?;
31        let map: HashMap<i32, Profile> = profiles.into_iter().map(|p| (p.prefix, p)).collect();
32        Ok(Self { profiles: map })
33    }
34
35    /// Load an external `profiles.json` file and merge its entries,
36    /// overriding any existing profiles by matching prefix.
37    ///
38    /// # Errors
39    /// Returns `DecodeError::Profile` if the file cannot be read or parsed.
40    pub fn load_external<P: AsRef<Path>>(&mut self, path: P) -> Result<(), DecodeError> {
41        let data = fs::read_to_string(path.as_ref())
42            .map_err(|e| DecodeError::Profile(format!("failed to read '{}': {e}", path.as_ref().display())))?;
43        let profiles = parse_profiles_json(&data)?;
44        for p in profiles {
45            self.profiles.insert(p.prefix, p);
46        }
47        Ok(())
48    }
49
50    /// Look up a profile by its big-endian format prefix.
51    #[must_use]
52    pub fn get(&self, prefix: i32) -> Option<&Profile> {
53        self.profiles.get(&prefix)
54    }
55
56    /// Return a reference to the entire profile map.
57    #[must_use]
58    pub fn all(&self) -> &HashMap<i32, Profile> {
59        &self.profiles
60    }
61
62    /// Return the number of profiles in the database.
63    #[must_use]
64    pub fn len(&self) -> usize {
65        self.profiles.len()
66    }
67
68    /// Returns `true` when no profiles are loaded.
69    #[must_use]
70    pub fn is_empty(&self) -> bool {
71        self.profiles.is_empty()
72    }
73}
74
75// ---------------------------------------------------------------------------
76// Tests
77// ---------------------------------------------------------------------------
78
79#[cfg(test)]
80#[allow(clippy::unwrap_used)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn load_builtin_has_54_profiles() {
86        let db = ProfileDb::load_builtin().unwrap();
87        assert_eq!(db.len(), 54);
88    }
89
90    #[test]
91    fn get_1007_returns_correct_profile() {
92        let db = ProfileDb::load_builtin().unwrap();
93        let p = db.get(1007).expect("profile 1007 should exist");
94        assert_eq!(p.prefix, 1007);
95        assert_eq!(p.width, 480);
96        assert_eq!(p.height, 864);
97        assert_eq!(p.encoding, crate::profile::Encoding::Rgb565);
98        assert_eq!(p.frame_byte_length, 829_440);
99        assert!(!p.is_padded);
100        assert!(p.little_endian); // default
101    }
102
103    #[test]
104    fn get_9999_returns_none() {
105        let db = ProfileDb::load_builtin().unwrap();
106        assert!(db.get(9999).is_none());
107    }
108
109    #[test]
110    fn get_1061_has_use_mhni_dimensions() {
111        let db = ProfileDb::load_builtin().unwrap();
112        let p = db.get(1061).expect("profile 1061 should exist");
113        assert!(p.use_mhni_dimensions);
114    }
115
116    #[test]
117    fn get_3004_has_padded_slot() {
118        let db = ProfileDb::load_builtin().unwrap();
119        let p = db.get(3004).expect("profile 3004 should exist");
120        assert!(p.is_padded);
121        assert_eq!(p.slot_size, 8192);
122        assert_eq!(p.encoding, crate::profile::Encoding::Rgb555);
123    }
124
125    #[test]
126    fn all_returns_full_map() {
127        let db = ProfileDb::load_builtin().unwrap();
128        let all = db.all();
129        assert!(all.contains_key(&1007));
130        assert!(all.contains_key(&3011));
131        assert_eq!(all.len(), 54);
132    }
133}