arcthis 0.5.1

A command-line tool for reading and managing compressed files
use std::io::{self, Read, Write};
use std::path::Path;

use super::{ArchiveBackend, ArchiveSource, display_entry_path, format_unix_time};
use crate::archive::codec::{StreamCompression, decoder};
use crate::error::{ArcthisError, Result};
use crate::extract::{ExtractionStats, ExtractionWriter, PlannedEntry};
use crate::model::{
    ArchiveCapabilities, ArchiveEntry, ArchiveFormat, EntryCopyResult, EntryKind,
    VerificationResult,
};
use crate::security::ExtractionLimits;

pub(crate) struct TarBackend {
    source: ArchiveSource,
    format: ArchiveFormat,
}

impl TarBackend {
    pub(crate) const fn new(source: ArchiveSource, format: ArchiveFormat) -> Self {
        Self { source, format }
    }

    fn reader(&self) -> Result<Box<dyn Read>> {
        match self.format {
            ArchiveFormat::Tar => self.source.reader().map(|reader| reader as Box<dyn Read>),
            ArchiveFormat::TarGzip => decoder(self.source.reader()?, StreamCompression::Gzip),
            ArchiveFormat::TarBzip2 => decoder(self.source.reader()?, StreamCompression::Bzip2),
            ArchiveFormat::TarXz => decoder(self.source.reader()?, StreamCompression::Xz),
            ArchiveFormat::TarZstd => decoder(self.source.reader()?, StreamCompression::Zstd),
            ArchiveFormat::Zip
            | ArchiveFormat::SevenZip
            | ArchiveFormat::Rar
            | ArchiveFormat::Gzip
            | ArchiveFormat::Bzip2
            | ArchiveFormat::Xz
            | ArchiveFormat::Zstd => Err(ArcthisError::UnsupportedOperation {
                message: format!("{} cannot be opened by the TAR backend", self.format),
            }),
        }
    }

    fn read_entries(&self) -> Result<Vec<ArchiveEntry>> {
        let reader = self.reader()?;
        let mut archive = tar::Archive::new(reader);
        let iterator = archive
            .entries()
            .map_err(|error| ArcthisError::InvalidArchive {
                message: error.to_string(),
            })?;
        let mut result = Vec::new();
        for entry in iterator {
            let entry = entry.map_err(|error| ArcthisError::CorruptedArchive {
                message: error.to_string(),
            })?;
            let header = entry.header();
            let entry_type = header.entry_type();
            let kind = if entry_type.is_file() {
                EntryKind::File
            } else if entry_type.is_dir() {
                EntryKind::Directory
            } else if entry_type.is_symlink() {
                EntryKind::Symlink
            } else if entry_type.is_hard_link() {
                EntryKind::Hardlink
            } else {
                EntryKind::Other
            };
            let link_target = entry
                .link_name_bytes()
                .map(|bytes| display_entry_path(bytes.as_ref()).0);
            let mode = header.mode().ok();
            let (path, path_encoding) = display_entry_path(entry.path_bytes().as_ref());
            result.push(ArchiveEntry {
                archive_index: u64::try_from(result.len()).unwrap_or(u64::MAX),
                path,
                path_encoding,
                kind,
                size: header
                    .size()
                    .map_err(|error| ArcthisError::InvalidArchive {
                        message: error.to_string(),
                    })?,
                compressed_size: None,
                modified_time: header.mtime().ok().and_then(format_unix_time),
                encrypted: false,
                executable: mode.is_some_and(|value| value & 0o111 != 0),
                symlink_target: link_target,
                crc32: None,
                mime_guess: None,
            });
        }
        Ok(result)
    }
}

impl ArchiveBackend for TarBackend {
    fn format(&self) -> ArchiveFormat {
        self.format
    }

    fn capabilities(&self) -> ArchiveCapabilities {
        ArchiveCapabilities::tar()
    }

