Skip to content

extract: add -k/--keep-old-files - #404

Closed
ozenyunus35 wants to merge 3 commits into
uutils:mainfrom
ozenyunus35:keep-old-files
Closed

ozenyunus35 wants to merge 3 commits into
uutils:mainfrom
ozenyunus35:keep-old-files

Conversation

@ozenyunus35

Copy link
Copy Markdown
Contributor

Closes #191

With -k/--keep-old-files, extract leaves an existing file as it is and reports it the way GNU tar does (tar: a.txt: Cannot open: File exists); an existing directory is not an error. The other members are still extracted, and the run ends with "Exiting with failure status due to previous errors" and status 2, using the same recoverable-error path as the .. member check.

extract_archive gets a keep_old flag; the unit test and bench callers pass false. Added test_extract_keep_old_files, which checks the message, the exit status, that the existing file keeps its content and that the other member is extracted. It fails without the change.

This touches the same extract_archive signature as #403 (-O), so whichever lands second needs a small rebase; I'll take care of it.

I used an AI assistant to help draft parts of the code and tests; I reviewed the changes and ran the tests myself.

Like GNU tar, -k leaves an existing file as it is and reports it as an
error ("Cannot open: File exists"); an existing directory is fine. The
other members are still extracted and the run exits with status 2.

Closes uutils#191
@ozenyunus35
ozenyunus35 requested a review from kaladron as a code owner October 14, 2026 13:44
@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.07%. Comparing base (791ae26) to head (7a44a42).
⚠️ Report is 90 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #404      +/-   ##
==========================================
+ Coverage   96.84%   98.07%   +1.22%     
==========================================
  Files          11       15       +4     
  Lines        1492     1925     +433     
  Branches       29       41      +12     
==========================================
+ Hits         1445     1888     +443     
+ Misses         46       36      -10     
  Partials        1        1              
Flag Coverage Δ
macos_latest 98.07% <100.00%> (+1.22%) ⬆️
ubuntu_latest 98.07% <100.00%> (+1.22%) ⬆️
windows_latest 0.00% <0.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed

codspeed Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

⚠️ 4 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 7 untouched benchmarks


Comparing ozenyunus35:keep-old-files (7a44a42) with main (fb55fee)

Open in CodSpeed

Comment thread src/uu/tar/src/operations/extract.rs Outdated

// -k: like GNU tar, an existing file is an error and is left as it is;
// an existing directory is fine.
if keep_old && !entry.header().entry_type().is_dir() && fs::symlink_metadata(&path).is_ok()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this checks the raw member path, so /etc/foo or ../foo is looked up outside the target dir, no?
it also runs before the .. check, so an existing ../foo says "File exists" instead of the .. message.
could you please check the path unpack_in will actually write to, and add a test for it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right on both counts. It now checks the path unpack_in writes to (leading / and . dropped), and a member with a .. component is left to the .. check. Added tests for /kept.txt landing on an existing kept.txt and for an existing ../item.

Comment thread tests/by-util/test_tar.rs
.succeeds();
at.write("a.txt", "already here");
at.remove("b.txt");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the asserts already say this, the comment can go

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment thread tests/by-util/test_tar.rs
.stderr_contains("Exiting with failure status due to previous errors");
assert_eq!(at.read("a.txt"), "already here");
assert_eq!(at.read("b.txt"), "also from archive");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please also add a test where the member is a directory that already exists (it should not be an error)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test_extract_keep_old_files_existing_directory.

Look up the path unpack_in writes (leading / and . dropped) instead of the
raw member name, and leave a member with a .. component to the .. check.
Test a leading /, a .. member and an existing directory.
Comment thread tests/by-util/test_tar.rs
assert_eq!(at.read("d/f.txt"), "from archive");
}

fn keep_old_members_tar(names: &[&str]) -> Vec<u8> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

almost the same code as in test_extract_reports_member_with_parent_dir_and_continues, could you please use this helper there too?
and maybe a less -k specific name, like tar_with_members

# Conflicts:
#	src/uu/tar/src/tar.rs
#	tests/by-util/test_tar.rs
@ozenyunus35

Copy link
Copy Markdown
Contributor Author

Closing this, as I can't follow up on it further. Thanks for the reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GNU tar compatibility: Add -k / --keep-old-files

2 participants