weavatrix-git 0.3.1

Fast, bounded, evidence-carrying Git reader with an optional read-only MCP server
Documentation
use std::{
    fs,
    sync::{Arc, MutexGuard},
    time::SystemTime,
};

use crate::{GitError, ObjectId, Repository, Result, error::invalid};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IndexEntry {
    pub ctime_seconds: u32,
    pub mtime_seconds: u32,
    pub mode: u32,
    pub size: u32,
    pub id: ObjectId,
    pub stage: u8,
    pub assume_valid: bool,
    pub skip_worktree: bool,
    pub intent_to_add: bool,
    pub path: Vec<u8>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Index {
    version: u32,
    entries: Vec<IndexEntry>,
}

pub(crate) struct CachedIndex {
    len: u64,
    modified: Option<SystemTime>,
    index: Arc<Index>,
}

impl Index {
    #[must_use]
    pub const fn version(&self) -> u32 {
        self.version
    }

    #[must_use]
    pub fn entries(&self) -> &[IndexEntry] {
        &self.entries
    }

    fn parse(data: &[u8], repository: &Repository) -> Result<Self> {
        if data.get(..4) != Some(b"DIRC") {
            return Err(invalid("invalid index signature"));
        }
        let version = read_u32(data, 4)?;
        if !(2..=4).contains(&version) {
            return Err(GitError::Unsupported(format!("index version {version}")));
        }
        let count = usize::try_from(read_u32(data, 8)?)
            .map_err(|_| invalid("index entry count overflow"))?;
        if count > repository.limits().max_index_entries {
            return Err(GitError::LimitExceeded {
                resource: "index entries",
                limit: repository.limits().max_index_entries,
            });
        }
        let trailer = repository.hash_kind().bytes();
        if data.len() < 12 + trailer {
            return Err(invalid("truncated index"));
        }
        let content_end = data.len() - trailer;
        let mut cursor = 12;
        let mut entries = Vec::with_capacity(count);
        let mut previous_path = Vec::new();
        for _ in 0..count {
            let entry = parse_entry(
                data,
                &mut cursor,
                content_end,
                version,
                repository.hash_kind(),
                &previous_path,
            )?;
            previous_path.clone_from(&entry.path);
            entries.push(entry);
        }
        if !entries.windows(2).all(|pair| {
            (pair[0].path.as_slice(), pair[0].stage) < (pair[1].path.as_slice(), pair[1].stage)
        }) {
            return Err(invalid("index entries are not sorted"));
        }
        parse_extensions(data, cursor, content_end)?;
        Ok(Self { version, entries })
    }
}

impl Repository {
    pub fn index(&self) -> Result<Index> {
        Ok((*self.index_shared()?).clone())
    }

    pub fn index_shared(&self) -> Result<Arc<Index>> {
        let path = self.git_dir().join("index");
        let metadata = fs::metadata(&path)?;
        let modified = metadata.modified().ok();
        let mut cache = self
            .index_cache
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(index) = cached(&cache, metadata.len(), modified) {
            return Ok(index);
        }
        let index = Arc::new(Index::parse(&fs::read(path)?, self)?);
        *cache = Some(CachedIndex {
            len: metadata.len(),
            modified,
            index: Arc::clone(&index),
        });
        Ok(index)
    }
}

fn cached(
    cache: &MutexGuard<'_, Option<CachedIndex>>,
    len: u64,
    modified: Option<SystemTime>,
) -> Option<Arc<Index>> {
    cache
        .as_ref()
        .filter(|cached| cached.len == len && cached.modified == modified)
        .map(|cached| Arc::clone(&cached.index))
}

fn parse_entry(
    data: &[u8],
    cursor: &mut usize,
    end: usize,
    version: u32,
    hash: crate::HashKind,
    previous_path: &[u8],
) -> Result<IndexEntry> {
    let start = *cursor;
    let fixed = 40_usize
        .checked_add(hash.bytes())
        .and_then(|value| value.checked_add(2))
        .ok_or_else(|| invalid("index entry length overflow"))?;
    if start.saturating_add(fixed) > end {
        return Err(invalid("truncated index entry"));
    }
    let ctime_seconds = read_u32(data, start)?;
    let mtime_seconds = read_u32(data, start + 8)?;
    let mode = read_u32(data, start + 24)?;
    let size = read_u32(data, start + 36)?;
    let oid_start = start + 40;
    let id = ObjectId::from_bytes(
        data.get(oid_start..oid_start + hash.bytes())
            .ok_or_else(|| invalid("truncated index object id"))?,
    )?;
    *cursor = oid_start + hash.bytes();
    let flags = read_u16(data, *cursor)?;
    *cursor += 2;
    let extended = flags & 0x4000 != 0;
    if version == 2 && extended {
        return Err(invalid("index v2 entry has extended flags"));
    }
    let extended_flags = if extended {
        let value = read_u16(data, *cursor)?;
        *cursor += 2;
        if value & 0x1fff != 0 {
            return Err(invalid("index entry has reserved extended flags"));
        }
        value
    } else {
        0
    };
    let path = if version == 4 {
        parse_v4_path(data, cursor, end, previous_path)?
    } else {
        let path = take_path(data, cursor, end)?;
        let entry_len = cursor
            .checked_sub(start)
            .ok_or_else(|| invalid("index entry cursor underflow"))?;
        *cursor = start
            .checked_add(entry_len.div_ceil(8) * 8)
            .ok_or_else(|| invalid("index entry padding overflow"))?;
        path
    };
    validate_path(&path)?;
    Ok(IndexEntry {
        ctime_seconds,
        mtime_seconds,
        mode,
        size,
        id,
        stage: u8::try_from((flags >> 12) & 3).expect("two bits fit u8"),
        assume_valid: flags & 0x8000 != 0,
        skip_worktree: extended_flags & 0x4000 != 0,
        intent_to_add: extended_flags & 0x2000 != 0,
        path,
    })
}

fn parse_v4_path(data: &[u8], cursor: &mut usize, end: usize, previous: &[u8]) -> Result<Vec<u8>> {
    let remove = variable_width(data, cursor, end)?;
    if remove > previous.len() {
        return Err(invalid("index v4 path prefix is out of bounds"));
    }
    let suffix = take_path(data, cursor, end)?;
    let mut path = previous[..previous.len() - remove].to_vec();
    path.extend(suffix);
    Ok(path)
}

fn variable_width(data: &[u8], cursor: &mut usize, end: usize) -> Result<usize> {
    let mut byte = take(data, cursor, end)?;
    let mut value = usize::from(byte & 0x7f);
    while byte & 0x80 != 0 {
        byte = take(data, cursor, end)?;
        value = value
            .checked_add(1)
            .and_then(|value| value.checked_shl(7))
            .and_then(|value| value.checked_add(usize::from(byte & 0x7f)))
            .ok_or_else(|| invalid("index v4 path prefix overflow"))?;
    }
    Ok(value)
}

fn take_path(data: &[u8], cursor: &mut usize, end: usize) -> Result<Vec<u8>> {
    let nul = data
        .get(*cursor..end)
        .and_then(|bytes| bytes.iter().position(|byte| *byte == 0))
        .map(|offset| *cursor + offset)
        .ok_or_else(|| invalid("index path has no terminator"))?;
    let path = data[*cursor..nul].to_vec();
    *cursor = nul + 1;
    Ok(path)
}

fn validate_path(path: &[u8]) -> Result<()> {
    if path.is_empty() || path.first() == Some(&b'/') || path.last() == Some(&b'/') {
        return Err(invalid("invalid index path"));
    }
    if path
        .split(|byte| *byte == b'/')
        .any(|part| matches!(part, b"." | b".." | b".git"))
    {
        return Err(invalid("unsafe index path component"));
    }
    Ok(())
}

fn parse_extensions(data: &[u8], mut cursor: usize, end: usize) -> Result<()> {
    while cursor < end {
        if cursor.saturating_add(8) > end {
            return Err(invalid("truncated index extension"));
        }
        let signature = &data[cursor..cursor + 4];
        let length = usize::try_from(read_u32(data, cursor + 4)?)
            .map_err(|_| invalid("index extension length overflow"))?;
        if signature[0].is_ascii_lowercase() {
            return Err(GitError::Unsupported(format!(
                "mandatory index extension {}",
                String::from_utf8_lossy(signature)
            )));
        }
        cursor = cursor
            .checked_add(8 + length)
            .ok_or_else(|| invalid("index extension overflow"))?;
        if cursor > end {
            return Err(invalid("index extension is out of bounds"));
        }
    }
    Ok(())
}

fn take(data: &[u8], cursor: &mut usize, end: usize) -> Result<u8> {
    if *cursor >= end {
        return Err(invalid("truncated index data"));
    }
    let value = data[*cursor];
    *cursor += 1;
    Ok(value)
}

fn read_u16(input: &[u8], offset: usize) -> Result<u16> {
    let bytes = input
        .get(offset..offset + 2)
        .ok_or_else(|| invalid("truncated index integer"))?;
    Ok(u16::from_be_bytes(bytes.try_into().expect("two bytes")))
}

fn read_u32(input: &[u8], offset: usize) -> Result<u32> {
    let bytes = input
        .get(offset..offset + 4)
        .ok_or_else(|| invalid("truncated index integer"))?;
    Ok(u32::from_be_bytes(bytes.try_into().expect("four bytes")))
}