Repository: ouch-org/ouch
Pull request #789: Store symlinks by default and add `--follow-symlinks` to toggle it
Author: login: tommady
Created: 2025-04-09T01:49:28Z | Merged: 2025-04-16T20:43:46Z

Closes #773
Closes #775
Closes #293
Closes #232 


Linked issue #773: Add option to disable symlink dereference
When using `ouch` to compress a directory into a `.tar.gz` file, symbolic links are dereferenced, storing the target files instead of the links. In some cases, symlinks are used to reduce redundancy within a directory structure, and this behavior can lead to an unnecessarily large `.tar.gz` file.  

It would be useful to have an option in `ouch` to preserve symbolic links instead of following them.
Linked issue comments: user: login: marcospb19
created_at: 2025-02-15T15:40:48Z
body: Would solve https://github.com/ouch-org/ouch/issues/293.
author_association: MEMBER

Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27643f51c..ba482f4c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@ Categories Used:
 - Add `--remove` flag for decompression subcommand to remove files after successful decompression [\#757](https://github.com/ouch-org/ouch/pull/757) ([ttys3](https://github.com/ttys3))
 - Add `br` (Brotli) support [\#765](https://github.com/ouch-org/ouch/pull/765) ([killercup](https://github.com/killercup))
 - Add rename option in overwrite menu [\#779](https://github.com/ouch-org/ouch/pull/779) ([talis-fb](https://github.com/talis-fb))
+- Store symlinks by default and add `--follow-symlinks` to store the target files [\#789](https://github.com/ouch-org/ouch/pull/789) ([tommady](https://github.com/tommady))
 
 ### Bug Fixes
 
diff --git a/src/archive/tar.rs b/src/archive/tar.rs
index 341607884..c0d962db0 100644
--- a/src/archive/tar.rs
+++ b/src/archive/tar.rs
@@ -31,7 +31,24 @@ pub fn unpack_archive(reader: Box<dyn Read>, output_folder: &Path, quiet: bool)
     for file in archive.entries()? {
         let mut file = file?;
 
-        file.unpack_in(output_folder)?;
+        match file.header().entry_type() {
+            tar::EntryType::Symlink => {
+                let relative_path = file.path()?.to_path_buf();
+                let full_path = output_folder.join(&relative_path);
+                let target = file
+                    .link_name()?
+                    .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Missing symlink target"))?;
+
+                #[cfg(unix)]
+                std::os::unix::fs::symlink(&target, &full_path)?;
+                #[cfg(windows)]
+                std::os::windows::fs::symlink_file(&target, &full_path)?;
+            }
+            tar::EntryType::Regular | tar::EntryType::Directory => {
+                file.unpack_in(output_folder)?;
+            }
+            _ => continue,
+        }
 
         // This is printed for every file in the archive and has little
         // importance for most users, but would generate lots of
@@ -87,6 +104,7 @@ pub fn build_archive_from_paths<W>(
     writer: W,
     file_visibility_policy: FileVisibilityPolicy,
     quiet: bool,
+    follow_symlinks: bool,
 ) -> crate::Result<W>
 where
     W: Write,
@@ -127,6 +145,18 @@ where
 
             if path.is_dir() {
                 builder.append_dir(path, path)?;
+            } else if path.is_symlink() && !follow_symlinks {
+                let target_path = path.read_link()?;
+
+                let mut header = tar::Header::new_gnu();
+                header.set_entry_type(tar::EntryType::Symlink);
+                header.set_size(0);
+
+                builder.append_link(&mut header, path, &target_path).map_err(|err| {
+                    FinalError::with_title("Could not create archive")
+                        .detail("Unexpected error while trying to read link")
+                        .detail(format!("Error: {err}."))
+                })?;
             } else {
                 let mut file = match fs::File::open(path) {
                     Ok(f) => f,
diff --git a/src/archive/zip.rs b/src/archive/zip.rs
index f525d3bd4..d593477ca 100644
--- a/src/archive/zip.rs
+++ b/src/archive/zip.rs
@@ -85,8 +85,23 @@ where
                     ));
                 }
 
-                let mut output_file = fs::File::create(file_path)?;
-                io::copy(&mut file, &mut output_file)?;
+                let mode = file.unix_mode().ok_or_else(|| {
+                    std::io::Error::new(std::io::ErrorKind::InvalidData, "Cannot extract file's mode")
+                })?;
+                let is_symlink = (mode & 0o170000) == 0o120000;
+
+                if is_symlink {
+                    let mut target = String::new();
+                    file.read_to_string(&mut target)?;
+
+                    #[cfg(unix)]
+                    std::os::unix::fs::symlink(&target, file_path)?;
+                    #[cfg(windows)]
+                    std::os::windows::fs::symlink_file(&target, file_path)?;
+                } else {
+                    let mut output_file = fs::File::create(file_path)?;
+                    io::copy(&mut file, &mut output_file)?;
+                }
 
                 set_last_modified_time(&file, file_path)?;
             }
@@ -155,6 +170,7 @@ pub fn build_archive_from_paths<W>(
     writer: W,
     file_visibility_policy: FileVisibilityPolicy,
     quiet: bool,
+    follow_symlinks: bool,
 ) -> crate::Result<W>
 where
     W: Write + Seek,
@@ -223,7 +239,7 @@ where
             };
 
             #[cfg(unix)]
-            let options = options.unix_permissions(metadata.permissions().mode());
+            let mode = metadata.permissions().mode();
 
             let entry_name = path.to_str().ok_or_else(|| {
                 FinalError::with_title("Zip requires that all directories names are valid UTF-8")
@@ -232,6 +248,21 @@ where
 
             if metadata.is_dir() {
                 writer.add_directory(entry_name, options)?;
+            } else if path.is_symlink() && !follow_symlinks {
+                let target_path = path.read_link()?;
+                let target_name = target_path.to_str().ok_or_else(|| {
+                    FinalError::with_title("Zip requires that all directories names are valid UTF-8")
+                        .detail(format!("File at '{target_path:?}' has a non-UTF-8 name"))
+                })?;
+
+                // This approach writes the symlink target path as the content of the symlink entry.
+                // We detect symlinks during extraction by checking for the Unix symlink mode (0o120000) in the entry's permissions.
+                #[cfg(unix)]
+                let symlink_options = options.unix_permissions(0o120000 | (mode & 0o777));
+                #[cfg(windows)]
+                let symlink_options = options.unix_permissions(0o120777);
+
+                writer.add_symlink(entry_name, target_name, symlink_options)?;
             } else {
                 #[cfg(not(unix))]
                 let options = if is_executable::is_executable(path) {
@@ -242,6 +273,8 @@ where
 
                 let mut file = fs::File::open(path)?;
 
+                #[cfg(unix)]
+                let options = options.unix_permissions(mode);
                 // Updated last modified time
                 let last_modified_time = options.last_modified_time(get_last_modified_time(&file));
 
diff --git a/src/cli/args.rs b/src/cli/args.rs
index 5701f4e4c..c72d5d3ae 100644
--- a/src/cli/args.rs
+++ b/src/cli/args.rs
@@ -81,6 +81,10 @@ pub enum Subcommand {
         /// conflicts with --level and --fast
         #[arg(long, group = "compression-level")]
         slow: bool,
+
+        /// Archive target files instead of storing symlinks (supported by `tar` and `zip`)
+        #[arg(long, short = 'S')]
+        follow_symlinks: bool,
     },
     /// Decompresses one or more files, optionally into another folder
     #[command(visible_alias = "d")]
@@ -201,6 +205,7 @@ mod tests {
                     level: None,
                     fast: false,
                     slow: false,
+                    follow_symlinks: false,
                 },
                 ..mock_cli_args()
             }
@@ -214,6 +219,7 @@ mod tests {
                     level: None,
                     fast: false,
                     slow: false,
+                    follow_symlinks: false,
                 },
                 ..mock_cli_args()
             }
@@ -227,6 +233,7 @@ mod tests {
                     level: None,
                     fast: false,
                     slow: false,
+                    follow_symlinks: false,
                 },
                 ..mock_cli_args()
             }
@@ -251,6 +258,7 @@ mod tests {
                         level: None,
                         fast: false,
                         slow: false,
+                        follow_symlinks: false,
                     },
                     format: Some("tar.gz".into()),
                     ..mock_cli_args()
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index ce6b5d591..185fae6cd 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -54,7 +54,7 @@ fn canonicalize_files(files: &[impl AsRef<Path>]) -> io::Result<Vec<PathBuf>> {
     files
         .iter()
         .map(|f| {
-            if is_path_stdin(f.as_ref()) {
+            if is_path_stdin(f.as_ref()) || f.as_ref().is_symlink() {
                 Ok(f.as_ref().to_path_buf())
             } else {
                 fs::canonicalize(f)
diff --git a/src/commands/compress.rs b/src/commands/compress.rs
index 59987b855..b6f70420f 100644
--- a/src/commands/compress.rs
+++ b/src/commands/compress.rs
@@ -31,6 +31,7 @@ pub fn compress_files(
     output_file: fs::File,
     output_path: &Path,
     quiet: bool,
+    follow_symlinks: bool,
     question_policy: QuestionPolicy,
     file_visibility_policy: FileVisibilityPolicy,
     level: Option<i16>,
@@ -108,7 +109,14 @@ pub fn compress_files(
             io::copy(&mut reader, &mut writer)?;
         }
         Tar => {
-            archive::tar::build_archive_from_paths(&files, output_path, &mut writer, file_visibility_policy, quiet)?;
+            archive::tar::build_archive_from_paths(
+                &files,
+                output_path,
+                &mut writer,
+                file_visibility_policy,
+                quiet,
+                follow_symlinks,
+            )?;
             writer.flush()?;
         }
         Zip => {
@@ -131,6 +139,7 @@ pub fn compress_files(
                 &mut vec_buffer,
                 file_visibility_policy,
                 quiet,
+                follow_symlinks,
             )?;
             vec_buffer.rewind()?;
             io::copy(&mut vec_buffer, &mut writer)?;
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index 60a286621..3e8718b30 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -67,6 +67,7 @@ pub fn run(
             level,
             fast,
             slow,
+            follow_symlinks,
         } => {
             // After cleaning, if there are no input files left, exit
             if files.is_empty() {
@@ -109,6 +110,7 @@ pub fn run(
                 output_file,
                 &output_path,
                 args.quiet,
+                follow_symlinks,
                 question_policy,
                 file_visibility_policy,
                 level,
diff --git a/tests/integration.rs b/tests/integration.rs
index 7a5e5494b..1a984a56b 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -1,7 +1,11 @@
 #[macro_use]
 mod utils;
 
-use std::{io::Write, iter::once, path::PathBuf};
+use std::{
+    io::Write,
+    iter::once,
+    path::{Path, PathBuf},
+};
 
 use fs_err as fs;
 use parse_display::Display;
@@ -467,3 +471,92 @@ fn unpack_rar_stdin() -> Result<(), Box<dyn std::error::Error>> {
 
     Ok(())
 }
+
+#[proptest(cases = 25)]
+fn symlink_pack_and_unpack(
+    ext: DirectoryExtension,
+    #[any(size_range(0..1).lift())] extra_extensions: Vec<FileExtension>,
+) {
+    if matches!(ext, DirectoryExtension::SevenZ) {
+        // Skip 7z because the 7z format does not support symlinks
+        return Ok(());
+    }
+
+    let temp_dir = tempdir()?;
+    let root_path = temp_dir.path();
+
+    let src_files_path = root_path.join("src_files");
+    fs::create_dir_all(&src_files_path)?;
+
+    let mut files_path = ["file1.txt", "file2.txt", "file3.txt", "file4.txt", "file5.txt"]
+        .into_iter()
+        .map(|f| src_files_path.join(f))
+        .map(|path| {
+            let mut file = fs::File::create(&path).unwrap();
+            file.write_all("Some content".as_bytes()).unwrap();
+            path
+        })
+        .collect::<Vec<_>>();
+
+    let dest_files_path = root_path.join("dest_files");
+    fs::create_dir_all(&dest_files_path)?;
+
+    let symlink_path = src_files_path.join(Path::new("symlink"));
+    #[cfg(unix)]
+    std::os::unix::fs::symlink(&files_path[0], &symlink_path)?;
+    #[cfg(windows)]
+    std::os::windows::fs::symlink_file(&files_path[0], &symlink_path)?;
+
+    files_path.push(symlink_path);
+
+    let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions)));
+
+    crate::utils::cargo_bin()
+        .arg("compress")
+        .args(files_path.clone())
+        .arg(archive)
+        .assert()
+        .success();
+
+    crate::utils::cargo_bin()
+        .arg("decompress")
+        .arg(archive)
+        .arg("-d")
+        .arg(&dest_files_path)
+        .assert()
+        .success();
+
+    assert_same_directory(&src_files_path, &dest_files_path, false);
+    // check the symlink stand still
+    for f in dest_files_path.as_path().read_dir()? {
+        let f = f?;
+        if f.file_name() == "symlink" {
+            assert!(f.file_type()?.is_symlink())
+        }
+    }
+
+    fs::remove_file(archive)?;
+    fs::remove_dir_all(&dest_files_path)?;
+
+    crate::utils::cargo_bin()
+        .arg("compress")
+        .arg("--follow-symlinks")
+        .args(files_path)
+        .arg(archive)
+        .assert()
+        .success();
+
+    crate::utils::cargo_bin()
+        .arg("decompress")
+        .arg(archive)
+        .arg("-d")
+        .arg(&dest_files_path)
+        .assert()
+        .success();
+
+    // check there is no symlinks
+    for f in dest_files_path.as_path().read_dir()? {
+        let f = f?;
+        assert!(!f.file_type().unwrap().is_symlink())
+    }
+}
diff --git a/tests/utils.rs b/tests/utils.rs
index 3648cef7f..4d2a84f0f 100644
--- a/tests/utils.rs
+++ b/tests/utils.rs
@@ -88,7 +88,7 @@ pub fn assert_same_directory(x: impl Into<PathBuf>, y: impl Into<PathBuf>, prese
 
                 if ft_x.is_dir() && ft_y.is_dir() {
                     assert_same_directory(x.path(), y.path(), preserve_permissions);
-                } else if ft_x.is_file() && ft_y.is_file() {
+                } else if (ft_x.is_file() && ft_y.is_file()) || (ft_x.is_symlink() && ft_y.is_symlink()) {
                     assert_eq!(meta_x.len(), meta_y.len());
                     assert_eq!(fs::read(x.path()).unwrap(), fs::read(y.path()).unwrap());
                 } else {


Reviews:
user: login: marcospb19
body: Tests are great!
Added some comments:
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-13T23:07:04Z
user: login: tommady
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T08:01:30Z
user: login: tommady
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T09:10:55Z
user: login: marcospb19
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T17:01:44Z
user: login: marcospb19
body: Looking great! One last thing: please add an entry to `CHANGELOG.md` so it appears in the release notes. :) 
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-15T17:05:59Z

Review comments:
diff_hunk: @@ -8,7 +8,7 @@ use std::{
     thread,
 };
 
-use fs_err as fs;
+use fs_err::{self as fs};
path: src/archive/tar.rs
user: login: marcospb19
body: I think this was done by accident.
created_at: 2025-04-13T22:41:46Z
author_association: MEMBER
diff_hunk: @@ -31,7 +31,24 @@ pub fn unpack_archive(reader: Box<dyn Read>, output_folder: &Path, quiet: bool)
     for file in archive.entries()? {
         let mut file = file?;
 
-        file.unpack_in(output_folder)?;
+        match file.header().entry_type() {
+            tar::EntryType::Symlink => {
+                let relative_path = file.path()?.to_path_buf();
+                let full_path = output_folder.join(&relative_path);
+                let target = file
+                    .link_name()?
+                    .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Missing symlink target"))?;
+
+                #[cfg(unix)]
+                std::os::unix::fs::symlink(&target, &full_path)?;
+                #[cfg(windows)]
+                std::os::windows::fs::symlink_file(&target, &full_path)?;
path: src/archive/tar.rs
user: login: marcospb19
body: I'm not sure `symlink_file` should always be used over `symlink_dir`, but I have no Windows system to check and Zip probably doesn't hold the metadata for us to differentiate, so I'll mark as resolved :) .
created_at: 2025-04-13T22:44:58Z
author_association: MEMBER
diff_hunk: @@ -232,6 +248,17 @@ where
 
             if metadata.is_dir() {
                 writer.add_directory(entry_name, options)?;
+            } else if path.is_symlink() && !follow_symlinks {
+                let target_path = path.read_link()?;
+                let target_name = target_path.to_str().ok_or_else(|| {
+                    FinalError::with_title("Zip requires that all directories names are valid UTF-8")
+                        .detail(format!("File at '{target_path:?}' has a non-UTF-8 name"))
+                })?;
+
+                // This approach writes the symlink target path as the content of the symlink entry.
+                // We detect symlinks during extraction by checking for the Unix symlink mode (0o120000) in the entry's permissions.
+                let symlink_options = options.unix_permissions(0o120777);
path: src/archive/zip.rs
user: login: marcospb19
body: By providing `777` here I fear we are not respecting the mode found when we read the file, we have the statement `let options = options.unix_permissions(...` a couple lines above, maybe we want to mix these two masks?

Something like `0o120000 | (metadata.permissions().mode() & 0o777)`? Does that make sense


created_at: 2025-04-13T22:51:37Z
author_association: MEMBER
diff_hunk: @@ -81,6 +81,10 @@ pub enum Subcommand {
         /// conflicts with --level and --fast
         #[arg(long, group = "compression-level")]
         slow: bool,
+
+        /// Tar and Zip specific: add the symlink target to the archive instead of the symlink itself
path: src/cli/args.rs
user: login: marcospb19
body: nitpick
```suggestion
        /// Archive target files instead of storing symlinks (supported by `tar` and `zip`)
```
created_at: 2025-04-13T22:56:13Z
author_association: MEMBER
diff_hunk: @@ -81,6 +81,10 @@ pub enum Subcommand {
         /// conflicts with --level and --fast
         #[arg(long, group = "compression-level")]
         slow: bool,
+
+        /// Tar and Zip specific: add the symlink target to the archive instead of the symlink itself
+        #[arg(long)]
path: src/cli/args.rs
user: login: marcospb19
body: I'd like to have a short flag but I'd like to keep lowercase `s` reserved.
```suggestion
        #[arg(long, short = 'S')]
```
created_at: 2025-04-13T22:56:48Z
author_association: MEMBER
diff_hunk: @@ -81,6 +81,10 @@ pub enum Subcommand {
         /// conflicts with --level and --fast
         #[arg(long, group = "compression-level")]
         slow: bool,
+
+        /// Tar and Zip specific: add the symlink target to the archive instead of the symlink itself
path: src/cli/args.rs
user: login: marcospb19
body: As an afterthought, maybe '--follow-symlinks' should also follow symlinks for the files you provided via CLI, even if you're not using an "archive" format, for example, if you're using `.zst` or `lz4` and the input file is a symlink.

What do you think? Any strong opinions about this?
created_at: 2025-04-13T23:00:11Z
author_association: MEMBER
diff_hunk: @@ -232,6 +248,17 @@ where
 
             if metadata.is_dir() {
                 writer.add_directory(entry_name, options)?;
+            } else if path.is_symlink() && !follow_symlinks {
+                let target_path = path.read_link()?;
+                let target_name = target_path.to_str().ok_or_else(|| {
+                    FinalError::with_title("Zip requires that all directories names are valid UTF-8")
+                        .detail(format!("File at '{target_path:?}' has a non-UTF-8 name"))
+                })?;
+
+                // This approach writes the symlink target path as the content of the symlink entry.
+                // We detect symlinks during extraction by checking for the Unix symlink mode (0o120000) in the entry's permissions.
+                let symlink_options = options.unix_permissions(0o120777);
path: src/archive/zip.rs
user: login: tommady
body: your consternation is correct I'll modify it.
created_at: 2025-04-14T08:01:29Z
author_association: COLLABORATOR
diff_hunk: @@ -81,6 +81,10 @@ pub enum Subcommand {
         /// conflicts with --level and --fast
         #[arg(long, group = "compression-level")]
         slow: bool,
+
+        /// Tar and Zip specific: add the symlink target to the archive instead of the symlink itself
path: src/cli/args.rs
user: login: tommady
body: if I understand your comment,
the test case will run those archive.{extension}
```bash
"/tmp/.tmpaMbuGC/archive.zip"
"/tmp/.tmpDN5Ha5/archive.tlz4"
"/tmp/.tmpArgT5x/archive.tbz3"
"/tmp/.tmp1BqBba/archive.tar"
"/tmp/.tmpBcPHXu/archive.tar"
"/tmp/.tmpRDEqHN/archive.txz"
"/tmp/.tmpiddmIu/archive.tsz"
"/tmp/.tmp3wgMAy/archive.tgz"
"/tmp/.tmpcX1OwW/archive.tlzma"
"/tmp/.tmpLc4c93/archive.tar"
"/tmp/.tmpzLGGNg/archive.tbz"
"/tmp/.tmptBmYT6/archive.txz"
"/tmp/.tmpWKPk3E/archive.txz"
"/tmp/.tmpPOHQoH/archive.tar"
"/tmp/.tmpQvajS9/archive.tbz"
"/tmp/.tmp6TCkTS/archive.tbz"
"/tmp/.tmpq2JWAD/archive.zip"
"/tmp/.tmp1Bmkgn/archive.tlzma"
"/tmp/.tmpdPt1Rq/archive.tbz3"
"/tmp/.tmpdoikJS/archive.tbz3"
"/tmp/.tmpjKpjqm/archive.tlz4"
"/tmp/.tmp8iGIsG/archive.tar"
"/tmp/.tmpxb4qiu/archive.tbz2"
"/tmp/.tmpkrrKA8/archive.tbz3"
test symlink_pack_and_unpack ... ok
```
are these already running the formats you mentioned?
created_at: 2025-04-14T09:10:55Z
author_association: COLLABORATOR
diff_hunk: @@ -81,6 +81,10 @@ pub enum Subcommand {
         /// conflicts with --level and --fast
         #[arg(long, group = "compression-level")]
         slow: bool,
+
+        /// Tar and Zip specific: add the symlink target to the archive instead of the symlink itself
path: src/cli/args.rs
user: login: marcospb19
body: > are these already running the formats you mentioned?
Yes they are

I was thinking about `file.gz`, without the `.tar` part.

But that would grow the scope of this PR, you know what, nevermind I'll reconsider doing that in another PR. :) 
created_at: 2025-04-15T17:01:44Z
author_association: MEMBER

PR comments:
user: login: marcospb19
created_at: 2025-04-09T17:00:08Z
body: 2 years ago I tried this at https://github.com/ouch-org/ouch/compare/main...symlinks but couldn't make it work, so I gave up, I even started suspecting there is a problem in the `tar` lib itself.
author_association: MEMBER
user: login: tommady
created_at: 2025-04-11T05:19:32Z
body: > 2 years ago I tried this at [main...symlinks](https://github.com/ouch-org/ouch/compare/main...symlinks) but couldn't make it work, so I gave up, I even started suspecting there is a problem in the `tar` lib itself.

ya haha, I saw your PR already,
I wanna try again that's all ahah

but I saw you said you were facing a panic while archiving which I am not,
could you share more details about the problem you faced?

I found out in your PR you did not give the link type which is required by the append_link function.

```
/// You must set the entry type to either [`EntryType::Link`] or [`EntryType::Symlink`].
```
pleases correct me if I am wrong.

right now I am facing the unpack does not unarchive the symlink file out from the test case which I tried to add, but it can work well by the compiled binary... ahah...

thanks

![image](https://github.com/user-attachments/assets/be4ebc10-966f-4d4d-962c-30e88cadfd74)
![image](https://github.com/user-attachments/assets/0b77d02c-c344-4dcd-b37d-111c8f2e66b8)
![image](https://github.com/user-attachments/assets/a3ce20b9-faad-4895-b334-85a846411094)


author_association: COLLABORATOR
user: login: tommady
created_at: 2025-04-11T10:03:17Z
body: right now the tar one should be fixed, I am studying the zip one 🙇🏻 
author_association: COLLABORATOR
user: login: marcospb19
created_at: 2025-04-11T16:54:48Z
body: > ya haha, I saw your PR already,
> I wanna try again that's all ahah

Oh, I'm glad you're taking a look at it! Really do appreciate the help as I couldn't figure out the solution by myself. Sorry if I worded that ambiguously.

> could you share more details about the problem you faced?

I don't quite recall, but you're right, maybe I forgot to pass the correct argument.

> I am studying the zip one 🙇🏻

I initially thought that Zip didn't support symlinks :facepalm: looks like it does, for this PR we also need to see what's done for 7Zip. 
author_association: MEMBER
user: login: tommady
created_at: 2025-04-13T04:53:33Z
body: Hi, the zip one is fixed, and the 7z does not support symlink compression.
Please help review it while you have time.
thanks.
author_association: COLLABORATOR
user: login: tommady
created_at: 2025-04-16T14:08:35Z
body: Hi @marcospb19, I added the changelog entries. You might modify those if they do not meet the format. 
Thanks.
author_association: COLLABORATOR
user: login: marcospb19
created_at: 2025-04-16T20:44:21Z
body: Thanks!!
author_association: MEMBER