use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Read, Seek};
use std::path::Path;
use crate::read::Entry;
use crate::streaming::{StreamingArchive, StreamingConfig};
use crate::{Error, Result};
#[cfg(feature = "aes")]
use crate::Password;
pub struct ArchiveFS<R> {
archive: StreamingArchive<R>,
path_index: HashMap<String, usize>,
dir_tree: HashMap<String, Vec<String>>,
}
impl ArchiveFS<BufReader<File>> {
#[cfg(feature = "aes")]
pub fn open(path: impl AsRef<Path>, password: impl Into<Password>) -> Result<Self> {
let archive =
StreamingArchive::open_path_with_config(path, password, StreamingConfig::default())?;
Self::from_archive(archive)
}
#[cfg(not(feature = "aes"))]
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let archive = StreamingArchive::open_path_with_config(path, StreamingConfig::default())?;
Self::from_archive(archive)
}
}
impl<R: Read + Seek + Send> ArchiveFS<R> {
#[cfg(feature = "aes")]
pub fn from_reader(reader: R, password: impl Into<Password>) -> Result<Self> {
let archive = StreamingArchive::open(reader, password)?;
Self::from_archive(archive)
}
#[cfg(not(feature = "aes"))]
pub fn from_reader(reader: R) -> Result<Self> {
let archive = StreamingArchive::open(reader)?;
Self::from_archive(archive)
}
pub fn from_archive(archive: StreamingArchive<R>) -> Result<Self> {
let mut path_index = HashMap::new();
let mut dir_tree: HashMap<String, Vec<String>> = HashMap::new();
for (idx, entry) in archive.entries_list().iter().enumerate() {
let path = normalize_path(entry.path.as_str());
path_index.insert(path.clone(), idx);
let parent = parent_path(&path);
let name = file_name(&path);
dir_tree.entry(parent).or_default().push(name);
}
dir_tree.entry(String::new()).or_default();
Ok(Self {
archive,
path_index,
dir_tree,
})
}
pub fn len(&self) -> usize {
self.archive.len()
}
pub fn is_empty(&self) -> bool {
self.archive.is_empty()
}
pub fn exists(&self, path: impl AsRef<str>) -> bool {
let path = normalize_path(path.as_ref());
self.path_index.contains_key(&path) || self.dir_tree.contains_key(&path)
}
pub fn is_dir(&self, path: impl AsRef<str>) -> bool {
let path = normalize_path(path.as_ref());
if let Some(&idx) = self.path_index.get(&path) {
if self.archive.entries_list()[idx].is_directory {
return true;
}
}
self.dir_tree.contains_key(&path)
}
pub fn is_file(&self, path: impl AsRef<str>) -> bool {
let path = normalize_path(path.as_ref());
if let Some(&idx) = self.path_index.get(&path) {
!self.archive.entries_list()[idx].is_directory
} else {
false
}
}
pub fn metadata(&self, path: impl AsRef<str>) -> Option<FileMetadata<'_>> {
let path = normalize_path(path.as_ref());
let &idx = self.path_index.get(&path)?;
let entry = &self.archive.entries_list()[idx];
Some(FileMetadata { entry })
}
pub fn entry(&self, path: impl AsRef<str>) -> Option<&Entry> {
let path = normalize_path(path.as_ref());
let &idx = self.path_index.get(&path)?;
Some(&self.archive.entries_list()[idx])
}
pub fn read_dir(&self, path: impl AsRef<str>) -> Result<impl Iterator<Item = DirEntry<'_>>> {
let path = normalize_path(path.as_ref());
let children = self
.dir_tree
.get(&path)
.ok_or_else(|| Error::InvalidFormat(format!("not a directory: {}", path)))?;
let entries: Vec<DirEntry<'_>> = children
.iter()
.map(|name| {
let full_path = if path.is_empty() {
name.clone()
} else {
format!("{}/{}", path, name)
};
let entry = self
.path_index
.get(&full_path)
.map(|&idx| &self.archive.entries_list()[idx]);
let is_dir = self.is_dir(&full_path);
DirEntry {
name: name.clone(),
entry,
is_dir,
}
})
.collect();
Ok(entries.into_iter())
}
pub fn walk(&self, path: impl AsRef<str>) -> impl Iterator<Item = &Entry> {
let path = normalize_path(path.as_ref());
let prefix = if path.is_empty() {
String::new()
} else {
format!("{}/", path)
};
self.archive.entries_list().iter().filter(move |entry| {
let entry_path = normalize_path(entry.path.as_str());
entry_path == path || entry_path.starts_with(&prefix)
})
}
pub fn files(&self) -> impl Iterator<Item = &Entry> {
self.archive
.entries_list()
.iter()
.filter(|e| !e.is_directory)
}
pub fn directories(&self) -> impl Iterator<Item = &Entry> {
self.archive
.entries_list()
.iter()
.filter(|e| e.is_directory)
}
pub fn glob(&self, pattern: impl AsRef<str>) -> impl Iterator<Item = &Entry> {
let pattern = pattern.as_ref().to_string();
self.archive
.entries_list()
.iter()
.filter(move |entry| glob_match(&pattern, entry.path.as_str()))
}
pub fn archive(&self) -> &StreamingArchive<R> {
&self.archive
}
pub fn archive_mut(&mut self) -> &mut StreamingArchive<R> {
&mut self.archive
}
pub fn into_inner(self) -> StreamingArchive<R> {
self.archive
}
}
#[derive(Debug)]
pub struct FileMetadata<'a> {
entry: &'a Entry,
}
impl<'a> FileMetadata<'a> {
pub fn path(&self) -> &str {
self.entry.path.as_str()
}
pub fn is_dir(&self) -> bool {
self.entry.is_directory
}
pub fn is_file(&self) -> bool {
!self.entry.is_directory
}
pub fn size(&self) -> u64 {
self.entry.size
}
pub fn modified(&self) -> Option<u64> {
self.entry.modification_time
}
pub fn created(&self) -> Option<u64> {
self.entry.creation_time
}
pub fn accessed(&self) -> Option<u64> {
self.entry.access_time
}
pub fn is_encrypted(&self) -> bool {
self.entry.is_encrypted
}
pub fn crc32(&self) -> Option<u32> {
self.entry.crc32
}
pub fn entry(&self) -> &Entry {
self.entry
}
}
#[derive(Debug)]
pub struct DirEntry<'a> {
name: String,
entry: Option<&'a Entry>,
is_dir: bool,
}
impl<'a> DirEntry<'a> {
pub fn name(&self) -> &str {
&self.name
}
pub fn is_dir(&self) -> bool {
self.is_dir
}
pub fn is_file(&self) -> bool {
!self.is_dir
}
pub fn entry(&self) -> Option<&Entry> {
self.entry
}
pub fn size(&self) -> u64 {
self.entry.map(|e| e.size).unwrap_or(0)
}
}
fn normalize_path(path: &str) -> String {
let path = path.trim_start_matches('/').trim_end_matches('/');
path.replace('\\', "/")
}
fn parent_path(path: &str) -> String {
match path.rfind('/') {
Some(idx) => path[..idx].to_string(),
None => String::new(), }
}
fn file_name(path: &str) -> String {
match path.rfind('/') {
Some(idx) => path[idx + 1..].to_string(),
None => path.to_string(),
}
}
fn glob_match(pattern: &str, path: &str) -> bool {
let pattern = pattern.trim_start_matches('/');
let path = path.trim_start_matches('/');
if pattern == "**" {
return true;
}
if pattern.ends_with("**") && !pattern[..pattern.len() - 2].contains('*') {
let prefix = pattern[..pattern.len() - 2].trim_end_matches('/');
if prefix.is_empty() {
return true; }
if path == prefix {
return true;
}
let prefix_with_slash = format!("{}/", prefix);
return path.starts_with(&prefix_with_slash);
}
if let Some(suffix_pattern) = pattern.strip_prefix("**/") {
if suffix_pattern.contains('*') {
if let Some(star_pos) = suffix_pattern.find('*') {
let pre = &suffix_pattern[..star_pos];
let post = &suffix_pattern[star_pos + 1..];
for (i, _) in path.char_indices() {
let candidate = &path[i..];
if candidate.starts_with(pre) && candidate.ends_with(post) {
let middle = &candidate[pre.len()..candidate.len() - post.len()];
if !middle.contains('/') {
if i == 0 || path.as_bytes()[i - 1] == b'/' {
return true;
}
}
}
}
return false;
}
} else {
if path == suffix_pattern {
return true;
}
return path.ends_with(&format!("/{}", suffix_pattern));
}
}
if pattern.starts_with('*')
&& !pattern.contains('/')
&& pattern.chars().filter(|&c| c == '*').count() == 1
{
let suffix = &pattern[1..];
return path.ends_with(suffix)
&& !path[..path.len().saturating_sub(suffix.len())].contains('/');
}
if pattern.contains('/') && pattern.chars().filter(|&c| c == '*').count() == 1 {
if let Some(star_pos) = pattern.find('*') {
let prefix = &pattern[..star_pos];
let suffix = &pattern[star_pos + 1..];
if path.starts_with(prefix) && path.ends_with(suffix) {
let middle_start = prefix.len();
let middle_end = path.len().saturating_sub(suffix.len());
if middle_end > middle_start {
let middle = &path[middle_start..middle_end];
return !middle.contains('/');
} else {
return middle_start == middle_end;
}
}
return false;
}
}
pattern == path
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path() {
assert_eq!(normalize_path("/foo/bar"), "foo/bar");
assert_eq!(normalize_path("foo/bar/"), "foo/bar");
assert_eq!(normalize_path("/foo/bar/"), "foo/bar");
assert_eq!(normalize_path("foo\\bar"), "foo/bar");
assert_eq!(normalize_path(""), "");
}
#[test]
fn test_parent_path() {
assert_eq!(parent_path("foo/bar/baz"), "foo/bar");
assert_eq!(parent_path("foo/bar"), "foo");
assert_eq!(parent_path("foo"), "");
assert_eq!(parent_path(""), "");
}
#[test]
fn test_file_name() {
assert_eq!(file_name("foo/bar/baz.txt"), "baz.txt");
assert_eq!(file_name("foo/bar"), "bar");
assert_eq!(file_name("file.txt"), "file.txt");
}
#[test]
fn test_glob_match_exact() {
assert!(glob_match("foo.txt", "foo.txt"));
assert!(!glob_match("foo.txt", "bar.txt"));
}
#[test]
fn test_glob_match_star() {
assert!(glob_match("*.txt", "foo.txt"));
assert!(glob_match("*.txt", "bar.txt"));
assert!(!glob_match("*.txt", "foo/bar.txt"));
assert!(glob_match("foo/*", "foo/bar"));
assert!(!glob_match("foo/*", "foo/bar/baz"));
}
#[test]
fn test_glob_match_double_star() {
assert!(glob_match("**/*.txt", "foo/bar.txt"));
assert!(glob_match("**/*.txt", "foo/bar/baz.txt"));
assert!(glob_match("**/*.txt", "foo.txt"));
assert!(glob_match("src/**", "src/foo"));
assert!(glob_match("src/**", "src/foo/bar"));
assert!(glob_match("**", "anything"));
assert!(glob_match("**", "foo/bar/baz"));
}
#[test]
fn test_dir_entry() {
let entry = DirEntry {
name: "test.txt".to_string(),
entry: None,
is_dir: false,
};
assert_eq!(entry.name(), "test.txt");
assert!(!entry.is_dir());
assert!(entry.is_file());
assert_eq!(entry.size(), 0);
}
}