Skip to content

Commit f732623

Browse files
authored
fix for archiving long paths that have path components starting with ".." crossing the 100-character mark
The gnu tar supports arbirary path length by putting path truncated to standard 100 chars into the header and the rest is appended to contents. tar-rs validates that no path components should be exactly ".." but in this case when a component starting with ".." (for example file named "..some_file") gets truncated after 2 characters we hit this validation and can't tar such file.
1 parent e1323ee commit f732623

File tree

3 files changed

+93
-15
lines changed

3 files changed

+93
-15
lines changed

src/builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ async fn prepare_header_path(
538538
// Truncate the path to store in the header we're about to emit to
539539
// ensure we've got something at least mentioned.
540540
let path = bytes2path(Cow::Borrowed(&data[..max]))?;
541-
header.set_path(&path)?;
541+
header.set_truncated_path_for_gnu_header(&path)?;
542542
}
543543
Ok(())
544544
}

src/header.rs

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -359,14 +359,29 @@ impl Header {
359359
/// in the appropriate format. May fail if the path is too long or if the
360360
/// path specified is not Unicode and this is a Windows platform.
361361
pub fn set_path<P: AsRef<Path>>(&mut self, p: P) -> io::Result<()> {
362-
self._set_path(p.as_ref())
362+
self.set_path_inner(p.as_ref(), false)
363363
}
364364

365-
fn _set_path(&mut self, path: &Path) -> io::Result<()> {
365+
// Sets the truncated path for GNU header
366+
//
367+
// Same as `set_path`` but skips some validations.
368+
pub(crate) fn set_truncated_path_for_gnu_header<P: AsRef<Path>>(
369+
&mut self,
370+
p: P,
371+
) -> io::Result<()> {
372+
self.set_path_inner(p.as_ref(), true)
373+
}
374+
375+
fn set_path_inner(&mut self, path: &Path, is_truncated_gnu_long_path: bool) -> io::Result<()> {
366376
if let Some(ustar) = self.as_ustar_mut() {
367377
return ustar.set_path(path);
368378
}
369-
copy_path_into(&mut self.as_old_mut().name, path, false).map_err(|err| {
379+
if is_truncated_gnu_long_path {
380+
copy_path_into_gnu_long(&mut self.as_old_mut().name, path, false)
381+
} else {
382+
copy_path_into(&mut self.as_old_mut().name, path, false)
383+
}
384+
.map_err(|err| {
370385
io::Error::new(
371386
err.kind(),
372387
format!("{} when setting path for {}", err, self.path_lossy()),
@@ -1465,25 +1480,29 @@ fn copy_into(slot: &mut [u8], bytes: &[u8]) -> io::Result<()> {
14651480
}
14661481
}
14671482

1468-
/// Copies `path` into the `slot` provided
1469-
///
1470-
/// Returns an error if:
1471-
///
1472-
/// * the path is too long to fit
1473-
/// * a nul byte was found
1474-
/// * an invalid path component is encountered (e.g. a root path or parent dir)
1475-
/// * the path itself is empty
1476-
fn copy_path_into(mut slot: &mut [u8], path: &Path, is_link_name: bool) -> io::Result<()> {
1483+
fn copy_path_into_inner(
1484+
mut slot: &mut [u8],
1485+
path: &Path,
1486+
is_link_name: bool,
1487+
is_truncated_gnu_long_path: bool,
1488+
) -> io::Result<()> {
14771489
let mut emitted = false;
14781490
let mut needs_slash = false;
1479-
for component in path.components() {
1491+
let mut iter = path.components().peekable();
1492+
while let Some(component) = iter.next() {
14801493
let bytes = path2bytes(Path::new(component.as_os_str()))?;
14811494
match (component, is_link_name) {
14821495
(Component::Prefix(..), false) | (Component::RootDir, false) => {
14831496
return Err(other("paths in archives must be relative"));
14841497
}
14851498
(Component::ParentDir, false) => {
1486-
return Err(other("paths in archives must not have `..`"));
1499+
if is_truncated_gnu_long_path && iter.peek().is_none() {
1500+
// If it's last component of a gnu long path we know that there might be more
1501+
// to the component than .. (the rest is stored elsewhere)
1502+
{}
1503+
} else {
1504+
return Err(other("paths in archives must not have `..`"));
1505+
}
14871506
}
14881507
// Allow "./" as the path
14891508
(Component::CurDir, false) if path.components().count() == 1 => {}
@@ -1520,6 +1539,32 @@ fn copy_path_into(mut slot: &mut [u8], path: &Path, is_link_name: bool) -> io::R
15201539
}
15211540
}
15221541

1542+
/// Copies `path` into the `slot` provided
1543+
///
1544+
/// Returns an error if:
1545+
///
1546+
/// * the path is too long to fit
1547+
/// * a nul byte was found
1548+
/// * an invalid path component is encountered (e.g. a root path or parent dir)
1549+
/// * the path itself is empty
1550+
fn copy_path_into(slot: &mut [u8], path: &Path, is_link_name: bool) -> io::Result<()> {
1551+
copy_path_into_inner(slot, path, is_link_name, false)
1552+
}
1553+
1554+
/// Copies `path` into the `slot` provided
1555+
///
1556+
/// Returns an error if:
1557+
///
1558+
/// * the path is too long to fit
1559+
/// * a nul byte was found
1560+
/// * an invalid path component is encountered (e.g. a root path or parent dir)
1561+
/// * the path itself is empty
1562+
///
1563+
/// This is less restrictive version meant to be used for truncated GNU paths.
1564+
fn copy_path_into_gnu_long(slot: &mut [u8], path: &Path, is_link_name: bool) -> io::Result<()> {
1565+
copy_path_into_inner(slot, path, is_link_name, true)
1566+
}
1567+
15231568
#[cfg(target_arch = "wasm32")]
15241569
fn ends_with_slash(p: &Path) -> bool {
15251570
p.to_string_lossy().ends_with('/')

tests/all.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,39 @@ async fn large_filename() {
213213
assert!(entries.next().await.is_none());
214214
}
215215

216+
// This test checks very particular scenario where path component
217+
// starting with ".." of a long path gets split at 100-byte mark
218+
// so that ".." goes into header and gets interpreted as parent dir
219+
// (and rejected) .
220+
#[async_std::test]
221+
async fn large_filename_with_dot_dot_at_100_byte_mark() {
222+
let mut ar = Builder::new(Vec::new());
223+
224+
let mut header = Header::new_gnu();
225+
header.set_cksum();
226+
header.set_mode(0o644);
227+
header.set_size(4);
228+
229+
let mut long_name_with_dot_dot = "tdir/".repeat(19);
230+
long_name_with_dot_dot.push_str("tt/..file");
231+
232+
t!(ar
233+
.append_data(&mut header, &long_name_with_dot_dot, &b"test"[..])
234+
.await);
235+
236+
let rd = Cursor::new(t!(ar.into_inner().await));
237+
let ar = Archive::new(rd);
238+
let mut entries = t!(ar.entries());
239+
240+
let mut f = entries.next().await.unwrap().unwrap();
241+
assert_eq!(&*f.path_bytes(), long_name_with_dot_dot.as_bytes());
242+
assert_eq!(f.header().size().unwrap(), 4);
243+
let mut s = String::new();
244+
t!(f.read_to_string(&mut s).await);
245+
assert_eq!(s, "test");
246+
assert!(entries.next().await.is_none());
247+
}
248+
216249
#[async_std::test]
217250
async fn reading_entries() {
218251
let rdr = Cursor::new(tar!("reading_files.tar"));

0 commit comments

Comments
 (0)