use crate::crawlfs::{crawl, FileMetadata, FileType};
use crate::disk::{
read_archive_header_sync, read_file_sync, read_filesystem_sync, write_filesystem,
ArchiveHeader, AsarError,
};
use crate::filesystem::{Filesystem, FilesystemEntry, ListOptions};
use crate::integrity::FileIntegrity;
use crate::path_validation::ensure_within;
use glob::Pattern;
use std::collections::HashMap;
use std::fs;
use std::io::{Read, Seek};
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub struct CreateOptions {
pub dot: bool,
pub ordering: Option<PathBuf>,
pub unpack: Option<String>,
pub unpack_dir: Option<String>,
}
impl Default for CreateOptions {
fn default() -> Self {
CreateOptions {
dot: true,
ordering: None,
unpack: None,
unpack_dir: None,
}
}
}
pub struct AsarArchive {
filesystem: Arc<Filesystem>,
}
impl AsarArchive {
pub fn pack(src: &Path, dest: &Path) -> Result<Self, AsarError> {
create_package(src, dest)?;
Self::open(dest)
}
pub fn pack_with_options(src: &Path, dest: &Path, options: CreateOptions) -> Result<Self, AsarError> {
create_package_with_options(src, dest, options)?;
Self::open(dest)
}
pub fn open(archive_path: &Path) -> Result<Self, AsarError> {
let filesystem = read_filesystem_sync(archive_path)?;
Ok(AsarArchive { filesystem })
}
pub fn raw_header(&self) -> Result<ArchiveHeader, AsarError> {
get_raw_header(self.filesystem.root_path())
}
pub fn list(&self) -> Result<Vec<String>, AsarError> {
Ok(self.filesystem.list_files(None))
}
pub fn list_with_flags(&self, opts: ListOptions) -> Result<Vec<String>, AsarError> {
Ok(self.filesystem.list_files(Some(&opts)))
}
pub fn stat(&self, filename: &str, follow_links: bool) -> Result<FilesystemEntry, AsarError> {
self.filesystem.get_file(filename, follow_links).cloned()
}
pub fn extract_file(&self, filename: &str) -> Result<Vec<u8>, AsarError> {
let info = self.filesystem.get_file(filename, true)?;
match info {
FilesystemEntry::File(file_entry) => read_file_sync(&self.filesystem, filename, file_entry),
FilesystemEntry::Directory(_) => Err(AsarError::Other(format!("Expected file but found directory: {}", filename))),
FilesystemEntry::Link(_) => Err(AsarError::Other(format!("Expected file but found link: {}", filename))),
}
}
pub fn extract_all(&self, dest: &Path) -> Result<(), AsarError> {
extract_all_from_fs(&self.filesystem, dest)
}
}
pub fn create_package(src: &Path, dest: &Path) -> Result<(), AsarError> {
create_package_with_options(src, dest, CreateOptions::default())
}
pub fn create_package_with_options(
src: &Path,
dest: &Path,
options: CreateOptions,
) -> Result<(), AsarError> {
let pattern = format!("{}/**/*", src.display());
let (filenames, metadata) = crawl(&pattern)?;
let (filenames, metadata) = if options.dot {
(filenames, metadata)
} else {
let filtered: Vec<_> = filenames.into_iter().filter(|p| {
let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
!name.starts_with('.')
}).collect();
let filtered_set: std::collections::HashSet<_> = filtered.iter().cloned().collect();
let filtered_meta: HashMap<_, _> = metadata.into_iter()
.filter(|(p, _)| filtered_set.contains(p))
.collect();
(filtered, filtered_meta)
};
create_package_from_files(src, dest, &filenames, metadata, options)
}
fn canonicalize_stripped(path: &Path) -> PathBuf {
let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
let s = canonical.to_string_lossy();
if cfg!(windows) && s.starts_with(r"\\?\") {
PathBuf::from(&s[4..])
} else {
canonical
}
}
pub fn create_package_from_files(
src: &Path,
dest: &Path,
filenames: &[PathBuf],
metadata: HashMap<PathBuf, FileMetadata>,
options: CreateOptions,
) -> Result<(), AsarError> {
let canonical_src = canonicalize_stripped(src);
let dest = dest.to_path_buf();
let mut filesystem = Filesystem::new(&canonical_src);
let mut file_entries: Vec<(PathBuf, bool)> = Vec::new();
let mut filenames_sorted: Vec<&PathBuf> = filenames.iter().collect();
filenames_sorted.sort_unstable();
if let Some(ref ordering_path) = options.ordering
&& let Ok(content) = fs::read_to_string(ordering_path)
{
let ordering_files: Vec<String> = content
.lines()
.map(|line| {
let line = if line.contains(':') {
line.split(':').next_back().unwrap_or(line)
} else {
line
};
line.trim().trim_start_matches('/').to_string()
})
.collect();
let mut path_index: std::collections::HashMap<&str, Vec<&PathBuf>> = std::collections::HashMap::new();
for filename in filenames {
let fname = filename.to_str().unwrap_or("");
path_index.entry(fname).or_default().push(filename);
}
let mut sorted = Vec::new();
let mut seen: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
for ordering_file in &ordering_files {
if let Some(matches) = path_index.get(ordering_file.as_str()) {
for filename in matches {
if seen.insert((*filename).clone()) {
sorted.push(*filename);
}
}
}
}
for filename in filenames {
if seen.insert(filename.clone()) {
sorted.push(filename);
}
}
filenames_sorted = sorted;
}
for filename in &filenames_sorted {
let file_meta = metadata.get(*filename);
let file_type = file_meta.map(|m| m.file_type.clone());
let abs_filename = canonicalize_stripped(filename);
let archive_path = abs_filename.strip_prefix(&canonical_src).unwrap_or(&abs_filename);
let should_unpack = {
let fname = archive_path.to_str().unwrap_or("");
let mut unpack = false;
if let Some(ref unpack_pattern) = options.unpack {
unpack = Pattern::new(unpack_pattern)
.map(|p| p.matches(fname))
.unwrap_or(false);
}
if !unpack
&& let Some(ref unpack_dir_pattern) = options.unpack_dir
{
unpack = Pattern::new(unpack_dir_pattern)
.map(|p| p.matches(fname))
.unwrap_or(false);
}
unpack
};
match file_type {
Some(FileType::Directory) => {
filesystem.insert_directory(archive_path, should_unpack)?;
}
Some(FileType::File) => {
let size = file_meta.unwrap().size;
let executable = false;
let integrity = compute_file_integrity(&abs_filename);
filesystem.insert_file(archive_path, size, executable, should_unpack, integrity)?;
file_entries.push((abs_filename, should_unpack));
}
Some(FileType::Link) => {
let link = fs::read_link(filename)
.map_err(AsarError::Io)?
.to_str()
.unwrap_or("")
.to_string();
let resolved = resolve_link(&canonical_src, archive_path, &link);
if Path::new(&resolved).is_absolute() || resolved.starts_with("..") {
return Err(AsarError::Other(format!(
"{}: file \"{}\" links out of the package",
filename.display(),
resolved
)));
}
filesystem.insert_link(archive_path, resolved, should_unpack)?;
file_entries.push((abs_filename, should_unpack));
}
None => {
return Err(AsarError::Other(format!(
"Unknown file type: {}",
filename.display()
)));
}
}
}
write_filesystem(&dest, &filesystem, &file_entries, &metadata)
}
fn compute_file_integrity(path: &Path) -> Option<FileIntegrity> {
if let Ok(mut file) = fs::File::open(path) {
FileIntegrity::from_reader(&mut file).ok()
} else {
None
}
}
fn resolve_link(src: &Path, parent_path: &Path, symlink: &str) -> String {
let parent = parent_path.parent().unwrap_or(Path::new("."));
let target = parent.join(symlink);
target
.strip_prefix(src)
.unwrap_or(&target)
.to_str()
.unwrap_or("")
.to_string()
}
pub fn stat_file(
archive_path: &Path,
filename: &str,
follow_links: bool,
) -> Result<FilesystemEntry, AsarError> {
let filesystem = read_filesystem_sync(archive_path)?;
filesystem
.get_file(filename, follow_links)
.cloned()
}
pub fn get_raw_header(archive_path: &Path) -> Result<ArchiveHeader, AsarError> {
read_archive_header_sync(archive_path)
}
pub fn list_package(
archive_path: &Path,
options: Option<ListOptions>,
) -> Result<Vec<String>, AsarError> {
let filesystem = read_filesystem_sync(archive_path)?;
Ok(filesystem.list_files(options.as_ref()))
}
pub fn extract_file(
archive_path: &Path,
filename: &str,
follow_links: bool,
) -> Result<Vec<u8>, AsarError> {
let filesystem = read_filesystem_sync(archive_path)?;
let info = filesystem
.get_file(filename, follow_links)?;
match info {
FilesystemEntry::File(file_entry) => read_file_sync(&filesystem, filename, file_entry),
FilesystemEntry::Directory(_) => Err(AsarError::Other(format!(
"Expected file but found directory: {}",
filename
))),
FilesystemEntry::Link(_) => Err(AsarError::Other(format!(
"Expected file but found link: {}",
filename
))),
}
}
pub fn extract_all(archive_path: &Path, dest: &Path) -> Result<(), AsarError> {
let filesystem = read_filesystem_sync(archive_path)?;
extract_all_from_fs(&filesystem, dest)
}
fn extract_all_from_fs(filesystem: &Arc<Filesystem>, dest: &Path) -> Result<(), AsarError> {
let file_list = filesystem.list_files(None);
fs::create_dir_all(dest)?;
let archive_path = filesystem.root_path();
let header_size = filesystem.header_size() as u64;
let archive_size = fs::metadata(archive_path)?.len();
let data_start = 8 + header_size;
let data_size = archive_size.saturating_sub(data_start);
let mut data_buf = Vec::new();
if data_size > 0 {
let mut file = fs::File::open(archive_path)?;
file.seek(std::io::SeekFrom::Start(data_start))?;
let ds = usize::try_from(data_size).map_err(|_| AsarError::Other("data size overflow".into()))?;
data_buf.resize(ds, 0);
file.read_exact(&mut data_buf)?;
}
let mut extraction_errors: Vec<String> = Vec::new();
for full_path in &file_list {
let filename = &full_path[1..];
let dest_filename = ensure_within(dest, filename)?;
let file_entry = filesystem.get_file(filename, cfg!(windows))?;
match file_entry {
FilesystemEntry::Directory(_) => {
fs::create_dir_all(&dest_filename)?;
}
FilesystemEntry::Link(link_entry) => {
let link_path = dest.join(&link_entry.link);
let dest_dir = dest_filename.parent().unwrap_or(Path::new("."));
let _relative_path = pathdiff::diff_paths(&link_path, dest_dir)
.unwrap_or_else(|| PathBuf::from(&link_entry.link));
let _ = fs::remove_file(&dest_filename);
#[cfg(unix)]
{
std::os::unix::fs::symlink(&_relative_path, &dest_filename)?;
}
#[cfg(windows)]
{
if let Some(parent) = dest_filename.parent() {
fs::create_dir_all(parent)?;
}
}
}
FilesystemEntry::File(file_info) => {
let result: Result<Vec<u8>, AsarError> = if file_info.unpacked {
let unpacked_dir = format!("{}.unpacked", filesystem.root_path().display());
fs::read(ensure_within(Path::new(&unpacked_dir), filename)?)
.map_err(AsarError::Io)
} else if file_info.size == 0 {
Ok(Vec::new())
} else {
let offset: u64 = file_info.offset.parse().map_err(|_| AsarError::Other("Invalid offset".to_string()))?;
let start = usize::try_from(offset).map_err(|_| AsarError::Other("offset overflow".into()))?;
let end = start + usize::try_from(file_info.size).map_err(|_| AsarError::Other("size overflow".into()))?;
if end <= data_buf.len() {
Ok(data_buf[start..end].to_vec())
} else {
Err(AsarError::Other("Data out of bounds".to_string()))
}
};
match result {
Ok(content) => {
if let Some(parent) = dest_filename.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&dest_filename, content)?;
#[cfg(unix)]
if file_info.executable {
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = fs::metadata(&dest_filename) {
let mut perms = meta.permissions();
perms.set_mode(0o755);
let _ = fs::set_permissions(&dest_filename, perms);
}
}
}
Err(e) => {
extraction_errors.push(format!("{}: {}", full_path, e));
}
}
}
}
}
if !extraction_errors.is_empty() {
return Err(AsarError::Other(format!(
"Unable to extract some files:\n\n{}",
extraction_errors.join("\n\n")
)));
}
Ok(())
}