Skip to content

Commit 2554a8a

Browse files
authored
fix: tar hardlink is not preserved after decompressing or compressing (#879)
* fix tar hard link compress and decompress Signed-off-by: tommady <[email protected]> * let tar can support windows Signed-off-by: tommady <[email protected]> * let tar can support windows Signed-off-by: tommady <[email protected]> * let tar not supporting windows Signed-off-by: tommady <[email protected]> * fix tar linter Signed-off-by: tommady <[email protected]> * add integration test Signed-off-by: tommady <[email protected]> * update CHANGELOG Signed-off-by: tommady <[email protected]> --------- Signed-off-by: tommady <[email protected]>
1 parent 3578992 commit 2554a8a

File tree

4 files changed

+133
-20
lines changed

4 files changed

+133
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Categories Used:
4646
- Handle broken symlinks in zip archives and normalize path separators [\#841](https://github.com/ouch-org/ouch/pull/841) ([zzzsyyy](https://github.com/zzzsyyy))
4747
- Fix folder softlink is not preserved after packing [\#850](https://github.com/ouch-org/ouch/pull/850) ([tommady](https://github.com/tommady))
4848
- Handle read-only directories in tar extraction [\#873](https://github.com/ouch-org/ouch/pull/873) ([vrmiguel](https://github.com/vrmiguel))
49+
- Fix tar hardlink is not preserved after decompressing or compressing [\#879](https://github.com/ouch-org/ouch/pull/879) ([tommady](https://github.com/tommady))
4950
- Fix enable gitignore flag should work without git [\#881](https://github.com/ouch-org/ouch/pull/881) ([tommady](https://github.com/tommady))
5051

5152
### Tweaks

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/archive/tar.rs

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! Contains Tar-specific building and unpacking functions
22
3+
#[cfg(unix)]
4+
use std::os::unix::fs::MetadataExt;
35
use std::{
6+
collections::HashMap,
47
env,
58
io::prelude::*,
69
ops::Not,
@@ -42,6 +45,17 @@ pub fn unpack_archive(reader: Box<dyn Read>, output_folder: &Path, quiet: bool)
4245

4346
create_symlink(&target, &full_path)?;
4447
}
48+
tar::EntryType::Link => {
49+
let link_path = file.path()?;
50+
let target = file
51+
.link_name()?
52+
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Missing hardlink target"))?;
53+
54+
let full_link_path = output_folder.join(&link_path);
55+
let full_target_path = output_folder.join(&target);
56+
57+
std::fs::hard_link(&full_target_path, &full_link_path)?;
58+
}
4559
tar::EntryType::Regular => {
4660
file.unpack_in(output_folder)?;
4761
}
@@ -135,6 +149,7 @@ where
135149
{
136150
let mut builder = tar::Builder::new(writer);
137151
let output_handle = Handle::from_path(output_path);
152+
let mut seen_inode: HashMap<(u64, u64), PathBuf> = HashMap::new();
138153

139154
for filename in input_filenames {
140155
let previous_location = utils::cd_into_same_dir_as(filename)?;
@@ -164,7 +179,9 @@ where
164179
info!("Compressing '{}'", EscapedPathDisplay::new(path));
165180
}
166181

167-
if !follow_symlinks && path.symlink_metadata()?.is_symlink() {
182+
let link_meta = path.symlink_metadata()?;
183+
184+
if !follow_symlinks && link_meta.is_symlink() {
168185
let target_path = path.read_link()?;
169186

170187
let mut header = tar::Header::new_gnu();
@@ -176,25 +193,59 @@ where
176193
.detail("Unexpected error while trying to read link")
177194
.detail(format!("Error: {err}."))
178195
})?;
179-
} else if path.is_dir() {
180-
builder.append_dir(path, path)?;
181-
} else {
182-
let mut file = match fs::File::open(path) {
183-
Ok(f) => f,
184-
Err(e) => {
185-
if e.kind() == std::io::ErrorKind::NotFound && path.is_symlink() {
186-
// This path is for a broken symlink, ignore it
187-
continue;
188-
}
189-
return Err(e.into());
196+
continue;
197+
}
198+
199+
// TODO: for supporting windows hard link easier
200+
// we should wait for this issue
201+
// https://github.com/rust-lang/rust/issues/63010
202+
#[cfg(unix)]
203+
if link_meta.nlink() > 1 && link_meta.is_file() {
204+
let key = (link_meta.dev(), link_meta.ino());
205+
206+
match seen_inode.get(&key) {
207+
Some(target_path) => {
208+
let mut header = tar::Header::new_gnu();
209+
header.set_entry_type(tar::EntryType::Link);
210+
header.set_size(0);
211+
212+
builder.append_link(&mut header, path, target_path).map_err(|err| {
213+
FinalError::with_title("Could not create archive").detail(format!(
214+
"Error appending hard link '{}': {}",
215+
path.display(),
216+
err
217+
))
218+
})?;
190219
}
191-
};
192-
builder.append_file(path, file.file_mut()).map_err(|err| {
193-
FinalError::with_title("Could not create archive")
194-
.detail("Unexpected error while trying to read file")
195-
.detail(format!("Error: {err}."))
196-
})?;
220+
None => {
221+
seen_inode.insert(key, path.to_path_buf());
222+
let mut file = fs::File::open(path)?;
223+
builder.append_file(path, file.file_mut())?
224+
}
225+
}
226+
continue;
227+
}
228+
229+
if path.is_dir() {
230+
builder.append_dir(path, path)?;
231+
continue;
197232
}
233+
234+
let mut file = match fs::File::open(path) {
235+
Ok(f) => f,
236+
Err(e) => {
237+
if e.kind() == std::io::ErrorKind::NotFound && path.is_symlink() {
238+
// This path is for a broken symlink, ignore it
239+
continue;
240+
}
241+
return Err(e.into());
242+
}
243+
};
244+
builder.append_file(path, file.file_mut()).map_err(|err| {
245+
FinalError::with_title("Could not create archive")
246+
.detail("Unexpected error while trying to read file")
247+
.detail(format!("Error: {err}."))
248+
})?;
198249
}
199250
env::set_current_dir(previous_location)?;
200251
}

tests/integration.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,3 +1148,64 @@ fn sevenz_list_should_not_failed() {
11481148

11491149
assert!(res.get_output().stdout.find(b"README.md").is_some());
11501150
}
1151+
1152+
// TODO: for supporting windows hard link easier
1153+
// we should wait for this issue
1154+
// https://github.com/rust-lang/rust/issues/63010
1155+
#[cfg(unix)]
1156+
#[test]
1157+
fn tar_hardlink_pack_and_unpack() {
1158+
use std::{fs::hard_link, os::unix::fs::MetadataExt};
1159+
1160+
let temp_dir = tempdir().unwrap();
1161+
let root_path = temp_dir.path();
1162+
let source_path = root_path.join("hardlink");
1163+
fs::create_dir_all(&source_path).unwrap();
1164+
let out_path = root_path.join("out");
1165+
fs::create_dir_all(&out_path).unwrap();
1166+
1167+
let source = fs::File::create(source_path.join("source")).unwrap();
1168+
let link1 = source_path.join("link1");
1169+
let link2 = source_path.join("link2");
1170+
hard_link(source.path(), link1.as_path()).unwrap();
1171+
hard_link(source.path(), link2.as_path()).unwrap();
1172+
1173+
let archive = root_path.join("archive.tar.gz");
1174+
crate::utils::cargo_bin()
1175+
.arg("compress")
1176+
.arg(&source_path)
1177+
.arg(&archive)
1178+
.assert()
1179+
.success();
1180+
1181+
crate::utils::cargo_bin()
1182+
.arg("decompress")
1183+
.arg(archive)
1184+
.arg("-d")
1185+
.arg(&out_path)
1186+
.assert()
1187+
.success();
1188+
1189+
let out_source_meta = fs::File::open(out_path.join("hardlink").join("source"))
1190+
.unwrap()
1191+
.metadata()
1192+
.unwrap();
1193+
let out_link1_meta = fs::File::open(out_path.join("hardlink").join("link1"))
1194+
.unwrap()
1195+
.metadata()
1196+
.unwrap();
1197+
let out_link2_meta = fs::File::open(out_path.join("hardlink").join("link2"))
1198+
.unwrap()
1199+
.metadata()
1200+
.unwrap();
1201+
1202+
assert!(out_source_meta.nlink() > 1);
1203+
assert!(out_link1_meta.nlink() > 1);
1204+
assert!(out_link2_meta.nlink() > 1);
1205+
1206+
assert_eq!(out_source_meta.dev(), out_link1_meta.dev());
1207+
assert_eq!(out_link1_meta.dev(), out_link2_meta.dev());
1208+
1209+
assert_eq!(out_source_meta.ino(), out_link1_meta.ino());
1210+
assert_eq!(out_link1_meta.ino(), out_link2_meta.ino());
1211+
}

0 commit comments

Comments
 (0)