const ASSUME_VALID: u16 = 0x8000;
const EXTENDED: u16 = 0x4000;
const SKIP_WORKTREE: u16 = 0x4000;
const INTENT_TO_ADD: u16 = 0x2000;
const HEADER_LENGTH: usize = 12;
const STAT_LENGTH: usize = 40;
const OBJECT_NAME_LENGTHS: [usize; 2] = [20, 32];
const SPLIT_INDEX_EXTENSION: &[u8; 4] = b"link";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IndexFlags {
Plain,
Marked,
Unreadable,
}
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]]));
}
fn scan_entries(bytes: &[u8], name_length: usize) -> Option<bool> {
let end: usize = bytes.len().checked_sub(name_length)?;
if bytes.get(..4)? != b"DIRC" {
return None;
}
let version: u32 = be32(bytes, 4)?;
if !(2..=4).contains(&version) {
return None;
}
let count: u32 = be32(bytes, 8)?;
let mut marked: bool = false;
let mut at: usize = HEADER_LENGTH;
for _entry in 0..count {
let flags: u16 = be16(bytes, at.checked_add(STAT_LENGTH)?.checked_add(name_length)?)?;
let mut head: usize = STAT_LENGTH + name_length + 2;
if flags & ASSUME_VALID != 0 {
marked = true;
}
if flags & EXTENDED != 0 {
if version < 3 {
return None;
}
if be16(bytes, at.checked_add(head)?)? & (SKIP_WORKTREE | INTENT_TO_ADD) != 0 {
marked = true;
}
head += 2;
}
let name_start: usize = at.checked_add(head)?;
if version == 4 {
let mut cursor: usize = name_start;
while bytes.get(cursor)? & 0x80 != 0 {
cursor += 1;
}
cursor += 1;
let terminator: usize = bytes.get(cursor..end)?.iter().position(|byte| { return *byte == 0; })?;
at = cursor + terminator + 1;
} else {
let terminator: usize = bytes.get(name_start..end)?.iter().position(|byte| { return *byte == 0; })?;
at = at.checked_add((head + terminator + 8) & !7)?;
}
if at > end {
return None;
}
}
while at < end {
let signature: &[u8] = bytes.get(at..at.checked_add(4)?)?;
if !signature.iter().all(u8::is_ascii_alphanumeric) || signature == SPLIT_INDEX_EXTENSION {
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(marked);
}
pub fn read_index_flags(bytes: &[u8]) -> IndexFlags {
for name_length in OBJECT_NAME_LENGTHS {
match scan_entries(bytes, name_length) {
Some(true) => return IndexFlags::Marked,
Some(false) => return IndexFlags::Plain,
None => {}
}
}
return IndexFlags::Unreadable;
}
#[cfg(test)]
#[path = "index_flags_tests.rs"]
mod tests;