banished_nexus/
install.rs1use std::io::Read;
9
10use crate::Error;
11
12pub const MAX_PACKAGE: u64 = 1 << 30;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Package {
19 pub name: String,
21 pub data: Vec<u8>,
23}
24
25fn kind(data: &[u8]) -> Option<&'static str> {
27 if data.starts_with(b"PK\x03\x04") || data.starts_with(b"PK\x05\x06") {
28 Some("zip")
29 } else if data.starts_with(b"7z\xbc\xaf\x27\x1c") {
30 Some("7z")
31 } else if data.starts_with(b"Rar!\x1a\x07") {
32 Some("rar")
33 } else {
34 None
35 }
36}
37
38fn package_name(path: &str) -> Option<String> {
40 let name = path.rsplit(['/', '\\']).next()?.trim();
41 let ok = name.len() > 4
42 && name.to_ascii_lowercase().ends_with(".pkm")
43 && !name.starts_with('.')
44 && !name.contains(['\0', ':']);
45 ok.then(|| name.to_string())
46}
47
48pub fn packages(file_name: &str, data: &[u8]) -> Result<Vec<Package>, Error> {
50 match kind(data) {
51 Some("zip") => from_zip(data),
52 Some(other) => Err(Error::Unsupported(other.to_string())),
53 None => match package_name(file_name) {
54 Some(name) => Ok(vec![Package {
55 name,
56 data: data.to_vec(),
57 }]),
58 None => Err(Error::Unsupported(crate::api::extension(file_name))),
59 },
60 }
61}
62
63fn from_zip(data: &[u8]) -> Result<Vec<Package>, Error> {
64 let bad = |e: zip::result::ZipError| Error::Answer(format!("the zip file is damaged: {e}"));
65 let mut archive = zip::ZipArchive::new(std::io::Cursor::new(data)).map_err(bad)?;
66 let mut out: Vec<Package> = Vec::new();
67 for i in 0..archive.len() {
68 let mut entry = archive.by_index(i).map_err(bad)?;
69 if !entry.is_file() {
70 continue;
71 }
72 let Some(name) = entry.name().ok().and_then(|n| package_name(&n)) else {
73 continue;
74 };
75 if entry.size() > MAX_PACKAGE {
76 return Err(Error::Answer(format!("{name} is too large")));
77 }
78 let mut bytes = Vec::with_capacity(entry.size() as usize);
79 (&mut entry)
80 .take(MAX_PACKAGE + 1)
81 .read_to_end(&mut bytes)
82 .map_err(|e| Error::Answer(format!("could not unpack {name}: {e}")))?;
83 if bytes.len() as u64 > MAX_PACKAGE {
84 return Err(Error::Answer(format!("{name} is too large")));
85 }
86 if !out.iter().any(|p| p.name.eq_ignore_ascii_case(&name)) {
88 out.push(Package { name, data: bytes });
89 }
90 }
91 if out.is_empty() {
92 return Err(Error::NoPackage);
93 }
94 Ok(out)
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use std::io::Write;
101
102 fn zip_of(files: &[(&str, &[u8])]) -> Vec<u8> {
103 let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
104 for (name, data) in files {
105 w.start_file(*name, zip::write::SimpleFileOptions::default())
106 .unwrap();
107 w.write_all(data).unwrap();
108 }
109 w.finish().unwrap().into_inner()
110 }
111
112 #[test]
113 fn takes_packages_from_a_zip() {
114 let z = zip_of(&[
115 ("Readme.txt", b"hello"),
116 ("MoreTerrain/MoreTerrain.pkm", b"PKG1"),
117 ("../../evil/Other.PKM", b"PKG2"),
118 ]);
119 let p = packages("More Terrain v6-19-6.zip", &z).unwrap();
120 let names: Vec<&str> = p.iter().map(|p| p.name.as_str()).collect();
121 assert_eq!(names, ["MoreTerrain.pkm", "Other.PKM"]);
122 assert_eq!(p[0].data, b"PKG1");
123 assert_eq!(
124 packages("x.zip", &zip_of(&[("a.txt", b"x")])),
125 Err(Error::NoPackage)
126 );
127 }
128
129 #[test]
130 fn bare_packages_and_other_archives() {
131 assert_eq!(
132 packages("Apiary.pkm", b"data").unwrap()[0].name,
133 "Apiary.pkm"
134 );
135 assert_eq!(
136 packages("a.rar", b"Rar!\x1a\x07\x00rest"),
137 Err(Error::Unsupported("rar".into()))
138 );
139 assert_eq!(
140 packages("a.7z", b"7z\xbc\xaf\x27\x1crest"),
141 Err(Error::Unsupported("7z".into()))
142 );
143 assert_eq!(
144 packages("notes.txt", b"x"),
145 Err(Error::Unsupported("txt".into()))
146 );
147 }
148}