use crate::{hash, pack, zlib::stream::inflate::InflateReaderBoxed};use flate2::Decompress;use git_features::hash::Sha1;use std::{fs, io}; /// Returned by [`Iter::new_from_header()`] and as part of `Item` of [`Iter`]#[derive(thiserror::Error, Debug)]#[allow(missing_docs)]pub enum Error { #[error("An IO operation failed while streaming an entry")] Io(#[from] io::Error), #[error(transparent)] PackParse(#[from] pack::data::parse::Error), #[error("pack checksum in trailer was {expected}, but actual checksum was {actual}")] ChecksumMismatch { expected: git_hash::ObjectId, actual: git_hash::ObjectId, }, #[error("pack is incomplete: it was decompressed into {actual} bytes but {expected} bytes where expected.")] IncompletePack { actual: u64, expected: u64 },} /// An item of the iteration produced by [`Iter`]#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]pub struct Entry { /// The header of a pack entry pub header: pack::data::Header, /// The amount of bytes used to encode the `header`. `pack_offset + header_size` is the beginning of /// the compressed data in the pack. pub header_size: u16, /// The first byte of the entry at which the `header` can be read. pub pack_offset: u64, /// The bytes consumed while producing `decompressed` /// These do not contain the header, which makes it possible to easily replace a RefDelta with offset deltas /// when resolving thin packs. /// Depends on `CompressionMode` when the iterator is initialized. pub compressed: Option<Vec<u8>>, /// The amount of bytes the compressed portion of the entry takes, i.e. the portion behind behind the header. pub compressed_size: u64, /// The CRC32 over the complete entry, that is encoded header and compressed object data. /// Depends on `CompressionMode` when the iterator is initialized pub crc32: Option<u32>, /// The amount of decompressed bytes of the entry. pub decompressed_size: u64, /// Set for the last object in the iteration, providing the hash over all bytes of the iteration /// for use as trailer in a pack or to verify it matches the trailer. pub trailer: Option<git_hash::ObjectId>,} /// An iterator over [`Entries`][Entry] in a byte stream.////// The iterator used as part of [Bundle::write_stream_to_directory(…)][pack::Bundle::write_stream_to_directory()].pub struct Iter<R> { read: R, decompressor: Option<Box<Decompress>>, offset: u64, had_error: bool, kind: pack::data::Version, objects_left: u32, hash: Option<Sha1>, mode: Mode, compressed: CompressedBytesMode, compressed_buf: Option<Vec<u8>>,} /// Iteration Mode#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]pub enum Mode { /// Provide the trailer as read from the pack AsIs, /// Generate an own hash and trigger an error on the last iterated object /// if it does not match the hash provided with the pack. /// /// This way the one iterating the data cannot miss corruption as long as /// the iteration is continued through to the end. Verify, /// Generate an own hash and if there was an error or the objects are depleted early /// due to partial packs, return the last valid entry and with our own hash thus far. /// Note that the existing pack hash, if present, will be ignored. /// As we won't know which objects fails, every object will have the hash obtained thus far. /// This also means that algorithms must know about this possibility, or else might wrongfully /// assume the pack is finished. Restore,} /// Define what to do with the compressed bytes portion of a pack [`Entry`]#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]pub enum CompressedBytesMode { /// Do nothing with the compressed bytes we read Ignore, /// Only create a CRC32 of the entry, otherwise similar to `Ignore` Crc32, /// Keep them and pass them along in a newly allocated buffer Keep, /// As above, but also compute a CRC32 KeepAndCrc32,} impl CompressedBytesMode { /// Returns true if a crc32 should be computed pub fn crc32(&self) -> bool { match self { CompressedBytesMode::KeepAndCrc32 | CompressedBytesMode::Crc32 => true, CompressedBytesMode::Keep | CompressedBytesMode::Ignore => false, } } /// Returns true if compressed bytes should be kept pub fn keep(&self) -> bool { match self { CompressedBytesMode::Keep | CompressedBytesMode::KeepAndCrc32 => true, CompressedBytesMode::Ignore | CompressedBytesMode::Crc32 => false, } }} impl<R> Iter<R>where R: io::BufRead,{ /// The pack version currently being iterated pub fn kind(&self) -> pack::data::Version { self.kind } /// The kind of iteration pub fn mode(&self) -> Mode { self.mode } /// Obtain an iterator from a `read` stream to a pack data file and configure it using `mode` and `compressed`. /// /// Note that `read` is expected at the beginning of a valid pack data file with a header, entries and a trailer. pub fn new_from_header(mut read: R, mode: Mode, compressed: CompressedBytesMode) -> Result<Iter<R>, Error> { let mut header_data = [0u8; 12]; read.read_exact(&mut header_data)?; let (kind, num_objects) = pack::data::parse::header(&header_data)?; assert_eq!( kind, pack::data::Version::V2, "let's stop here if we see undocumented pack formats" ); Ok(Iter { read, decompressor: None, compressed, offset: 12, had_error: false, kind, objects_left: num_objects, hash: if mode != Mode::AsIs { let mut hash = Sha1::default(); hash.update(&header_data); Some(hash) } else { None }, mode, compressed_buf: None, }) } fn next_inner(&mut self) -> Result<Entry, Error> { self.objects_left -= 1; // even an error counts as objects // Read header let entry = match self.hash.take() { Some(hash) => { let mut read = read_and_pass_to( &mut self.read, hash::Write { inner: io::sink(), hash, }, ); let res = pack::data::Entry::from_read(&mut read, self.offset); self.hash = Some(read.write.hash); res } None => pack::data::Entry::from_read(&mut self.read, self.offset), } .map_err(Error::from)?; // Decompress object to learn it's compressed bytes let mut decompressor = self .decompressor .take() .unwrap_or_else(|| Box::new(Decompress::new(true))); let compressed_buf = self.compressed_buf.take().unwrap_or_else(|| Vec::with_capacity(4096)); decompressor.reset(true); let mut decompressed_reader = InflateReaderBoxed { inner: read_and_pass_to( &mut self.read, if self.compressed.keep() { Vec::with_capacity(entry.decompressed_size as usize) } else { compressed_buf }, ), decompressor, }; let bytes_copied = io::copy(&mut decompressed_reader, &mut io::sink())?; if bytes_copied != entry.decompressed_size { return Err(Error::IncompletePack { actual: bytes_copied, expected: entry.decompressed_size, }); } let pack_offset = self.offset; let compressed_size = decompressed_reader.decompressor.total_in(); self.offset += entry.header_size() as u64 + compressed_size; self.decompressor = Some(decompressed_reader.decompressor); let mut compressed = decompressed_reader.inner.write; debug_assert_eq!( compressed_size, compressed.len() as u64, "we must track exactly the same amount of bytes as read by the decompressor" ); if let Some(hash) = self.hash.as_mut() { hash.update(&compressed); } let crc32 = if self.compressed.crc32() { let mut header_buf = [0u8; 32]; let header_len = entry.header.to_write(bytes_copied, header_buf.as_mut())?; let state = git_features::hash::crc32_update(0, &header_buf[..header_len]); Some(git_features::hash::crc32_update(state, &compressed)) } else { None }; let compressed = if self.compressed.keep() { Some(compressed) } else { compressed.clear(); self.compressed_buf = Some(compressed); None }; // Last objects gets trailer (which is potentially verified) let trailer = if self.objects_left == 0 { let mut id = git_hash::ObjectId::from([0; 20]); if let Err(err) = self.read.read_exact(id.as_mut_slice()) { if self.mode != Mode::Restore { return Err(err.into()); } } if let Some(hash) = self.hash.take() { let actual_id = git_hash::ObjectId::from(hash.digest()); if self.mode == Mode::Restore { id = actual_id; } if id != actual_id { return Err(Error::ChecksumMismatch { actual: actual_id, expected: id, }); } } Some(id) } else if self.mode == Mode::Restore { let hash = self.hash.clone().expect("in restore mode a hash is set"); Some(git_hash::ObjectId::from(hash.digest())) } else { None }; Ok(Entry { header: entry.header, header_size: entry.header_size() as u16, compressed, compressed_size, crc32, pack_offset, decompressed_size: bytes_copied, trailer, }) }} fn read_and_pass_to<R: io::Read, W: io::Write>(read: &mut R, to: W) -> PassThrough<&mut R, W> { PassThrough { read, write: to }} impl<R> Iterator for Iter<R>where R: io::BufRead,{ type Item = Result<Entry, Error>; fn next(&mut self) -> Option<Self::Item> { if self.had_error || self.objects_left == 0 { return None; } let result = self.next_inner(); self.had_error = result.is_err(); if self.had_error { self.objects_left = 0; } if self.mode == Mode::Restore && self.had_error { None } else { Some(result) } } fn size_hint(&self) -> (usize, Option<usize>) { (self.objects_left as usize, Some(self.objects_left as usize)) }}impl<R> std::iter::ExactSizeIterator for Iter<R> where R: io::BufRead {} struct PassThrough<R, W> { read: R, write: W,} impl<R, W> io::BufRead for PassThrough<R, W>where Self: io::Read, R: io::BufRead, W: io::Write,{ fn fill_buf(&mut self) -> io::Result<&[u8]> { self.read.fill_buf() } fn consume(&mut self, amt: usize) { let buf = self .read .fill_buf() .expect("never fail as we called fill-buf before and this does nothing"); self.write .write_all(&buf[..amt]) .expect("a write to never fail - should be a memory buffer"); self.read.consume(amt) }} impl<R, W> io::Read for PassThrough<R, W>where W: io::Write, R: io::Read,{ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { let bytes_read = self.read.read(buf)?; self.write.write_all(&buf[..bytes_read])?; Ok(bytes_read) }} impl pack::data::File { /// Returns an iterator over [`Entries`][pack::data::iter::Entry], without making use of the memory mapping. pub fn streaming_iter(&self) -> Result<Iter<impl io::BufRead>, Error> { let reader = io::BufReader::with_capacity(4096 * 8, fs::File::open(&self.path)?); Iter::new_from_header(reader, Mode::Verify, CompressedBytesMode::KeepAndCrc32) }}
use crate::{hash, pack, zlib::stream::inflate::InflateReaderBoxed};
use flate2::Decompress;
use git_features::hash::Sha1;
use std::{fs, io};
/// Returned by [`Iter::new_from_header()`] and as part of `Item` of [`Iter`]
#[derive(thiserror::Error, Debug)]
#[allow(missing_docs)]
pub enum Error {
#[error("An IO operation failed while streaming an entry")]
Io(#[from] io::Error),
#[error(transparent)]
PackParse(#[from] pack::data::parse::Error),
#[error("pack checksum in trailer was {expected}, but actual checksum was {actual}")]
ChecksumMismatch {
expected: git_hash::ObjectId,
actual: git_hash::ObjectId,
},
#[error("pack is incomplete: it was decompressed into {actual} bytes but {expected} bytes where expected.")]
IncompletePack { actual: u64, expected: u64 },
}
/// An item of the iteration produced by [`Iter`]
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
pub struct Entry {
/// The header of a pack entry
pub header: pack::data::Header,
/// The amount of bytes used to encode the `header`. `pack_offset + header_size` is the beginning of
/// the compressed data in the pack.
pub header_size: u16,
/// The first byte of the entry at which the `header` can be read.
pub pack_offset: u64,
/// The bytes consumed while producing `decompressed`
/// These do not contain the header, which makes it possible to easily replace a RefDelta with offset deltas
/// when resolving thin packs.
/// Depends on `CompressionMode` when the iterator is initialized.
pub compressed: Option<Vec<u8>>,
/// The amount of bytes the compressed portion of the entry takes, i.e. the portion behind behind the header.
pub compressed_size: u64,
/// The CRC32 over the complete entry, that is encoded header and compressed object data.
/// Depends on `CompressionMode` when the iterator is initialized
pub crc32: Option<u32>,
/// The amount of decompressed bytes of the entry.
pub decompressed_size: u64,
/// Set for the last object in the iteration, providing the hash over all bytes of the iteration
/// for use as trailer in a pack or to verify it matches the trailer.
pub trailer: Option<git_hash::ObjectId>,
}
/// An iterator over [`Entries`][Entry] in a byte stream.
///
/// The iterator used as part of [Bundle::write_stream_to_directory(…)][pack::Bundle::write_stream_to_directory()].
pub struct Iter<R> {
read: R,
decompressor: Option<Box<Decompress>>,
offset: u64,
had_error: bool,
kind: pack::data::Version,
objects_left: u32,
hash: Option<Sha1>,
mode: Mode,
compressed: CompressedBytesMode,
compressed_buf: Option<Vec<u8>>,
}
/// Iteration Mode
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
pub enum Mode {
/// Provide the trailer as read from the pack
AsIs,
/// Generate an own hash and trigger an error on the last iterated object
/// if it does not match the hash provided with the pack.
///
/// This way the one iterating the data cannot miss corruption as long as
/// the iteration is continued through to the end.
Verify,
/// Generate an own hash and if there was an error or the objects are depleted early
/// due to partial packs, return the last valid entry and with our own hash thus far.
/// Note that the existing pack hash, if present, will be ignored.
/// As we won't know which objects fails, every object will have the hash obtained thus far.
/// This also means that algorithms must know about this possibility, or else might wrongfully
/// assume the pack is finished.
Restore,
}
/// Define what to do with the compressed bytes portion of a pack [`Entry`]
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
pub enum CompressedBytesMode {
/// Do nothing with the compressed bytes we read
Ignore,
/// Only create a CRC32 of the entry, otherwise similar to `Ignore`
Crc32,
/// Keep them and pass them along in a newly allocated buffer
Keep,
/// As above, but also compute a CRC32
KeepAndCrc32,
}
impl CompressedBytesMode {
/// Returns true if a crc32 should be computed
pub fn crc32(&self) -> bool {
match self {
CompressedBytesMode::KeepAndCrc32 | CompressedBytesMode::Crc32 => true,
CompressedBytesMode::Keep | CompressedBytesMode::Ignore => false,
}
}
/// Returns true if compressed bytes should be kept
pub fn keep(&self) -> bool {
match self {
CompressedBytesMode::Keep | CompressedBytesMode::KeepAndCrc32 => true,
CompressedBytesMode::Ignore | CompressedBytesMode::Crc32 => false,
}
}
}
impl<R> Iter<R>
where
R: io::BufRead,
{
/// The pack version currently being iterated
pub fn kind(&self) -> pack::data::Version {
self.kind
}
/// The kind of iteration
pub fn mode(&self) -> Mode {
self.mode
}
/// Obtain an iterator from a `read` stream to a pack data file and configure it using `mode` and `compressed`.
///
/// Note that `read` is expected at the beginning of a valid pack data file with a header, entries and a trailer.
pub fn new_from_header(mut read: R, mode: Mode, compressed: CompressedBytesMode) -> Result<Iter<R>, Error> {
let mut header_data = [0u8; 12];
read.read_exact(&mut header_data)?;
let (kind, num_objects) = pack::data::parse::header(&header_data)?;
assert_eq!(
kind,
pack::data::Version::V2,
"let's stop here if we see undocumented pack formats"
);
Ok(Iter {
read,
decompressor: None,
compressed,
offset: 12,
had_error: false,
kind,
objects_left: num_objects,
hash: if mode != Mode::AsIs {
let mut hash = Sha1::default();
hash.update(&header_data);
Some(hash)
} else {
None
},
mode,
compressed_buf: None,
})
}
fn next_inner(&mut self) -> Result<Entry, Error> {
self.objects_left -= 1; // even an error counts as objects
// Read header
let entry = match self.hash.take() {
Some(hash) => {
let mut read = read_and_pass_to(
&mut self.read,
hash::Write {
inner: io::sink(),
hash,
},
);
let res = pack::data::Entry::from_read(&mut read, self.offset);
self.hash = Some(read.write.hash);
res
}
None => pack::data::Entry::from_read(&mut self.read, self.offset),
}
.map_err(Error::from)?;
// Decompress object to learn it's compressed bytes
let mut decompressor = self
.decompressor
.take()
.unwrap_or_else(|| Box::new(Decompress::new(true)));
let compressed_buf = self.compressed_buf.take().unwrap_or_else(|| Vec::with_capacity(4096));
decompressor.reset(true);
let mut decompressed_reader = InflateReaderBoxed {
inner: read_and_pass_to(
&mut self.read,
if self.compressed.keep() {
Vec::with_capacity(entry.decompressed_size as usize)
} else {
compressed_buf
},
),
decompressor,
};
let bytes_copied = io::copy(&mut decompressed_reader, &mut io::sink())?;
if bytes_copied != entry.decompressed_size {
return Err(Error::IncompletePack {
actual: bytes_copied,
expected: entry.decompressed_size,
});
}
let pack_offset = self.offset;
let compressed_size = decompressed_reader.decompressor.total_in();
self.offset += entry.header_size() as u64 + compressed_size;
self.decompressor = Some(decompressed_reader.decompressor);
let mut compressed = decompressed_reader.inner.write;
debug_assert_eq!(
compressed_size,
compressed.len() as u64,
"we must track exactly the same amount of bytes as read by the decompressor"
);
if let Some(hash) = self.hash.as_mut() {
hash.update(&compressed);
}
let crc32 = if self.compressed.crc32() {
let mut header_buf = [0u8; 32];
let header_len = entry.header.to_write(bytes_copied, header_buf.as_mut())?;
let state = git_features::hash::crc32_update(0, &header_buf[..header_len]);
Some(git_features::hash::crc32_update(state, &compressed))
} else {
None
};
let compressed = if self.compressed.keep() {
Some(compressed)
} else {
compressed.clear();
self.compressed_buf = Some(compressed);
None
};
// Last objects gets trailer (which is potentially verified)
let trailer = if self.objects_left == 0 {
let mut id = git_hash::ObjectId::from([0; 20]);
if let Err(err) = self.read.read_exact(id.as_mut_slice()) {
if self.mode != Mode::Restore {
return Err(err.into());
}
}
if let Some(hash) = self.hash.take() {
let actual_id = git_hash::ObjectId::from(hash.digest());
if self.mode == Mode::Restore {
id = actual_id;
}
if id != actual_id {
return Err(Error::ChecksumMismatch {
actual: actual_id,
expected: id,
});
}
}
Some(id)
} else if self.mode == Mode::Restore {
let hash = self.hash.clone().expect("in restore mode a hash is set");
Some(git_hash::ObjectId::from(hash.digest()))
} else {
None
};
Ok(Entry {
header: entry.header,
header_size: entry.header_size() as u16,
compressed,
compressed_size,
crc32,
pack_offset,
decompressed_size: bytes_copied,
trailer,
})
}
}
fn read_and_pass_to<R: io::Read, W: io::Write>(read: &mut R, to: W) -> PassThrough<&mut R, W> {
PassThrough { read, write: to }
}
impl<R> Iterator for Iter<R>
where
R: io::BufRead,
{
type Item = Result<Entry, Error>;
fn next(&mut self) -> Option<Self::Item> {
if self.had_error || self.objects_left == 0 {
return None;
}
let result = self.next_inner();
self.had_error = result.is_err();
if self.had_error {
self.objects_left = 0;
}
if self.mode == Mode::Restore && self.had_error {
None
} else {
Some(result)
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.objects_left as usize, Some(self.objects_left as usize))
}
}
impl<R> std::iter::ExactSizeIterator for Iter<R> where R: io::BufRead {}
struct PassThrough<R, W> {
read: R,
write: W,
}
impl<R, W> io::BufRead for PassThrough<R, W>
where
Self: io::Read,
R: io::BufRead,
W: io::Write,
{
fn fill_buf(&mut self) -> io::Result<&[u8]> {
self.read.fill_buf()
}
fn consume(&mut self, amt: usize) {
let buf = self
.read
.fill_buf()
.expect("never fail as we called fill-buf before and this does nothing");
self.write
.write_all(&buf[..amt])
.expect("a write to never fail - should be a memory buffer");
self.read.consume(amt)
}
}
impl<R, W> io::Read for PassThrough<R, W>
where
W: io::Write,
R: io::Read,
{
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let bytes_read = self.read.read(buf)?;
self.write.write_all(&buf[..bytes_read])?;
Ok(bytes_read)
}
}
impl pack::data::File {
/// Returns an iterator over [`Entries`][pack::data::iter::Entry], without making use of the memory mapping.
pub fn streaming_iter(&self) -> Result<Iter<impl io::BufRead>, Error> {
let reader = io::BufReader::with_capacity(4096 * 8, fs::File::open(&self.path)?);
Iter::new_from_header(reader, Mode::Verify, CompressedBytesMode::KeepAndCrc32)
}
}