use std::io::ErrorKind;
use std::path::Path;
pub use vti_common::secure_file::{
restrict_dir_to_owner, restrict_file_to_owner, write_secret_file,
};
const MAX_SLUG_LEN: usize = 64;
pub fn check_export_path(path: &Path, force: bool) -> Result<(), Box<dyn std::error::Error>> {
if !force && std::fs::symlink_metadata(path).is_ok() {
return Err(already_exists(path));
}
Ok(())
}
pub fn write_secret_export(
path: &Path,
bytes: &[u8],
force: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if force {
match std::fs::remove_file(path) {
Ok(()) => {}
Err(e) if e.kind() == ErrorKind::NotFound => {}
Err(e) => {
return Err(
format!("could not remove existing file {}: {e}", path.display()).into(),
);
}
}
}
write_secret_file(path, bytes).map_err(|e| {
if e.kind() == ErrorKind::AlreadyExists {
already_exists(path)
} else {
format!("could not write {}: {e}", path.display()).into()
}
})
}
fn already_exists(path: &Path) -> Box<dyn std::error::Error> {
format!(
"{} already exists. Choose another path with --output, or pass --force to replace it.",
path.display()
)
.into()
}
pub fn did_filename_slug(did: Option<&str>, fallback: &str) -> String {
let slug: String = did
.and_then(|d| d.rsplit(':').next())
.unwrap_or("")
.chars()
.filter(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
.take(MAX_SLUG_LEN)
.collect();
if slug.chars().all(|c| c == '.') {
fallback.to_string()
} else {
slug
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp_dir() -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("vta-test-export-{}", rand::random::<u32>()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn slug_keeps_an_ordinary_did_segment() {
assert_eq!(
did_filename_slug(Some("did:webvh:QmScid:example.com"), "vtc"),
"example.com"
);
assert_eq!(
did_filename_slug(Some("did:key:z6MkAbc_1-2"), "vta"),
"z6MkAbc_1-2"
);
}
#[test]
fn slug_strips_path_separators_and_other_characters() {
for did in [
"did:web:../../x",
"did:web:..\\..\\x",
"did:web:a/b c\0d%2Fe",
"did:web:caf\u{e9}\n",
] {
let slug = did_filename_slug(Some(did), "vtc");
assert!(
slug.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')),
"{did:?} -> {slug:?}"
);
let name = format!("vtc-backup-{slug}-20260101.vtcbak");
assert_eq!(
Path::new(&name).components().count(),
1,
"{did:?} must yield a single path component, got {name:?}"
);
}
assert_eq!(did_filename_slug(Some("did:web:../../x"), "vtc"), "....x");
}
#[test]
fn slug_falls_back_when_nothing_usable_is_left() {
assert_eq!(did_filename_slug(None, "vta"), "vta");
assert_eq!(did_filename_slug(Some("did:web:"), "vtc"), "vtc");
assert_eq!(did_filename_slug(Some("did:web:.."), "vtc"), "vtc");
assert_eq!(did_filename_slug(Some("did:web:/\\"), "vtc"), "vtc");
}
#[test]
fn slug_is_capped() {
let did = format!("did:web:{}", "a".repeat(500));
assert_eq!(did_filename_slug(Some(&did), "vtc").len(), MAX_SLUG_LEN);
}
#[test]
fn export_refuses_an_existing_file_without_force() {
let dir = tmp_dir();
let f = dir.join("backup.vtabak");
std::fs::write(&f, b"original").unwrap();
assert!(check_export_path(&f, false).is_err());
let err = write_secret_export(&f, b"new", false).unwrap_err();
assert!(err.to_string().contains("--force"), "{err}");
assert_eq!(std::fs::read(&f).unwrap(), b"original");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn export_replaces_an_existing_file_with_force() {
let dir = tmp_dir();
let f = dir.join("backup.vtabak");
std::fs::write(&f, b"original").unwrap();
check_export_path(&f, true).unwrap();
write_secret_export(&f, b"new", true).unwrap();
assert_eq!(std::fs::read(&f).unwrap(), b"new");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&f).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn export_creates_a_new_file() {
let dir = tmp_dir();
let f = dir.join("backup.vtabak");
check_export_path(&f, false).unwrap();
write_secret_export(&f, b"data", false).unwrap();
assert_eq!(std::fs::read(&f).unwrap(), b"data");
let _ = std::fs::remove_dir_all(&dir);
}
}