arcature-cli 2026.2.0

Developer lifecycle CLI for Arcature applications.
Documentation
//! SHA-256 checksums for the production artifact bundle (AP2.1-10).
//!
//! `checksums.sha256` is the file an operator (or a deploy/rollback
//! orchestrator) uses to verify a deployed bundle bit-for-bit. It lists one
//! line per bundled file: `<sha256-hex>  <relative-path>` (the two-space
//! `sha256sum` format, compatible with `sha256sum -c checksums.sha256`).
//! The lines are sorted by relative path so the file is deterministic.
//!
//! Uses the already-vetted [`sha2`] crate (admitted as a workspace
//! dependency this phase — see `docs/dependency-reviews/phase-ap21-10-
//! production.md`). `sha2::Sha256` implements the SHA-256 algorithm in
//! safe Rust (RustCrypto, `#![forbid(unsafe_code)]` upstream); no custom
//! crypto (AGENTS.md §3, §26: Arcature does not own crypto).

use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;

use sha2::{Digest, Sha256};

use super::error::PackageError;

/// Compute the SHA-256 digest of `path`'s contents, returning lowercase
/// hex. Streams the file in 64 KiB chunks so large binaries do not load
/// fully into memory.
pub(crate) fn file_sha256(path: &Path) -> Result<String, PackageError> {
    let file = File::open(path).map_err(|source| PackageError::Read {
        path: path.to_path_buf(),
        source,
    })?;
    let mut reader = BufReader::new(file);
    let mut hasher = Sha256::new();
    let mut buffer = [0u8; 64 * 1024];
    loop {
        let n = reader
            .read(&mut buffer)
            .map_err(|source| PackageError::Read {
                path: path.to_path_buf(),
                source,
            })?;
        if n == 0 {
            break;
        }
        hasher.update(&buffer[..n]);
    }
    let digest = hasher.finalize();
    Ok(format!("{:x}", digest))
}

/// Write the checksums file to `path` (the bundle root's `checksums.sha256`).
pub(crate) fn write_checksums(path: &Path, content: &str) -> Result<(), PackageError> {
    std::fs::write(path, content).map_err(|source| PackageError::Write {
        path: path.to_path_buf(),
        source,
    })
}

#[cfg(test)]
mod tests {
    use super::file_sha256;
    use std::fs;

    #[test]
    fn file_sha256_matches_known_value() {
        // SHA-256 of "abc" is well-known.
        let dir = tempfile::tempdir().expect("tempdir");
        let path = dir.path().join("abc.txt");
        fs::write(&path, b"abc").expect("write");
        let digest = file_sha256(&path).expect("digest");
        assert_eq!(
            digest,
            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
        );
    }

    #[test]
    fn empty_file_has_known_digest() {
        let dir = tempfile::tempdir().expect("tempdir");
        let path = dir.path().join("empty");
        fs::write(&path, b"").expect("write");
        let digest = file_sha256(&path).expect("digest");
        assert_eq!(
            digest,
            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        );
    }
}