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