aiward 0.5.19

Local-first AI secret firewall for development environments.
Documentation
use std::{
    fs::{self, File, OpenOptions},
    path::Path,
};

use anyhow::{Context, Result};

pub(crate) fn ensure_parent_dir(path: &Path) -> Result<()> {
    match path
        .parent()
        .filter(|parent| !parent.as_os_str().is_empty())
    {
        Some(parent) => {
            fs::create_dir_all(parent).context(format!("failed to create {}", parent.display()))
        }
        None => Ok(()),
    }
}

pub(crate) fn ensure_private_parent_dir(path: &Path) -> Result<()> {
    match path
        .parent()
        .filter(|parent| !parent.as_os_str().is_empty())
    {
        Some(parent) => ensure_private_dir(parent),
        None => Ok(()),
    }
}

pub(crate) fn ensure_private_dir(path: &Path) -> Result<()> {
    fs::create_dir_all(path).context(format!("failed to create {}", path.display()))?;
    set_private_dir_permissions(path)
}

pub(crate) fn write_private_file(path: &Path, contents: &[u8]) -> Result<()> {
    ensure_private_parent_dir(path)?;
    fs::write(path, contents).context(format!("failed to write {}", path.display()))?;
    set_private_file_permissions(path)
}

pub(crate) fn open_private_append(path: &Path) -> Result<File> {
    ensure_private_parent_dir(path)?;
    let file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)
        .context(format!("failed to open {}", path.display()))?;
    set_private_file_permissions(path)?;
    Ok(file)
}

#[cfg(unix)]
pub(crate) fn set_private_dir_permissions(path: &Path) -> Result<()> {
    use std::os::unix::fs::PermissionsExt;

    fs::set_permissions(path, fs::Permissions::from_mode(0o700))
        .context(format!("failed to chmod {}", path.display()))
}

#[cfg(not(unix))]
pub(crate) fn set_private_dir_permissions(_path: &Path) -> Result<()> {
    Ok(())
}

#[cfg(unix)]
pub(crate) fn set_private_file_permissions(path: &Path) -> Result<()> {
    use std::os::unix::fs::PermissionsExt;

    fs::set_permissions(path, fs::Permissions::from_mode(0o600))
        .context(format!("failed to chmod {}", path.display()))
}

#[cfg(not(unix))]
pub(crate) fn set_private_file_permissions(_path: &Path) -> Result<()> {
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(unix)]
    use std::os::unix::fs::PermissionsExt;

    #[test]
    fn ensure_parent_dir_creates_parent_and_allows_plain_file_name() {
        let tempdir = tempfile::tempdir().unwrap();
        let nested = tempdir.path().join("nested").join("file.txt");

        ensure_parent_dir(&nested).unwrap();
        ensure_parent_dir(Path::new("file.txt")).unwrap();
        ensure_private_parent_dir(Path::new("file.txt")).unwrap();

        assert!(tempdir.path().join("nested").is_dir());
    }

    #[test]
    fn private_file_helpers_write_and_append() {
        let tempdir = tempfile::tempdir().unwrap();
        let path = tempdir.path().join("state").join("file.jsonl");

        write_private_file(&path, b"one\n").unwrap();
        {
            use std::io::Write;
            let mut file = open_private_append(&path).unwrap();
            writeln!(file, "two").unwrap();
        }

        assert_eq!(std::fs::read_to_string(&path).unwrap(), "one\ntwo\n");

        #[cfg(unix)]
        {
            let dir_mode = std::fs::metadata(path.parent().unwrap())
                .unwrap()
                .permissions()
                .mode()
                & 0o777;
            let file_mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
            assert_eq!(dir_mode, 0o700);
            assert_eq!(file_mode, 0o600);
        }
    }

    #[test]
    fn private_helpers_report_blocked_paths() {
        let tempdir = tempfile::tempdir().unwrap();
        let blocked = tempdir.path().join("blocked");
        std::fs::write(&blocked, "").unwrap();

        assert!(ensure_private_dir(&blocked).is_err());
        assert!(write_private_file(&blocked, b"contents").is_ok());
        assert!(open_private_append(&blocked).is_ok());
    }
}