haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! Missing restart limits are planned once and written only on a start.

use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
use std::path::Path;
use std::time::SystemTime;

use rustix::fs::{Access, AtFlags, CWD, accessat};
use serde_json::Value;

pub(super) struct Plan {
    pub bytes: Vec<u8>,
    pub names: Vec<String>,
}

pub(super) fn plan(path: &Path, original: &[u8]) -> Result<Plan, String> {
    plan_bytes(original)
        .map_err(|error| format!("door_config_upgrade_failed: {}: {error}", path.display()))
}

fn plan_bytes(original: &[u8]) -> Result<Plan, Box<dyn std::error::Error>> {
    let Ok(mut value) = serde_json::from_slice::<Value>(original) else {
        // Invalid JSON belongs to the strict loader's refusal.
        return Ok(Plan {
            bytes: original.to_vec(),
            names: Vec::new(),
        });
    };
    let mut names = Vec::new();
    if let Some(children) = value.get_mut("children").and_then(Value::as_array_mut) {
        for child in children {
            if let Some(child) = child.as_object_mut()
                && !child.contains_key("give_up_after")
            {
                let name = child
                    .get("name")
                    .and_then(Value::as_str)
                    .ok_or("a child missing give_up_after must name itself")?
                    .to_owned();
                child.insert("give_up_after".to_owned(), Value::from(3));
                names.push(name);
            }
        }
    }
    let bytes = if names.is_empty() {
        original.to_vec()
    } else {
        serde_json::to_vec_pretty(&value)?
    };
    Ok(Plan { bytes, names })
}

pub(super) fn apply(path: &Path, original: &[u8], plan: &Plan) -> Result<(), String> {
    check_apply(path, plan)?;
    if plan.names.is_empty() {
        return Ok(());
    }
    let mode = metadata(path)?.permissions().mode() & 0o7777;
    apply_file(path, original, plan, mode)
        .map_err(|error| format!("door_config_upgrade_failed: {}: {error}", path.display()))
}

pub(super) fn check_apply(path: &Path, plan: &Plan) -> Result<(), String> {
    if plan.names.is_empty() {
        return Ok(());
    }
    let writable = || -> Result<(), Box<dyn std::error::Error>> {
        let parent = path
            .parent()
            .ok_or("configuration has no parent directory")?;
        let folder = fs::metadata(parent)?;
        if !folder.is_dir() {
            return Err("configuration parent must be a folder".into());
        }
        accessat(
            CWD,
            parent,
            Access::WRITE_OK | Access::EXEC_OK,
            AtFlags::EACCESS,
        )?;
        let user = effective_uid();
        if folder.mode() & 0o1000 != 0 && metadata(path)?.uid() != user && folder.uid() != user {
            return Err("sticky configuration folder forbids replacing another user's file".into());
        }
        Ok(())
    };
    writable().map_err(|error| format!("door_config_upgrade_failed: {}: {error}", path.display()))
}

fn effective_uid() -> u32 {
    #[cfg(test)]
    {
        permission_tests::EFFECTIVE_UID.with(std::cell::Cell::get)
    }
    #[cfg(not(test))]
    {
        rustix::process::geteuid().as_raw()
    }
}

fn apply_file(
    path: &Path,
    original: &[u8],
    plan: &Plan,
    mode: u32,
) -> Result<(), Box<dyn std::error::Error>> {
    let parent = path
        .parent()
        .ok_or("configuration has no parent directory")?;
    let filename = path
        .file_name()
        .ok_or("configuration has no file name")?
        .to_string_lossy();
    let stamp =
        chrono::DateTime::<chrono::Utc>::from(SystemTime::now()).format("%Y%m%dT%H%M%S%.9fZ");
    let backup = parent.join(format!("{filename}.before-give-up-after-{stamp}"));
    let temporary = parent.join(format!(".{filename}.give-up-after-{stamp}.tmp"));
    write_new(&backup, original, 0o600)?;
    let result = (|| -> Result<(), Box<dyn std::error::Error>> {
        write_new(&temporary, &plan.bytes, mode)?;
        if fs::read(&temporary)? != plan.bytes {
            return Err("written configuration did not read back byte-identical".into());
        }
        if read_source(path)? != original {
            return Err("configuration changed during upgrade; it was left unchanged".into());
        }
        sync_file(&File::open(parent)?)?;
        fs::rename(&temporary, path)?;
        sync_file(&File::open(parent)?)?;
        Ok(())
    })();
    if result.is_err()
        && temporary.exists()
        && let Err(error) = fs::remove_file(&temporary)
    {
        return Err(format!(
            "{}; temporary cleanup failed: {error}",
            result.err().ok_or("upgrade error missing")?
        )
        .into());
    }
    result?;
    eprintln!(
        "door_config_upgraded: give_up_after=3 written for {}; a child that exits three times in a row before printing ready is failing at its configuration or its store, not crashing once in passing; the file as it stood is kept at {}",
        plan.names.join(", "),
        backup.display()
    );
    Ok(())
}

fn write_new(path: &Path, bytes: &[u8], mode: u32) -> std::io::Result<()> {
    let mut file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .mode(mode)
        .open(path)?;
    file.write_all(bytes)?;
    file.set_permissions(fs::Permissions::from_mode(mode))?;
    sync_file(&file)
}

fn sync_file(file: &File) -> std::io::Result<()> {
    file.sync_all()
}

pub(super) fn check_path(path: &Path) -> Result<(), String> {
    metadata(path).map(|_| ())
}

fn metadata(path: &Path) -> Result<fs::Metadata, String> {
    let metadata = fs::symlink_metadata(path)
        .map_err(|error| format!("door_config_upgrade_failed: {}: {error}", path.display()))?;
    if metadata.file_type().is_symlink() {
        return Err(format!(
            "door_config_upgrade_failed: configuration is a symlink: {}",
            path.display()
        ));
    }
    Ok(metadata)
}

pub(super) fn open_source(path: &Path) -> Result<File, String> {
    check_path(path)?;
    let flags = i32::try_from(rustix::fs::OFlags::NOFOLLOW.bits())
        .map_err(|error| format!("door_config_upgrade_failed: {error}"))?;
    OpenOptions::new()
        .read(true)
        .custom_flags(flags)
        .open(path)
        .map_err(|error| format!("door_config_upgrade_failed: {}: {error}", path.display()))
}

fn read_source(path: &Path) -> Result<Vec<u8>, String> {
    let mut bytes = Vec::new();
    open_source(path)?
        .read_to_end(&mut bytes)
        .map_err(|error| format!("door_config_upgrade_failed: {}: {error}", path.display()))?;
    Ok(bytes)
}

#[cfg(test)]
#[path = "config_upgrade_permission_tests.rs"]
mod permission_tests;