Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix for archiving long paths that have path components starting with ".." crossing the 100-character mark #390

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down
48 changes: 38 additions & 10 deletions src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<P: AsRef<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<P: AsRef<Path>>(
&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()),
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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()),
Expand All @@ -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()),
Expand Down Expand Up @@ -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 => {}
Expand Down
30 changes: 30 additions & 0 deletions tests/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R: Read>(mut entries: Entries<R>) {
let mut a = t!(entries.next().unwrap());
assert_eq!(&*a.header().path_bytes(), b"a");
Expand Down