use std::path::Path;
use std::path::PathBuf;
use std::time::SystemTime;
use crate::ArchiveError;
use crate::Result;
use crate::ZipEntryIndex;
use crate::closed_enum::closed_enum;
use crate::formats::detect::ArchiveType;
use crate::security::fs_view::LinkKind;
use crate::security::fs_view::check_link_target_length;
#[derive(Debug, Clone)]
pub struct ArchiveManifest {
entries: Vec<ArchiveEntry>,
oversized: Vec<OversizedEntry>,
total_size: u64,
format: ArchiveType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct EntryPosition(usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct OversizedEntry {
pub index: usize,
pub bytes: usize,
}
impl OversizedEntry {
#[must_use]
pub const fn new(index: usize, bytes: usize) -> Self {
Self { index, bytes }
}
}
impl ArchiveManifest {
#[must_use]
pub fn new(format: ArchiveType) -> Self {
Self {
entries: Vec::new(),
oversized: Vec::new(),
total_size: 0,
format,
}
}
pub(crate) fn add_oversized(&mut self, bytes: usize) {
let index = self.entries.len() + self.oversized.len();
self.oversized.push(OversizedEntry::new(index, bytes));
}
#[must_use]
pub fn oversized_entries(&self) -> &[OversizedEntry] {
&self.oversized
}
pub(crate) fn position_of_zip_entry(&self, index: ZipEntryIndex) -> Option<usize> {
let index = index.get();
let dropped_before = self.oversized.partition_point(|o| o.index < index);
if self
.oversized
.get(dropped_before)
.is_some_and(|o| o.index == index)
{
return None;
}
let position = index - dropped_before;
(position < self.entries.len()).then_some(position)
}
pub fn add_entry(&mut self, entry: ArchiveEntry) -> Result<()> {
self.total_size = self.total_size.checked_add(entry.size).ok_or_else(|| {
ArchiveError::InvalidArchive("archive total size overflows u64".to_string())
})?;
self.entries.push(entry);
Ok(())
}
pub(crate) fn next_position(&self) -> EntryPosition {
EntryPosition(self.entries.len())
}
pub(crate) fn set_link_target(&mut self, position: EntryPosition, target: LinkTarget) {
if let Some(ArchiveEntry {
kind: ManifestEntryKind::Symlink { target: slot },
..
}) = self.entries.get_mut(position.0)
{
*slot = target;
}
}
#[must_use]
pub fn entries(&self) -> &[ArchiveEntry] {
&self.entries
}
#[must_use]
pub fn into_entries(self) -> Vec<ArchiveEntry> {
self.entries
}
#[must_use]
pub fn total_entries(&self) -> usize {
self.entries.len()
}
#[must_use]
pub const fn total_size(&self) -> u64 {
self.total_size
}
#[must_use]
pub const fn format(&self) -> ArchiveType {
self.format
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ArchiveEntry {
pub path: PathBuf,
pub kind: ManifestEntryKind,
pub size: u64,
pub compression: EntryCompression,
pub mode: Option<u32>,
pub modified: Option<SystemTime>,
}
impl ArchiveEntry {
#[must_use]
pub const fn new(path: PathBuf, kind: ManifestEntryKind, size: u64) -> Self {
Self {
path,
kind,
size,
compression: EntryCompression::Unknown,
mode: None,
modified: None,
}
}
#[must_use]
pub const fn compressed_size(&self) -> Option<u64> {
self.compression.compressed_size()
}
#[must_use]
pub fn compression_ratio(&self) -> Option<f64> {
self.compression
.entry_compressed_size()
.filter(|&c| c > 0)
.map(|c| self.size as f64 / c as f64)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct SolidBlock {
pub compressed: u64,
pub uncompressed: u64,
}
impl SolidBlock {
#[must_use]
pub const fn new(compressed: u64, uncompressed: u64) -> Self {
Self {
compressed,
uncompressed,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EntryCompression {
Unknown,
Entry(u64),
SolidBlockStart(SolidBlock),
SolidBlockMember,
}
impl EntryCompression {
#[must_use]
pub const fn compressed_size(&self) -> Option<u64> {
match self {
Self::Entry(compressed) => Some(*compressed),
Self::SolidBlockStart(block) => Some(block.compressed),
Self::Unknown | Self::SolidBlockMember => None,
}
}
#[must_use]
pub const fn entry_compressed_size(&self) -> Option<u64> {
match self {
Self::Entry(compressed) => Some(*compressed),
Self::Unknown | Self::SolidBlockStart(_) | Self::SolidBlockMember => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LinkTarget {
Recorded(PathBuf),
Missing,
Unread,
Unreadable,
Oversized {
bytes: usize,
},
}
impl LinkTarget {
#[must_use]
pub fn classify(path: PathBuf, kind: LinkKind) -> Self {
if check_link_target_length(&path, kind).is_ok() {
Self::Recorded(path)
} else {
Self::Oversized {
bytes: path.as_os_str().len(),
}
}
}
#[must_use]
pub fn path(&self) -> Option<&Path> {
match self {
Self::Recorded(path) => Some(path),
Self::Missing | Self::Unread | Self::Unreadable | Self::Oversized { .. } => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ManifestEntryKind {
File,
Directory,
Symlink {
target: LinkTarget,
},
Hardlink {
target: LinkTarget,
},
}
impl ManifestEntryKind {
#[must_use]
pub const fn entry_type(&self) -> ManifestEntryType {
match self {
Self::File => ManifestEntryType::File,
Self::Directory => ManifestEntryType::Directory,
Self::Symlink { .. } => ManifestEntryType::Symlink,
Self::Hardlink { .. } => ManifestEntryType::Hardlink,
}
}
#[must_use]
pub const fn is_link(&self) -> bool {
matches!(self, Self::Symlink { .. } | Self::Hardlink { .. })
}
#[must_use]
pub fn symlink_target(&self) -> Option<&Path> {
match self {
Self::Symlink { target } => target.path(),
Self::File | Self::Directory | Self::Hardlink { .. } => None,
}
}
#[must_use]
pub fn hardlink_target(&self) -> Option<&Path> {
match self {
Self::Hardlink { target } => target.path(),
Self::File | Self::Directory | Self::Symlink { .. } => None,
}
}
#[must_use]
pub fn link_target(&self) -> Option<&Path> {
self.symlink_target().or_else(|| self.hardlink_target())
}
}
closed_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ManifestEntryType {
File => "File",
Directory => "Directory",
Symlink => "Symlink",
Hardlink => "Hardlink",
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::assert_matches;
use super::*;
#[test]
fn test_manifest_new() {
let manifest = ArchiveManifest::new(ArchiveType::TarGz);
assert_eq!(manifest.total_entries(), 0);
assert_eq!(manifest.total_size(), 0);
assert!(manifest.entries().is_empty());
assert_eq!(manifest.format(), ArchiveType::TarGz);
}
#[test]
fn test_manifest_add_entry() {
let mut manifest = ArchiveManifest::new(ArchiveType::TarGz);
let entry = ArchiveEntry {
path: PathBuf::from("file.txt"),
kind: ManifestEntryKind::File,
size: 1024,
compression: EntryCompression::Entry(512),
mode: Some(0o644),
modified: None,
};
manifest.add_entry(entry).unwrap();
assert_eq!(manifest.total_entries(), 1);
assert_eq!(manifest.total_size(), 1024);
assert_eq!(manifest.entries().len(), 1);
}
#[test]
fn test_manifest_add_entry_size_overflow() {
let mut manifest = ArchiveManifest::new(ArchiveType::Tar);
let big = ArchiveEntry::new(PathBuf::from("a"), ManifestEntryKind::File, u64::MAX);
manifest.add_entry(big).unwrap();
let more = ArchiveEntry::new(PathBuf::from("b"), ManifestEntryKind::File, 1);
assert_matches!(
manifest.add_entry(more),
Err(ArchiveError::InvalidArchive(_))
);
assert_eq!(manifest.total_entries(), 1);
assert_eq!(manifest.total_size(), u64::MAX);
}
#[test]
fn test_compression_ratio() {
let entry = ArchiveEntry {
path: PathBuf::from("file.txt"),
kind: ManifestEntryKind::File,
size: 1000,
compression: EntryCompression::Entry(100),
mode: None,
modified: None,
};
assert_eq!(entry.compression_ratio(), Some(10.0));
}
#[test]
fn test_compression_ratio_no_compressed() {
let entry = ArchiveEntry {
path: PathBuf::from("file.txt"),
kind: ManifestEntryKind::File,
size: 1000,
compression: EntryCompression::Unknown,
mode: None,
modified: None,
};
assert_eq!(entry.compression_ratio(), None);
}
#[test]
fn test_solid_entries_have_no_per_file_ratio() {
let mut entry = ArchiveEntry::new(PathBuf::from("a"), ManifestEntryKind::File, 1000);
entry.compression = EntryCompression::SolidBlockStart(SolidBlock::new(10, 2000));
assert_eq!(entry.compression_ratio(), None);
assert_eq!(entry.compressed_size(), Some(10));
entry.compression = EntryCompression::SolidBlockMember;
assert_eq!(entry.compression_ratio(), None);
assert_eq!(entry.compressed_size(), None);
}
#[test]
fn test_manifest_entry_type_display() {
assert_eq!(ManifestEntryType::File.to_string(), "File");
assert_eq!(ManifestEntryType::Directory.to_string(), "Directory");
assert_eq!(ManifestEntryType::Symlink.to_string(), "Symlink");
assert_eq!(ManifestEntryType::Hardlink.to_string(), "Hardlink");
}
#[test]
fn test_position_of_zip_entry_skips_dropped_entries() {
let mut manifest = ArchiveManifest::new(ArchiveType::Zip);
manifest.add_oversized(300);
for name in ["a", "b"] {
let entry = ArchiveEntry::new(PathBuf::from(name), ManifestEntryKind::File, 0);
manifest.add_entry(entry).unwrap();
}
manifest.add_oversized(300);
let position = |index| manifest.position_of_zip_entry(ZipEntryIndex::new(index));
assert_eq!(position(0), None);
assert_eq!(position(1), Some(0));
assert_eq!(position(2), Some(1));
assert_eq!(position(3), None);
assert_eq!(position(9), None);
}
}