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