use crate::index_cache_tree::CacheTree;
use crate::index_fsmonitor::{Fsmonitor, read_fsmonitor};
use crate::index_resolve_undo::ResolveUndo;
use crate::index_stat::{StatData, be32};
use crate::index_untracked::Untracked;
use crate::index_untracked_io::read_untracked;
pub const ASSUME_VALID: u16 = 0x8000;
pub const STAGE_MASK: u16 = 0x3000;
pub const EXTENDED: u16 = 0x4000;
pub const INTENT_TO_ADD: u16 = 0x2000;
pub const SKIP_WORKTREE: u16 = 0x4000;
const HEADER_LENGTH: usize = 12;
const FIXED_LENGTH: usize = 40;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FullEntry {
pub stat: StatData,
pub mode: u32,
pub id: Vec<u8>,
pub flags: u16,
pub extended: u16,
pub path: Vec<u8>,
pub monitor_valid: bool,
}
impl FullEntry {
pub fn stage(&self) -> u8 {
return ((self.flags & STAGE_MASK) >> 12) as u8;
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FullIndex {
pub version: u32,
pub entries: Vec<FullEntry>,
pub cache_tree: Option<CacheTree>,
pub resolve_undo: Option<ResolveUndo>,
pub untracked: Option<Untracked>,
pub fsmonitor: Option<Fsmonitor>,
}
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 read_entry(bytes: &[u8], at: usize, end: usize, version: u32, hash_length: usize, previous: &[u8]) -> Option<(FullEntry, usize)> {
let stat: StatData = StatData {
ctime_sec: be32(bytes, at)?,
ctime_nsec: be32(bytes, at + 4)?,
mtime_sec: be32(bytes, at + 8)?,
mtime_nsec: be32(bytes, at + 12)?,
dev: be32(bytes, at + 16)?,
ino: be32(bytes, at + 20)?,
uid: be32(bytes, at + 28)?,
gid: be32(bytes, at + 32)?,
size: be32(bytes, at + 36)?,
};
let mode: u32 = be32(bytes, at + 24)?;
let id_at: usize = at + FIXED_LENGTH;
let id: Vec<u8> = bytes.get(id_at..id_at + hash_length)?.to_vec();
let flags_at: usize = id_at + hash_length;
let raw_flags: u16 = be16(bytes, flags_at)?;
let mut head: usize = FIXED_LENGTH + hash_length + 2;
let mut extended: u16 = 0;
if raw_flags & EXTENDED != 0 {
if version < 3 {
return None;
}
extended = be16(bytes, flags_at + 2)?;
head += 2;
}
let name_start: usize = at + head;
let (path, next): (Vec<u8>, usize) = if version == 4 {
let (strip, after) = crate::index_stat::decode_varint(bytes.get(name_start..end)?)?;
let keep: usize = previous.len().checked_sub(usize::try_from(strip).ok()?)?;
let cursor: usize = end - after.len();
let terminator: usize = after.iter().position(|byte| { return *byte == 0; })?;
let mut joined: Vec<u8> = previous[..keep].to_vec();
joined.extend_from_slice(&after[..terminator]);
(joined, cursor + terminator + 1)
} else {
let terminator: usize = bytes.get(name_start..end)?.iter().position(|byte| { return *byte == 0; })?;
(bytes[name_start..name_start + terminator].to_vec(), at.checked_add((head + terminator + 8) & !7)?)
};
let defined: bool = extended & !(INTENT_TO_ADD | SKIP_WORKTREE) == 0;
let canonical: bool = matches!(mode, 0o100_644 | 0o100_755 | 0o120_000 | 0o160_000);
if next > end || path.is_empty() || !defined || !canonical {
return None;
}
let entry: FullEntry = FullEntry { stat, mode, id, flags: raw_flags & (ASSUME_VALID | STAGE_MASK), extended, path, monitor_valid: false };
return Some((entry, next));
}
pub fn parse_full_index(bytes: &[u8], hash_length: usize) -> Option<FullIndex> {
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<FullEntry> = Vec::with_capacity(count.min(1 << 20));
let mut at: usize = HEADER_LENGTH;
for _ in 0..count {
let previous: &[u8] = entries.last().map_or(&[], |entry| { return entry.path.as_slice(); });
let (entry, next) = read_entry(bytes, at, end, version, hash_length, previous)?;
entries.push(entry);
at = next;
}
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;
}
let mut index: FullIndex = FullIndex { version, entries, cache_tree: None, resolve_undo: None, untracked: None, fsmonitor: None };
while at < end {
let signature: &[u8] = bytes.get(at..at + 4)?;
let size: usize = usize::try_from(be32(bytes, at + 4)?).ok()?;
let data: &[u8] = bytes.get(at + 8..(at + 8).checked_add(size)?)?;
if !signature.iter().all(u8::is_ascii_alphanumeric) || !signature[0].is_ascii_uppercase() {
return None;
}
match signature {
b"TREE" => index.cache_tree = Some(CacheTree::read(data, hash_length)?),
b"REUC" => index.resolve_undo = Some(ResolveUndo::read(data, hash_length)?),
b"UNTR" => index.untracked = Some(read_untracked(data, hash_length)?),
b"FSMN" => index.fsmonitor = Some(read_fsmonitor(data, index.entries.len())?),
b"EOIE" | b"IEOT" => {}
_ => {}
}
at += 8 + size;
}
if at != end {
return None;
}
return Some(index);
}
#[cfg(test)]
#[path = "index_full_tests.rs"]
mod tests;