Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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()
        ),
    }
}