1use anyhow::{bail, Context, Result};
2use flate2::read::GzDecoder;
3use std::fs;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use tar::Archive;
7use tracing::{debug, info};
8
9pub fn extract(data: &[u8], dest_dir: &Path) -> Result<()> {
10 let abs_dest = dest_dir.canonicalize().context("resolving dest dir")?;
11
12 let decoder = GzDecoder::new(data);
13 let mut archive = Archive::new(decoder);
14
15 let mut entry_count = 0u64;
16 for entry_result in archive.entries().context("reading tar entries")? {
17 let mut entry = entry_result.context("reading tar entry")?;
18 entry_count += 1;
19
20 let path = entry.path().context("reading entry path")?.into_owned();
21
22 if entry_count <= 10 {
23 info!(
24 name = %path.display(),
25 kind = entry.header().entry_type().as_byte(),
26 size = entry.header().size().unwrap_or(0),
27 "tar entry"
28 );
29 }
30
31 match validate_entry(&path, &entry, &abs_dest)? {
32 EntryAction::Skip => {
33 debug!(name = %path.display(), "skipping tar entry");
34 continue;
35 }
36 EntryAction::Process => {}
37 }
38
39 let target = abs_dest.join(clean_path(&path));
40
41 match entry.header().entry_type() {
42 tar::EntryType::Directory => {
43 fs::create_dir_all(&target)
44 .with_context(|| format!("creating directory {}", target.display()))?;
45 }
46 tar::EntryType::Regular => {
47 if let Some(parent) = target.parent() {
48 fs::create_dir_all(parent)
49 .with_context(|| format!("creating parent dir for {}", target.display()))?;
50 }
51 let mode = entry.header().mode().unwrap_or(0o644);
52 let mut contents = Vec::new();
53 entry
54 .read_to_end(&mut contents)
55 .with_context(|| format!("reading file {}", target.display()))?;
56 fs::write(&target, &contents)
57 .with_context(|| format!("writing file {}", target.display()))?;
58 #[cfg(unix)]
59 {
60 use std::os::unix::fs::PermissionsExt;
61 fs::set_permissions(&target, fs::Permissions::from_mode(mode)).ok();
62 }
63 }
64 _ => {
65 continue;
66 }
67 }
68 }
69
70 info!(total_entries = entry_count, "tar extraction complete");
71 Ok(())
72}
73
74fn clean_path(path: &Path) -> PathBuf {
75 let mut clean = PathBuf::new();
76 for component in path.components() {
77 match component {
78 std::path::Component::Normal(c) => clean.push(c),
79 std::path::Component::CurDir => {}
80 _ => {}
81 }
82 }
83 clean
84}
85
86enum EntryAction {
87 Process,
88 Skip,
89}
90
91fn validate_entry(
92 path: &Path,
93 entry: &tar::Entry<impl Read>,
94 dest_dir: &Path,
95) -> Result<EntryAction> {
96 let path_str = path.to_string_lossy();
97
98 if path.is_absolute() {
99 bail!("tar contains absolute path: {path_str}");
100 }
101
102 if path_str.contains("..") {
103 bail!("tar contains path traversal: {path_str}");
104 }
105
106 let target = dest_dir.join(clean_path(path));
107 if !target.starts_with(dest_dir) {
108 bail!("tar entry escapes destination: {path_str}");
109 }
110
111 let entry_type = entry.header().entry_type();
112 if entry_type == tar::EntryType::Symlink || entry_type == tar::EntryType::Link {
113 return Ok(EntryAction::Skip);
114 }
115
116 Ok(EntryAction::Process)
117}