Skip to main content

modde_sources/wabbajack/
inline.rs

1//! Reads inline data entries embedded in a `.wabbajack` zip archive, guarding
2//! against unsafe (absolute, traversal, or symlink) entry paths. See
3//! [`InlineSource`].
4
5use std::collections::HashSet;
6use std::fs::File;
7use std::io::Read as _;
8use std::path::{Path, PathBuf};
9use std::sync::Mutex;
10
11use anyhow::{Context, Result, bail};
12
13pub struct InlineSource {
14    path: PathBuf,
15    names: HashSet<String>,
16    archive: Mutex<zip::ZipArchive<File>>,
17}
18
19impl InlineSource {
20    pub fn open(path: &Path) -> Result<Self> {
21        let file = File::open(path)
22            .with_context(|| format!("failed to open wabbajack file: {}", path.display()))?;
23        let archive = zip::ZipArchive::new(file)
24            .with_context(|| format!("failed to read wabbajack zip: {}", path.display()))?;
25        let mut names = HashSet::with_capacity(archive.len());
26        for i in 0..archive.len() {
27            if let Some(name) = archive.name_for_index(i) {
28                names.insert(name.to_string());
29            }
30        }
31
32        Ok(Self {
33            path: path.to_path_buf(),
34            names,
35            archive: Mutex::new(archive),
36        })
37    }
38
39    pub fn read(&self, name: &str) -> Result<Vec<u8>> {
40        if !self.names.contains(name) {
41            bail!(
42                "inline data entry '{}' not found in wabbajack zip {}",
43                name,
44                self.path.display()
45            );
46        }
47
48        let mut archive = self
49            .archive
50            .lock()
51            .map_err(|_| anyhow::anyhow!("inline zip lock poisoned"))?;
52        let mut entry = archive
53            .by_name(name)
54            .with_context(|| format!("inline data entry '{name}' not found in wabbajack zip"))?;
55        validate_zip_entry(&entry)?;
56        let mut data = Vec::with_capacity(entry.size() as usize);
57        entry.read_to_end(&mut data)?;
58        Ok(data)
59    }
60}
61
62fn validate_zip_entry<R: std::io::Read + ?Sized>(entry: &zip::read::ZipFile<'_, R>) -> Result<()> {
63    let name = entry.name();
64    let normalized = name.replace('\\', "/");
65    if normalized.starts_with('/') || normalized.split('/').any(|part| part == "..") {
66        bail!("inline zip entry contains unsafe path: {name}");
67    }
68    if entry.is_symlink() {
69        bail!("inline zip entry is a symlink (rejected): {name}");
70    }
71    Ok(())
72}