modde_sources/wabbajack/
inline.rs1use 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}