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
//! Prerequisites are inspected without opening credentials, sockets or logs.

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

use super::children::Config;

pub(super) fn configuration(config: &super::config::Config) -> Result<(), String> {
    let mut errors = Vec::new();
    if let Err(error) = super::lys::check_secret(&config.lys.secret_file) {
        errors.push(error);
    }
    if let Some(assistant) = &config.assistant
        && let Err(error) = super::assistant_socket::check_parent(&assistant.socket)
    {
        errors.push(format!("{}: {}", error.code, error.message));
    }
    for config in &config.children {
        if let Err(error) = child(config) {
            errors.push(error);
        }
    }
    if errors.is_empty() {
        Ok(())
    } else {
        Err(errors.join("\n"))
    }
}

pub(super) fn child(config: &Config) -> Result<(), String> {
    let program_error = |error| {
        format!(
            "door_child_program_missing: {}: {}: {error}",
            config.name,
            config.program.display()
        )
    };
    let program =
        fs::metadata(&config.program).map_err(|error| program_error(error.to_string()))?;
    if !program.is_file() {
        return Err(program_error(
            "program must resolve to a regular file".to_owned(),
        ));
    }
    accessat(CWD, &config.program, Access::EXEC_OK, AtFlags::EACCESS)
        .map_err(|error| program_error(error.to_string()))?;
    log(config)
}

fn log(config: &Config) -> Result<(), String> {
    let log_error = |error| {
        format!(
            "door_child_log_unwritable: {}: {}: {error}",
            config.name,
            config.log.display()
        )
    };
    let mut path = config.log.clone();
    loop {
        match fs::metadata(&path) {
            Ok(log) => {
                if log.is_dir() {
                    return Err(log_error("log must not resolve to a folder".to_owned()));
                }
                return accessat(CWD, &path, Access::WRITE_OK, AtFlags::EACCESS)
                    .map_err(|error| log_error(error.to_string()));
            }
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
            Err(error) => return Err(log_error(error.to_string())),
        }
        match fs::symlink_metadata(&path) {
            Ok(link) if link.file_type().is_symlink() => {
                let target = fs::read_link(&path).map_err(|error| log_error(error.to_string()))?;
                path = if target.is_absolute() {
                    target
                } else {
                    path.parent()
                        .ok_or_else(|| log_error("log must have a parent folder".to_owned()))?
                        .join(target)
                };
            }
            Ok(_) => {
                return Err(log_error(
                    "log changed while checking its target".to_owned(),
                ));
            }
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => break,
            Err(error) => return Err(log_error(error.to_string())),
        }
    }
    let folder = path
        .parent()
        .ok_or_else(|| log_error("log must have a parent folder".to_owned()))?;
    if !fs::metadata(folder)
        .map_err(|error| log_error(error.to_string()))?
        .is_dir()
    {
        return Err(log_error("log parent must be a folder".to_owned()));
    }
    accessat(
        CWD,
        folder,
        Access::WRITE_OK | Access::EXEC_OK,
        AtFlags::EACCESS,
    )
    .map_err(|error| log_error(error.to_string()))?;
    Ok(())
}