Repository: jubako/arx (language: python)

Issue:
arx crashed on my first archive...
So, I created my first arx archive ("arxive"? :zany_face:):

```
$ arx create Projects/ibus-cangjie/ -v -o ibus-cangjie
```

Then I tried to extract it;

```
$ arx extract ibus-cangjie
[... snip 49093 lines of errors ...]
```

Alright, that went well... :facepalm: 

So, I now have 13389 files or error reports. Want to look at them one by one? :joy: 

Here is one:

```
$ cat /tmp/report-fffde92d-da93-422f-a90d-b3a72af3d10a.toml
"name" = "arx"
"operating_system" = "Fedora 41.0.0 [64-bit]"
"crate_version" = "0.3.2"
"explanation" = """
Panic occurred in file '/home/bochecha/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libarx-0.3.2/src/tools.rs' at line 217
"""
"cause" = 'called `Result::unwrap()` on an `Err` value: Os { code: 17, kind: AlreadyExists, message: "File exists" }'
"method" = "Panic"
"backtrace" = """
   0:     0x561bd5110e64 - rust_begin_unwind
   1:     0x561bd4d24e83 - core::panicking::panic_fmt::h815ef412e87b03aa
   2:     0x561bd4d252f6 - core::result::unwrap_failed::h0a82129c7e885128
   3:     0x561bd4e5dd00 - rayon_core::scope::ScopeBase::execute_job_closure::h2cc325eb606b44e8
   4:     0x561bd4e4f498 - <rayon_core::job::HeapJob<BODY> as rayon_core::job::Job>::execute::hc72360b72d159738
   5:     0x561bd4d217e6 - rayon_core::registry::WorkerThread::wait_until_cold::h901a170b160c8aeb
   6:     0x561bd506a082 - rayon_core::registry::ThreadBuilder::run::h0dc8128d4faea263
   7:     0x561bd506fd5a - std::sys::backtrace::__rust_begin_short_backtrace::ha47dd9667ecf3f6b
   8:     0x561bd506ecf2 - core::ops::function::FnOnce::call_once{{vtable.shim}}::hcdbe244f1d4fa616
   9:     0x561bd511746b - std::sys::pal::unix::thread::Thread::new::thread_start::h8914793b525b6448
  10:     0x7f0768470cd7 - start_thread
  11:     0x7f07684f4c8c - __GI___clone3
  12:                0x0 - <unresolved>
"""
```


Discussion / hints:
I suspect you have tried to extract the archive in the same place than the source. So writing the files fails because (source) files already exist.
And as writing files is done in threads this is not properly catched.

Thanks for the report. I will probably fix it by printing a proper message if the file already exists and also add an option to overwrite existing files or not.
Ah, that's very possible indeed...

I was in my home directory when I created the archive with the path `Projects/ibus-cangjie/`.

Not knowing how arx would extract I ran in my home dir the command `arx extract`, and indeed, at that point the whole tree still exists...
> Ah, that's very possible indeed...
> 
> I was in my home directory when I created the archive with the path `Projects/ibus-cangjie/`.
> 
> Not knowing how arx would extract I ran in my home dir the command `arx extract`, and indeed, at that point the whole tree still exists...

And that was exactly the problem:

```
$ arx extract ibus-cangjie -C /tmp
```

And that worked perfectly. :wink: 

So yes, a warning that the path already exists and what to do about it (overwrite, cancel) would work fine. :blush: 

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 76e44bb..b1a8d29 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -49,6 +49,8 @@ jobs:
 
       - name: Test code
         run: cargo test --workspace --verbose --features in_ci
+        env:
+          RUST_BACKTRACE: 1
 
       - name: Upload artifacts
         uses: actions/upload-artifact@v4
diff --git a/Cargo.lock b/Cargo.lock
index 2efa0fa..8ffe2c4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -136,7 +136,7 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
 
 [[package]]
 name = "arx"
