use crate::{ObjectFormat, ObjectId, ObjectReadError as Error};
#[derive(Debug, Clone)]
pub(crate) struct Entry {
pub id: ObjectId,
pub offset: usize,
pub end: usize,
pub crc: Option<u32>,
}
#[derive(Debug)]
pub(crate) struct Index {
format: ObjectFormat,
legacy: bool,
pub ranges: Vec<(usize, usize)>,
pub offsets: Vec<usize>,
pub pack_hash: ObjectId,
}
impl Index {
pub fn parse(format: ObjectFormat, bytes: &[u8], pack_end: usize) -> Result<Self, Error> {
if bytes.len() < 8 {
return Err(Error::Corrupt("truncated index header"));
}
if &bytes[..4] != b"\xfftOc" {
return Self::parse_v1(format, bytes, pack_end);
}
let version = word(bytes, 4)?;
if version != 2 {
return Err(Error::IndexVersion(version));
}
let width = format.digest_len();
let count = word(bytes, 1028)? as usize;
let table_end = count
.checked_mul(width + 8)
.and_then(|n| n.checked_add(1032))
.ok_or(Error::Corrupt("index length overflow"))?;
let trailer = bytes
.len()
.checked_sub(2 * width)
.ok_or(Error::Corrupt("truncated index"))?;
if table_end > trailer || (trailer - table_end) % 8 != 0 {
return Err(Error::Corrupt("index table length"));
}
let names = &bytes[1032..1032 + count * width];
check_names(names, width)?;
let large = &bytes[table_end..trailer];
let mut used_large = vec![false; large.len() / 8];
if used_large.len() > count {
return Err(Error::Corrupt("excess large offsets"));
}
let table = &bytes[1032 + count * (width + 4)..table_end];
let mut ranges = Vec::with_capacity(count);
for raw in table.as_chunks::<4>().0 {
let raw = u32::from_be_bytes(*raw);
let offset = if raw & 0x8000_0000 == 0 {
u64::from(raw)
} else {
let slot = (raw & 0x7fff_ffff) as usize;
let used = used_large
.get_mut(slot)
.ok_or(Error::Corrupt("large offset index"))?;
if *used {
return Err(Error::Corrupt("duplicate large offset index"));
}
*used = true;
u64::from_be_bytes(large[slot * 8..slot * 8 + 8].try_into().unwrap())
};
ranges.push((checked_offset(offset, pack_end)?, 0));
}
if used_large.contains(&false) {
return Err(Error::Corrupt("unused large offset"));
}
Self::finish(format, bytes, (8, names), ranges, pack_end)
}
fn parse_v1(format: ObjectFormat, bytes: &[u8], pack_end: usize) -> Result<Self, Error> {
let width = format.digest_len();
let count = word(bytes, 1020)? as usize;
let trailer = count
.checked_mul(width + 4)
.and_then(|n| n.checked_add(1024))
.ok_or(Error::Corrupt("index length overflow"))?;
if trailer.checked_add(2 * width) != Some(bytes.len()) {
return Err(Error::Corrupt("index table length"));
}
let table = &bytes[1024..trailer];
let mut previous: Option<&[u8]> = None;
let mut ranges = Vec::with_capacity(count);
for record in table.chunks_exact(width + 4) {
let (offset, name) = record.split_at(4);
if previous.is_some_and(|previous| previous >= name) {
return Err(Error::Corrupt("unsorted or duplicate index identities"));
}
previous = Some(name);
let offset = u32::from_be_bytes(offset.try_into().unwrap());
ranges.push((checked_offset(u64::from(offset), pack_end)?, 0));
}
Self::finish(format, bytes, (0, table), ranges, pack_end)
}
fn finish(
format: ObjectFormat,
bytes: &[u8],
(fanout_start, names): (usize, &[u8]),
mut ranges: Vec<(usize, usize)>,
pack_end: usize,
) -> Result<Self, Error> {
let width = format.digest_len();
let legacy = fanout_start == 0;
let first_byte = usize::from(legacy) * 4;
let mut fanout = [0u32; 256];
if !ranges.is_empty() {
for record in names.chunks_exact(names.len() / ranges.len()) {
fanout[usize::from(record[first_byte])] += 1;
}
}
let mut cumulative = 0;
for (bucket, size) in fanout.into_iter().enumerate() {
cumulative += size;
if word(bytes, fanout_start + bucket * 4)? != cumulative {
return Err(Error::Corrupt("index fanout"));
}
}
let offsets = offset_order(&ranges);
let mut next = pack_end;
for &position in offsets.iter().rev() {
let range = &mut ranges[position];
if range.0 == next {
return Err(Error::Corrupt("duplicate pack offset"));
}
range.1 = next;
next = range.0;
}
if next != 12 {
return Err(Error::Corrupt("unindexed pack bytes"));
}
let trailer = bytes.len() - 2 * width;
Ok(Self {
format,
legacy,
ranges,
offsets,
pack_hash: ObjectId::from_bytes(format, &bytes[trailer..trailer + width]).unwrap(),
})
}
pub fn len(&self) -> usize {
self.ranges.len()
}
pub fn identity(&self, bytes: &[u8], position: usize) -> ObjectId {
let width = self.format.digest_len();
let start = if self.legacy {
1028 + position * (width + 4)
} else {
1032 + position * width
};
ObjectId::from_bytes(self.format, &bytes[start..start + width]).unwrap()
}
pub fn entry(&self, bytes: &[u8], position: usize) -> Entry {
let (offset, end) = self.ranges[position];
let crc = (!self.legacy).then(|| {
let start = 1032 + self.len() * self.format.digest_len() + position * 4;
u32::from_be_bytes(bytes[start..start + 4].try_into().unwrap())
});
Entry {
id: self.identity(bytes, position),
offset,
end,
crc,
}
}
pub fn find(&self, bytes: &[u8], id: ObjectId) -> Option<usize> {
let (mut low, mut high) = (0, self.len());
while low < high {
let middle = low + (high - low) / 2;
match self.identity(bytes, middle).cmp(&id) {
std::cmp::Ordering::Less => low = middle + 1,
std::cmp::Ordering::Greater => high = middle,
std::cmp::Ordering::Equal => return Some(middle),
}
}
None
}
pub fn at_offset(&self, offset: usize) -> Result<usize, Error> {
let position = self
.offsets
.binary_search_by_key(&offset, |&i| self.ranges[i].0)
.map_err(|_| Error::Corrupt("base offset is not an indexed entry"))?;
Ok(self.offsets[position])
}
}
fn check_names(names: &[u8], width: usize) -> Result<(), Error> {
let mut names = names.chunks_exact(width);
let Some(mut previous) = names.next() else {
return Ok(());
};
for name in names {
if previous >= name {
return Err(Error::Corrupt("unsorted or duplicate index identities"));
}
previous = name;
}
Ok(())
}
fn checked_offset(offset: u64, pack_end: usize) -> Result<usize, Error> {
let offset = usize::try_from(offset).map_err(|_| Error::Corrupt("offset overflow"))?;
if offset < 12 || offset >= pack_end {
return Err(Error::Corrupt("offset outside pack entries"));
}
Ok(offset)
}
fn offset_order(ranges: &[(usize, usize)]) -> Vec<usize> {
let shift = usize::BITS - ranges.len().leading_zeros();
let largest = ranges.iter().map(|range| range.0).max().unwrap_or(0) as u64;
let offset_bits = u64::BITS - largest.leading_zeros();
if offset_bits + shift > u64::BITS {
let mut order: Vec<_> = (0..ranges.len()).collect();
order.sort_unstable_by_key(|&position| ranges[position].0);
return order;
}
let mut keys: Vec<u64> = ranges
.iter()
.enumerate()
.map(|(position, range)| (range.0 as u64) << shift | position as u64)
.collect();
let passes = offset_bits.div_ceil(16).max(1);
let digit_bits = offset_bits.div_ceil(passes);
let mask = (1u64 << digit_bits) - 1;
let mut scratch = vec![0; keys.len()];
let mut counts = vec![0usize; 1 << digit_bits];
for pass in 0..passes {
let digit_shift = shift + pass * digit_bits;
let digit = |key: u64| ((key >> digit_shift) & mask) as usize;
counts.fill(0);
for &key in &keys {
counts[digit(key)] += 1;
}
let mut start = 0;
for count in &mut counts {
(*count, start) = (start, start + *count);
}
for &key in &keys {
let slot = &mut counts[digit(key)];
scratch[*slot] = key;
*slot += 1;
}
std::mem::swap(&mut keys, &mut scratch);
}
let positions = (1u64 << shift) - 1;
keys.into_iter()
.map(|key| (key & positions) as usize)
.collect()
}
pub(crate) fn word(bytes: &[u8], position: usize) -> Result<u32, Error> {
let word = bytes
.get(
position
..position
.checked_add(4)
.ok_or(Error::Corrupt("integer offset overflow"))?,
)
.ok_or(Error::Corrupt("truncated integer"))?;
Ok(u32::from_be_bytes(word.try_into().unwrap()))
}
pub(crate) fn verify_hash(
format: ObjectFormat,
bytes: &[u8],
reason: &'static str,
) -> Result<(), Error> {
let end = bytes
.len()
.checked_sub(format.digest_len())
.ok_or(Error::Corrupt(reason))?;
if *format.checksum(&bytes[..end]).as_bytes() != bytes[end..] {
return Err(Error::Corrupt(reason));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::offset_order;
#[test]
fn offset_order_sorts_by_offset_at_every_key_width() {
for offsets in [
vec![],
vec![12],
vec![900, 12, 70_000, 4_000_000_000, 13],
vec![1 << 40, 12, 1 << 33, 99],
] {
let ranges: Vec<_> = offsets.iter().map(|&offset| (offset, 0)).collect();
let mut expected: Vec<_> = (0..ranges.len()).collect();
expected.sort_by_key(|&position| offsets[position]);
assert_eq!(offset_order(&ranges), expected);
}
}
#[test]
fn offset_order_falls_back_when_keys_exceed_64_bits() {
let ranges = [(usize::MAX - 1, 0), (12, 0), (usize::MAX - 2, 0)];
assert_eq!(offset_order(&ranges), [1, 2, 0]);
}
}