harn-hostlib 0.8.7

Opt-in code-intelligence and deterministic-tool host builtins for the Harn VM
Documentation
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::{LazyLock, Mutex};

use sha2::{Digest, Sha256};

use crate::error::HostlibError;

static ARTIFACTS: LazyLock<Mutex<BTreeMap<String, CommandArtifacts>>> =
    LazyLock::new(|| Mutex::new(BTreeMap::new()));

#[derive(Clone, Debug)]
pub(crate) struct CommandArtifacts {
    pub(crate) output_path: PathBuf,
    pub(crate) stdout_path: PathBuf,
    pub(crate) stderr_path: PathBuf,
    pub(crate) line_count: u64,
    pub(crate) byte_count: u64,
    pub(crate) output_sha256: String,
}

pub(crate) fn persist_artifacts(
    command_id: &str,
    stdout: &[u8],
    stderr: &[u8],
    handle_id: Option<&str>,
) -> Result<CommandArtifacts, HostlibError> {
    let artifacts = planned_artifact_paths(command_id);
    std::fs::create_dir_all(artifacts.output_path.parent().unwrap()).map_err(|e| {
        HostlibError::Backend {
            builtin: "hostlib_tools_run_command",
            message: format!("failed to create command artifact dir: {e}"),
        }
    })?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let _ = std::fs::set_permissions(
            artifacts.output_path.parent().unwrap(),
            std::fs::Permissions::from_mode(0o700),
        );
    }
    std::fs::write(&artifacts.stdout_path, stdout).map_err(|e| HostlibError::Backend {
        builtin: "hostlib_tools_run_command",
        message: format!("failed to write stdout artifact: {e}"),
    })?;
    std::fs::write(&artifacts.stderr_path, stderr).map_err(|e| HostlibError::Backend {
        builtin: "hostlib_tools_run_command",
        message: format!("failed to write stderr artifact: {e}"),
    })?;
    let mut combined = Vec::with_capacity(stdout.len() + stderr.len());
    combined.extend_from_slice(stdout);
    combined.extend_from_slice(stderr);
    std::fs::write(&artifacts.output_path, &combined).map_err(|e| HostlibError::Backend {
        builtin: "hostlib_tools_run_command",
        message: format!("failed to write combined output artifact: {e}"),
    })?;
    let output_sha256 = format!("sha256:{}", hex::encode(Sha256::digest(&combined)));
    let artifacts = CommandArtifacts {
        output_path: artifacts.output_path,
        stdout_path: artifacts.stdout_path,
        stderr_path: artifacts.stderr_path,
        line_count: line_count(&combined),
        byte_count: combined.len() as u64,
        output_sha256,
    };
    register_artifacts(command_id, handle_id, &artifacts);
    Ok(artifacts)
}

pub(crate) fn planned_artifact_paths(command_id: &str) -> CommandArtifacts {
    let dir = std::env::temp_dir().join(format!("harn-command-{command_id}"));
    CommandArtifacts {
        output_path: dir.join("combined.txt"),
        stdout_path: dir.join("stdout.txt"),
        stderr_path: dir.join("stderr.txt"),
        line_count: 0,
        byte_count: 0,
        output_sha256: String::new(),
    }
}

pub(crate) fn resolve_output_path(
    command_id: Option<&str>,
    handle_id: Option<&str>,
    path: Option<&str>,
) -> Option<PathBuf> {
    if let Some(path) = path {
        return Some(PathBuf::from(path));
    }
    let artifacts = ARTIFACTS.lock().expect("command artifact store poisoned");
    command_id
        .and_then(|id| artifacts.get(id))
        .or_else(|| handle_id.and_then(|id| artifacts.get(id)))
        .map(|a| a.output_path.clone())
}

fn register_artifacts(command_id: &str, handle_id: Option<&str>, artifacts: &CommandArtifacts) {
    let mut store = ARTIFACTS.lock().expect("command artifact store poisoned");
    store.insert(command_id.to_string(), artifacts.clone());
    if let Some(handle_id) = handle_id {
        store.insert(handle_id.to_string(), artifacts.clone());
    }
}

fn line_count(bytes: &[u8]) -> u64 {
    if bytes.is_empty() {
        return 0;
    }
    let newlines = bytes.iter().filter(|b| **b == b'\n').count() as u64;
    if bytes.ends_with(b"\n") {
        newlines
    } else {
        newlines + 1
    }
}