use crate::candidate_error::{CandidateError, CandidateFailure};
use crate::candidate_object::{ObjectId, parse_object_id};
use std::io::{BufRead, Read};
pub const MAX_BATCH_HEADER_BYTES: usize = 128;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ObjectKind {
Blob,
Tree,
Commit,
Tag,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BatchReply {
Found {
object: ObjectId,
kind: ObjectKind,
bytes: Vec<u8>,
},
Missing,
}
fn reply_failure(failure: CandidateFailure, detail: &str) -> CandidateError {
return CandidateError::new(
failure,
format!(
"cli-git could not read a Git object: git cat-file --batch {detail}. \
No candidate content was accepted from this reply."
)
.as_str(),
);
}
fn read_header_line(stream: &mut dyn BufRead) -> Result<Vec<u8>, CandidateError> {
let mut line: Vec<u8> = Vec::new();
let limit: u64 = MAX_BATCH_HEADER_BYTES as u64 + 1;
match stream.take(limit).read_until(b'\n', &mut line) {
Ok(_) => {}
Err(error) => {
return Err(reply_failure(
CandidateFailure::ReaderEnded,
format!("could not be read ({error})").as_str(),
));
}
}
if line.is_empty() {
return Err(reply_failure(
CandidateFailure::ReaderEnded,
"ended before answering a request",
));
}
if line.last() != Some(&b'\n') {
if line.len() > MAX_BATCH_HEADER_BYTES {
return Err(reply_failure(
CandidateFailure::ReplyMalformed,
"sent a header longer than any object header",
));
}
return Err(reply_failure(
CandidateFailure::ReplyTruncated,
"ended in the middle of a header",
));
}
line.pop();
return Ok(line);
}
fn parse_size(digits: &[u8]) -> Option<usize> {
if digits.len() > 1 && digits[0] == b'0' {
return None;
}
if !digits.iter().all(u8::is_ascii_digit) {
return None;
}
let text: &str = std::str::from_utf8(digits).ok()?;
return text.parse::<usize>().ok();
}
fn object_kind(word: &[u8]) -> Option<ObjectKind> {
if word == b"blob" {
return Some(ObjectKind::Blob);
}
if word == b"tree" {
return Some(ObjectKind::Tree);
}
if word == b"commit" {
return Some(ObjectKind::Commit);
}
if word == b"tag" {
return Some(ObjectKind::Tag);
}
return None;
}
fn is_space(byte: &u8) -> bool {
return *byte == b' ';
}
fn parse_found_header(header: &[u8]) -> Option<(ObjectId, ObjectKind, usize)> {
let fields: Vec<&[u8]> = header.split(is_space).collect();
if fields.len() != 3 {
return None;
}
let object: ObjectId = parse_object_id(fields[0])?;
let kind: ObjectKind = object_kind(fields[1])?;
let size: usize = parse_size(fields[2])?;
return Some((object, kind, size));
}
fn read_content(stream: &mut dyn BufRead, size: usize) -> Result<Vec<u8>, CandidateError> {
let mut bytes: Vec<u8> = Vec::new();
if let Err(error) = (&mut *stream).take(size as u64).read_to_end(&mut bytes) {
return Err(reply_failure(
CandidateFailure::ReaderEnded,
format!("could not be read ({error})").as_str(),
));
}
if bytes.len() != size {
return Err(reply_failure(
CandidateFailure::ReplyTruncated,
"ended before the declared object size",
));
}
let mut terminator: [u8; 1] = [0_u8; 1];
if stream.read_exact(&mut terminator).is_err() {
return Err(reply_failure(
CandidateFailure::ReplyTruncated,
"ended before the line feed that closes an object",
));
}
if terminator[0] != b'\n' {
return Err(reply_failure(
CandidateFailure::ReplyMalformed,
"sent content longer than the declared object size",
));
}
return Ok(bytes);
}
pub fn read_batch_reply(
stream: &mut dyn BufRead,
request: &[u8],
) -> Result<BatchReply, CandidateError> {
let header: Vec<u8> = read_header_line(stream)?;
if header.strip_suffix(b" missing") == Some(request) {
return Ok(BatchReply::Missing);
}
let Some((object, kind, size)) = parse_found_header(header.as_slice()) else {
return Err(reply_failure(
CandidateFailure::ReplyMalformed,
"sent a header that is neither an object header nor a missing notice",
));
};
let requested: Option<ObjectId> = parse_object_id(request);
if requested.is_some() && requested.as_ref() != Some(&object) {
return Err(reply_failure(
CandidateFailure::ReplyMismatched,
"answered with a different object than the one requested",
));
}
let bytes: Vec<u8> = read_content(stream, size)?;
return Ok(BatchReply::Found {
object,
kind,
bytes,
});
}
#[cfg(test)]
#[path = "candidate_batch_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "candidate_batch_resume_tests.rs"]
mod resume_tests;