#![doc(html_root_url = "http://alexcrichton.com/tar-rs")]
#![deny(missing_docs)]
#![cfg_attr(test, deny(warnings))]
extern crate libc;
extern crate filetime;
use std::borrow::Cow;
use std::cell::{RefCell, Cell};
use std::cmp;
use std::fmt;
use std::fs;
use std::io::prelude::*;
use std::io::{self, Error, ErrorKind, SeekFrom};
use std::iter::repeat;
use std::mem;
use std::path::{Path, PathBuf, Component};
use std::str;
use filetime::FileTime;
#[cfg(unix)] use std::os::unix::prelude::*;
#[cfg(unix)] use std::ffi::{OsStr, OsString};
#[cfg(windows)] use std::os::windows::prelude::*;
macro_rules! try_iter{ ($me:expr, $e:expr) => (
match $e {
Ok(e) => e,
Err(e) => { $me.done = true; return Some(Err(e)) }
}
) }
pub struct Archive<R> {
obj: RefCell<R>,
pos: Cell<u64>,
}
pub struct Files<'a, R:'a> {
archive: &'a Archive<R>,
done: bool,
offset: u64,
}
pub struct FilesMut<'a, R:'a> {
archive: &'a Archive<R>,
next: u64,
done: bool,
}
pub struct File<'a, R: 'a> {
header: Header,
archive: &'a Archive<R>,
pos: u64,
size: u64,
seek: fn(&File<R>) -> io::Result<()>,
tar_offset: u64,
}
#[repr(C)]
#[allow(missing_docs)]
pub struct Header {
pub name: [u8; 100],
pub mode: [u8; 8],
pub owner_id: [u8; 8],
pub group_id: [u8; 8],
pub size: [u8; 12],
pub mtime: [u8; 12],
pub cksum: [u8; 8],
pub link: [u8; 1],
pub linkname: [u8; 100],
pub ustar: [u8; 6],
pub ustar_version: [u8; 2],
pub owner_name: [u8; 32],
pub group_name: [u8; 32],
pub dev_major: [u8; 8],
pub dev_minor: [u8; 8],
pub prefix: [u8; 155],
_rest: [u8; 12],
}
impl<O> Archive<O> {
pub fn new(obj: O) -> Archive<O> {
Archive { obj: RefCell::new(obj), pos: Cell::new(0) }
}
pub fn into_inner(self) -> O {
self.obj.into_inner()
}
}
impl<R: Seek + Read> Archive<R> {
pub fn files(&self) -> io::Result<Files<R>> {
try!(self.seek(0));
Ok(Files { archive: self, done: false, offset: 0 })
}
fn seek(&self, pos: u64) -> io::Result<()> {
if self.pos.get() == pos { return Ok(()) }
try!(self.obj.borrow_mut().seek(SeekFrom::Start(pos)));
self.pos.set(pos);
Ok(())
}
}
impl<R: Read> Archive<R> {
pub fn files_mut(&mut self) -> io::Result<FilesMut<R>> {
Ok(FilesMut { archive: self, done: false, next: 0 })
}
pub fn unpack<P: AsRef<Path>>(&mut self, dst: P) -> io::Result<()> {
self.unpack2(dst.as_ref())
}
fn unpack2(&mut self, dst: &Path) -> io::Result<()> {
'outer: for file in try!(self.files_mut()) {
let mut file = try!(file);
let mut file_dst = dst.to_path_buf();
for part in try!(file.header().path()).components() {
match part {
Component::Prefix(..) |
Component::RootDir |
Component::CurDir => continue,
Component::ParentDir => continue 'outer,
Component::Normal(part) => file_dst.push(part),
}
}
if *dst == *file_dst {
continue
}
if file.header().link[0] == b'5' {
try!(fs::create_dir_all(&file_dst));
} else {
try!(fs::create_dir_all(&file_dst.parent().unwrap()));
try!(file.unpack(&file_dst));
}
}
Ok(())
}
fn skip(&self, mut amt: u64) -> io::Result<()> {
let mut buf = [0u8; 4096 * 8];
let mut me = self;
while amt > 0 {
let n = cmp::min(amt, buf.len() as u64);
let n = try!(Read::read(&mut me, &mut buf[..n as usize]));
amt -= n as u64;
}
Ok(())
}
fn next_file(&self, offset: &mut u64, seek: fn(&File<R>) -> io::Result<()>)
-> io::Result<Option<File<R>>> {
let mut chunk = [0; 512];
let mut cnt = 0;
let mut me = self;
loop {
try!(read_all(&mut me, &mut chunk));
*offset += 512;
if chunk.iter().any(|i| *i != 0) { break }
cnt += 1;
if cnt > 1 { return Ok(None) }
}
let sum = chunk[..148].iter().map(|i| *i as u32).fold(0, |a, b| a + b) +
chunk[156..].iter().map(|i| *i as u32).fold(0, |a, b| a + b) +
32 * 8;
let header: Header = unsafe { mem::transmute(chunk) };
let ret = File {
archive: self,
pos: 0,
size: try!(header.size()),
header: header,
tar_offset: *offset,
seek: seek,
};
let cksum = try!(ret.header.cksum());
if sum != cksum { return Err(bad_archive()) }
let size = (ret.size + 511) & !(512 - 1);
*offset += size;
return Ok(Some(ret));
}
}
impl<W: Write> Archive<W> {
pub fn append(&self, header: &Header, mut data: &mut Read) -> io::Result<()> {
let mut obj = self.obj.borrow_mut();
try!(obj.write_all(header.as_bytes()));
let len = try!(io::copy(&mut data, &mut *obj));
let buf = [0; 512];
let remaining = 512 - (len % 512);
if remaining < 512 {
try!(obj.write_all(&buf[..remaining as usize]));
}
Ok(())
}
pub fn append_path<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
self.append_path2(path.as_ref())
}
fn append_path2(&self, path: &Path) -> io::Result<()> {
let mut file = try!(fs::File::open(path));
self.append_file(path, &mut file)
}
pub fn append_file<P: AsRef<Path>>(&self, path: P, file: &mut fs::File)
-> io::Result<()> {
self.append_file2(path.as_ref(), file)
}
fn append_file2(&self, path: &Path, file: &mut fs::File) -> io::Result<()> {
let stat = try!(file.metadata());
let mut header = Header::new();
try!(header.set_path(path));
header.set_metadata(&stat);
header.set_cksum();
self.append(&header, file)
}
pub fn finish(&self) -> io::Result<()> {
let b = [0; 1024];
self.obj.borrow_mut().write_all(&b)
}
}
impl<'a, R: Seek + Read> Iterator for Files<'a, R> {
type Item = io::Result<File<'a, R>>;
fn next(&mut self) -> Option<io::Result<File<'a, R>>> {
if self.done { return None }
try_iter!(self, self.archive.seek(self.offset));
fn doseek<R: Seek + Read>(file: &File<R>) -> io::Result<()> {
file.archive.seek(file.tar_offset + file.pos)
}
match try_iter!(self, self.archive.next_file(&mut self.offset, doseek)) {
None => { self.done = true; None }
Some(f) => Some(Ok(f)),
}
}
}
impl<'a, R: Read> Iterator for FilesMut<'a, R> {
type Item = io::Result<File<'a, R>>;
fn next(&mut self) -> Option<io::Result<File<'a, R>>> {
if self.done { return None }
let delta = self.next - self.archive.pos.get();
try_iter!(self, self.archive.skip(delta));
fn doseek<R>(_: &File<R>) -> io::Result<()> { Ok(()) }
match try_iter!(self, self.archive.next_file(&mut self.next, doseek)) {
None => { self.done = true; None }
Some(f) => Some(Ok(f)),
}
}
}
impl Header {
pub fn new() -> Header {
let mut header: Header = unsafe { mem::zeroed() };
header.ustar = *b"ustar\0";
header.ustar_version = *b"00";
return header
}
fn is_ustar(&self) -> bool {
&self.ustar[..5] == b"ustar"
}
fn as_bytes(&self) -> &[u8; 512] {
unsafe { &*(self as *const _ as *const [u8; 512]) }
}
pub fn set_metadata(&mut self, meta: &fs::Metadata) {
self.fill_from(meta);
}
pub fn size(&self) -> io::Result<u64> {
octal_from(&self.size)
}
pub fn set_size(&mut self, size: u64) {
octal_into(&mut self.size, size)
}
pub fn path(&self) -> io::Result<Cow<Path>> {
return bytes2path(self.path_bytes());
#[cfg(windows)]
fn bytes2path(bytes: Cow<[u8]>) -> io::Result<Cow<Path>> {
match bytes {
Cow::Borrowed(bytes) => {
let s = try!(str::from_utf8(bytes).map_err(|_| {
not_unicode()
}));
Ok(Cow::Borrowed(Path::new(s)))
}
Cow::Owned(bytes) => {
let s = try!(String::from_utf8(bytes).map_err(|_| {
not_unicode()
}));
Ok(Cow::Owned(PathBuf::from(s)))
}
}
}
#[cfg(unix)]
fn bytes2path(bytes: Cow<[u8]>) -> io::Result<Cow<Path>> {
Ok(match bytes {
Cow::Borrowed(bytes) => Cow::Borrowed({
Path::new(OsStr::from_bytes(bytes))
}),
Cow::Owned(bytes) => Cow::Owned({
PathBuf::from(OsString::from_vec(bytes))
})
})
}
}
pub fn path_bytes(&self) -> Cow<[u8]> {
if (!self.is_ustar() || self.prefix[0] == 0) &&
!self.name.contains(&b'\\') {
Cow::Borrowed(truncate(&self.name))
} else {
Cow::Owned(truncate(&self.prefix).iter().cloned()
.chain(Some(b'/'))
.chain(truncate(&self.name).iter().cloned())
.map(|b| if b == b'\\' {b'/'} else {b})
.collect())
}
}
pub fn set_path<P: AsRef<Path>>(&mut self, p: P) -> io::Result<()> {
self.set_path2(p.as_ref())
}
fn set_path2(&mut self, path: &Path) -> io::Result<()> {
let bytes = match bytes(path) {
Some(b) => b,
None => return Err(Error::new(ErrorKind::Other, "path was not \
valid unicode")),
};
if bytes.iter().any(|b| *b == 0) {
return Err(Error::new(ErrorKind::Other, "path contained a nul byte"))
}
let (namelen, prefixlen) = (self.name.len(), self.prefix.len());
if bytes.len() < namelen {
try!(copy_into(&mut self.name, bytes, true));
} else {
let prefix = &bytes[..cmp::min(bytes.len(), prefixlen)];
let pos = match prefix.iter().rposition(|&b| b == b'/' || b == b'\\') {
Some(i) => i,
None => return Err(Error::new(ErrorKind::Other,
"path cannot be split to be \
inserted into archive")),
};
try!(copy_into(&mut self.name, &bytes[pos + 1..], true));
try!(copy_into(&mut self.prefix, &bytes[..pos], true));
}
return Ok(());
#[cfg(windows)]
fn bytes(p: &Path) -> Option<&[u8]> {
p.as_os_str().to_str().map(|s| s.as_bytes())
}
#[cfg(unix)]
fn bytes(p: &Path) -> Option<&[u8]> {
Some(p.as_os_str().as_bytes())
}
}
pub fn mode(&self) -> io::Result<u32> {
octal_from(&self.mode).map(|u| u as u32)
}
pub fn set_mode(&mut self, mode: u32) {
octal_into(&mut self.mode, mode & 0o3777);
}
pub fn uid(&self) -> io::Result<u32> {
octal_from(&self.owner_id).map(|u| u as u32)
}
pub fn set_uid(&mut self, uid: u32) {
octal_into(&mut self.owner_id, uid);
}
pub fn gid(&self) -> io::Result<u32> {
octal_from(&self.group_id).map(|u| u as u32)
}
pub fn set_gid(&mut self, gid: u32) {
octal_into(&mut self.group_id, gid);
}
pub fn mtime(&self) -> io::Result<u64> {
octal_from(&self.mtime)
}
pub fn set_mtime(&mut self, mtime: u64) {
octal_into(&mut self.mtime, mtime);
}
pub fn username(&self) -> Option<&str> {
self.username_bytes().and_then(|s| str::from_utf8(s).ok())
}
pub fn username_bytes(&self) -> Option<&[u8]> {
if self.is_ustar() {
Some(truncate(&self.owner_name))
} else {
None
}
}
pub fn set_username(&mut self, name: &str) -> io::Result<()> {
copy_into(&mut self.owner_name, name.as_bytes(), false)
}
pub fn groupname(&self) -> Option<&str> {
self.groupname_bytes().and_then(|s| str::from_utf8(s).ok())
}
pub fn groupname_bytes(&self) -> Option<&[u8]> {
if self.is_ustar() {
Some(truncate(&self.group_name))
} else {
None
}
}
pub fn set_groupname(&mut self, name: &str) -> io::Result<()> {
copy_into(&mut self.group_name, name.as_bytes(), false)
}
pub fn device_major(&self) -> Option<io::Result<u32>> {
if self.is_ustar() {
Some(octal_from(&self.dev_major).map(|u| u as u32))
} else {
None
}
}
pub fn set_device_major(&mut self, major: u32) {
octal_into(&mut self.dev_major, major);
}
pub fn device_minor(&self) -> Option<io::Result<u32>> {
if self.is_ustar() {
Some(octal_from(&self.dev_minor).map(|u| u as u32))
} else {
None
}
}
pub fn set_device_minor(&mut self, minor: u32) {
octal_into(&mut self.dev_minor, minor);
}
pub fn cksum(&self) -> io::Result<u32> {
octal_from(&self.cksum).map(|u| u as u32)
}
pub fn set_cksum(&mut self) {
let cksum = {
let bytes = self.as_bytes();
bytes[..148].iter().map(|i| *i as u32).fold(0, |a, b| a + b) +
bytes[156..].iter().map(|i| *i as u32).fold(0, |a, b| a + b) +
32 * (self.cksum.len() as u32)
};
octal_into(&mut self.cksum, cksum);
}
#[cfg(unix)]
fn fill_from(&mut self, meta: &fs::Metadata) {
self.set_mode((meta.mode() & 0o3777) as u32);
self.set_mtime(meta.mtime() as u64);
self.set_uid(meta.uid() as u32);
self.set_gid(meta.gid() as u32);
self.set_size(meta.size() as u64);
self.set_device_major(0);
self.set_device_minor(0);
self.link[0] = match meta.mode() & libc::S_IFMT {
libc::S_IFREG => b'0',
libc::S_IFLNK => b'2',
libc::S_IFCHR => b'3',
libc::S_IFBLK => b'4',
libc::S_IFDIR => b'5',
libc::S_IFIFO => b'6',
_ => b' ',
};
}
#[cfg(windows)]
fn fill_from(&mut self, meta: &fs::Metadata) {
let readonly = meta.file_attributes() & libc::FILE_ATTRIBUTE_READONLY;
let mode = match (meta.is_dir(), readonly != 0) {
(true, false) => 0o755,
(true, true) => 0o555,
(false, false) => 0o644,
(false, true) => 0o444,
};
self.set_mode(mode);
self.set_uid(0);
self.set_gid(0);
self.set_size(meta.len());
self.set_device_major(0);
self.set_device_minor(0);
let ft = meta.file_type();
self.link[0] = if ft.is_dir() {
b'5'
} else if ft.is_file() {
b'0'
} else if ft.is_symlink() {
b'2'
} else {
b' '
};
let mtime = (meta.last_write_time() / (1_000_000_000 / 100)) - 11644473600;
self.set_mtime(mtime);
}
}
impl<'a, R: Read> File<'a, R> {
pub fn header(&self) -> &Header { &self.header }
pub fn unpack<P: AsRef<Path>>(&mut self, dst: P) -> io::Result<()> {
self.unpack2(dst.as_ref())
}
fn unpack2(&mut self, dst: &Path) -> io::Result<()> {
try!(io::copy(self, &mut try!(fs::File::create(dst))));
let mtime = try!(self.header().mtime());
let mtime = FileTime::from_seconds_since_1970(mtime, 0);
try!(filetime::set_file_times(dst, mtime, mtime));
try!(set_perms(dst, try!(self.header().mode())));
return Ok(());
#[cfg(unix)]
fn set_perms(dst: &Path, mode: u32) -> io::Result<()> {
use std::os::unix::raw;
let perm = fs::Permissions::from_mode(mode as raw::mode_t);
fs::set_permissions(dst, perm)
}
#[cfg(windows)]
fn set_perms(dst: &Path, mode: u32) -> io::Result<()> {
let mut perm = try!(fs::metadata(dst)).permissions();
perm.set_readonly(mode & 0o200 != 0o200);
fs::set_permissions(dst, perm)
}
}
}
impl<'a, R: Read> Read for &'a Archive<R> {
fn read(&mut self, into: &mut [u8]) -> io::Result<usize> {
self.obj.borrow_mut().read(into).map(|i| {
self.pos.set(self.pos.get() + i as u64);
i
})
}
}
impl<'a, R: Read> Read for File<'a, R> {
fn read(&mut self, into: &mut [u8]) -> io::Result<usize> {
if self.size == self.pos { return Ok(0) }
try!((self.seek)(self));
let amt = cmp::min((self.size - self.pos) as usize, into.len());
let amt = try!(Read::read(&mut self.archive, &mut into[..amt]));
self.pos += amt as u64;
Ok(amt)
}
}
impl<'a, R: Read + Seek> Seek for File<'a, R> {
fn seek(&mut self, how: SeekFrom) -> io::Result<u64> {
let next = match how {
SeekFrom::Start(pos) => pos as i64,
SeekFrom::Current(pos) => self.pos as i64 + pos,
SeekFrom::End(pos) => self.size as i64 + pos,
};
if next < 0 {
Err(Error::new(ErrorKind::Other, "cannot seek before position 0"))
} else if next as u64 > self.size {
Err(Error::new(ErrorKind::Other, "cannot seek past end of file"))
} else {
self.pos = next as u64;
Ok(self.pos)
}
}
}
fn bad_archive() -> Error {
Error::new(ErrorKind::Other, "invalid tar archive")
}
fn octal_from(slice: &[u8]) -> io::Result<u64> {
let num = match str::from_utf8(truncate(slice)) {
Ok(n) => n,
Err(_) => return Err(bad_archive()),
};
match u64::from_str_radix(num.trim(), 8) {
Ok(n) => Ok(n),
Err(_) => Err(bad_archive())
}
}
fn octal_into<T: fmt::Octal>(dst: &mut [u8], val: T) {
let o = format!("{:o}", val);
let value = o.bytes().rev().chain(repeat(b'0'));
for (slot, value) in dst.iter_mut().rev().skip(1).zip(value) {
*slot = value;
}
}
fn truncate<'a>(slice: &'a [u8]) -> &'a [u8] {
match slice.iter().position(|i| *i == 0) {
Some(i) => &slice[..i],
None => slice,
}
}
fn read_all<R: Read>(r: &mut R, buf: &mut [u8]) -> io::Result<()> {
let mut read = 0;
while read < buf.len() {
match try!(r.read(&mut buf[read..])) {
0 => return Err(bad_archive()),
n => read += n,
}
}
Ok(())
}
fn copy_into(slot: &mut [u8], bytes: &[u8], map_slashes: bool) -> io::Result<()> {
if bytes.len() > slot.len() {
Err(Error::new(ErrorKind::Other, "provided value is too long"))
} else if bytes.iter().any(|b| *b == 0) {
Err(Error::new(ErrorKind::Other, "provided value contains a nul byte"))
} else {
for (slot, val) in slot.iter_mut().zip(bytes) {
if map_slashes && *val == b'\\' {
*slot = b'/';
} else {
*slot = *val;
}
}
Ok(())
}
}
#[cfg(windows)]
fn not_unicode() -> Error {
Error::new(ErrorKind::Other, "only unicode paths are supported on windows")
}
#[cfg(test)]
mod tests {
extern crate tempdir;
use std::io::prelude::*;
use std::io::{Cursor, SeekFrom};
use std::iter::repeat;
use std::fs::{self, File};
use filetime::FileTime;
use self::tempdir::TempDir;
use super::Archive;
macro_rules! t {
($e:expr) => (match $e {
Ok(v) => v,
Err(e) => panic!("{} returned {}", stringify!($e), e),
})
}
#[test]
fn simple() {
let ar = Archive::new(Cursor::new(&include_bytes!("tests/simple.tar")[..]));
for file in t!(ar.files()) {
t!(file);
}
}
#[test]
fn reading_files() {
let rdr = Cursor::new(&include_bytes!("tests/reading_files.tar")[..]);
let ar = Archive::new(rdr);
let mut files = t!(ar.files());
let mut a = t!(files.next().unwrap());
let mut b = t!(files.next().unwrap());
assert!(files.next().is_none());
assert_eq!(&*a.header().path_bytes(), b"a");
assert_eq!(&*b.header().path_bytes(), b"b");
let mut s = String::new();
t!(a.read_to_string(&mut s));
assert_eq!(s, "a\na\na\na\na\na\na\na\na\na\na\n");
s.truncate(0);
t!(b.read_to_string(&mut s));
assert_eq!(s, "b\nb\nb\nb\nb\nb\nb\nb\nb\nb\nb\n");
t!(a.seek(SeekFrom::Start(0)));
s.truncate(0);
t!(a.read_to_string(&mut s));
assert_eq!(s, "a\na\na\na\na\na\na\na\na\na\na\n");
}
#[test]
fn writing_files() {
let wr = Cursor::new(Vec::new());
let ar = Archive::new(wr);
let td = t!(TempDir::new("tar-rs"));
let path = td.path().join("test");
t!(t!(File::create(&path)).write_all(b"test"));
t!(ar.append_file("test2", &mut t!(File::open(&path))));
t!(ar.finish());
let rd = Cursor::new(ar.into_inner().into_inner());
let ar = Archive::new(rd);
let mut files = t!(ar.files());
let mut f = t!(files.next().unwrap());
assert!(files.next().is_none());
assert_eq!(&*f.header().path_bytes(), b"test2");
assert_eq!(f.header().size().unwrap(), 4);
let mut s = String::new();
t!(f.read_to_string(&mut s));
assert_eq!(s, "test");
}
#[test]
fn large_filename() {
let ar = Archive::new(Cursor::new(Vec::new()));
let td = t!(TempDir::new("tar-rs"));
let path = td.path().join("test");
t!(t!(File::create(&path)).write_all(b"test"));
let filename = repeat("abcd/").take(50).collect::<String>();
t!(ar.append_file(&filename, &mut t!(File::open(&path))));
t!(ar.finish());
let too_long = repeat("abcd").take(200).collect::<String>();
assert!(ar.append_file(&too_long, &mut t!(File::open(&path))).is_err());
let rd = Cursor::new(ar.into_inner().into_inner());
let ar = Archive::new(rd);
let mut files = t!(ar.files());
let mut f = files.next().unwrap().unwrap();
assert!(files.next().is_none());
assert_eq!(&*f.header().path_bytes(), filename.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");
}
#[test]
fn reading_files_mut() {
let rdr = Cursor::new(&include_bytes!("tests/reading_files.tar")[..]);
let mut ar = Archive::new(rdr);
let mut files = t!(ar.files_mut());
let mut a = t!(files.next().unwrap());
assert_eq!(&*a.header().path_bytes(), b"a");
let mut s = String::new();
t!(a.read_to_string(&mut s));
assert_eq!(s, "a\na\na\na\na\na\na\na\na\na\na\n");
s.truncate(0);
t!(a.read_to_string(&mut s));
assert_eq!(s, "");
let mut b = t!(files.next().unwrap());
assert_eq!(&*b.header().path_bytes(), b"b");
s.truncate(0);
t!(b.read_to_string(&mut s));
assert_eq!(s, "b\nb\nb\nb\nb\nb\nb\nb\nb\nb\nb\n");
assert!(files.next().is_none());
}
#[test]
fn extracting_directories() {
let td = t!(TempDir::new("tar-rs"));
let rdr = Cursor::new(&include_bytes!("tests/directory.tar")[..]);
let mut ar = Archive::new(rdr);
t!(ar.unpack(td.path()));
let dir_a = td.path().join("a");
let dir_b = td.path().join("a/b");
let file_c = td.path().join("a/c");
assert!(fs::metadata(&dir_a).map(|m| m.is_dir()).unwrap_or(false));
assert!(fs::metadata(&dir_b).map(|m| m.is_dir()).unwrap_or(false));
assert!(fs::metadata(&file_c).map(|m| m.is_file()).unwrap_or(false));
}
#[test]
fn extracting_duplicate_dirs() {
let td = t!(TempDir::new("tar-rs"));
let rdr = Cursor::new(&include_bytes!("tests/duplicate_dirs.tar")[..]);
let mut ar = Archive::new(rdr);
t!(ar.unpack(td.path()));
let some_dir = td.path().join("some_dir");
assert!(fs::metadata(&some_dir).map(|m| m.is_dir()).unwrap_or(false));
}
#[test]
fn extracting_malicious_tarball() {
use std::fs;
use std::fs::OpenOptions;
use std::io::{Seek, Write};
let td = t!(TempDir::new("tar-rs"));
let mut evil_tar = Cursor::new(Vec::new());
{
let a = Archive::new(&mut evil_tar);
let mut evil_txt_f = t!(OpenOptions::new().read(true).write(true)
.create(true)
.open(td.path().join("evil.txt")));
t!(writeln!(evil_txt_f, "This is an evil file."));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file("/tmp/abs_evil.txt", &mut evil_txt_f));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file("//tmp/abs_evil2.txt", &mut evil_txt_f));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file("///tmp/abs_evil3.txt", &mut evil_txt_f));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file("/./tmp/abs_evil4.txt", &mut evil_txt_f));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file("//./tmp/abs_evil5.txt", &mut evil_txt_f));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file("///./tmp/abs_evil6.txt", &mut evil_txt_f));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file("/../tmp/rel_evil.txt", &mut evil_txt_f));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file("../rel_evil2.txt", &mut evil_txt_f));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file("./../rel_evil3.txt", &mut evil_txt_f));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file("some/../../rel_evil4.txt", &mut evil_txt_f));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file("", &mut evil_txt_f));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file("././//./", &mut evil_txt_f));
t!(evil_txt_f.seek(SeekFrom::Start(0)));
t!(a.append_file(".", &mut evil_txt_f));
t!(a.finish());
}
t!(evil_tar.seek(SeekFrom::Start(0)));
let mut ar = Archive::new(&mut evil_tar);
t!(ar.unpack(td.path()));
assert!(fs::metadata("/tmp/abs_evil.txt").is_err());
assert!(fs::metadata("/tmp/abs_evil.txt2").is_err());
assert!(fs::metadata("/tmp/abs_evil.txt3").is_err());
assert!(fs::metadata("/tmp/abs_evil.txt4").is_err());
assert!(fs::metadata("/tmp/abs_evil.txt5").is_err());
assert!(fs::metadata("/tmp/abs_evil.txt6").is_err());
assert!(fs::metadata("/tmp/rel_evil.txt").is_err());
assert!(fs::metadata("/tmp/rel_evil.txt").is_err());
assert!(fs::metadata(td.path().join("../tmp/rel_evil.txt")).is_err());
assert!(fs::metadata(td.path().join("../rel_evil2.txt")).is_err());
assert!(fs::metadata(td.path().join("../rel_evil3.txt")).is_err());
assert!(fs::metadata(td.path().join("../rel_evil4.txt")).is_err());
assert!(fs::metadata(td.path().join("some")).is_err());
assert!(fs::metadata(td.path().join("tmp")).map(|m| m.is_dir())
.unwrap_or(false));
assert!(fs::metadata(td.path().join("tmp/abs_evil.txt"))
.map(|m| m.is_file()).unwrap_or(false));
assert!(fs::metadata(td.path().join("tmp/abs_evil2.txt"))
.map(|m| m.is_file()).unwrap_or(false));
assert!(fs::metadata(td.path().join("tmp/abs_evil3.txt"))
.map(|m| m.is_file()).unwrap_or(false));
assert!(fs::metadata(td.path().join("tmp/abs_evil4.txt"))
.map(|m| m.is_file()).unwrap_or(false));
assert!(fs::metadata(td.path().join("tmp/abs_evil5.txt"))
.map(|m| m.is_file()).unwrap_or(false));
assert!(fs::metadata(td.path().join("tmp/abs_evil6.txt"))
.map(|m| m.is_file()).unwrap_or(false));
}
#[test]
fn octal_spaces() {
let rdr = Cursor::new(&include_bytes!("tests/spaces.tar")[..]);
let ar = Archive::new(rdr);
let file = ar.files().unwrap().next().unwrap().unwrap();
assert_eq!(file.header().mode().unwrap() & 0o777, 0o777);
assert_eq!(file.header().uid().unwrap(), 0);
assert_eq!(file.header().gid().unwrap(), 0);
assert_eq!(file.header().size().unwrap(), 2);
assert_eq!(file.header().mtime().unwrap(), 0o12440016664);
assert_eq!(file.header().cksum().unwrap(), 0o4253);
}
#[test]
fn empty_filename()
{
let td = t!(TempDir::new("tar-rs"));
let rdr = Cursor::new(&include_bytes!("tests/empty_filename.tar")[..]);
let mut ar = Archive::new(rdr);
assert!(ar.unpack(td.path()).is_err());
}
#[test]
fn file_times() {
let td = t!(TempDir::new("tar-rs"));
let rdr = Cursor::new(&include_bytes!("tests/file_times.tar")[..]);
let mut ar = Archive::new(rdr);
t!(ar.unpack(td.path()));
let meta = fs::metadata(td.path().join("a")).unwrap();
let mtime = FileTime::from_last_modification_time(&meta);
let atime = FileTime::from_last_access_time(&meta);
assert_eq!(mtime.seconds_relative_to_1970(), 1000000000);
assert_eq!(mtime.nanoseconds(), 0);
assert_eq!(atime.seconds_relative_to_1970(), 1000000000);
assert_eq!(atime.nanoseconds(), 0);
}
}