Skip to main content

git_policy_cli/
repo_read_pack.rs

1//! What:
2//!  A reader of one Git pack: the version 2 index lookup by object name and the entry read, with `OFS_DELTA` and
3//!  `REF_DELTA` resolution.
4//! Why:
5//!  Most objects of a real repository are packed.
6//!  The reader opens the two files, seeks for the few bytes a lookup needs, and returns `None` for anything it does not
7//!  fully understand (a version 1 index, an inconsistent size, a delta chain deeper than the limit, a bad delta),
8//!  which sends the caller to Git.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! const pack = openPack(idxPath, hashLength); const object = pack.read(pack.find(name), resolveByName);
13//! ```
14
15#![cfg(unix)]
16
17use super::repo_read_object::{ObjectKind, OBJECT_SIZE_LIMIT, RawObject, inflate_exact};
18use std::fs::File;
19use std::os::unix::fs::FileExt;
20use std::path::Path;
21
22/// The deepest delta chain this reader follows.
23const DELTA_DEPTH_LIMIT: usize = 100;
24
25/// The bytes of the index header and fanout table: magic, version, 256 counts.
26const FANOUT_END: u64 = 8 + 256 * 4;
27
28/// One open pack and its version 2 index.
29#[derive(Debug)]
30pub struct Pack {
31    /// The index file.
32    index: File,
33    /// The pack file.
34    data: File,
35    /// The size in bytes of an object name.
36    hash_length: usize,
37    /// How many objects the pack holds.
38    count: u64,
39    /// The size in bytes of the pack file.
40    data_length: u64,
41    /// The cumulative fanout counts by first byte.
42    fanout: Vec<u32>,
43}
44
45/// Read exactly `length` bytes at `position`, or `None`.
46fn read_at(file: &File, position: u64, length: usize) -> Option<Vec<u8>> {
47    let mut buffer: Vec<u8> = vec![0; length];
48    file.read_exact_at(buffer.as_mut_slice(), position).ok()?;
49    return Some(buffer);
50}
51
52/// A big-endian `u32` at `position`.
53fn be32(file: &File, position: u64) -> Option<u32> {
54    let bytes: Vec<u8> = read_at(file, position, 4)?;
55    return Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]));
56}
57
58/// One delta size varint at `cursor`: seven bits per byte, low first.
59fn delta_size(delta: &[u8], cursor: &mut usize) -> Option<usize> {
60    let mut size: usize = 0;
61    let mut shift: u32 = 0;
62    loop {
63        let byte: u8 = *delta.get(*cursor)?;
64        *cursor += 1;
65        size |= usize::from(byte & 0x7f).checked_shl(shift)?;
66        if byte & 0x80 == 0 {
67            return Some(size);
68        }
69        shift += 7;
70        if shift > 56 {
71            return None;
72        }
73    }
74}
75
76/// What:
77///  Apply a Git delta to its base.
78/// Why:
79///   A delta states the base size and the result size and then copies from the base or inserts bytes; every number is
80///  checked, because a corrupt delta must be refused and not read past a buffer.
81///
82/// In TS you'd write (pseudocode):
83/// ```ts
84/// function applyDelta(base: Uint8Array, delta: Uint8Array): Uint8Array | undefined;
85/// ```
86pub fn apply_delta(base: &[u8], delta: &[u8]) -> Option<Vec<u8>> {
87    let mut cursor: usize = 0;
88    if delta_size(delta, &mut cursor)? != base.len() {
89        return None;
90    }
91    let result_size: usize = delta_size(delta, &mut cursor)?;
92    if result_size > OBJECT_SIZE_LIMIT {
93        return None;
94    }
95    let mut result: Vec<u8> = Vec::with_capacity(result_size);
96    while cursor < delta.len() {
97        let op: u8 = delta[cursor];
98        cursor += 1;
99        if op & 0x80 != 0 {
100            let mut offset: usize = 0;
101            let mut length: usize = 0;
102            for bit in 0..4 {
103                if op & (1 << bit) != 0 {
104                    offset |= usize::from(*delta.get(cursor)?) << (8 * bit);
105                    cursor += 1;
106                }
107            }
108            for bit in 0..3 {
109                if op & (0x10 << bit) != 0 {
110                    length |= usize::from(*delta.get(cursor)?) << (8 * bit);
111                    cursor += 1;
112                }
113            }
114            if length == 0 {
115                length = 0x10000;
116            }
117            result.extend_from_slice(base.get(offset..offset.checked_add(length)?)?);
118        } else if op != 0 {
119            let end: usize = cursor.checked_add(usize::from(op))?;
120            result.extend_from_slice(delta.get(cursor..end)?);
121            cursor = end;
122        } else {
123            return None;
124        }
125        if result.len() > result_size {
126            return None;
127        }
128    }
129    if result.len() != result_size {
130        return None;
131    }
132    return Some(result);
133}
134
135impl Pack {
136    /// Open the pack whose index is at `index_path` (the pack is the same name ending `.pack`); `None` unless the index
137    /// is version 2 and its size agrees with its own count.
138    pub fn open(index_path: &Path, hash_length: usize) -> Option<Pack> {
139        let index: File = File::open(index_path).ok()?;
140        let data: File = File::open(index_path.with_extension("pack")).ok()?;
141        let header: Vec<u8> = read_at(&index, 0, 8)?;
142        if header != b"\xfftOc\0\0\0\x02" {
143            return None;
144        }
145        let mut fanout: Vec<u32> = Vec::with_capacity(256);
146        for slot in 0..256_u64 {
147            fanout.push(be32(&index, 8 + slot * 4)?);
148        }
149        if fanout.windows(2).any(|pair| { return pair[0] > pair[1]; }) {
150            return None;
151        }
152        let count: u64 = u64::from(fanout[255]);
153        let index_length: u64 = index.metadata().ok()?.len();
154        let minimum: u64 = FANOUT_END + count * (hash_length as u64 + 8) + 2 * hash_length as u64;
155        if index_length < minimum {
156            return None;
157        }
158        let data_length: u64 = data.metadata().ok()?.len();
159        // The pack states its own version and object count; they must agree with the index.
160        let pack_header: Vec<u8> = read_at(&data, 0, 12)?;
161        let pack_count: u32 = u32::from_be_bytes([pack_header[8], pack_header[9], pack_header[10], pack_header[11]]);
162        if &pack_header[..4] != b"PACK" || !matches!(pack_header[7], 2 | 3) || pack_header[4..7] != [0, 0, 0] || u64::from(pack_count) != count {
163            return None;
164        }
165        return Some(Pack { index, data, hash_length, count, data_length, fanout });
166    }
167
168    /// The pack offset of the object named `name`, or `None` when this pack does not hold it.
169    pub fn find(&self, name: &[u8]) -> Option<u64> {
170        if name.len() != self.hash_length {
171            return None;
172        }
173        let first: usize = usize::from(name[0]);
174        let mut low: u64 = if first == 0 { 0 } else { u64::from(self.fanout[first - 1]) };
175        let mut high: u64 = u64::from(self.fanout[first]);
176        let names_at: u64 = FANOUT_END;
177        while low < high {
178            let middle: u64 = low + (high - low) / 2;
179            let candidate: Vec<u8> = read_at(&self.index, names_at + middle * self.hash_length as u64, self.hash_length)?;
180            match candidate.as_slice().cmp(name) {
181                std::cmp::Ordering::Equal => return self.offset_of(middle),
182                std::cmp::Ordering::Less => low = middle + 1,
183                std::cmp::Ordering::Greater => high = middle,
184            }
185        }
186        return None;
187    }
188
189    /// The pack offset stored for the `position`th name, following the large-offset table.
190    fn offset_of(&self, position: u64) -> Option<u64> {
191        let offsets_at: u64 = FANOUT_END + self.count * (self.hash_length as u64 + 4);
192        let word: u32 = be32(&self.index, offsets_at + position * 4)?;
193        if word & 0x8000_0000 == 0 {
194            return Some(u64::from(word));
195        }
196        let large_at: u64 = offsets_at + self.count * 4 + u64::from(word & 0x7fff_ffff) * 8;
197        let bytes: Vec<u8> = read_at(&self.index, large_at, 8)?;
198        let mut array: [u8; 8] = [0; 8];
199        array.copy_from_slice(bytes.as_slice());
200        return Some(u64::from_be_bytes(array));
201    }
202
203    /// What:
204    ///  Read the object stored at `offset`, resolving deltas; `by_name` finds the base of a `REF_DELTA`.
205    /// Why:
206    ///   A packed object is a base object or a delta over another entry of the pack or over any object by name.
207    ///
208    /// In TS you'd write (pseudocode):
209    /// ```ts
210    /// read(offset: number, byName: (name: Uint8Array) => RawObject | undefined, depth = 0): RawObject | undefined;
211    /// ```
212    pub fn read(&self, offset: u64, by_name: &dyn Fn(&[u8]) -> Option<RawObject>, depth: usize) -> Option<RawObject> {
213        if depth > DELTA_DEPTH_LIMIT || offset < 12 || offset >= self.data_length {
214            return None;
215        }
216        // Room for the size and distance varints and, for a `REF_DELTA`, the base name that follows them.
217        let head: Vec<u8> = read_at(&self.data, offset, (32 + self.hash_length).min((self.data_length - offset) as usize))?;
218        let mut cursor: usize = 0;
219        let first: u8 = head[0];
220        cursor += 1;
221        let code: u8 = (first >> 4) & 7;
222        let mut size: usize = usize::from(first & 15);
223        let mut shift: u32 = 4;
224        let mut byte: u8 = first;
225        while byte & 0x80 != 0 {
226            byte = *head.get(cursor)?;
227            cursor += 1;
228            size |= usize::from(byte & 0x7f).checked_shl(shift)?;
229            shift += 7;
230            if shift > 60 {
231                return None;
232            }
233        }
234        match code {
235            1 => return self.base_object(ObjectKind::Commit, offset + cursor as u64, size),
236            2 => return self.base_object(ObjectKind::Tree, offset + cursor as u64, size),
237            3 => return self.base_object(ObjectKind::Blob, offset + cursor as u64, size),
238            4 => return self.base_object(ObjectKind::Tag, offset + cursor as u64, size),
239            6 => {
240                let mut c: u8 = *head.get(cursor)?;
241                cursor += 1;
242                let mut distance: u64 = u64::from(c & 0x7f);
243                while c & 0x80 != 0 {
244                    c = *head.get(cursor)?;
245                    cursor += 1;
246                    distance = distance.checked_add(1)?.checked_shl(7)? | u64::from(c & 0x7f);
247                }
248                let base: RawObject = self.read(offset.checked_sub(distance)?, by_name, depth + 1)?;
249                return self.apply(base, offset + cursor as u64, size);
250            }
251            7 => {
252                let name: &[u8] = head.get(cursor..cursor + self.hash_length)?;
253                cursor += self.hash_length;
254                let base: RawObject = by_name(name)?;
255                return self.apply(base, offset + cursor as u64, size);
256            }
257            _ => return None,
258        }
259    }
260
261    /// Inflate the stream at `start` that must be `size` bytes.
262    fn inflated(&self, start: u64, size: usize) -> Option<Vec<u8>> {
263        // The size comes from the pack; it is checked before any window is allocated for it.
264        if size > OBJECT_SIZE_LIMIT {
265            return None;
266        }
267        let remaining: u64 = self.data_length.checked_sub(start)?.checked_sub(self.hash_length as u64)?;
268        for slack in [size / 4 + 4096, size * 2 + 65536] {
269            let want: usize = usize::try_from(remaining).ok()?.min(size + slack);
270            let window: Vec<u8> = read_at(&self.data, start, want)?;
271            if let Some(found) = inflate_exact(window.as_slice(), size) {
272                return Some(found);
273            }
274            if want as u64 == remaining {
275                break;
276            }
277        }
278        return None;
279    }
280
281    /// A non-delta entry.
282    fn base_object(&self, kind: ObjectKind, start: u64, size: usize) -> Option<RawObject> {
283        return Some(RawObject { kind, bytes: self.inflated(start, size)? });
284    }
285
286    /// A delta entry over `base`.
287    fn apply(&self, base: RawObject, start: u64, delta_length: usize) -> Option<RawObject> {
288        let delta: Vec<u8> = self.inflated(start, delta_length)?;
289        return Some(RawObject { kind: base.kind, bytes: apply_delta(base.bytes.as_slice(), delta.as_slice())? });
290    }
291}
292
293#[cfg(test)]
294#[path = "repo_read_pack_tests.rs"]
295mod tests;