code-system-graph-store-sqlite 1.0.0

Exact-schema SQLite persistence for Code System Graph.
Documentation
use std::fs;
#[cfg(unix)]
use std::process::Command;
#[cfg(unix)]
use std::time::Duration;

use super::remove_database_artifacts;
#[cfg(unix)]
use super::{SQLITE_ARTIFACT_SUFFIXES, artifact_path};
#[cfg(unix)]
use crate::SqliteStore;
use crate::StoreError;

#[cfg(unix)]
const UMASK_HELPER_ENV: &str = "CODE_SYSTEM_GRAPH_UMASK_PERMISSION_HELPER";
#[cfg(unix)]
const UMASK_READY_ENV: &str = "CODE_SYSTEM_GRAPH_UMASK_PERMISSION_READY";
#[cfg(unix)]
const UMASK_DATABASE_ENV: &str = "CODE_SYSTEM_GRAPH_UMASK_PERMISSION_DATABASE";

#[cfg(unix)]
#[test]
fn open_should_restrict_sidecars_created_during_schema_initialization()
-> Result<(), Box<dyn std::error::Error>> {
    use std::os::unix::fs::PermissionsExt;

    let temporary = tempfile::tempdir()?;
    let database = temporary.path().join("fresh.db");
    SqliteStore::open(&database)?;

    for suffix in &SQLITE_ARTIFACT_SUFFIXES[1..] {
        let sidecar_path = artifact_path(&database, suffix);
        if sidecar_path.exists() {
            assert_eq!(
                fs::metadata(&sidecar_path)?.permissions().mode() & 0o777,
                0o600,
                "sidecar `{}` must be owner-only after initialization",
                sidecar_path.display()
            );
        }
    }
    Ok(())
}

#[test]
fn artifact_cleanup_should_report_unremovable_sidecar() -> Result<(), Box<dyn std::error::Error>> {
    let temporary = tempfile::tempdir()?;
    let database = temporary.path().join("store.db");
    let sidecar_directory = temporary.path().join("store.db-wal");
    let shm = temporary.path().join("store.db-shm");
    let journal = temporary.path().join("store.db-journal");
    fs::write(&database, b"partial")?;
    fs::create_dir(&sidecar_directory)?;
    fs::write(&shm, b"sensitive")?;
    fs::write(&journal, b"sensitive")?;

    let result = remove_database_artifacts(&database);

    assert!(matches!(
        result,
        Err(StoreError::Io { path, .. }) if path == sidecar_directory
    ));
    assert!(!shm.exists(), "cleanup must continue after the first error");
    assert!(
        !journal.exists(),
        "cleanup must attempt every database artifact"
    );
    Ok(())
}

#[cfg(unix)]
#[test]
fn umask_permission_helper() -> Result<(), Box<dyn std::error::Error>> {
    if std::env::var(UMASK_HELPER_ENV).as_deref() != Ok("1") {
        return Ok(());
    }
    {
        use std::os::unix::fs::PermissionsExt;

        let database = std::env::var_os(UMASK_DATABASE_ENV)
            .map(std::path::PathBuf::from)
            .ok_or_else(|| std::io::Error::other("umask database environment is missing"))?;
        let ready = std::env::var_os(UMASK_READY_ENV)
            .map(std::path::PathBuf::from)
            .ok_or_else(|| std::io::Error::other("umask ready environment is missing"))?;
        let backup = database.with_extension("db.backup");
        {
            let store = SqliteStore::open(&database)?;
            store.backup_to(&backup)?;
        }
        let mut paths = vec![database.clone(), backup];
        paths.extend(
            SQLITE_ARTIFACT_SUFFIXES[1..]
                .iter()
                .map(|suffix| artifact_path(&database, suffix)),
        );
        for path in paths {
            if path.exists() {
                assert_eq!(
                    fs::metadata(&path)?.permissions().mode() & 0o777,
                    0o600,
                    "permissions differ for `{}`",
                    path.display()
                );
            }
        }
        fs::write(ready, "ready")?;
        std::thread::sleep(Duration::from_secs(30));
    }
    Ok(())
}

#[cfg(unix)]
#[test]
fn database_files_should_use_owner_only_permissions_under_permissive_umask()
-> Result<(), Box<dyn std::error::Error>> {
    let temporary = tempfile::tempdir()?;
    let database = temporary.path().join("umask.db");
    let ready = temporary.path().join("ready");
    let executable = std::env::current_exe()?;
    let mut child = Command::new("sh")
        .arg("-c")
        .arg(format!(
            "umask 000; exec {} --exact file_permissions::tests::umask_permission_helper --nocapture",
            executable.display()
        ))
        .env(UMASK_HELPER_ENV, "1")
        .env(UMASK_DATABASE_ENV, &database)
        .env(UMASK_READY_ENV, &ready)
        .spawn()?;
    let mut helper_ready = false;
    for _attempt in 0..200 {
        if ready.exists() {
            helper_ready = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    if !helper_ready {
        child.kill()?;
        let _status = child.wait()?;
        return Err(std::io::Error::other("umask helper did not become ready").into());
    }
    child.kill()?;
    let _status = child.wait()?;
    Ok(())
}