mod format;
use core::ops::Range;
use std::{
collections::HashMap,
io::{Read, Write},
};
use anyhow::{Context, Result, ensure};
use zstd::stream::Decoder;
use self::format::{Footer, FooterReference, Manifest, TarSplitEntry};
#[derive(Debug, Clone)]
pub struct ContentReference {
pub range: Range<u64>,
pub digest: String,
pub size: u64,
}
#[derive(Debug, Clone)]
pub enum Chunk {
Inline(Box<[u8]>),
External(Box<[ContentReference]>),
}
#[derive(Debug)]
pub struct Stream {
pub chunks: Vec<Chunk>,
}
impl Stream {
pub fn new_from_frames(manifest: impl Read, tarsplit: impl Read) -> Result<Self> {
let manifest: Manifest = serde_json::from_reader(Decoder::new(manifest)?)?;
ensure!(
manifest.version == 1,
"Incorrect zstd:chunked CRFS manifest version"
);
let manifest_entries: HashMap<String, ContentReference> = manifest
.entries
.into_iter()
.filter_map(|entry| {
Some((
entry.name,
ContentReference {
digest: entry.digest?,
size: entry.size?,
range: entry.offset?..entry.end_offset?,
},
))
})
.collect();
let chunks = serde_json::Deserializer::from_reader(Decoder::new(tarsplit)?)
.into_iter()
.map(|entry| {
Ok(match entry? {
TarSplitEntry {
name: Some(name),
size: Some(size),
.. } => {
let reference = manifest_entries.get(&name)
.with_context(|| format!("Filename {name} in zstd:chunked tarsplit missing from manifest"))?;
ensure!(size == reference.size, "size mismatch");
Some(Chunk::External(Box::from([reference.clone()])))
}
TarSplitEntry {
payload: Some(payload),
..
} => Some(Chunk::Inline(payload)),
_ => None,
})
})
.filter_map(Result::transpose)
.collect::<Result<Vec<_>>>()?;
Ok(Self { chunks })
}
pub fn references(&self) -> impl Iterator<Item = &ContentReference> {
self.chunks.iter().flat_map(|chunk| {
if let Chunk::External(items) = chunk {
items.as_ref()
} else {
&[]
}
})
}
pub fn write_to(
&self,
write: &mut impl Write,
resolve_reference: impl Fn(&ContentReference) -> Result<Vec<u8>>,
) -> Result<()> {
for chunk in &self.chunks {
match chunk {
Chunk::Inline(data) => {
write.write_all(data)?;
}
Chunk::External(refs) => {
for r#ref in refs {
write.write_all(&resolve_reference(r#ref)?)?;
}
}
}
}
Ok(())
}
}
pub struct MetadataReference {
pub range: Range<u64>,
pub digest: Option<String>,
pub uncompressed_size: u64,
}
impl MetadataReference {
const fn from_footer(value: &FooterReference) -> Self {
let start = value.offset.get();
let end = start + value.length_compressed.get();
Self {
range: start..end,
digest: None,
uncompressed_size: value.length_uncompressed.get(),
}
}
}
pub struct MetadataReferences {
pub manifest: MetadataReference,
pub tarsplit: MetadataReference,
}
fn to_vec_u64(value: &str) -> Option<Vec<u64>> {
value.split(':').map(|s| s.parse().ok()).collect()
}
impl MetadataReferences {
#[must_use]
pub fn from_footer(suffix: &[u8]) -> Option<Self> {
let footer = Footer::from_suffix(suffix)?;
Some(Self {
manifest: MetadataReference::from_footer(&footer.manifest),
tarsplit: MetadataReference::from_footer(&footer.tarsplit),
})
}
#[allow(clippy::needless_pass_by_value)]
#[must_use]
pub fn from_oci(annotations: HashMap<String, String>) -> Option<Self> {
let manifest_digest =
annotations.get("io.github.containers.zstd-chunked.manifest-checksum");
let manifest_position =
annotations.get("io.github.containers.zstd-chunked.manifest-position")?;
let tarsplit_digest =
annotations.get("io.github.containers.zstd-chunked.tarsplit-checksum");
let tarsplit_position =
annotations.get("io.github.containers.zstd-chunked.tarsplit-position")?;
Some(Self {
manifest: match to_vec_u64(manifest_position)?.as_slice() {
&[start, length, uncompressed_size, 1] => MetadataReference {
range: start..(start + length),
digest: manifest_digest.cloned(),
uncompressed_size,
},
_ => None?,
},
tarsplit: match to_vec_u64(tarsplit_position)?.as_slice() {
&[start, length, uncompressed_size] => MetadataReference {
range: start..(start + length),
digest: tarsplit_digest.cloned(),
uncompressed_size,
},
_ => None?,
},
})
}
}