Replace the 'dpkg-buildpackage -S' wrapper with a native pipeline in src/build/: - deb822 control parser/writer with dpkg-compatible multiline rendering (control.rs) - md5/sha1/sha256 checksum registry, insertion-ordered like dpkg's artifact accumulation (checksums.rs) - Debian version splitting/validation and full changelog entry parsing, including binNMU binary-only entries (metadata.rs) - build-type bitflags and rules-target/artifact-suffix mapping (buildtype.rs) - environment setup: SOURCE_DATE_EPOCH, DEB_BUILD_OPTIONS, dpkg-architecture env dump, vendor default profiles and the sanitized Environment field recorded in .buildinfo (env.rs) - debian/files registry with atomic saves (files.rs) - native .buildinfo writer, including the Installed-Build-Depends closure computed over the dpkg status database (buildinfo.rs) - native .changes writer emitting dpkg's canonical field order with legacy Files + Checksums-Sha1/Sha256 (changes.rs) - gpgme clearsigning with the transitive checksum cascade (dsc -> buildinfo -> changes), key discovery from the changelog maintainer and UNRELEASED no-sign handling (sign.rs) dpkg-source (-b/--before-build/--after-build) intentionally remains a subprocess; debian/rules execution is unchanged. Validated differentially against real dpkg-buildpackage -S -I -i -nc -d on native and 3.0 (quilt) fixture packages: .dsc byte-identical, .changes payload matches modulo machine-dependent Installed-Build-Depends and Environment content, all signatures verify with gpg, artifact ordering and UNRELEASED no-sign behavior match dpkg.
256 lines
7.8 KiB
Rust
256 lines
7.8 KiB
Rust
//! File checksum computation and formatting for `.changes` / `.buildinfo`
|
|
//! fields (MD5, SHA-1, SHA-256 + size), mirroring `Dpkg::Checksums`.
|
|
|
|
use std::collections::HashMap;
|
|
use std::io::Read;
|
|
use std::path::Path;
|
|
|
|
use md5::Md5;
|
|
use sha1::Sha1;
|
|
use sha2::{Digest, Sha256};
|
|
|
|
/// Checksums and size of a single file.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Entry {
|
|
/// File size in bytes.
|
|
pub size: u64,
|
|
/// Lowercase hexadecimal MD5 digest.
|
|
pub md5: String,
|
|
/// Lowercase hexadecimal SHA-1 digest.
|
|
pub sha1: String,
|
|
/// Lowercase hexadecimal SHA-256 digest.
|
|
pub sha256: String,
|
|
}
|
|
|
|
/// Compute all supported checksums of a file.
|
|
fn compute(path: &Path) -> Result<Entry, Box<dyn std::error::Error>> {
|
|
let mut file = std::fs::File::open(path)
|
|
.map_err(|e| format!("cannot open '{}' for checksumming: {}", path.display(), e))?;
|
|
|
|
let mut md5_hasher = Md5::new();
|
|
let mut sha1_hasher = Sha1::new();
|
|
let mut sha256_hasher = Sha256::new();
|
|
let mut size: u64 = 0;
|
|
let mut buf = [0u8; 64 * 1024];
|
|
|
|
loop {
|
|
let n = file.read(&mut buf)?;
|
|
if n == 0 {
|
|
break;
|
|
}
|
|
md5_hasher.update(&buf[..n]);
|
|
sha1_hasher.update(&buf[..n]);
|
|
sha256_hasher.update(&buf[..n]);
|
|
size += n as u64;
|
|
}
|
|
|
|
Ok(Entry {
|
|
size,
|
|
md5: hex::encode(md5_hasher.finalize()),
|
|
sha1: hex::encode(sha1_hasher.finalize()),
|
|
sha256: hex::encode(sha256_hasher.finalize()),
|
|
})
|
|
}
|
|
|
|
/// A registry of checksummed files, keyed by the name they are distributed
|
|
/// under (which may differ from the on-disk path).
|
|
///
|
|
/// Insertion order is preserved, matching the order in which
|
|
/// `dpkg-genchanges` accumulates artifacts (dsc, tarballs, debs, buildinfo).
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct FileChecksums {
|
|
entries: Vec<(String, Entry)>,
|
|
index: HashMap<String, usize>,
|
|
}
|
|
|
|
impl FileChecksums {
|
|
/// Create an empty registry.
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Add a file, registering it under its own file name.
|
|
pub fn add_file(&mut self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let key = path
|
|
.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.ok_or_else(|| format!("invalid file name: {}", path.display()))?
|
|
.to_string();
|
|
self.add_file_as(path, &key)
|
|
}
|
|
|
|
/// Add a file, registering it under an explicit distribution key.
|
|
pub fn add_file_as(
|
|
&mut self,
|
|
path: &Path,
|
|
key: &str,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let entry = compute(path)?;
|
|
self.insert_entry(key, entry);
|
|
Ok(())
|
|
}
|
|
|
|
/// Insert a pre-computed entry (e.g. taken from a `.dsc` checksum field).
|
|
/// Re-inserting an existing key updates it in place, keeping its position.
|
|
pub fn insert_entry(&mut self, key: &str, entry: Entry) {
|
|
if let Some(&pos) = self.index.get(key) {
|
|
self.entries[pos].1 = entry;
|
|
return;
|
|
}
|
|
self.index.insert(key.to_string(), self.entries.len());
|
|
self.entries.push((key.to_string(), entry));
|
|
}
|
|
|
|
/// Remove a file from the registry. Returns true if it was present.
|
|
pub fn remove(&mut self, key: &str) -> bool {
|
|
match self.index.remove(key) {
|
|
Some(pos) => {
|
|
self.entries.remove(pos);
|
|
// Reindex the shifted tail.
|
|
for (i, (k, _)) in self.entries.iter().enumerate().skip(pos) {
|
|
self.index.insert(k.clone(), i);
|
|
}
|
|
true
|
|
}
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
/// Look up the entry for a given key.
|
|
pub fn get(&self, key: &str) -> Option<&Entry> {
|
|
self.index.get(key).map(|&pos| &self.entries[pos].1)
|
|
}
|
|
|
|
/// Iterate over `(key, entry)` pairs in insertion order.
|
|
pub fn iter(&self) -> impl Iterator<Item = (&String, &Entry)> {
|
|
self.entries.iter().map(|(k, e)| (k, e))
|
|
}
|
|
|
|
/// Number of registered files.
|
|
pub fn len(&self) -> usize {
|
|
self.entries.len()
|
|
}
|
|
|
|
/// True if no file is registered.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.entries.is_empty()
|
|
}
|
|
|
|
/// Format a `Checksums-*` style field value: one `\n`-separated line per
|
|
/// file of the form `" <hash> <size> <key>"`.
|
|
fn format_field<F>(&self, hash_of: F) -> String
|
|
where
|
|
F: Fn(&Entry) -> &str,
|
|
{
|
|
let mut out = String::new();
|
|
for (key, e) in self.iter() {
|
|
out.push('\n');
|
|
out.push_str(hash_of(e));
|
|
out.push(' ');
|
|
out.push_str(&e.size.to_string());
|
|
out.push(' ');
|
|
out.push_str(key);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Value for the `Checksums-Md5` field (empty string if no file).
|
|
pub fn field_md5(&self) -> String {
|
|
self.format_field(|e| &e.md5)
|
|
}
|
|
|
|
/// Value for the `Checksums-Sha1` field (empty string if no file).
|
|
pub fn field_sha1(&self) -> String {
|
|
self.format_field(|e| &e.sha1)
|
|
}
|
|
|
|
/// Value for the `Checksums-Sha256` field (empty string if no file).
|
|
pub fn field_sha256(&self) -> String {
|
|
self.format_field(|e| &e.sha256)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn known_digests() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let p = dir.path().join("sample.txt");
|
|
std::fs::write(&p, b"hello world\n").unwrap();
|
|
|
|
let mut cs = FileChecksums::new();
|
|
cs.add_file(&p).unwrap();
|
|
|
|
let e = cs.get("sample.txt").unwrap();
|
|
// Verified with coreutils: echo "hello world" | md5sum / sha1sum / sha256sum
|
|
assert_eq!(e.md5, "6f5902ac237024bdd0c176cb93063dc4");
|
|
assert_eq!(e.sha1, "22596363b3de40b06f981fb85d82312e8c0ed511");
|
|
assert_eq!(
|
|
e.sha256,
|
|
"a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447"
|
|
);
|
|
assert_eq!(e.size, 12);
|
|
}
|
|
|
|
#[test]
|
|
fn insertion_order_preserved() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let a = dir.path().join("a.txt");
|
|
let b = dir.path().join("b.txt");
|
|
std::fs::write(&a, b"aaa").unwrap();
|
|
std::fs::write(&b, b"bb").unwrap();
|
|
|
|
let mut cs = FileChecksums::new();
|
|
// Insert b first: insertion order (not alphabetical) must be kept,
|
|
// matching dpkg's artifact accumulation order.
|
|
cs.add_file(&b).unwrap();
|
|
cs.add_file(&a).unwrap();
|
|
|
|
let keys: Vec<&str> = cs.iter().map(|(k, _)| k.as_str()).collect();
|
|
assert_eq!(keys, vec!["b.txt", "a.txt"]);
|
|
|
|
assert_eq!(
|
|
cs.field_md5(),
|
|
"\n21ad0bd836b90d08f4cf640b4c298e7c 2 b.txt\n47bce5c74f589f4867dbd57e9ca9f808 3 a.txt"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reinsert_updates_in_place() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let a = dir.path().join("a.txt");
|
|
std::fs::write(&a, b"aaa").unwrap();
|
|
|
|
let mut cs = FileChecksums::new();
|
|
cs.add_file(&a).unwrap();
|
|
std::fs::write(&a, b"bbbb").unwrap();
|
|
cs.add_file(&a).unwrap(); // updated in place, same position
|
|
|
|
assert_eq!(cs.len(), 1);
|
|
assert_eq!(cs.get("a.txt").unwrap().size, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_keeps_order() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let a = dir.path().join("a.txt");
|
|
let b = dir.path().join("b.txt");
|
|
let c = dir.path().join("c.txt");
|
|
std::fs::write(&a, b"1").unwrap();
|
|
std::fs::write(&b, b"2").unwrap();
|
|
std::fs::write(&c, b"3").unwrap();
|
|
|
|
let mut cs = FileChecksums::new();
|
|
cs.add_file(&a).unwrap();
|
|
cs.add_file(&b).unwrap();
|
|
cs.add_file(&c).unwrap();
|
|
assert!(cs.remove("b.txt"));
|
|
assert!(!cs.remove("b.txt"));
|
|
|
|
let keys: Vec<&str> = cs.iter().map(|(k, _)| k.as_str()).collect();
|
|
assert_eq!(keys, vec!["a.txt", "c.txt"]);
|
|
}
|
|
}
|