use std::cell::Cell;
use std::collections::BTreeMap;
use std::io::Read;
use std::path::{Component, Path};
use serde::Deserialize;
use super::{ContentError, ContentLock};
use crate::integrity::Sha256Digest;
pub(super) const MANIFEST_ENTRY: &str = "bundle-manifest.toml";
pub const MAX_BUNDLE_BYTES: u64 = 64 * 1024 * 1024;
pub const MAX_UNPACKED_BYTES: u64 = 256 * 1024 * 1024;
pub const MAX_BUNDLE_ENTRIES: usize = 20_000;
const STREAM_SLACK_PER_ENTRY: u64 = 4 * 1024;
#[derive(Debug, Clone, Copy)]
pub(super) struct Limits {
pub bundle_bytes: u64,
pub unpacked_bytes: u64,
pub entries: usize,
}
impl Limits {
pub(super) const DEFAULT: Self = Self {
bundle_bytes: MAX_BUNDLE_BYTES,
unpacked_bytes: MAX_UNPACKED_BYTES,
entries: MAX_BUNDLE_ENTRIES,
};
fn stream_bytes(self) -> u64 {
let entries = u64::try_from(self.entries).unwrap_or(u64::MAX);
self.unpacked_bytes.saturating_add(
entries
.saturating_add(1)
.saturating_mul(STREAM_SLACK_PER_ENTRY),
)
}
}
struct CappedReader<'c, R> {
inner: R,
read: &'c Cell<u64>,
cap: u64,
}
impl<R: Read> Read for CappedReader<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let over = || std::io::Error::other("decompressed stream over its cap");
let used = self.read.get();
if used > self.cap {
return Err(over());
}
let room = usize::try_from((self.cap - used).saturating_add(1)).unwrap_or(usize::MAX);
let len = buf.len().min(room);
let n = self.inner.read(&mut buf[..len])?;
let used = used + n as u64;
self.read.set(used);
if used > self.cap { Err(over()) } else { Ok(n) }
}
}
pub const SUPPORTED_SCHEMA_MAJOR: u32 = 1;
#[derive(Deserialize)]
struct SchemaProbe {
schema_major: Option<u32>,
}
#[derive(Deserialize)]
struct BundleManifest {
tag: String,
}
enum UnpackError {
Corrupt(String),
TooLarge(String),
}
pub(super) fn load_verified(
cache_dir: &Path,
lock: &ContentLock,
) -> Result<BTreeMap<String, Vec<u8>>, ContentError> {
load_verified_with(cache_dir, lock, Limits::DEFAULT)
}
pub(super) fn load_verified_with(
cache_dir: &Path,
lock: &ContentLock,
limits: Limits,
) -> Result<BTreeMap<String, Vec<u8>>, ContentError> {
let path = cache_dir.join(lock.bundle_file_name());
let bytes = read_capped(&path, limits.bundle_bytes)?;
let actual = Sha256Digest::of_bytes(&bytes);
actual
.verify(lock.sha256())
.map_err(|_| ContentError::ChecksumMismatch {
path: path.clone(),
expected: lock.sha256().clone(),
actual: actual.clone(),
})?;
let corrupt = |reason: String| ContentError::BundleCorrupt {
path: path.clone(),
reason,
};
let entries = unpack(&bytes, limits).map_err(|e| match e {
UnpackError::Corrupt(reason) => corrupt(reason),
UnpackError::TooLarge(reason) => ContentError::BundleTooLarge {
path: path.clone(),
reason,
},
})?;
let manifest = entries
.get(MANIFEST_ENTRY)
.ok_or_else(|| corrupt(format!("no {MANIFEST_ENTRY} entry")))?;
let manifest = std::str::from_utf8(manifest)
.map_err(|e| corrupt(format!("{MANIFEST_ENTRY} is not UTF-8: {e}")))?;
let probe: SchemaProbe =
toml::from_str(manifest).map_err(|e| corrupt(format!("{MANIFEST_ENTRY}: {e}")))?;
let schema_major = probe
.schema_major
.ok_or_else(|| corrupt(format!("{MANIFEST_ENTRY} has no schema_major")))?;
if schema_major > SUPPORTED_SCHEMA_MAJOR {
return Err(ContentError::UnsupportedSchema {
path,
bundle: schema_major,
supported: SUPPORTED_SCHEMA_MAJOR,
});
}
let manifest: BundleManifest =
toml::from_str(manifest).map_err(|e| corrupt(format!("{MANIFEST_ENTRY}: {e}")))?;
if manifest.tag != lock.tag() {
return Err(ContentError::TagMismatch {
lock_tag: lock.tag().to_owned(),
bundle_tag: manifest.tag,
});
}
Ok(entries)
}
fn read_capped(path: &Path, cap: u64) -> Result<Vec<u8>, ContentError> {
let unreadable = |source| ContentError::BundleUnreadable {
path: path.to_path_buf(),
source,
};
let too_large = |len: u64| ContentError::BundleTooLarge {
path: path.to_path_buf(),
reason: format!("the file is {len} bytes, over the {cap}-byte cap"),
};
let file = match std::fs::File::open(path) {
Ok(file) => file,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(ContentError::BundleMissing {
path: path.to_path_buf(),
});
}
Err(source) => return Err(unreadable(source)),
};
let len = file.metadata().map_err(unreadable)?.len();
if len > cap {
return Err(too_large(len));
}
let mut bytes = Vec::with_capacity(usize::try_from(len).unwrap_or(0));
file.take(cap.saturating_add(1))
.read_to_end(&mut bytes)
.map_err(unreadable)?;
let read = bytes.len() as u64;
if read > cap {
return Err(too_large(read));
}
Ok(bytes)
}
fn unpack(bytes: &[u8], limits: Limits) -> Result<BTreeMap<String, Vec<u8>>, UnpackError> {
let streamed = Cell::new(0u64);
let stream_cap = limits.stream_bytes();
let io_err = |e: std::io::Error| {
if streamed.get() > stream_cap {
UnpackError::TooLarge(format!(
"the decompressed stream is over {stream_cap} bytes"
))
} else {
UnpackError::Corrupt(e.to_string())
}
};
let mut archive = tar::Archive::new(CappedReader {
inner: flate2::read::GzDecoder::new(bytes),
read: &streamed,
cap: stream_cap,
});
let mut files = BTreeMap::new();
let (mut count, mut total) = (0usize, 0u64);
for entry in archive.entries().map_err(io_err)? {
let mut entry = entry.map_err(io_err)?;
count += 1;
if count > limits.entries {
return Err(UnpackError::TooLarge(format!(
"more than {} entries",
limits.entries
)));
}
let before = total;
total = total.saturating_add(entry.size());
if total > limits.unpacked_bytes {
return Err(UnpackError::TooLarge(format!(
"entries declare more than {} bytes",
limits.unpacked_bytes
)));
}
let kind = entry.header().entry_type();
let raw = entry.path().map_err(io_err)?.into_owned();
let name = relative_key(&raw).ok_or_else(|| {
UnpackError::Corrupt(format!("entry {} is not a relative path", raw.display()))
})?;
if kind.is_dir() {
continue;
}
if !kind.is_file() {
return Err(UnpackError::Corrupt(format!(
"entry {name} is not a regular file"
)));
}
let room = limits.unpacked_bytes - before;
let mut data = Vec::new();
(&mut entry)
.take(room.saturating_add(1))
.read_to_end(&mut data)
.map_err(io_err)?;
if data.len() as u64 > room {
return Err(UnpackError::TooLarge(format!(
"entry {name} holds more than {room} bytes"
)));
}
if files.insert(name.clone(), data).is_some() {
return Err(UnpackError::Corrupt(format!("entry {name} appears twice")));
}
}
Ok(files)
}
pub(super) fn relative_key(path: &Path) -> Option<String> {
let mut parts = Vec::new();
for component in path.components() {
match component {
Component::Normal(part) => parts.push(part.to_str()?.to_owned()),
Component::CurDir => {}
Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
}
}
if parts.is_empty() {
None
} else {
Some(parts.join("/"))
}
}