Skip to main content

lovely/
archive.rs

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 &central_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}