use super::repo_read_layout::ObjectFormat;
use super::repo_read_object::name_length;
const HEADER_LENGTH: usize = 12;
const STAT_LENGTH: usize = 40;
pub const INTENT_TO_ADD: u16 = 0x2000;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IndexEntry {
pub path: Vec<u8>,
pub mode: u32,
pub id: Vec<u8>,
pub stage: u8,
pub extended: u16,
}
fn be16(bytes: &[u8], at: usize) -> Option<u16> {
let pair: &[u8] = bytes.get(at..at.checked_add(2)?)?;
return Some(u16::from_be_bytes([pair[0], pair[1]]));
}
fn be32(bytes: &[u8], at: usize) -> Option<u32> {
let quad: &[u8] = bytes.get(at..at.checked_add(4)?)?;
return Some(u32::from_be_bytes([quad[0], quad[1], quad[2], quad[3]]));
}
pub fn parse_index(bytes: &[u8], format: ObjectFormat) -> Option<Vec<IndexEntry>> {
let hash_length: usize = name_length(format);
let end: usize = bytes.len().checked_sub(hash_length)?;
if bytes.get(..4)? != b"DIRC" {
return None;
}
let version: u32 = be32(bytes, 4)?;
if !(2..=4).contains(&version) {
return None;
}
let count: usize = usize::try_from(be32(bytes, 8)?).ok()?;
let mut entries: Vec<IndexEntry> = Vec::with_capacity(count.min(1 << 20));
let mut at: usize = HEADER_LENGTH;
let mut previous: Vec<u8> = Vec::new();
for _ in 0..count {
let mode: u32 = be32(bytes, at.checked_add(24)?)?;
let id_at: usize = at.checked_add(STAT_LENGTH)?;
let id: Vec<u8> = bytes.get(id_at..id_at.checked_add(hash_length)?)?.to_vec();
let flags_at: usize = id_at + hash_length;
let flags: u16 = be16(bytes, flags_at)?;
let mut head: usize = STAT_LENGTH + hash_length + 2;
let mut extended: u16 = 0;
if flags & 0x4000 != 0 {
if version < 3 {
return None;
}
extended = be16(bytes, flags_at + 2)?;
head += 2;
}
let name_start: usize = at.checked_add(head)?;
let path: Vec<u8>;
if version == 4 {
let mut cursor: usize = name_start;
let mut byte: u8 = *bytes.get(cursor)?;
cursor += 1;
let mut strip: usize = usize::from(byte & 0x7f);
while byte & 0x80 != 0 {
byte = *bytes.get(cursor)?;
cursor += 1;
strip = strip.checked_add(1)?.checked_shl(7)? | usize::from(byte & 0x7f);
}
let keep: usize = previous.len().checked_sub(strip)?;
let terminator: usize = bytes.get(cursor..end)?.iter().position(|stored| { return *stored == 0; })?;
let mut joined: Vec<u8> = previous[..keep].to_vec();
joined.extend_from_slice(&bytes[cursor..cursor + terminator]);
path = joined;
at = cursor + terminator + 1;
} else {
let terminator: usize = bytes.get(name_start..end)?.iter().position(|byte| { return *byte == 0; })?;
path = bytes[name_start..name_start + terminator].to_vec();
at = at.checked_add((head + terminator + 8) & !7)?;
}
let defined: bool = extended & !(INTENT_TO_ADD | 0x4000) == 0;
let canonical: bool = matches!(mode, 0o100_644 | 0o100_755 | 0o120_000 | 0o160_000);
if at > end || path.is_empty() || !defined || !canonical {
return None;
}
previous.clone_from(&path);
let stage: u8 = ((flags >> 12) & 3) as u8;
entries.push(IndexEntry { path, mode, id, stage, extended });
}
if entries.windows(2).any(|pair| { return (pair[0].path.as_slice(), pair[0].stage) >= (pair[1].path.as_slice(), pair[1].stage); }) {
return None;
}
while at < end {
let signature: &[u8] = bytes.get(at..at.checked_add(4)?)?;
let optional: bool = signature[0].is_ascii_uppercase();
if !signature.iter().all(u8::is_ascii_alphanumeric) || !optional || signature == b"link" || signature == b"sdir" {
return None;
}
let size: usize = usize::try_from(be32(bytes, at + 4)?).ok()?;
at = at.checked_add(8)?.checked_add(size)?;
}
if at != end {
return None;
}
return Some(entries);
}
#[cfg(test)]
#[path = "repo_read_index_tests.rs"]
mod tests;