use crate::candidate_error::{CandidateError, CandidateFailure};
use crate::candidate_object::{CandidateMode, ObjectId, mode_from_git, parse_object_id};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TreeRecord {
pub mode: CandidateMode,
pub object: ObjectId,
pub path: Vec<u8>,
}
fn tree_failure(failure: CandidateFailure, position: usize, detail: &str) -> CandidateError {
return CandidateError::new(
failure,
format!(
"cli-git could not read the HEAD commit's files: tree record {position} {detail}. \
Inspect the entries with `git ls-tree -r HEAD`."
)
.as_str(),
);
}
fn is_nul(byte: &u8) -> bool {
return *byte == 0;
}
fn is_space(byte: &u8) -> bool {
return *byte == b' ';
}
fn is_tab(byte: &u8) -> bool {
return *byte == b'\t';
}
fn expected_type(mode: CandidateMode) -> &'static [u8] {
match mode {
CandidateMode::Gitlink => return b"commit",
CandidateMode::Regular | CandidateMode::Executable | CandidateMode::Symlink => {
return b"blob";
}
}
}
fn parse_tree_record(position: usize, record: &[u8]) -> Result<TreeRecord, CandidateError> {
let Some(tab) = record.iter().position(is_tab) else {
return Err(tree_failure(
CandidateFailure::ListingMalformed,
position,
"has no tab before its pathname",
));
};
let path: &[u8] = &record[tab + 1..];
let fields: Vec<&[u8]> = record[..tab].split(is_space).collect();
if fields.len() != 3 || path.is_empty() {
return Err(tree_failure(
CandidateFailure::ListingMalformed,
position,
"does not have three metadata fields and a pathname",
));
}
let Some(mode) = mode_from_git(fields[0]) else {
return Err(tree_failure(
CandidateFailure::UnsupportedMode,
position,
"has a file mode other than 100644, 100755, 120000 or 160000",
));
};
if fields[1] != expected_type(mode) {
return Err(tree_failure(
CandidateFailure::ListingMalformed,
position,
"has an object type that does not match its mode",
));
}
let Some(object) = parse_object_id(fields[2]) else {
return Err(tree_failure(
CandidateFailure::ListingMalformed,
position,
"does not carry a complete object name",
));
};
return Ok(TreeRecord {
mode,
object,
path: path.to_vec(),
});
}
pub fn parse_tree_records(output: &[u8]) -> Result<Vec<TreeRecord>, CandidateError> {
let mut records: Vec<TreeRecord> = Vec::new();
if output.is_empty() {
return Ok(records);
}
let Some(body) = output.strip_suffix(&[0_u8]) else {
return Err(tree_failure(
CandidateFailure::ListingMalformed,
0,
"is cut short: the listing does not end with a NUL byte",
));
};
for (position, record) in body.split(is_nul).enumerate() {
records.push(parse_tree_record(position, record)?);
}
return Ok(records);
}
#[cfg(test)]
#[path = "candidate_tree_tests.rs"]
mod tests;