Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/gunnar-bitmap/src/pack_order.rs

//! Turning a bitmap position into something you can look up.
//!
//! # The trap
//!
//! Every **bit inside** a bitmap in a `.bitmap` file is a **pack position**: the
//! object's rank when the pack's objects are ordered by their offset in the
//! `.pack` file. It is *not* the object's position in the `.idx`, which is
//! sorted by object id.
//!
//! Confusingly, the `commit_index_pos` in an entry header *is* an `.idx`
//! position. One file, two spaces; [`crate::file`] documents the measurement.
//!
//! The format documentation says "the position **in the index** for the packfile
//! or multi-pack index", which reads like `.idx` order and is how this crate
//! first implemented it. It is wrong for a packfile. git's own
//! `bitmap_position_packfile()` goes `find_pack_entry_one()` to an offset and
//! then `offset_to_pack_pos()`, which is pack order.
//!
//! Measured on a git-2.53 fixture pack of 4136 objects, 2026-08-04:
//!
//! * sorted by pack offset, objects 0..=198 are all 199 commits, exactly
//!   matching the commits type bitmap (`bit_size` 199, popcount 199);
//! * sorted by object id, position 0 is a *tree*.
//!
//! Reading positions as `.idx` positions therefore does not fail loudly. It
//! returns a set of the right *size* made of the wrong objects, which is the
//! worst possible failure for a fetch: a plausible pack full of things the
//! client did not ask for. The only reason it was caught here is that the tests
//! compare against `git rev-list --objects` rather than against this crate's own
//! encoder.
//!
//! # Why this lives in this crate
//!
//! The permutation is derived from the pack offsets the caller already has in
//! the `.idx`, so it needs no new dependency, and putting it anywhere else means
//! every consumer rediscovers the paragraph above the hard way.

/// The permutation between pack positions and pack index positions.
#[derive(Clone, Debug, Default)]
pub struct PackOrder {
    /// Indexed by pack position, holds the `.idx` position.
    to_index: Vec<u32>,
    /// Indexed by `.idx` position, holds the pack position.
    to_pack: Vec<u32>,
}

impl PackOrder {
    /// Build from the pack offsets of every object, in `.idx` order.
    ///
    /// `offsets[i]` is the offset in the `.pack` of the object at `.idx`
    /// position `i` — exactly what iterating a pack index gives you.
    pub fn from_index_offsets(offsets: &[u64]) -> Self {
        let mut order: Vec<u32> = (0..offsets.len() as u32).collect();
        // Offsets are unique within a pack, so the sort is total and this
        // permutation is the same one git computes for its reverse index.
        order.sort_unstable_by_key(|&i| offsets[i as usize]);
        let mut to_pack = vec![0u32; offsets.len()];
        for (pack_pos, &index_pos) in order.iter().enumerate() {
            to_pack[index_pos as usize] = pack_pos as u32;
        }
        PackOrder {
            to_index: order,
            to_pack,
        }
    }

    /// Number of objects covered.
    pub fn len(&self) -> usize {
        self.to_index.len()
    }

    /// Whether the pack is empty.
    pub fn is_empty(&self) -> bool {
        self.to_index.is_empty()
    }

    /// `.idx` position of the object at bitmap position `pack_pos`.
    pub fn index_position(&self, pack_pos: u32) -> Option<u32> {
        self.to_index.get(pack_pos as usize).copied()
    }

    /// Bitmap position of the object at `.idx` position `index_pos`.
    pub fn pack_position(&self, index_pos: u32) -> Option<u32> {
        self.to_pack.get(index_pos as usize).copied()
    }

    /// Translate a whole set of bitmap positions into `.idx` positions.
    ///
    /// Positions beyond the end of the pack are dropped; a bitmap can carry
    /// trailing bits in its last word that address nothing.
    pub fn index_positions<'a>(&'a self, set: &'a crate::Bitmap) -> impl Iterator<Item = u32> + 'a {
        set.iter_set_bits().filter_map(|p| self.index_position(p))
    }
}