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
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
use std::path::Path;

use sha2::{Digest, Sha256};

#[cfg(test)]
use super::fixtures::Hooks;
use super::types::{Entry, Error};

pub(super) enum Payload {
    File(File),
    Worker(File),
    Bytes(Vec<u8>),
}

impl Payload {
    pub(super) fn executable(path: &Path) -> Result<Self, Error> {
        let file = regular(path, "bundle_source_invalid")?;
        let metadata = file
            .metadata()
            .map_err(|error| Error::new("bundle_source_invalid", &error))?;
        if metadata.len() == 0 || metadata.permissions().mode() & 0o111 == 0 {
            return Err(Error::new(
                "bundle_source_invalid",
                "worker or operator is empty or not executable",
            ));
        }
        Ok(Self::File(file))
    }

    pub(super) fn worker(path: &Path) -> Result<Self, Error> {
        match Self::executable(path)? {
            Self::File(file) => Ok(Self::Worker(file)),
            Self::Worker(_) | Self::Bytes(_) => Err(Error::new(
                "bundle_source_invalid",
                "worker must be an executable file",
            )),
        }
    }

    pub(super) fn entry(&mut self, mode: u32) -> Result<Entry, Error> {
        let worker = matches!(self, Self::Worker(_));
        let (bytes, sha256) = match self {
            Self::File(file) | Self::Worker(file) => {
                file.rewind()
                    .map_err(|error| Error::new("bundle_source_invalid", &error))?;
                digest(file, worker)?
            }
            Self::Bytes(bytes) => (bytes.len() as u64, format!("{:x}", Sha256::digest(bytes))),
        };
        Ok(Entry {
            bytes,
            sha256,
            mode,
        })
    }

    pub(super) fn write(
        &mut self,
        path: &Path,
        expected: &Entry,
        #[cfg(test)] hooks: &mut Hooks,
    ) -> Result<(), Error> {
        let mut target = OpenOptions::new()
            .write(true)
            .create_new(true)
            .mode(0o600)
            .open(path)
            .map_err(|error| Error::new("bundle_write_failed", &error))?;
        match self {
            Self::File(source) | Self::Worker(source) => {
                source
                    .seek(SeekFrom::Start(0))
                    .map_err(|error| Error::new("bundle_source_changed", &error))?;
                let limit = expected.bytes.checked_add(1).ok_or_else(|| {
                    Error::new("bundle_size_overflow", "payload copy bound overflowed")
                })?;
                std::io::copy(&mut source.take(limit), &mut target)
                    .map_err(|error| Error::new("bundle_write_failed", &error))?;
            }
            Self::Bytes(bytes) => target
                .write_all(bytes)
                .map_err(|error| Error::new("bundle_write_failed", &error))?,
        }
        target
            .set_permissions(fs::Permissions::from_mode(expected.mode))
            .map_err(|error| Error::new("bundle_write_failed", &error))?;
        super::publication::sync_file(
            &target,
            path,
            #[cfg(test)]
            hooks,
        )?;
        let actual = entry(path, "bundle_source_changed")?;
        if actual != *expected {
            return Err(Error::new(
                "bundle_source_changed",
                "copied bytes or mode differ from the preflight",
            ));
        }
        Ok(())
    }
}

pub(super) fn regular(path: &Path, code: &'static str) -> Result<File, Error> {
    let before = fs::symlink_metadata(path).map_err(|error| Error::new(code, &error))?;
    if !before.file_type().is_file() {
        return Err(Error::new(
            code,
            "expected a regular file, not a symlink or directory",
        ));
    }
    let file = File::open(path).map_err(|error| Error::new(code, &error))?;
    let opened = file.metadata().map_err(|error| Error::new(code, &error))?;
    if before.dev() != opened.dev() || before.ino() != opened.ino() || !opened.is_file() {
        return Err(Error::new(code, "file changed while it was opened"));
    }
    Ok(file)
}

pub(super) fn bounded(path: &Path, code: &'static str) -> Result<Vec<u8>, Error> {
    const MAX_BYTES: u64 = 1_048_576;
    let mut bytes = Vec::new();
    regular(path, code)?
        .take(MAX_BYTES + 1)
        .read_to_end(&mut bytes)
        .map_err(|error| Error::new(code, &error))?;
    if bytes.len() as u64 > MAX_BYTES {
        return Err(Error::new(
            code,
            "configuration or manifest exceeds its byte bound",
        ));
    }
    Ok(bytes)
}

pub(super) fn entry(path: &Path, code: &'static str) -> Result<Entry, Error> {
    let mut file = regular(path, code)?;
    let mode = file
        .metadata()
        .map_err(|error| Error::new(code, &error))?
        .permissions()
        .mode()
        & 0o7777;
    let (bytes, sha256) = digest(&mut file, false)?;
    Ok(Entry {
        bytes,
        sha256,
        mode,
    })
}

fn digest(reader: &mut impl Read, worker: bool) -> Result<(u64, String), Error> {
    const MARKER: &[u8] = b"embed-stand-in";
    let mut bytes = 0_u64;
    let mut hash = Sha256::new();
    let mut buffer = vec![0_u8; 65_536 + MARKER.len() - 1].into_boxed_slice();
    let mut carried = 0;
    loop {
        let read = reader
            .read(&mut buffer[carried..])
            .map_err(|error| Error::new("bundle_read_failed", &error))?;
        if read == 0 {
            break;
        }
        bytes = bytes
            .checked_add(read as u64)
            .ok_or_else(|| Error::new("bundle_size_overflow", "payload byte count overflowed"))?;
        let end = carried + read;
        hash.update(&buffer[carried..end]);
        if worker
            && buffer[..end]
                .windows(MARKER.len())
                .any(|part| part == MARKER)
        {
            return Err(Error::new(
                "bundle_test_worker_refused",
                "a test-only worker cannot be installed",
            ));
        }
        carried = if worker { end.min(MARKER.len() - 1) } else { 0 };
        buffer.copy_within(end - carried..end, 0);
    }
    Ok((bytes, format!("{:x}", hash.finalize())))
}

#[cfg(test)]
#[path = "file_tests.rs"]
mod tests;