1use std::fs;
7use std::io::{Read as _, Write as _};
8use std::path::{Path, PathBuf};
9
10use flate2::write::DeflateEncoder;
11use flate2::{Compression, GzBuilder};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Entry {
16 pub name: String,
18 pub path: PathBuf,
20 pub executable: bool,
22}
23
24pub fn entries(
28 dir: &Path,
29 prefix: &str,
30 executable: &dyn Fn(&Path) -> bool,
31) -> Result<Vec<Entry>, String> {
32 let mut found = Vec::new();
33 walk(dir, prefix, executable, &mut found)?;
34 found.sort_by(|a, b| a.name.cmp(&b.name));
35 Ok(found)
36}
37
38fn walk(
39 dir: &Path,
40 prefix: &str,
41 executable: &dyn Fn(&Path) -> bool,
42 found: &mut Vec<Entry>,
43) -> Result<(), String> {
44 let list = fs::read_dir(dir).map_err(|e| format!("read {}: {e}", dir.display()))?;
45 for item in list {
46 let path = item
47 .map_err(|e| format!("read {}: {e}", dir.display()))?
48 .path();
49 let name = format!(
50 "{prefix}{}",
51 path.file_name().unwrap_or_default().to_string_lossy()
52 );
53 if path.is_dir() {
54 walk(&path, &format!("{name}/"), executable, found)?;
55 } else {
56 found.push(Entry {
57 executable: executable(&path),
58 name,
59 path,
60 });
61 }
62 }
63 Ok(())
64}
65
66const DOS_DATE: u16 = (1 << 5) | 1;
68
69pub fn zip(out: &Path, entries: &[Entry]) -> Result<(), String> {
71 let mut body = Vec::new();
72 let mut central = Vec::new();
73 for entry in entries {
74 let data =
75 fs::read(&entry.path).map_err(|e| format!("read {}: {e}", entry.path.display()))?;
76 let mut encoder = DeflateEncoder::new(Vec::new(), Compression::best());
77 encoder
78 .write_all(&data)
79 .map_err(|e| format!("deflate {}: {e}", entry.name))?;
80 let packed = encoder
81 .finish()
82 .map_err(|e| format!("deflate {}: {e}", entry.name))?;
83 let too_big = || format!("zip: {} is over 4 GiB", entry.name);
84 let size = u32::try_from(data.len()).map_err(|_| too_big())?;
85 let packed_size = u32::try_from(packed.len()).map_err(|_| too_big())?;
86 let offset = u32::try_from(body.len()).map_err(|_| too_big())?;
87 let crc = crc32fast::hash(&data);
88 let name = entry.name.as_bytes();
89 let name_len = u16::try_from(name.len())
90 .map_err(|_| format!("zip: {} is too long a name", entry.name))?;
91 let mut shared = Vec::new();
94 for half in [20u16, 0x0800, 8, 0, DOS_DATE] {
95 shared.extend(half.to_le_bytes());
96 }
97 for word in [crc, packed_size, size] {
98 shared.extend(word.to_le_bytes());
99 }
100 shared.extend(name_len.to_le_bytes());
101 shared.extend(0u16.to_le_bytes());
102
103 body.extend(0x0403_4b50u32.to_le_bytes());
104 body.extend(&shared);
105 body.extend(name);
106 body.extend(&packed);
107
108 let mode: u32 = if entry.executable {
109 0o100_755
110 } else {
111 0o100_644
112 };
113 central.extend(0x0201_4b50u32.to_le_bytes());
114 central.extend(0x031eu16.to_le_bytes());
116 central.extend(&shared);
117 central.extend([0u8; 6]);
119 central.extend((mode << 16).to_le_bytes());
120 central.extend(offset.to_le_bytes());
121 central.extend(name);
122 }
123 let count = u16::try_from(entries.len()).map_err(|_| "zip: over 65535 files".to_owned())?;
124 let too_big = || "zip: over 4 GiB".to_owned();
125 let central_size = u32::try_from(central.len()).map_err(|_| too_big())?;
126 let central_at = u32::try_from(body.len()).map_err(|_| too_big())?;
127 body.extend(central);
128 body.extend(0x0605_4b50u32.to_le_bytes());
129 body.extend([0u8; 4]);
130 body.extend(count.to_le_bytes());
131 body.extend(count.to_le_bytes());
132 body.extend(central_size.to_le_bytes());
133 body.extend(central_at.to_le_bytes());
134 body.extend(0u16.to_le_bytes());
135 fs::write(out, body).map_err(|e| format!("write {}: {e}", out.display()))
136}
137
138pub fn zip_names(bytes: &[u8]) -> Result<Vec<String>, String> {
141 let end = bytes
142 .windows(4)
143 .rposition(|w| w == 0x0605_4b50u32.to_le_bytes())
144 .ok_or("zip: no end of central directory")?;
145 let half = |at: usize| u16::from_le_bytes([bytes[at], bytes[at + 1]]);
146 let word =
147 |at: usize| u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]);
148 let count = half(end + 10);
149 let mut at = word(end + 16) as usize;
150 let mut names = Vec::new();
151 for _ in 0..count {
152 if bytes.get(at..at + 4) != Some(&0x0201_4b50u32.to_le_bytes()[..]) {
153 return Err("zip: a broken central directory".to_owned());
154 }
155 let name_len = half(at + 28) as usize;
156 let skip = half(at + 30) as usize + half(at + 32) as usize;
157 let name = bytes
158 .get(at + 46..at + 46 + name_len)
159 .ok_or("zip: a name past the end")?;
160 names.push(String::from_utf8_lossy(name).into_owned());
161 at += 46 + name_len + skip;
162 }
163 Ok(names)
164}
165
166pub fn read(path: &Path, limit: u64) -> Result<Vec<(String, Vec<u8>)>, String> {
170 let bytes = fs::read(path).map_err(|e| format!("read {}: {e}", path.display()))?;
171 let read = if path.to_string_lossy().ends_with(".tar.gz") {
172 read_tar_gz(&bytes, limit)
173 } else {
174 read_zip(&bytes, limit)
175 };
176 read.map_err(|e| format!("{}: {e}", path.display()))
177}
178
179fn read_zip(bytes: &[u8], limit: u64) -> Result<Vec<(String, Vec<u8>)>, String> {
181 let at = |at: usize, n: usize| bytes.get(at..at + n).ok_or("zip: cut short".to_owned());
182 let half = |offset: usize| at(offset, 2).map(|b| u16::from_le_bytes([b[0], b[1]]) as usize);
183 let word = |offset: usize| {
184 at(offset, 4).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize)
185 };
186 let end = bytes
187 .windows(4)
188 .rposition(|w| w == 0x0605_4b50u32.to_le_bytes())
189 .ok_or("zip: no end of central directory")?;
190 let mut next = word(end + 16)?;
191 let mut total = 0u64;
192 let mut files = Vec::new();
193 for _ in 0..half(end + 10)? {
194 if at(next, 4)? != 0x0201_4b50u32.to_le_bytes() {
195 return Err("zip: a broken central directory".to_owned());
196 }
197 let method = half(next + 10)?;
198 let packed = word(next + 20)?;
199 let size = word(next + 24)? as u64;
200 let name_len = half(next + 28)?;
201 let skip = half(next + 30)? + half(next + 32)?;
202 let local = word(next + 42)?;
203 let name = String::from_utf8_lossy(at(next + 46, name_len)?).into_owned();
204 total = total.saturating_add(size);
205 if total > limit {
206 return Err(format!("zip: over {limit} bytes unpacked"));
207 }
208 let start = local + 30 + half(local + 26)? + half(local + 28)?;
209 let data = at(start, packed)?;
210 let inner = match method {
211 0 => data.to_vec(),
212 8 => {
213 let mut inner = Vec::new();
214 flate2::read::DeflateDecoder::new(data)
215 .take(size)
216 .read_to_end(&mut inner)
217 .map_err(|e| format!("zip: {name}: {e}"))?;
218 inner
219 }
220 other => return Err(format!("zip: {name}: compression method {other}")),
221 };
222 files.push((name, inner));
223 next += 46 + name_len + skip;
224 }
225 Ok(files)
226}
227
228fn read_tar_gz(bytes: &[u8], limit: u64) -> Result<Vec<(String, Vec<u8>)>, String> {
230 let mut tar = Vec::new();
231 flate2::read::GzDecoder::new(bytes)
232 .take(limit.saturating_add(1))
233 .read_to_end(&mut tar)
234 .map_err(|e| format!("tar: {e}"))?;
235 if tar.len() as u64 > limit {
236 return Err(format!("tar: over {limit} bytes unpacked"));
237 }
238 let text = |field: &[u8]| {
239 let end = field.iter().position(|&b| b == 0).unwrap_or(field.len());
240 String::from_utf8_lossy(&field[..end]).into_owned()
241 };
242 let mut files = Vec::new();
243 let mut at = 0;
244 while let Some(header) = tar.get(at..at + 512).filter(|header| header[0] != 0) {
245 let name = text(&header[..100]);
246 let prefix = text(&header[345..500]);
247 let size = usize::from_str_radix(text(&header[124..136]).trim(), 8)
248 .map_err(|_| format!("tar: {name}: a broken size"))?;
249 let data = at + 512;
250 let inner = data
251 .checked_add(size)
252 .and_then(|end| tar.get(data..end))
253 .ok_or_else(|| format!("tar: {name}: cut short"))?;
254 let name = if prefix.is_empty() {
255 name
256 } else {
257 format!("{prefix}/{name}")
258 };
259 files.push((name, inner.to_vec()));
260 at = data + size.div_ceil(512) * 512;
261 }
262 Ok(files)
263}
264
265pub fn tar_gz(out: &Path, entries: &[Entry]) -> Result<(), String> {
267 let file = fs::File::create(out).map_err(|e| format!("write {}: {e}", out.display()))?;
268 let mut gz = GzBuilder::new().mtime(0).write(file, Compression::best());
269 let failed = |e: std::io::Error| format!("write {}: {e}", out.display());
270 for entry in entries {
271 let data =
272 fs::read(&entry.path).map_err(|e| format!("read {}: {e}", entry.path.display()))?;
273 let mode = if entry.executable { 0o755 } else { 0o644 };
274 gz.write_all(&tar_header(&entry.name, mode, data.len() as u64)?)
275 .map_err(failed)?;
276 gz.write_all(&data).map_err(failed)?;
277 let pad = (512 - data.len() % 512) % 512;
278 gz.write_all(&vec![0; pad]).map_err(failed)?;
279 }
280 gz.write_all(&[0; 1024]).map_err(failed)?;
281 gz.finish().map_err(failed)?;
282 Ok(())
283}
284
285fn tar_header(name: &str, mode: u32, size: u64) -> Result<[u8; 512], String> {
288 let mut header = [0u8; 512];
289 let (prefix, name) = if name.len() <= 100 {
290 ("", name)
291 } else {
292 name.char_indices()
293 .filter(|&(at, c)| c == '/' && at <= 155 && name.len() - at - 1 <= 100)
294 .map(|(at, _)| (&name[..at], &name[at + 1..]))
295 .next()
296 .ok_or_else(|| format!("tar: the name {name} is too long"))?
297 };
298 let put = |header: &mut [u8; 512], at: usize, text: &[u8]| {
299 header[at..at + text.len()].copy_from_slice(text);
300 };
301 put(&mut header, 0, name.as_bytes());
302 put(&mut header, 100, format!("{mode:07o}\0").as_bytes());
303 put(&mut header, 108, b"0000000\0");
304 put(&mut header, 116, b"0000000\0");
305 put(&mut header, 124, format!("{size:011o}\0").as_bytes());
306 put(&mut header, 136, b"00000000000\0");
307 put(&mut header, 148, b" ");
308 header[156] = b'0';
309 put(&mut header, 257, b"ustar\0");
310 put(&mut header, 263, b"00");
311 put(&mut header, 265, b"root");
312 put(&mut header, 297, b"root");
313 put(&mut header, 345, prefix.as_bytes());
314 let sum: u32 = header.iter().map(|&b| u32::from(b)).sum();
315 put(&mut header, 148, format!("{sum:06o}\0 ").as_bytes());
316 Ok(header)
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 fn fixture(name: &str) -> PathBuf {
324 let dir = std::env::temp_dir().join(format!("shuyi-archive-{name}-{}", std::process::id()));
325 let _ = fs::remove_dir_all(&dir);
326 fs::create_dir_all(dir.join("pkg/sub")).unwrap();
327 fs::write(dir.join("pkg/game"), b"\x7fELF program").unwrap();
328 fs::write(dir.join("pkg/NOTICES.txt"), "notices\n".repeat(50)).unwrap();
329 fs::write(dir.join("pkg/sub/a.ogg"), b"ogg").unwrap();
330 dir
331 }
332
333 fn listed(dir: &Path, prefix: &str) -> Vec<Entry> {
334 entries(&dir.join("pkg"), prefix, &|path| path.ends_with("game")).unwrap()
335 }
336
337 #[test]
338 fn entries_are_sorted_and_named_from_the_prefix() {
339 let dir = fixture("entries");
340 let found = listed(&dir, "g-1.0-linux/");
341 let names: Vec<&str> = found.iter().map(|e| e.name.as_str()).collect();
342 assert_eq!(
343 names,
344 [
345 "g-1.0-linux/NOTICES.txt",
346 "g-1.0-linux/game",
347 "g-1.0-linux/sub/a.ogg"
348 ]
349 );
350 assert!(found[1].executable && !found[0].executable);
351 assert!(entries(&dir.join("missing"), "", &|_| false).is_err());
352 fs::remove_dir_all(&dir).unwrap();
353 }
354
355 #[test]
356 fn a_zip_lists_its_files_and_inflates_to_them_the_same_each_time() {
357 let dir = fixture("zip");
358 let found = listed(&dir, "");
359 zip(&dir.join("a.zip"), &found).unwrap();
360 zip(&dir.join("b.zip"), &found).unwrap();
361 let bytes = fs::read(dir.join("a.zip")).unwrap();
362 assert_eq!(bytes, fs::read(dir.join("b.zip")).unwrap());
363 assert_eq!(
364 zip_names(&bytes).unwrap(),
365 ["NOTICES.txt", "game", "sub/a.ogg"]
366 );
367 let name_len = u16::from_le_bytes([bytes[26], bytes[27]]) as usize;
369 let packed = u32::from_le_bytes([bytes[18], bytes[19], bytes[20], bytes[21]]) as usize;
370 let start = 30 + name_len;
371 let mut out = String::new();
372 flate2::read::DeflateDecoder::new(&bytes[start..start + packed])
373 .read_to_string(&mut out)
374 .unwrap();
375 assert_eq!(out, "notices\n".repeat(50));
376 assert!(zip_names(b"not a zip").is_err());
377 fs::remove_dir_all(&dir).unwrap();
378 }
379
380 #[test]
381 fn a_tar_gz_holds_each_file_with_its_mode() {
382 let dir = fixture("tar");
383 tar_gz(&dir.join("a.tar.gz"), &listed(&dir, "g/")).unwrap();
384 let mut tar = Vec::new();
385 flate2::read::GzDecoder::new(fs::File::open(dir.join("a.tar.gz")).unwrap())
386 .read_to_end(&mut tar)
387 .unwrap();
388 assert_eq!(tar.len(), 512 * (2 + 2 + 2 + 2));
390 assert_eq!(&tar[..13], b"g/NOTICES.txt");
391 assert_eq!(&tar[100..107], b"0000644");
392 assert_eq!(&tar[1024..1030], b"g/game");
393 assert_eq!(&tar[1024 + 100..1024 + 107], b"0000755");
394 assert_eq!(&tar[257..262], b"ustar");
395 let mut header = tar[..512].to_vec();
398 header[148..156].copy_from_slice(b" ");
399 let sum: u32 = header.iter().map(|&b| u32::from(b)).sum();
400 assert_eq!(&tar[148..155], format!("{sum:06o}\0").as_bytes());
401 fs::remove_dir_all(&dir).unwrap();
402 }
403
404 #[test]
405 fn a_long_tar_name_goes_in_the_prefix() {
406 let long = format!("{}/{}", "d".repeat(120), "f".repeat(50));
407 let header = tar_header(&long, 0o644, 1).unwrap();
408 assert_eq!(&header[..50], "f".repeat(50).as_bytes());
409 assert_eq!(&header[345..465], "d".repeat(120).as_bytes());
410 assert!(tar_header(&"x".repeat(300), 0o644, 1).is_err());
411 }
412}