git_policy_cli/
blob_batch.rs1use super::transaction_git::{GitContext, GitOutput, GitRequest, run_git};
18use std::collections::HashMap;
20use std::path::Path;
22
23pub fn parse_batch_header(header: &str) -> Result<(String, usize), String> {
38 let fields: Vec<&str> = header.split(' ').collect();
39 if fields.len() != 3 {
40 return Err(format!(
41 "Git blob batch returned malformed header: {header}"
42 ));
43 }
44 let (oid, kind, size_text) = (fields[0], fields[1], fields[2]);
45 if kind != "blob" {
46 return Err(format!("Git blob batch returned {kind} for {oid}."));
47 }
48 let canonical: bool = !size_text.is_empty()
49 && size_text.bytes().all(is_ascii_digit)
50 && (size_text == "0" || !size_text.starts_with('0'));
51 let parsed: Option<i64> = if canonical {
52 size_text.parse::<i64>().ok()
53 } else {
54 None
55 };
56 if let Some(found) = parsed
57 && found <= super::json_record::MAX_SAFE_INTEGER
58 && let Ok(size) = usize::try_from(found)
59 {
60 return Ok((String::from(oid), size));
61 }
62 return Err(format!(
63 "Git blob batch returned invalid size for {oid}: {size_text}"
64 ));
65}
66
67fn is_ascii_digit(byte: u8) -> bool {
77 return byte.is_ascii_digit();
78}
79
80pub fn parse_batch_output(
96 output: &[u8],
97 requested: &[String],
98) -> Result<HashMap<String, Vec<u8>>, String> {
99 let mut blobs: HashMap<String, Vec<u8>> = HashMap::new();
101 let mut offset: usize = 0;
102 for oid in requested {
103 let Some(relative) = output[offset..].iter().position(is_newline) else {
104 return Err(format!("Git blob batch omitted header for {oid}."));
105 };
106 let header_end: usize = offset + relative;
107 let Ok(header) = std::str::from_utf8(&output[offset..header_end]) else {
108 return Err(format!(
109 "Git blob batch returned a header that is not UTF-8 for {oid}."
110 ));
111 };
112 let (found, size) = parse_batch_header(header)?;
113 if &found != oid {
114 return Err(format!(
115 "Git blob batch returned {found} while {oid} was requested."
116 ));
117 }
118 let start: usize = header_end + 1;
119 let end: usize = match start.checked_add(size) {
120 Some(value) if value < output.len() && output[value] == b'\n' => value,
121 _ => {
122 return Err(format!(
123 "Git blob batch returned truncated content for {oid}."
124 ));
125 }
126 };
127 blobs.insert(oid.clone(), output[start..end].to_vec());
128 offset = end + 1;
129 }
130 if offset != output.len() {
131 return Err(String::from(
132 "Git blob batch returned unexpected trailing bytes.",
133 ));
134 }
135 return Ok(blobs);
136}
137
138fn is_newline(byte: &u8) -> bool {
148 return *byte == b'\n';
149}
150
151pub fn load_blob_batch(
164 context: &GitContext,
165 cwd: &Path,
166 oids: &[String],
167 object_directory: Option<&Path>,
168) -> Result<HashMap<String, Vec<u8>>, String> {
169 let mut requested: Vec<String> = Vec::new();
171 for oid in oids {
172 if !requested.contains(oid) {
173 requested.push(oid.clone());
174 }
175 }
176 if requested.is_empty() {
177 return Ok(HashMap::new());
178 }
179 let mut input: String = requested.join("\n");
180 input.push('\n');
181 let mut request: GitRequest = GitRequest::new(cwd, &["cat-file", "--batch"]);
182 request.input = Some(input.into_bytes());
183 request.object_directory = object_directory.map(Path::to_path_buf);
184 let output: GitOutput = match run_git(context, &request) {
185 Ok(finished) => finished,
186 Err(error) => return Err(format!("git cat-file --batch could not start: {error}")),
187 };
188 if !output.succeeded() {
189 return Err(format!(
190 "git cat-file --batch failed: {}",
191 output.error_text()
192 ));
193 }
194 return parse_batch_output(output.stdout.as_slice(), requested.as_slice());
195}
196
197#[cfg(test)]
199#[path = "blob_batch_tests.rs"]
200mod tests;