use super::common;
use super::traits::ArchiveFormat;
use crate::ArchiveError;
use crate::EntryCompression;
use crate::ExtractionOptions;
use crate::ExtractionReport;
use crate::ProgressCallback;
use crate::Result;
use crate::SecurityConfig;
use crate::VerificationOptions;
use crate::config::Validated;
use crate::copy::CopyBuffer;
use crate::error::QuotaResource;
use crate::inspection::report::IssueCategory;
use crate::inspection::report::IssueSeverity;
use crate::inspection::report::VerificationIssue;
use crate::security::EntryValidator;
use crate::security::validator::EntryPath;
use crate::security::validator::ValidatedEntryType;
use crate::security::zipbomb::validate_entry_compression;
use crate::types::DestDir;
use crate::types::EntryType;
use crate::types::safe_symlink::require_symlinks_allowed;
use classify::abort_with;
use classify::reparse_point_refusal;
use decode::decode_archive_entries;
use links::read_sevenz_link_target;
use lzma2::DecodeKnobs;
use std::io;
use std::io::ErrorKind;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::path::Path;
use write::lstat_dest;
use write::refuse_undecoded_target;
use write::write_file_direct;
use write::write_file_with_permit;
#[derive(Debug, Clone)]
struct CachedEntry {
name: String,
size: u64,
class: SevenZClass,
compression: EntryCompression,
}
const SIGNATURE_HEADER_SIZE: u64 = 32;
#[derive(Debug)]
pub struct SevenZArchive<R: Read + Seek> {
source: R,
entries: Vec<CachedEntry>,
header: SevenZHeader,
}
impl<R: Read + Seek> SevenZArchive<R> {
pub fn new(mut source: R) -> Result<Self> {
let header = read_sevenz_header(&mut source)?;
let source_len = source.seek(SeekFrom::End(0)).map_err(ArchiveError::Io)?;
let compression = sevenz_entry_compression(&header, source_len)?;
let entries: Vec<CachedEntry> = header
.files()
.iter()
.zip(compression)
.map(|(e, compression)| CachedEntry {
name: e.name.clone(),
size: e.size,
class: SevenZClass::of(e),
compression,
})
.collect();
source.rewind().map_err(ArchiveError::Io)?;
Ok(Self {
source,
entries,
header,
})
}
}
impl<R: Read + Seek> SevenZArchive<R> {
fn anti_item_count(&self) -> u64 {
let count = self
.entries
.iter()
.filter(|entry| entry.class == SevenZClass::AntiItem)
.count();
u64::try_from(count).unwrap_or(u64::MAX)
}
fn validate_stream_symlink(
entry: &sevenz_rust2::ArchiveEntry,
reader: &mut dyn Read,
entry_path: &Path,
validator: &mut EntryValidator,
) -> Result<crate::security::validator::ValidatedEntry> {
let target = match read_sevenz_link_target(reader, entry.size)? {
Ok(StoredLinkTarget::Read(target)) => target,
Ok(StoredLinkTarget::Oversized { bytes }) => {
return Err(common::symlink_target_too_large(bytes));
}
Err(failure) => return Err(failure.into_extract_error(&entry.name)),
};
validator.validate_entry(
entry_path,
&EntryType::Symlink { target },
entry.size,
None,
None,
None,
)
}
#[expect(
clippy::too_many_arguments,
reason = "arguments are threaded through from the caller; a parameter struct adds indirection"
)]
fn process_entry_inner(
entry: &sevenz_rust2::ArchiveEntry,
kind: SevenZKind,
reader: &mut dyn Read,
entry_path: &std::path::Path,
validator: &mut EntryValidator,
dest: &DestDir,
report: &mut ExtractionReport,
dir_cache: &mut common::DirCache,
skip_duplicates: bool,
config: &SecurityConfig<Validated>,
duplicate_skips: &mut u64,
disallowed_extension_skips: &mut u64,
pending_error: &mut Option<ArchiveError>,
copy_buffer: &mut CopyBuffer,
) -> std::result::Result<u64, sevenz_rust2::Error> {
match kind {
SevenZKind::ReparsePoint => {
return Err(abort_with(
pending_error,
reparse_point_refusal(&entry.name),
));
}
SevenZKind::UnixSymlink => {
let validated = Self::validate_stream_symlink(entry, reader, entry_path, validator)
.map_err(|e| abort_with(pending_error, e))?;
if let ValidatedEntryType::Symlink(symlink) = validated.entry_type() {
common::create_symlink(
symlink,
dest,
report,
dir_cache,
skip_duplicates,
duplicate_skips,
)
.map_err(|e| abort_with(pending_error, e))?;
}
return Ok(0);
}
SevenZKind::File | SevenZKind::Directory => {}
}
if kind == SevenZKind::File
&& !common::check_extension_allowed(
entry_path,
config,
report,
disallowed_extension_skips,
)
{
return Ok(0);
}
let safe_path = validator
.validate_entry_path(entry_path, kind.node_kind(), None)
.map_err(|e| abort_with(pending_error, e))?
.into_inner();
let dest_path = dest.join_path(safe_path.as_path());
match kind {
SevenZKind::Directory => {
dir_cache.ensure_dir(&dest_path)?;
report.directories_created += 1;
Ok(0)
}
SevenZKind::File => {
dir_cache.ensure_parent_dir(&dest_path)?;
let existing = lstat_dest(&dest_path)?;
if existing.is_some() && skip_duplicates {
report.files_skipped = report
.files_skipped
.checked_add(1)
.ok_or_else(|| abort_with(pending_error, counter_overflow()))?;
*duplicate_skips = duplicate_skips.saturating_add(1);
return Ok(0);
}
#[cfg(unix)]
if let Some(meta) = &existing
&& meta.file_type().is_symlink()
{
return Err(common::symlink_at_dest_error(safe_path.as_path()).into());
}
if let Some(meta) = &existing
&& meta.is_dir()
{
*pending_error = Some(ArchiveError::Io(std::io::Error::from(
ErrorKind::IsADirectory,
)));
return Err(sevenz_rust2::Error::Other(
"destination path is a pre-existing directory".into(),
));
}
let permit = validator
.reserve_file(entry.size)
.map_err(|e| abort_with(pending_error, e))?;
let bytes_written = if existing.is_some() {
std::fs::remove_file(&dest_path)?;
write_file_with_permit(reader, &dest_path, entry.size, copy_buffer, permit)
} else {
write_file_direct(reader, &dest_path, entry.size, copy_buffer, permit)
}
.map_err(|e| abort_with(pending_error, e))?;
report.bytes_written = report
.bytes_written
.checked_add(bytes_written)
.ok_or_else(|| abort_with(pending_error, counter_overflow()))?;
report.files_extracted += 1;
Ok(bytes_written)
}
SevenZKind::ReparsePoint | SevenZKind::UnixSymlink => Err(sevenz_rust2::Error::Other(
"link entries are handled before path validation".into(),
)),
}
}
#[expect(
clippy::too_many_arguments,
reason = "arguments are threaded through from the caller; a parameter struct adds indirection"
)]
fn extract_with_callback(
source: &mut R,
header: &SevenZHeader,
dest: &DestDir,
validator: &mut EntryValidator,
dir_cache: &mut common::DirCache,
skip_duplicates: bool,
progress: &mut dyn ProgressCallback,
total: usize,
anti_items: u64,
config: &SecurityConfig<Validated>,
) -> Result<ExtractionReport> {
struct SzContext<'a> {
report: ExtractionReport,
dir_cache: &'a mut common::DirCache,
progress: &'a mut dyn ProgressCallback,
current_idx: usize,
duplicate_skips: u64,
disallowed_extension_skips: u64,
pending_error: Option<ArchiveError>,
copy_buffer: CopyBuffer,
}
let mut ctx = SzContext {
report: ExtractionReport::new(),
dir_cache,
progress,
current_idx: 0,
duplicate_skips: 0,
disallowed_extension_skips: 0,
pending_error: None,
copy_buffer: CopyBuffer::new(),
};
let is_solid = header.is_solid();
let mut extract_fn = |entry: &sevenz_rust2::ArchiveEntry,
reader: &mut dyn Read|
-> std::result::Result<bool, sevenz_rust2::Error> {
let kind = match SevenZClass::of(entry).present(&entry.name) {
Ok(Some(kind)) => kind,
Ok(None) => return Ok(true),
Err(refusal) => return Err(abort_with(&mut ctx.pending_error, refusal)),
};
let entry_path = std::path::PathBuf::from(common::normalize_entry_name(&entry.name));
ctx.current_idx = ctx.current_idx.saturating_add(1);
let idx = ctx.current_idx;
ctx.progress
.on_entry_start(entry_path.as_path(), total, idx);
let result = Self::process_entry_inner(
entry,
kind,
reader,
&entry_path,
validator,
dest,
&mut ctx.report,
ctx.dir_cache,
skip_duplicates,
config,
&mut ctx.duplicate_skips,
&mut ctx.disallowed_extension_skips,
&mut ctx.pending_error,
&mut ctx.copy_buffer,
);
let result = if is_solid {
result.and_then(|written| {
io::copy(&mut (&mut *reader).take(entry.size), &mut io::sink())?;
Ok(written)
})
} else {
result
};
match result {
Ok(bytes_written) => {
if bytes_written > 0 {
ctx.progress.on_bytes_written(bytes_written);
}
ctx.progress.on_entry_complete(entry_path.as_path());
Ok(true)
}
Err(e) => {
ctx.progress.on_entry_complete(entry_path.as_path());
Err(e)
}
}
};
let result = decode_archive_entries(
source,
header,
config.parallel_decode,
DecodeKnobs::production(),
&mut extract_fn,
);
let mut accumulated = ctx.report;
push_skip_warnings(
&mut accumulated,
(ctx.duplicate_skips, ctx.disallowed_extension_skips),
anti_items,
);
let e = match result {
Ok(()) => {
debug_assert!(
ctx.pending_error.is_none(),
"pending_error must be unset when for_each_entries reports success"
);
return Ok(accumulated);
}
Err(e) => ctx
.pending_error
.take()
.unwrap_or_else(|| ArchiveError::from(e)),
};
Err(ArchiveError::partial_or(accumulated, e))
}
}
const fn counter_overflow() -> ArchiveError {
ArchiveError::QuotaExceeded {
resource: QuotaResource::IntegerOverflow,
}
}
fn push_skip_warnings(report: &mut ExtractionReport, skips: (u64, u64), anti_items: u64) {
let (duplicates, disallowed) = skips;
common::push_duplicate_skip_warning(report, duplicates, "entry", "entries");
common::push_disallowed_extension_warning(report, disallowed);
push_anti_item_warning(report, anti_items);
}
fn push_anti_item_warning(report: &mut ExtractionReport, count: u64) {
if count > 0 {
report.warnings.push(format!(
"skipped {count} 7z {} (deletion markers are not applied)",
anti_item_noun(count)
));
}
}
const fn anti_item_noun(count: u64) -> &'static str {
if count == 1 {
"anti-item"
} else {
"anti-items"
}
}
fn anti_item_issue(count: u64) -> Option<VerificationIssue> {
(count > 0).then(|| {
VerificationIssue::new(
IssueSeverity::Info,
IssueCategory::ArchiveNote,
format!(
"{count} 7z {} skipped (deletion markers are not applied)",
anti_item_noun(count)
),
)
})
}
impl<R: Read + Seek> ArchiveFormat for SevenZArchive<R> {
fn extract(
&mut self,
output_dir: &Path,
config: &SecurityConfig<Validated>,
options: &ExtractionOptions,
progress: &mut dyn ProgressCallback,
) -> Result<ExtractionReport> {
if let Some(refusal) = iter_sevenz_refusals(&self.header, config).next() {
return Err(refusal.into_error());
}
let dest = DestDir::new_or_create(output_dir.to_path_buf())?;
let mut prevalidator = EntryValidator::new(config, &dest);
for (index, entry) in self.entries.iter().enumerate() {
let Some(kind) = entry.class.present(&entry.name)? else {
continue;
};
let path = std::path::PathBuf::from(common::normalize_entry_name(&entry.name));
let safe_path = match prevalidator.validate_entry_path(&path, kind.node_kind(), None)? {
EntryPath::Root(_) => continue,
EntryPath::Child(safe_path) => safe_path,
};
match kind {
SevenZKind::Directory => continue,
SevenZKind::ReparsePoint => return Err(reparse_point_refusal(&entry.name)),
SevenZKind::UnixSymlink => {
validate_entry_compression(&entry.compression, entry.size, config)?;
require_symlinks_allowed(config)?;
common::require_symlink_creation()?;
refuse_undecoded_target(
&safe_path,
&self.header.files()[index],
&dest,
config,
)?;
prevalidator.reserve_symlink(entry.size)?;
continue;
}
SevenZKind::File => {}
}
validate_entry_compression(&entry.compression, entry.size, config)?;
let dest_path = dest.join_path(safe_path.as_path());
let existing = lstat_dest(&dest_path)?;
if existing.is_some() && options.skip_duplicates {
continue;
}
#[cfg(unix)]
if let Some(meta) = &existing
&& meta.file_type().is_symlink()
{
return Err(common::symlink_at_dest_error(safe_path.as_path()).into());
}
let _permit = prevalidator.reserve_file(entry.size)?;
}
let total = self
.entries
.iter()
.filter(|entry| matches!(entry.class, SevenZClass::Present(_)))
.count();
let anti_items = self.anti_item_count();
if total == 0 {
let mut report = ExtractionReport::new();
push_anti_item_warning(&mut report, anti_items);
return Ok(report);
}
let mut validator = EntryValidator::new(config, &dest);
let mut dir_cache = common::DirCache::new();
let report = Self::extract_with_callback(
&mut self.source,
&self.header,
&dest,
&mut validator,
&mut dir_cache,
options.skip_duplicates,
progress,
total,
anti_items,
config,
)?;
progress.on_complete();
Ok(report)
}
fn list(
&mut self,
config: &SecurityConfig<Validated>,
) -> Result<crate::inspection::ArchiveManifest> {
use crate::inspection::list::ListPass;
use crate::inspection::list::list_sevenz_parsed;
list_sevenz_parsed(
&self.header,
&mut self.source,
config,
&mut ListPass::strict(),
)
}
fn verify(
&mut self,
config: &SecurityConfig<Validated>,
options: &VerificationOptions,
) -> Result<crate::inspection::VerificationReport> {
use crate::inspection::list::list_sevenz_parsed;
use crate::inspection::report::ArchiveStructure;
use crate::inspection::verify::list_for_verify;
use crate::inspection::verify::verify_manifest_with_structure;
let mut issues = refusal_issues(&sevenz_refusals(&self.header, config));
issues.extend(anti_item_issue(self.anti_item_count()));
let listed = list_for_verify(config, *options, |listing| {
list_sevenz_parsed(&self.header, &mut self.source, config, listing)
})?;
let structure = ArchiveStructure::of_manifest(&listed.manifest).with_refusals(&issues);
verify_manifest_with_structure(&listed.manifest, config, &structure, listed.integrity)
}
fn format_name(&self) -> &'static str {
"7z"
}
}
pub(crate) fn is_password_error(error: &sevenz_rust2::Error) -> bool {
matches!(
error,
sevenz_rust2::Error::PasswordRequired | sevenz_rust2::Error::MaybeBadPassword(_)
)
}
impl From<sevenz_rust2::Error> for ArchiveError {
fn from(err: sevenz_rust2::Error) -> Self {
if is_password_error(&err) {
return common::encrypted_archive_error();
}
match err {
sevenz_rust2::Error::Io(io_err, _) => Self::Io(io_err),
sevenz_rust2::Error::FileOpen(io_err, path) => Self::Io(std::io::Error::new(
io_err.kind(),
format!("{path}: {io_err}"),
)),
other => Self::InvalidArchive(format!("7z error: {other}")),
}
}
}
#[cfg(test)]
mod link_walk_tests;
#[cfg(test)]
mod parallel_tests;
pub(crate) use admit::iter_sevenz_refusals;
pub(crate) use admit::refusal_issues;
pub(crate) use admit::sevenz_entry_compression;
pub(crate) use admit::sevenz_refusals;
pub(crate) use classify::SevenZClass;
pub(crate) use classify::SevenZKind;
pub(crate) use header::SEVENZ_MAGIC;
pub(crate) use header::SevenZHeader;
pub(crate) use header::read_sevenz_header;
pub(crate) use links::LinkCapture;
pub(crate) use links::LinkOutcome;
pub(crate) use links::LinkTable;
pub(crate) use links::StoredLinkTarget;
pub(crate) use links::deliver_undecoded_links;
pub(crate) use links::walk_sevenz_links;
pub(crate) use verify::verify_sevenz_blocks;
mod admit;
mod classify;
mod decode;
mod header;
mod links;
mod lzma2;
mod verify;
mod write;
#[cfg(test)]
mod tests;