    fn validate(&self) -> Result<()> {
        let reader = self.reader()?;
        let mut archive = tar::Archive::new(reader);
        let mut entries = archive
            .entries()
            .map_err(|error| ArcthisError::InvalidArchive {
                message: error.to_string(),
            })?;
        if let Some(entry) = entries.next() {
            entry.map_err(|error| ArcthisError::CorruptedArchive {
                message: error.to_string(),
            })?;
        }
        Ok(())
    }

    fn entries(&self) -> Result<Vec<ArchiveEntry>> {
        self.read_entries()
    }

    fn copy_entry_to(&self, path: &str, writer: &mut dyn Write) -> Result<EntryCopyResult> {
        let reader = self.reader()?;
        let mut archive = tar::Archive::new(reader);
        let entries = archive
            .entries()
            .map_err(|error| ArcthisError::InvalidArchive {
                message: error.to_string(),
            })?;
        for entry in entries {
            let mut entry = entry.map_err(|error| ArcthisError::CorruptedArchive {
                message: error.to_string(),
            })?;
            if display_entry_path(entry.path_bytes().as_ref()).0 != path {
                continue;
            }
            let bytes_written = io::copy(&mut entry, writer)
                .map_err(|error| ArcthisError::io("streaming TAR entry", error))?;
            return Ok(EntryCopyResult { bytes_written });
        }
        Err(ArcthisError::EntryNotFound {
            entry: path.to_owned(),
        })
    }

    fn extract_plan(
        &self,
        plan: &[PlannedEntry],
        staging_root: &Path,
        limits: &ExtractionLimits,
    ) -> Result<ExtractionStats> {
        let targets = plan
            .iter()
            .map(|entry| (entry.archive_path.as_str(), entry))
            .collect::<std::collections::HashMap<_, _>>();
        let mut writer = ExtractionWriter::new(staging_root, limits);
        let reader = self.reader()?;
        let mut archive = tar::Archive::new(reader);
        let entries = archive
            .entries()
            .map_err(|error| ArcthisError::InvalidArchive {
                message: error.to_string(),
            })?;
        for entry in entries {
            let mut entry = entry.map_err(|error| ArcthisError::CorruptedArchive {
                message: error.to_string(),
            })?;
            let path = display_entry_path(entry.path_bytes().as_ref()).0;
            let Some(target) = targets.get(path.as_str()) else {
                continue;
            };
            match target.kind {
                EntryKind::Directory => writer.create_directory(target)?,
                EntryKind::File => writer.write_file(target, &mut entry)?,
                EntryKind::Symlink | EntryKind::Hardlink | EntryKind::Other => {
                    return Err(ArcthisError::UnsafePath {
                        path,
                        reason: "links and special entries cannot be extracted".to_owned(),
                    });
                }
            }
        }
        Ok(writer.stats())
    }

    fn verify(&self) -> Result<VerificationResult> {
        let reader = self.reader()?;
        let mut archive = tar::Archive::new(reader);
        let (entries_checked, bytes_checked) = {
            let entries = archive
                .entries()
                .map_err(|error| ArcthisError::VerificationFailed {
                    message: error.to_string(),
                })?;
            let mut entries_checked = 0_u64;
            let mut bytes_checked = 0_u64;
            for entry in entries {
                let mut entry = entry.map_err(|error| ArcthisError::VerificationFailed {
                    message: error.to_string(),
                })?;
                let bytes = io::copy(&mut entry, &mut io::sink()).map_err(|error| {
                    ArcthisError::VerificationFailed {
                        message: error.to_string(),
                    }
                })?;
                bytes_checked = bytes_checked.checked_add(bytes).ok_or_else(|| {
                    ArcthisError::VerificationFailed {
                        message: "verified byte count overflowed u64".to_owned(),
                    }
                })?;
                entries_checked += 1;
            }
            (entries_checked, bytes_checked)
        };
        let mut remainder = archive.into_inner();
        io::copy(&mut remainder, &mut io::sink()).map_err(|error| {
            ArcthisError::VerificationFailed {
                message: error.to_string(),
            }
        })?;
        Ok(VerificationResult {
            verified: true,
            entries_checked,
            bytes_checked,
        })
    }
}