weavatrix-git 0.2.0

Dependency-free, evidence-carrying Git repository reader
Documentation
mod delta;
mod index;

use index::CachedObject;
pub(crate) use index::PackIndex;

use crate::{GitError, Object, ObjectId, ObjectKind, Result, error::invalid, inflate};

impl PackIndex {
    pub(crate) fn read(
        &self,
        id: ObjectId,
        max_object_bytes: usize,
        max_delta_depth: usize,
        external: &dyn Fn(ObjectId, usize) -> Result<Object>,
        depth: usize,
    ) -> Result<Option<Object>> {
        let Some(offset) = self.offset(id) else {
            return Ok(None);
        };
        self.read_at(
            id,
            offset,
            max_object_bytes,
            max_delta_depth,
            external,
            depth,
        )
        .map(Some)
    }

    pub(crate) fn read_at(
        &self,
        id: ObjectId,
        offset: u64,
        max_object_bytes: usize,
        max_delta_depth: usize,
        external: &dyn Fn(ObjectId, usize) -> Result<Object>,
        depth: usize,
    ) -> Result<Object> {
        let (kind, data) =
            self.decode(offset, max_object_bytes, max_delta_depth, external, depth)?;
        Ok(Object { id, kind, data })
    }

    fn decode(
        &self,
        offset: u64,
        max_object_bytes: usize,
        max_delta_depth: usize,
        external: &dyn Fn(ObjectId, usize) -> Result<Object>,
        depth: usize,
    ) -> Result<(ObjectKind, Vec<u8>)> {
        if depth > max_delta_depth {
            return Err(GitError::LimitExceeded {
                resource: "pack delta depth",
                limit: max_delta_depth,
            });
        }
        if let Some(object) = self.cached(offset) {
            return Ok((object.kind, object.data));
        }
        let pack = self.pack_bytes()?;
        validate_pack(pack, self.object_count())?;
        let mut cursor = usize::try_from(offset).map_err(|_| invalid("pack offset overflow"))?;
        let (kind, declared) = entry_header(pack, &mut cursor)?;
        let base = match kind {
            6 => Some(Base::Offset(ofs_base(pack, &mut cursor, offset)?)),
            7 => {
                let hash_len = self
                    .id_at_offset(offset)
                    .ok_or_else(|| invalid("pack index has no object for offset"))?
                    .kind()
                    .bytes();
                let end = cursor
                    .checked_add(hash_len)
                    .ok_or_else(|| invalid("pack base id overflow"))?;
                let bytes = pack
                    .get(cursor..end)
                    .ok_or_else(|| invalid("truncated REF_DELTA base id"))?;
                cursor = end;
                Some(Base::Id(ObjectId::from_bytes(bytes)?))
            }
            _ => None,
        };
        let inflate_limit = max_object_bytes.max(declared);
        let (payload, _) = inflate::zlib(
            pack.get(cursor..)
                .ok_or_else(|| invalid("pack entry offset is out of bounds"))?,
            inflate_limit,
        )?;
        if payload.len() != declared {
            return Err(invalid("pack entry inflated size mismatch"));
        }
        if let Some(base) = base {
            let (base_kind, base_data) = match base {
                Base::Offset(base_offset) => self.decode(
                    base_offset,
                    max_object_bytes,
                    max_delta_depth,
                    external,
                    depth + 1,
                )?,
                Base::Id(base_id) => {
                    if let Some(base_offset) = self.offset(base_id) {
                        self.decode(
                            base_offset,
                            max_object_bytes,
                            max_delta_depth,
                            external,
                            depth + 1,
                        )?
                    } else {
                        let object = external(base_id, depth + 1)?;
                        (object.kind, object.data)
                    }
                }
            };
            let object = (
                base_kind,
                delta::apply(&base_data, &payload, max_object_bytes)?,
            );
            self.cache(
                offset,
                CachedObject {
                    kind: object.0,
                    data: object.1.clone(),
                },
            );
            return Ok(object);
        }
        let object_kind = match kind {
            1 => ObjectKind::Commit,
            2 => ObjectKind::Tree,
            3 => ObjectKind::Blob,
            4 => ObjectKind::Tag,
            _ => return Err(invalid("invalid non-delta pack object kind")),
        };
        self.cache(
            offset,
            CachedObject {
                kind: object_kind,
                data: payload.clone(),
            },
        );
        Ok((object_kind, payload))
    }
}

enum Base {
    Offset(u64),
    Id(ObjectId),
}

fn validate_pack(pack: &[u8], expected_count: usize) -> Result<()> {
    if pack.get(..4) != Some(b"PACK") {
        return Err(invalid("invalid pack signature"));
    }
    let version = read_u32(pack, 4)?;
    if version != 2 && version != 3 {
        return Err(invalid("unsupported pack version"));
    }
    let count =
        usize::try_from(read_u32(pack, 8)?).map_err(|_| invalid("pack object count overflow"))?;
    if count != expected_count {
        return Err(invalid("pack and index object counts differ"));
    }
    Ok(())
}

fn entry_header(input: &[u8], cursor: &mut usize) -> Result<(u8, usize)> {
    let first = take(input, cursor)?;
    let kind = (first >> 4) & 7;
    let mut size = usize::from(first & 0x0f);
    let mut shift = 4;
    let mut byte = first;
    while byte & 0x80 != 0 {
        byte = take(input, cursor)?;
        size = size
            .checked_add(usize::from(byte & 0x7f) << shift)
            .ok_or_else(|| invalid("pack entry size overflow"))?;
        shift += 7;
        if shift >= usize::BITS {
            return Err(invalid("pack entry size is too long"));
        }
    }
    Ok((kind, size))
}

fn ofs_base(input: &[u8], cursor: &mut usize, offset: u64) -> Result<u64> {
    let mut byte = take(input, cursor)?;
    let mut distance = u64::from(byte & 0x7f);
    while byte & 0x80 != 0 {
        byte = take(input, cursor)?;
        distance = distance
            .checked_add(1)
            .and_then(|value| value.checked_shl(7))
            .and_then(|value| value.checked_add(u64::from(byte & 0x7f)))
            .ok_or_else(|| invalid("OFS_DELTA distance overflow"))?;
    }
    offset
        .checked_sub(distance)
        .ok_or_else(|| invalid("OFS_DELTA base precedes pack"))
}

fn take(input: &[u8], cursor: &mut usize) -> Result<u8> {
    let value = *input
        .get(*cursor)
        .ok_or_else(|| invalid("truncated pack entry"))?;
    *cursor += 1;
    Ok(value)
}

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