Skip to main content

trueno/tuner/brick_tuner/
persistence.rs

1//! APR persistence and JSON serialization for BrickTuner.
2//!
3//! Implements the APR1 binary format (uncompressed) and JSON serialization
4//! for saving/loading tuner models.
5
6use super::super::error::TunerError;
7use super::super::helpers::crc32_update;
8use super::BrickTuner;
9
10impl BrickTuner {
11    /// APR format magic bytes (APR1 = uncompressed)
12    const APR_MAGIC: [u8; 4] = [b'A', b'P', b'R', b'1'];
13
14    /// Serialize to JSON
15    pub fn to_json(&self) -> Result<String, TunerError> {
16        serde_json::to_string_pretty(self).map_err(|e| TunerError::Serialization(e.to_string()))
17    }
18
19    /// Deserialize from JSON
20    pub fn from_json(json: &str) -> Result<Self, TunerError> {
21        serde_json::from_str(json).map_err(|e| TunerError::Serialization(e.to_string()))
22    }
23
24    // =========================================================================
25    // APR Persistence (SOVEREIGN STACK - GH#81)
26    // =========================================================================
27
28    /// Get the default cache path for tuner models.
29    ///
30    /// Returns `~/.cache/trueno/tuner_model_v{VERSION}.apr`
31    #[cfg(feature = "hardware-detect")]
32    pub fn cache_path() -> std::path::PathBuf {
33        let cache_dir =
34            dirs::cache_dir().unwrap_or_else(|| std::path::PathBuf::from(".")).join("trueno");
35
36        // Create directory if it doesn't exist
37        let _ = std::fs::create_dir_all(&cache_dir);
38
39        cache_dir.join(format!("tuner_model_v{}.apr", Self::VERSION))
40    }
41
42    /// Load tuner from cache or create new with defaults.
43    ///
44    /// This is the recommended way to create a BrickTuner for production use.
45    /// It will:
46    /// 1. Check for cached model at `~/.cache/trueno/tuner_model_v{VERSION}.apr`
47    /// 2. Load if exists and version matches
48    /// 3. Create new with defaults if not found or version mismatch
49    #[cfg(feature = "hardware-detect")]
50    pub fn load_or_default() -> Self {
51        let path = Self::cache_path();
52
53        if path.exists() {
54            match Self::load_apr(&path) {
55                Ok(tuner) => {
56                    // Version check
57                    if tuner.version == Self::VERSION {
58                        return tuner;
59                    }
60                    // Version mismatch - create new
61                }
62                Err(_) => {
63                    // Load failed - create new
64                }
65            }
66        }
67
68        Self::new()
69    }
70
71    /// Save tuner model to .apr file.
72    ///
73    /// APR1 format (uncompressed):
74    /// - 4-byte magic: "APR1"
75    /// - 4-byte metadata_len: u32 LE
76    /// - JSON metadata
77    /// - 4-byte CRC32: checksum
78    pub fn save_apr<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), TunerError> {
79        use std::io::Write;
80
81        let json = self.to_json()?;
82        let json_bytes = json.as_bytes();
83
84        let mut file = std::fs::File::create(path).map_err(|e| TunerError::Io(e.to_string()))?;
85
86        // Write magic
87        file.write_all(&Self::APR_MAGIC).map_err(|e| TunerError::Io(e.to_string()))?;
88
89        // Write metadata length
90        let len = json_bytes.len() as u32;
91        file.write_all(&len.to_le_bytes()).map_err(|e| TunerError::Io(e.to_string()))?;
92
93        // Write JSON metadata
94        file.write_all(json_bytes).map_err(|e| TunerError::Io(e.to_string()))?;
95
96        // Calculate and write CRC32
97        let mut crc = 0u32;
98        crc = crc32_update(crc, &Self::APR_MAGIC);
99        crc = crc32_update(crc, &len.to_le_bytes());
100        crc = crc32_update(crc, json_bytes);
101        file.write_all(&crc.to_le_bytes()).map_err(|e| TunerError::Io(e.to_string()))?;
102
103        Ok(())
104    }
105
106    /// Load tuner model from .apr file.
107    pub fn load_apr<P: AsRef<std::path::Path>>(path: P) -> Result<Self, TunerError> {
108        use std::io::Read;
109
110        let mut file = std::fs::File::open(path).map_err(|e| TunerError::Io(e.to_string()))?;
111
112        // Read and verify magic
113        let mut magic = [0u8; 4];
114        file.read_exact(&mut magic).map_err(|e| TunerError::Io(e.to_string()))?;
115
116        if magic != Self::APR_MAGIC {
117            return Err(TunerError::InvalidFormat("Invalid APR magic bytes".to_string()));
118        }
119
120        // Read metadata length
121        let mut len_bytes = [0u8; 4];
122        file.read_exact(&mut len_bytes).map_err(|e| TunerError::Io(e.to_string()))?;
123        let len = u32::from_le_bytes(len_bytes) as usize;
124
125        // Read JSON metadata
126        let mut json_bytes = vec![0u8; len];
127        file.read_exact(&mut json_bytes).map_err(|e| TunerError::Io(e.to_string()))?;
128
129        // Read and verify CRC32
130        let mut crc_bytes = [0u8; 4];
131        file.read_exact(&mut crc_bytes).map_err(|e| TunerError::Io(e.to_string()))?;
132        let stored_crc = u32::from_le_bytes(crc_bytes);
133
134        let mut computed_crc = 0u32;
135        computed_crc = crc32_update(computed_crc, &Self::APR_MAGIC);
136        computed_crc = crc32_update(computed_crc, &len_bytes);
137        computed_crc = crc32_update(computed_crc, &json_bytes);
138
139        if stored_crc != computed_crc {
140            return Err(TunerError::InvalidFormat("CRC32 checksum mismatch".to_string()));
141        }
142
143        // Parse JSON
144        let json =
145            String::from_utf8(json_bytes).map_err(|e| TunerError::Serialization(e.to_string()))?;
146
147        Self::from_json(&json)
148    }
149
150    /// Save to default cache path.
151    #[cfg(feature = "hardware-detect")]
152    pub fn save_to_cache(&self) -> Result<(), TunerError> {
153        self.save_apr(Self::cache_path())
154    }
155}