-version = "0.3.2"
+version = "0.4.0-dev.0"
 dependencies = [
  "anyhow",
  "clap",
@@ -146,6 +146,7 @@ dependencies = [
  "const_format",
  "daemonize",
  "env_logger",
+ "filetime",
  "format-bytes",
  "git-version",
  "human-panic",
@@ -1090,7 +1091,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
 [[package]]
 name = "libarx"
-version = "0.3.2"
+version = "0.4.0-dev.0"
 dependencies = [
  "blake3",
  "bstr",
@@ -1464,7 +1465,7 @@ dependencies = [
 
 [[package]]
 name = "python-libarx"
-version = "0.3.2"
+version = "0.4.0-dev.0"
 dependencies = [
  "jubako",
  "libarx",
@@ -1789,7 +1790,7 @@ dependencies = [
 
 [[package]]
 name = "tar2arx"
-version = "0.3.2"
+version = "0.4.0-dev.0"
 dependencies = [
  "anyhow",
  "bstr",
@@ -2433,7 +2434,7 @@ dependencies = [
 
 [[package]]
 name = "zip2arx"
-version = "0.3.2"
+version = "0.4.0-dev.0"
 dependencies = [
  "clap",
  "clap_complete",
diff --git a/arx/Cargo.toml b/arx/Cargo.toml
index 3d3799d..8987a8e 100644
--- a/arx/Cargo.toml
+++ b/arx/Cargo.toml
@@ -38,6 +38,7 @@ rand = { version = "0.8.5", features = ["small_rng"]}
 lipsum = "0.9.0"
 tempfile = "3.8.0"
 format-bytes = "0.3.0"
+filetime = "0.2.25"
 
 [features]
 default = ["zstd", "fuse"]
diff --git a/arx/src/bin/auto_mount.rs b/arx/src/bin/auto_mount.rs
index 88a8154..781a74c 100644
--- a/arx/src/bin/auto_mount.rs
+++ b/arx/src/bin/auto_mount.rs
@@ -34,7 +34,6 @@ mod inner {
 #[cfg(unix)]
 fn main() -> ExitCode {
     use inner::*;
-    use log::{error, info};
 
     human_panic::setup_panic!(human_panic::Metadata::new(
         env!("CARGO_PKG_NAME"),
@@ -46,18 +45,18 @@ fn main() -> ExitCode {
     match env::current_exe() {
         Ok(exe_path) => {
             if args.verbose > 0 {
-                info!("Auto Mount archive {:?} in {:?}", exe_path, args.mountdir);
+                print!("Auto Mount archive {:?} in {:?}", exe_path, args.mountdir);
             }
             match mount(exe_path, args.mountdir) {
                 Ok(()) => ExitCode::SUCCESS,
                 Err(e) => match e.error {
                     jbk::ErrorKind::NotAJbk => {
-                        error!("Impossible to locate a Jubako archive in the executable.");
-                        error!("This binary is not intented to be directly used, you must put a Jubako archive at its end.");
+                        eprintln!("Impossible to locate a Jubako archive in the executable.");
+                        eprintln!("This binary is not intented to be directly used, you must put a Jubako archive at its end.");
                         ExitCode::FAILURE
                     }
                     _ => {
-                        error!("Error: {e}");
+                        eprintln!("Error: {e}");
                         ExitCode::FAILURE
                     }
                 },
diff --git a/arx/src/bin/mount_fuse_arx.rs b/arx/src/bin/mount_fuse_arx.rs
index efd1c4f..cf7fd7c 100644
--- a/arx/src/bin/mount_fuse_arx.rs
+++ b/arx/src/bin/mount_fuse_arx.rs
@@ -33,7 +33,6 @@ mod inner {
 #[cfg(unix)]
 fn main() -> jbk::Result<()> {
     use inner::*;
-    use log::error;
 
     human_panic::setup_panic!(human_panic::Metadata::new(
         env!("CARGO_PKG_NAME"),
@@ -43,7 +42,7 @@ fn main() -> jbk::Result<()> {
     let args = Cli::parse();
 
     if args.option.contains(&"rw".into()) {
-        error!("arx cannot be mounted rw");
+        eprintln!("arx cannot be mounted rw");
         return Err("arx cannot be mounted rw".into());
     }
 
diff --git a/arx/src/extract.rs b/arx/src/extract.rs
index 2856c23..a072e11 100644
--- a/arx/src/extract.rs
+++ b/arx/src/extract.rs
@@ -70,6 +70,9 @@ pub struct Options {
         required_unless_present("infile")
     )]
     infile_old: Option<PathBuf>,
+
+    #[arg(long, default_value = "warn")]
+    overwrite: arx::Overwrite,
 }
 
 fn get_files_to_extract(options: &Options) -> jbk::Result<HashSet<arx::PathBuf>> {
@@ -108,6 +111,7 @@ pub fn extract(options: Options) -> anyhow::Result<()> {
             files_to_extract,
             options.recurse,
             options.progress,
+            options.overwrite,
         )?,
         Some(p) => {
             let relative_path = arx::Path::from_path(&p)?;
@@ -120,6 +124,7 @@ pub fn extract(options: Options) -> anyhow::Result<()> {
                     files_to_extract,
                     options.recurse,
                     options.progress,
+                    options.overwrite,
                 )?,
                 _ => return Err(anyhow::anyhow!("{} must be a directory", p.display())),
             }
diff --git a/arx/src/main.rs b/arx/src/main.rs
index 27dda8c..04b1e88 100644
--- a/arx/src/main.rs
+++ b/arx/src/main.rs
@@ -8,7 +8,6 @@ mod mount;
 
 use anyhow::Result;
 use clap::{CommandFactory, Parser};
-use log::error;
 use std::process::ExitCode;
 
 const VERSION: &str = const_format::formatcp!(
@@ -146,7 +145,7 @@ fn main() -> ExitCode {
     match run() {
         Ok(()) => ExitCode::SUCCESS,
         Err(e) => {
-            error!("Error : {e:#}");
+            eprintln!("Error : {e:#}");
             ExitCode::FAILURE
         }
     }
diff --git a/libarx/src/lib.rs b/libarx/src/lib.rs
index 9c635db..d5f4af2 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, extract_arx_range};
+pub use tools::{extract, extract_arx, extract_arx_range, Overwrite};
 pub use walk::*;
diff --git a/libarx/src/tools.rs b/libarx/src/tools.rs
index 596730b..f7bde4a 100644
--- a/libarx/src/tools.rs
+++ b/libarx/src/tools.rs
@@ -1,11 +1,14 @@
 use std::collections::HashSet;
-use std::fs::{create_dir, create_dir_all, OpenOptions};
-use std::io::Write;
+use std::fs::{create_dir_all, OpenOptions};
+use std::io::{ErrorKind, Write};
 #[cfg(unix)]
 use std::os::unix::fs::symlink;
 #[cfg(windows)]
 use std::os::windows::fs::symlink_file as symlink;
 use std::path::{Path, PathBuf};
+use std::sync::atomic::AtomicBool;
+use std::sync::Arc;
+use std::time::{Duration, SystemTime};
 
 use crate::{AllProperties, Arx, Builder, Walker};
 use jbk::reader::builder::PropertyBuilderTrait;
@@ -15,16 +18,19 @@ use jbk::reader::MayMissPack;
 struct FileEntry {
     path: String,
     content: jbk::ContentAddress,
+    mtime: u64,
 }
 
 struct Link {
     path: String,
     target: String,
+    mtime: u64,
 }
 
 struct FileBuilder {
     path_property: jbk::reader::builder::ArrayProperty,
     content_address_property: jbk::reader::builder::ContentProperty,
+    mtime_property: jbk::reader::builder::IntProperty,
 }
 
 impl Builder for FileBuilder {
@@ -34,6 +40,7 @@ impl Builder for FileBuilder {
         Self {
             path_property: properties.path_property.clone(),
             content_address_property: properties.file_content_address_property,
+            mtime_property: properties.mtime_property.clone(),
         }
     }
 
@@ -42,9 +49,11 @@ impl Builder for FileBuilder {
         let mut path = vec![];
         path_prop.resolve_to_vec(&mut path)?;
         let content = self.content_address_property.create(reader)?;
+        let mtime = self.mtime_property.create(reader)?;
         Ok(FileEntry {
             path: String::from_utf8(path)?,
             content,
+            mtime,
         })
     }
 }
@@ -52,6 +61,7 @@ impl Builder for FileBuilder {
 struct LinkBuilder {
     path_property: jbk::reader::builder::ArrayProperty,
     link_property: jbk::reader::builder::ArrayProperty,
+    mtime_property: jbk::reader::builder::IntProperty,
 }
 
 impl Builder for LinkBuilder {
@@ -61,6 +71,7 @@ impl Builder for LinkBuilder {
         Self {
             path_property: properties.path_property.clone(),
             link_property: properties.link_target_property.clone(),
+            mtime_property: properties.mtime_property.clone(),
         }
     }
 
@@ -72,9 +83,11 @@ impl Builder for LinkBuilder {
         let target_prop = self.link_property.create(reader)?;
         let mut target = vec![];
         target_prop.resolve_to_vec(&mut target)?;
+        let mtime = self.mtime_property.create(reader)?;
         Ok(Link {
             path: String::from_utf8(path)?,
             target: String::from_utf8(target)?,
+            mtime,
         })
     }
 }
@@ -102,6 +115,16 @@ impl Builder for DirBuilder {
 
 type FullBuilder = (FileBuilder, LinkBuilder, DirBuilder);
 
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "cmd_utils", derive(clap::ValueEnum))]
+pub enum Overwrite {
+    Skip,
+    Warn,
+    Newer,
+    Overwrite,
+    Error,
+}
+
 struct Extractor<'a, 'scope>
 where
     'a: 'scope,
@@ -112,6 +135,8 @@ where
     base_dir: PathBuf,
     print_progress: bool,
     recurse: bool,
+    extract_ok: Arc<AtomicBool>,
+    overwrite: Overwrite,
 }
 
 impl Extractor<'_, '_> {
@@ -177,11 +202,9 @@ where
             return Ok(false);
         }
         let abs_path = self.abs_path(current_path);
-        if !abs_path.try_exists()? {
-            create_dir(&abs_path)?;
-            if self.print_progress {
-                println!("{}", abs_path.display());
-            }
+        create_dir_all(&abs_path)?;
+        if self.print_progress {
+            println!("{}", abs_path.display());
         }
         Ok(true)
     }
@@ -205,55 +228,128 @@ where
             return Ok(());
         }
         let bytes = arx.container.get_bytes(entry_content).unwrap();
+        let extract_ok = Arc::clone(&self.extract_ok);
 
-        self.scope.spawn(move |_scope| {
-            match bytes {
-                MayMissPack::FOUND(bytes) => {
-                    let abs_path = abs_path.clone();
-                    let mut file = OpenOptions::new()
-                        .write(true)
-                        .create_new(true)
-                        .open(abs_path)
-                        .unwrap();
+        match bytes {
+            MayMissPack::FOUND(bytes) => {
+                let abs_path = abs_path.clone();
+                let mut file = match OpenOptions::new()
+                    .write(true)
+                    .create_new(true)
+                    .open(&abs_path)
+                {
+                    Ok(f) => f,
+                    Err(e) => match e.kind() {
+                        ErrorKind::AlreadyExists => match self.overwrite {
+                            Overwrite::Skip => return Ok(()),
+                            Overwrite::Warn => {
+                                eprintln!("File {} already exists.", abs_path.display());
+                                return Ok(());
+                            }
+                            Overwrite::Newer => {
+                                let existing_metadata = std::fs::metadata(&abs_path)?;
+                                let existing_time = existing_metadata.modified()?;
+                                let new_time =
+                                    SystemTime::UNIX_EPOCH + Duration::from_secs(entry.mtime);
+                                if new_time >= existing_time {
+                                    OpenOptions::new()
+                                        .write(true)
+                                        .truncate(true)
+                                        .open(&abs_path)?
+                                } else {
+                                    return Ok(());
+                                }
+                            }
+                            Overwrite::Overwrite => OpenOptions::new()
+                                .write(true)
+                                .truncate(true)
+                                .open(&abs_path)?,
+                            Overwrite::Error => {
+                                return Err(
+                                    format!("File {} already exists.", abs_path.display()).into()
+                                );
+                            }
+                        },
+                        _ => return Err(e.into()),
+                    },
+                };
 
+                self.scope.spawn(move |_scope| {
                     // Don't use std::io::copy as it use an internal buffer where it read data into before writing in file.
                     // If content is compressed, we already have a buffer. Same thing for uncompress as the cluster is probably mmapped.
                     let size = bytes.size().into_u64();
                     let mut offset = 0;
-                    loop {
-                        let sub_size = std::cmp::min(size - offset, 4 * 1024) as usize;
-                        let written = file
-                            .write(&bytes.get_slice(offset.into(), sub_size).unwrap())
-                            .unwrap();
-                        offset += written as u64;
-                        if offset == size {
-                            break;
+                    let mut write_function = move || -> jbk::Result<()> {
+                        loop {
+                            let sub_size = std::cmp::min(size - offset, 4 * 1024) as usize;
+                            let written = file.write(&bytes.get_slice(offset.into(), sub_size)?)?;
+                            offset += written as u64;
+                            if offset == size {
+                                break;
+                            }
                         }
+                        Ok(())
+                    };
+                    if let Err(e) = write_function() {
+                        log::error!("Error writing content to {} : {}", abs_path.display(), e);
+                        extract_ok.store(false, std::sync::atomic::Ordering::Relaxed);
                     }
-                }
-                MayMissPack::MISSING(pack_info) => {
-                    eprintln!(
-                        "Missing pack {} for {}. Declared location is {}",
-                        pack_info.uuid,
-                        abs_path.display(),
-                        String::from_utf8_lossy(&pack_info.pack_location)
-                    );
-                }
+                });
             }
-            if print_progress {
-                println!("{}", abs_path.display());
+            MayMissPack::MISSING(pack_info) => {
+                log::error!(
+                    "Missing pack {} for {}. Declared location is {}",
+                    pack_info.uuid,
+                    abs_path.display(),
+                    String::from_utf8_lossy(&pack_info.pack_location)
+                );
             }
-        });
+        }
+
+        if print_progress {
+            println!("{}", abs_path.display());
+        }
+
         Ok(())
     }
     fn on_link(&self, current_path: &mut crate::PathBuf, link: &Link) -> jbk::Result<()> {
+        let mut current_path = current_path.clone();
         current_path.push(&link.path);
-        if !self.should_extract(current_path, false) {
+        if !self.should_extract(&current_path, false) {
             current_path.pop();
             return Ok(());
         }
-        let abs_path = self.abs_path(current_path);
-        symlink(PathBuf::from(&link.target), PathBuf::from(&abs_path))?;
+        let abs_path = self.abs_path(&current_path);
+        if let Err(e) = symlink(PathBuf::from(&link.target), PathBuf::from(&abs_path)) {
+            match e.kind() {
+                ErrorKind::AlreadyExists => match self.overwrite {
+                    Overwrite::Skip => return Ok(()),
+                    Overwrite::Warn => {
+                        eprintln!("Link {} already exists.", abs_path.display());
+                        return Ok(());
+                    }
+                    Overwrite::Newer => {
+                        let existing_metadata = std::fs::symlink_metadata(&abs_path)?;
+                        let existing_time = existing_metadata.modified()?;
+                        let new_time = SystemTime::UNIX_EPOCH + Duration::from_secs(link.mtime);
+                        if new_time >= existing_time {
+                            std::fs::remove_file(&abs_path)?;
+                            symlink(PathBuf::from(&link.target), PathBuf::from(&abs_path))?;
+                        } else {
+                            return Ok(());
+                        }
+                    }
+                    Overwrite::Overwrite => {
+                        std::fs::remove_file(&abs_path)?;
+                        symlink(PathBuf::from(&link.target), PathBuf::from(&abs_path))?;
+                    }
+                    Overwrite::Error => {
+                        return Err(format!("Link {} already exists.", abs_path.display()).into());
+                    }
+                },
+                _ => return Err(e.into()),
+            }
+        }
         if self.print_progress {
             println!("{}", abs_path.display());
         }
@@ -268,9 +364,10 @@ pub fn extract(
     files_to_extract: HashSet<crate::PathBuf>,
     recurse: bool,
     progress: bool,
+    overwrite: Overwrite,
 ) -> jbk::Result<()> {
     let arx = Arx::new(infile)?;
-    extract_arx(&arx, outdir, files_to_extract, recurse, progress)
+    extract_arx(&arx, outdir, files_to_extract, recurse, progress, overwrite)
 }
 
 pub fn extract_arx(
@@ -279,8 +376,10 @@ pub fn extract_arx(
     files_to_extract: HashSet<crate::PathBuf>,
     recurse: bool,
     progress: bool,
+    overwrite: Overwrite,
 ) -> jbk::Result<()> {
     let mut walker = Walker::new(arx, Default::default());
+    let extract_ok = Arc::new(AtomicBool::new(true));
     rayon::scope(|scope| {
         let extractor = Extractor {
             arx,
@@ -288,10 +387,17 @@ pub fn extract_arx(
             files: files_to_extract,
             base_dir: outdir.to_path_buf(),
             print_progress: progress,
+            extract_ok: Arc::clone(&extract_ok),
             recurse,
+            overwrite,
         };
         walker.run(&extractor)
-    })
+    })?;
+    if !extract_ok.load(std::sync::atomic::Ordering::Relaxed) {
+        Err("Some errors appends during extraction.".to_owned().into())
+    } else {
+        Ok(())
+    }
 }
 
 pub fn extract_arx_range<R: jbk::reader::Range + Sync>(
@@ -301,8 +407,10 @@ pub fn extract_arx_range<R: jbk::reader::Range + Sync>(
     files_to_extract: HashSet<crate::PathBuf>,
     recurse: bool,
     progress: bool,
+    overwrite: Overwrite,
 ) -> jbk::Result<()> {
     let mut walker = Walker::new(arx, Default::default());
+    let extract_ok = Arc::new(AtomicBool::new(true));
     rayon::scope(|scope| {
         let extractor = Extractor {
             arx,
@@ -310,8 +418,15 @@ pub fn extract_arx_range<R: jbk::reader::Range + Sync>(
             files: files_to_extract,
             base_dir: outdir.to_path_buf(),
             print_progress: progress,
+            extract_ok: Arc::clone(&extract_ok),
             recurse,
+            overwrite,
         };
         walker.run_from_range(&extractor, range)
-    })
+    })?;
+    if !extract_ok.load(std::sync::atomic::Ordering::Relaxed) {
+        Err("Some errors appends during extraction.".to_owned().into())
+    } else {
+        Ok(())
+    }
 }
diff --git a/python/src/arx.rs b/python/src/arx.rs
index 5e99485..74fc708 100644
--- a/python/src/arx.rs
+++ b/python/src/arx.rs
@@ -106,7 +106,14 @@ impl Arx {
     /// Extract the whole archive in
     #[pyo3(signature=(extract_path=std::path::PathBuf::from(".")))]
     fn extract(&self, extract_path: std::path::PathBuf) -> PyResult<()> {
-        arx::extract_arx(&self.0, &extract_path, Default::default(), true, false)
-            .map_err(|e| PyRuntimeError::new_err(e.to_string()))
+        arx::extract_arx(
+            &self.0,
+            &extract_path,
+            Default::default(),
+            true,
+            false,
+            arx::Overwrite::Warn,
+        )
+        .map_err(|e| PyRuntimeError::new_err(e.to_string()))
     }
 }


Test patch:
diff --git a/arx/tests/create.rs b/arx/tests/create.rs
index 63d4190..d0f5827 100644
--- a/arx/tests/create.rs
+++ b/arx/tests/create.rs
@@ -9,7 +9,7 @@ fn test_crate_non_existant_input() -> Result {
     temp_arx!(arx_file);
     cmd!("arx", "create", "--outfile", &arx_file, "non_existant_dir").check_fail(
         b"",
-        b"[ERROR arx] Error : Input non_existant_dir path doesn't exist or cannot be accessed\n",
+        b"Error : Input non_existant_dir path doesn't exist or cannot be accessed\n",
     );
     assert!(!arx_file.exists());
     Ok(())
@@ -33,7 +33,7 @@ fn test_crate_non_existant_output_directory() -> Result {
     .check_fail(
         b"",
         &format_bytes!(
-            b"[ERROR arx] Error : Directory {} doesn't exist\n",
+            b"Error : Directory {} doesn't exist\n",
             arx_file.parent().unwrap().as_os_str().as_encoded_bytes()
         ),
     );
@@ -66,7 +66,7 @@ fn test_crate_existant_output() -> Result {
     .check_fail(
         b"",
         &format_bytes!(
-            b"[ERROR arx] Error : File {} already exists. Use option --force to overwrite it.\n",
+            b"Error : File {} already exists. Use option --force to overwrite it.\n",
             arx_file.as_os_str().as_encoded_bytes()
         ),
     );
@@ -85,7 +85,7 @@ fn test_crate_existant_output() -> Result {
         tmp_source_dir.file_name().unwrap(),
         "--force"
     )
-    .check_output(b"", b"");
+    .check_output(Some(b""), Some(b""));
     {
         let mut f = std::fs::File::open(&arx_file)?;
         let mut buf = [0; 10];
diff --git a/arx/tests/extract_mount.rs b/arx/tests/extract_mount.rs
index b47dbe3..c3492fa 100644
--- a/arx/tests/extract_mount.rs
+++ b/arx/tests/extract_mount.rs
@@ -1,6 +1,9 @@
 mod utils;
 
+use format_bytes::format_bytes;
 use std::{
+    collections::HashMap,
+    ffi::OsStr,
     path::{Path, PathBuf},
     sync::LazyLock,
 };
@@ -40,7 +43,7 @@ pub static BASE_ARX_FILE: LazyLock<TmpArx> = LazyLock::new(|| {
         source_dir.file_name().unwrap(),
         source_dir.file_name().unwrap()
     )
-    .check();
+    .check_output(Some(b""), Some(b""));
     TmpArx::new(tmp_arx_dir, tmp_arx)
 });
 
@@ -54,7 +57,7 @@ fn test_mount() -> Result {
     let arx = arx::Arx::new(arx_file)?;
     let arxfs = arx::ArxFs::new(arx)?;
     let _mount_handle = arxfs.spawn_mount("Test mounted arx".into(), mount_point.path())?;
-    assert!(tree_equal(tmp_source_dir, mount_point)?);
+    assert!(tree_diff(mount_point, tmp_source_dir, Default::default())?);
     Ok(())
 }
 
@@ -70,8 +73,23 @@ fn test_extract() -> Result {
         Default::default(),
         true,
         false,
+        arx::Overwrite::Error,
     )?;
-    assert!(tree_equal(tmp_source_dir, extract_dir)?);
+    assert!(tree_diff(extract_dir, tmp_source_dir, Default::default())?);
+    Ok(())
+}
+
+#[test]
+fn test_extract_same_dir() -> Result {
+    // This test that everything go "fine" when extracting an archive in the source directory.
+    // But here we don't want to take the risk to polute our source directory shared with other tests.
+    // So we extract twice the same archive in the same place.
+
+    let arx_file = BASE_ARX_FILE.path();
+
+    let extract_dir = tempfile::TempDir::with_prefix_in("extract_", env!("CARGO_TARGET_TMPDIR"))?;
+    cmd!("arx", "extract", &arx_file, "-C", extract_dir.path()).check_output(Some(b""), Some(b""));
+    cmd!("arx", "extract", &arx_file, "-C", extract_dir.path()).check_output(Some(b""), None);
     Ok(())
 }
 
@@ -87,12 +105,17 @@ fn test_extract_filter() -> Result {
         ["sub_dir_a".into()].into(),
         true,
         true,
+        arx::Overwrite::Error,
     )?;
 
     let source_sub_dir = join!(tmp_source_dir / "sub_dir_a");
-    let extract_sub_dir = join!((extract_dir.path()) / "sub_dir_a");
+    let extract_sub_dir = join!(extract_dir / "sub_dir_a");
 
-    assert!(tree_equal(source_sub_dir, extract_sub_dir)?);
+    assert!(tree_diff(
+        extract_sub_dir,
+        source_sub_dir,
+        Default::default()
+    )?);
     Ok(())
 }
 
@@ -112,11 +135,11 @@ fn test_extract_subdir() -> Result {
         "-C",
         extract_dir.path()
     )
-    .check_output(b"", b"");
+    .check_output(Some(b""), Some(b""));
 
     let source_sub_dir = join!(tmp_source_dir / "sub_dir_a");
 
-    assert!(tree_equal(source_sub_dir, extract_dir)?);
+    assert!(tree_diff(extract_dir, source_sub_dir, Default::default())?);
     Ok(())
 }
 
@@ -135,9 +158,233 @@ fn test_extract_subfile() -> Result {
         "-C",
         extract_dir.path()
     )
+    .check_fail(b"", b"Error : sub_dir_a/file1.txt must be a directory\n");
+    Ok(())
+}
+
+#[test]
+fn test_extract_existing_content_skip() -> Result {
+    let tmp_source_dir = SHARED_TEST_DIR.path();
+    let arx_file = BASE_ARX_FILE.path();
+
+    let extract_dir = temp_tree!(0, {
+        dir "sub_dir_a" {
+            text "existing_file" 100,
+            link "existing_link" -> "other_file"
+        }
+    });
+    let file_content = std::fs::read(join!(extract_dir / "sub_dir_a" / "existing_file"))?;
+
+    cmd!(
+        "arx",
+        "extract",
+        &arx_file,
+        "-C",
+        extract_dir.path(),
+        "--overwrite=skip"
+    )
+    .check_output(Some(b""), Some(b""));
+    assert!(tree_diff(
+        extract_dir,
+        tmp_source_dir,
+        HashMap::from([
+            (
+                join!("sub_dir_a" / "existing_file"),
+                ExistingExpected::Content(file_content)
+            ),
+            (
+                join!("sub_dir_a" / "existing_link"),
+                ExistingExpected::Link(OsStr::new("other_file").to_os_string())
+            )
+        ])
+    )?);
+    Ok(())
+}
+
+#[test]
+fn test_extract_existing_content_warn() -> Result {
+    let tmp_source_dir = SHARED_TEST_DIR.path();
+    let arx_file = BASE_ARX_FILE.path();
+
+    let extract_dir = temp_tree!(0, {
+        dir "sub_dir_a" {
+            text "existing_file" 100,
+            link "existing_link" -> "other_file"
+        }
+    });
+    let file_content = std::fs::read(join!(extract_dir / "sub_dir_a" / "existing_file"))?;
+
+    cmd!(
+        "arx",
+        "extract",
+        &arx_file,
+        "-C",
+        extract_dir.path(),
+        "--overwrite=warn"
+    )
+    .check_output(
+        Some(b""),
+        Some(&format_bytes!(
+            b"File {} already exists.\nLink {} already exists.\n",
+            join!(extract_dir / "sub_dir_a" / "existing_file")
+                .to_str()
+                .unwrap()
+                .as_bytes(),
+            join!(extract_dir / "sub_dir_a" / "existing_link")
+                .to_str()
+                .unwrap()
+                .as_bytes()
+        )),
+    );
+    assert!(tree_diff(
+        extract_dir,
+        tmp_source_dir,
+        HashMap::from([
+            (
+                join!("sub_dir_a" / "existing_file"),
+                ExistingExpected::Content(file_content)
+            ),
+            (
+                join!("sub_dir_a" / "existing_link"),
+                ExistingExpected::Link(OsStr::new("other_file").to_os_string())
+            )
+        ])
+    )?);
+    Ok(())
+}
+
+#[test]
+fn test_extract_existing_content_newer_true() -> Result {
+    let tmp_source_dir = SHARED_TEST_DIR.path();
+    let arx_file = BASE_ARX_FILE.path();
+
+    let extract_dir = temp_tree!(0, {
+        dir "sub_dir_a" {
+            text "existing_file" 100,
+            link "existing_link" -> "other_file"
+        }
+    });
+
+    // File is modified far before arx created, so we should overwrite
+    filetime::set_file_mtime(
+        join!(extract_dir / "sub_dir_a" / "existing_file"),
+        filetime::FileTime::from_unix_time(0, 0),
+    )?;
+    filetime::set_symlink_file_times(
+        join!(extract_dir / "sub_dir_a" / "existing_link"),
+        filetime::FileTime::from_unix_time(0, 0),
+        filetime::FileTime::from_unix_time(0, 0),
+    )?;
+
+    cmd!(
+        "arx",
+        "extract",
+        &arx_file,
+        "-C",
+        extract_dir.path(),
+        "--overwrite=newer"
+    )
+    .check_output(Some(b""), Some(b""));
+    assert!(tree_diff(extract_dir, tmp_source_dir, Default::default())?);
+    Ok(())
+}
+
+#[test]
+fn test_extract_existing_content_newer_false() -> Result {
+    let tmp_source_dir = SHARED_TEST_DIR.path();
+    let arx_file = BASE_ARX_FILE.path();
+
+    // File is created after source, so we should not overwrite
+    let extract_dir = temp_tree!(0, {
+        dir "sub_dir_a" {
+            text "existing_file" 100,
+            link "existing_link" -> "other_file"
+        }
+    });
+
+    let file_content = std::fs::read(join!(extract_dir / "sub_dir_a" / "existing_file"))?;
+
+    cmd!(
+        "arx",
+        "extract",
+        &arx_file,
+        "-C",
+        extract_dir.path(),
+        "--overwrite=newer"
+    )
+    .check_output(Some(b""), Some(b""));
+    assert!(tree_diff(
+        extract_dir,
+        tmp_source_dir,
+        HashMap::from([
+            (
+                join!("sub_dir_a" / "existing_file"),
+                ExistingExpected::Content(file_content)
+            ),
+            (
+                join!("sub_dir_a" / "existing_link"),
+                ExistingExpected::Link(OsStr::new("other_file").to_os_string())
+            )
+        ])
+    )?);
+    Ok(())
+}
+
+#[test]
+fn test_extract_existing_content_overwrite() -> Result {
+    let tmp_source_dir = SHARED_TEST_DIR.path();
+    let arx_file = BASE_ARX_FILE.path();
+
+    let extract_dir = temp_tree!(0, {
+        dir "sub_dir_a" {
+            text "existing_file" 100,
+            link "existing_link" -> "other_file"
+        }
+    });
+
+    cmd!(
+        "arx",
+        "extract",
+        &arx_file,
+        "-C",
+        extract_dir.path(),
+        "--overwrite=overwrite"
+    )
+    .check_output(Some(b""), Some(b""));
+    assert!(tree_diff(extract_dir, tmp_source_dir, Default::default())?);
+    Ok(())
+}
+
+#[test]
+fn test_extract_existing_content_error() -> Result {
+    let tmp_source_dir = SHARED_TEST_DIR.path();
+    let arx_file = BASE_ARX_FILE.path();
+
+    let extract_dir = temp_tree!(0, {
+        dir "sub_dir_a" {
+            text "existing_file" 100,
+            link "existing_link" -> "other_file"
+        }
+    });
+
+    cmd!(
+        "arx",
+        "extract",
+        &arx_file,
+        "-C",
+        extract_dir.path(),
+        "--overwrite=error"
+    )
     .check_fail(
         b"",
-        b"[ERROR arx] Error : sub_dir_a/file1.txt must be a directory\n",
+        &format_bytes!(
+            b"Error : Unknown error : File {} already exists.\n",
+            join!(extract_dir / "sub_dir_a" / "existing_file")
+                .to_str()
+                .unwrap()
+                .as_bytes()
+        ),
     );
+    assert!(!tree_diff(extract_dir, tmp_source_dir, Default::default())?);
     Ok(())
 }
diff --git a/arx/tests/utils/mod.rs b/arx/tests/utils/mod.rs
index 2fdd0ab..f6e7813 100644
--- a/arx/tests/utils/mod.rs
+++ b/arx/tests/utils/mod.rs
@@ -1,29 +1,27 @@
-use std::{
-    fs::{create_dir, write},
-    io::Read,
-    path::Path,
-    process::Command,
-    sync::LazyLock,
-};
+mod tree_diff;
+use std::{io::Read, path::Path, process::Command, sync::LazyLock};
 
 use rand::prelude::*;
 pub type Result = anyhow::Result<()>;
 
+#[allow(unused_imports)]
+pub use tree_diff::{tree_diff, ExistingExpected};
+
 #[cfg(unix)]
-fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(path: P, target: Q) -> std::io::Result<()> {
+pub fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(path: P, target: Q) -> std::io::Result<()> {
     std::os::unix::fs::symlink(target, path)
 }
 #[cfg(unix)]
-fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(path: P, target: Q) -> std::io::Result<()> {
+pub fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(path: P, target: Q) -> std::io::Result<()> {
     std::os::unix::fs::symlink(target, path)
 }
 
 #[cfg(windows)]
-fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(path: P, target: Q) -> std::io::Result<()> {
+pub fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(path: P, target: Q) -> std::io::Result<()> {
     std::os::windows::fs::symlink_file(target, path)
 }
 #[cfg(windows)]
-fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(path: P, target: Q) -> std::io::Result<()> {
+pub fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(path: P, target: Q) -> std::io::Result<()> {
     std::os::windows::fs::symlink_dir(target, path)
 }
 
@@ -77,9 +75,9 @@ macro_rules! temp_tree {
         {
             let temp_path =
             tempfile::TempDir::with_prefix_in("source_", env!("CARGO_TARGET_TMPDIR"))?;
-            let mut rng = SmallRng::seed_from_u64($seed);
+            let mut rng = <rand::rngs::SmallRng as rand::SeedableRng>::seed_from_u64($seed);
             temp_tree!(@instr, temp_path.path(), rng, [], $($what)*);
-            Ok(temp_path)
+            temp_path
         }
     };
 
@@ -160,13 +158,13 @@ macro_rules! temp_tree {
 
     // Empty dir
     (@dir, $path:expr, $rng:ident, $context:tt, $sub_path:tt, { }) => {
-        create_dir($path.join(&temp_tree!(@ctx, $sub_path, $context)))?;
+        std::fs::create_dir($path.join(&temp_tree!(@ctx, $sub_path, $context)))?;
     };
     // Dir with content
     (@dir, $path:expr, $rng:ident, $context:tt, $sub_path:tt, { $($what:tt)+ }) => {
         {
             let new_path = $path.join(&temp_tree!(@ctx, $sub_path, $context));
-            create_dir(&new_path)?;
+            std::fs::create_dir(&new_path)?;
             temp_tree!(@instr, new_path, $rng, $context, $($what)+) ;
         }
     };
@@ -175,7 +173,7 @@ macro_rules! temp_tree {
     (@text, $path:expr, $rng:ident, $context:tt, $sub_path:tt, $len:tt) => {
         let len = temp_tree!(@num, $rng, $len);
         let data = lipsum::lipsum_words_with_rng(&mut $rng, len);
-        write($path.join(&temp_tree!(@ctx, $sub_path, $context)), data.as_bytes())?;
+        std::fs::write($path.join(&temp_tree!(@ctx, $sub_path, $context)), data.as_bytes())?;
     };
 
 
@@ -192,7 +190,7 @@ macro_rules! temp_tree {
         {
             let sub_path = $path.join(temp_tree!(@ctx, $sub_path, $context));
             let target = temp_tree!(@ctx, $target, $context);
-            symlink(sub_path, target)?;
+            $crate::utils::symlink(sub_path, target)?;
         }
     };
 
@@ -201,7 +199,7 @@ macro_rules! temp_tree {
         {
             let sub_path = $path.join(temp_tree!(@ctx, $sub_path, $context));
             let target = temp_tree!(@ctx, $target, $context);
-            symlink_dir(sub_path, target)?;
+            $crate::utils::symlink_dir(sub_path, target)?;
         }
     };
 
@@ -239,8 +237,10 @@ macro_rules! temp_tree {
 
 pub static SHARED_TEST_DIR: LazyLock<tempfile::TempDir> = LazyLock::new(|| {
     (|| -> std::io::Result<tempfile::TempDir> {
-        temp_tree!(1, {
+        Ok(temp_tree!(1, {
             dir "sub_dir_a" {
+                text "existing_file" 50,
+                link "existing_link" -> "existing_file",
                 text "file_2.txt" (500..1000),
                 loop  (10..50) { text "file{ctx}.txt" (500..1000) }
             },
@@ -256,9 +256,9 @@ pub static SHARED_TEST_DIR: LazyLock<tempfile::TempDir> = LazyLock::new(|| {
                     loop (1..2) { dir "gen_sub_empty_dir_{ctx}{dir_ctx:0}" {} }
                 }
             }
-        })
+        }))
     })()
-    .unwrap()
+    .expect("Error creating the directory tree")
 });
 
 #[macro_export]
@@ -317,48 +317,44 @@ macro_rules! temp_arx {
     };
 }
 
-#[allow(dead_code)]
-pub fn tree_equal<P: AsRef<Path>, Q: AsRef<Path>>(a: P, b: Q) -> anyhow::Result<bool> {
-    let a = a.as_ref();
-    let b = b.as_ref();
-    let output = run!(output, "diff", "-r", a, b);
-    if output.status.success() {
-        Ok(true)
-    } else {
-        println!(
-            "Diff failed between {a} and {b}:\n{err}",
-            a = a.display(),
-            b = b.display(),
-            err = String::from_utf8_lossy(&output.stderr)
-        );
-        Ok(false)
-    }
-}
-
 #[allow(dead_code)]
 pub trait CheckCommand {
     fn check_fail(&mut self, stdout: &[u8], stderr: &[u8]);
-    fn check_output(&mut self, stdout: &[u8], stderr: &[u8]);
+    fn check_output(&mut self, stdout: Option<&[u8]>, stderr: Option<&[u8]>);
     fn check(&mut self);
 }
 
 impl CheckCommand for Command {
-    fn check_output(&mut self, stdout: &[u8], stderr: &[u8]) {
+    fn check_output(&mut self, stdout: Option<&[u8]>, stderr: Option<&[u8]>) {
         println!("Running command {self:?}");
         let output = self.output().expect("Running command should work.");
-        assert_eq!(
-            output.stdout,
-            stdout,
-            "Output is {}",
-            String::from_utf8_lossy(&output.stdout)
-        );
-        assert_eq!(
-            output.stderr,
-            stderr,
-            "Err is {}",
-            String::from_utf8_lossy(&output.stderr)
-        );
-        assert!(output.status.success());
+        let mut success = output.status.success();
+        if let Some(stdout) = stdout {
+            success &= output.stdout == stdout
+        }
+        if let Some(stderr) = stderr {
+            success &= output.stderr == stderr
+        }
+        if !success {
+            println!("Command failed. Status is {}", output.status);
+            if let Some(stdout) = stdout {
+                println!(
+                    "Output is {}\nExpected is {}",
+                    String::from_utf8_lossy(&output.stdout),
+                    String::from_utf8_lossy(stdout)
+                );
+            }
+            if let Some(stderr) = stderr {
+                println!(
+                    "Err is {}\nExpected is {}",
+                    String::from_utf8_lossy(&output.stderr),
+                    String::from_utf8_lossy(stderr)
+                );
+            }
+            panic!("Running command {self:?} fails.")
+        } else {
+            println!("Command run succeed.");
+        }
     }
     fn check_fail(&mut self, stdout: &[u8], stderr: &[u8]) {
         println!("Running command {self:?}");
@@ -366,32 +362,36 @@ impl CheckCommand for Command {
         assert_eq!(
             output.stdout,
             stdout,
-            "Output is {}",
-            String::from_utf8_lossy(&output.stdout)
+            "Output is {}\nExpected is {}",
+            String::from_utf8_lossy(&output.stdout),
+            String::from_utf8_lossy(stdout),
         );
         assert_eq!(
             output.stderr,
             stderr,
-            "Err is {}",
-            String::from_utf8_lossy(&output.stderr)
+            "Err is {}\nExpected is {}",
+            String::from_utf8_lossy(&output.stderr),
+            String::from_utf8_lossy(stderr)
         );
         assert!(!output.status.success());
     }
     fn check(&mut self) {
-        println!("Running command {self:?}");
-        let status = self.status().expect("Running command should work.");
-        assert!(status.success());
+        self.check_output(None, None)
     }
 }
 
 #[macro_export]
 macro_rules! join {
     ($first:tt / $($args:tt)/+) => {
+        join!(@init, $first, $($args),+)
+    };
+    (@init, $first:expr, $($args:tt),+) => {
         {
-            let mut path = $first.to_path_buf();
+            let mut path:PathBuf = AsRef::<Path>::as_ref(&$first).to_path_buf();
             join!(@append, path, $($args),+);
             path
         }
+
     };
     (@append, $path:ident, $args:expr) => {
         $path.push($args);
diff --git a/arx/tests/utils/tree_diff.rs b/arx/tests/utils/tree_diff.rs
new file mode 100644
index 0000000..c2259cf
--- /dev/null
+++ b/arx/tests/utils/tree_diff.rs
@@ -0,0 +1,289 @@
+#![allow(dead_code)]
+
+use std::{
+    cmp::Ordering,
+    collections::{BTreeSet, HashMap},
+    ffi::{OsStr, OsString},
+    fs::{read_dir, read_link, symlink_metadata, File, ReadDir},
+    io::{self, BufReader, Read},
+    ops::Deref,
+    path::{Path, PathBuf},
+};
+
+struct ReadAsIter<R: Read>(BufReader<R>);
+
+impl<R: Read> Iterator for ReadAsIter<R> {
+    type Item = u8;
+    fn next(&mut self) -> Option<Self::Item> {
+        let mut result: [u8; 1] = [0; 1];
+        match self.0.read(&mut result).expect("read should succeed") {
+            0 => None,
+            1 => Some(result[0]),
+            _ => unreachable!(),
+        }
+    }
+}
+
+impl<R: Read> From<R> for ReadAsIter<R> {
+    fn from(value: R) -> Self {
+        Self(BufReader::new(value))
+    }
+}
+
+#[derive(Debug)]
+enum Entry {
+    Dir(PathBuf),
+    File(PathBuf),
+    Link(PathBuf),
+}
+
+impl Entry {
+    fn new(p: &Path) -> io::Result<Self> {
+        let metadata = symlink_metadata(p)?;
+        if metadata.is_dir() {
+            Ok(Self::Dir(p.to_path_buf()))
+        } else if metadata.is_file() {
+            Ok(Self::File(p.to_path_buf()))
+        } else if metadata.is_symlink() {
+            Ok(Self::File(p.to_path_buf()))
+        } else {
+            unreachable!()
+        }
+    }
+
+    fn path(&self) -> &Path {
+        match self {
+            Entry::Dir(p) => p,
+            Entry::File(p) => p,
+            Entry::Link(p) => p,
+        }
+    }
+
+    fn file_name(&self) -> &OsStr {
+        self.path().file_name().unwrap()
+    }
+}
+
+impl PartialEq for Entry {
+    fn eq(&self, other: &Self) -> bool {
+        match (self, other) {
+            (Entry::File(a), Entry::File(b)) => {
+                let file_a: ReadAsIter<_> = File::open(a).expect("Open should succeed").into();
+                let file_b: ReadAsIter<_> = File::open(b).expect("Open should succeed").into();
+                file_a.cmp(file_b) == Ordering::Equal
+            }
+            (Entry::Link(a), Entry::Link(b)) => {
+                let target_a = read_link(a).expect("Read_link should succeed");
+                let target_b = read_link(b).expect("Read_link should succeed");
+                target_a.cmp(&target_b) == Ordering::Equal
+            }
+            _ => false,
+        }
+    }
+}
+
+#[derive(Debug)]
+struct OrderByPath(Entry);
+
+impl OrderByPath {
+    fn key(&self) -> &OsStr {
+        self.0.file_name()
+    }
+}
+
+impl Eq for OrderByPath {}
+
+impl Ord for OrderByPath {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.key().cmp(other.key())
+    }
+}
+
+impl PartialOrd for OrderByPath {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        self.key().partial_cmp(other.key())
+    }
+}
+
+impl PartialEq for OrderByPath {
+    fn eq(&self, other: &Self) -> bool {
+        self.key() == other.key()
+    }
+}
+
+impl Deref for OrderByPath {
+    type Target = Entry;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+struct EntryIterator(ReadDir);
+
+impl EntryIterator {
+    fn new(p: &Path) -> Self {
+        Self(read_dir(p).unwrap())
+    }
+}
+
+impl Iterator for EntryIterator {
+    type Item = OrderByPath;
+    fn next(&mut self) -> Option<Self::Item> {
+        let next = self.0.next()?.expect("Iter dir should succeed");
+        let file_type = next.file_type().unwrap();
+        Some(OrderByPath(if file_type.is_dir() {
+            Entry::Dir(next.path())
+        } else if file_type.is_file() {
+            Entry::File(next.path())
+        } else if file_type.is_symlink() {
+            Entry::Link(next.path())
+        } else {
+            unreachable!()
+        }))
+    }
+}
+
+struct ExceptionMatcher {
+    current_path: PathBuf,
+    exceptions: HashMap<PathBuf, ExistingExpected>,
+    equal: bool,
+}
+
+impl ExceptionMatcher {
+    fn new(exceptions: HashMap<PathBuf, ExistingExpected>) -> Self {
+        Self {
+            current_path: PathBuf::new(),
+            exceptions,
+            equal: true,
+        }
+    }
+
+    fn result(self) -> bool {
+        self.equal
+    }
+
+    fn push(&mut self, entry_name: &OsStr) {
+        self.current_path.push(entry_name);
+    }
+
+    fn pop(&mut self) {
+        self.current_path.pop();
+    }
+
+    fn added(&mut self, entry: &Entry) {
+        println!(
+            "Added {} in {}",
+            entry.path().display(),
+            self.current_path.display()
+        );
+        self.equal = false;
+    }
+
+    fn removed(&mut self, entry: &Entry) {
+        println!(
+            "Removed {} in {}",
+            entry.path().display(),
+            self.current_path.display()
+        );
+        self.equal = false;
+    }
+
+    fn diff(&mut self, entry_tested: &Entry, entry_ref: &Entry) {
+        match self.exceptions.get(&self.current_path) {
+            None => {
+                println!(
+                    "Entry {} is different than {}",
+                    entry_tested.path().display(),
+                    entry_ref.path().display()
+                );
+                self.equal = false;
+            }
+            Some(ExistingExpected::Existing) => {
+                //Nothing to do
+            }
+            Some(ExistingExpected::Content(expected)) => {
+                let found_content = std::fs::read(entry_tested.path()).unwrap();
+                if found_content != *expected {
+                    println!(
+                        "Entry {} is different than expected content",
+                        entry_tested.path().display()
+                    );
+                    self.equal = false;
+                }
+            }
+            Some(ExistingExpected::Link(target)) => {
+                let found_target = std::fs::read_link(entry_tested.path()).unwrap();
+                if found_target != *target {
+                    println!(
+                        "Entry {} is different than expected content",
+                        entry_tested.path().display()
+                    );
+                    self.equal = false;
+                }
+            }
+        }
+    }
+}
+
+fn diff(tested: &Entry, reference: &Entry, matcher: &mut ExceptionMatcher) {
+    match (tested, reference) {
+        // two files or links, equal
+        (Entry::File(_), Entry::File(_)) if tested == reference => {}
+        (Entry::Link(_), Entry::Link(_)) if tested == reference => {}
+
+        // Directories, we must compare
+        (Entry::Dir(path_tested), Entry::Dir(path_ref)) => {
+            let children_tested = EntryIterator::new(&path_tested).collect::<BTreeSet<_>>();
+            let children_ref = EntryIterator::new(&path_ref).collect::<BTreeSet<_>>();
+            for v_tested in children_tested.intersection(&children_ref) {
+                let v_ref = children_ref.get(v_tested).expect("intersection to work");
+                matcher.push(v_tested.file_name());
+                diff(&v_tested, &v_ref, matcher);
+                matcher.pop();
+            }
+            for k in children_tested.difference(&children_ref) {
+                matcher.added(k);
+            }
+            for k in children_ref.difference(&children_tested) {
+                matcher.removed(k);
+            }
+        }
+
+        // Different
+        _ => matcher.diff(tested, reference),
+    }
+}
+
+pub enum ExistingExpected {
+    Existing,
+    Content(Vec<u8>),
+    Link(OsString),
+}
+
+/// Compare two paths and return true if they are identical.
+/// Identical means:
+/// - Same kind
+/// - Same content for files
+/// - Same target for symlink
+/// - Same entries for directory
+///
+/// `exceptions` is a HashMap of entry than can differ from source.
+/// If value in the hashmap is `None`, we accept different content all the time.
+/// If value is `Some(content)`, the content of the file must match `content`.
+///
+/// Please note that tested (generated by thing being tested) content comes first and
+/// reference second. This is important when `exceptions` has values as we will load
+/// file content in first (tested) tree.
+pub fn tree_diff(
+    tested: impl AsRef<Path>,
+    reference: impl AsRef<Path>,
+    exceptions: HashMap<PathBuf, ExistingExpected>,
+) -> std::io::Result<bool> {
+    let mut matcher = ExceptionMatcher::new(exceptions);
+    diff(
+        &Entry::new(tested.as_ref())?,
+        &Entry::new(reference.as_ref())?,
+        &mut matcher,
+    );
+    Ok(matcher.result())
+}