pacenotes_installer\zip/
central_directory.rs1const SIGNATURE_END_OF_DIRECTORY: u32 = 0x0605_4b50;
2const SIGNATURE_DIRECTORY_ENTRY: u32 = 0x0201_4b50;
3pub const SIGNATURE_LOCAL_HEADER: u32 = 0x0403_4b50;
4const END_OF_DIRECTORY_LENGTH: usize = 22;
5const DIRECTORY_ENTRY_LENGTH: usize = 46;
6const ZIP64_COUNT: usize = 0xFFFF;
8const ZIP64_FIELD: usize = 0xFFFF_FFFF;
10
11pub struct Entry {
14 pub path: String,
16 pub is_dir: bool,
17 pub method: u16,
18 pub crc32: u32,
19 pub compressed: usize,
20 pub uncompressed: usize,
21 pub local_offset: usize,
22}
23
24pub fn read(zip: &[u8]) -> Result<Vec<Entry>, super::ZipError> {
26 let end_record = find_end_of_directory(zip)?;
27 let count = u16_at(zip, end_record + 10).ok_or_else(super::ZipError::truncated)? as usize;
28 let size = u32_at(zip, end_record + 12).ok_or_else(super::ZipError::truncated)? as usize;
29 let offset = u32_at(zip, end_record + 16).ok_or_else(super::ZipError::truncated)? as usize;
30 if count == ZIP64_COUNT || size == ZIP64_FIELD || offset == ZIP64_FIELD {
31 return Err(super::ZipError::new(
32 "this package uses the zip64 format, which the installer cannot read",
33 ));
34 }
35 let end = offset
36 .checked_add(size)
37 .filter(|&end| end <= zip.len())
38 .ok_or_else(super::ZipError::truncated)?;
39
40 let mut entries = Vec::with_capacity(count);
41 let mut at = offset;
42 while at < end && u32_at(zip, at) == Some(SIGNATURE_DIRECTORY_ENTRY) {
43 let (entry, next) = read_entry(zip, at)?;
44 entries.push(entry);
45 at = next;
46 }
47 if entries.len() != count {
49 return Err(super::ZipError::new(format!(
50 "this package is damaged: it lists {count} entries but only {} could be read",
51 entries.len()
52 )));
53 }
54 Ok(keep_last_of_each_path(entries))
55}
56
57fn keep_last_of_each_path(entries: Vec<Entry>) -> Vec<Entry> {
60 let mut seen = std::collections::BTreeSet::new();
61 let mut kept: Vec<Entry> = entries
62 .into_iter()
63 .rev()
64 .filter(|entry| seen.insert(entry.path.to_lowercase()))
65 .collect();
66 kept.reverse();
67 kept
68}
69
70fn read_entry(zip: &[u8], at: usize) -> Result<(Entry, usize), super::ZipError> {
72 let field16 = |offset: usize| u16_at(zip, at + offset).ok_or_else(super::ZipError::truncated);
73 let field32 = |offset: usize| u32_at(zip, at + offset).ok_or_else(super::ZipError::truncated);
74 let flags = field16(8)?;
75 let method = field16(10)?;
76 let crc32 = field32(16)?;
77 let compressed = field32(20)? as usize;
78 let uncompressed = field32(24)? as usize;
79 let name_length = field16(28)? as usize;
80 let extra_length = field16(30)? as usize;
81 let comment_length = field16(32)? as usize;
82 let local_offset = field32(42)? as usize;
83
84 let name_at = at + DIRECTORY_ENTRY_LENGTH;
85 let raw_name = zip
86 .get(name_at..name_at + name_length)
87 .ok_or_else(super::ZipError::truncated)?;
88 let name = super::entry_name::decode_name(raw_name, flags);
89 let is_dir = name.ends_with('/') || name.ends_with('\\');
91 let path = super::entry_name::safe_relative(&name).ok_or_else(|| {
92 super::ZipError::new(format!(
93 "this package contains an entry that would write outside the \
94 game folder ({name}); it will not be installed"
95 ))
96 })?;
97
98 let entry = Entry {
99 path,
100 is_dir,
101 method,
102 crc32,
103 compressed,
104 uncompressed,
105 local_offset,
106 };
107 Ok((entry, name_at + name_length + extra_length + comment_length))
108}
109
110fn find_end_of_directory(zip: &[u8]) -> Result<usize, super::ZipError> {
113 if zip.len() < END_OF_DIRECTORY_LENGTH {
114 return Err(super::ZipError::new("this package is not a zip archive"));
115 }
116 let latest = zip.len() - END_OF_DIRECTORY_LENGTH;
117 let earliest = latest.saturating_sub(u16::MAX as usize);
118 (earliest..=latest)
119 .rev()
120 .find(|&at| u32_at(zip, at) == Some(SIGNATURE_END_OF_DIRECTORY))
121 .ok_or_else(|| super::ZipError::new("this package is not a zip archive, or it is damaged"))
122}
123
124pub fn u16_at(bytes: &[u8], at: usize) -> Option<u16> {
125 Some(u16::from_le_bytes(bytes.get(at..at + 2)?.try_into().ok()?))
126}
127
128pub fn u32_at(bytes: &[u8], at: usize) -> Option<u32> {
129 Some(u32::from_le_bytes(bytes.get(at..at + 4)?.try_into().ok()?))
130}