Skip to main content

runlight/
zip.rs

1//! A ZIP file of text files, stored without compression, and CSV rows.
2
3use crate::js::{self, Value};
4use crate::re::{js_re, test};
5
6fn crc32(bytes: &[u8]) -> u32 {
7    static TABLE: std::sync::LazyLock<[u32; 256]> = std::sync::LazyLock::new(|| {
8        let mut t = [0u32; 256];
9        for (n, slot) in t.iter_mut().enumerate() {
10            let mut c = n as u32;
11            for _ in 0..8 {
12                c = if c & 1 != 0 { 0xedb8_8320 ^ (c >> 1) } else { c >> 1 };
13            }
14            *slot = c;
15        }
16        t
17    });
18    let mut crc = 0xffff_ffffu32;
19    for b in bytes {
20        crc = TABLE[((crc ^ u32::from(*b)) & 0xff) as usize] ^ (crc >> 8);
21    }
22    crc ^ 0xffff_ffff
23}
24
25/// DOS date and time, as ZIP stores them, for an instant in UTC.
26fn dos_time(ms: i64) -> (u16, u16) {
27    let days = ms.div_euclid(86_400_000);
28    let rest = ms.rem_euclid(86_400_000) / 1000;
29    let (y, m, d) = js::civil_from_days(days);
30    let time = ((rest / 3600) << 11) | (((rest / 60) % 60) << 5) | ((rest % 60) / 2);
31    let day = ((y - 1980) << 9) | (m << 5) | d;
32    (time as u16, day as u16)
33}
34
35/// A ZIP of text files, stored, dated `now` (epoch milliseconds, UTC).
36pub fn zip(files: &[(String, String)], now: i64) -> Vec<u8> {
37    let (time, day) = dos_time(now);
38    let mut out = Vec::new();
39    let mut central = Vec::new();
40    for (name, text) in files {
41        let name = name.as_bytes();
42        let data = text.as_bytes();
43        let crc = crc32(data);
44        let offset = out.len() as u32;
45        out.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
46        out.extend_from_slice(&20u16.to_le_bytes());
47        out.extend_from_slice(&0x0800u16.to_le_bytes()); // names are UTF-8
48        out.extend_from_slice(&0u16.to_le_bytes()); // stored
49        out.extend_from_slice(&time.to_le_bytes());
50        out.extend_from_slice(&day.to_le_bytes());
51        out.extend_from_slice(&crc.to_le_bytes());
52        out.extend_from_slice(&(data.len() as u32).to_le_bytes());
53        out.extend_from_slice(&(data.len() as u32).to_le_bytes());
54        out.extend_from_slice(&(name.len() as u16).to_le_bytes());
55        out.extend_from_slice(&0u16.to_le_bytes());
56        out.extend_from_slice(name);
57        out.extend_from_slice(data);
58
59        central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
60        central.extend_from_slice(&20u16.to_le_bytes());
61        central.extend_from_slice(&20u16.to_le_bytes());
62        central.extend_from_slice(&0x0800u16.to_le_bytes());
63        central.extend_from_slice(&0u16.to_le_bytes());
64        central.extend_from_slice(&time.to_le_bytes());
65        central.extend_from_slice(&day.to_le_bytes());
66        central.extend_from_slice(&crc.to_le_bytes());
67        central.extend_from_slice(&(data.len() as u32).to_le_bytes());
68        central.extend_from_slice(&(data.len() as u32).to_le_bytes());
69        central.extend_from_slice(&(name.len() as u16).to_le_bytes());
70        central.extend_from_slice(&[0u8; 12]);
71        central.extend_from_slice(&offset.to_le_bytes());
72        central.extend_from_slice(name);
73    }
74    let size = central.len() as u32;
75    let offset = out.len() as u32;
76    out.extend_from_slice(&central);
77    out.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
78    out.extend_from_slice(&[0u8; 4]);
79    out.extend_from_slice(&(files.len() as u16).to_le_bytes());
80    out.extend_from_slice(&(files.len() as u16).to_le_bytes());
81    out.extend_from_slice(&size.to_le_bytes());
82    out.extend_from_slice(&offset.to_le_bytes());
83    out.extend_from_slice(&[0u8; 2]);
84    out
85}
86
87/// One CSV cell's text from a value, as `String(v)` writes it, null as
88/// empty.
89pub fn cell(v: &Value) -> String {
90    if v.is_null() { String::new() } else { js::js_string(v) }
91}
92
93/// One CSV row, quoting what needs it; a leading =, +, -, or @ is escaped so
94/// a spreadsheet will not run it.
95pub fn csv_row(values: &[String]) -> String {
96    values
97        .iter()
98        .map(|v| {
99            let mut s = v.clone();
100            if test(js_re!(r"^[=+\-@\t\r]"), &s) && !test(js_re!(r"^-?\d+(\.\d+)?$"), &s) {
101                s = format!("'{s}");
102            }
103            // Quoted with a semicolon or tab in it too: Excel in a locale that separates lists with ";" splits an
104            // unquoted cell there, and what follows could start with = and run as a formula.
105            if s.contains(['"', ',', ';', '\t', '\n', '\r']) { format!("\"{}\"", s.replace('"', "\"\"")) } else { s }
106        })
107        .collect::<Vec<_>>()
108        .join(",")
109}
110
111/// A CSV file: the header and the rows, each line ended with CRLF.
112pub fn csv(header: &[&str], rows: &[Vec<String>]) -> String {
113    let mut lines = vec![csv_row(&header.iter().map(|s| s.to_string()).collect::<Vec<_>>())];
114    lines.extend(rows.iter().map(|r| csv_row(r)));
115    lines.join("\r\n") + "\r\n"
116}