1use crate::fsutil;
2use crate::{LovelyError, Result};
3use std::fs::{self, File};
4use std::io::{Seek, Write};
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct ArchiveEntry {
9 pub name: String,
10 pub bytes: Vec<u8>,
11}
12
13impl ArchiveEntry {
14 pub fn file(name: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Result<Self> {
15 let name = name.into();
16 validate_archive_name(&name)?;
17 Ok(Self {
18 name,
19 bytes: bytes.into(),
20 })
21 }
22}
23
24pub fn create_love_archive(
25 source: &Path,
26 output: &Path,
27 includes: &[String],
28 excludes: &[String],
29) -> Result<Vec<ArchiveEntry>> {
30 let mut entries = Vec::new();
31 for file in fsutil::collect_included_files(source, includes, excludes)? {
32 let rel = fsutil::relative_path(source, &file)?;
33 let name = fsutil::normalize_slashes(&rel);
34 validate_archive_name(&name)?;
35 let bytes = fs::read(&file).map_err(|err| LovelyError::io(&file, err))?;
36 entries.push(ArchiveEntry { name, bytes });
37 }
38
39 entries.sort_by(|a, b| a.name.cmp(&b.name));
40 write_zip(output, &entries)?;
41 Ok(entries)
42}
43
44pub fn write_zip(output: &Path, entries: &[ArchiveEntry]) -> Result<()> {
45 if let Some(parent) = output.parent() {
46 fsutil::ensure_dir(parent)?;
47 }
48
49 let mut file = File::create(output).map_err(|err| LovelyError::io(output, err))?;
50 let mut central_records = Vec::new();
51
52 for entry in entries {
53 validate_archive_name(&entry.name)?;
54 let offset = file.stream_position().map_err(LovelyError::plain_io)? as u32;
55 let crc = crc32(&entry.bytes);
56 let size = checked_u32(entry.bytes.len(), "file is too large for ZIP32")?;
57 let name = entry.name.as_bytes();
58 let name_len = checked_u16(name.len(), "file name is too long")?;
59
60 write_u32(&mut file, 0x0403_4b50)?;
61 write_u16(&mut file, 20)?;
62 write_u16(&mut file, 0)?;
63 write_u16(&mut file, 0)?;
64 write_u16(&mut file, 0)?;
65 write_u16(&mut file, 33)?;
66 write_u32(&mut file, crc)?;
67 write_u32(&mut file, size)?;
68 write_u32(&mut file, size)?;
69 write_u16(&mut file, name_len)?;
70 write_u16(&mut file, 0)?;
71 file.write_all(name).map_err(LovelyError::plain_io)?;
72 file.write_all(&entry.bytes)
73 .map_err(LovelyError::plain_io)?;
74
75 central_records.push(CentralRecord {
76 name: entry.name.clone(),
77 crc,
78 size,
79 offset,
80 });
81 }
82
83 let central_offset = file.stream_position().map_err(LovelyError::plain_io)? as u32;
84 for record in ¢ral_records {
85 let name = record.name.as_bytes();
86 let name_len = checked_u16(name.len(), "file name is too long")?;
87
88 write_u32(&mut file, 0x0201_4b50)?;
89 write_u16(&mut file, 20)?;
90 write_u16(&mut file, 20)?;
91 write_u16(&mut file, 0)?;
92 write_u16(&mut file, 0)?;
93 write_u16(&mut file, 0)?;
94 write_u16(&mut file, 33)?;
95 write_u32(&mut file, record.crc)?;
96 write_u32(&mut file, record.size)?;
97 write_u32(&mut file, record.size)?;
98 write_u16(&mut file, name_len)?;
99 write_u16(&mut file, 0)?;
100 write_u16(&mut file, 0)?;
101 write_u16(&mut file, 0)?;
102 write_u16(&mut file, 0)?;
103 write_u32(&mut file, 0o100644 << 16)?;
104 write_u32(&mut file, record.offset)?;
105 file.write_all(name).map_err(LovelyError::plain_io)?;
106 }
107
108 let central_size =
109 file.stream_position().map_err(LovelyError::plain_io)? as u32 - central_offset;
110 write_u32(&mut file, 0x0605_4b50)?;
111 write_u16(&mut file, 0)?;
112 write_u16(&mut file, 0)?;
113 write_u16(
114 &mut file,
115 checked_u16(central_records.len(), "too many ZIP entries")?,
116 )?;
117 write_u16(
118 &mut file,
119 checked_u16(central_records.len(), "too many ZIP entries")?,
120 )?;
121 write_u32(&mut file, central_size)?;
122 write_u32(&mut file, central_offset)?;
123 write_u16(&mut file, 0)?;
124
125 Ok(())
126}
127
128pub fn write_tar(output: &Path, entries: &[ArchiveEntry]) -> Result<()> {
129 if let Some(parent) = output.parent() {
130 fsutil::ensure_dir(parent)?;
131 }
132 let mut file = File::create(output).map_err(|err| LovelyError::io(output, err))?;
133 for entry in entries {
134 validate_archive_name(&entry.name)?;
135 write_tar_header(&mut file, entry)?;
136 file.write_all(&entry.bytes)
137 .map_err(LovelyError::plain_io)?;
138 let padding = (512 - (entry.bytes.len() % 512)) % 512;
139 if padding > 0 {
140 file.write_all(&vec![0; padding])
141 .map_err(LovelyError::plain_io)?;
142 }
143 }
144 file.write_all(&[0; 1024]).map_err(LovelyError::plain_io)?;
145 Ok(())
146}
147
148pub fn archive_entries_from_files(
149 root: &Path,
150 files: &[PathBuf],
151 prefix: &str,
152) -> Result<Vec<ArchiveEntry>> {
153 let mut entries = Vec::new();
154 for file in files {
155 let rel = fsutil::relative_path(root, file)?;
156 let name = format!(
157 "{}/{}",
158 prefix.trim_matches('/'),
159 fsutil::normalize_slashes(&rel)
160 );
161 let bytes = fs::read(file).map_err(|err| LovelyError::io(file, err))?;
162 entries.push(ArchiveEntry::file(name, bytes)?);
163 }
164 entries.sort_by(|a, b| a.name.cmp(&b.name));
165 Ok(entries)
166}
167
168fn write_tar_header(mut file: impl Write, entry: &ArchiveEntry) -> Result<()> {
169 let mut header = [0u8; 512];
170 write_octal(&mut header[100..108], 0o644);
171 write_octal(&mut header[108..116], 0);
172 write_octal(&mut header[116..124], 0);
173 write_octal(&mut header[124..136], entry.bytes.len() as u64);
174 write_octal(&mut header[136..148], 0);
175 header[156] = b'0';
176 header[257..263].copy_from_slice(b"ustar\0");
177 header[263..265].copy_from_slice(b"00");
178
179 let name_bytes = entry.name.as_bytes();
180 if name_bytes.len() <= 100 {
181 header[0..name_bytes.len()].copy_from_slice(name_bytes);
182 } else if let Some(split) = entry.name.rfind('/') {
183 let (prefix, name) = entry.name.split_at(split);
184 let name = &name[1..];
185 if prefix.len() > 155 || name.len() > 100 {
186 return Err(LovelyError::Archive(format!(
187 "tar path is too long: {}",
188 entry.name
189 )));
190 }
191 header[0..name.len()].copy_from_slice(name.as_bytes());
192 header[345..345 + prefix.len()].copy_from_slice(prefix.as_bytes());
193 } else {
194 return Err(LovelyError::Archive(format!(
195 "tar path is too long: {}",
196 entry.name
197 )));
198 }
199
200 for byte in &mut header[148..156] {
201 *byte = b' ';
202 }
203 let checksum: u32 = header.iter().map(|byte| *byte as u32).sum();
204 write_checksum(&mut header[148..156], checksum);
205 file.write_all(&header).map_err(LovelyError::plain_io)?;
206 Ok(())
207}
208
209fn write_octal(field: &mut [u8], value: u64) {
210 let text = format!("{:0width$o}\0", value, width = field.len() - 1);
211 field.copy_from_slice(text.as_bytes());
212}
213
214fn write_checksum(field: &mut [u8], value: u32) {
215 let text = format!("{:06o}\0 ", value);
216 field.copy_from_slice(text.as_bytes());
217}
218
219fn validate_archive_name(name: &str) -> Result<()> {
220 if name.is_empty()
221 || name.starts_with('/')
222 || name.contains('\\')
223 || name.split('/').any(|part| part == ".." || part.is_empty())
224 {
225 return Err(LovelyError::Archive(format!(
226 "unsafe archive path: {name:?}"
227 )));
228 }
229 Ok(())
230}
231
232fn checked_u16(value: usize, message: &str) -> Result<u16> {
233 u16::try_from(value).map_err(|_| LovelyError::Archive(message.to_string()))
234}
235
236fn checked_u32(value: usize, message: &str) -> Result<u32> {
237 u32::try_from(value).map_err(|_| LovelyError::Archive(message.to_string()))
238}
239
240fn write_u16(mut writer: impl Write, value: u16) -> Result<()> {
241 writer
242 .write_all(&value.to_le_bytes())
243 .map_err(LovelyError::plain_io)
244}
245
246fn write_u32(mut writer: impl Write, value: u32) -> Result<()> {
247 writer
248 .write_all(&value.to_le_bytes())
249 .map_err(LovelyError::plain_io)
250}
251
252fn crc32(bytes: &[u8]) -> u32 {
253 let mut crc = 0xffff_ffffu32;
254 for byte in bytes {
255 crc ^= *byte as u32;
256 for _ in 0..8 {
257 let mask = (crc & 1).wrapping_neg();
258 crc = (crc >> 1) ^ (0xedb8_8320 & mask);
259 }
260 }
261 !crc
262}
263
264struct CentralRecord {
265 name: String,
266 crc: u32,
267 size: u32,
268 offset: u32,
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn rejects_unsafe_paths() {
277 assert!(ArchiveEntry::file("../x", b"").is_err());
278 assert!(ArchiveEntry::file("/x", b"").is_err());
279 assert!(ArchiveEntry::file("a\\b", b"").is_err());
280 }
281
282 #[test]
283 fn crc_known_value() {
284 assert_eq!(crc32(b"123456789"), 0xcbf4_3926);
285 }
286}