1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
///
pub mod object {
    ///
    pub mod header {
        //! loose object header encoding and decoding
        use byteorder::WriteBytesExt;
        use git_object as object;

        /// Returned by [`decode()`]
        #[derive(thiserror::Error, Debug)]
        #[allow(missing_docs)]
        pub enum Error {
            #[error("{message}: {:?}", std::str::from_utf8(.number))]
            ParseIntegerError {
                source: btoi::ParseIntegerError,
                message: &'static str,
                number: Vec<u8>,
            },
            #[error("{0}")]
            InvalidHeader(&'static str),
            #[error(transparent)]
            ObjectHeader(#[from] object::Error),
        }

        /// Decode a loose object header, being `<kind> <size>\0`, returns ([`Kind`][object::Kind], `size`, `consumed bytes`).
        ///
        /// `size` is the uncompressed size of the payload in bytes.
        pub fn decode(input: &[u8]) -> Result<(object::Kind, u64, usize), Error> {
            let header_end = input
                .iter()
                .position(|&b| b == 0)
                .ok_or(Error::InvalidHeader("Did not find 0 byte in header"))?;
            let header = &input[..header_end];
            let mut split = header.split(|&b| b == b' ');
            match (split.next(), split.next()) {
                (Some(kind), Some(size)) => Ok((
                    object::Kind::from_bytes(kind)?,
                    btoi::btoi(size).map_err(|source| Error::ParseIntegerError {
                        message: "Object size in header could not be parsed",
                        number: size.to_owned(),
                        source,
                    })?,
                    header_end + 1, // account for 0 byte
                )),
                _ => Err(Error::InvalidHeader("Expected '<type> <size>'")),
            }
        }

        fn kind_to_bytes_with_space(object: object::Kind) -> &'static [u8] {
            use object::Kind::*;
            match object {
                Tree => b"tree ",
                Blob => b"blob ",
                Commit => b"commit ",
                Tag => b"tag ",
            }
        }

        /// Encode the objects `Kind` and `size` into a format suitable for use with [`decode()`].
        pub fn encode(object: object::Kind, size: u64, mut out: impl std::io::Write) -> Result<usize, std::io::Error> {
            let mut written = out.write(kind_to_bytes_with_space(object))?;
            written += itoa::write(&mut out, size)?;
            out.write_u8(0)?;
            Ok(written + 1)
        }

        #[cfg(test)]
        mod tests {
            mod encode_decode_round_trip {
                use git_object::bstr::ByteSlice;

                use crate::loose::object::header;

                #[test]
                fn all() -> Result<(), Box<dyn std::error::Error>> {
                    let mut buf = [0; 20];
                    for (kind, size, expected) in &[
                        (git_object::Kind::Tree, 1234, &b"tree 1234\0"[..]),
                        (git_object::Kind::Blob, 0, b"blob 0\0"),
                        (git_object::Kind::Commit, 24241, b"commit 24241\0"),
                        (git_object::Kind::Tag, 9999999999, b"tag 9999999999\0"),
                    ] {
                        let written = header::encode(*kind, *size, &mut buf[..])?;
                        assert_eq!(buf[..written].as_bstr(), expected.as_bstr());
                        let (actual_kind, actual_size, actual_read) = header::decode(&buf[..written])?;
                        assert_eq!(actual_kind, *kind);
                        assert_eq!(actual_size, *size);
                        assert_eq!(actual_read, written);
                    }
                    Ok(())
                }
            }
        }
    }
}