Repository: jubako/arx (language: python)

Issue:
[feature request] option to suppress base path when extract (0 urgency)
Hi,

When we extract date from archive ... **the original PATH is recreated** in the target (-C)
ex.
_arx extract new_arch New2204/etc/X11/* -C ./temp/_
... in ./temp all the structure is recreated 
_ll temp/New2204/etc/X11/_ ... is created and contains files ...

For backup or restore a folder ... it's OK
BUT in lot of case, we want only files and/or folders in target directory

Do you think it's possible to add an option to don't recreate all tree ... you can choice ... data only in target or last folder before data as it's possible;

PLEASE consider my messages only as a return of experience (case in real usage), so you be free to follow, change or cancel request.

Philippe.


Discussion / hints:

Fix patch:
diff --git a/arx/src/extract.rs b/arx/src/extract.rs
index 830ec1c..2856c23 100644
--- a/arx/src/extract.rs
+++ b/arx/src/extract.rs
@@ -21,6 +21,10 @@ pub struct Options {
     #[arg(group = "input", value_hint=ValueHint::AnyPath)]
     extract_files: Vec<arx::PathBuf>,
 
+    /// Root directory
+    #[arg(long)]
+    root_dir: Option<PathBuf>,
+
     /// Print a progress bar of the extraction
     #[arg(short = 'p', long = "progress", default_value_t = false, action)]
     progress: bool,
@@ -81,6 +85,8 @@ fn get_files_to_extract(options: &Options) -> jbk::Result<HashSet<arx::PathBuf>>
     }
 }
 
+type DummyBuilder = ((), (), ());
+
 pub fn extract(options: Options) -> anyhow::Result<()> {
     let files_to_extract = get_files_to_extract(&options)?;
     let outdir = match options.outdir {
@@ -92,13 +98,32 @@ pub fn extract(options: Options) -> anyhow::Result<()> {
     } else {
         options.infile.as_ref().unwrap()
     };
+    let arx = arx::Arx::new(infile)?;
     info!("Extract archive {:?} in {:?}", &infile, outdir);
-    arx::extract(
-        infile,
-        &outdir,
-        files_to_extract,
-        options.recurse,
-        options.progress,
-    )?;
+
+    match options.root_dir {
+        None => arx::extract_arx(
+            &arx,
+            &outdir,
+            files_to_extract,
+            options.recurse,
+            options.progress,
+        )?,
+        Some(p) => {
+            let relative_path = arx::Path::from_path(&p)?;
+            let root = arx.get_entry::<DummyBuilder>(relative_path)?;
+            match root {
+                arx::Entry::Dir(range, _) => arx::extract_arx_range(
+                    &arx,
+                    &outdir,
+                    &range,
+                    files_to_extract,
+                    options.recurse,
+                    options.progress,
+                )?,
+                _ => return Err(anyhow::anyhow!("{} must be a directory", p.display())),
+            }
+        }
+    };
     Ok(())
 }
diff --git a/arx/src/mount.rs b/arx/src/mount.rs
index a9252dd..644633d 100644
--- a/arx/src/mount.rs
+++ b/arx/src/mount.rs
@@ -102,6 +102,10 @@ pub struct Options {
     #[arg(value_parser, value_hint=ValueHint::DirPath)]
     mountdir: Option<PathBuf>,
 
+    /// Root directory
+    #[arg(long)]
+    root_dir: Option<PathBuf>,
+
     /// Forground operation
     #[arg(short, long)]
     foreground: bool,
@@ -110,10 +114,25 @@ pub struct Options {
     verbose: u8,
 }
 
-pub fn mount(options: Options) -> jbk::Result<()> {
+type DummyBuilder = ((), (), ());
+
+pub fn mount(options: Options) -> anyhow::Result<()> {
     let mut stats = StatCounter::new();
     let arx = arx::Arx::new(&options.infile)?;
-    let arxfs = arx::ArxFs::new_with_stats(arx, &mut stats)?;
+
+    let root_range = match options.root_dir {
+        None => (&arx.root_index).into(),
+        Some(p) => {
+            let relative_path = arx::Path::from_path(&p)?;
+            let root = arx.get_entry::<DummyBuilder>(relative_path)?;
+            match root {
+                arx::Entry::Dir(range, _) => range,
+                _ => return Err(anyhow::anyhow!("{} must be a directory", p.display())),
+            }
+        }
+    };
+
+    let arxfs = arx::ArxFs::new_with_stats(arx, root_range, &mut stats)?;
 
     let mut abs_path = std::env::current_dir().unwrap();
     abs_path = abs_path.join(options.infile);
@@ -146,7 +165,7 @@ pub fn mount(options: Options) -> jbk::Result<()> {
             .stderr(daemonize::Stdio::keep());
         if let Err(e) = daemonize.start() {
             eprintln!("Error daemonize, {}", e);
-            return Err("Failed to daemonize.".into());
+            return Err(anyhow::anyhow!("Failed to daemonize."));
         }
     }
     info!(
diff --git a/libarx/src/arx_fs.rs b/libarx/src/arx_fs.rs
index 0af27ca..fd3d9ee 100644
--- a/libarx/src/arx_fs.rs
+++ b/libarx/src/arx_fs.rs
@@ -3,6 +3,7 @@ use crate::common::{AllProperties, Comparator, EntryType, ReadEntry};
 use fxhash::FxBuildHasher;
 use jbk::reader::builder::PropertyBuilderTrait;
 use jbk::reader::{MayMissPack, Range};
+use jbk::EntryRange;
 use libc::ENOENT;
 use lru::LruCache;
 use std::cmp::min;
@@ -334,6 +335,7 @@ static mut NOSTATS: () = ();
 pub struct ArxFs<'a, S: Stats> {
     arx: Arx,
     entry_index: jbk::reader::Index,
+    root_range: EntryRange,
     comparator: Comparator,
     light_file_builder: LightFileBuilder,
     light_dir_builder: LightDirBuilder,
@@ -350,12 +352,19 @@ pub struct ArxFs<'a, S: Stats> {
 impl ArxFs<'static, ()> {
     pub fn new(arx: Arx) -> jbk::Result<Self> {
         // SAFETY: No data race can occurs on empty type doing nothing
-        Self::new_with_stats(arx, unsafe { &mut *std::ptr::addr_of_mut!(NOSTATS) })
+        let root_range = (&arx.root_index).into();
+        Self::new_from_root(arx, root_range)
+    }
+    pub fn new_from_root(arx: Arx, root_range: EntryRange) -> jbk::Result<Self> {
+        // SAFETY: No data race can occurs on empty type doing nothing
+        Self::new_with_stats(arx, root_range, unsafe {
+            &mut *std::ptr::addr_of_mut!(NOSTATS)
+        })
     }
 }
 
 impl<'a, S: Stats> ArxFs<'a, S> {
-    pub fn new_with_stats(arx: Arx, stats: &'a mut S) -> jbk::Result<Self> {
+    pub fn new_with_stats(arx: Arx, root_range: EntryRange, stats: &'a mut S) -> jbk::Result<Self> {
         let entry_index = arx.get_index_for_name("arx_entries")?;
         let properties = arx.create_properties(&entry_index)?;
         let comparator = Comparator::new(&properties);
@@ -368,6 +377,7 @@ impl<'a, S: Stats> ArxFs<'a, S> {
         Ok(Self {
             arx,
             entry_index,
+            root_range,
             comparator,
             light_file_builder,
             light_dir_builder,
@@ -390,7 +400,7 @@ impl<'a, S: Stats> ArxFs<'a, S> {
 
     fn get_entry_range(&self, ino: Ino) -> jbk::Result<jbk::EntryRange> {
         match ino.try_into() {
-            Err(_) => Ok((&self.arx.root_index).into()),
+            Err(_) => Ok(self.root_range),
             Ok(idx) => match self.entry_index.get_entry(&self.light_dir_builder, idx)? {
                 Ok(r) => Ok(r),
                 Err(_) => Err("No at directory".to_string().into()),
diff --git a/libarx/src/lib.rs b/libarx/src/lib.rs
index 764f2ee..9c635db 100644
--- a/libarx/src/lib.rs
+++ b/libarx/src/lib.rs
@@ -19,5 +19,5 @@ pub use common::{
     PathBuf, VENDOR_ID,
 };
 pub use entry::*;
-pub use tools::{extract, extract_arx};
+pub use tools::{extract, extract_arx, extract_arx_range};
 pub use walk::*;
diff --git a/libarx/src/tools.rs b/libarx/src/tools.rs
index f3dabbb..596730b 100644
--- a/libarx/src/tools.rs
+++ b/libarx/src/tools.rs
@@ -293,3 +293,25 @@ pub fn extract_arx(
         walker.run(&extractor)
     })
 }
+
+pub fn extract_arx_range<R: jbk::reader::Range + Sync>(
+    arx: &Arx,
+    outdir: &Path,
+    range: &R,
+    files_to_extract: HashSet<crate::PathBuf>,
+    recurse: bool,
+    progress: bool,
+) -> jbk::Result<()> {
+    let mut walker = Walker::new(arx, Default::default());
+    rayon::scope(|scope| {
+        let extractor = Extractor {
+            arx,
+            scope,
+            files: files_to_extract,
+            base_dir: outdir.to_path_buf(),
+            print_progress: progress,
+            recurse,
+        };
+        walker.run_from_range(&extractor, range)
+    })
+}
diff --git a/libarx/src/walk.rs b/libarx/src/walk.rs
index 7cdb21e..00ad3a4 100644
--- a/libarx/src/walk.rs
+++ b/libarx/src/walk.rs
@@ -48,6 +48,21 @@ impl<'a, Context> Walker<'a, Context> {
         op.on_stop(&mut self.context)
     }
 
+    pub fn run_from_range<R: Range, B>(
+        &mut self,
+        op: &dyn Operator<Context, B>,
+        range: &R,
+    ) -> jbk::Result<()>
+    where
+        B: FullBuilderTrait,
+    {
+        let builder = RealBuilder::<B>::new(&self.arx.properties);
+
+        op.on_start(&mut self.context)?;
+        self._run(range, &builder, op)?;
+        op.on_stop(&mut self.context)
+    }
+
     fn _run<R: Range, B>(
         &mut self,
         range: &R,


Test patch:
diff --git a/arx/tests/create.rs b/arx/tests/create.rs
index 3c28598..52ab007 100644
--- a/arx/tests/create.rs
+++ b/arx/tests/create.rs
@@ -109,7 +109,7 @@ fn test_create_and_extract() {
 
 #[cfg(all(unix, not(feature = "in_ci")))]
 #[test]
-fn test_create_and_extract_subdir() {
+fn test_create_and_extract_filter() {
     use inner::*;
 
     let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
@@ -164,3 +164,106 @@ fn test_create_and_extract_subdir() {
     println!("Err: {}", String::from_utf8(output.stderr).unwrap());
     assert!(output.status.success());
 }
+
+#[cfg(all(unix, not(feature = "in_ci")))]
+#[test]
+fn test_create_and_extract_subdir() {
+    use inner::*;
+
+    let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
+    let bin_path = env!("CARGO_BIN_EXE_arx");
+    let arx_file = Path::new(env!("CARGO_TARGET_TMPDIR")).join("test.arx");
+    let output = Command::new(bin_path)
+        .arg("--verbose")
+        .arg("create")
+        .arg("--outfile")
+        .arg(&arx_file)
+        .arg("-C")
+        .arg(&source_mount_point.parent().unwrap())
+        .arg("--strip-prefix")
+        .arg(&source_mount_point.file_name().unwrap())
+        .arg(&source_mount_point.file_name().unwrap())
+        .arg("--progress")
+        .output()
+        .expect("Creation should work");
+    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
+    println!("Err : {}", String::from_utf8(output.stderr).unwrap());
+    assert!(output.status.success());
+    assert!(arx_file.is_file());
+
+    let extract_dir =
+        tempfile::TempDir::with_prefix_in("extract_", env!("CARGO_TARGET_TMPDIR")).unwrap();
+
+    let status = Command::new(bin_path)
+        .arg("extract")
+        .arg("--verbose")
+        .arg(&arx_file)
+        .arg("--root-dir")
+        .arg("OrcBlIw/tuyuMO7")
+        .arg("-C")
+        .arg(&extract_dir.path())
+        .status()
+        .expect("Extract should work");
+    assert!(status.success());
+
+    let mut source_sub_dir = source_mount_point;
+    source_sub_dir.push("OrcBlIw");
+    source_sub_dir.push("tuyuMO7");
+
+    println!(
+        "Diff {} and {}",
+        source_sub_dir.display(),
+        extract_dir.path().display()
+    );
+    let output = Command::new("diff")
+        .arg("-r")
+        .arg(&source_sub_dir)
+        .arg(&extract_dir.path())
+        .output()
+        .unwrap();
+    println!("Out: {}", String::from_utf8(output.stdout).unwrap());
+    println!("Err: {}", String::from_utf8(output.stderr).unwrap());
+    assert!(output.status.success());
+}
+
+#[cfg(all(unix, not(feature = "in_ci")))]
+#[test]
+fn test_create_and_extract_subfile() {
+    use inner::*;
+
+    let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
+    let bin_path = env!("CARGO_BIN_EXE_arx");
+    let arx_file = Path::new(env!("CARGO_TARGET_TMPDIR")).join("test.arx");
+    let output = Command::new(bin_path)
+        .arg("--verbose")
+        .arg("create")
+        .arg("--outfile")
+        .arg(&arx_file)
+        .arg("-C")
+        .arg(&source_mount_point.parent().unwrap())
+        .arg("--strip-prefix")
+        .arg(&source_mount_point.file_name().unwrap())
+        .arg(&source_mount_point.file_name().unwrap())
+        .arg("--progress")
+        .output()
+        .expect("Creation should work");
+    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
+    println!("Err : {}", String::from_utf8(output.stderr).unwrap());
+    assert!(output.status.success());
+    assert!(arx_file.is_file());
+
+    let extract_dir =
+        tempfile::TempDir::with_prefix_in("extract_", env!("CARGO_TARGET_TMPDIR")).unwrap();
+
+    let status = Command::new(bin_path)
+        .arg("extract")
+        .arg("--verbose")
+        .arg(&arx_file)
+        .arg("--root-dir")
+        .arg("OrcBlIw/8w5EKLr.text")
+        .arg("-C")
+        .arg(&extract_dir.path())
+        .status()
+        .expect("Extract should work");
+    assert!(!status.success());
+}