use anyhow::{bail, Context, Result};
use flate2::read::GzDecoder;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use tar::Archive;
use tracing::{debug, info};
pub fn extract(data: &[u8], dest_dir: &Path) -> Result<()> {
let abs_dest = dest_dir.canonicalize().context("resolving dest dir")?;
let decoder = GzDecoder::new(data);
let mut archive = Archive::new(decoder);
let mut entry_count = 0u64;
for entry_result in archive.entries().context("reading tar entries")? {
let mut entry = entry_result.context("reading tar entry")?;
entry_count += 1;
let path = entry.path().context("reading entry path")?.into_owned();
if entry_count <= 10 {
info!(
name = %path.display(),
kind = entry.header().entry_type().as_byte(),
size = entry.header().size().unwrap_or(0),
"tar entry"
);
}
match validate_entry(&path, &entry, &abs_dest)? {
EntryAction::Skip => {
debug!(name = %path.display(), "skipping tar entry");
continue;
}
EntryAction::Process => {}
}
let target = abs_dest.join(clean_path(&path));
match entry.header().entry_type() {
tar::EntryType::Directory => {
fs::create_dir_all(&target)
.with_context(|| format!("creating directory {}", target.display()))?;
}
tar::EntryType::Regular => {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("creating parent dir for {}", target.display()))?;
}
let mode = entry.header().mode().unwrap_or(0o644);
let mut contents = Vec::new();
entry
.read_to_end(&mut contents)
.with_context(|| format!("reading file {}", target.display()))?;
fs::write(&target, &contents)
.with_context(|| format!("writing file {}", target.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&target, fs::Permissions::from_mode(mode)).ok();
}
}
_ => {
continue;
}
}
}
info!(total_entries = entry_count, "tar extraction complete");
Ok(())
}
fn clean_path(path: &Path) -> PathBuf {
let mut clean = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::Normal(c) => clean.push(c),
std::path::Component::CurDir => {}
_ => {}
}
}
clean
}
enum EntryAction {
Process,
Skip,
}
fn validate_entry(
path: &Path,
entry: &tar::Entry<impl Read>,
dest_dir: &Path,
) -> Result<EntryAction> {
let path_str = path.to_string_lossy();
if path.is_absolute() {
bail!("tar contains absolute path: {path_str}");
}
if path_str.contains("..") {
bail!("tar contains path traversal: {path_str}");
}
let target = dest_dir.join(clean_path(path));
if !target.starts_with(dest_dir) {
bail!("tar entry escapes destination: {path_str}");
}
let entry_type = entry.header().entry_type();
if entry_type == tar::EntryType::Symlink || entry_type == tar::EntryType::Link {
return Ok(EntryAction::Skip);
}
Ok(EntryAction::Process)
}