From 7eea94a63aad9a05b3548fb7ca584c0fb61d175c Mon Sep 17 00:00:00 2001 From: mtkennerly Date: Mon, 30 Dec 2024 21:30:59 -0500 Subject: [PATCH] Add --dump-registry option --- CHANGELOG.md | 4 + src/cli.rs | 22 ++++- src/cli/parse.rs | 20 +++++ src/cli/report.rs | 172 ++++++++++++++++++++++++++++++++++++++- src/scan.rs | 73 ++++++++++++++++- src/scan/layout.rs | 22 ++++- src/scan/preview.rs | 7 +- src/scan/registry/win.rs | 35 ++++---- src/scan/saves.rs | 7 +- 9 files changed, 335 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e7ec9f..f173eee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ For more info, [see the custom games document](/docs/help/custom-games.md). * CLI: The `backup`, `restore`, `cloud upload`, and `cloud download` commands now support a `--gui` option for graphical dialog prompts. + * CLI: The `backup` and `restore` commands now support a `--dump-registry` option, + which includes the serialized registry content in the output. + This may be useful if you're consuming the `--api` output to back up with another tool, + but don't have a good way to check the registry keys directly. * Changed: * When the game list is filtered, the summary line (e.g., "1 of 10 games") now reflects the filtered totals. diff --git a/src/cli.rs b/src/cli.rs index a24020e..6f93ba6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -152,6 +152,7 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool) differential_limit, cloud_sync, no_cloud_sync, + dump_registry, games, } => { let games = parse_games(games); @@ -353,7 +354,14 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool) } for (name, scan_info, backup_info, decision) in info { - if !reporter.add_game(name, &scan_info, backup_info.as_ref(), &decision, &duplicate_detector) { + if !reporter.add_game( + name, + &scan_info, + backup_info.as_ref(), + &decision, + &duplicate_detector, + dump_registry, + ) { failed = true; } } @@ -369,6 +377,7 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool) backup, cloud_sync, no_cloud_sync, + dump_registry, games, } => { let games = parse_games(games); @@ -517,7 +526,14 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool) } for (name, scan_info, backup_info, decision, _) in info { - if !reporter.add_game(name, &scan_info, Some(&backup_info), &decision, &duplicate_detector) { + if !reporter.add_game( + name, + &scan_info, + Some(&backup_info), + &decision, + &duplicate_detector, + dump_registry, + ) { failed = true; } } @@ -928,6 +944,7 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool) backup: Default::default(), cloud_sync: Default::default(), no_cloud_sync: Default::default(), + dump_registry: Default::default(), }, no_manifest_update, try_manifest_update, @@ -979,6 +996,7 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool) differential_limit: Default::default(), cloud_sync: Default::default(), no_cloud_sync: Default::default(), + dump_registry: Default::default(), }, no_manifest_update, try_manifest_update, diff --git a/src/cli/parse.rs b/src/cli/parse.rs index a147546..59e6138 100644 --- a/src/cli/parse.rs +++ b/src/cli/parse.rs @@ -209,6 +209,11 @@ pub enum Subcommand { #[clap(long, conflicts_with("cloud_sync"))] no_cloud_sync: bool, + /// Include the serialized registry content in the output. + /// Only includes the native Windows registry, not Wine. + #[clap(long)] + dump_registry: bool, + /// Only back up these specific games. /// Alternatively supports stdin (one value per line). #[clap()] @@ -260,6 +265,11 @@ pub enum Subcommand { #[clap(long, conflicts_with("cloud_sync"))] no_cloud_sync: bool, + /// Include the serialized registry content in the output. + /// Only includes the native Windows registry, not Wine. + #[clap(long)] + dump_registry: bool, + /// Only restore these specific games. /// Alternatively supports stdin (one value per line). #[clap()] @@ -721,6 +731,7 @@ mod tests { differential_limit: None, cloud_sync: false, no_cloud_sync: false, + dump_registry: false, games: vec![], }), }, @@ -754,6 +765,7 @@ mod tests { "--differential-limit", "2", "--cloud-sync", + "--dump-registry", "game1", "game2", ], @@ -776,6 +788,7 @@ mod tests { differential_limit: Some(2), cloud_sync: true, no_cloud_sync: false, + dump_registry: true, games: vec![s("game1"), s("game2")], }), }, @@ -805,6 +818,7 @@ mod tests { differential_limit: None, cloud_sync: false, no_cloud_sync: false, + dump_registry: false, games: vec![], }), }, @@ -842,6 +856,7 @@ mod tests { differential_limit: None, cloud_sync: false, no_cloud_sync: false, + dump_registry: false, games: vec![], }), }, @@ -872,6 +887,7 @@ mod tests { differential_limit: None, cloud_sync: false, no_cloud_sync: false, + dump_registry: false, games: vec![], }), }, @@ -896,6 +912,7 @@ mod tests { backup: None, cloud_sync: false, no_cloud_sync: false, + dump_registry: false, games: vec![], }), }, @@ -918,6 +935,7 @@ mod tests { "--backup", ".", "--cloud-sync", + "--dump-registry", "game1", "game2", ], @@ -938,6 +956,7 @@ mod tests { backup: Some(s(".")), cloud_sync: true, no_cloud_sync: false, + dump_registry: true, games: vec![s("game1"), s("game2")], }), }, @@ -978,6 +997,7 @@ mod tests { backup: None, cloud_sync: false, no_cloud_sync: false, + dump_registry: false, games: vec![], }), }, diff --git a/src/cli/report.rs b/src/cli/report.rs index e9d60c1..3aa8879 100644 --- a/src/cli/report.rs +++ b/src/cli/report.rs @@ -8,8 +8,8 @@ use crate::{ prelude::StrictPath, resource::manifest::Os, scan::{ - layout::Backup, BackupError, BackupInfo, DuplicateDetector, OperationStatus, OperationStepDecision, ScanChange, - ScanInfo, + layout::Backup, registry, BackupError, BackupInfo, DuplicateDetector, OperationStatus, OperationStepDecision, + ScanChange, ScanInfo, }, }; @@ -136,6 +136,22 @@ struct ApiRegistryValue { duplicated_by: BTreeSet, } +#[derive(Debug, Default, serde::Serialize, schemars::JsonSchema)] +#[serde(rename_all = "camelCase")] +struct ApiDump { + /// Serialized registry content, if any, when enabled by `--dump-registry`. + #[serde(skip_serializing_if = "Option::is_none")] + registry: Option, +} + +impl ApiDump { + pub fn is_empty(&self) -> bool { + let Self { registry } = self; + + registry.is_none() + } +} + #[derive(Debug, serde::Serialize, schemars::JsonSchema)] #[serde(untagged, rename_all = "camelCase")] enum ApiGame { @@ -150,6 +166,9 @@ enum ApiGame { files: BTreeMap, /// Each key is a registry path. registry: BTreeMap, + /// Raw data. + #[serde(skip_serializing_if = "ApiDump::is_empty")] + dump: ApiDump, }, /// Used by the `backups` command. Stored { @@ -288,6 +307,7 @@ impl Reporter { backup_info: Option<&BackupInfo>, decision: &OperationStepDecision, duplicate_detector: &DuplicateDetector, + dump_registry: bool, ) -> bool { if !scan_info.can_report_game() { return true; @@ -374,6 +394,13 @@ impl Reporter { } } + if let Some(dumped_registry) = scan_info.dumped_registry.as_ref() { + let label = TRANSLATOR.custom_registry_label(); + parts.push(format!("---------- {} ----------", &label)); + parts.push(dumped_registry.serialize(registry::Format::Reg)); + parts.push("-".repeat(22 + label.len())); + } + // Blank line between games. parts.push("".to_string()); @@ -476,6 +503,17 @@ impl Reporter { registry.insert(scan_key.render(), api_registry); } + let dump = ApiDump { + registry: dump_registry + .then(|| { + scan_info + .dumped_registry + .as_ref() + .map(|hives| hives.serialize(registry::Format::Reg)) + }) + .flatten(), + }; + if let Some(overall) = output.overall.as_mut() { overall.add_game(scan_info, backup_info, decision == OperationStepDecision::Processed); } @@ -486,6 +524,7 @@ impl Reporter { change: scan_info.overall_change(), files, registry, + dump, }, ); } @@ -633,7 +672,7 @@ pub fn report_cloud_changes(changes: &[CloudChange], api: bool) { #[cfg(test)] mod tests { use pretty_assertions::assert_eq; - use velcro::hash_map; + use velcro::{btree_map, hash_map}; use super::*; use crate::{ @@ -650,6 +689,7 @@ mod tests { None, &OperationStepDecision::Processed, &DuplicateDetector::default(), + false, ); assert_eq!( r#" @@ -708,6 +748,7 @@ Overall: }), &OperationStepDecision::Processed, &DuplicateDetector::default(), + false, ); assert_eq!( r#" @@ -756,6 +797,7 @@ Overall: None, &OperationStepDecision::Processed, &DuplicateDetector::default(), + false, ); reporter.add_game( "bar", @@ -778,6 +820,7 @@ Overall: None, &OperationStepDecision::Processed, &DuplicateDetector::default(), + false, ); assert_eq!( r#" @@ -831,6 +874,7 @@ Overall: None, &OperationStepDecision::Processed, &DuplicateDetector::default(), + false, ); assert_eq!( r#" @@ -884,6 +928,7 @@ Overall: None, &OperationStepDecision::Processed, &duplicate_detector, + false, ); assert_eq!( r#" @@ -921,6 +966,7 @@ Overall: None, &OperationStepDecision::Processed, &DuplicateDetector::default(), + false, ); reporter.add_game( "bar", @@ -935,6 +981,7 @@ Overall: None, &OperationStepDecision::Processed, &DuplicateDetector::default(), + false, ); assert_eq!( r#" @@ -957,6 +1004,54 @@ Overall: ); } + #[test] + fn can_render_in_standard_mode_with_dumps() { + let mut reporter = Reporter::standard(); + + reporter.add_game( + "foo", + &ScanInfo { + game_name: s("foo"), + found_registry_keys: hash_map! { + "HKEY_CURRENT_USER/Key".into(): ScannedRegistry::new().with_value_same("value"), + }, + dumped_registry: Some(registry::Hives(btree_map! { + r"HKEY_CURRENT_USER".into(): registry::Keys(btree_map! { + r"Key".into(): registry::Entries(btree_map! { + "value".into(): registry::Entry::Sz("data".to_string()), + }), + }) + })), + ..Default::default() + }, + None, + &OperationStepDecision::Processed, + &DuplicateDetector::default(), + true, + ); + assert_eq!( + r#" +foo [0 B]: + - HKEY_CURRENT_USER/Key + - value +---------- Registry: ---------- +Windows Registry Editor Version 5.00 + +[HKEY_CURRENT_USER\Key] +"value"="data" + +------------------------------- + +Overall: + Games: 1 + Size: 0 B + Location: /dev/null + "# + .trim(), + reporter.render(&StrictPath::new(s("/dev/null"))) + ); + } + #[test] fn can_render_in_json_mode_with_minimal_input() { let mut reporter = Reporter::json(); @@ -967,6 +1062,7 @@ Overall: None, &OperationStepDecision::Processed, &DuplicateDetector::default(), + true, ); assert_eq!( r#" @@ -1019,6 +1115,7 @@ Overall: }), &OperationStepDecision::Processed, &DuplicateDetector::default(), + false, ); assert_eq!( r#" @@ -1118,6 +1215,7 @@ Overall: None, &OperationStepDecision::Processed, &DuplicateDetector::default(), + false, ); assert_eq!( r#" @@ -1193,6 +1291,7 @@ Overall: None, &OperationStepDecision::Processed, &duplicate_detector, + false, ); assert_eq!( r#" @@ -1258,6 +1357,7 @@ Overall: None, &OperationStepDecision::Processed, &DuplicateDetector::default(), + false, ); assert_eq!( r#" @@ -1298,6 +1398,72 @@ Overall: "registry": {} } } +} + "# + .trim(), + reporter.render(&StrictPath::new(s("/dev/null"))) + ); + } + + #[test] + fn can_render_in_json_mode_with_dumps() { + let mut reporter = Reporter::json(); + + reporter.add_game( + "foo", + &ScanInfo { + game_name: s("foo"), + found_registry_keys: hash_map! { + "HKEY_CURRENT_USER/Key".into(): ScannedRegistry::new().change_as(ScanChange::Same).with_value_same("value"), + }, + dumped_registry: Some(registry::Hives(btree_map! { + r"HKEY_CURRENT_USER".into(): registry::Keys(btree_map! { + r"Key".into(): registry::Entries(btree_map! { + "value".into(): registry::Entry::Sz("data".to_string()), + }), + }) + })), + ..Default::default() + }, + None, + &OperationStepDecision::Processed, + &DuplicateDetector::default(), + true, + ); + assert_eq!( + r#" +{ + "overall": { + "totalGames": 1, + "totalBytes": 0, + "processedGames": 1, + "processedBytes": 0, + "changedGames": { + "new": 0, + "different": 0, + "same": 1 + } + }, + "games": { + "foo": { + "decision": "Processed", + "change": "Same", + "files": {}, + "registry": { + "HKEY_CURRENT_USER/Key": { + "change": "Same", + "values": { + "value": { + "change": "Same" + } + } + } + }, + "dump": { + "registry": "Windows Registry Editor Version 5.00\n\n[HKEY_CURRENT_USER\\Key]\n\"value\"=\"data\"\n" + } + } + } } "# .trim(), diff --git a/src/scan.rs b/src/scan.rs index fe3c4fc..9a6f8f0 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -518,6 +518,8 @@ pub fn scan_game_for_backup( let mut found_files = HashMap::new(); #[cfg_attr(not(target_os = "windows"), allow(unused))] let mut found_registry_keys = HashMap::new(); + #[allow(unused)] + let mut dumped_registry = None; let has_backups = previous.is_some(); let mut paths_to_check = HashSet::<(StrictPath, Option)>::new(); @@ -783,6 +785,7 @@ pub fn scan_game_for_backup( #[cfg(target_os = "windows")] { let previous_registry = previous.and_then(|x| x.registry_content); + let mut current_registry = registry::Hives::default(); for key in game.registry.keys() { if key.trim().is_empty() { @@ -805,10 +808,10 @@ pub fn scan_game_for_backup( )); } - for candidate in candidates { + for candidate in &candidates { log::trace!("[{name}] checking registry: {candidate}"); for (scan_key, mut scanned) in - registry::win::scan_registry(name, &candidate, filter, ignored_registry, previous_registry.as_ref()) + registry::win::scan_registry(name, candidate, filter, ignored_registry, previous_registry.as_ref()) .unwrap_or_default() { log::debug!("[{name}] found registry: {}", scan_key.raw()); @@ -832,6 +835,8 @@ pub fn scan_game_for_backup( } } + let _ = current_registry.back_up_key(name, &scan_key, &scanned); + found_registry_keys.insert(scan_key, scanned); } } @@ -857,6 +862,8 @@ pub fn scan_game_for_backup( } } } + + dumped_registry = (!current_registry.is_empty()).then_some(current_registry); } log::trace!("[{name}] completed scan for backup"); @@ -868,6 +875,7 @@ pub fn scan_game_for_backup( available_backups: vec![], backup: None, has_backups, + dumped_registry, } } @@ -1527,6 +1535,18 @@ mod tests { .with_value_new("qword") .with_value_new("sz") }, + dumped_registry: Some(registry::Hives(btree_map! { + r"HKEY_CURRENT_USER".into(): registry::Keys(btree_map! { + r"Software\Ludusavi\game3".into(): registry::Entries(btree_map! { + "binary".into(): registry::Entry::Binary(vec![65]), + "dword".into(): registry::Entry::Dword(1), + "expandSz".into(): registry::Entry::ExpandSz("baz".to_string()), + "multiSz".into(): registry::Entry::MultiSz("bar".to_string()), + "qword".into(): registry::Entry::Qword(2), + "sz".into(): registry::Entry::Sz("foo".to_string()), + }), + }) + })), ..Default::default() }, scan_game_for_backup( @@ -1567,6 +1587,23 @@ mod tests { .with_value_new("dword"), "HKEY_CURRENT_USER/Software/Ludusavi/other".into(): ScannedRegistry::new().change_as(ScanChange::New), }, + dumped_registry: Some(registry::Hives(btree_map! { + r"HKEY_CURRENT_USER".into(): registry::Keys(btree_map! { + r"Software\Ludusavi".into(): registry::Entries(btree_map! {}), + r"Software\Ludusavi\game3".into(): registry::Entries(btree_map! { + "binary".into(): registry::Entry::Binary(vec![65]), + "dword".into(): registry::Entry::Dword(1), + "expandSz".into(): registry::Entry::ExpandSz("baz".to_string()), + "multiSz".into(): registry::Entry::MultiSz("bar".to_string()), + "qword".into(): registry::Entry::Qword(2), + "sz".into(): registry::Entry::Sz("foo".to_string()), + }), + r"Software\Ludusavi\invalid".into(): registry::Entries(btree_map! { + "dword".into(): registry::Entry::Raw { kind: registry::RegistryKind::Dword, data: vec![0, 0, 0, 0, 0, 0, 0, 0] }, + }), + r"Software\Ludusavi\other".into(): registry::Entries(btree_map! {}), + }) + })), ..Default::default() }, scan_game_for_backup( @@ -1610,6 +1647,19 @@ mod tests { .with_value_new("qword") .with_value_new("sz"), }, + Some(registry::Hives(btree_map! { + r"HKEY_CURRENT_USER".into(): registry::Keys(btree_map! { + r"Software\Ludusavi".into(): registry::Entries(btree_map! {}), + r"Software\Ludusavi\game3".into(): registry::Entries(btree_map! { + "binary".into(): registry::Entry::Binary(vec![65]), + "dword".into(): registry::Entry::Dword(1), + "expandSz".into(): registry::Entry::ExpandSz("baz".to_string()), + "multiSz".into(): registry::Entry::MultiSz("bar".to_string()), + "qword".into(): registry::Entry::Qword(2), + "sz".into(): registry::Entry::Sz("foo".to_string()), + }), + }) + })), ), ( BackupFilter::default(), @@ -1631,6 +1681,7 @@ mod tests { .with_value("dword", ScanChange::New, true), "HKEY_CURRENT_USER/Software/Ludusavi/other".into(): ScannedRegistry::new().ignored().change_as(ScanChange::New), }, + None, ), ( BackupFilter::default(), @@ -1658,15 +1709,31 @@ mod tests { .with_value_new("dword"), "HKEY_CURRENT_USER/Software/Ludusavi/other".into(): ScannedRegistry::new().ignored().change_as(ScanChange::New), }, + Some(registry::Hives(btree_map! { + r"HKEY_CURRENT_USER".into(): registry::Keys(btree_map! { + r"Software\Ludusavi".into(): registry::Entries(btree_map! {}), + r"Software\Ludusavi\game3".into(): registry::Entries(btree_map! { + "binary".into(): registry::Entry::Binary(vec![65]), + "dword".into(): registry::Entry::Dword(1), + "expandSz".into(): registry::Entry::ExpandSz("baz".to_string()), + "multiSz".into(): registry::Entry::MultiSz("bar".to_string()), + "sz".into(): registry::Entry::Sz("foo".to_string()), + }), + r"Software\Ludusavi\invalid".into(): registry::Entries(btree_map! { + "dword".into(): registry::Entry::Raw { kind: registry::RegistryKind::Dword, data: vec![0, 0, 0, 0, 0, 0, 0, 0] }, + }), + }) + })), ), ]; - for (filter, ignored, found) in cases { + for (filter, ignored, found, dumped_registry) in cases { assert_eq!( ScanInfo { game_name: s("game3-outer"), found_files: hash_map! {}, found_registry_keys: found, + dumped_registry, ..Default::default() }, scan_game_for_backup( diff --git a/src/scan/layout.rs b/src/scan/layout.rs index 62e55ce..f332795 100644 --- a/src/scan/layout.rs +++ b/src/scan/layout.rs @@ -623,6 +623,8 @@ impl GameLayout { available_backups: vec![], backup: None, has_backups: true, + // Registry is handled separately. + dumped_registry: None, }) } } @@ -1581,9 +1583,10 @@ impl GameLayout { let mut found_files = HashMap::new(); #[cfg_attr(not(target_os = "windows"), allow(unused_mut))] let mut found_registry_keys = HashMap::new(); - #[cfg_attr(not(target_os = "windows"), allow(unused_mut))] let mut available_backups = vec![]; let mut backup = None; + #[cfg_attr(not(target_os = "windows"), allow(unused_mut))] + let mut dumped_registry = None; let id = self.verify_id(id); @@ -1651,6 +1654,8 @@ impl GameLayout { ); } } + + dumped_registry = Some(hives); } } @@ -1665,6 +1670,7 @@ impl GameLayout { available_backups, backup, has_backups, + dumped_registry, } } @@ -3369,6 +3375,7 @@ mod tests { available_backups: backups.clone(), backup: Some(backups[0].clone()), has_backups: true, + dumped_registry: None, }, layout.scan_for_restoration( "game1", @@ -3416,6 +3423,18 @@ mod tests { ..Default::default() })), has_backups: true, + dumped_registry: Some(registry::Hives(btree_map! { + r"HKEY_CURRENT_USER".into(): registry::Keys(btree_map! { + r"Software\Ludusavi\game3".into(): registry::Entries(btree_map! { + "binary".into(): registry::Entry::Binary(vec![65]), + "dword".into(): registry::Entry::Dword(1), + "expandSz".into(): registry::Entry::ExpandSz("baz".to_string()), + "multiSz".into(): registry::Entry::MultiSz("bar".to_string()), + "qword".into(): registry::Entry::Qword(2), + "sz".into(): registry::Entry::Sz("foo".to_string()), + }), + }) + })), }, layout.scan_for_restoration( "game3", @@ -3449,6 +3468,7 @@ mod tests { ..Default::default() })), has_backups: true, + dumped_registry: None, }, layout.scan_for_restoration( "game3", diff --git a/src/scan/preview.rs b/src/scan/preview.rs index b6f4616..0d050d1 100644 --- a/src/scan/preview.rs +++ b/src/scan/preview.rs @@ -4,8 +4,9 @@ use crate::{ path::StrictPath, resource::config::{ToggledPaths, ToggledRegistry}, scan::{ - layout::Backup, registry::RegistryItem, BackupInfo, ScanChange, ScanChangeCount, ScanKind, ScannedFile, - ScannedRegistry, + layout::Backup, + registry::{self, RegistryItem}, + BackupInfo, ScanChange, ScanChangeCount, ScanKind, ScannedFile, ScannedRegistry, }, }; @@ -23,6 +24,8 @@ pub struct ScanInfo { pub backup: Option, /// Cheaper version of `!available_backups.is_empty()`, always populated. pub has_backups: bool, + /// Full registry data, if any. + pub dumped_registry: Option, } impl ScanInfo { diff --git a/src/scan/registry/win.rs b/src/scan/registry/win.rs index 9029ee0..9a3632e 100644 --- a/src/scan/registry/win.rs +++ b/src/scan/registry/win.rs @@ -234,14 +234,6 @@ impl Hives { let mut failed = HashMap::new(); for (scan_key, scanned) in scan { - if scanned.ignored && scanned.values.values().all(|x| x.ignored) { - continue; - } - match scanned.change { - ScanChange::New | ScanChange::Different | ScanChange::Same => (), - ScanChange::Removed | ScanChange::Unknown => continue, - } - if let Err(e) = self.back_up_key(game, scan_key, scanned) { failed.insert(scan_key.clone(), e); } @@ -254,7 +246,20 @@ impl Hives { } } - fn back_up_key(&mut self, game: &str, scan_key: &RegistryItem, scan: &ScannedRegistry) -> Result<(), BackupError> { + pub fn back_up_key( + &mut self, + game: &str, + scan_key: &RegistryItem, + scan: &ScannedRegistry, + ) -> Result<(), BackupError> { + if scan.all_ignored() { + return Ok(()); + } + match scan.change { + ScanChange::New | ScanChange::Different | ScanChange::Same => (), + ScanChange::Removed | ScanChange::Unknown => return Ok(()), + } + let Some((hive_name, key)) = scan_key.split_hive() else { log::error!("[{game}] Unable to split hive: {:?}", scan_key); return Err(BackupError::Raw(format!("Unable to split hive: {}", scan_key.raw()))); @@ -387,7 +392,7 @@ mod tests { #[test] fn can_store_key_from_full_path_of_leaf_key_with_values() { let scan_key = RegistryItem::from("HKEY_CURRENT_USER/Software/Ludusavi/game3"); - let scanned = ScannedRegistry::new(); + let scanned = ScannedRegistry::new().change_as(ScanChange::New); let mut hives = Hives::default(); hives.back_up_key("foo", &scan_key, &scanned).unwrap(); assert_eq!( @@ -410,7 +415,9 @@ mod tests { #[test] fn can_store_key_from_full_path_of_leaf_key_with_ignored_values() { let scan_key = RegistryItem::from("HKEY_CURRENT_USER/Software/Ludusavi/game3"); - let scanned = ScannedRegistry::new().with_value("binary", ScanChange::New, true); + let scanned = ScannedRegistry::new() + .change_as(ScanChange::New) + .with_value("binary", ScanChange::New, true); let mut hives = Hives::default(); hives.back_up_key("foo", &scan_key, &scanned).unwrap(); assert_eq!( @@ -432,7 +439,7 @@ mod tests { #[test] fn can_store_key_from_full_path_of_leaf_key_with_invalid_values() { let scan_key = RegistryItem::from("HKEY_CURRENT_USER/Software/Ludusavi/invalid"); - let scanned = ScannedRegistry::new(); + let scanned = ScannedRegistry::new().change_as(ScanChange::New); let mut hives = Hives::default(); hives.back_up_key("foo", &scan_key, &scanned).unwrap(); assert_eq!( @@ -450,7 +457,7 @@ mod tests { #[test] fn can_store_key_from_full_path_of_leaf_key_without_values() { let scan_key = RegistryItem::from("HKEY_CURRENT_USER/Software/Ludusavi/other"); - let scanned = ScannedRegistry::new(); + let scanned = ScannedRegistry::new().change_as(ScanChange::New); let mut hives = Hives::default(); hives.back_up_key("foo", &scan_key, &scanned).unwrap(); assert_eq!( @@ -466,7 +473,7 @@ mod tests { #[test] fn can_store_key_from_full_path_of_parent_key_without_values() { let scan_key = RegistryItem::from("HKEY_CURRENT_USER/Software/Ludusavi"); - let scanned = ScannedRegistry::new(); + let scanned = ScannedRegistry::new().change_as(ScanChange::New); let mut hives = Hives::default(); hives.back_up_key("foo", &scan_key, &scanned).unwrap(); assert_eq!( diff --git a/src/scan/saves.rs b/src/scan/saves.rs index d56234b..2c7923e 100644 --- a/src/scan/saves.rs +++ b/src/scan/saves.rs @@ -202,8 +202,11 @@ impl ScannedRegistry { } pub fn change(&self, scan_kind: ScanKind) -> ScanChange { - self.change - .normalize(self.ignored && self.values.values().all(|x| x.ignored), scan_kind) + self.change.normalize(self.all_ignored(), scan_kind) + } + + pub fn all_ignored(&self) -> bool { + self.ignored && self.values.values().all(|x| x.ignored) } }