diff --git a/src/ui/main_window.rs b/src/ui/main_window.rs index 1049f0d..33e078a 100644 --- a/src/ui/main_window.rs +++ b/src/ui/main_window.rs @@ -46,16 +46,7 @@ impl MainWindow { Ok(settings_dir) => { let fields_file = settings_dir.join(SETTINGS_FILE); match fs::read(fields_file) { - Ok(content) => match serde_json::from_slice::(&content) { - Ok(obj) => { - self.tab_daplink = obj.tab_daplink; - self.tab_ws = obj.tab_ws; - println!("Settings loaded !"); - } - Err(e) => { - eprintln!("Failed to deserialize settings. Error: {e}"); - } - }, + Ok(content) => self.load_settings(&content), Err(e) => eprintln!("Failed to open settings file ({e})"), } } @@ -112,4 +103,104 @@ impl MainWindow { pub fn application_subscription(&self) -> Subscription { event::listen().map(Message::ApplicationEvent) } + + /// Deserialize the on-disk settings, with a fallback to the legacy 0.1.x + /// schema where `TabDaplink` fields were serialized directly at the top + /// level and are now mapped onto `tab_daplink`. Successful migrations get + /// rewritten in the new shape on the next `CloseRequested`. + fn load_settings(&mut self, content: &[u8]) { + match serde_json::from_slice::(content) { + Ok(obj) => { + self.tab_daplink = obj.tab_daplink; + self.tab_ws = obj.tab_ws; + println!("Settings loaded !"); + } + Err(new_err) => match serde_json::from_slice::(content) { + Ok(legacy) => { + self.tab_daplink = legacy; + println!("Legacy settings migrated to the new schema"); + } + Err(legacy_err) => { + eprintln!( + "Failed to deserialize settings. New schema error: {new_err}. Legacy schema error: {legacy_err}" + ); + } + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const LEGACY_JSON: &[u8] = br#"{ + "bootloader_path": "/tmp/bl.bin", + "firmware_path": "/tmp/fw.bin", + "user_file_path": "/tmp/user.bin", + "target_waiting_time": 7, + "target_name": "LEGACY-STEAMI" + }"#; + + const NEW_JSON: &[u8] = br#"{ + "tab_daplink": { + "bootloader_path": "/tmp/bl.bin", + "firmware_path": "/tmp/fw.bin", + "user_file_path": "/tmp/user.bin", + "target_waiting_time": 7, + "target_name": "LEGACY-STEAMI" + }, + "tab_ws": { + "fw_selected": "BleHciExt" + } + }"#; + + #[test] + fn loads_new_schema() { + let mut w = MainWindow::default(); + w.load_settings(NEW_JSON); + let s = serde_json::to_string(&w).unwrap(); + assert!(s.contains(r#""target_name":"LEGACY-STEAMI""#)); + assert!(s.contains(r#""fw_selected":"BleHciExt""#)); + } + + #[test] + fn legacy_schema_migrates_and_serializes_in_new_shape() { + let mut w = MainWindow::default(); + w.load_settings(LEGACY_JSON); + + // Re-serialize the way the CloseRequested handler does. + let s = serde_json::to_string(&w).unwrap(); + + // New shape: top-level keys for both tabs. + assert!(s.contains(r#""tab_daplink""#), "missing tab_daplink in: {s}"); + assert!(s.contains(r#""tab_ws""#), "missing tab_ws in: {s}"); + + // Legacy data preserved under tab_daplink. + assert!(s.contains(r#""bootloader_path":"/tmp/bl.bin""#)); + assert!(s.contains(r#""firmware_path":"/tmp/fw.bin""#)); + assert!(s.contains(r#""user_file_path":"/tmp/user.bin""#)); + assert!(s.contains(r#""target_waiting_time":7"#)); + assert!(s.contains(r#""target_name":"LEGACY-STEAMI""#)); + } + + #[test] + fn malformed_json_keeps_defaults() { + let mut w = MainWindow::default(); + w.load_settings(b"{ this is not json"); + // No assertion on stderr — load_settings reports both errors via eprintln, + // exercised manually. The state must remain at defaults: serializing + // shouldn't carry any of the malformed input. + let s = serde_json::to_string(&w).unwrap(); + assert!(!s.contains("LEGACY-STEAMI")); + } + + #[test] + fn empty_content_keeps_defaults() { + let mut w = MainWindow::default(); + w.load_settings(b""); + let s = serde_json::to_string(&w).unwrap(); + assert!(s.contains(r#""tab_daplink""#)); + assert!(s.contains(r#""tab_ws""#)); + } }