use super::admit::RefusalScope;
use super::admit::sevenz_refusals;
use super::decode::EntryResult;
use super::decode::decode_block_entries;
use super::decode::is_sevenz_payload;
use super::decode::skip_payload;
use super::header::BlockIndex;
use super::header::SevenZHeader;
use super::lzma2::DecodeKnobs;
use crate::ArchiveError;
use crate::EntryCompression;
use crate::Result;
use crate::SecurityConfig;
use crate::config::Validated;
use crate::copy::CopyError;
use crate::formats::common;
use crate::inspection::integrity::Charge;
use crate::inspection::integrity::DecodeFailure;
use crate::inspection::integrity::DecodeOutcome;
use crate::inspection::integrity::IntegrityPass;
use crate::inspection::integrity::Slot;
use crate::inspection::manifest::EntryPosition;
use crate::inspection::manifest::LinkTarget;
use crate::inspection::report::UnverifiedReason;
use crate::security::fs_view::LinkKind;
use crate::security::zipbomb::validate_entry_compression;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::fmt;
use std::io;
use std::io::Read;
use std::io::Seek;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StoredLinkTarget {
Read(PathBuf),
Oversized {
bytes: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkReadFailure {
BlockRefused,
Decode(LinkDecodeFailure),
Truncated,
Checksum,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkDecodeFailure {
Malformed,
DeclaredSizeOverrun,
ResourceLimit,
NotDecodable,
BlockAborted,
NotReached,
}
impl fmt::Display for LinkDecodeFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Malformed => "the compressed data is malformed",
Self::DeclaredSizeOverrun => "the data exceeds the declared size",
Self::ResourceLimit => "decoding exceeds a resource limit",
Self::NotDecodable => "the block cannot be decoded",
Self::BlockAborted => "the decoder failed before the target",
Self::NotReached => "the entry was not decoded",
})
}
}
impl LinkReadFailure {
fn from_decode(failure: DecodeFailure) -> Result<Self> {
match failure {
DecodeFailure::Checksum => Ok(Self::Checksum),
DecodeFailure::Truncated { .. } => Ok(Self::Truncated),
DecodeFailure::Io(e) => Err(ArchiveError::Io(e)),
DecodeFailure::Malformed(_) => Ok(Self::Decode(LinkDecodeFailure::Malformed)),
DecodeFailure::DeclaredSizeOverrun { .. } => {
Ok(Self::Decode(LinkDecodeFailure::DeclaredSizeOverrun))
}
DecodeFailure::ResourceLimit(_) => Ok(Self::Decode(LinkDecodeFailure::ResourceLimit)),
DecodeFailure::NotDecodable(_) => Ok(Self::Decode(LinkDecodeFailure::NotDecodable)),
}
}
pub(crate) fn into_extract_error(self, name: &str) -> ArchiveError {
let why = match self {
Self::BlockRefused => "its block is not admitted for decoding".to_owned(),
Self::Decode(failure) => failure.to_string(),
Self::Truncated => "its data ends early".to_owned(),
Self::Checksum => "checksum mismatch".to_owned(),
};
ArchiveError::InvalidArchive(format!("failed to read 7z symlink target {name}: {why}"))
}
}
pub type LinkOutcome = std::result::Result<StoredLinkTarget, LinkReadFailure>;
pub(super) fn read_sevenz_link_target(reader: &mut dyn Read, size: u64) -> Result<LinkOutcome> {
if size > common::MAX_STORED_SYMLINK_TARGET {
return Ok(Ok(StoredLinkTarget::Oversized { bytes: size }));
}
let mut bytes = Vec::new();
match (&mut *reader).take(size).read_to_end(&mut bytes) {
Ok(_) if bytes.len() as u64 == size => {}
Ok(_) => return Ok(Err(LinkReadFailure::Truncated)),
Err(e) => {
let failure = DecodeFailure::from_copy(CopyError::Read(e), size);
return LinkReadFailure::from_decode(failure).map(Err);
}
}
common::symlink_target_from_bytes(&bytes).map(|target| Ok(StoredLinkTarget::Read(target)))
}
pub(super) enum LinkStorage {
Empty,
Oversized(u64),
Stream,
}
pub(super) fn link_storage(file: &sevenz_rust2::ArchiveEntry) -> LinkStorage {
if !is_sevenz_payload(file) {
LinkStorage::Empty
} else if file.size > common::MAX_STORED_SYMLINK_TARGET {
LinkStorage::Oversized(file.size)
} else {
LinkStorage::Stream
}
}
#[derive(Debug)]
pub struct LinkTable<T> {
entries: Vec<(usize, T)>,
}
impl<T> Default for LinkTable<T> {
fn default() -> Self {
Self {
entries: Vec::new(),
}
}
}
impl<T> LinkTable<T> {
pub(crate) fn push(&mut self, file_index: usize, value: T) -> Result<()> {
if self
.entries
.last()
.is_some_and(|&(last, _)| last >= file_index)
{
return Err(ArchiveError::InvalidArchive(
"7z link entries are out of order".into(),
));
}
self.entries.push((file_index, value));
Ok(())
}
pub(crate) fn get(&self, file_index: usize) -> Option<&T> {
self.entries
.binary_search_by_key(&file_index, |&(index, _)| index)
.ok()
.map(|at| &self.entries[at].1)
}
pub(crate) fn indices(&self) -> impl Iterator<Item = usize> + '_ {
self.entries.iter().map(|&(index, _)| index)
}
}
pub fn deliver_undecoded_links(
header: &SevenZHeader,
indices: impl Iterator<Item = usize>,
each: &mut impl FnMut(usize, LinkOutcome) -> Result<()>,
) -> Result<Vec<usize>> {
let mut streamed = Vec::new();
for index in indices {
match link_storage(&header.files()[index]) {
LinkStorage::Empty => each(index, Ok(StoredLinkTarget::Read(PathBuf::new())))?,
LinkStorage::Oversized(bytes) => {
each(index, Ok(StoredLinkTarget::Oversized { bytes }))?;
}
LinkStorage::Stream => streamed.push(index),
}
}
Ok(streamed)
}
pub(super) fn blocks_closed_to_links(
header: &SevenZHeader,
compression: &[EntryCompression],
config: &SecurityConfig<Validated>,
) -> Vec<bool> {
let mut closed = vec![false; header.block_count()];
for refusal in sevenz_refusals(header, config) {
match refusal.scope {
RefusalScope::Archive => closed.fill(true),
RefusalScope::Block(index) => closed[index.get()] = true,
}
}
for (index, file) in header.files().iter().enumerate() {
if let Some(block) = header.file_block(index)
&& validate_entry_compression(&compression[index], file.size, config).is_err()
{
closed[block.get()] = true;
}
}
closed
}
pub(super) fn is_entry_checksum_error(error: &io::Error) -> bool {
error
.get_ref()
.and_then(|inner| inner.downcast_ref::<sevenz_rust2::Error>())
.is_some_and(|inner| matches!(inner, sevenz_rust2::Error::ChecksumVerificationFailed))
}
pub(super) fn drain_unwanted(
file: &sevenz_rust2::ArchiveEntry,
reader: &mut dyn Read,
) -> EntryResult {
if is_sevenz_payload(file) {
match io::copy(&mut (&mut *reader).take(file.size), &mut io::sink()) {
Ok(_) => {}
Err(e) if is_entry_checksum_error(&e) => {}
Err(e) => return Err(sevenz_rust2::Error::Io(e, "link walk".into())),
}
}
Ok(true)
}
pub fn walk_sevenz_links<R: Read + Seek, T>(
header: &SevenZHeader,
source: &mut R,
compression: &[EntryCompression],
config: &SecurityConfig<Validated>,
links: &LinkTable<T>,
each: &mut impl FnMut(usize, LinkOutcome) -> Result<()>,
) -> Result<()> {
let streamed = deliver_undecoded_links(header, links.indices(), each)?;
if streamed.is_empty() {
return Ok(());
}
let closed = blocks_closed_to_links(header, compression, config);
let mut by_block: BTreeMap<BlockIndex, Vec<usize>> = BTreeMap::new();
for index in streamed {
match header.file_block(index) {
Some(block) => by_block.entry(block).or_default().push(index),
None => each(index, Err(LinkReadFailure::BlockRefused))?,
}
}
for (block, wanted) in by_block {
if closed[block.get()] {
for index in wanted {
each(index, Err(LinkReadFailure::BlockRefused))?;
}
} else {
walk_block_links(header, source, config, block, &wanted, each)?;
}
}
Ok(())
}
pub(super) fn walk_block_links<R: Read + Seek>(
header: &SevenZHeader,
source: &mut R,
config: &SecurityConfig<Validated>,
block: BlockIndex,
wanted: &[usize],
each: &mut impl FnMut(usize, LinkOutcome) -> Result<()>,
) -> Result<()> {
let mut next_file = header.block_first_file(block);
let mut delivered = 0;
let mut abort = None;
let result = decode_block_entries(
source,
header,
block,
config.parallel_decode,
DecodeKnobs::production(),
&mut |file, reader| {
let index = next_file;
next_file += 1;
if wanted.get(delivered) != Some(&index) {
return drain_unwanted(file, reader);
}
let outcome = match read_sevenz_link_target(reader, file.size) {
Ok(outcome) => outcome,
Err(e) => {
abort = Some(e);
return Ok(false);
}
};
delivered += 1;
let aligned = !matches!(
outcome,
Err(LinkReadFailure::Truncated | LinkReadFailure::Decode(_))
);
if let Err(e) = each(index, outcome) {
abort = Some(e);
return Ok(false);
}
Ok(aligned && delivered < wanted.len())
},
);
if let Some(e) = abort {
return Err(e);
}
let failure = match result {
Err(_) => LinkReadFailure::Decode(LinkDecodeFailure::BlockAborted),
Ok(_) => LinkReadFailure::Decode(LinkDecodeFailure::NotReached),
};
for &index in &wanted[delivered..] {
each(index, Err(failure))?;
}
Ok(())
}
pub struct LinkCapture<'a> {
pub(crate) links: &'a LinkTable<EntryPosition>,
pub(crate) set: &'a mut dyn FnMut(EntryPosition, LinkTarget),
}
impl LinkCapture<'_> {
fn position(
&self,
file_index: usize,
file: &sevenz_rust2::ArchiveEntry,
) -> Option<EntryPosition> {
matches!(link_storage(file), LinkStorage::Stream)
.then(|| self.links.get(file_index).copied())
.flatten()
}
pub(super) fn mark_unreadable(
&mut self,
header: &SevenZHeader,
indices: impl Iterator<Item = usize>,
) {
for index in indices {
if let Some(position) = self.position(index, &header.files()[index]) {
(self.set)(position, LinkTarget::Unreadable);
}
}
}
}
pub(super) struct BlockScan<'a, 'c> {
pub(super) pass: &'a mut IntegrityPass<'c>,
pub(super) slots: &'a [Option<Slot>],
pub(super) links: Option<LinkCapture<'a>>,
pub(super) next_file: usize,
pub(super) visited: BTreeSet<usize>,
pub(super) stop: Option<BlockStop>,
}
#[derive(Debug)]
pub(super) enum BlockStop {
Abandoned(UnverifiedReason),
Fatal(ArchiveError),
}
impl BlockScan<'_, '_> {
pub(super) fn visit(
&mut self,
file: &sevenz_rust2::ArchiveEntry,
reader: &mut dyn Read,
) -> bool {
let file_index = self.next_file;
self.next_file += 1;
let Some(Some(slot)) = self.slots.get(file_index).copied() else {
return !is_sevenz_payload(file) || skip_payload(file, reader);
};
if !is_sevenz_payload(file) {
return true;
}
self.visited.insert(file_index);
let path = PathBuf::from(common::normalize_entry_name(&file.name));
let link_position = self
.links
.as_ref()
.and_then(|c| c.position(file_index, file));
let outcome = match link_position {
Some(position) => match self.read_link(position, slot, file, reader) {
Ok(outcome) => outcome,
Err(e) => {
self.stop = Some(BlockStop::Fatal(e));
return false;
}
},
None => self.pass.decode(slot, reader, file.size, Charge::Free),
};
let abandon = match &outcome {
DecodeOutcome::Failed(DecodeFailure::ResourceLimit(_)) => {
Some(BlockStop::Abandoned(UnverifiedReason::ResourceLimit))
}
DecodeOutcome::Verified
| DecodeOutcome::Unverified(_)
| DecodeOutcome::Failed(DecodeFailure::Checksum | DecodeFailure::Io(_)) => None,
DecodeOutcome::Failed(_) => Some(BlockStop::Abandoned(UnverifiedReason::NotDecodable)),
};
if let Err(e) = self.pass.record(slot, &path, outcome) {
self.stop = Some(BlockStop::Fatal(e));
return false;
}
let keep_going = abandon.is_none();
self.stop = abandon;
keep_going
}
fn read_link(
&mut self,
position: EntryPosition,
slot: Slot,
file: &sevenz_rust2::ArchiveEntry,
reader: &mut dyn Read,
) -> Result<DecodeOutcome> {
if !self.pass.is_admitted(slot) {
return Ok(DecodeOutcome::Unverified(UnverifiedReason::ResourceLimit));
}
let mut bytes = Vec::new();
match self.pass.copy_into(reader, &mut bytes, file.size) {
Ok(_) => {
if let Some(capture) = self.links.as_mut() {
let target = common::symlink_target_from_bytes(&bytes)?;
(capture.set)(position, LinkTarget::classify(target, LinkKind::Symlink));
}
Ok(DecodeOutcome::Verified)
}
Err(failure) => {
if let Some(capture) = self.links.as_mut() {
(capture.set)(position, LinkTarget::Unreadable);
}
Ok(DecodeOutcome::Failed(failure))
}
}
}
}