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