diff --git a/crates/storage/src/config.rs b/crates/storage/src/config.rs index b01ced6..5a6861c 100644 --- a/crates/storage/src/config.rs +++ b/crates/storage/src/config.rs @@ -608,16 +608,23 @@ impl Config { if !vault.is_dir() { return Err(ConfigError::VaultIsNotDirectory { path: vault }); } - let vault = vault - .to_str() - .ok_or(ConfigError::InvalidField { field: "vault" })?; let mut document = self.document.clone(); let root = document .as_table_mut() .ok_or_else(|| ConfigError::Malformed { path: self.source.clone(), })?; - root.insert("vault".to_owned(), toml::Value::String(vault.to_owned())); + if vault != self.vault { + let base = self + .source + .parent() + .ok_or(ConfigError::InvalidField { field: "source" })?; + let stored = vault.strip_prefix(base).unwrap_or(&vault); + root.insert( + "vault".to_owned(), + toml::Value::String(path_text(stored, "vault")?), + ); + } root.insert( "default_key".to_owned(), toml::Value::String(settings.default_key), @@ -1152,7 +1159,7 @@ struct RawGitRemote { fn validate_config( source: PathBuf, - document: toml::Value, + mut document: toml::Value, raw: RawConfig, ) -> Result { let base = source @@ -1162,6 +1169,7 @@ fn validate_config( if vault.exists() && !vault.is_dir() { return Err(ConfigError::VaultIsNotDirectory { path: vault }); } + make_local_path_relative(&mut document, base, &vault, "vault")?; let key_material = resolve_required_path(base, raw.key_material, "key_material")?; if !key_material.exists() { @@ -1176,6 +1184,7 @@ fn validate_config( } let key_material = fs::canonicalize(&key_material) .map_err(|_| ConfigError::InvalidKeyMaterial { path: key_material })?; + make_local_path_relative(&mut document, base, &key_material, "key_material")?; let default_key = raw .default_key @@ -1276,7 +1285,54 @@ fn resolve_required_path( if value.as_os_str().is_empty() { return Err(ConfigError::InvalidField { field }); } - Ok(resolve_path(base, &value)) + let resolved = resolve_path(base, &value); + if resolved.exists() || !value.is_absolute() { + return Ok(resolved); + } + Ok(relocated_mobile_path(base, &resolved, field).unwrap_or(resolved)) +} + +fn relocated_mobile_path(base: &Path, path: &Path, field: &'static str) -> Option { + let leaf = match field { + "vault" => "vault", + "key_material" => "keys", + _ => return None, + }; + let suffix = Path::new("Library/Application Support/ironstorage").join(leaf); + let is_stale_mobile_path = [ + Path::new("/private/var/mobile/Containers/Data/Application"), + Path::new("/var/mobile/Containers/Data/Application"), + ] + .iter() + .any(|prefix| { + path.strip_prefix(prefix).is_ok_and(|relative| { + let mut components = relative.components(); + components.next().is_some() && components.as_path() == suffix + }) + }); + let relocated = base.join(leaf); + (is_stale_mobile_path && relocated.exists()).then_some(relocated) +} + +fn make_local_path_relative( + document: &mut toml::Value, + base: &Path, + path: &Path, + field: &'static str, +) -> Result<(), ConfigError> { + let Ok(relative) = path.strip_prefix(base) else { + return Ok(()); + }; + let root = document + .as_table_mut() + .ok_or_else(|| ConfigError::Malformed { + path: base.join(CONFIG_FILE), + })?; + root.insert( + field.to_owned(), + toml::Value::String(path_text(relative, field)?), + ); + Ok(()) } fn path_text(path: &Path, field: &'static str) -> Result { @@ -1649,4 +1705,31 @@ mod tests { ("ALICE", Some(1_789_000_000), MobileTab::Preferences) ); } + + #[test] + fn stale_ios_container_path_follows_the_current_container() { + let temporary = tempfile::tempdir().expect("temporary directory"); + let vault = temporary.path().join("vault"); + let keys = temporary.path().join("keys"); + let source = temporary.path().join("config.toml"); + fs::create_dir(&vault).expect("vault"); + fs::create_dir(&keys).expect("keys"); + fs::write( + &source, + r#"vault = "/private/var/mobile/Containers/Data/Application/OLD-CONTAINER/Library/Application Support/ironstorage/vault" +default_key = "ALICE" +key_material = "keys" +"#, + ) + .expect("config"); + + let config = Config::load(Some(&source)).expect("recover moved container"); + assert_eq!(config.vault(), fs::canonicalize(vault).expect("vault path")); + config + .update_mobile_tab(MobileTab::Preferences) + .expect("persist recovered config"); + let contents = fs::read_to_string(source).expect("read config"); + assert!(contents.contains("vault = \"vault\"")); + assert!(!contents.contains("OLD-CONTAINER")); + } } diff --git a/crates/storage/src/mobile_key_transfer.rs b/crates/storage/src/mobile_key_transfer.rs index 594d8cb..c8fe0cf 100644 --- a/crates/storage/src/mobile_key_transfer.rs +++ b/crates/storage/src/mobile_key_transfer.rs @@ -842,6 +842,9 @@ mod tests { .as_str(), ALICE_FINGERPRINT ); + let config = std::fs::read_to_string(fixture.config.source())?; + assert!(config.contains("vault = \"vault\"")); + assert!(config.contains("key_material = \"keys\"")); Ok(()) } @@ -870,8 +873,8 @@ mod tests { std::fs::write( &config_path, format!( - "vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\n", - vault, ALICE_FINGERPRINT, keys + "vault = \"vault\"\ndefault_key = {:?}\nkey_material = \"keys\"\n", + ALICE_FINGERPRINT ), )?; let config = Config::load(Some(&config_path))?;