girt 0.13.0

An incremental, idiomatic Rust implementation of Git
Documentation
use std::io::{self, Read};
use std::sync::atomic::{AtomicBool, Ordering};

use rstest::rstest;

use super::*;
use crate::{ObjectFormat, ObjectId};

fn bytes(format: ObjectFormat, tail: &[u8]) -> Vec<u8> {
    [
        format!("{} {} ", ObjectId::null(format), id(format)).as_bytes(),
        tail,
    ]
    .concat()
}
fn id(format: ObjectFormat) -> ObjectId {
    ObjectId::from_hex(format, &"12".repeat(format.digest_len())).unwrap()
}
fn read(format: ObjectFormat, data: &[u8], limits: ReflogLimits) -> ImportedReflog {
    ImportedReflog::read(format, data, limits, &AtomicBool::new(false))
}

#[rstest]
#[case::cr(b"A <a@b> 1 +0000\ta\rb\n", b"a\rb", 1)]
#[case::nul(b"A <a@b> 1 +0000\ta\0b\n", b"a\0b", 1)]
#[case::suffix(b"A <a@b> 1 +0000suffix\tfixture\n", b"suffix\tfixture", 1)]
#[case::unsigned(b"A <a@b> 18446744073709551615 +0000\tfixture\n", b"fixture", u64::MAX as i128)]
#[case::negative(b"A <a@b> -1 +0000\tfixture\n", b"fixture", -1)]
fn retains_exact_imports(
    #[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
    #[case] tail: &[u8],
    #[case] message: &[u8],
    #[case] seconds: i128,
) {
    let data = bytes(format, tail);
    let log = read(format, &data, ReflogLimits::default());
    assert!(log.is_complete());
    assert_eq!(
        log.records(),
        &[ImportedRecord::File {
            format,
            bytes: data
        }]
    );
    let fields = log.records()[0].fields().unwrap();
    assert_eq!(fields.seconds, seconds);
    assert_eq!(fields.message, message);
    assert_eq!(log.recoverable_roots().collect::<Vec<_>>(), [id(format)]);
}

#[rstest]
#[case::short(b"A <a@b> 1 +01\tx\n")]
#[case::overflow(b"A <a@b> 9999999999999999999999999999999999999999 +0000\tx\n")]
#[case::unterminated(b"A <a@b> 1 +0000\tx")]
#[case::identity(b"broken\n")]
fn corrupt_records_keep_roots(
    #[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
    #[case] tail: &[u8],
) {
    let data = bytes(format, tail);
    let log = read(format, &data, ReflogLimits::default());
    assert!(!log.is_complete());
    assert!(matches!(log.end(), ReflogReadEnd::Eof));
    assert_eq!(log.recoverable_roots().collect::<Vec<_>>(), [id(format)]);
    assert_eq!(
        log.records(),
        &[ImportedRecord::File {
            format,
            bytes: data
        }]
    );
}

#[rstest]
#[case::input(ReflogLimits { bytes: 90, ..ReflogLimits::default() })]
#[case::retained(ReflogLimits { retained_bytes: 90, ..ReflogLimits::default() })]
#[case::record(ReflogLimits { record_bytes: 90, ..ReflogLimits::default() })]
fn limits_retain_a_prefix_and_its_roots(#[case] limits: ReflogLimits) {
    let data = bytes(ObjectFormat::Sha1, b"A <a@b> 1 +0000\tmessage\n");
    let log = read(ObjectFormat::Sha1, &data, limits);
    assert!(!log.is_complete());
    assert!(matches!(log.end(), ReflogReadEnd::Limit(_)));
    assert_eq!(
        log.records(),
        &[ImportedRecord::File {
            format: ObjectFormat::Sha1,
            bytes: data[..90].to_vec()
        }]
    );
    assert_eq!(
        log.recoverable_roots().collect::<Vec<_>>(),
        [id(ObjectFormat::Sha1)]
    );
}

