Skip to main content

git_zero_core/
reader.rs

1//! Bounded object reads and resumable repository-format hashing. State is inline;
2//! callers own the body buffer and can retain state between individual steps.
3use crate::{Error, ObjectDigest, ObjectFormat, ObjectId, ObjectInfo, Repository, Result};
4
5/// Stack scratch used by convenience pack/HTTP writers for streamed bodies.
6/// The `*_with_scratch` variants let callers choose another budget.
7pub const DEFAULT_READ_BUFFER_SIZE: usize = 1024;
8
9/// A cursor over a stable repository object. It contains only a repository
10/// reference, index, metadata, and offset, with no body buffer or allocation.
11/// Use `core::mem::size_of::<ObjectReader<'_, R>>()` for the host's state budget.
12pub struct ObjectReader<'repo, R: Repository + ?Sized> {
13    repository: &'repo R,
14    index: usize,
15    info: ObjectInfo,
16    position: u64,
17}
18
19impl<'repo, R: Repository + ?Sized> ObjectReader<'repo, R> {
20    pub fn new(repository: &'repo R, index: usize) -> Result<Self> {
21        Ok(Self {
22            repository,
23            index,
24            info: repository.object_info(index)?,
25            position: 0,
26        })
27    }
28
29    pub fn info(&self) -> ObjectInfo {
30        self.info
31    }
32
33    pub fn object_format(&self) -> ObjectFormat {
34        self.repository.object_format()
35    }
36
37    pub fn position(&self) -> u64 {
38        self.position
39    }
40
41    pub fn remaining(&self) -> u64 {
42        self.info.size - self.position
43    }
44
45    /// Resume or reread at an offset within the body, including EOF.
46    pub fn seek(&mut self, position: u64) -> Result<()> {
47        if position > self.info.size {
48            return Err(Error::InvalidObject);
49        }
50        self.position = position;
51        Ok(())
52    }
53
54    /// Read one bounded chunk. Empty scratch is an error unless at EOF. Early
55    /// EOF or a provider reporting more bytes than requested is `InvalidObject`.
56    /// On error the position is unchanged, so a stable provider can be retried.
57    pub fn read(&mut self, storage: &mut [u8]) -> Result<usize> {
58        if self.remaining() == 0 {
59            return Ok(0);
60        }
61        if storage.is_empty() {
62            return Err(Error::BufferTooSmall);
63        }
64        let capacity = self.remaining().min(storage.len() as u64) as usize;
65        let length =
66            self.repository
67                .read_object(self.index, self.position, &mut storage[..capacity])?;
68        if length == 0 || length > capacity {
69            return Err(Error::InvalidObject);
70        }
71        self.position += length as u64;
72        Ok(length)
73    }
74}
75
76/// Resumable Git object hashing. Retain this value between `step` calls; it owns
77/// a cursor and hash state but no scratch. `size_of::<ObjectHasher<'_, R>>()`
78/// gives the exact inline state budget for a target. IDs include kind and size.
79pub struct ObjectHasher<'repo, R: Repository + ?Sized> {
80    reader: ObjectReader<'repo, R>,
81    hash: ObjectDigest,
82}
83
84impl<'repo, R: Repository + ?Sized> ObjectHasher<'repo, R> {
85    pub fn new(repository: &'repo R, index: usize) -> Result<Self> {
86        let reader = ObjectReader::new(repository, index)?;
87        let hash = ObjectDigest::new(reader.info(), repository.object_format());
88        Ok(Self { reader, hash })
89    }
90
91    pub fn position(&self) -> u64 {
92        self.reader.position()
93    }
94
95    /// Perform at most one provider read. `None` means more steps are needed;
96    /// `Some(id)` means the declared body is complete. Completed calls repeat
97    /// the same ID without rereading. Read errors leave the hash unchanged.
98    pub fn step(&mut self, scratch: &mut [u8]) -> Result<Option<ObjectId>> {
99        if self.reader.remaining() != 0 {
100            let length = self.reader.read(scratch)?;
101            self.hash.update(&scratch[..length])?;
102        }
103        if self.reader.remaining() == 0 {
104            Ok(Some(self.hash.finish()?))
105        } else {
106            Ok(None)
107        }
108    }
109
110    /// Hash the remaining body synchronously using only the supplied scratch.
111    pub fn finish(mut self, scratch: &mut [u8]) -> Result<ObjectId> {
112        loop {
113            if let Some(id) = self.step(scratch)? {
114                return Ok(id);
115            }
116        }
117    }
118}