#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use use_archive_entry::{ArchiveEntry, ArchiveEntryKind};
use use_archive_path::{ArchivePathIssue, archive_path_issues};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ArchivePolicy {
pub allow_absolute_paths: bool,
pub allow_parent_traversal: bool,
pub allow_symlinks: bool,
pub max_entry_size: Option<u64>,
pub max_total_size: Option<u64>,
pub max_entries: Option<usize>,
}
impl ArchivePolicy {
#[must_use]
pub const fn strict() -> Self {
Self {
allow_absolute_paths: false,
allow_parent_traversal: false,
allow_symlinks: false,
max_entry_size: None,
max_total_size: None,
max_entries: None,
}
}
#[must_use]
pub const fn permissive() -> Self {
Self {
allow_absolute_paths: true,
allow_parent_traversal: true,
allow_symlinks: true,
max_entry_size: None,
max_total_size: None,
max_entries: None,
}
}
#[must_use]
pub const fn list_only() -> Self {
Self::permissive()
}
#[must_use]
pub const fn with_max_entry_size(mut self, max_entry_size: u64) -> Self {
self.max_entry_size = Some(max_entry_size);
self
}
#[must_use]
pub const fn with_max_total_size(mut self, max_total_size: u64) -> Self {
self.max_total_size = Some(max_total_size);
self
}
#[must_use]
pub const fn with_max_entries(mut self, max_entries: usize) -> Self {
self.max_entries = Some(max_entries);
self
}
#[must_use]
pub fn allows_path(&self, path: &str) -> bool {
archive_path_issues(path)
.into_iter()
.all(|issue| self.path_issue_allowed(issue))
}
#[must_use]
pub fn entry_issues(&self, entry: &ArchiveEntry) -> Vec<ArchivePolicyIssue> {
let mut issues = Vec::new();
for path_issue in archive_path_issues(entry.path()) {
if !self.path_issue_allowed(path_issue) {
issues.push(ArchivePolicyIssue::PathIssue(path_issue));
}
}
if !self.allow_symlinks && entry.kind() == ArchiveEntryKind::Symlink {
issues.push(ArchivePolicyIssue::SymlinkNotAllowed);
}
if let (Some(size), Some(max)) = (entry.size(), self.max_entry_size)
&& size > max
{
issues.push(ArchivePolicyIssue::EntryTooLarge { size, max });
}
issues
}
#[must_use]
pub fn allows_entry(&self, entry: &ArchiveEntry) -> bool {
self.entry_issues(entry).is_empty()
}
#[must_use]
pub fn entries_issues(&self, entries: &[ArchiveEntry]) -> Vec<ArchivePolicyIssue> {
let mut issues = Vec::new();
for entry in entries {
issues.extend(self.entry_issues(entry));
}
if let Some(max_entries) = self.max_entries
&& entries.len() > max_entries
{
issues.push(ArchivePolicyIssue::TooManyEntries {
entries: entries.len(),
max: max_entries,
});
}
if let Some(max_total_size) = self.max_total_size {
let total = entries.iter().filter_map(ArchiveEntry::size).sum();
if total > max_total_size {
issues.push(ArchivePolicyIssue::TotalSizeTooLarge {
total,
max: max_total_size,
});
}
}
issues
}
#[must_use]
pub fn allows_entries(&self, entries: &[ArchiveEntry]) -> bool {
self.entries_issues(entries).is_empty()
}
fn path_issue_allowed(&self, issue: ArchivePathIssue) -> bool {
match issue {
ArchivePathIssue::AbsolutePath
| ArchivePathIssue::WindowsDrivePrefix
| ArchivePathIssue::WindowsUncPrefix => self.allow_absolute_paths,
ArchivePathIssue::ParentTraversal => self.allow_parent_traversal,
ArchivePathIssue::EmptyComponent | ArchivePathIssue::SuspiciousSeparator => {
self.allow_absolute_paths && self.allow_parent_traversal
},
}
}
}
impl Default for ArchivePolicy {
fn default() -> Self {
Self::strict()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ArchivePolicyIssue {
PathIssue(ArchivePathIssue),
SymlinkNotAllowed,
EntryTooLarge {
size: u64,
max: u64,
},
TotalSizeTooLarge {
total: u64,
max: u64,
},
TooManyEntries {
entries: usize,
max: usize,
},
}
#[cfg(test)]
mod tests {
use super::{ArchivePolicy, ArchivePolicyIssue};
use use_archive_entry::{ArchiveEntry, ArchiveEntryKind};
use use_archive_path::ArchivePathIssue;
#[test]
fn strict_policy_rejects_unsafe_paths_and_symlinks() {
let policy = ArchivePolicy::strict();
let unsafe_entry = ArchiveEntry::new("../secrets.env", ArchiveEntryKind::File);
let symlink = ArchiveEntry::new("link", ArchiveEntryKind::Symlink);
assert!(
policy
.entry_issues(&unsafe_entry)
.contains(&ArchivePolicyIssue::PathIssue(
ArchivePathIssue::ParentTraversal
))
);
assert!(
policy
.entry_issues(&symlink)
.contains(&ArchivePolicyIssue::SymlinkNotAllowed)
);
}
#[test]
fn enforces_size_and_count_limits() {
let policy = ArchivePolicy::strict()
.with_max_entry_size(10)
.with_max_total_size(20)
.with_max_entries(1);
let entries = vec![
ArchiveEntry::file("one.txt").with_size(15),
ArchiveEntry::file("two.txt").with_size(15),
];
let issues = policy.entries_issues(&entries);
assert!(issues.contains(&ArchivePolicyIssue::EntryTooLarge { size: 15, max: 10 }));
assert!(issues.contains(&ArchivePolicyIssue::TotalSizeTooLarge { total: 30, max: 20 }));
assert!(issues.contains(&ArchivePolicyIssue::TooManyEntries { entries: 2, max: 1 }));
}
#[test]
fn permissive_policy_allows_listing_paths() {
let policy = ArchivePolicy::list_only();
assert!(policy.allows_path("../secrets.env"));
assert!(policy.allows_path(r"C:\Users\name\secret.txt"));
}
}