cxterminfo/
terminfo.rs

1//  Copyleft (ↄ) 2021 BxNiom <bxniom@protonmail.com> | https://github.com/bxniom
2//
3//  This work is free. You can redistribute it and/or modify it under the
4//  terms of the Do What The Fuck You Want To Public License, Version 2,
5//  as published by Sam Hocevar. See the COPYING file for more details.
6
7use std::collections::HashMap;
8use std::fmt::{Debug, Display, Formatter};
9use std::fs;
10use std::fs::File;
11use std::io::Read;
12use std::path::PathBuf;
13
14use crate::capabilities::{BoolCapability, NumberCapability, StringCapability};
15
16/// magic number octal 0432 for legacy ncurses terminfo
17const MAGIC_LEGACY: i16 = 0x11A;
18/// magic number octal 01036 for new ncruses terminfo
19const MAGIC_32BIT: i16 = 0x21E;
20/// the offset into data where the names section begins.
21const NAMES_OFFSET: usize = 12;
22
23const EXT_HEADER_SIZE: usize = 10;
24const TERMINFO_HEADER_SIZE: usize = 12;
25const TERMINFO_MAX_SIZE: usize = 4096;
26
27/// Terminfo database information
28#[derive(Debug)]
29pub struct TermInfo {
30    data: Vec<u8>,
31    read_i32: bool,
32    int_size: usize,
33    sec_name_size: usize,
34    sec_bool_size: usize,
35    sec_number_size: usize,
36    sec_str_offsets_size: usize,
37    sec_str_table_size: usize,
38    ext_bool: HashMap<String, bool>,
39    ext_numbers: HashMap<String, i32>,
40    ext_strings: HashMap<String, String>,
41}
42
43#[derive(Debug)]
44pub enum TermInfoError {
45    InvalidDataSize,
46    InvalidMagicNum,
47    InvalidData,
48    InvalidName,
49}
50
51impl Display for TermInfoError {
52    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53        write!(f, "{}",
54               match self {
55                   TermInfoError::InvalidDataSize => "file/data length is above 4096 bytes or under 12 bytes",
56                   TermInfoError::InvalidMagicNum => "magic number mismatch",
57                   TermInfoError::InvalidData => "terminfo data is invalid or corrupt",
58                   TermInfoError::InvalidName => "terminfo not found"
59               })
60    }
61}
62
63impl TermInfo {
64    /// Returns the string value for the capability or Option::None
65    ///
66    /// # Arguments
67    /// * `cap` - string capability
68    ///
69    /// # Example
70    /// ```
71    /// use cxterminfo::terminfo;
72    /// use cxterminfo::capabilities::StringCapability;
73    ///
74    /// if let Ok(info) = terminfo::from_env() {
75    ///     println!("{:?}", info.get_string(StringCapability::Bell));
76    /// }
77    /// ```
78    pub fn get_string(&self, cap: StringCapability) -> Option<String> {
79        let idx = cap as usize;
80        if idx >= self.sec_str_offsets_size {
81            None
82        } else {
83            let tbl_idx = read_i16(&self.data, self.offset_str_offsets() + (idx * 2)) as usize;
84            if tbl_idx == 0 {
85                None
86            } else {
87                Some(read_str(&self.data, self.offset_str_table() + tbl_idx).0.to_string())
88            }
89        }
90    }
91
92    /// Returns the number value for the capability or Option::None
93    ///
94    /// # Arguments
95    /// * `cap` - number capability
96    ///
97    /// # Example
98    /// ```
99    /// use cxterminfo::terminfo;
100    /// use cxterminfo::capabilities::NumberCapability;
101    ///
102    /// if let Ok(info) = terminfo::from_env() {
103    ///     println!("{:?}", info.get_number(NumberCapability::MaxColors));
104    /// }
105    /// ```
106    pub fn get_number(&self, cap: NumberCapability) -> Option<i32> {
107        let idx = cap as usize;
108        if idx >= self.sec_number_size {
109            None
110        } else {
111            Some(read_int(&self.data, self.offset_number() + (idx * self.int_size), self.read_i32))
112        }
113    }
114
115    /// Returns the bool value for the capability or Option::None
116    ///
117    /// # Arguments
118    /// * `cap` - bool capability
119    ///
120    /// # Example
121    /// ```
122    /// use cxterminfo::terminfo;
123    /// use cxterminfo::capabilities::BoolCapability;
124    ///
125    /// if let Ok(info) = terminfo::from_env() {
126    ///     println!("{:?}", info.get_bool(BoolCapability::AutoLeftMargin));
127    /// }
128    /// ```
129    pub fn get_bool(&self, cap: BoolCapability) -> Option<bool> {
130        let idx = cap as usize;
131        if idx >= self.sec_bool_size {
132            None
133        } else {
134            Some(self.data[(self.offset_bool() + idx)] == 1)
135        }
136    }
137
138    /// Returns the extended bool value for the given name or Option::None if name not exist
139    ///
140    /// # Arguments
141    /// * `name` - key
142    ///
143    /// # Example
144    /// ```
145    /// use cxterminfo::terminfo;
146    ///
147    /// if let Ok(info) = terminfo::from_env() {
148    ///     println!("{:?}", info.get_ext_bool("AT"));
149    /// }
150    /// ```
151    pub fn get_ext_bool(&self, name: &str) -> Option<&bool> {
152        self.ext_bool.get(name)
153    }
154
155    /// Returns the extended number value for the given name or Option::None if name not exist
156    ///
157    /// # Arguments
158    /// * `name` - key
159    ///
160    /// # Example
161    /// ```
162    /// use cxterminfo::terminfo;
163    ///
164    /// if let Ok(info) = terminfo::from_env() {
165    ///     println!("{:?}", info.get_ext_number("?"));
166    /// }
167    /// ```
168    pub fn get_ext_number(&self, name: &str) -> Option<&i32> {
169        self.ext_numbers.get(name)
170    }
171
172    /// Returns the extended string value for the given name or Option::None if name not exist
173    ///
174    /// # Arguments
175    /// * `name` - key
176    ///
177    /// # Example
178    /// ```
179    /// use cxterminfo::terminfo;
180    ///
181    /// if let Ok(info) = terminfo::from_env() {
182    ///     println!("{:?}", info.get_ext_number("xm"));
183    /// }
184    /// ```
185    pub fn get_ext_string(&self, name: &str) -> Option<&String> {
186        self.ext_strings.get(name)
187    }
188
189    /// Create terminfo database, using TERM environment var.
190    pub fn from_env() -> Result<Self, TermInfoError> {
191        if let Ok(term) = std::env::var("TERM") {
192            TermInfo::from_name(term.as_str())
193        } else {
194            Err(TermInfoError::InvalidName)
195        }
196    }
197
198    /// Create terminfo database for the given name
199    pub fn from_name(name: &str) -> Result<Self, TermInfoError> {
200        if name.len() == 0 {
201            return Err(TermInfoError::InvalidName);
202        }
203
204        let first_letter = name.chars().nth(0).unwrap_or('X');
205
206        let mut paths: Vec<PathBuf> = Vec::new();
207        // env TERMINFO
208        if let Ok(env_terminfo) = std::env::var("TERMINFO") {
209            paths.push(PathBuf::from(format!("{}/{}/{}", env_terminfo, first_letter, name)));
210        }
211
212        // HOME .terminfo
213        if let Ok(env_home) = std::env::var("HOME") {
214            paths.push(PathBuf::from(format!("{}/{}/{}", env_home, first_letter, name)));
215        }
216
217        // Linux
218        paths.push(PathBuf::from(format!("/etc/terminfo/{}/{}", first_letter, name)));
219        paths.push(PathBuf::from(format!("/lib/terminfo/{}/{}", first_letter, name)));
220        paths.push(PathBuf::from(format!("/usr/share/terminfo/{}/{}", first_letter, name)));
221        paths.push(PathBuf::from(format!("/usr/share/misc/terminfo/{}/{}", first_letter, name)));
222
223        // Mac
224        paths.push(PathBuf::from(format!("/etc/terminfo/{:X}/{}", first_letter as u8, name)));
225        paths.push(PathBuf::from(format!("/lib/terminfo/{:X}/{}", first_letter as u8, name)));
226        paths.push(PathBuf::from(format!("/usr/share/terminfo/{:X}/{}", first_letter as u8, name)));
227        paths.push(PathBuf::from(format!("/usr/share/misc/terminfo/{:X}/{}", first_letter as u8, name)));
228
229        for path in paths {
230            if path.exists() {
231                return TermInfo::from_file(path.to_str().unwrap())
232            }
233        }
234
235        Err(TermInfoError::InvalidName)
236    }
237
238    /// Create terminfo database using given filename
239    pub fn from_file(filename: &str) -> Result<Self, TermInfoError> {
240        TermInfo::from_data(read_all_bytes_from_file(filename))
241    }
242
243    /// Create terminfo database by parse byte-array directly
244    pub fn from_data(data: Vec<u8>) -> Result<TermInfo, TermInfoError> {
245        if data.len() < TERMINFO_HEADER_SIZE || data.len() > TERMINFO_MAX_SIZE {
246            return Err(TermInfoError::InvalidDataSize);
247        }
248
249        let mut info = TermInfo {
250            data,
251            read_i32: false,
252            int_size: 2,
253            sec_name_size: 0,
254            sec_bool_size: 0,
255            sec_number_size: 0,
256            sec_str_offsets_size: 0,
257            sec_str_table_size: 0,
258            ext_bool: HashMap::new(),
259            ext_numbers: HashMap::new(),
260            ext_strings: HashMap::new(),
261        };
262
263        // read the magic number.
264        let magic = read_i16(&info.data, 0);
265
266        info.read_i32 = match magic {
267            MAGIC_LEGACY => false,
268            MAGIC_32BIT => true,
269            _ => return Err(TermInfoError::InvalidMagicNum),
270        };
271
272        info.int_size = match info.read_i32 {
273            true => 4,
274            false => 2,
275        };
276
277        if read_i16(&info.data, 2) < 0
278            || read_i16(&info.data, 4) < 0
279            || read_i16(&info.data, 6) < 0
280            || read_i16(&info.data, 8) < 0
281            || read_i16(&info.data, 10) < 0
282        {
283            return Err(TermInfoError::InvalidData)
284        }
285
286        info.sec_name_size = read_i16(&info.data, 2) as usize;
287        info.sec_bool_size = read_i16(&info.data, 4) as usize;
288        info.sec_number_size = read_i16(&info.data, 6) as usize;
289        info.sec_str_offsets_size = read_i16(&info.data, 8) as usize;
290        info.sec_str_table_size = read_i16(&info.data, 10) as usize;
291
292
293        // In addition to the main section of bools, numbers, and strings, there is also
294        // an "extended" section.  This section contains additional entries that don't
295        // have well-known indices, and are instead named mappings.  As such, we parse
296        // all of this data now rather than on each request, as the mapping is fairly complicated.
297        // This function relies on the data stored above, so it's the last thing we run.
298        let mut ext_offset = round_up_even(info.offset_str_table() + info.sec_str_table_size);
299
300        // Check if there is an extended section
301        if ext_offset + EXT_HEADER_SIZE < info.data.len() {
302            if read_i16(&info.data, ext_offset) < 0
303                || read_i16(&info.data, ext_offset + 2) < 0
304                || read_i16(&info.data, ext_offset + 4) < 0
305            {
306                // The extended contained invalid data
307                return Ok(info);
308            }
309
310            let ext_bool_count = read_i16(&info.data, ext_offset) as usize;
311            let ext_number_count = read_i16(&info.data, ext_offset + 2) as usize;
312            let ext_str_count = read_i16(&info.data, ext_offset + 4) as usize;
313
314            // Read extended bool values
315            let mut bool_values = Vec::with_capacity(ext_bool_count);
316
317            ext_offset += EXT_HEADER_SIZE;
318            for i in 0..ext_bool_count {
319                let pos = ext_offset + read_i16(&info.data, ext_offset + i * 2) as usize;
320
321                if pos == 0 || ext_offset > info.data.len() {
322                    return Ok(info);
323                }
324
325                bool_values.push(info.data[pos] == 1);
326            }
327
328            // Read extended number values
329            let mut number_values = Vec::with_capacity(ext_number_count);
330
331            ext_offset += if ext_bool_count == 0 { 0 } else { (ext_bool_count - 1) * 2 };
332            for i in 0..ext_number_count {
333                let pos = ext_offset + read_i16(&info.data, ext_offset + i * 2) as usize;
334
335                if pos == 0 || ext_offset > info.data.len() {
336                    return Ok(info);
337                }
338
339                &number_values.push(read_int(&info.data, pos, info.read_i32));
340            }
341
342            // Now we need to parse all of the extended string values.  These aren't necessarily
343            // "in order", meaning the offsets aren't guaranteed to be increasing.  Instead, we parse
344            // the offsets in order, pulling out each string it references and storing them into our
345            // value vector in the order of the offsets.
346            let mut str_values = Vec::with_capacity(ext_str_count);
347
348            ext_offset += if ext_number_count == 0 { 0 } else { (ext_number_count - 1) * 2 };
349
350            let tbl_offset = ext_offset
351                + ext_str_count * 2
352                + (ext_bool_count + ext_number_count + ext_str_count) * 2;
353            let mut last_end: usize = 0;
354            for i in 0..ext_str_count {
355                let pos = tbl_offset + read_i16(&info.data, ext_offset + i * 2) as usize;
356
357                if pos == 0 || ext_offset > info.data.len() {
358                    return Ok(info);
359                }
360
361                let (str, null_term_pos) = read_str(&info.data, pos);
362                &str_values.push(str);
363                last_end = last_end.max(null_term_pos)
364            }
365
366            // Read extended names
367            // The names are in order for the bools, then the numbers, and then the strings.
368            let mut names = Vec::with_capacity(ext_bool_count + ext_number_count + ext_str_count);
369            let mut pos = last_end + 1;
370
371            while pos < info.data.len() {
372                let (str, null_term_pos) = read_str(&info.data, pos);
373                &names.push(str);
374                pos = null_term_pos + 1;
375            }
376
377            // Associate names with the bool values
378            for i in 0..ext_bool_count {
379                &info.ext_bool.insert(names[i].to_string(), bool_values[i]);
380            }
381
382            // Associate names with the number values
383            for i in 0..ext_number_count {
384                &info.ext_numbers
385                     .insert(names[i + ext_bool_count - 1].to_string(), number_values[i]);
386            }
387
388            // Associate names with the string values
389            for i in 0..ext_str_count {
390                &info.ext_strings.insert(
391                    names[i + ext_bool_count + ext_number_count].to_string(),
392                    str_values[i].to_string(),
393                );
394            }
395        }
396
397        Ok(info)
398    }
399
400    /// The offset into data where the bools section begins
401    fn offset_bool(&self) -> usize {
402        NAMES_OFFSET + self.sec_name_size
403    }
404    /// The offset into data where the numbers section begins
405    fn offset_number(&self) -> usize {
406        round_up_even(self.offset_bool() + self.sec_bool_size)
407    }
408    /// The offset into data where the string offsets section begins.  We index into this section
409    /// to find the location within the strings table where a string value exists.
410    fn offset_str_offsets(&self) -> usize {
411        self.offset_number() + (self.sec_number_size * self.int_size)
412    }
413    /// The offset into data where the string table exists
414    fn offset_str_table(&self) -> usize {
415        self.offset_str_offsets() + (self.sec_str_offsets_size * 2)
416    }
417}
418
419/// Read i16 or i32
420///
421/// # Arguments
422/// * `data`        -
423/// * `pos`         - start position in data
424/// * `as_32bit`    - true => read_i32, false => read_i16
425///
426///
427/// # Warning
428/// NOT SAFE
429fn read_int(data: &Vec<u8>, pos: usize, as_32bit: bool) -> i32 {
430    match as_32bit {
431        true => read_i32(data, pos),
432        false => read_i16(data, pos) as i32,
433    }
434}
435
436/// Read i32 from data
437///
438/// # Warning
439/// NOT SAFE
440fn read_i32(data: &Vec<u8>, pos: usize) -> i32 {
441    ((data[pos] as i32) << 24)
442        | ((data[pos + 1] as i32) << 16)
443        | ((data[pos + 2] as i32) << 8)
444        | (data[pos + 3] as i32)
445}
446
447/// Read i16 from data
448///
449/// # Warning
450/// NOT SAFE
451fn read_i16(data: &Vec<u8>, pos: usize) -> i16 {
452    ((data[pos + 1] as i16) << 8) | (data[pos] as i16)
453}
454
455/// Read all data from binary file to a vec<u8>
456///
457/// # Warning
458/// NOT SAFE
459fn read_all_bytes_from_file(filename: &str) -> Vec<u8> {
460    let mut f = File::open(&filename).expect("no file found");
461    let metadata = fs::metadata(&filename).expect("unable to read metadata");
462    let mut buffer = vec![0; metadata.len() as usize];
463    f.read(&mut buffer).expect("buffer overflow");
464
465    buffer
466}
467
468/// Read string from data
469///
470/// # Warning
471/// NOT SAFE
472fn read_str(data: &Vec<u8>, pos: usize) -> (String, usize) {
473    let null_term = find_null_term(data, pos);
474    (data[pos..null_term].iter()
475                         .map(|c| *c as char)
476                         .collect::<String>(),
477     null_term)
478}
479
480/// Find the next '\0' char in data
481fn find_null_term(data: &Vec<u8>, pos: usize) -> usize {
482    let mut term_pos = pos as i32;
483    while term_pos < data.len() as i32 && data[term_pos as usize] != '\0' as u8 {
484        term_pos += 1;
485    }
486    term_pos as usize
487}
488
489/// Simple int rounding to get even numbers
490fn round_up_even(n: usize) -> usize {
491    match n % 2 {
492        1 => n + 1,
493        _ => n,
494    }
495}