crates/gunnar-bitmap/tests/damage.rs
//! What a damaged `.bitmap` must do, and what it silently does instead.
//!
//! These exist because a red probe embarrassed the parser. Truncating a real
//! 28 520-byte bitmap to 14 260 bytes did not produce an error: it produced a
//! perfectly well-formed index reporting 571 objects instead of 4136. The object
//! count is not stated anywhere in the file, only inferred from how many bytes
//! of name-hash cache are left over, so a shorter file is a *valid* file about a
//! smaller pack.
//!
//! git never notices this either, because it always has the pack open and checks
//! the count against it. A standalone reader has no pack, so it needs the
//! trailing checksum — which git does not verify at all.
mod support;
use gunnar_bitmap::{Error, Index};
use support::Fixture;
fn fixture_bytes() -> (Vec<u8>, gix_hash::Kind) {
let fx = Fixture::build(30, gix_hash::Kind::Sha1);
(fx.bitmap_bytes(), fx.object_hash)
}
#[test]
fn an_intact_bitmap_verifies() {
let (data, kind) = fixture_bytes();
let index = Index::from_bytes(&data, kind).expect("parse");
index.verify_checksum().expect("git's own file must verify");
// The positive case has to be established before any red below means
// anything: a `verify_checksum` that always failed would pass every
// negative test in this file and nothing else would notice.
}
#[test]
fn a_flipped_byte_is_caught_by_the_checksum() {
let (data, kind) = fixture_bytes();
let mut checked = 0;
// Flip a byte in each region: a type bitmap, an entry bitmap, the hash
// cache. Corruption anywhere in the body must be caught.
for at in [40usize, data.len() / 2, data.len() - 40] {
let mut damaged = data.clone();
damaged[at] ^= 0xFF;
assert_ne!(damaged, data, "the flip at {at} changed nothing");
// Parsing may or may not succeed depending on where the damage lands;
// what must never happen is a clean parse that also verifies.
if let Ok(index) = Index::from_bytes(&damaged, kind) {
let err = index.verify_checksum().expect_err(
"a bitmap with a flipped byte verified clean, so verify_checksum is blind",
);
assert!(
matches!(err, Error::ChecksumMismatch { .. }),
"expected a checksum mismatch at {at}, got {err}"
);
}
checked += 1;
}
assert_eq!(checked, 3);
}
#[test]
fn a_bitmap_short_by_one_name_hash_still_parses_and_only_the_checksum_catches_it() {
let (data, kind) = fixture_bytes();
let full = Index::from_bytes(&data, kind).expect("parse");
let objects = full.num_objects().expect("git writes a hash cache");
let hash_len = 20; // the fixture is SHA-1
// Remove four bytes from the start of the name-hash cache. The result is
// four bytes shorter, so the derived object count drops by exactly one and
// every structural check still passes. This is the shape that matters: not
// a mangled file, a *plausible* one.
let cache_start = data.len() - hash_len - 4 * objects as usize;
let mut short = data.clone();
short.drain(cache_start..cache_start + 4);
assert_eq!(short.len(), data.len() - 4);
let index = Index::from_bytes(&short, kind)
.expect("a bitmap short by one name hash still parses, which is exactly the problem");
assert_eq!(
index.num_objects(),
Some(objects - 1),
"the derived object count must have silently dropped, or this test is not \
demonstrating the hazard it claims"
);
assert_eq!(
index.entries().len(),
full.entries().len(),
"the entries are untouched, so nothing structural could have flagged this"
);
// Only the checksum sees it.
let err = index
.verify_checksum()
.expect_err("a bitmap missing four bytes must fail checksum verification");
assert!(matches!(err, Error::ChecksumMismatch { .. }), "got {err}");
// And so would the pack, if the caller has one.
assert!(index.verify_object_count(objects).is_err());
}
#[test]
fn a_bitmap_truncated_mid_entry_fails_to_parse() {
let (data, kind) = fixture_bytes();
let err = Index::from_bytes(&data[..data.len() / 2], kind)
.expect_err("a cut through the entry region must be a parse error");
assert!(
matches!(err, Error::UnexpectedEof { .. }),
"expected an eof error, got {err}"
);
}
#[test]
fn an_object_count_that_disagrees_with_the_pack_is_caught() {
let (data, kind) = fixture_bytes();
let index = Index::from_bytes(&data, kind).expect("parse");
let n = index.num_objects().expect("git writes a hash cache");
index
.verify_object_count(n)
.expect("the true count must pass");
let err = index
.verify_object_count(n + 1)
.expect_err("a disagreeing count must be reported");
assert!(
matches!(err, Error::ObjectCountMismatch { .. }),
"got {err}"
);
}
#[test]
fn a_bitmap_with_the_wrong_magic_is_rejected() {
let (mut data, kind) = fixture_bytes();
data[0] = b'X';
let err = Index::from_bytes(&data, kind).expect_err("bad magic must be rejected");
assert!(matches!(err, Error::Magic { .. }), "got {err}");
}
#[test]
fn the_mandatory_full_dag_flag_is_enforced() {
let (mut data, kind) = fixture_bytes();
// Clear bit 0 of the flags word at offset 6..8.
data[7] &= !0x01;
let err = Index::from_bytes(&data, kind).expect_err("FULL_DAG is required by the format");
assert!(matches!(err, Error::MissingFullDag { .. }), "got {err}");
}
#[test]
fn reading_a_sha1_bitmap_as_sha256_fails_rather_than_misparsing() {
// The header checksum width follows the repository hash and nothing in the
// file records which one it is. Guessing wrong shifts every following field
// by 12 bytes; that must not yield a plausible index.
let (data, _) = fixture_bytes();
match Index::from_bytes(&data, gix_hash::Kind::Sha256) {
Err(_) => {}
Ok(index) => panic!(
"a SHA-1 bitmap parsed as SHA-256 and produced {} entries; a wrong hash \
guess must fail loudly",
index.entries().len()
),
}
}