Skip to main content

geop_ops_assembly/urdf/
zip.rs

1//! [`zip`]: files packed into one ZIP archive, stored as they are.
2//!
3//! A robot is a URDF file and its meshes; a browser can offer one file to
4//! save, so they go into one archive. Every unzip tool reads a stored
5//! (uncompressed) entry, and that is all this writes: a local header and
6//! the bytes per file, then the central directory — no compression, no
7//! ZIP64 (a robot's meshes are far below 4 GB), and every entry dated
8//! 1980-01-01, so the same robot packs into the same bytes.
9
10use geop_core_math::geop_error::{GeopError, GeopResult};
11
12/// The CRC-32 (IEEE 802.3, reflected) of `bytes`, as ZIP checks entries by.
13fn crc32(bytes: &[u8]) -> u32 {
14    let mut crc = !0u32;
15    for &byte in bytes {
16        crc ^= u32::from(byte);
17        for _ in 0..8 {
18            let mask = (crc & 1).wrapping_neg();
19            crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
20        }
21    }
22    !crc
23}
24
25/// `files` — each a path inside the archive, `/`-separated, and its bytes
26/// — as a ZIP archive. Fails for a file or an archive too large for a
27/// plain (non-ZIP64) archive.
28pub fn zip(files: &[(String, Vec<u8>)]) -> GeopResult<Vec<u8>> {
29    let small = |n: usize, what: &str| {
30        u32::try_from(n)
31            .map_err(|_| GeopError::new(format!("{what} is too large for a ZIP archive")))
32    };
33    let mut out = Vec::new();
34    let mut central = Vec::new();
35    for (path, bytes) in files {
36        let offset = small(out.len(), "the archive")?;
37        let size = small(bytes.len(), path)?;
38        let name_len = u16::try_from(path.len()).map_err(|_| {
39            GeopError::new(format!("the path {path:?} is too long for a ZIP archive"))
40        })?;
41        let crc = crc32(bytes);
42        // What the local header and the central directory's entry share:
43        // version needed (1.0), flags (UTF-8 names), method (stored), time
44        // and date (1980-01-01 00:00), CRC, sizes, name length.
45        let mut common = Vec::new();
46        common.extend(10u16.to_le_bytes());
47        common.extend((1u16 << 11).to_le_bytes());
48        common.extend(0u16.to_le_bytes());
49        common.extend(0u16.to_le_bytes());
50        common.extend(((1u16 << 5) | 1).to_le_bytes());
51        common.extend(crc.to_le_bytes());
52        common.extend(size.to_le_bytes());
53        common.extend(size.to_le_bytes());
54        common.extend(name_len.to_le_bytes());
55
56        out.extend(0x0403_4b50u32.to_le_bytes());
57        out.extend(&common);
58        out.extend(0u16.to_le_bytes()); // extra field length
59        out.extend(path.as_bytes());
60        out.extend(bytes);
61
62        central.extend(0x0201_4b50u32.to_le_bytes());
63        central.extend(20u16.to_le_bytes()); // made by: 2.0, MS-DOS
64        central.extend(&common);
65        central.extend([0u8; 12]); // extra, comment, disk, attributes
66        central.extend(offset.to_le_bytes());
67        central.extend(path.as_bytes());
68    }
69    let count = u16::try_from(files.len())
70        .map_err(|_| GeopError::new("too many files for a ZIP archive"))?;
71    let start = small(out.len(), "the archive")?;
72    let length = small(central.len(), "the archive's directory")?;
73    out.extend(central);
74    out.extend(0x0605_4b50u32.to_le_bytes());
75    out.extend([0u8; 4]); // this disk, the directory's disk
76    out.extend(count.to_le_bytes());
77    out.extend(count.to_le_bytes());
78    out.extend(length.to_le_bytes());
79    out.extend(start.to_le_bytes());
80    out.extend(0u16.to_le_bytes()); // comment length
81    Ok(out)
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    fn u16_at(bytes: &[u8], at: usize) -> usize {
89        u16::from_le_bytes([bytes[at], bytes[at + 1]]) as usize
90    }
91
92    fn u32_at(bytes: &[u8], at: usize) -> usize {
93        u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) as usize
94    }
95
96    /// The check value every CRC-32 implementation is tested against.
97    #[test]
98    fn crc_of_the_check_string() {
99        assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
100        assert_eq!(crc32(b""), 0);
101    }
102
103    /// Read back the way an unzip tool reads: from the end record to the
104    /// central directory, from each entry to its local header and bytes.
105    #[test]
106    fn files_are_found_through_the_central_directory() {
107        let files = vec![
108            ("robot.urdf".to_string(), b"<robot/>".to_vec()),
109            ("meshes/link.stl".to_string(), vec![0, 1, 2, 255]),
110        ];
111        let archive = zip(&files).unwrap();
112        let end = archive.len() - 22;
113        assert_eq!(u32_at(&archive, end), 0x0605_4b50);
114        assert_eq!(u16_at(&archive, end + 10), 2);
115        let mut entry = u32_at(&archive, end + 16);
116        for (path, bytes) in &files {
117            assert_eq!(u32_at(&archive, entry), 0x0201_4b50);
118            assert_eq!(u32_at(&archive, entry + 16), crc32(bytes) as usize);
119            let name_len = u16_at(&archive, entry + 28);
120            assert_eq!(&archive[entry + 46..entry + 46 + name_len], path.as_bytes());
121            let local = u32_at(&archive, entry + 42);
122            assert_eq!(u32_at(&archive, local), 0x0403_4b50);
123            let size = u32_at(&archive, local + 22);
124            let data = local + 30 + u16_at(&archive, local + 26) + u16_at(&archive, local + 28);
125            assert_eq!(&archive[data..data + size], bytes.as_slice());
126            entry += 46 + name_len;
127        }
128        assert_eq!(entry, end);
129    }
130}