Skip to main content

git_policy_cli/
repo_read_object.rs

1//! What:
2//!  The shapes of a Git object read in process, object names as bytes and hexadecimal, and the zlib step.
3//! Why:
4//!  Loose objects and pack entries are zlib streams; every reader of this family shares one inflate call
5//!  (a pure Rust decoder) and one definition of an object name, so a size or format mismatch is refused in one place.
6//!
7//! In TS you'd write (pseudocode):
8//! ```ts
9//! type RawObject = { kind: 'commit' | 'tree' | 'blob' | 'tag'; bytes: Uint8Array };
10//! ```
11
12use super::repo_read_layout::ObjectFormat;
13use miniz_oxide::inflate::decompress_to_vec_zlib_with_limit;
14
15/// The largest object this family inflates; anything bigger is left to Git.
16pub const OBJECT_SIZE_LIMIT: usize = 256 << 20;
17
18/// The four kinds of Git object.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum ObjectKind {
21    /// A commit.
22    Commit,
23    /// A tree.
24    Tree,
25    /// A blob.
26    Blob,
27    /// An annotated tag.
28    Tag,
29}
30
31impl ObjectKind {
32    /// The word Git writes in an object header and `cat-file -t` prints.
33    pub fn word(self) -> &'static str {
34        return match self {
35            ObjectKind::Commit => "commit",
36            ObjectKind::Tree => "tree",
37            ObjectKind::Blob => "blob",
38            ObjectKind::Tag => "tag",
39        };
40    }
41
42    /// The kind a loose-object header word names.
43    pub fn from_word(word: &[u8]) -> Option<ObjectKind> {
44        return match word {
45            b"commit" => Some(ObjectKind::Commit),
46            b"tree" => Some(ObjectKind::Tree),
47            b"blob" => Some(ObjectKind::Blob),
48            b"tag" => Some(ObjectKind::Tag),
49            _ => None,
50        };
51    }
52}
53
54/// One object with its kind and its content, without the header.
55#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct RawObject {
57    /// What the object is.
58    pub kind: ObjectKind,
59    /// The content bytes.
60    pub bytes: Vec<u8>,
61}
62
63/// The length in bytes of an object name.
64pub fn name_length(format: ObjectFormat) -> usize {
65    return format.hex_length() / 2;
66}
67
68/// The lower-case hexadecimal spelling of an object name.
69pub fn to_hex(name: &[u8]) -> String {
70    let mut text: String = String::with_capacity(name.len() * 2);
71    for byte in name {
72        text.push(char::from(b"0123456789abcdef"[usize::from(byte >> 4)]));
73        text.push(char::from(b"0123456789abcdef"[usize::from(byte & 15)]));
74    }
75    return text;
76}
77
78/// The raw object name a hexadecimal spelling of the right length stands for.
79pub fn from_hex(text: &[u8], format: ObjectFormat) -> Option<Vec<u8>> {
80    if text.len() != format.hex_length() {
81        return None;
82    }
83    let digit = |byte: u8| -> Option<u8> {
84        return match byte {
85            b'0'..=b'9' => Some(byte - b'0'),
86            b'a'..=b'f' => Some(byte - b'a' + 10),
87            _ => None,
88        };
89    };
90    let mut name: Vec<u8> = Vec::with_capacity(text.len() / 2);
91    for pair in text.chunks(2) {
92        name.push(digit(pair[0])? << 4 | digit(pair[1])?);
93    }
94    return Some(name);
95}
96
97/// What:
98///  Read a loose object file's bytes: the zlib stream of `<kind> <size>\0<content>`; the kind and content come back
99///  only when the header's size equals the content length.
100/// Why:
101///   A loose file that disagrees with its own header is corrupt, and Git words that failure itself.
102///
103/// In TS you'd write (pseudocode):
104/// ```ts
105/// const object = parseLoose(await readFile(path)); // undefined: ask Git
106/// ```
107pub fn parse_loose(file_bytes: &[u8]) -> Option<RawObject> {
108    let whole: Vec<u8> = decompress_to_vec_zlib_with_limit(file_bytes, OBJECT_SIZE_LIMIT + 32).ok()?;
109    let nul: usize = whole.iter().position(|byte| { return *byte == 0; })?;
110    let header: &[u8] = &whole[..nul];
111    let space: usize = header.iter().position(|byte| { return *byte == b' '; })?;
112    let kind: ObjectKind = ObjectKind::from_word(&header[..space])?;
113    let size_text: &str = std::str::from_utf8(&header[space + 1..]).ok()?;
114    if size_text.is_empty() || (size_text.len() > 1 && size_text.starts_with('0')) || !size_text.bytes().all(|byte| { return byte.is_ascii_digit(); }) {
115        return None;
116    }
117    let size: usize = size_text.parse::<usize>().ok()?;
118    let content: &[u8] = &whole[nul + 1..];
119    if content.len() != size {
120        return None;
121    }
122    return Some(RawObject { kind, bytes: content.to_vec() });
123}
124
125/// What:
126///  Inflate a zlib stream whose result must be exactly `expected` bytes; trailing input is ignored.
127/// Why:
128///   Pack entries do not record their compressed length, so the caller hands a window that may run past the stream.
129///
130/// In TS you'd write (pseudocode):
131/// ```ts
132/// const out = inflateZlib(window, expected); // undefined when the stream is bad, short or too long
133/// ```
134pub fn inflate_exact(window: &[u8], expected: usize) -> Option<Vec<u8>> {
135    if expected > OBJECT_SIZE_LIMIT {
136        return None;
137    }
138    let inflated: Vec<u8> = decompress_to_vec_zlib_with_limit(window, expected).ok()?;
139    if inflated.len() != expected {
140        return None;
141    }
142    return Some(inflated);
143}