#[test]
fn recovers_second_id_when_first_is_corrupt_and_continues() {
    let mut first = bytes(ObjectFormat::Sha1, b"bad\n");
    first[0] = b'z';
    let second = bytes(ObjectFormat::Sha1, b"A <a@b> 1 +0000\tm\n");
    let log = read(
        ObjectFormat::Sha1,
        &[first, second].concat(),
        ReflogLimits::default(),
    );
    assert!(!log.is_complete());
    assert_eq!(log.records().len(), 2);
    assert_eq!(
        log.recoverable_roots().collect::<Vec<_>>(),
        [id(ObjectFormat::Sha1); 2]
    );
}

#[test]
fn record_count_and_exact_byte_boundary_are_explicit() {
    let data = bytes(ObjectFormat::Sha1, b"A <a@b> 1 +0000\tm\n");
    let log = read(
        ObjectFormat::Sha1,
        &data.repeat(2),
        ReflogLimits {
            records: 1,
            ..ReflogLimits::default()
        },
    );
    assert_eq!(log.records().len(), 1);
    assert!(matches!(log.end(), ReflogReadEnd::Limit("records")));
    let exact = read(
        ObjectFormat::Sha1,
        &data,
        ReflogLimits {
            bytes: data.len(),
            ..ReflogLimits::default()
        },
    );
    assert!(!exact.is_complete());
    assert_eq!(exact.records().len(), 1);
}

struct FaultReader {
    prefix: io::Cursor<Vec<u8>>,
}
impl Read for FaultReader {
    fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
        let count = self.prefix.read(output)?;
        if count == 0 {
            return Err(io::Error::other("original injected read fault"));
        }
        Ok(count)
    }
}
#[test]
fn io_failure_preserves_complete_and_partial_records() {
    let complete = bytes(ObjectFormat::Sha1, b"A <a@b> 1 +0000\tm\n");
    let partial = bytes(ObjectFormat::Sha1, b"A <a@b>");
    let data = [complete, partial.clone()].concat();
    let reader = FaultReader {
        prefix: io::Cursor::new(data),
    };
    let log = ImportedReflog::read(
        ObjectFormat::Sha1,
        reader,
        ReflogLimits::default(),
        &AtomicBool::new(false),
    );
    assert!(matches!(log.end(), ReflogReadEnd::Io(_)));
    assert!(!log.is_complete());
    assert_eq!(
        log.records()[1],
        ImportedRecord::File {
            format: ObjectFormat::Sha1,
            bytes: partial
        }
    );
    assert_eq!(log.recoverable_roots().count(), 2);
}

struct CancellingReader<'a> {
    cancel: &'a AtomicBool,
    reads: usize,
}
impl Read for CancellingReader<'_> {
    fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
        self.reads += 1;
        if self.reads == 2 {
            self.cancel.store(true, Ordering::Relaxed);
        }
        output.fill(b'x');
        Ok(output.len())
    }
}
#[test]
fn cancellation_during_large_record_keeps_bounded_prefix() {
    let cancel = AtomicBool::new(false);
    let reader = CancellingReader {
        cancel: &cancel,
        reads: 0,
    };
    let log = ImportedReflog::read(ObjectFormat::Sha1, reader, ReflogLimits::default(), &cancel);
    assert!(matches!(log.end(), ReflogReadEnd::Cancelled));
    assert_eq!(
        log.records(),
        &[ImportedRecord::File {
            format: ObjectFormat::Sha1,
            bytes: vec![b'x'; 8192]
        }]
    );
}

#[test]
fn empty_and_zero_budget_scans_are_distinct() {
    assert!(read(ObjectFormat::Sha1, b"", ReflogLimits::default()).is_complete());
    let log = read(
        ObjectFormat::Sha1,
        b"",
        ReflogLimits {
            bytes: 0,
            ..ReflogLimits::default()
        },
    );
    assert!(matches!(log.end(), ReflogReadEnd::Limit(_)));
    assert!(!log.is_complete());
}