Skip to main content

pacenotes_installer\zip/
entry_name.rs

1/// An entry name as a relative `/`-separated path that cannot leave the target,
2/// or `None` for a name that tries: any `..` segment, a leading separator, a
3/// `:` (a drive or an alternate data stream), a NUL, or nothing at all. Names
4/// Windows would quietly rewrite go too: a trailing dot or space, and a
5/// reserved device name such as `aux.ini`.
6///
7/// `\` is a separator too: Windows archivers write it, and treating it as a
8/// character would both mis-name files and let `..\..\x` through.
9pub fn safe_relative(name: &str) -> Option<String> {
10    if name.starts_with('/') || name.starts_with('\\') {
11        return None;
12    }
13    let mut segments: Vec<&str> = Vec::new();
14    for segment in name.split(['/', '\\']) {
15        match segment {
16            "" | "." => continue,
17            ".." => return None,
18            _ if segment.contains(':') || segment.contains('\0') => return None,
19            _ if segment.ends_with('.') || segment.ends_with(' ') => return None,
20            _ if is_reserved_device(segment) => return None,
21            _ => segments.push(segment),
22        }
23    }
24    if segments.is_empty() {
25        return None;
26    }
27    Some(segments.join("/"))
28}
29
30/// Decided on the stem, since Windows opens `NUL.txt` as the device just as it does `NUL`.
31fn is_reserved_device(segment: &str) -> bool {
32    const RESERVED: [&str; 22] = [
33        "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
34        "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
35    ];
36    let stem = segment.split('.').next().unwrap_or(segment);
37    RESERVED
38        .iter()
39        .any(|reserved| stem.eq_ignore_ascii_case(reserved))
40}
41
42/// UTF-8 when general-purpose flag bit 11 says so, else CP437 as the spec and
43/// .NET have it, so a file is written under the name the game expects.
44pub fn decode_name(raw: &[u8], flags: u16) -> String {
45    const UTF8_FLAG: u16 = 1 << 11;
46    if flags & UTF8_FLAG != 0 {
47        if let Ok(text) = std::str::from_utf8(raw) {
48            return text.to_string();
49        }
50    }
51    raw.iter()
52        .map(|&byte| {
53            if byte < 0x80 {
54                byte as char
55            } else {
56                CP437_HIGH[(byte - 0x80) as usize]
57            }
58        })
59        .collect()
60}
61
62/// CP437 code points 0x80-0xFF; the low half is ASCII.
63#[rustfmt::skip]
64const CP437_HIGH: [char; 128] = [
65    'Ç','ü','é','â','ä','à','å','ç','ê','ë','è','ï','î','ì','Ä','Å',
66    'É','æ','Æ','ô','ö','ò','û','ù','ÿ','Ö','Ü','¢','£','¥','₧','ƒ',
67    'á','í','ó','ú','ñ','Ñ','ª','º','¿','⌐','¬','½','¼','¡','«','»',
68    '░','▒','▓','│','┤','╡','╢','╖','╕','╣','║','╗','╝','╜','╛','┐',
69    '└','┴','┬','├','─','┼','╞','╟','╚','╔','╩','╦','╠','═','╬','╧',
70    '╨','╤','╥','╙','╘','╒','╓','╫','╪','┘','┌','█','▄','▌','▐','▀',
71    'α','ß','Γ','π','Σ','σ','µ','τ','Φ','Θ','Ω','δ','∞','φ','ε','∩',
72    '≡','±','≥','≤','⌠','⌡','÷','≈','°','∙','·','√','ⁿ','²','■',' ',
73];