diff --git a/src/builder.rs b/src/builder.rs index 82e73541..569b14e6 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -768,7 +768,7 @@ fn prepare_header_path(dst: &mut dyn Write, header: &mut Header, path: &Path) -> Ok(s) => s, Err(e) => str::from_utf8(&data[..e.valid_up_to()]).unwrap(), }; - header.set_path(truncated)?; + header.set_truncated_path_for_gnu_header(&truncated)?; } Ok(()) } diff --git a/src/header.rs b/src/header.rs index 8e39ab63..6afee29a 100644 --- a/src/header.rs +++ b/src/header.rs @@ -380,14 +380,30 @@ impl Header { /// use `Builder` methods to insert a long-name extension at the same time /// as the file content. pub fn set_path>(&mut self, p: P) -> io::Result<()> { - self._set_path(p.as_ref()) + self._set_path(p.as_ref(), false) } - fn _set_path(&mut self, path: &Path) -> io::Result<()> { + // Sets the truncated path for GNU header + // + // Same as set_path but skips some validations. + pub(crate) fn set_truncated_path_for_gnu_header>( + &mut self, + p: P, + ) -> io::Result<()> { + self._set_path(p.as_ref(), true) + } + + fn _set_path(&mut self, path: &Path, is_truncated_gnu_long_path: bool) -> io::Result<()> { if let Some(ustar) = self.as_ustar_mut() { return ustar.set_path(path); } - copy_path_into(&mut self.as_old_mut().name, path, false).map_err(|err| { + copy_path_into( + &mut self.as_old_mut().name, + path, + false, + is_truncated_gnu_long_path, + ) + .map_err(|err| { io::Error::new( err.kind(), format!("{} when setting path for {}", err, self.path_lossy()), @@ -439,7 +455,7 @@ impl Header { } fn _set_link_name(&mut self, path: &Path) -> io::Result<()> { - copy_path_into(&mut self.as_old_mut().linkname, path, true).map_err(|err| { + copy_path_into(&mut self.as_old_mut().linkname, path, true, false).map_err(|err| { io::Error::new( err.kind(), format!("{} when setting link name for {}", err, self.path_lossy()), @@ -991,7 +1007,7 @@ impl UstarHeader { let bytes = path2bytes(path)?; let (maxnamelen, maxprefixlen) = (self.name.len(), self.prefix.len()); if bytes.len() <= maxnamelen { - copy_path_into(&mut self.name, path, false).map_err(|err| { + copy_path_into(&mut self.name, path, false, false).map_err(|err| { io::Error::new( err.kind(), format!("{} when setting path for {}", err, self.path_lossy()), @@ -1015,14 +1031,14 @@ impl UstarHeader { break; } } - copy_path_into(&mut self.prefix, prefix, false).map_err(|err| { + copy_path_into(&mut self.prefix, prefix, false, false).map_err(|err| { io::Error::new( err.kind(), format!("{} when setting path for {}", err, self.path_lossy()), ) })?; let path = bytes2path(Cow::Borrowed(&bytes[prefixlen + 1..]))?; - copy_path_into(&mut self.name, &path, false).map_err(|err| { + copy_path_into(&mut self.name, &path, false, false).map_err(|err| { io::Error::new( err.kind(), format!("{} when setting path for {}", err, self.path_lossy()), @@ -1540,17 +1556,29 @@ fn copy_into(slot: &mut [u8], bytes: &[u8]) -> io::Result<()> { /// * a nul byte was found /// * an invalid path component is encountered (e.g. a root path or parent dir) /// * the path itself is empty -fn copy_path_into(mut slot: &mut [u8], path: &Path, is_link_name: bool) -> io::Result<()> { +fn copy_path_into( + mut slot: &mut [u8], + path: &Path, + is_link_name: bool, + is_truncated_gnu_long_path: bool, +) -> io::Result<()> { let mut emitted = false; let mut needs_slash = false; - for component in path.components() { + let mut iter = path.components().peekable(); + while let Some(component) = iter.next() { let bytes = path2bytes(Path::new(component.as_os_str()))?; match (component, is_link_name) { (Component::Prefix(..), false) | (Component::RootDir, false) => { return Err(other("paths in archives must be relative")); } (Component::ParentDir, false) => { - return Err(other("paths in archives must not have `..`")); + if is_truncated_gnu_long_path && iter.peek().is_none() { + // If it's last component of a gnu long path we know that there might be more + // to the component than .. (the rest is stored elsewhere) + {} + } else { + return Err(other("paths in archives must not have `..`")); + } } // Allow "./" as the path (Component::CurDir, false) if path.components().count() == 1 => {} diff --git a/tests/all.rs b/tests/all.rs index 0e1c2613..62e534b3 100644 --- a/tests/all.rs +++ b/tests/all.rs @@ -203,6 +203,36 @@ fn large_filename() { assert!(entries.next().is_none()); } +// This test checks very particular scenario where a path component starting +// with ".." of a long path gets split at 100-byte mark so that ".." part goes +// into header and gets interpreted as parent dir (and rejected) . +#[test] +fn large_filename_with_dot_dot_at_100_byte_mark() { + let mut ar = Builder::new(Vec::new()); + + let mut header = Header::new_gnu(); + header.set_cksum(); + header.set_mode(0o644); + header.set_size(4); + + let mut long_name_with_dot_dot = "tdir/".repeat(19); + long_name_with_dot_dot.push_str("tt/..file"); + + t!(ar.append_data(&mut header, &long_name_with_dot_dot, &b"test"[..])); + + let rd = Cursor::new(t!(ar.into_inner())); + let mut ar = Archive::new(rd); + let mut entries = t!(ar.entries()); + + let mut f = entries.next().unwrap().unwrap(); + assert_eq!(&*f.path_bytes(), long_name_with_dot_dot.as_bytes()); + assert_eq!(f.header().size().unwrap(), 4); + let mut s = String::new(); + t!(f.read_to_string(&mut s)); + assert_eq!(s, "test"); + assert!(entries.next().is_none()); +} + fn reading_entries_common(mut entries: Entries) { let mut a = t!(entries.next().unwrap()); assert_eq!(&*a.header().path_bytes(), b"a");