Skip to main content

amiss_git/
object.rs

1use std::io::Read as _;
2
3use amiss_wire::controls::{GitMode, ResourceName};
4use amiss_wire::model::{ObjectFormat, Oid};
5use flate2::bufread::ZlibDecoder;
6use sha1_checked::Digest as _;
7use sha2::Digest as _;
8
9use crate::Error;
10use crate::resources::ValueCap;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum ObjectKind {
14    Blob,
15    Commit,
16    Tag,
17    Tree,
18}
19
20impl ObjectKind {
21    #[must_use]
22    pub const fn as_str(self) -> &'static str {
23        match self {
24            Self::Blob => "blob",
25            Self::Commit => "commit",
26            Self::Tag => "tag",
27            Self::Tree => "tree",
28        }
29    }
30
31    pub(crate) const fn from_pack_type(code: u8) -> Option<Self> {
32        match code {
33            1 => Some(Self::Commit),
34            2 => Some(Self::Tree),
35            3 => Some(Self::Blob),
36            4 => Some(Self::Tag),
37            _ => None,
38        }
39    }
40
41    fn from_token(token: &[u8]) -> Option<Self> {
42        match token {
43            b"blob" => Some(Self::Blob),
44            b"commit" => Some(Self::Commit),
45            b"tag" => Some(Self::Tag),
46            b"tree" => Some(Self::Tree),
47            _ => None,
48        }
49    }
50}
51
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct Object {
54    pub kind: ObjectKind,
55    pub body: Vec<u8>,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct TreeEntry {
60    pub mode: GitMode,
61    pub name: Vec<u8>,
62    pub oid: Oid,
63}
64
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct Commit {
67    pub tree: Oid,
68    pub parents: Vec<Oid>,
69}
70
71/// Inflates, grammar-checks, and hash-verifies one loose zlib stream.
72///
73/// # Errors
74///
75/// `ObjectUnreadable` for any header, stream, or digest defect;
76/// `ResourceLimit` when the declared size crosses `inflated_cap` digitwise.
77pub fn decode_loose(
78    compressed: &[u8],
79    object_format: ObjectFormat,
80    oid: &Oid,
81    inflated_cap: u64,
82    value_cap: Option<&ValueCap>,
83) -> Result<Object, Error> {
84    let mut decoder = ZlibDecoder::new(compressed);
85    let header = read_header(&mut decoder, inflated_cap, value_cap)?;
86    let mut body = vec![0_u8; header.size];
87    fill_exact(&mut decoder, &mut body)?;
88    let mut probe = [0_u8; 1];
89    match decoder.read(&mut probe) {
90        Ok(0) => {}
91        Ok(_) | Err(_) => return Err(Error::ObjectUnreadable),
92    }
93    if decoder.total_in() != u64::try_from(compressed.len()).map_err(discard_to_unreadable)? {
94        return Err(Error::ObjectUnreadable);
95    }
96    verify_oid(object_format, oid, &header.raw, &body)?;
97    Ok(Object {
98        kind: header.kind,
99        body,
100    })
101}
102
103struct Header {
104    kind: ObjectKind,
105    size: usize,
106    raw: Vec<u8>,
107}
108
109fn read_header(
110    decoder: &mut ZlibDecoder<&[u8]>,
111    inflated_cap: u64,
112    value_cap: Option<&ValueCap>,
113) -> Result<Header, Error> {
114    const MAX_SAFE: u64 = 9_007_199_254_740_991;
115    let mut raw: Vec<u8> = Vec::new();
116    let mut token: Vec<u8> = Vec::new();
117    let kind = loop {
118        let byte = next_byte(decoder)?;
119        raw.push(byte);
120        if byte == b' ' {
121            break ObjectKind::from_token(&token).ok_or(Error::ObjectUnreadable)?;
122        }
123        token.push(byte);
124        if token.len() > 6 {
125            return Err(Error::ObjectUnreadable);
126        }
127    };
128
129    let mut value: u64 = 0;
130    let mut digits: usize = 0;
131    let mut leading_zero = false;
132    let mut saturated = false;
133    loop {
134        let byte = next_byte(decoder)?;
135        raw.push(byte);
136        if byte == 0 {
137            break;
138        }
139        if !byte.is_ascii_digit() || leading_zero {
140            return Err(Error::ObjectUnreadable);
141        }
142        if digits == 0 && byte == b'0' {
143            leading_zero = true;
144        }
145        digits = digits.saturating_add(1);
146        value = value
147            .saturating_mul(10)
148            .saturating_add(u64::from(byte.wrapping_sub(b'0')));
149        if value > MAX_SAFE {
150            value = MAX_SAFE;
151            saturated = true;
152            break;
153        }
154    }
155    if digits == 0 && !saturated {
156        return Err(Error::ObjectUnreadable);
157    }
158    if let Some(cap) = value_cap
159        && value > cap.limit
160    {
161        return Err(Error::ResourceLimit {
162            resource: cap.resource,
163            configured_limit: cap.limit,
164            observed_lower_bound: value,
165        });
166    }
167    if value > inflated_cap {
168        return Err(Error::ResourceLimit {
169            resource: ResourceName::GitObjectBytes,
170            configured_limit: inflated_cap,
171            observed_lower_bound: value,
172        });
173    }
174    let size = usize::try_from(value).map_err(discard_to_unreadable)?;
175    Ok(Header { kind, size, raw })
176}
177
178fn next_byte(decoder: &mut ZlibDecoder<&[u8]>) -> Result<u8, Error> {
179    let mut buf = [0_u8; 1];
180    match decoder.read(&mut buf) {
181        Ok(1) => buf.first().copied().ok_or(Error::ObjectUnreadable),
182        Ok(_) | Err(_) => Err(Error::ObjectUnreadable),
183    }
184}
185
186fn fill_exact(decoder: &mut ZlibDecoder<&[u8]>, body: &mut [u8]) -> Result<(), Error> {
187    let mut filled = 0_usize;
188    while filled < body.len() {
189        let target = body.get_mut(filled..).ok_or(Error::ObjectUnreadable)?;
190        match decoder.read(target) {
191            Ok(0) | Err(_) => return Err(Error::ObjectUnreadable),
192            Ok(n) => filled = filled.saturating_add(n),
193        }
194    }
195    Ok(())
196}
197
198pub(crate) fn discard_to_unreadable<T>(_defect: T) -> Error {
199    Error::ObjectUnreadable
200}
201
202pub(crate) fn ordinary_digest(object_format: ObjectFormat, data: &[u8]) -> Vec<u8> {
203    match object_format {
204        ObjectFormat::Sha1 => {
205            let mut hasher = sha1_checked::Sha1::builder()
206                .detect_collision(false)
207                .build();
208            hasher.update(data);
209            hasher.try_finalize().hash().to_vec()
210        }
211        ObjectFormat::Sha256 => {
212            let mut hasher = sha2::Sha256::new();
213            hasher.update(data);
214            hasher.finalize().to_vec()
215        }
216    }
217}
218
219pub(crate) fn verify_oid(
220    object_format: ObjectFormat,
221    oid: &Oid,
222    raw_header: &[u8],
223    body: &[u8],
224) -> Result<(), Error> {
225    let actual = match object_format {
226        ObjectFormat::Sha1 => {
227            let mut hasher = sha1_checked::Sha1::builder()
228                .detect_collision(true)
229                .safe_hash(false)
230                .use_ubc(true)
231                .build();
232            hasher.update(raw_header);
233            hasher.update(body);
234            let result = hasher.try_finalize();
235            if result.has_collision() {
236                return Err(Error::ObjectUnreadable);
237            }
238            hex(result.hash().as_slice())
239        }
240        ObjectFormat::Sha256 => {
241            let mut hasher = sha2::Sha256::new();
242            hasher.update(raw_header);
243            hasher.update(body);
244            hex(&hasher.finalize())
245        }
246    };
247    if actual == oid.as_str() {
248        Ok(())
249    } else {
250        Err(Error::ObjectUnreadable)
251    }
252}
253
254pub(crate) fn hex(bytes: &[u8]) -> String {
255    let mut out = String::new();
256    for byte in bytes {
257        out.push(hex_digit(u32::from(byte.wrapping_shr(4))));
258        out.push(hex_digit(u32::from(byte & 0xF)));
259    }
260    out
261}
262
263fn hex_digit(value: u32) -> char {
264    char::from_digit(value, 16).unwrap_or('0')
265}
266
267/// Parses a tree body under `git-object-grammar-v1`.
268///
269/// # Errors
270///
271/// `ObjectUnreadable` for any mode, name, width, ordering, or padding defect.
272pub fn parse_tree(object_format: ObjectFormat, body: &[u8]) -> Result<Vec<TreeEntry>, Error> {
273    let oid_width = match object_format {
274        ObjectFormat::Sha1 => 20_usize,
275        ObjectFormat::Sha256 => 32_usize,
276    };
277    let mut entries: Vec<TreeEntry> = Vec::new();
278    let mut previous_key: Option<Vec<u8>> = None;
279    let mut pos = 0_usize;
280    while pos < body.len() {
281        let rest = body.get(pos..).ok_or(Error::ObjectUnreadable)?;
282        let space = rest
283            .iter()
284            .position(|&b| b == b' ')
285            .ok_or(Error::ObjectUnreadable)?;
286        let mode_bytes = rest.get(..space).ok_or(Error::ObjectUnreadable)?;
287        let mode = tree_mode(mode_bytes)?;
288        let after_mode = rest
289            .get(space.saturating_add(1)..)
290            .ok_or(Error::ObjectUnreadable)?;
291        let nul = after_mode
292            .iter()
293            .position(|&b| b == 0)
294            .ok_or(Error::ObjectUnreadable)?;
295        let name = after_mode.get(..nul).ok_or(Error::ObjectUnreadable)?;
296        if name.is_empty() || name.contains(&b'/') || name == b"." || name == b".." {
297            return Err(Error::ObjectUnreadable);
298        }
299        let oid_start = nul.saturating_add(1);
300        let oid_end = oid_start.saturating_add(oid_width);
301        let raw_oid = after_mode
302            .get(oid_start..oid_end)
303            .ok_or(Error::ObjectUnreadable)?;
304        let oid = Oid::new(object_format, hex(raw_oid)).ok_or(Error::ObjectUnreadable)?;
305
306        let mut key = name.to_vec();
307        if mode == GitMode::Tree {
308            key.push(b'/');
309        }
310        if let Some(previous) = &previous_key
311            && *previous >= key
312        {
313            return Err(Error::ObjectUnreadable);
314        }
315        previous_key = Some(key);
316
317        entries.push(TreeEntry {
318            mode,
319            name: name.to_vec(),
320            oid,
321        });
322        pos = pos
323            .saturating_add(space)
324            .saturating_add(1)
325            .saturating_add(oid_end);
326    }
327    Ok(entries)
328}
329
330fn tree_mode(bytes: &[u8]) -> Result<GitMode, Error> {
331    match bytes {
332        b"40000" => Ok(GitMode::Tree),
333        b"100644" => Ok(GitMode::RegularFile),
334        b"100755" => Ok(GitMode::ExecutableFile),
335        b"120000" => Ok(GitMode::Symlink),
336        b"160000" => Ok(GitMode::Gitlink),
337        _ => Err(Error::ObjectUnreadable),
338    }
339}
340
341/// Parses a commit body's headers under `git-object-grammar-v1`; the message
342/// stays opaque.
343///
344/// # Errors
345///
346/// `ObjectUnreadable` for any header order, multiplicity, byte, or
347/// continuation defect.
348pub fn parse_commit(object_format: ObjectFormat, body: &[u8]) -> Result<Commit, Error> {
349    let mut lines = HeaderLines { body, pos: 0 };
350
351    let first = lines.next_line()?;
352    let tree_hex = first
353        .strip_prefix(b"tree ")
354        .ok_or(Error::ObjectUnreadable)?;
355    let tree = header_oid(object_format, tree_hex)?;
356
357    let mut parents: Vec<Oid> = Vec::new();
358    let mut line = lines.next_line()?;
359    while let Some(parent_hex) = line.strip_prefix(b"parent ") {
360        parents.push(header_oid(object_format, parent_hex)?);
361        line = lines.next_line()?;
362    }
363
364    let author = line
365        .strip_prefix(b"author ")
366        .ok_or(Error::ObjectUnreadable)?;
367    if author.is_empty() {
368        return Err(Error::ObjectUnreadable);
369    }
370    let committer_line = lines.next_line()?;
371    let committer = committer_line
372        .strip_prefix(b"committer ")
373        .ok_or(Error::ObjectUnreadable)?;
374    if committer.is_empty() {
375        return Err(Error::ObjectUnreadable);
376    }
377
378    let mut seen_extension = false;
379    loop {
380        let line = lines.next_line()?;
381        if line.is_empty() {
382            break;
383        }
384        if line.first() == Some(&b' ') {
385            if !seen_extension {
386                return Err(Error::ObjectUnreadable);
387            }
388            continue;
389        }
390        let space = line
391            .iter()
392            .position(|&b| b == b' ')
393            .ok_or(Error::ObjectUnreadable)?;
394        let key = line.get(..space).ok_or(Error::ObjectUnreadable)?;
395        let key_ok = !key.is_empty()
396            && key
397                .iter()
398                .all(|&b| b.is_ascii() && !b.is_ascii_control() && b != b' ')
399            && !matches!(key, b"tree" | b"parent" | b"author" | b"committer");
400        if !key_ok {
401            return Err(Error::ObjectUnreadable);
402        }
403        seen_extension = true;
404    }
405
406    Ok(Commit { tree, parents })
407}
408
409struct HeaderLines<'a> {
410    body: &'a [u8],
411    pos: usize,
412}
413
414impl HeaderLines<'_> {
415    fn next_line(&mut self) -> Result<&[u8], Error> {
416        let rest = self.body.get(self.pos..).ok_or(Error::ObjectUnreadable)?;
417        let lf = rest
418            .iter()
419            .position(|&b| b == b'\n')
420            .ok_or(Error::ObjectUnreadable)?;
421        let line = rest.get(..lf).ok_or(Error::ObjectUnreadable)?;
422        if line.iter().any(|&b| b == 0 || b == b'\r') {
423            return Err(Error::ObjectUnreadable);
424        }
425        self.pos = self.pos.saturating_add(lf).saturating_add(1);
426        Ok(line)
427    }
428}
429
430fn header_oid(object_format: ObjectFormat, hex_bytes: &[u8]) -> Result<Oid, Error> {
431    let text = std::str::from_utf8(hex_bytes).map_err(discard_to_unreadable)?;
432    Oid::new(object_format, text.to_owned()).ok_or(Error::ObjectUnreadable)
433}