#![cfg(unix)]
use super::repo_read_object::{ObjectKind, OBJECT_SIZE_LIMIT, RawObject, inflate_exact};
use std::fs::File;
use std::os::unix::fs::FileExt;
use std::path::Path;
const DELTA_DEPTH_LIMIT: usize = 100;
const CACHE_BYTE_LIMIT: usize = 32 * 1024 * 1024;
const FANOUT_END: u64 = 8 + 256 * 4;
#[derive(Debug)]
pub struct Pack {
index: File,
data: File,
hash_length: usize,
count: u64,
data_length: u64,
fanout: Vec<u32>,
decoded: std::cell::Cell<u64>,
cache: std::cell::RefCell<std::collections::HashMap<u64, RawObject>>,
cached_bytes: std::cell::Cell<usize>,
}
fn read_at(file: &File, position: u64, length: usize) -> Option<Vec<u8>> {
let mut buffer: Vec<u8> = vec![0; length];
file.read_exact_at(buffer.as_mut_slice(), position).ok()?;
return Some(buffer);
}
fn be32(file: &File, position: u64) -> Option<u32> {
let bytes: Vec<u8> = read_at(file, position, 4)?;
return Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]));
}
fn delta_size(delta: &[u8], cursor: &mut usize) -> Option<usize> {
let mut size: usize = 0;
let mut shift: u32 = 0;
loop {
let byte: u8 = *delta.get(*cursor)?;
*cursor += 1;
size |= usize::from(byte & 0x7f).checked_shl(shift)?;
if byte & 0x80 == 0 {
return Some(size);
}
shift += 7;
if shift > 56 {
return None;
}
}
}
pub fn apply_delta(base: &[u8], delta: &[u8]) -> Option<Vec<u8>> {
let mut cursor: usize = 0;
if delta_size(delta, &mut cursor)? != base.len() {
return None;
}
let result_size: usize = delta_size(delta, &mut cursor)?;
if result_size > OBJECT_SIZE_LIMIT {
return None;
}
let mut result: Vec<u8> = Vec::with_capacity(result_size);
while cursor < delta.len() {
let op: u8 = delta[cursor];
cursor += 1;
if op & 0x80 != 0 {
let mut offset: usize = 0;
let mut length: usize = 0;
for bit in 0..4 {
if op & (1 << bit) != 0 {
offset |= usize::from(*delta.get(cursor)?) << (8 * bit);
cursor += 1;
}
}
for bit in 0..3 {
if op & (0x10 << bit) != 0 {
length |= usize::from(*delta.get(cursor)?) << (8 * bit);
cursor += 1;
}
}
if length == 0 {
length = 0x10000;
}
result.extend_from_slice(base.get(offset..offset.checked_add(length)?)?);
} else if op != 0 {
let end: usize = cursor.checked_add(usize::from(op))?;
result.extend_from_slice(delta.get(cursor..end)?);
cursor = end;
} else {
return None;
}
if result.len() > result_size {
return None;
}
}
if result.len() != result_size {
return None;
}
return Some(result);
}
impl Pack {
pub fn open(index_path: &Path, hash_length: usize) -> Option<Pack> {
let index: File = File::open(index_path).ok()?;
let data: File = File::open(index_path.with_extension("pack")).ok()?;
let header: Vec<u8> = read_at(&index, 0, 8)?;
if header != b"\xfftOc\0\0\0\x02" {
return None;
}
let mut fanout: Vec<u32> = Vec::with_capacity(256);
for slot in 0..256_u64 {
fanout.push(be32(&index, 8 + slot * 4)?);
}
if fanout.windows(2).any(|pair| { return pair[0] > pair[1]; }) {
return None;
}
let count: u64 = u64::from(fanout[255]);
let index_length: u64 = index.metadata().ok()?.len();
let minimum: u64 = FANOUT_END + count * (hash_length as u64 + 8) + 2 * hash_length as u64;
if index_length < minimum {
return None;
}
let data_length: u64 = data.metadata().ok()?.len();
let pack_header: Vec<u8> = read_at(&data, 0, 12)?;
let pack_count: u32 = u32::from_be_bytes([pack_header[8], pack_header[9], pack_header[10], pack_header[11]]);
if &pack_header[..4] != b"PACK" || !matches!(pack_header[7], 2 | 3) || pack_header[4..7] != [0, 0, 0] || u64::from(pack_count) != count {
return None;
}
return Some(Pack { index, data, hash_length, count, data_length, fanout, decoded: std::cell::Cell::new(0), cache: std::cell::RefCell::default(), cached_bytes: std::cell::Cell::new(0) });
}
pub fn decoded_entries(&self) -> u64 {
return self.decoded.get();
}
pub fn find(&self, name: &[u8]) -> Option<u64> {
if name.len() != self.hash_length {
return None;
}
let first: usize = usize::from(name[0]);
let mut low: u64 = if first == 0 { 0 } else { u64::from(self.fanout[first - 1]) };
let mut high: u64 = u64::from(self.fanout[first]);
let names_at: u64 = FANOUT_END;
while low < high {
let middle: u64 = low + (high - low) / 2;
let candidate: Vec<u8> = read_at(&self.index, names_at + middle * self.hash_length as u64, self.hash_length)?;
match candidate.as_slice().cmp(name) {
std::cmp::Ordering::Equal => return self.offset_of(middle),
std::cmp::Ordering::Less => low = middle + 1,
std::cmp::Ordering::Greater => high = middle,
}
}
return None;
}
fn offset_of(&self, position: u64) -> Option<u64> {
let offsets_at: u64 = FANOUT_END + self.count * (self.hash_length as u64 + 4);
let word: u32 = be32(&self.index, offsets_at + position * 4)?;
if word & 0x8000_0000 == 0 {
return Some(u64::from(word));
}
let large_at: u64 = offsets_at + self.count * 4 + u64::from(word & 0x7fff_ffff) * 8;
let bytes: Vec<u8> = read_at(&self.index, large_at, 8)?;
let mut array: [u8; 8] = [0; 8];
array.copy_from_slice(bytes.as_slice());
return Some(u64::from_be_bytes(array));
}
pub fn read(&self, offset: u64, by_name: &dyn Fn(&[u8]) -> Option<RawObject>, depth: usize) -> Option<RawObject> {
if let Some(kept) = self.cache.borrow().get(&offset) {
return Some(kept.clone());
}
let object: RawObject = self.decode(offset, by_name, depth)?;
if self.cached_bytes.get() + object.bytes.len() <= CACHE_BYTE_LIMIT {
self.cached_bytes.set(self.cached_bytes.get() + object.bytes.len());
self.cache.borrow_mut().insert(offset, object.clone());
}
return Some(object);
}
fn decode(&self, offset: u64, by_name: &dyn Fn(&[u8]) -> Option<RawObject>, depth: usize) -> Option<RawObject> {
if depth > DELTA_DEPTH_LIMIT || offset < 12 || offset >= self.data_length {
return None;
}
self.decoded.set(self.decoded.get() + 1);
let head: Vec<u8> = read_at(&self.data, offset, (32 + self.hash_length).min((self.data_length - offset) as usize))?;
let mut cursor: usize = 0;
let first: u8 = head[0];
cursor += 1;
let code: u8 = (first >> 4) & 7;
let mut size: usize = usize::from(first & 15);
let mut shift: u32 = 4;
let mut byte: u8 = first;
while byte & 0x80 != 0 {
byte = *head.get(cursor)?;
cursor += 1;
size |= usize::from(byte & 0x7f).checked_shl(shift)?;
shift += 7;
if shift > 60 {
return None;
}
}
match code {
1 => return self.base_object(ObjectKind::Commit, offset + cursor as u64, size),
2 => return self.base_object(ObjectKind::Tree, offset + cursor as u64, size),
3 => return self.base_object(ObjectKind::Blob, offset + cursor as u64, size),
4 => return self.base_object(ObjectKind::Tag, offset + cursor as u64, size),
6 => {
let mut c: u8 = *head.get(cursor)?;
cursor += 1;
let mut distance: u64 = u64::from(c & 0x7f);
while c & 0x80 != 0 {
c = *head.get(cursor)?;
cursor += 1;
distance = distance.checked_add(1)?.checked_shl(7)? | u64::from(c & 0x7f);
}
let base: RawObject = self.read(offset.checked_sub(distance)?, by_name, depth + 1)?;
return self.apply(base, offset + cursor as u64, size);
}
7 => {
let name: &[u8] = head.get(cursor..cursor + self.hash_length)?;
cursor += self.hash_length;
let base: RawObject = by_name(name)?;
return self.apply(base, offset + cursor as u64, size);
}
_ => return None,
}
}
fn inflated(&self, start: u64, size: usize) -> Option<Vec<u8>> {
if size > OBJECT_SIZE_LIMIT {
return None;
}
let remaining: u64 = self.data_length.checked_sub(start)?.checked_sub(self.hash_length as u64)?;
for slack in [size / 4 + 4096, size * 2 + 65536] {
let want: usize = usize::try_from(remaining).ok()?.min(size + slack);
let window: Vec<u8> = read_at(&self.data, start, want)?;
if let Some(found) = inflate_exact(window.as_slice(), size) {
return Some(found);
}
if want as u64 == remaining {
break;
}
}
return None;
}
fn base_object(&self, kind: ObjectKind, start: u64, size: usize) -> Option<RawObject> {
return Some(RawObject { kind, bytes: self.inflated(start, size)? });
}
fn apply(&self, base: RawObject, start: u64, delta_length: usize) -> Option<RawObject> {
let delta: Vec<u8> = self.inflated(start, delta_length)?;
return Some(RawObject { kind: base.kind, bytes: apply_delta(base.bytes.as_slice(), delta.as_slice())? });
}
}
#[cfg(test)]
#[path = "repo_read_pack_tests.rs"]
mod tests;