use crate::ArchivePath;
use crate::ownership::UnixOwnership;
use crate::timestamp::Timestamp;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Entry {
pub path: ArchivePath,
pub is_directory: bool,
pub size: u64,
pub crc32: Option<u32>,
#[doc(hidden)]
pub crc64: Option<u64>,
pub modification_time: Option<u64>,
pub creation_time: Option<u64>,
pub access_time: Option<u64>,
pub attributes: Option<u32>,
pub is_encrypted: bool,
pub is_symlink: bool,
pub is_anti: bool,
pub ownership: Option<UnixOwnership>,
#[allow(dead_code)] pub(crate) index: usize,
pub(crate) folder_index: Option<usize>,
pub(crate) stream_index: Option<usize>,
}
impl Entry {
pub fn name(&self) -> &str {
self.path.file_name()
}
pub fn is_file(&self) -> bool {
!self.is_directory
}
pub fn modified(&self) -> Option<std::time::SystemTime> {
self.modification_time
.map(|ft| Timestamp::from_filetime(ft).as_system_time())
}
pub fn created(&self) -> Option<std::time::SystemTime> {
self.creation_time
.map(|ft| Timestamp::from_filetime(ft).as_system_time())
}
pub fn accessed(&self) -> Option<std::time::SystemTime> {
self.access_time
.map(|ft| Timestamp::from_filetime(ft).as_system_time())
}
pub fn modification_timestamp(&self) -> Option<Timestamp> {
self.modification_time.map(Timestamp::from_filetime)
}
pub fn creation_timestamp(&self) -> Option<Timestamp> {
self.creation_time.map(Timestamp::from_filetime)
}
pub fn access_timestamp(&self) -> Option<Timestamp> {
self.access_time.map(Timestamp::from_filetime)
}
pub fn unix_mode(&self) -> Option<u32> {
self.attributes.and_then(crate::ownership::decode_unix_mode)
}
pub fn owner_uid(&self) -> Option<u32> {
self.ownership.as_ref().and_then(|o| o.uid)
}
pub fn owner_gid(&self) -> Option<u32> {
self.ownership.as_ref().and_then(|o| o.gid)
}
pub fn owner_name(&self) -> Option<&str> {
self.ownership.as_ref().and_then(|o| o.user_name.as_deref())
}
pub fn group_name(&self) -> Option<&str> {
self.ownership
.as_ref()
.and_then(|o| o.group_name.as_deref())
}
}
pub trait EntrySelector {
fn select(&self, entry: &Entry) -> bool;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SelectAll;
impl EntrySelector for SelectAll {
fn select(&self, _entry: &Entry) -> bool {
true
}
}
#[derive(Debug, Clone)]
pub struct SelectByName {
names: Vec<String>,
}
impl SelectByName {
pub fn new<S: Into<String>>(names: impl IntoIterator<Item = S>) -> Self {
Self {
names: names.into_iter().map(Into::into).collect(),
}
}
}
impl EntrySelector for SelectByName {
fn select(&self, entry: &Entry) -> bool {
self.names.iter().any(|name| entry.path.as_str() == name)
}
}
pub struct SelectByPredicate<F> {
predicate: F,
}
impl<F: Fn(&Entry) -> bool> SelectByPredicate<F> {
pub fn new(predicate: F) -> Self {
Self { predicate }
}
}
impl<F: Fn(&Entry) -> bool> EntrySelector for SelectByPredicate<F> {
fn select(&self, entry: &Entry) -> bool {
(self.predicate)(entry)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SelectFilesOnly;
impl EntrySelector for SelectFilesOnly {
fn select(&self, entry: &Entry) -> bool {
entry.is_file()
}
}
impl EntrySelector for () {
fn select(&self, _entry: &Entry) -> bool {
true
}
}
impl<F: Fn(&Entry) -> bool> EntrySelector for F {
fn select(&self, entry: &Entry) -> bool {
self(entry)
}
}
impl EntrySelector for &[&str] {
fn select(&self, entry: &Entry) -> bool {
self.iter().any(|name| entry.path.as_str() == *name)
}
}
impl EntrySelector for Vec<String> {
fn select(&self, entry: &Entry) -> bool {
self.iter().any(|name| entry.path.as_str() == name)
}
}
#[cfg(feature = "regex")]
#[derive(Debug, Clone)]
pub struct SelectByRegex {
pattern: regex::Regex,
}
#[cfg(feature = "regex")]
impl SelectByRegex {
pub fn new(pattern: &str) -> crate::Result<Self> {
let regex = regex::Regex::new(pattern).map_err(|e| crate::Error::InvalidRegex {
pattern: pattern.to_string(),
reason: e.to_string(),
})?;
Ok(Self { pattern: regex })
}
pub fn pattern(&self) -> ®ex::Regex {
&self.pattern
}
}
#[cfg(feature = "regex")]
impl EntrySelector for SelectByRegex {
fn select(&self, entry: &Entry) -> bool {
self.pattern.is_match(entry.path.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entry(path: &str, is_dir: bool) -> Entry {
Entry {
path: ArchivePath::new(path).unwrap(),
is_directory: is_dir,
size: 100,
crc32: Some(0x12345678),
crc64: None,
modification_time: None,
creation_time: None,
access_time: None,
attributes: None,
is_encrypted: false,
is_symlink: false,
is_anti: false,
ownership: None,
index: 0,
folder_index: None,
stream_index: None,
}
}
#[test]
fn test_entry_is_file() {
let file = make_entry("test.txt", false);
assert!(file.is_file());
assert!(!file.is_directory);
let dir = make_entry("subdir", true);
assert!(!dir.is_file());
assert!(dir.is_directory);
}
#[test]
fn test_entry_name() {
let entry = make_entry("path/to/file.txt", false);
assert_eq!(entry.name(), "file.txt");
}
#[test]
fn test_select_all() {
let entry = make_entry("test.txt", false);
assert!(SelectAll.select(&entry));
assert!(().select(&entry));
}
#[test]
fn test_select_by_name() {
let entry1 = make_entry("file1.txt", false);
let entry2 = make_entry("file2.txt", false);
let entry3 = make_entry("other.txt", false);
let selector = SelectByName::new(["file1.txt", "file2.txt"]);
assert!(selector.select(&entry1));
assert!(selector.select(&entry2));
assert!(!selector.select(&entry3));
}
#[test]
fn test_select_by_predicate() {
let file = make_entry("test.txt", false);
let dir = make_entry("subdir", true);
let selector = SelectByPredicate::new(|e: &Entry| e.is_file());
assert!(selector.select(&file));
assert!(!selector.select(&dir));
}
#[test]
fn test_select_files_only() {
let file = make_entry("test.txt", false);
let dir = make_entry("subdir", true);
assert!(SelectFilesOnly.select(&file));
assert!(!SelectFilesOnly.select(&dir));
}
#[test]
fn test_select_closure() {
let entry = make_entry("test.txt", false);
let selector = |e: &Entry| e.size > 50;
assert!(selector.select(&entry));
}
#[test]
fn test_select_slice() {
let entry1 = make_entry("file1.txt", false);
let entry2 = make_entry("other.txt", false);
let names: &[&str] = &["file1.txt", "file2.txt"];
assert!(names.select(&entry1));
assert!(!names.select(&entry2));
}
#[cfg(feature = "regex")]
#[test]
fn test_select_by_regex() {
let txt_file = make_entry("path/to/file.txt", false);
let rs_file = make_entry("src/main.rs", false);
let other_file = make_entry("README.md", false);
let txt_selector = SelectByRegex::new(r"\.txt$").unwrap();
assert!(txt_selector.select(&txt_file));
assert!(!txt_selector.select(&rs_file));
assert!(!txt_selector.select(&other_file));
let src_selector = SelectByRegex::new(r"^src/").unwrap();
assert!(!src_selector.select(&txt_file));
assert!(src_selector.select(&rs_file));
assert!(!src_selector.select(&other_file));
let rs_selector = SelectByRegex::new(r"\.rs$").unwrap();
assert!(!rs_selector.select(&txt_file));
assert!(rs_selector.select(&rs_file));
assert!(!rs_selector.select(&other_file));
}
#[cfg(feature = "regex")]
#[test]
fn test_select_by_regex_invalid_pattern() {
let result = SelectByRegex::new(r"[invalid");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, crate::Error::InvalidRegex { .. }));
let err_str = err.to_string();
assert!(
err_str.contains("[invalid"),
"Error should contain the pattern"
);
}
}