use crate::disk::AsarError;
use crate::integrity::FileIntegrity;
use indexmap::IndexMap;
use std::collections::HashSet;
use std::path::{Component, Path, PathBuf};
const SYMLINK_MAX_DEPTH: usize = 40;
const UINT32_MAX: u64 = 4_294_967_295;
#[derive(Debug, Clone)]
pub struct FileEntry {
pub offset: String,
pub size: u64,
pub executable: bool,
pub unpacked: bool,
pub integrity: Option<FileIntegrity>,
}
#[derive(Debug, Clone)]
pub struct DirectoryEntry {
pub files: IndexMap<String, FilesystemEntry>,
pub unpacked: bool,
}
#[derive(Debug, Clone)]
pub struct LinkEntry {
pub link: String,
pub unpacked: bool,
}
#[derive(Debug, Clone)]
pub enum FilesystemEntry {
File(FileEntry),
Directory(DirectoryEntry),
Link(LinkEntry),
}
impl FilesystemEntry {
pub fn is_directory(&self) -> bool {
matches!(self, FilesystemEntry::Directory(_))
}
pub fn is_file(&self) -> bool {
matches!(self, FilesystemEntry::File(_))
}
pub fn is_link(&self) -> bool {
matches!(self, FilesystemEntry::Link(_))
}
}
#[derive(Debug, Clone)]
pub struct Filesystem {
src: PathBuf,
header: FilesystemEntry,
header_size: u32,
offset: u64,
}
impl Filesystem {
pub fn new(src: &Path) -> Self {
Filesystem {
src: src.to_path_buf(),
header: FilesystemEntry::Directory(DirectoryEntry {
files: IndexMap::new(),
unpacked: false,
}),
header_size: 0,
offset: 0,
}
}
pub fn root_path(&self) -> &Path {
&self.src
}
pub fn get_header(&self) -> &FilesystemEntry {
&self.header
}
pub fn header_size(&self) -> u32 {
self.header_size
}
pub fn set_header(&mut self, header: FilesystemEntry, header_size: u32) {
self.header = header;
self.header_size = header_size;
}
pub fn current_offset(&self) -> u64 {
self.offset
}
pub fn advance_offset(&mut self, size: u64) {
self.offset += size;
}
fn search_node_from_directory(
&mut self,
p: &Path,
) -> Result<&mut FilesystemEntry, AsarError> {
let mut current = &mut self.header;
for component in p.components() {
if matches!(component, Component::RootDir | Component::CurDir) {
continue;
}
let name = component.as_os_str().to_str().unwrap_or("");
if name.is_empty() {
continue;
}
if let FilesystemEntry::Directory(dir) = current {
current = dir.files.entry(name.to_string()).or_insert_with(|| {
FilesystemEntry::Directory(DirectoryEntry {
files: IndexMap::new(),
unpacked: false,
})
});
} else {
return Err(AsarError::Other(format!(
"Unexpected directory state while traversing: {}",
p.display()
)));
}
}
Ok(current)
}
fn search_node_from_path(&mut self, p: &Path) -> Result<&mut FilesystemEntry, AsarError> {
let relative = p.strip_prefix(&self.src).unwrap_or(p);
if relative.as_os_str().is_empty() {
return Ok(&mut self.header);
}
let parent = relative.parent().unwrap_or(Path::new("."));
let name = relative.file_name().unwrap().to_str().unwrap_or("");
let node = self.search_node_from_directory(parent)?;
if let FilesystemEntry::Directory(dir) = node {
let entry = dir.files.entry(name.to_string()).or_insert_with(|| {
FilesystemEntry::File(FileEntry {
offset: "0".to_string(),
size: 0,
executable: false,
unpacked: false,
integrity: None,
})
});
Ok(entry)
} else {
Err(AsarError::Other(format!(
"Unexpected state while searching path: {}",
p.display()
)))
}
}
pub fn insert_directory(&mut self, p: &Path, should_unpack: bool) -> Result<(), AsarError> {
let node = self.search_node_from_path(p)?;
match node {
FilesystemEntry::Directory(dir) => {
if should_unpack {
dir.unpacked = true;
}
}
_ => {
*node = FilesystemEntry::Directory(DirectoryEntry {
files: IndexMap::new(),
unpacked: should_unpack,
});
}
}
Ok(())
}
pub fn insert_file(
&mut self,
p: &Path,
size: u64,
executable: bool,
should_unpack: bool,
integrity: Option<FileIntegrity>,
) -> Result<(), AsarError> {
if !should_unpack && size > UINT32_MAX {
return Err(AsarError::FileTooLarge {
path: p.display().to_string(),
});
}
let offset = self.offset;
let parent = p.parent().unwrap_or(Path::new("."));
let relative_parent = parent.strip_prefix(&self.src).unwrap_or(parent);
let parent_node = self.search_node_from_directory(relative_parent)?;
let parent_unpacked = match parent_node {
FilesystemEntry::Directory(dir) => dir.unpacked,
_ => false,
};
let node = self.search_node_from_path(p)?;
match node {
FilesystemEntry::File(file) => {
if should_unpack || parent_unpacked {
file.size = size;
file.unpacked = true;
file.integrity = integrity;
} else {
file.size = size;
file.offset = offset.to_string();
file.executable = executable;
file.integrity = integrity;
self.offset = offset + size;
}
}
_ => {
return Err(AsarError::Other(format!(
"Expected file entry for: {}",
p.display()
)));
}
}
Ok(())
}
pub fn insert_link(&mut self, p: &Path, link: String, should_unpack: bool) -> Result<(), AsarError> {
let parent = p.parent().unwrap_or(Path::new("."));
let relative_parent = parent.strip_prefix(&self.src).unwrap_or(parent);
let parent_node = self.search_node_from_directory(relative_parent)?;
let parent_unpacked = match parent_node {
FilesystemEntry::Directory(dir) => dir.unpacked,
_ => false,
};
let node = self.search_node_from_path(p)?;
*node = FilesystemEntry::Link(LinkEntry {
link,
unpacked: should_unpack || parent_unpacked,
});
Ok(())
}
pub fn list_files(&self, options: Option<&ListOptions>) -> Vec<String> {
let mut files = Vec::with_capacity(64);
self.fill_files_from_metadata("/", &self.header, &mut files, options);
files
}
fn fill_files_from_metadata(
&self,
base_path: &str,
metadata: &FilesystemEntry,
files: &mut Vec<String>,
options: Option<&ListOptions>,
) {
if let FilesystemEntry::Directory(dir) = metadata {
let mut keys: Vec<&String> = dir.files.keys().collect();
keys.sort_unstable();
for name in keys {
let child = &dir.files[name];
let full_path = if base_path == "/" {
format!("/{}", name)
} else {
format!("{}/{}", base_path, name)
};
let display = if let Some(opts) = options
&& opts.is_pack
{
let state = match child {
FilesystemEntry::File(f) if f.unpacked => "unpack",
FilesystemEntry::Link(l) if l.unpacked => "unpack",
FilesystemEntry::Directory(d) if d.unpacked => "unpack",
_ => "pack ",
};
format!("{} : {}", state, full_path)
} else {
full_path.clone()
};
files.push(display);
self.fill_files_from_metadata(&full_path, child, files, options);
}
}
}
pub fn get_file(&self, p: &str, follow_links: bool) -> Result<&FilesystemEntry, AsarError> {
self.get_file_internal(p, follow_links, 0, &mut HashSet::new())
}
fn check_symlink(&self, p: &str, link_target: &str, depth: usize, visited: &mut HashSet<String>) -> Result<(), AsarError> {
if visited.contains(link_target) {
return Err(AsarError::CircularSymlink(format!("\"{}\": circular symlink detected at \"{}\"", p, link_target)));
}
if depth >= SYMLINK_MAX_DEPTH {
return Err(AsarError::SymlinkDepth);
}
visited.insert(link_target.to_string());
Ok(())
}
fn get_file_internal(
&self,
p: &str,
follow_links: bool,
depth: usize,
visited: &mut HashSet<String>,
) -> Result<&FilesystemEntry, AsarError> {
let info = self.get_node_internal(p, follow_links, depth, visited)?;
if let FilesystemEntry::Link(link_entry) = info
&& follow_links
{
let link = link_entry.link.clone();
self.check_symlink(p, &link, depth, visited)?;
return self.get_file_internal(&link, follow_links, depth + 1, visited);
}
Ok(info)
}
fn get_node_internal(
&self,
p: &str,
follow_links: bool,
depth: usize,
visited: &mut HashSet<String>,
) -> Result<&FilesystemEntry, AsarError> {
let path = Path::new(p);
let parent = path.parent().unwrap_or(Path::new("."));
let name = path.file_name().unwrap_or_default().to_str().unwrap_or("");
let node = self.search_node_from_directory_readonly(parent);
if let FilesystemEntry::Link(link_entry) = node
&& follow_links
{
let resolved = Path::new(&link_entry.link).join(name);
let resolved_str = resolved.to_str().unwrap_or("").to_string();
self.check_symlink(p, &resolved_str, depth, visited)?;
return self.get_node_internal(&resolved_str, follow_links, depth + 1, visited);
}
if name.is_empty() {
Ok(node)
} else if let FilesystemEntry::Directory(dir) = node {
dir.files
.get(name)
.ok_or_else(|| AsarError::NotFound(format!("\"{}\" was not found in this archive", p)))
} else {
Err(AsarError::NotFound(format!("\"{}\" was not found in this archive", p)))
}
}
fn search_node_from_directory_readonly(&self, p: &Path) -> &FilesystemEntry {
let mut current = &self.header;
for component in p.components() {
if matches!(component, Component::RootDir | Component::CurDir) {
continue;
}
let name = component.as_os_str().to_str().unwrap_or("");
if name.is_empty() {
continue;
}
if let FilesystemEntry::Directory(dir) = current {
match dir.files.get(name) {
Some(node) => current = node,
None => return current,
}
} else {
return current;
}
}
current
}
}
pub struct ListOptions {
pub is_pack: bool,
}