use crate::errors::{SafeError, SafeResult};
use crate::snapshot;
use crate::vault::VaultFile;
use std::path::Path;
pub const CURRENT_SCHEMA: &str = "tsafe/vault/v1";
struct Migration {
from: &'static str,
to: &'static str,
apply: fn(VaultFile) -> SafeResult<VaultFile>,
}
static MIGRATIONS: &[Migration] = &[];
pub fn run(path: &Path, file: VaultFile, profile: &str) -> SafeResult<(VaultFile, bool)> {
if file.schema == CURRENT_SCHEMA {
return Ok((file, false));
}
let mut chain: Vec<&Migration> = Vec::new();
let mut cursor = file.schema.as_str();
loop {
if cursor == CURRENT_SCHEMA {
break;
}
let step = MIGRATIONS
.iter()
.find(|m| m.from == cursor)
.ok_or_else(|| SafeError::MigrationFailed {
reason: format!("no migration path from schema '{cursor}' to '{CURRENT_SCHEMA}'"),
})?;
chain.push(step);
cursor = step.to;
}
if path.exists() {
let _ = snapshot::take(path, profile, snapshot::DEFAULT_SNAPSHOT_KEEP);
}
let mut current = file;
for step in chain {
current = (step.apply)(current).map_err(|e| SafeError::MigrationFailed {
reason: format!("migration {} → {}: {e}", step.from, step.to),
})?;
}
let json = serde_json::to_string_pretty(¤t)?;
let tmp = path.with_extension("vault.migrate.tmp");
std::fs::write(&tmp, &json)?;
std::fs::rename(&tmp, path)?;
Ok((current, true))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vault::{KdfParams, VaultChallenge, VaultFile};
use chrono::Utc;
use std::collections::HashMap;
use tempfile::tempdir;
fn dummy_file(schema: &str) -> VaultFile {
VaultFile {
schema: schema.to_string(),
kdf: KdfParams {
algorithm: "argon2id".into(),
m_cost: 65536,
t_cost: 3,
p_cost: 4,
salt: "AAAA".into(),
},
cipher: "xchacha20poly1305".into(),
vault_challenge: VaultChallenge {
nonce: "AAAA".into(),
ciphertext: "AAAA".into(),
},
created_at: Utc::now(),
updated_at: Utc::now(),
secrets: HashMap::new(),
age_recipients: Vec::new(),
wrapped_dek: None,
}
}
#[test]
fn current_schema_is_noop() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("test.vault");
let file = dummy_file(CURRENT_SCHEMA);
let (_, changed) = temp_env::with_var("TSAFE_VAULT_DIR", tmp.path().to_str(), || {
run(&path, file, "test").unwrap()
});
assert!(!changed);
}
#[test]
fn unknown_schema_errors() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("unknown.vault");
let file = dummy_file("unknown/vault/v99");
let err = temp_env::with_var("TSAFE_VAULT_DIR", tmp.path().to_str(), || {
run(&path, file, "unknown").unwrap_err()
});
assert!(matches!(err, SafeError::MigrationFailed { .. }));
}
}