use crate::transaction_git::{GitContext, GitOutput, GitRequest, run_git};
use std::collections::HashMap;
use std::path::Path;
pub fn parse_batch_header(header: &str) -> Result<(String, usize), String> {
let fields: Vec<&str> = header.split(' ').collect();
if fields.len() != 3 {
return Err(format!(
"Git blob batch returned malformed header: {header}"
));
}
let (oid, kind, size_text) = (fields[0], fields[1], fields[2]);
if kind != "blob" {
return Err(format!("Git blob batch returned {kind} for {oid}."));
}
let canonical: bool = !size_text.is_empty()
&& size_text.bytes().all(is_ascii_digit)
&& (size_text == "0" || !size_text.starts_with('0'));
let parsed: Option<i64> = if canonical {
size_text.parse::<i64>().ok()
} else {
None
};
if let Some(found) = parsed
&& found <= crate::json_record::MAX_SAFE_INTEGER
&& let Ok(size) = usize::try_from(found)
{
return Ok((String::from(oid), size));
}
return Err(format!(
"Git blob batch returned invalid size for {oid}: {size_text}"
));
}
fn is_ascii_digit(byte: u8) -> bool {
return byte.is_ascii_digit();
}
pub fn parse_batch_output(
output: &[u8],
requested: &[String],
) -> Result<HashMap<String, Vec<u8>>, String> {
let mut blobs: HashMap<String, Vec<u8>> = HashMap::new();
let mut offset: usize = 0;
for oid in requested {
let Some(relative) = output[offset..].iter().position(is_newline) else {
return Err(format!("Git blob batch omitted header for {oid}."));
};
let header_end: usize = offset + relative;
let Ok(header) = std::str::from_utf8(&output[offset..header_end]) else {
return Err(format!(
"Git blob batch returned a header that is not UTF-8 for {oid}."
));
};
let (found, size) = parse_batch_header(header)?;
if &found != oid {
return Err(format!(
"Git blob batch returned {found} while {oid} was requested."
));
}
let start: usize = header_end + 1;
let end: usize = match start.checked_add(size) {
Some(value) if value < output.len() && output[value] == b'\n' => value,
_ => {
return Err(format!(
"Git blob batch returned truncated content for {oid}."
));
}
};
blobs.insert(oid.clone(), output[start..end].to_vec());
offset = end + 1;
}
if offset != output.len() {
return Err(String::from(
"Git blob batch returned unexpected trailing bytes.",
));
}
return Ok(blobs);
}
fn is_newline(byte: &u8) -> bool {
return *byte == b'\n';
}
pub fn load_blob_batch(
context: &GitContext,
cwd: &Path,
oids: &[String],
object_directory: Option<&Path>,
) -> Result<HashMap<String, Vec<u8>>, String> {
let mut requested: Vec<String> = Vec::new();
for oid in oids {
if !requested.contains(oid) {
requested.push(oid.clone());
}
}
if requested.is_empty() {
return Ok(HashMap::new());
}
let mut input: String = requested.join("\n");
input.push('\n');
let mut request: GitRequest = GitRequest::new(cwd, &["cat-file", "--batch"]);
request.input = Some(input.into_bytes());
request.object_directory = object_directory.map(Path::to_path_buf);
let output: GitOutput = match run_git(context, &request) {
Ok(finished) => finished,
Err(error) => return Err(format!("git cat-file --batch could not start: {error}")),
};
if !output.succeeded() {
return Err(format!(
"git cat-file --batch failed: {}",
output.error_text()
));
}
return parse_batch_output(output.stdout.as_slice(), requested.as_slice());
}
#[cfg(test)]
#[path = "blob_batch_tests.rs"]
mod tests;