diff --git a/src/entry.rs b/src/entry.rs index f99c897b..83d6a466 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -433,6 +433,21 @@ impl<'a> EntryFields<'a> { }) } + // on Windows one path may be using the extended length syntax while another + // is not, so we remove any prefix before comparing them. + fn path_has_base(&self, path: &Path, base: &Path) -> bool { + let path: PathBuf = path + .components() + .skip_while(|c| matches!(*c, Component::Prefix(_))) + .collect(); + let base: PathBuf = base + .components() + .skip_while(|c| matches!(*c, Component::Prefix(_))) + .collect(); + + path.starts_with(&base) + } + /// Returns access to the header of this entry in the archive. fn unpack(&mut self, target_base: Option<&Path>, dst: &Path) -> io::Result { let kind = self.header.entry_type(); @@ -467,6 +482,9 @@ impl<'a> EntryFields<'a> { // the destination of this hard link is both present and // inside our own directory. This is needed because we want // to make sure to not overwrite anything outside the root. + // If the link points to an absolute path, assume it's relative + // to the target dir by removing the leading '/', as we already + // do for files, see unpack_in() comments. // // Note that this logic is only needed for hard links // currently. With symlinks the `validate_inside_dst` which @@ -475,6 +493,21 @@ impl<'a> EntryFields<'a> { // links though they're canonicalized to their existing path // so we need to validate at this time. Some(ref p) => { + let mut src = src.to_path_buf(); + if src.is_absolute() { + let dest_canon = p.canonicalize()?; + if !self.path_has_base(&src, &dest_canon) { + // Skip root component, making the target relative to the target dir. + // Also skip prefix because on Windows the path may use the extended syntax. + src = src + .components() + .skip_while(|c| { + matches!(*c, Component::RootDir | Component::Prefix(_)) + }) + .collect(); + } + } + let link_src = p.join(src); self.validate_inside_dst(p, &link_src)?; link_src diff --git a/tests/entry.rs b/tests/entry.rs index 757fd7da..784a5f60 100644 --- a/tests/entry.rs +++ b/tests/entry.rs @@ -44,6 +44,16 @@ fn absolute_symlink() { #[test] fn absolute_hardlink() { let td = t!(Builder::new().prefix("tar").tempdir()); + // on macos the tempdir is created in /var/folders/ which is actually + // /private/var/folders/. Canonicalize the path so the link target has + // the proper base. Do not do this on Windows as canonicalize() will + // convert the path to the extended length syntax which cannot be used + // in the tarball paths. + let td_canon = if cfg!(unix) { + td.path().canonicalize().unwrap() + } else { + td.path().to_path_buf() + }; let mut ar = tar::Builder::new(Vec::new()); let mut header = tar::Header::new_gnu(); @@ -58,16 +68,30 @@ fn absolute_hardlink() { header.set_entry_type(tar::EntryType::Link); t!(header.set_path("bar")); // This absolute path under tempdir will be created at unpack time - t!(header.set_link_name(td.path().join("foo"))); + t!(header.set_link_name(td_canon.join("foo"))); + header.set_cksum(); + t!(ar.append(&header, &[][..])); + + let mut header = tar::Header::new_gnu(); + header.set_size(0); + header.set_entry_type(tar::EntryType::Link); + t!(header.set_path("baz")); + // This absolute path on root will be converted when unpacking + if cfg!(unix) { + t!(header.set_link_name("/foo")); + } else { + t!(header.set_link_name("C:\\foo")); + } header.set_cksum(); t!(ar.append(&header, &[][..])); let bytes = t!(ar.into_inner()); let mut ar = tar::Archive::new(&bytes[..]); - t!(ar.unpack(td.path())); + t!(ar.unpack(&td_canon)); t!(td.path().join("foo").metadata()); t!(td.path().join("bar").metadata()); + t!(td.path().join("baz").metadata()); } #[test]