checksums: add SHA-512 and a Checksums-* field parser
The checksum model only carried md5/sha1/sha256 while deb-buildinfo(5) defines Checksums-Sha512, and there was no way to parse a Checksums-* field body back into entries. Add ChecksumKind::Sha512 (computed alongside the others), a field parser validating the '<hex> <size> <name>' grammar, and an only-if-populated Checksums-Sha512 emission in .buildinfo — deliberately dormant in the dpkg-parity flows, which never emit it, and .dsc/.changes untouched.
This commit is contained in:
@@ -110,6 +110,9 @@ pub fn generate_binary_metadata(
|
||||
let entry_hashes = hashes
|
||||
.remove(name)
|
||||
.ok_or_else(|| format!("artifact '{name}' listed in debian/files but not found"))?;
|
||||
// SHA-512 stays unknown here: like dpkg-genbuildinfo, no SHA-512
|
||||
// digest is computed for the artifacts, and an empty digest keeps
|
||||
// the `Checksums-Sha512` field of the `.buildinfo` omitted.
|
||||
checksums.insert_entry(
|
||||
name,
|
||||
ChecksumEntry {
|
||||
@@ -117,6 +120,7 @@ pub fn generate_binary_metadata(
|
||||
md5: entry_hashes.md5,
|
||||
sha1: entry_hashes.sha1,
|
||||
sha256: entry_hashes.sha256,
|
||||
sha512: String::new(),
|
||||
},
|
||||
);
|
||||
// Architecture accumulation in encounter order (dpkg-genchanges).
|
||||
@@ -285,6 +289,9 @@ pub fn generate_binary_metadata(
|
||||
md5: h.md5.clone(),
|
||||
sha1: h.sha1.clone(),
|
||||
sha256: h.sha256.clone(),
|
||||
// No SHA-512 digest available (see above); keeps the
|
||||
// `Checksums-Sha512` `.buildinfo` field omitted.
|
||||
sha512: String::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -457,6 +464,8 @@ fn include_dsc_artifacts(
|
||||
md5: h.md5.clone(),
|
||||
sha1: h.sha1.clone(),
|
||||
sha256: h.sha256.clone(),
|
||||
// No SHA-512 digest available (see above).
|
||||
sha512: String::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -474,6 +483,9 @@ fn include_dsc_artifacts(
|
||||
md5: p.md5.clone().unwrap_or_default(),
|
||||
sha1: p.sha1.clone().unwrap_or_default(),
|
||||
sha256: p.sha256.clone().unwrap_or_default(),
|
||||
// The `.dsc` records no SHA-512 (dpkg only writes
|
||||
// sha1/sha256 there).
|
||||
sha512: String::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -296,6 +296,12 @@ pub fn render_buildinfo(input: &BuildInfoInput) -> Paragraph {
|
||||
p.set("Checksums-Md5", &input.checksums.field_md5());
|
||||
p.set("Checksums-Sha1", &input.checksums.field_sha1());
|
||||
p.set("Checksums-Sha256", &input.checksums.field_sha256());
|
||||
// Only-if-populated: entries merged from a `.dsc` carry no SHA-512
|
||||
// (dpkg only records sha1/sha256 there), and an incomplete checksum
|
||||
// list must never be rendered.
|
||||
if let Some(sha512) = input.checksums.field_sha512() {
|
||||
p.set("Checksums-Sha512", &sha512);
|
||||
}
|
||||
}
|
||||
p.set("Build-Origin", &input.build_origin);
|
||||
p.set("Build-Architecture", &input.build_architecture);
|
||||
@@ -494,4 +500,78 @@ Architecture: amd64
|
||||
);
|
||||
assert_eq!(p.get("Format"), Some("1.0"));
|
||||
}
|
||||
|
||||
/// `Checksums-Sha512` is emitted (after `Checksums-Sha256`) only when
|
||||
/// every distributed file has a SHA-512 digest; entries merged without
|
||||
/// one (e.g. taken from a `.dsc`) omit the field entirely instead of
|
||||
/// rendering an incomplete checksum list.
|
||||
#[test]
|
||||
fn checksums_sha512_emitted_only_when_populated() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let artifact = dir.path().join("hello_1.0_all.deb");
|
||||
std::fs::write(&artifact, b"deb payload").unwrap();
|
||||
|
||||
let mut checksums = FileChecksums::new();
|
||||
checksums.add_file(&artifact).unwrap();
|
||||
|
||||
let mk_input = |checksums: FileChecksums| BuildInfoInput {
|
||||
source: "hello".to_string(),
|
||||
binaries: vec![],
|
||||
architecture: "all".to_string(),
|
||||
version: "1.0".to_string(),
|
||||
binary_only_changes: None,
|
||||
build_origin: "debian".to_string(),
|
||||
build_architecture: "amd64".to_string(),
|
||||
build_date: "Sat, 22 Aug 2026 23:08:42 +0200".to_string(),
|
||||
checksums,
|
||||
installed_build_depends: String::new(),
|
||||
environment: String::new(),
|
||||
};
|
||||
|
||||
// All digests computed: the field is present and parses back.
|
||||
let p = render_buildinfo(&mk_input(checksums.clone()));
|
||||
let keys: Vec<&str> = p.iter().map(|(k, _)| k).collect();
|
||||
assert_eq!(
|
||||
keys,
|
||||
vec![
|
||||
"Format",
|
||||
"Source",
|
||||
"Architecture",
|
||||
"Version",
|
||||
"Checksums-Md5",
|
||||
"Checksums-Sha1",
|
||||
"Checksums-Sha256",
|
||||
"Checksums-Sha512",
|
||||
"Build-Origin",
|
||||
"Build-Architecture",
|
||||
"Build-Date",
|
||||
]
|
||||
);
|
||||
let sha512_field = p.get("Checksums-Sha512").unwrap();
|
||||
let parsed =
|
||||
FileChecksums::parse_field(crate::debian::ChecksumKind::Sha512, sha512_field).unwrap();
|
||||
assert_eq!(parsed.len(), 1);
|
||||
assert_eq!(parsed[0].0, "hello_1.0_all.deb");
|
||||
assert_eq!(
|
||||
parsed[0].1.sha512,
|
||||
checksums.get("hello_1.0_all.deb").unwrap().sha512
|
||||
);
|
||||
|
||||
// An entry without SHA-512 (as merged from a `.dsc`) suppresses the
|
||||
// field; the other Checksums fields keep listing every file.
|
||||
checksums.insert_entry(
|
||||
"hello_1.0.orig.tar.xz",
|
||||
crate::debian::ChecksumEntry {
|
||||
size: 3,
|
||||
md5: checksums.get("hello_1.0_all.deb").unwrap().md5.clone(),
|
||||
sha1: String::new(),
|
||||
sha256: String::new(),
|
||||
sha512: String::new(),
|
||||
},
|
||||
);
|
||||
let p = render_buildinfo(&mk_input(checksums));
|
||||
assert!(p.get("Checksums-Sha512").is_none());
|
||||
let sha256_lines = p.get("Checksums-Sha256").unwrap().lines();
|
||||
assert_eq!(sha256_lines.filter(|l| !l.is_empty()).count(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,6 +544,10 @@ pub fn run_source_build(
|
||||
md5: partial.md5.clone().unwrap_or_default(),
|
||||
sha1: partial.sha1.clone().unwrap_or_default(),
|
||||
sha256: partial.sha256.clone().unwrap_or_default(),
|
||||
// The .dsc records no SHA-512 (dpkg only writes sha1/sha256
|
||||
// there); the empty digest keeps the .buildinfo's
|
||||
// `Checksums-Sha512` field omitted.
|
||||
sha512: String::new(),
|
||||
},
|
||||
);
|
||||
tarball_paths.push(path);
|
||||
|
||||
+267
-3
@@ -1,5 +1,5 @@
|
||||
//! File checksum computation and formatting for `.changes` / `.buildinfo`
|
||||
//! fields (MD5, SHA-1, SHA-256 + size), mirroring `Dpkg::Checksums`.
|
||||
//! fields (MD5, SHA-1, SHA-256, SHA-512 + size), mirroring `Dpkg::Checksums`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
@@ -7,7 +7,7 @@ use std::path::Path;
|
||||
|
||||
use md5::Md5;
|
||||
use sha1::Sha1;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
|
||||
/// Checksums and size of a single file.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -20,6 +20,40 @@ pub struct Entry {
|
||||
pub sha1: String,
|
||||
/// Lowercase hexadecimal SHA-256 digest.
|
||||
pub sha256: String,
|
||||
/// Lowercase hexadecimal SHA-512 digest.
|
||||
pub sha512: String,
|
||||
}
|
||||
|
||||
/// The checksum algorithm carried by a `Checksums-*` field body, as handled
|
||||
/// by [`FileChecksums::parse_field`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ChecksumKind {
|
||||
/// SHA-1 (`Checksums-Sha1` field).
|
||||
Sha1,
|
||||
/// SHA-256 (`Checksums-Sha256` field).
|
||||
Sha256,
|
||||
/// SHA-512 (`Checksums-Sha512` field).
|
||||
Sha512,
|
||||
}
|
||||
|
||||
impl ChecksumKind {
|
||||
/// The `Checksums-*` field name carrying this digest.
|
||||
pub fn field_name(self) -> &'static str {
|
||||
match self {
|
||||
ChecksumKind::Sha1 => "Checksums-Sha1",
|
||||
ChecksumKind::Sha256 => "Checksums-Sha256",
|
||||
ChecksumKind::Sha512 => "Checksums-Sha512",
|
||||
}
|
||||
}
|
||||
|
||||
/// Length in lowercase hex characters of one digest of this kind.
|
||||
fn digest_len(self) -> usize {
|
||||
match self {
|
||||
ChecksumKind::Sha1 => 40,
|
||||
ChecksumKind::Sha256 => 64,
|
||||
ChecksumKind::Sha512 => 128,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute all supported checksums of a file.
|
||||
@@ -30,6 +64,7 @@ fn compute(path: &Path) -> Result<Entry, Box<dyn std::error::Error>> {
|
||||
let mut md5_hasher = Md5::new();
|
||||
let mut sha1_hasher = Sha1::new();
|
||||
let mut sha256_hasher = Sha256::new();
|
||||
let mut sha512_hasher = Sha512::new();
|
||||
let mut size: u64 = 0;
|
||||
let mut buf = [0u8; 64 * 1024];
|
||||
|
||||
@@ -41,6 +76,7 @@ fn compute(path: &Path) -> Result<Entry, Box<dyn std::error::Error>> {
|
||||
md5_hasher.update(&buf[..n]);
|
||||
sha1_hasher.update(&buf[..n]);
|
||||
sha256_hasher.update(&buf[..n]);
|
||||
sha512_hasher.update(&buf[..n]);
|
||||
size += n as u64;
|
||||
}
|
||||
|
||||
@@ -49,6 +85,7 @@ fn compute(path: &Path) -> Result<Entry, Box<dyn std::error::Error>> {
|
||||
md5: hex::encode(md5_hasher.finalize()),
|
||||
sha1: hex::encode(sha1_hasher.finalize()),
|
||||
sha256: hex::encode(sha256_hasher.finalize()),
|
||||
sha512: hex::encode(sha512_hasher.finalize()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -168,6 +205,84 @@ impl FileChecksums {
|
||||
pub fn field_sha256(&self) -> String {
|
||||
self.format_field(|e| &e.sha256)
|
||||
}
|
||||
|
||||
/// Value for the `Checksums-Sha512` field, or `None` when any registered
|
||||
/// file has no SHA-512 digest (e.g. entries merged from a `.dsc`, which
|
||||
/// dpkg only writes with sha1/sha256 checksums): renderers omit the
|
||||
/// field instead of writing an incomplete checksum list.
|
||||
pub fn field_sha512(&self) -> Option<String> {
|
||||
if self.iter().any(|(_, e)| e.sha512.is_empty()) {
|
||||
return None;
|
||||
}
|
||||
Some(self.format_field(|e| &e.sha512))
|
||||
}
|
||||
|
||||
/// Parse the body of a `Checksums-Sha1` / `Checksums-Sha256` /
|
||||
/// `Checksums-Sha512` field (as rendered by [`FileChecksums::field_sha1`],
|
||||
/// [`FileChecksums::field_sha256`] or [`FileChecksums::field_sha512`])
|
||||
/// into `(name, entry)` pairs, ready to be fed into
|
||||
/// [`FileChecksums::insert_entry`] (e.g. when consuming a `.dsc`).
|
||||
///
|
||||
/// Each non-blank line holds `"<hex digest> <size> <name>"`; blank lines
|
||||
/// are tolerated and anything else is a malformed line, reported as an
|
||||
/// error naming [`ChecksumKind::field_name`] and the offending line. Only
|
||||
/// the digest selected by `kind` is filled in the returned entries: the
|
||||
/// other digest fields are left empty and must be completed from the
|
||||
/// remaining `Checksums-*` fields (or by recomputation) before rendering.
|
||||
///
|
||||
/// Note: this deliberately re-implements the line grammar of the private
|
||||
/// `build::parse_checksum_field` helper (which additionally accepts the
|
||||
/// legacy 5-column `Files` layout); the two are intentionally not unified
|
||||
/// across modules.
|
||||
pub fn parse_field(kind: ChecksumKind, value: &str) -> Result<Vec<(String, Entry)>, String> {
|
||||
let mut entries = Vec::new();
|
||||
for line in value.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let tokens: Vec<&str> = line.split_whitespace().collect();
|
||||
let [digest, size, name] = tokens.as_slice() else {
|
||||
return Err(format!(
|
||||
"malformed '{}' line (expected 'checksum size name', got {} \
|
||||
columns): '{line}'",
|
||||
kind.field_name(),
|
||||
tokens.len()
|
||||
));
|
||||
};
|
||||
let size: u64 = size.parse().map_err(|_| {
|
||||
format!(
|
||||
"malformed '{}' line (size '{size}' is not a number): '{line}'",
|
||||
kind.field_name()
|
||||
)
|
||||
})?;
|
||||
let digest_ok = digest.len() == kind.digest_len()
|
||||
&& digest
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
|
||||
if !digest_ok {
|
||||
return Err(format!(
|
||||
"malformed '{}' line (digest '{digest}' is not {} lowercase \
|
||||
hex characters): '{line}'",
|
||||
kind.field_name(),
|
||||
kind.digest_len()
|
||||
));
|
||||
}
|
||||
let mut entry = Entry {
|
||||
size,
|
||||
md5: String::new(),
|
||||
sha1: String::new(),
|
||||
sha256: String::new(),
|
||||
sha512: String::new(),
|
||||
};
|
||||
match kind {
|
||||
ChecksumKind::Sha1 => entry.sha1 = digest.to_string(),
|
||||
ChecksumKind::Sha256 => entry.sha256 = digest.to_string(),
|
||||
ChecksumKind::Sha512 => entry.sha512 = digest.to_string(),
|
||||
}
|
||||
entries.push((name.to_string(), entry));
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -184,16 +299,165 @@ mod tests {
|
||||
cs.add_file(&p).unwrap();
|
||||
|
||||
let e = cs.get("sample.txt").unwrap();
|
||||
// Verified with coreutils: echo "hello world" | md5sum / sha1sum / sha256sum
|
||||
// Verified with coreutils: echo "hello world" | md5sum / sha1sum / sha256sum / sha512sum
|
||||
assert_eq!(e.md5, "6f5902ac237024bdd0c176cb93063dc4");
|
||||
assert_eq!(e.sha1, "22596363b3de40b06f981fb85d82312e8c0ed511");
|
||||
assert_eq!(
|
||||
e.sha256,
|
||||
"a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447"
|
||||
);
|
||||
assert_eq!(
|
||||
e.sha512,
|
||||
"db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de\
|
||||
84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593"
|
||||
);
|
||||
assert_eq!(e.size, 12);
|
||||
}
|
||||
|
||||
/// SHA-512 of the empty input is a well-known constant: a zero-size file
|
||||
/// must still carry it (never an empty digest string, which is reserved
|
||||
/// for "digest unknown", e.g. entries merged from a `.dsc`).
|
||||
#[test]
|
||||
fn sha512_of_empty_file_is_the_known_constant() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p = dir.path().join("empty.txt");
|
||||
std::fs::write(&p, b"").unwrap();
|
||||
|
||||
let mut cs = FileChecksums::new();
|
||||
cs.add_file(&p).unwrap();
|
||||
|
||||
let e = cs.get("empty.txt").unwrap();
|
||||
assert_eq!(e.size, 0);
|
||||
assert_eq!(
|
||||
e.sha512,
|
||||
"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce\
|
||||
47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
|
||||
);
|
||||
}
|
||||
|
||||
/// All four digests render; the `Checksums-Sha512` field round-trips
|
||||
/// through [`FileChecksums::parse_field`] back into a registry with
|
||||
/// identical names (insertion order), sizes and SHA-512 digests.
|
||||
#[test]
|
||||
fn sha512_field_round_trip() {
|
||||
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();
|
||||
cs.add_file(&b).unwrap();
|
||||
cs.add_file(&a).unwrap();
|
||||
|
||||
// Every digest kind must be populated and render a full field.
|
||||
assert!(!cs.field_md5().is_empty());
|
||||
assert!(!cs.field_sha1().is_empty());
|
||||
assert!(!cs.field_sha256().is_empty());
|
||||
let sha512_field = cs.field_sha512().expect("all entries have sha512");
|
||||
|
||||
// Round-trip the Checksums-Sha512 field through the parser.
|
||||
let mut reparsed = FileChecksums::new();
|
||||
for (key, entry) in FileChecksums::parse_field(ChecksumKind::Sha512, &sha512_field).unwrap()
|
||||
{
|
||||
reparsed.insert_entry(&key, entry);
|
||||
}
|
||||
let keys: Vec<&str> = reparsed.iter().map(|(k, _)| k.as_str()).collect();
|
||||
assert_eq!(keys, vec!["b.txt", "a.txt"], "insertion order preserved");
|
||||
for (key, e) in cs.iter() {
|
||||
let got = reparsed.get(key).unwrap();
|
||||
assert_eq!(got.size, e.size, "{key}");
|
||||
assert_eq!(got.sha512, e.sha512, "{key}");
|
||||
}
|
||||
// Rendering the re-parsed registry yields the same field value.
|
||||
assert_eq!(reparsed.field_sha512().unwrap(), sha512_field);
|
||||
|
||||
// The parser dispatches on `kind`: a Checksums-Sha256 body fills the
|
||||
// sha256 column, leaving the others (including sha512) unknown.
|
||||
let (key, entry) =
|
||||
&FileChecksums::parse_field(ChecksumKind::Sha256, &cs.field_sha256()).unwrap()[0];
|
||||
assert_eq!(entry.sha256, cs.get(key).unwrap().sha256);
|
||||
assert!(entry.sha512.is_empty());
|
||||
let (_, entry) =
|
||||
&FileChecksums::parse_field(ChecksumKind::Sha512, &sha512_field).unwrap()[0];
|
||||
assert!(entry.md5.is_empty() && entry.sha1.is_empty() && entry.sha256.is_empty());
|
||||
assert!(!entry.sha512.is_empty());
|
||||
}
|
||||
|
||||
/// Malformed `Checksums-Sha512` bodies (wrong column count, non-numeric
|
||||
/// size, wrong digest shape) must be rejected with an error naming the
|
||||
/// field and the offending line; blank lines are tolerated.
|
||||
#[test]
|
||||
fn parse_field_rejects_malformed_lines() {
|
||||
// 128 lowercase hex characters, as rendered by field_sha512.
|
||||
let digest = "ab".repeat(64);
|
||||
|
||||
// Blank lines are skipped.
|
||||
let entries =
|
||||
FileChecksums::parse_field(ChecksumKind::Sha512, &format!("\n {digest} 12 a.txt\n\n"))
|
||||
.unwrap();
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].0, "a.txt");
|
||||
assert_eq!(entries[0].1.size, 12);
|
||||
|
||||
// 2 columns: missing the name.
|
||||
let err =
|
||||
FileChecksums::parse_field(ChecksumKind::Sha512, &format!("{digest} 12")).unwrap_err();
|
||||
assert!(err.contains("Checksums-Sha512"), "{err}");
|
||||
assert!(err.contains(&format!("{digest} 12")), "{err}");
|
||||
|
||||
// 4 columns.
|
||||
let err =
|
||||
FileChecksums::parse_field(ChecksumKind::Sha512, &format!(" {digest} 12 bogus a.txt"))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("Checksums-Sha512"), "{err}");
|
||||
assert!(err.contains("a.txt"), "{err}");
|
||||
|
||||
// Non-numeric size.
|
||||
let err =
|
||||
FileChecksums::parse_field(ChecksumKind::Sha512, &format!(" {digest} twelve a.txt"))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("not a number"), "{err}");
|
||||
assert!(err.contains("twelve"), "{err}");
|
||||
|
||||
// Digest that is not 128 lowercase hex characters.
|
||||
let err = FileChecksums::parse_field(ChecksumKind::Sha512, " abc123 12 a.txt").unwrap_err();
|
||||
assert!(err.contains("lowercase hex"), "{err}");
|
||||
}
|
||||
|
||||
/// `field_sha512` is only-if-populated: an entry merged without a SHA-512
|
||||
/// digest (e.g. taken from a `.dsc`) suppresses the whole field instead
|
||||
/// of rendering an incomplete checksum list.
|
||||
#[test]
|
||||
fn field_sha512_omitted_when_any_digest_missing() {
|
||||
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();
|
||||
assert!(cs.field_sha512().is_some());
|
||||
|
||||
cs.insert_entry(
|
||||
"from.dsc",
|
||||
Entry {
|
||||
size: 12,
|
||||
md5: "d41d8cd98f00b204e9800998ecf8427e".to_string(),
|
||||
sha1: "da39a3ee5e6b4b0d3255bfef95601890afd80709".to_string(),
|
||||
sha256: format!("e3b0{:0>62}", "0"),
|
||||
sha512: String::new(), // not recorded in .dsc files
|
||||
},
|
||||
);
|
||||
assert!(
|
||||
cs.field_sha512().is_none(),
|
||||
"one incomplete entry must suppress Checksums-Sha512"
|
||||
);
|
||||
// The other kinds are unaffected.
|
||||
assert!(!cs.field_md5().is_empty());
|
||||
assert!(!cs.field_sha1().is_empty());
|
||||
assert!(!cs.field_sha256().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insertion_order_preserved() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ pub use changelog::{
|
||||
ChangelogEntry, parse_changelog_entry, parse_changelog_entry_from_str,
|
||||
parse_previous_version_from_str,
|
||||
};
|
||||
pub use checksums::{Entry as ChecksumEntry, FileChecksums};
|
||||
pub use checksums::{ChecksumKind, Entry as ChecksumEntry, FileChecksums};
|
||||
pub use control::{
|
||||
ControlInfo, Paragraph, parse_paragraphs, strip_clearsigned_armour, write_paragraph,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user