1use crate::crawlfs::{crawl, FileMetadata, FileType};
2use crate::disk::{
3 read_archive_header_sync, read_file_sync, read_filesystem_sync, write_filesystem,
4 ArchiveHeader, AsarError,
5};
6use crate::filesystem::{Filesystem, FilesystemEntry, ListOptions};
7use crate::integrity::FileIntegrity;
8use crate::path_validation::ensure_within;
9use glob::Pattern;
10use std::collections::HashMap;
11use std::fs;
12use std::io::{Read, Seek};
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15
16pub struct CreateOptions {
21 pub dot: bool,
23 pub ordering: Option<PathBuf>,
25 pub unpack: Option<String>,
27 pub unpack_dir: Option<String>,
29}
30
31impl Default for CreateOptions {
32 fn default() -> Self {
33 CreateOptions {
34 dot: true,
35 ordering: None,
36 unpack: None,
37 unpack_dir: None,
38 }
39 }
40}
41
42pub struct AsarArchive {
64 filesystem: Arc<Filesystem>,
65}
66
67impl AsarArchive {
68 pub fn pack(src: &Path, dest: &Path) -> Result<Self, AsarError> {
70 create_package(src, dest)?;
71 Self::open(dest)
72 }
73
74 pub fn pack_with_options(src: &Path, dest: &Path, options: CreateOptions) -> Result<Self, AsarError> {
76 create_package_with_options(src, dest, options)?;
77 Self::open(dest)
78 }
79
80 pub fn open(archive_path: &Path) -> Result<Self, AsarError> {
82 let filesystem = read_filesystem_sync(archive_path)?;
83 Ok(AsarArchive { filesystem })
84 }
85
86 pub fn raw_header(&self) -> Result<ArchiveHeader, AsarError> {
88 get_raw_header(self.filesystem.root_path())
89 }
90
91 pub fn list(&self) -> Result<Vec<String>, AsarError> {
93 Ok(self.filesystem.list_files(None))
94 }
95
96 pub fn list_with_flags(&self, opts: ListOptions) -> Result<Vec<String>, AsarError> {
98 Ok(self.filesystem.list_files(Some(&opts)))
99 }
100
101 pub fn stat(&self, filename: &str, follow_links: bool) -> Result<FilesystemEntry, AsarError> {
103 self.filesystem.get_file(filename, follow_links).cloned()
104 }
105
106 pub fn extract_file(&self, filename: &str) -> Result<Vec<u8>, AsarError> {
108 let info = self.filesystem.get_file(filename, true)?;
109 match info {
110 FilesystemEntry::File(file_entry) => read_file_sync(&self.filesystem, filename, file_entry),
111 FilesystemEntry::Directory(_) => Err(AsarError::Other(format!("Expected file but found directory: {}", filename))),
112 FilesystemEntry::Link(_) => Err(AsarError::Other(format!("Expected file but found link: {}", filename))),
113 }
114 }
115
116 pub fn extract_all(&self, dest: &Path) -> Result<(), AsarError> {
118 extract_all_from_fs(&self.filesystem, dest)
119 }
120}
121
122pub fn create_package(src: &Path, dest: &Path) -> Result<(), AsarError> {
124 create_package_with_options(src, dest, CreateOptions::default())
125}
126
127pub fn create_package_with_options(
129 src: &Path,
130 dest: &Path,
131 options: CreateOptions,
132) -> Result<(), AsarError> {
133 let pattern = format!("{}/**/*", src.display());
134 let (filenames, metadata) = crawl(&pattern)?;
135 let (filenames, metadata) = if options.dot {
136 (filenames, metadata)
137 } else {
138 let filtered: Vec<_> = filenames.into_iter().filter(|p| {
139 let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
140 !name.starts_with('.')
141 }).collect();
142 let filtered_set: std::collections::HashSet<_> = filtered.iter().cloned().collect();
143 let filtered_meta: HashMap<_, _> = metadata.into_iter()
144 .filter(|(p, _)| filtered_set.contains(p))
145 .collect();
146 (filtered, filtered_meta)
147 };
148 create_package_from_files(src, dest, &filenames, metadata, options)
149}
150
151fn canonicalize_stripped(path: &Path) -> PathBuf {
152 let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
153 let s = canonical.to_string_lossy();
154 if cfg!(windows) && s.starts_with(r"\\?\") {
155 PathBuf::from(&s[4..])
156 } else {
157 canonical
158 }
159}
160
161pub fn create_package_from_files(
167 src: &Path,
168 dest: &Path,
169 filenames: &[PathBuf],
170 metadata: HashMap<PathBuf, FileMetadata>,
171 options: CreateOptions,
172) -> Result<(), AsarError> {
173 let canonical_src = canonicalize_stripped(src);
174 let dest = dest.to_path_buf();
175
176 let mut filesystem = Filesystem::new(&canonical_src);
177 let mut file_entries: Vec<(PathBuf, bool)> = Vec::new();
178
179 let mut filenames_sorted: Vec<&PathBuf> = filenames.iter().collect();
180 filenames_sorted.sort_unstable();
181
182 if let Some(ref ordering_path) = options.ordering
183 && let Ok(content) = fs::read_to_string(ordering_path)
184 {
185 let ordering_files: Vec<String> = content
186 .lines()
187 .map(|line| {
188 let line = if line.contains(':') {
189 line.split(':').next_back().unwrap_or(line)
190 } else {
191 line
192 };
193 line.trim().trim_start_matches('/').to_string()
194 })
195 .collect();
196
197 let mut path_index: std::collections::HashMap<&str, Vec<&PathBuf>> = std::collections::HashMap::new();
198 for filename in filenames {
199 let fname = filename.to_str().unwrap_or("");
200 path_index.entry(fname).or_default().push(filename);
201 }
202
203 let mut sorted = Vec::new();
204 let mut seen: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
205
206 for ordering_file in &ordering_files {
207 if let Some(matches) = path_index.get(ordering_file.as_str()) {
208 for filename in matches {
209 if seen.insert((*filename).clone()) {
210 sorted.push(*filename);
211 }
212 }
213 }
214 }
215
216 for filename in filenames {
217 if seen.insert(filename.clone()) {
218 sorted.push(filename);
219 }
220 }
221
222 filenames_sorted = sorted;
223 }
224
225 for filename in &filenames_sorted {
226 let file_meta = metadata.get(*filename);
227 let file_type = file_meta.map(|m| m.file_type.clone());
228 let abs_filename = canonicalize_stripped(filename);
229 let archive_path = abs_filename.strip_prefix(&canonical_src).unwrap_or(&abs_filename);
230
231 let should_unpack = {
232 let fname = archive_path.to_str().unwrap_or("");
233 let mut unpack = false;
234 if let Some(ref unpack_pattern) = options.unpack {
235 unpack = Pattern::new(unpack_pattern)
236 .map(|p| p.matches(fname))
237 .unwrap_or(false);
238 }
239 if !unpack
240 && let Some(ref unpack_dir_pattern) = options.unpack_dir
241 {
242 unpack = Pattern::new(unpack_dir_pattern)
243 .map(|p| p.matches(fname))
244 .unwrap_or(false);
245 }
246 unpack
247 };
248
249 match file_type {
250 Some(FileType::Directory) => {
251 filesystem.insert_directory(archive_path, should_unpack)?;
252 }
253 Some(FileType::File) => {
254 let size = file_meta.unwrap().size;
255 let executable = false;
256 let integrity = compute_file_integrity(&abs_filename);
257 filesystem.insert_file(archive_path, size, executable, should_unpack, integrity)?;
258 file_entries.push((abs_filename, should_unpack));
259 }
260 Some(FileType::Link) => {
261 let link = fs::read_link(filename)
262 .map_err(AsarError::Io)?
263 .to_str()
264 .unwrap_or("")
265 .to_string();
266 let resolved = resolve_link(&canonical_src, archive_path, &link);
267 if Path::new(&resolved).is_absolute() || resolved.starts_with("..") {
268 return Err(AsarError::Other(format!(
269 "{}: file \"{}\" links out of the package",
270 filename.display(),
271 resolved
272 )));
273 }
274 filesystem.insert_link(archive_path, resolved, should_unpack)?;
275 file_entries.push((abs_filename, should_unpack));
276 }
277 None => {
278 return Err(AsarError::Other(format!(
279 "Unknown file type: {}",
280 filename.display()
281 )));
282 }
283 }
284 }
285
286 write_filesystem(&dest, &filesystem, &file_entries, &metadata)
287}
288
289fn compute_file_integrity(path: &Path) -> Option<FileIntegrity> {
290 if let Ok(mut file) = fs::File::open(path) {
291 FileIntegrity::from_reader(&mut file).ok()
292 } else {
293 None
294 }
295}
296
297fn resolve_link(src: &Path, parent_path: &Path, symlink: &str) -> String {
298 let parent = parent_path.parent().unwrap_or(Path::new("."));
299 let target = parent.join(symlink);
300 target
301 .strip_prefix(src)
302 .unwrap_or(&target)
303 .to_str()
304 .unwrap_or("")
305 .to_string()
306}
307
308pub fn stat_file(
310 archive_path: &Path,
311 filename: &str,
312 follow_links: bool,
313) -> Result<FilesystemEntry, AsarError> {
314 let filesystem = read_filesystem_sync(archive_path)?;
315 filesystem
316 .get_file(filename, follow_links)
317 .cloned()
318}
319
320pub fn get_raw_header(archive_path: &Path) -> Result<ArchiveHeader, AsarError> {
322 read_archive_header_sync(archive_path)
323}
324
325pub fn list_package(
327 archive_path: &Path,
328 options: Option<ListOptions>,
329) -> Result<Vec<String>, AsarError> {
330 let filesystem = read_filesystem_sync(archive_path)?;
331 Ok(filesystem.list_files(options.as_ref()))
332}
333
334pub fn extract_file(
336 archive_path: &Path,
337 filename: &str,
338 follow_links: bool,
339) -> Result<Vec<u8>, AsarError> {
340 let filesystem = read_filesystem_sync(archive_path)?;
341 let info = filesystem
342 .get_file(filename, follow_links)?;
343
344 match info {
345 FilesystemEntry::File(file_entry) => read_file_sync(&filesystem, filename, file_entry),
346 FilesystemEntry::Directory(_) => Err(AsarError::Other(format!(
347 "Expected file but found directory: {}",
348 filename
349 ))),
350 FilesystemEntry::Link(_) => Err(AsarError::Other(format!(
351 "Expected file but found link: {}",
352 filename
353 ))),
354 }
355}
356
357pub fn extract_all(archive_path: &Path, dest: &Path) -> Result<(), AsarError> {
359 let filesystem = read_filesystem_sync(archive_path)?;
360 extract_all_from_fs(&filesystem, dest)
361}
362
363fn extract_all_from_fs(filesystem: &Arc<Filesystem>, dest: &Path) -> Result<(), AsarError> {
364 let file_list = filesystem.list_files(None);
365
366 fs::create_dir_all(dest)?;
367
368 let archive_path = filesystem.root_path();
370 let header_size = filesystem.header_size() as u64;
371 let archive_size = fs::metadata(archive_path)?.len();
372 let data_start = 8 + header_size;
373 let data_size = archive_size.saturating_sub(data_start);
374 let mut data_buf = Vec::new();
375 if data_size > 0 {
376 let mut file = fs::File::open(archive_path)?;
377 file.seek(std::io::SeekFrom::Start(data_start))?;
378 let ds = usize::try_from(data_size).map_err(|_| AsarError::Other("data size overflow".into()))?;
379 data_buf.resize(ds, 0);
380 file.read_exact(&mut data_buf)?;
381 }
382
383 let mut extraction_errors: Vec<String> = Vec::new();
384
385 for full_path in &file_list {
386 let filename = &full_path[1..];
387 let dest_filename = ensure_within(dest, filename)?;
388
389 let file_entry = filesystem.get_file(filename, cfg!(windows))?;
390
391 match file_entry {
392 FilesystemEntry::Directory(_) => {
393 fs::create_dir_all(&dest_filename)?;
394 }
395 FilesystemEntry::Link(link_entry) => {
396 let link_path = dest.join(&link_entry.link);
397 let dest_dir = dest_filename.parent().unwrap_or(Path::new("."));
398 let _relative_path = pathdiff::diff_paths(&link_path, dest_dir)
399 .unwrap_or_else(|| PathBuf::from(&link_entry.link));
400 let _ = fs::remove_file(&dest_filename);
401 #[cfg(unix)]
402 {
403 std::os::unix::fs::symlink(&_relative_path, &dest_filename)?;
404 }
405 #[cfg(windows)]
406 {
407 if let Some(parent) = dest_filename.parent() {
408 fs::create_dir_all(parent)?;
409 }
410 }
411 }
412 FilesystemEntry::File(file_info) => {
413 let result: Result<Vec<u8>, AsarError> = if file_info.unpacked {
414 let unpacked_dir = format!("{}.unpacked", filesystem.root_path().display());
415 fs::read(ensure_within(Path::new(&unpacked_dir), filename)?)
416 .map_err(AsarError::Io)
417 } else if file_info.size == 0 {
418 Ok(Vec::new())
419 } else {
420 let offset: u64 = file_info.offset.parse().map_err(|_| AsarError::Other("Invalid offset".to_string()))?;
421 let start = usize::try_from(offset).map_err(|_| AsarError::Other("offset overflow".into()))?;
422 let end = start + usize::try_from(file_info.size).map_err(|_| AsarError::Other("size overflow".into()))?;
423 if end <= data_buf.len() {
424 Ok(data_buf[start..end].to_vec())
425 } else {
426 Err(AsarError::Other("Data out of bounds".to_string()))
427 }
428 };
429
430 match result {
431 Ok(content) => {
432 if let Some(parent) = dest_filename.parent() {
433 fs::create_dir_all(parent)?;
434 }
435 fs::write(&dest_filename, content)?;
436 #[cfg(unix)]
437 if file_info.executable {
438 use std::os::unix::fs::PermissionsExt;
439 if let Ok(meta) = fs::metadata(&dest_filename) {
440 let mut perms = meta.permissions();
441 perms.set_mode(0o755);
442 let _ = fs::set_permissions(&dest_filename, perms);
443 }
444 }
445 }
446 Err(e) => {
447 extraction_errors.push(format!("{}: {}", full_path, e));
448 }
449 }
450 }
451 }
452 }
453
454 if !extraction_errors.is_empty() {
455 return Err(AsarError::Other(format!(
456 "Unable to extract some files:\n\n{}",
457 extraction_errors.join("\n\n")
458 )));
459 }
460
461 Ok(())
462}