mod format;
use core::ops::Range;
use std::{collections::HashMap, io::Write};
use anyhow::{Context, Result, ensure};
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(ContentReference),
}
#[derive(Debug)]
pub struct Stream {
pub chunks: Vec<Chunk>,
}
impl Stream {
pub fn new_from_frames(manifest: &[u8], tarsplit: &[u8]) -> Result<Self> {
let manifest = zstd::decode_all(manifest)?;
let manifest: Manifest = serde_json::from_slice(&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 tarsplit = String::from_utf8(zstd::decode_all(tarsplit)?)?;
let mut chunks = vec![];
for line in tarsplit.lines() {
let entry: TarSplitEntry = serde_json::from_str(line)?;
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");
chunks.push(Chunk::External(reference.clone()));
}
TarSplitEntry {
payload: Some(payload),
..
} => chunks.push(Chunk::Inline(payload)),
_ => {}
}
}
Ok(Self { chunks })
}
pub fn references(&self) -> impl Iterator<Item = &ContentReference> {
self.chunks.iter().filter_map(|chunk| {
if let Chunk::External(reference) = chunk {
Some(reference)
} else {
None
}
})
}
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(r#ref) => {
write.write_all(&resolve_reference(r#ref)?)?;
}
}
}
Ok(())
}
}
#[derive(Debug)]
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(),
}
}
}
#[derive(Debug)]
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),
})
}
pub fn from_oci<'a, S: AsRef<str> + 'a>(get: impl Fn(&str) -> Option<&'a S>) -> Option<Self> {
let manifest_digest = get("io.github.containers.zstd-chunked.manifest-checksum");
let manifest_position = get("io.github.containers.zstd-chunked.manifest-position")?;
let tarsplit_digest = get("io.github.containers.zstd-chunked.tarsplit-checksum");
let tarsplit_position = get("io.github.containers.zstd-chunked.tarsplit-position")?;
Some(Self {
manifest: match to_vec_u64(manifest_position.as_ref())?.as_slice() {
&[start, length, uncompressed_size, 1] => MetadataReference {
range: start..(start + length),
digest: manifest_digest.map(|s| s.as_ref().to_owned()),
uncompressed_size,
},
_ => None?,
},
tarsplit: match to_vec_u64(tarsplit_position.as_ref())?.as_slice() {
&[start, length, uncompressed_size] => MetadataReference {
range: start..(start + length),
digest: tarsplit_digest.map(|s| s.as_ref().to_owned()),
uncompressed_size,
},
_ => None?,
},
})
}
}