//! Configuration file migration. use std::fs; use std::path::Path; use toml::map::Entry; use toml::{Table, Value}; use crate::cli::MigrateOptions; use crate::config; /// Handle migration. pub fn migrate(options: MigrateOptions) { // Find configuration file path. let config_path = options .config_file .clone() .or_else(|| config::installed_config("toml")) .or_else(|| config::installed_config("yml")); // Abort if system has no installed configuration. let config_path = match config_path { Some(config_path) => config_path, None => { eprintln!("No configuration file found"); std::process::exit(1); }, }; // If we're doing a wet run, perform a dry run first for safety. if !options.dry_run { #[allow(clippy::redundant_clone)] let mut options = options.clone(); options.silent = true; options.dry_run = true; if let Err(err) = migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) { eprintln!("Configuration file migration failed:"); eprintln!(" {config_path:?}: {err}"); std::process::exit(1); } } // Migrate the root config. match migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) { Ok(new_path) => { if !options.silent { println!("Successfully migrated {config_path:?} to {new_path:?}"); } }, Err(err) => { eprintln!("Configuration file migration failed:"); eprintln!(" {config_path:?}: {err}"); std::process::exit(1); }, } } /// Migrate a specific configuration file. fn migrate_config( options: &MigrateOptions, path: &Path, recursion_limit: usize, ) -> Result { // Ensure configuration file has an extension. let path_str = path.to_string_lossy(); let (prefix, suffix) = match path_str.rsplit_once('.') { Some((prefix, suffix)) => (prefix, suffix), None => return Err("missing file extension".to_string()), }; // Abort if config is already toml. if suffix == "toml" { return Err("already in TOML format".to_string()); } // Try to parse the configuration file. let mut config = match config::deserialize_config(path, !options.dry_run) { Ok(config) => config, Err(err) => return Err(format!("parsing error: {err}")), }; // Migrate config imports. if !options.skip_imports { migrate_imports(options, &mut config, recursion_limit)?; } // Migrate deprecated field names to their new location. if !options.skip_renames { migrate_renames(&mut config)?; } // Convert to TOML format. let toml = toml::to_string(&config).map_err(|err| format!("conversion error: {err}"))?; let new_path = format!("{prefix}.toml"); if options.dry_run && !options.silent { // Output new content to STDOUT. println!( "\nv-----Start TOML for {path:?}-----v\n\n{toml}\n^-----End TOML for {path:?}-----^\n" ); } else if !options.dry_run { // Write the new toml configuration. fs::write(&new_path, toml).map_err(|err| format!("filesystem error: {err}"))?; } Ok(new_path) } /// Migrate the imports of a config. fn migrate_imports( options: &MigrateOptions, config: &mut Value, recursion_limit: usize, ) -> Result<(), String> { let imports = match config::imports(config, recursion_limit) { Ok(imports) => imports, Err(err) => return Err(format!("import error: {err}")), }; // Migrate the individual imports. let mut new_imports = Vec::new(); for import in imports { let import = match import { Ok(import) => import, Err(err) => return Err(format!("import error: {err}")), }; // Keep yaml import if path does not exist. if !import.exists() { if options.dry_run { eprintln!("Keeping yaml config for nonexistent import: {import:?}"); } new_imports.push(Value::String(import.to_string_lossy().into())); continue; } let new_path = migrate_config(options, &import, recursion_limit - 1)?; // Print new import path. if options.dry_run { println!("Successfully migrated import {import:?} to {new_path:?}"); } new_imports.push(Value::String(new_path)); } // Update the imports field. if let Some(import) = config.get_mut("import") { *import = Value::Array(new_imports); } Ok(()) } /// Migrate deprecated fields. fn migrate_renames(config: &mut Value) -> Result<(), String> { let config_table = match config.as_table_mut() { Some(config_table) => config_table, None => return Ok(()), }; // draw_bold_text_with_bright_colors -> colors.draw_bold_text_with_bright_colors move_value(config_table, &["draw_bold_text_with_bright_colors"], &[ "colors", "draw_bold_text_with_bright_colors", ])?; // key_bindings -> keyboard.bindings move_value(config_table, &["key_bindings"], &["keyboard", "bindings"])?; // mouse_bindings -> mouse.bindings move_value(config_table, &["mouse_bindings"], &["mouse", "bindings"])?; Ok(()) } /// Move a toml value from one map to another. fn move_value(config_table: &mut Table, origin: &[&str], target: &[&str]) -> Result<(), String> { if let Some(value) = remove_node(config_table, origin)? { if !insert_node_if_empty(config_table, target, value)? { return Err(format!( "conflict: both `{}` and `{}` are set", origin.join("."), target.join(".") )); } } Ok(()) } /// Remove a node from a tree of tables. fn remove_node(table: &mut Table, path: &[&str]) -> Result, String> { if path.len() == 1 { Ok(table.remove(path[0])) } else { let next_table_value = match table.get_mut(path[0]) { Some(next_table_value) => next_table_value, None => return Ok(None), }; let next_table = match next_table_value.as_table_mut() { Some(next_table) => next_table, None => return Err(format!("invalid `{}` table", path[0])), }; remove_node(next_table, &path[1..]) } } /// Try to insert a node into a tree of tables. /// /// Returns `false` if the node already exists. fn insert_node_if_empty(table: &mut Table, path: &[&str], node: Value) -> Result { if path.len() == 1 { match table.entry(path[0]) { Entry::Vacant(vacant_entry) => { vacant_entry.insert(node); Ok(true) }, Entry::Occupied(_) => Ok(false), } } else { let next_table_value = table.entry(path[0]).or_insert_with(|| Value::Table(Table::new())); let next_table = match next_table_value.as_table_mut() { Some(next_table) => next_table, None => return Err(format!("invalid `{}` table", path[0])), }; insert_node_if_empty(next_table, &path[1..], node) } } #[cfg(test)] mod tests { use super::*; #[test] fn move_values() { let input = r#" root_value = 3 [table] table_value = 5 [preexisting] not_moved = 9 "#; let mut value: Value = toml::from_str(input).unwrap(); let table = value.as_table_mut().unwrap(); move_value(table, &["root_value"], &["new_table", "root_value"]).unwrap(); move_value(table, &["table", "table_value"], &["preexisting", "subtable", "new_name"]) .unwrap(); let output = toml::to_string(table).unwrap(); assert_eq!( output, "[new_table]\nroot_value = 3\n\n[preexisting]\nnot_moved = \ 9\n\n[preexisting.subtable]\nnew_name = 5\n\n[table]\n" ); } }