bread-ecosystem/bakery/src/download.rs
Breadway a8be86be03 fix: comprehensive bakery package manager audit and repair
Critical fixes:
- gen-index.sh: emit services, config, optional_system_deps from bakery.toml;
  parse product list from registry TOML instead of hardcoded array; fail loudly
  when bakery.toml is missing (was silently producing empty metadata in prod)
- install.rs: download service units and example configs from dl server at
  install time (were never fetched); check systemctl exit codes (were swallowed);
  save state before file cleanup in remove_package (was inconsistent on error)
- doctor.rs: rewrite dep detection to use `pacman -Q` as primary (no more
  dependency on `which` or pkg-config name mismatches); add optional_system_deps
  support returning (missing, warnings) — warnings print but never block install
- get.sh: fix GitHub fallback URL (was 404 for both latest and versioned
  releases); add SHA-256 checksum verification using published .sha256 file

High priority fixes:
- bakery doctor <unknown-pkg>: exit non-zero (was silently passing)
- bakery update: add --all flag (documented in README but missing from CLI);
  add doctor gate before update (was bypassing dep check)
- bread_deps: now resolved recursively with cycle detection (was ignored)
- manifest.rs: add artifact_urls() helper and optional_system_deps field
- state.rs: atomic save via tmp+rename; cmd_info shows optional_system_deps

Tests: 17 new unit tests across doctor, download, install, state modules;
scripts/test-gen-index.sh fixture test for full pipeline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:37:09 +08:00

78 lines
2.1 KiB
Rust

use anyhow::{bail, Context, Result};
use sha2::{Digest, Sha256};
use std::path::Path;
use crate::manifest::{fetch_binary, Binary};
/// Download a binary to a temp path, verify its SHA-256, then atomically move
/// it into place. Bails before touching `dest` if the checksum fails.
pub fn fetch_and_place(binary: &Binary, dest: &Path) -> Result<()> {
println!(" downloading {}", binary.name);
let bytes = fetch_binary(&binary.dl_url, &binary.github_url)
.with_context(|| format!("downloading {}", binary.name))?;
verify_sha256(&bytes, &binary.sha256)
.with_context(|| format!("checksum mismatch for {}", binary.name))?;
if let Some(dir) = dest.parent() {
std::fs::create_dir_all(dir)?;
}
let tmp = dest.with_extension("tmp");
std::fs::write(&tmp, &bytes).context("writing binary to tmp")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o755))?;
}
std::fs::rename(&tmp, dest).context("placing binary")?;
println!(" installed {}", dest.display());
Ok(())
}
fn verify_sha256(bytes: &[u8], expected_hex: &str) -> Result<()> {
let mut hasher = Sha256::new();
hasher.update(bytes);
let actual = hex::encode(hasher.finalize());
if actual != expected_hex {
bail!(
"SHA-256 mismatch\n expected: {}\n actual: {}",
expected_hex,
actual
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use sha2::{Digest, Sha256};
fn sha256_hex(data: &[u8]) -> String {
hex::encode(Sha256::digest(data))
}
#[test]
fn verify_correct_hash() {
let bytes = b"hello bakery";
let hash = sha256_hex(bytes);
assert!(verify_sha256(bytes, &hash).is_ok());
}
#[test]
fn verify_wrong_hash_fails() {
let bytes = b"hello bakery";
let wrong = "0".repeat(64);
assert!(verify_sha256(bytes, &wrong).is_err());
}
#[test]
fn verify_empty_bytes() {
let bytes = b"";
let hash = sha256_hex(bytes);
assert!(verify_sha256(bytes, &hash).is_ok());
}
}