pkgwatch/src/hash.rs
Austin Schaefer 3e87282b21
All checks were successful
CI / build (pull_request) Successful in 48s
CI / test (pull_request) Successful in 2m1s
CI / audit (pull_request) Successful in 9s
CI / coverage (pull_request) Successful in 3m53s
Move high-level docs into docs/, matching doubleo7's convention
SPEC.md and ARCHITECTURE.md were sitting at the repo root alongside
Cargo.toml/Makefile.toml/packages.d — moved both into docs/ (doubleo7
already does this for its own supplementary docs, so this matches an
existing convention in the fleet rather than inventing a new one).

Updated every doc-comment cross-reference across src/*.rs and
Makefile.toml (23 references) to the new docs/SPEC.md / docs/
ARCHITECTURE.md paths. The two files' own cross-references to each other
didn't need changing — they're still same-directory relative references.

Also updated the project reference memory pointing at ARCHITECTURE.md's
location, so it doesn't go stale pointing at a path that no longer exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 13:22:40 +02:00

81 lines
2.7 KiB
Rust

//! Two functions, shared by two real callers (`verifier`, `builder`) —
//! not a general-purpose utils dump. See docs/ARCHITECTURE.md > "organize by
//! pipeline stage, not by layer" for why that distinction matters.
use anyhow::{Context, Result};
use sha2::{Digest, Sha256};
use std::io::Read;
use std::path::Path;
/// In-memory digest — test-only now that both real callers (`verifier`,
/// `builder`) hash a file already on disk via `sha256_hex_file` instead.
/// Kept for building expected hashes from in-memory test fixtures.
#[cfg(test)]
pub(crate) fn sha256_hex(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
hex::encode(hasher.finalize())
}
/// Same digest as `sha256_hex(&std::fs::read(path)?)`, but streamed in
/// fixed-size chunks instead of reading the whole file into memory first —
/// downloaded release assets are tens of MB, and the file is already on
/// disk, so there's no reason to hold a second full copy in memory just to
/// hash it.
pub fn sha256_hex_file(path: &Path) -> Result<String> {
let mut file =
std::fs::File::open(path).with_context(|| format!("opening {}", path.display()))?;
let mut hasher = Sha256::new();
let mut buf = [0u8; 64 * 1024];
loop {
let n = file
.read(&mut buf)
.with_context(|| format!("reading {}", path.display()))?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
Ok(hex::encode(hasher.finalize()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_known_sha256() {
// printf 'hello world' | sha256sum
assert_eq!(
sha256_hex(b"hello world"),
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
);
}
#[test]
fn sha256_hex_file_matches_in_memory_digest() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("data.bin");
// Bigger than one read chunk, to actually exercise the loop.
let data = vec![0x5au8; 200 * 1024];
std::fs::write(&path, &data).unwrap();
assert_eq!(sha256_hex_file(&path).unwrap(), sha256_hex(&data));
}
#[test]
fn sha256_hex_file_matches_for_empty_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("empty.bin");
std::fs::write(&path, b"").unwrap();
assert_eq!(sha256_hex_file(&path).unwrap(), sha256_hex(b""));
}
#[test]
fn sha256_hex_file_errors_on_missing_file() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("does-not-exist.bin");
assert!(sha256_hex_file(&missing).is_err());
}
}