use super::{BatchReply, MAX_BATCH_HEADER_BYTES, ObjectKind, read_batch_reply};
use crate::candidate_error::{CandidateError, CandidateFailure};
use crate::candidate_object::{ObjectId, parse_object_id};
use std::io::{BufRead, Read};
const NAME: &str = "0123456789abcdef0123456789abcdef01234567";
const OTHER: &str = "89abcdef0123456789abcdef0123456789abcdef";
fn name() -> ObjectId {
return parse_object_id(NAME.as_bytes()).expect("fixture name");
}
fn failure_of(stream: &[u8]) -> CandidateFailure {
let mut remaining: &[u8] = stream;
let error: CandidateError =
read_batch_reply(&mut remaining, NAME.as_bytes()).expect_err("rejected reply");
assert!(
error
.message
.starts_with("cli-git could not read a Git object: git cat-file --batch "),
"{error}"
);
return error.failure;
}
struct BrokenStream;
impl Read for BrokenStream {
fn read(&mut self, _buffer: &mut [u8]) -> std::io::Result<usize> {
return Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe));
}
}
impl BufRead for BrokenStream {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
return Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe));
}
fn consume(&mut self, _amount: usize) {}
}
#[test]
fn found_reply_is_framed_by_its_declared_size() {
let content: Vec<u8> = format!("a\0b\u{0}\n{OTHER} blob 3\nxyz\n").into_bytes();
let mut content_with_byte: Vec<u8> = content.clone();
content_with_byte.push(0xff);
let mut stream: Vec<u8> = format!("{NAME} blob {}\n", content_with_byte.len()).into_bytes();
stream.extend_from_slice(content_with_byte.as_slice());
stream.extend_from_slice(b"\nNEXT");
let mut remaining: &[u8] = stream.as_slice();
assert_eq!(
read_batch_reply(&mut remaining, NAME.as_bytes()).expect("found"),
BatchReply::Found {
object: name(),
kind: ObjectKind::Blob,
bytes: content_with_byte.clone(),
}
);
assert_eq!(remaining, b"NEXT");
assert!(
content_with_byte.len() >= 10,
"a two-digit size is exercised"
);
}
#[test]
fn empty_object_is_found_with_no_bytes() {
let stream: String = format!("{NAME} blob 0\n\n");
let mut remaining: &[u8] = stream.as_bytes();
assert_eq!(
read_batch_reply(&mut remaining, NAME.as_bytes()).expect("empty"),
BatchReply::Found {
object: name(),
kind: ObjectKind::Blob,
bytes: Vec::new(),
}
);
assert!(remaining.is_empty());
}
#[test]
fn consecutive_replies_stay_in_step() {
let stream: String = format!("{NAME} blob 2\nhi\n{OTHER} missing\n{OTHER} tree 1\nT\n");
let mut remaining: &[u8] = stream.as_bytes();
let first: BatchReply = read_batch_reply(&mut remaining, NAME.as_bytes()).expect("first");
assert_eq!(
first,
BatchReply::Found {
object: name(),
kind: ObjectKind::Blob,
bytes: b"hi".to_vec(),
}
);
assert_eq!(
read_batch_reply(&mut remaining, OTHER.as_bytes()).expect("second"),
BatchReply::Missing
);
assert_eq!(
read_batch_reply(&mut remaining, OTHER.as_bytes()).expect("third"),
BatchReply::Found {
object: parse_object_id(OTHER.as_bytes()).expect("name"),
kind: ObjectKind::Tree,
bytes: b"T".to_vec(),
}
);
assert!(remaining.is_empty());
}
#[test]
fn object_types_and_symbolic_requests_are_reported() {
for (word, kind) in [
("blob", ObjectKind::Blob),
("tree", ObjectKind::Tree),
("commit", ObjectKind::Commit),
("tag", ObjectKind::Tag),
] {
let stream: String = format!("{NAME} {word} 3\nabc\n");
let mut remaining: &[u8] = stream.as_bytes();
assert_eq!(
read_batch_reply(&mut remaining, b"HEAD").expect("found"),
BatchReply::Found {
object: name(),
kind,
bytes: b"abc".to_vec(),
}
);
}
}
#[test]
fn missing_reply_echoes_the_request() {
for request in [NAME, "HEAD", ""] {
let stream: String = format!("{request} missing\nNEXT");
let mut remaining: &[u8] = stream.as_bytes();
assert_eq!(
read_batch_reply(&mut remaining, request.as_bytes()).expect("missing"),
BatchReply::Missing
);
assert_eq!(remaining, b"NEXT");
}
}
#[test]
fn malformed_headers_are_refused() {
let huge: String = "9".repeat(30);
for header in [
format!("{NAME} blob"),
format!("{NAME} blob 3 extra"),
format!("{NAME} blob 3"),
format!(" {NAME} blob 3"),
format!("{} blob 3", NAME.to_uppercase()),
format!("{} blob 3", &NAME[..39]),
format!("{NAME} BLOB 3"),
format!("{NAME} blobs 3"),
format!("{NAME} file 3"),
format!("{NAME} blob "),
format!("{NAME} blob +3"),
format!("{NAME} blob -3"),
format!("{NAME} blob 3x"),
format!("{NAME} blob 03"),
format!("{NAME} blob 00"),
format!("{NAME} blob 0x3"),
format!("{NAME} blob {huge}"),
format!("{NAME} ambiguous"),
format!("{OTHER} missing"),
format!("{NAME} missing "),
String::from("missing"),
String::from(""),
] {
let stream: String = format!("{header}\nabc\n");
assert_eq!(
failure_of(stream.as_bytes()),
CandidateFailure::ReplyMalformed,
"{header:?}"
);
}
}
#[test]
fn reply_for_another_object_is_refused() {
let stream: String = format!("{OTHER} blob 3\nabc\n");
assert_eq!(
failure_of(stream.as_bytes()),
CandidateFailure::ReplyMismatched
);
}
#[test]
fn truncated_replies_are_refused() {
for stream in [
format!("{NAME} blob 3"),
format!("{NAME} bl"),
format!("{NAME} blob 3\n"),
format!("{NAME} blob 3\nab"),
format!("{NAME} blob 3\nabc"),
format!("{NAME} blob 0\n"),
format!("{NAME} blob 999999999999999\nabc\n"),
] {
assert_eq!(
failure_of(stream.as_bytes()),
CandidateFailure::ReplyTruncated,
"{stream:?}"
);
}
}
#[test]
fn content_past_the_declared_size_is_refused() {
let stream: String = format!("{NAME} blob 3\nabcd\n");
assert_eq!(
failure_of(stream.as_bytes()),
CandidateFailure::ReplyMalformed
);
let no_feed: String = format!("{NAME} blob 3\nabc\r");
assert_eq!(
failure_of(no_feed.as_bytes()),
CandidateFailure::ReplyMalformed
);
}
#[test]
fn ended_and_unreadable_streams_are_reader_failures() {
assert_eq!(failure_of(b""), CandidateFailure::ReaderEnded);
let mut broken: BrokenStream = BrokenStream;
let error: CandidateError =
read_batch_reply(&mut broken, NAME.as_bytes()).expect_err("unreadable header");
assert_eq!(error.failure, CandidateFailure::ReaderEnded);
assert!(error.message.contains("could not be read"), "{error}");
}
#[test]
fn unreadable_content_is_a_reader_failure() {
let header: String = format!("{NAME} blob 3\n");
let mut stream: std::io::BufReader<std::io::Chain<&[u8], BrokenStream>> =
std::io::BufReader::new(header.as_bytes().chain(BrokenStream));
let error: CandidateError =
read_batch_reply(&mut stream, NAME.as_bytes()).expect_err("unreadable content");
assert_eq!(error.failure, CandidateFailure::ReaderEnded);
}
#[test]
fn header_length_bound_is_exact() {
assert_eq!(MAX_BATCH_HEADER_BYTES, 128);
let longest: String = "r".repeat(MAX_BATCH_HEADER_BYTES - 8);
let accepted: String = format!("{longest} missing\n");
let mut remaining: &[u8] = accepted.as_bytes();
assert_eq!(
read_batch_reply(&mut remaining, longest.as_bytes()).expect("longest header"),
BatchReply::Missing
);
let too_long: String = "r".repeat(MAX_BATCH_HEADER_BYTES - 7);
let refused: String = format!("{too_long} missing\n");
let mut refused_stream: &[u8] = refused.as_bytes();
assert_eq!(
read_batch_reply(&mut refused_stream, too_long.as_bytes())
.expect_err("overlong header")
.failure,
CandidateFailure::ReplyMalformed
);
let endless: Vec<u8> = vec![b'x'; MAX_BATCH_HEADER_BYTES * 4];
let mut endless_stream: &[u8] = endless.as_slice();
assert_eq!(
read_batch_reply(&mut endless_stream, NAME.as_bytes())
.expect_err("endless header")
.failure,
CandidateFailure::ReplyMalformed
);
assert_eq!(
endless_stream.len(),
MAX_BATCH_HEADER_BYTES * 4 - (MAX_BATCH_HEADER_BYTES + 1)
);
let at_bound: Vec<u8> = vec![b'x'; MAX_BATCH_HEADER_BYTES];
assert_eq!(
failure_of(at_bound.as_slice()),
CandidateFailure::ReplyTruncated
);
}