1use std::io::{Cursor, Read};
9
10use super::error::OrkError;
11use super::warning::{Imported, Warning, WarningKind};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
15#[serde(rename_all = "snake_case")]
16#[non_exhaustive]
17pub enum Container {
18 Zip,
20 Gzip,
22 Xml,
24}
25
26impl Container {
27 pub fn as_str(self) -> &'static str {
29 match self {
30 Self::Zip => "zip",
31 Self::Gzip => "gzip",
32 Self::Xml => "xml",
33 }
34 }
35
36 pub fn sniff(bytes: &[u8]) -> Option<Self> {
48 match bytes {
49 [b'P', b'K', 3, 4, ..] | [b'P', b'K', 5, 6, ..] | [b'P', b'K', 7, 8, ..] => {
50 Some(Self::Zip)
51 }
52 [0x1f, 0x8b, ..] => Some(Self::Gzip),
53 _ if xml_body(bytes).starts_with(b"<") => Some(Self::Xml),
54 _ => None,
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Attachment {
66 pub name: String,
68 pub bytes: Vec<u8>,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct Unpacked {
75 pub container: Container,
77 pub design_entry: Option<String>,
79 pub design: String,
81 pub attachments: Vec<Attachment>,
83}
84
85pub fn unpack(bytes: &[u8]) -> Result<Imported<Unpacked>, OrkError> {
91 unpack_within(bytes, MAX_UNPACKED_BYTES)
92}
93
94pub fn unpack_within(bytes: &[u8], budget: u64) -> Result<Imported<Unpacked>, OrkError> {
100 match Container::sniff(bytes) {
101 Some(Container::Zip) => unpack_zip(bytes, budget),
102 Some(Container::Gzip) => {
103 let mut text = Vec::new();
104 flate2::read::GzDecoder::new(bytes)
105 .take(budget.saturating_add(1))
106 .read_to_end(&mut text)
107 .map_err(|error| OrkError::Gzip {
108 reason: error.to_string(),
109 })?;
110 if text.len() as u64 > budget {
111 return Err(OrkError::TooBig { limit: budget });
112 }
113 Ok(Imported::clean(Unpacked {
114 container: Container::Gzip,
115 design_entry: None,
116 design: utf8(text)?,
117 attachments: Vec::new(),
118 }))
119 }
120 Some(Container::Xml) => Ok(Imported::clean(Unpacked {
121 container: Container::Xml,
122 design_entry: None,
123 design: utf8(bytes.to_vec())?,
124 attachments: Vec::new(),
125 })),
126 None => Err(OrkError::UnknownContainer { head: head(bytes) }),
127 }
128}
129
130const DESIGN_ENTRY: &str = "rocket.ork";
132
133pub const MAX_UNPACKED_BYTES: u64 = 256 * 1024 * 1024;
143
144fn unpack_zip(bytes: &[u8], budget: u64) -> Result<Imported<Unpacked>, OrkError> {
145 let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|error| OrkError::Zip {
146 reason: error.to_string(),
147 })?;
148 let mut warnings = Vec::new();
149 let mut entries: Vec<Attachment> = Vec::new();
150 let mut used = 0u64;
151 for index in 0..archive.len() {
152 let mut entry = match archive.by_index(index) {
153 Ok(entry) => entry,
154 Err(error) => {
155 warnings.push(Warning::new(
156 format!("entry {index}"),
157 WarningKind::Skipped,
158 format!("this entry could not be opened and was left out: {error}"),
159 ));
160 continue;
161 }
162 };
163 if entry.is_dir() {
164 continue;
165 }
166 let name = entry.name().to_owned();
167 let left = budget.saturating_sub(used);
168 let mut content = Vec::new();
169 if let Err(error) = entry.by_ref().take(left + 1).read_to_end(&mut content) {
172 warnings.push(Warning::new(
173 name,
174 WarningKind::Skipped,
175 format!("this entry could not be decompressed and was left out: {error}"),
176 ));
177 continue;
178 }
179 if content.len() as u64 > left {
180 warnings.push(Warning::new(
181 name,
182 WarningKind::Skipped,
183 format!(
184 "this entry was left out: unpacking it would pass the {budget}-byte limit on \
185 what one archive may decompress to"
186 ),
187 ));
188 continue;
189 }
190 used += content.len() as u64;
191 entries.push(Attachment {
192 name,
193 bytes: content,
194 });
195 }
196
197 let chosen = choose_design(&entries, &mut warnings).ok_or(OrkError::NoDesign)?;
198 let design_bytes = entries.remove(chosen);
199 let design = utf8(design_bytes.bytes)?;
200 Ok(Imported {
201 value: Unpacked {
202 container: Container::Zip,
203 design_entry: Some(design_bytes.name),
204 design,
205 attachments: entries,
206 },
207 warnings,
208 })
209}
210
211fn choose_design(entries: &[Attachment], warnings: &mut Vec<Warning>) -> Option<usize> {
213 if let Some(index) = entries.iter().position(|e| e.name == DESIGN_ENTRY) {
214 return Some(index);
215 }
216 let candidates: Vec<usize> = entries
217 .iter()
218 .enumerate()
219 .filter(|(_, e)| {
220 let lower = e.name.to_ascii_lowercase();
221 lower.ends_with(".ork") || lower.ends_with(".xml")
222 })
223 .map(|(index, _)| index)
224 .collect();
225 let first = *candidates.first()?;
226 warnings.push(Warning::new(
227 entries[first].name.clone(),
228 WarningKind::Unusual,
229 if candidates.len() > 1 {
230 format!(
231 "the archive has no `{DESIGN_ENTRY}`; this is the first of {} entries that could \
232 be the design, and the others are kept as attachments",
233 candidates.len()
234 )
235 } else {
236 format!("the archive has no `{DESIGN_ENTRY}`; this entry was read as the design")
237 },
238 ));
239 Some(first)
240}
241
242fn xml_body(bytes: &[u8]) -> &[u8] {
244 let rest = bytes.strip_prefix(&[0xef, 0xbb, 0xbf]).unwrap_or(bytes);
245 let start = rest
246 .iter()
247 .position(|byte| !byte.is_ascii_whitespace())
248 .unwrap_or(rest.len());
249 &rest[start..]
250}
251
252fn utf8(bytes: Vec<u8>) -> Result<String, OrkError> {
255 let mut text = String::from_utf8(bytes).map_err(|_| OrkError::NotUtf8)?;
256 if text.starts_with('\u{feff}') {
257 text.remove(0);
258 }
259 Ok(text)
260}
261
262fn head(bytes: &[u8]) -> String {
264 if bytes.is_empty() {
265 return "nothing (the file is empty)".to_owned();
266 }
267 bytes
268 .iter()
269 .take(4)
270 .map(|byte| format!("{byte:02x}"))
271 .collect::<Vec<_>>()
272 .join(" ")
273}