#[cfg(test)]
mod tests;
use std::{collections::TryReserveError, fmt, io};
use crate::artifact::{self, ArtifactError, ArtifactIdentity, Sha256Digest};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ArchiveLimits {
pub archive_bytes: usize,
pub decompressed_bytes: usize,
pub members: u32,
pub member_bytes: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ArchiveResource {
Members,
MemberBytes,
}
#[derive(Debug)]
pub enum ArchiveError {
InvalidMember,
Archive(ArtifactError),
Decompression(ArtifactError),
TrailingCompressedData,
Tar(io::Error),
LimitExceeded {
resource: ArchiveResource,
limit: u64,
},
UnsupportedEntry {
entry: u32,
kind: u8,
},
TruncatedEntry {
entry: u32,
},
InvalidPadding,
MissingMember,
DuplicateMember,
Member(ArtifactError),
Allocation(TryReserveError),
}
impl fmt::Display for ArchiveError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidMember => {
f.write_str("archive member must be a canonical relative POSIX path")
}
Self::Archive(_) => f.write_str("compressed archive admission failed"),
Self::Decompression(_) => f.write_str("bounded gzip decoding failed"),
Self::TrailingCompressedData => f.write_str("bytes follow the single gzip stream"),
Self::Tar(_) => f.write_str("tar decoding failed"),
Self::LimitExceeded { resource, limit } => {
write!(f, "archive {resource:?} limit {limit} exceeded")
}
Self::UnsupportedEntry { entry, kind } => {
write!(f, "unsupported tar type {kind} at record {entry}")
}
Self::TruncatedEntry { entry } => write!(f, "truncated tar record {entry}"),
Self::InvalidPadding => f.write_str("invalid tar end markers or padding"),
Self::MissingMember => f.write_str("selected tar member is missing"),
Self::DuplicateMember => f.write_str("selected tar member is duplicated"),
Self::Member(_) => f.write_str("selected tar member admission failed"),
Self::Allocation(_) => f.write_str("selected tar member allocation failed"),
}
}
}
impl std::error::Error for ArchiveError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Archive(source) | Self::Decompression(source) | Self::Member(source) => {
Some(source)
}
Self::Tar(source) => Some(source),
Self::Allocation(source) => Some(source),
_ => None,
}
}
}
pub struct ExtractedMember {
pub bytes: Vec<u8>,
pub archive_identity: ArtifactIdentity,
pub member_identity: ArtifactIdentity,
}
impl fmt::Debug for ExtractedMember {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExtractedMember")
.field("archive_identity", &self.archive_identity)
.field("member_identity", &self.member_identity)
.finish_non_exhaustive()
}
}
pub fn extract_tar_gz(
bytes: &[u8],
archive_sha256: Sha256Digest,
member: &str,
member_sha256: Sha256Digest,
limits: ArchiveLimits,
) -> Result<ExtractedMember, ArchiveError> {
if !valid_member(member) {
return Err(ArchiveError::InvalidMember);
}
let archive_identity =
artifact::verify_reader(bytes, limits.archive_bytes as u64, archive_sha256)
.map_err(ArchiveError::Archive)?;
let mut decoder = flate2::bufread::GzDecoder::new(bytes);
let inflated = artifact::read_reader(&mut decoder, limits.decompressed_bytes)
.map_err(ArchiveError::Decompression)?;
if !decoder.into_inner().is_empty() {
return Err(ArchiveError::TrailingCompressedData);
}
let payload = select_member(&inflated, member, limits)?;
let member_identity =
artifact::verify_reader(payload, limits.member_bytes as u64, member_sha256)
.map_err(ArchiveError::Member)?;
let mut selected = Vec::new();
selected
.try_reserve_exact(payload.len())
.map_err(ArchiveError::Allocation)?;
selected.extend_from_slice(payload);
Ok(ExtractedMember {
bytes: selected,
archive_identity,
member_identity,
})
}
fn valid_member(member: &str) -> bool {
!member.is_empty()
&& !member.bytes().any(|byte| matches!(byte, 0 | b'\\' | b':'))
&& member
.split('/')
.all(|component| !matches!(component, "" | "." | ".."))
}
fn select_member<'a>(
bytes: &'a [u8],
member: &str,
limits: ArchiveLimits,
) -> Result<&'a [u8], ArchiveError> {
let mut archive = tar::Archive::new(bytes);
let entries = archive.entries().map_err(ArchiveError::Tar)?.raw(true);
let mut next = 0_usize;
let mut selected = None;
for (index, entry) in entries.enumerate() {
let entry = entry.map_err(ArchiveError::Tar)?;
let count = u32::try_from(index)
.ok()
.and_then(|count| count.checked_add(1))
.filter(|count| *count <= limits.members)
.ok_or_else(|| ArchiveError::LimitExceeded {
resource: ArchiveResource::Members,
limit: u64::from(limits.members),
})?;
let kind = entry.header().entry_type();
if !kind.is_file() && !kind.is_dir() {
return Err(ArchiveError::UnsupportedEntry {
entry: count,
kind: kind.as_byte(),
});
}
let start = usize::try_from(entry.raw_file_position())
.map_err(|_| ArchiveError::TruncatedEntry { entry: count })?;
let size = usize::try_from(entry.size())
.map_err(|_| ArchiveError::TruncatedEntry { entry: count })?;
let end = start
.checked_add(size)
.ok_or(ArchiveError::TruncatedEntry { entry: count })?;
next = end
.checked_add(511)
.map(|value| value & !511)
.filter(|value| *value <= bytes.len())
.ok_or(ArchiveError::TruncatedEntry { entry: count })?;
if header_matches(entry.header(), member.as_bytes()) {
if selected.is_some() {
return Err(ArchiveError::DuplicateMember);
}
if !kind.is_file() {
return Err(ArchiveError::UnsupportedEntry {
entry: count,
kind: kind.as_byte(),
});
}
if size > limits.member_bytes {
return Err(ArchiveError::LimitExceeded {
resource: ArchiveResource::MemberBytes,
limit: limits.member_bytes as u64,
});
}
selected = Some(
bytes
.get(start..end)
.ok_or(ArchiveError::TruncatedEntry { entry: count })?,
);
}
}
let padding = &bytes[next..];
if padding.len() < 1024
|| !padding.len().is_multiple_of(512)
|| padding.iter().any(|byte| *byte != 0)
{
return Err(ArchiveError::InvalidPadding);
}
selected.ok_or(ArchiveError::MissingMember)
}
fn header_matches(header: &tar::Header, member: &[u8]) -> bool {
let name = header
.as_old()
.name
.split(|byte| *byte == 0)
.next()
.unwrap_or(&[]);
if let Some(ustar) = header.as_ustar() {
let prefix = ustar.prefix.split(|byte| *byte == 0).next().unwrap_or(&[]);
if !prefix.is_empty() {
return member
.strip_prefix(prefix)
.and_then(|rest| rest.strip_prefix(b"/"))
== Some(name);
}
}
member == name
}