use crate::header::TarHeader;
use std::ffi::OsStr;
use std::fs::{self, OpenOptions};
use std::io::{self, Read};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
pub struct Decoder<R: Read> {
reader: R,
}
impl<R: Read> Decoder<R> {
pub fn new(reader: R) -> Self {
Self { reader }
}
pub fn unpack<P: AsRef<Path>>(&mut self, dst: P) -> io::Result<()> {
let dst_path = dst.as_ref();
if !dst_path.exists() {
fs::create_dir_all(dst_path)?;
}
loop {
let mut header_buf = [0u8; 512];
match self.reader.read_exact(&mut header_buf) {
Ok(_) => (),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e),
}
if header_buf.iter().all(|&b| b == 0) {
break;
}
let header = unsafe { &*(header_buf.as_ptr() as *const TarHeader) };
let name_bytes = header.name.split(|&b| b == 0).next().unwrap_or(&[]);
let name_os_str = OsStr::from_bytes(name_bytes);
let size = TarHeader::parse_octal(&header.size)?;
let mode = TarHeader::parse_octal(&header.mode)? as u32;
let target_path = dst_path.join(name_os_str);
if !target_path.starts_with(dst_path) {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"Path traversal detected",
));
}
if let Some(parent) = target_path.parent() {
fs::create_dir_all(parent)?;
}
if header.typeflag == b'0' || header.typeflag == b'\0' {
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&target_path)?;
file.set_permissions(fs::Permissions::from_mode(mode))?;
let mut limit = self.reader.by_ref().take(size);
io::copy(&mut limit, &mut file)?;
let remainder = size % 512;
if remainder > 0 {
let padding = 512 - remainder;
io::copy(&mut self.reader.by_ref().take(padding), &mut io::sink())?;
}
}
}
Ok(())
}
}