use crate::artifact_compression::{
ArtifactCompression, ArtifactPreparation, prepare_compressed_artifact,
read_compressed_artifact_range, verify_compressed_artifact,
};
use std::io::{self, Read, Seek, SeekFrom, Write};
const BUFFER_BYTES: u32 = 256 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactEncoding {
Raw,
ChunkedZstd,
}
pub struct ArtifactReader<R> {
source: R,
encoding: ArtifactEncoding,
logical_bytes: u64,
position: u64,
buffer_start: u64,
buffer: Vec<u8>,
}
impl<R: Read + Seek> ArtifactReader<R> {
pub fn new(mut source: R, encoding: ArtifactEncoding) -> io::Result<Self> {
let logical_bytes = match encoding {
ArtifactEncoding::Raw => source.seek(SeekFrom::End(0))?,
ArtifactEncoding::ChunkedZstd => read_compressed_artifact_range(&mut source, 0, 1)?.0,
};
Ok(Self {
source,
encoding,
logical_bytes,
position: 0,
buffer_start: 0,
buffer: Vec::new(),
})
}
#[must_use]
pub const fn logical_bytes(&self) -> u64 {
self.logical_bytes
}
#[must_use]
pub fn into_inner(self) -> R {
self.source
}
}
impl<R: Read + Seek> Read for ArtifactReader<R> {
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
if output.is_empty() || self.position >= self.logical_bytes {
return Ok(0);
}
if self.encoding == ArtifactEncoding::Raw {
self.source.seek(SeekFrom::Start(self.position))?;
let limit =
usize::try_from((self.logical_bytes - self.position).min(output.len() as u64))
.map_err(|_| io::Error::other("artifact range overflow"))?;
let count = self.source.read(&mut output[..limit])?;
if count == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"artifact source changed",
));
}
self.position += count as u64;
return Ok(count);
}
let buffer_end = self.buffer_start + self.buffer.len() as u64;
if self.position < self.buffer_start || self.position >= buffer_end {
self.buffer_start = self.position / u64::from(BUFFER_BYTES) * u64::from(BUFFER_BYTES);
let (total, bytes) =
read_compressed_artifact_range(&mut self.source, self.buffer_start, BUFFER_BYTES)?;
if total != self.logical_bytes {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"artifact source changed",
));
}
self.buffer = bytes;
}
let start = usize::try_from(self.position - self.buffer_start)
.map_err(|_| io::Error::other("artifact buffer overflow"))?;
let count = output.len().min(self.buffer.len() - start);
output[..count].copy_from_slice(&self.buffer[start..start + count]);
self.position += count as u64;
Ok(count)
}
}
impl<R: Read + Seek> Seek for ArtifactReader<R> {
fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
let position = match from {
SeekFrom::Start(position) => Some(position),
SeekFrom::Current(delta) => self.position.checked_add_signed(delta),
SeekFrom::End(delta) => self.logical_bytes.checked_add_signed(delta),
}
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "invalid logical artifact seek")
})?;
self.position = position;
Ok(position)
}
}
pub fn prepare_artifact_transition(
source: &mut (impl Read + Seek),
encoding: ArtifactEncoding,
candidate: &mut (impl Read + Write + Seek),
target: ArtifactCompression,
minimum_saved_bytes: u64,
mut check_cancelled: impl FnMut() -> io::Result<()>,
) -> io::Result<ArtifactTransition> {
check_cancelled()?;
let previous_bytes = source.seek(SeekFrom::End(0))?;
let mut logical = ArtifactReader::new(source, encoding)?;
let logical_bytes = logical.logical_bytes();
let preparation =
prepare_compressed_artifact(&mut logical, candidate, target, 0, &mut check_cancelled)?;
let candidate_bytes = match preparation {
ArtifactPreparation::Ready {
candidate_bytes, ..
}
| ArtifactPreparation::InsufficientSavings {
candidate_bytes, ..
} => candidate_bytes,
};
let saved_bytes = previous_bytes
.checked_sub(candidate_bytes)
.filter(|saved| *saved > 0 && *saved >= minimum_saved_bytes);
if saved_bytes.is_some()
&& matches!(preparation, ArtifactPreparation::InsufficientSavings { .. })
{
logical.rewind()?;
verify_compressed_artifact(candidate, &mut logical, &mut check_cancelled)?;
}
Ok(ArtifactTransition {
logical_bytes,
previous_bytes,
candidate_bytes,
saved_bytes,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ArtifactTransition {
pub logical_bytes: u64,
pub previous_bytes: u64,
pub candidate_bytes: u64,
pub saved_bytes: Option<u64>,
}
pub fn restore_artifact(
source: &mut (impl Read + Seek),
encoding: ArtifactEncoding,
destination: &mut (impl Write + Seek),
mut check_cancelled: impl FnMut() -> io::Result<()>,
) -> io::Result<u64> {
check_cancelled()?;
if destination.seek(SeekFrom::End(0))? != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"artifact destination must be empty",
));
}
let mut reader = ArtifactReader::new(source, encoding)?;
let mut buffer = vec![0; BUFFER_BYTES as usize];
loop {
check_cancelled()?;
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
destination.write_all(&buffer[..count])?;
}
destination.flush()?;
Ok(reader.logical_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn fixture() -> Vec<u8> {
let line = "terminal output: café 世界 🦀\n";
line.repeat(40_000).into_bytes()
}
#[test]
fn raw_to_light_to_deep_and_restore_are_byte_exact() {
let bytes = fixture();
let mut raw = Cursor::new(bytes.clone());
let mut light = Cursor::new(Vec::new());
let first = prepare_artifact_transition(
&mut raw,
ArtifactEncoding::Raw,
&mut light,
ArtifactCompression::Light,
1,
|| Ok(()),
)
.expect("light");
assert!(first.saved_bytes.is_some());
let mut deep = Cursor::new(Vec::new());
let next = prepare_artifact_transition(
&mut light,
ArtifactEncoding::ChunkedZstd,
&mut deep,
ArtifactCompression::Deep,
1,
|| Ok(()),
)
.expect("deep");
assert_eq!(next.previous_bytes, first.candidate_bytes);
assert_eq!(next.logical_bytes, bytes.len() as u64);
let mut restored = Cursor::new(Vec::new());
assert_eq!(
restore_artifact(
&mut deep,
ArtifactEncoding::ChunkedZstd,
&mut restored,
|| Ok(())
)
.expect("restore"),
bytes.len() as u64
);
assert_eq!(restored.into_inner(), bytes);
assert_eq!(raw.into_inner(), bytes);
}
#[test]
fn seeks_and_small_reads_preserve_original_offsets() {
let bytes = fixture();
let mut compressed = Cursor::new(Vec::new());
prepare_artifact_transition(
&mut Cursor::new(&bytes),
ArtifactEncoding::Raw,
&mut compressed,
ArtifactCompression::Light,
1,
|| Ok(()),
)
.expect("prepare");
let mut reader =
ArtifactReader::new(compressed, ArtifactEncoding::ChunkedZstd).expect("reader");
for start in [
0,
19,
BUFFER_BYTES as usize - 3,
BUFFER_BYTES as usize + 7,
bytes.len() - 10,
] {
reader.seek(SeekFrom::Start(start as u64)).expect("seek");
let mut actual = [0; 10];
reader.read_exact(&mut actual).expect("read");
assert_eq!(actual, bytes[start..start + 10]);
}
reader.seek(SeekFrom::End(10)).expect("past eof");
assert_eq!(reader.read(&mut [0; 1]).expect("eof"), 0);
reader.rewind().expect("rewind");
assert!(reader.seek(SeekFrom::Current(-1)).is_err());
}
#[test]
fn repeated_tier_does_not_claim_logical_size_as_savings() {
let bytes = fixture();
let mut first = Cursor::new(Vec::new());
prepare_artifact_transition(
&mut Cursor::new(bytes),
ArtifactEncoding::Raw,
&mut first,
ArtifactCompression::Light,
1,
|| Ok(()),
)
.expect("first");
let mut second = Cursor::new(Vec::new());
let result = prepare_artifact_transition(
&mut first,
ArtifactEncoding::ChunkedZstd,
&mut second,
ArtifactCompression::Light,
1,
|| Ok(()),
)
.expect("second");
assert_eq!(result.saved_bytes, None);
assert_eq!(first.into_inner(), second.into_inner());
}
#[test]
fn explicit_encoding_and_cancellation_fail_closed() {
let bytes = b"not a compressed container";
assert!(ArtifactReader::new(Cursor::new(bytes), ArtifactEncoding::ChunkedZstd).is_err());
let mut raw = ArtifactReader::new(Cursor::new(bytes), ArtifactEncoding::Raw).expect("raw");
let mut actual = Vec::new();
raw.read_to_end(&mut actual).expect("read");
assert_eq!(actual, bytes);
let mut destination = Cursor::new(Vec::new());
assert!(
restore_artifact(
&mut Cursor::new(bytes),
ArtifactEncoding::Raw,
&mut destination,
|| Err(io::Error::new(io::ErrorKind::Interrupted, "cancelled"))
)
.is_err()
);
assert!(destination.into_inner().is_empty());
}
}