Skip to main content

term/terminfo/parser/
compiled.rs

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