term/terminfo/parser/
compiled.rs

1// Copyright 2013 The Rust Project Developers. See the COPYRIGHT
2// file at the top-level directory of this distribution and at
3// http://rust-lang.org/COPYRIGHT.
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! ncurses-compatible compiled terminfo format parsing (term(5))
12
13use std::collections::HashMap;
14use std::io::prelude::*;
15use std::io;
16
17use byteorder::{LittleEndian, ReadBytesExt};
18
19use terminfo::Error::*;
20use terminfo::TermInfo;
21use Result;
22
23pub use terminfo::parser::names::*;
24
25// These are the orders ncurses uses in its compiled format (as of 5.9). Not
26// sure if portable.
27
28fn read_le_u16(r: &mut io::Read) -> io::Result<u32> {
29    return r.read_u16::<LittleEndian>().map(|i| i as u32);
30}
31
32fn read_le_u32(r: &mut io::Read) -> io::Result<u32> {
33    return r.read_u32::<LittleEndian>();
34}
35
36fn read_byte(r: &mut io::Read) -> io::Result<u8> {
37    match r.bytes().next() {
38        Some(s) => s,
39        None => Err(io::Error::new(io::ErrorKind::Other, "end of file")),
40    }
41}
42
43/// Parse a compiled terminfo entry, using long capability names if `longnames`
44/// is true
45pub fn parse(file: &mut io::Read, longnames: bool) -> Result<TermInfo> {
46    let (bnames, snames, nnames) = if longnames {
47        (boolfnames, stringfnames, numfnames)
48    } else {
49        (boolnames, stringnames, numnames)
50    };
51
52    // Check magic number
53    let magic = file.read_u16::<LittleEndian>()?;
54
55    let read_number = match magic {
56        0x011A => read_le_u16,
57        0x021e => read_le_u32,
58        _ => return Err(BadMagic(magic).into()),
59    };
60
61    // According to the spec, these fields must be >= -1 where -1 means that the
62    // feature is not
63    // supported. Using 0 instead of -1 works because we skip sections with length
64    // 0.
65    macro_rules! read_nonneg {
66        () => {{
67            match read_le_u16(file)? as i16 {
68                n if n >= 0 => n as usize,
69                -1 => 0,
70                _ => return Err(InvalidLength.into()),
71            }
72        }}
73    }
74
75    let names_bytes = read_nonneg!();
76    let bools_bytes = read_nonneg!();
77    let numbers_count = read_nonneg!();
78    let string_offsets_count = read_nonneg!();
79    let string_table_bytes = read_nonneg!();
80
81    if names_bytes == 0 {
82        return Err(ShortNames.into());
83    }
84
85    if bools_bytes > boolnames.len() {
86        return Err(TooManyBools.into());
87    }
88
89    if numbers_count > numnames.len() {
90        return Err(TooManyNumbers.into());
91    }
92
93    if string_offsets_count > stringnames.len() {
94        return Err(TooManyStrings.into());
95    }
96
97    // don't read NUL
98    let mut bytes = Vec::new();
99    file.take((names_bytes - 1) as u64).read_to_end(&mut bytes)?;
100    let names_str = match String::from_utf8(bytes) {
101        Ok(s) => s,
102        Err(e) => return Err(NotUtf8(e.utf8_error()).into()),
103    };
104
105    let term_names: Vec<String> = names_str.split('|').map(|s| s.to_owned()).collect();
106    // consume NUL
107    if read_byte(file)? != b'\0' {
108        return Err(NamesMissingNull.into());
109    }
110
111    let bools_map = (0..bools_bytes)
112        .filter_map(|i| match read_byte(file) {
113            Err(e) => Some(Err(e)),
114            Ok(1) => Some(Ok((bnames[i], true))),
115            Ok(_) => None,
116        })
117        .collect::<io::Result<HashMap<_, _>>>()?;
118
119    if (bools_bytes + names_bytes) % 2 == 1 {
120        read_byte(file)?; // compensate for padding
121    }
122
123    let numbers_map = (0..numbers_count)
124        .filter_map(|i| match read_number(file) {
125            Ok(0xFFFF) => None,
126            Ok(n) => Some(Ok((nnames[i], n))),
127            Err(e) => Some(Err(e)),
128        })
129        .collect::<io::Result<HashMap<_, _>>>()?;
130
131    let string_map: HashMap<&str, Vec<u8>> = if string_offsets_count > 0 {
132        let string_offsets = (0..string_offsets_count)
133            .map(|_| file.read_u16::<LittleEndian>())
134            .collect::<io::Result<Vec<_>>>()?;
135
136        let mut string_table = Vec::new();
137        file.take(string_table_bytes as u64)
138            .read_to_end(&mut string_table)?;
139
140        string_offsets
141            .into_iter()
142            .enumerate()
143            .filter(|&(_, offset)| {
144                // non-entry
145                offset != 0xFFFF
146            })
147            .map(|(i, offset)| {
148                let offset = offset as usize;
149
150                let name = if snames[i] == "_" {
151                    stringfnames[i]
152                } else {
153                    snames[i]
154                };
155
156                if offset == 0xFFFE {
157                    // undocumented: FFFE indicates cap@, which means the capability
158                    // is not present
159                    // unsure if the handling for this is correct
160                    return Ok((name, Vec::new()));
161                }
162
163                // Find the offset of the NUL we want to go to
164                let nulpos = string_table[offset..string_table_bytes]
165                    .iter()
166                    .position(|&b| b == 0);
167                match nulpos {
168                    Some(len) => Ok((name, string_table[offset..offset + len].to_vec())),
169                    None => return Err(::Error::TerminfoParsing(StringsMissingNull)),
170                }
171            })
172            .collect::<Result<HashMap<_, _>>>()?
173    } else {
174        HashMap::new()
175    };
176
177    // And that's all there is to it
178    Ok(TermInfo {
179        names: term_names,
180        bools: bools_map,
181        numbers: numbers_map,
182        strings: string_map,
183    })
184}
185
186#[cfg(test)]
187mod test {
188
189    use super::{boolfnames, boolnames, numfnames, numnames, stringfnames, stringnames};
190
191    #[test]
192    fn test_veclens() {
193        assert_eq!(boolfnames.len(), boolnames.len());
194        assert_eq!(numfnames.len(), numnames.len());
195        assert_eq!(stringfnames.len(), stringnames.len());
196    }
197}