Skip to main content

git_zero_core/
object.rs

1use core::fmt;
2use sha1::{Digest, Sha1};
3use sha2::Sha256;
4
5use crate::{Error, Result, io::SliceWriter};
6
7#[cfg(feature = "read")]
8mod parse;
9#[cfg(feature = "read")]
10pub use parse::{Commit, Tag, Tree, TreeEntries, TreeEntry};
11
12#[cfg(feature = "read")]
13mod links;
14#[cfg(feature = "read")]
15pub use links::{LinkDecoder, LinkLimits, LinkProgress, LinkRelation, LinkStatus};
16
17/// One repository's object/hash encoding. Object formats cannot be mixed.
18#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Hash)]
19pub enum ObjectFormat {
20    #[default]
21    Sha1,
22    Sha256,
23}
24
25impl ObjectFormat {
26    pub const fn name(self) -> &'static str {
27        match self {
28            Self::Sha1 => "sha1",
29            Self::Sha256 => "sha256",
30        }
31    }
32    pub const fn id_length(self) -> usize {
33        match self {
34            Self::Sha1 => 20,
35            Self::Sha256 => 32,
36        }
37    }
38    pub const fn hex_length(self) -> usize {
39        self.id_length() * 2
40    }
41    pub const fn zero_id(self) -> ObjectId {
42        match self {
43            Self::Sha1 => ObjectId::Sha1([0; 20]),
44            Self::Sha256 => ObjectId::Sha256([0; 32]),
45        }
46    }
47    pub const fn capability(self) -> &'static [u8] {
48        match self {
49            Self::Sha1 => b"object-format=sha1",
50            Self::Sha256 => b"object-format=sha256",
51        }
52    }
53    pub(crate) fn hash(self) -> Hash {
54        match self {
55            Self::Sha1 => Hash::Sha1(Sha1::new()),
56            Self::Sha256 => Hash::Sha256(Sha256::new()),
57        }
58    }
59    pub(crate) fn checked_hash(self) -> CheckedHash {
60        match self {
61            Self::Sha1 => CheckedHash::Sha1(sha1_checked::Sha1::default()),
62            Self::Sha256 => CheckedHash::Sha256(Sha256::new()),
63        }
64    }
65    #[cfg(feature = "read")]
66    pub(crate) fn digest(self, bytes: &[u8]) -> Result<ObjectId> {
67        let mut hash = self.checked_hash();
68        hash.update(bytes);
69        hash.finish()
70    }
71}
72
73// The collision detector retains its state inline: boxing it would introduce
74// an allocator into object/pack authentication on bare-metal and Wasm targets.
75#[allow(clippy::large_enum_variant)]
76#[derive(Clone)]
77pub(crate) enum CheckedHash {
78    Sha1(sha1_checked::Sha1),
79    Sha256(Sha256),
80}
81impl CheckedHash {
82    pub(crate) fn update(&mut self, bytes: &[u8]) {
83        match self {
84            Self::Sha1(hash) => hash.update(bytes),
85            Self::Sha256(hash) => hash.update(bytes),
86        }
87    }
88    pub(crate) fn finish(self) -> Result<ObjectId> {
89        match self {
90            Self::Sha1(hash) => match hash.try_finalize() {
91                sha1_checked::CollisionResult::Ok(digest) => Ok(ObjectId::Sha1(digest.into())),
92                _ => Err(Error::InvalidObject),
93            },
94            Self::Sha256(hash) => Ok(ObjectId::Sha256(hash.finalize().into())),
95        }
96    }
97}
98
99#[derive(Clone)]
100pub(crate) enum Hash {
101    Sha1(Sha1),
102    Sha256(Sha256),
103}
104impl Hash {
105    pub(crate) fn update(&mut self, bytes: &[u8]) {
106        match self {
107            Self::Sha1(hash) => hash.update(bytes),
108            Self::Sha256(hash) => hash.update(bytes),
109        }
110    }
111    pub(crate) fn finish(self) -> ObjectId {
112        match self {
113            Self::Sha1(hash) => ObjectId::Sha1(hash.finalize().into()),
114            Self::Sha256(hash) => ObjectId::Sha256(hash.finalize().into()),
115        }
116    }
117}
118
119/// A format-tagged object ID; no truncation or implicit format conversion.
120#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
121pub enum ObjectId {
122    Sha1([u8; 20]),
123    Sha256([u8; 32]),
124}
125
126/// Fixed-memory hex storage, exposing only the active 40 or 64 bytes.
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub struct HexId {
129    bytes: [u8; 64],
130    length: usize,
131}
132impl HexId {
133    pub fn as_bytes(&self) -> &[u8] {
134        &self.bytes[..self.length]
135    }
136    pub fn as_slice(&self) -> &[u8] {
137        self.as_bytes()
138    }
139}
140impl core::ops::Deref for HexId {
141    type Target = [u8];
142    fn deref(&self) -> &[u8] {
143        self.as_bytes()
144    }
145}
146
147impl ObjectId {
148    pub fn from_hex(hex: &[u8]) -> Result<Self> {
149        let format = match hex.len() {
150            40 => ObjectFormat::Sha1,
151            64 => ObjectFormat::Sha256,
152            _ => return Err(Error::InvalidObjectId),
153        };
154        let mut bytes = [0; 32];
155        for (destination, pair) in bytes[..format.id_length()]
156            .iter_mut()
157            .zip(hex.chunks_exact(2))
158        {
159            *destination = (nibble(pair[0])? << 4) | nibble(pair[1])?;
160        }
161        Self::from_bytes(format, &bytes[..format.id_length()])
162    }
163
164    pub fn from_bytes(format: ObjectFormat, bytes: &[u8]) -> Result<Self> {
165        Ok(match format {
166            ObjectFormat::Sha1 => Self::Sha1(bytes.try_into().map_err(|_| Error::InvalidObjectId)?),
167            ObjectFormat::Sha256 => {
168                Self::Sha256(bytes.try_into().map_err(|_| Error::InvalidObjectId)?)
169            }
170        })
171    }
172    pub const fn format(self) -> ObjectFormat {
173        match self {
174            Self::Sha1(_) => ObjectFormat::Sha1,
175            Self::Sha256(_) => ObjectFormat::Sha256,
176        }
177    }
178    pub fn as_bytes(&self) -> &[u8] {
179        match self {
180            Self::Sha1(bytes) => bytes,
181            Self::Sha256(bytes) => bytes,
182        }
183    }
184    pub fn is_zero(self) -> bool {
185        self.as_bytes().iter().all(|&byte| byte == 0)
186    }
187    pub fn to_hex(self) -> HexId {
188        const HEX: &[u8; 16] = b"0123456789abcdef";
189        let mut result = HexId {
190            bytes: [0; 64],
191            length: self.format().hex_length(),
192        };
193        for (i, byte) in self.as_bytes().iter().enumerate() {
194            result.bytes[i * 2] = HEX[(byte >> 4) as usize];
195            result.bytes[i * 2 + 1] = HEX[(byte & 15) as usize];
196        }
197        result
198    }
199}
200
201impl fmt::Display for ObjectId {
202    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
203        let hex = self.to_hex();
204        let text = core::str::from_utf8(&hex).map_err(|_| fmt::Error)?;
205        output.write_str(text)
206    }
207}
208
209fn nibble(byte: u8) -> Result<u8> {
210    match byte {
211        b'0'..=b'9' => Ok(byte - b'0'),
212        b'a'..=b'f' => Ok(byte - b'a' + 10),
213        b'A'..=b'F' => Ok(byte - b'A' + 10),
214        _ => Err(Error::InvalidObjectId),
215    }
216}
217
218#[derive(Clone, Copy, Debug, Eq, PartialEq)]
219#[repr(u8)]
220pub enum ObjectKind {
221    Commit = 1,
222    Tree = 2,
223    Blob = 3,
224    Tag = 4,
225}
226
227impl ObjectKind {
228    pub fn name(self) -> &'static str {
229        match self {
230            Self::Commit => "commit",
231            Self::Tree => "tree",
232            Self::Blob => "blob",
233            Self::Tag => "tag",
234        }
235    }
236
237    #[cfg(feature = "read")]
238    pub(crate) fn from_name(name: &[u8]) -> Result<Self> {
239        match name {
240            b"commit" => Ok(Self::Commit),
241            b"tree" => Ok(Self::Tree),
242            b"blob" => Ok(Self::Blob),
243            b"tag" => Ok(Self::Tag),
244            _ => Err(Error::InvalidObject),
245        }
246    }
247
248    #[cfg(feature = "read")]
249    pub(crate) fn from_tag_header(line: &[u8]) -> Result<Self> {
250        Self::from_name(line.strip_prefix(b"type ").ok_or(Error::InvalidObject)?)
251    }
252}
253
254#[derive(Clone, Copy, Debug)]
255pub struct Object<'a> {
256    pub kind: ObjectKind,
257    pub data: &'a [u8],
258}
259
260/// Object metadata independent of how its body is stored. Lengths are 64-bit
261/// so generated objects need not fit in one slice, including on 32-bit hosts.
262#[derive(Clone, Copy, Debug, Eq, PartialEq)]
263pub struct ObjectInfo {
264    pub kind: ObjectKind,
265    pub size: u64,
266}
267
268impl ObjectInfo {
269    /// Parse one complete canonical Git header, including its terminating NUL.
270    /// Sizes use unsigned decimal without leading zeros and fit in 64 bits.
271    #[cfg(feature = "read")]
272    pub fn from_header(header: &[u8]) -> Result<Self> {
273        let header = header.strip_suffix(&[0]).ok_or(Error::InvalidObject)?;
274        let space = header
275            .iter()
276            .position(|&byte| byte == b' ')
277            .ok_or(Error::InvalidObject)?;
278        let kind = ObjectKind::from_name(&header[..space])?;
279        let decimal = &header[space + 1..];
280        if decimal.is_empty()
281            || decimal.len() > 20
282            || !decimal[0].is_ascii_digit()
283            || (decimal.len() > 1 && decimal[0] == b'0')
284        {
285            return Err(Error::InvalidObject);
286        }
287        let size = core::str::from_utf8(decimal)
288            .map_err(|_| Error::InvalidObject)?
289            .parse()
290            .map_err(|_| Error::InvalidObject)?;
291        Ok(Self { kind, size })
292    }
293
294    /// Write the loose-object/hash header. A 32-byte buffer fits every header.
295    pub fn header<'header>(&self, storage: &'header mut [u8]) -> Result<&'header [u8]> {
296        use core::fmt::Write;
297        let mut header = SliceWriter::new(storage);
298        write!(&mut header, "{} {}\0", self.kind.name(), self.size)
299            .map_err(|_| Error::BufferTooSmall)?;
300        Ok(header.finish())
301    }
302}
303
304/// Canonical object hashing for chunked or asynchronous storage. Updates are
305/// all-or-nothing; a length mismatch never authenticates a partial object.
306#[derive(Clone)]
307pub struct ObjectDigest {
308    hash: CheckedHash,
309    remaining: u64,
310}
311
312impl ObjectDigest {
313    pub fn new(info: ObjectInfo, format: ObjectFormat) -> Self {
314        let mut hash = format.checked_hash();
315        let mut header = [0; 32];
316        hash.update(
317            info.header(&mut header)
318                .expect("object header fits in 32 bytes"),
319        );
320        Self {
321            hash,
322            remaining: info.size,
323        }
324    }
325
326    pub fn remaining(&self) -> u64 {
327        self.remaining
328    }
329
330    pub fn update(&mut self, bytes: &[u8]) -> Result<()> {
331        if bytes.len() as u64 > self.remaining {
332            return Err(Error::InvalidObject);
333        }
334        self.hash.update(bytes);
335        self.remaining -= bytes.len() as u64;
336        Ok(())
337    }
338
339    pub fn finish(&self) -> Result<ObjectId> {
340        if self.remaining != 0 {
341            return Err(Error::InvalidObject);
342        }
343        self.hash.clone().finish()
344    }
345}
346
347impl<'a> Object<'a> {
348    /// Validate already inflated loose bytes before exposing a borrowed body.
349    /// Header syntax, exact body length and the expected ID's hash must all match.
350    /// This checks the object's envelope, not its commit/tree/tag semantics.
351    #[cfg(feature = "read")]
352    pub fn from_loose(bytes: &'a [u8], expected: ObjectId) -> Result<Self> {
353        let end = bytes
354            .iter()
355            .take(32)
356            .position(|&byte| byte == 0)
357            .ok_or(Error::InvalidObject)?
358            + 1;
359        let info = ObjectInfo::from_header(&bytes[..end])?;
360        let data = &bytes[end..];
361        if data.len() as u64 != info.size || expected.format().digest(bytes)? != expected {
362            return Err(Error::InvalidObject);
363        }
364        Ok(Self {
365            kind: info.kind,
366            data,
367        })
368    }
369
370    pub fn info(&self) -> ObjectInfo {
371        ObjectInfo {
372            kind: self.kind,
373            size: self.data.len() as u64,
374        }
375    }
376
377    pub fn header<'header>(&self, storage: &'header mut [u8]) -> Result<&'header [u8]> {
378        self.info().header(storage)
379    }
380
381    pub fn id(&self) -> ObjectId {
382        self.id_with_format(ObjectFormat::Sha1)
383    }
384
385    pub fn id_with_format(&self, format: ObjectFormat) -> ObjectId {
386        // This infallible helper computes the conventional ID. Authentication
387        // uses ObjectDigest, whose fallible finish also rejects SHA-1 attacks.
388        let mut hash = format.hash();
389        hash.update(self.header(&mut [0; 32]).expect("object header fits"));
390        hash.update(self.data);
391        hash.finish()
392    }
393}
394
395#[cfg(test)]
396mod collision_tests;