zinc-wallet-cli 0.4.0

Agent-first Bitcoin + Ordinals CLI wallet with account-based taproot ordinals + native segwit payment addresses (optional human mode)
use crate::error::AppError;
use std::fs;
use std::path::{Path, PathBuf};

use crate::lock::now_unix;
use std::process;

pub fn create_secure_dir_all<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
    let path = path.as_ref();
    #[cfg(unix)]
    {
        use std::os::unix::fs::DirBuilderExt;
        let mut builder = fs::DirBuilder::new();
        builder.recursive(true);
        builder.mode(0o700);
        builder.create(path)
    }
    #[cfg(not(unix))]
    {
        fs::create_dir_all(path)
    }
}

pub fn write_secure_file<P: AsRef<Path>>(path: P, contents: &[u8]) -> std::io::Result<()> {
    let path = path.as_ref();
    #[cfg(unix)]
    {
        use std::io::Write;
        use std::os::unix::fs::OpenOptionsExt;
        let mut options = fs::OpenOptions::new();
        options.write(true).create(true).truncate(true).mode(0o600);
        let mut file = options.open(path)?;
        file.write_all(contents)?;
        file.sync_all()
    }
    #[cfg(not(unix))]
    {
        fs::write(path, contents)
    }
}

pub fn data_dir(config: &crate::config::ServiceConfig<'_>) -> std::path::PathBuf {
    if let Some(path) = config.data_dir {
        path.to_path_buf()
    } else {
        crate::paths::home_dir().join(".zinc-cli")
    }
}
#[must_use]
pub fn home_dir() -> PathBuf {
    if let Some(home) = std::env::var_os("HOME") {
        PathBuf::from(home)
    } else {
        PathBuf::from(".")
    }
}

pub fn profile_path(config: &crate::config::ServiceConfig<'_>) -> Result<PathBuf, AppError> {
    let root = data_dir(config);
    let profiles = root.join("profiles");
    if !profiles.exists() {
        create_secure_dir_all(&profiles)
            .map_err(|e| AppError::Config(format!("failed to create profiles dir: {e}")))?;
    }
    Ok(profiles.join(format!("{}.json", config.profile)))
}

pub fn profile_lock_path(config: &crate::config::ServiceConfig<'_>) -> Result<PathBuf, AppError> {
    Ok(profile_path(config)?.with_extension("lock"))
}

pub fn snapshot_dir(config: &crate::config::ServiceConfig<'_>) -> Result<PathBuf, AppError> {
    let root = data_dir(config);
    let directory = root.join("snapshots").join(config.profile);
    create_secure_dir_all(&directory)
        .map_err(|e| AppError::Config(format!("failed to create snapshot dir: {e}")))?;
    Ok(directory)
}

pub fn write_bytes_atomic(path: &Path, bytes: &[u8], label: &str) -> Result<(), AppError> {
    if let Some(parent) = path.parent() {
        create_secure_dir_all(parent)
            .map_err(|e| AppError::Config(format!("failed to create dir for {label}: {e}")))?;
    }
    let tmp_name = format!(
        ".{}.tmp-{}-{}",
        path.file_name().and_then(|n| n.to_str()).unwrap_or("tmp"),
        process::id(),
        now_unix()
    );
    let tmp_path = path.with_file_name(tmp_name);

    write_secure_file(&tmp_path, bytes)
        .map_err(|e| AppError::Config(format!("failed to write temp {label}: {e}")))?;
    if let Err(e) = fs::rename(&tmp_path, path) {
        let _ = fs::remove_file(&tmp_path);
        return Err(AppError::Config(format!(
            "failed to commit {label} write: {e}"
        )));
    }
    Ok(())
}