test/term/terminfo/parser/
compiled.rs

1#![allow(non_upper_case_globals, missing_docs)]
2
3//! ncurses-compatible compiled terminfo format parsing (term(5))
4
5use std::collections::HashMap;
6use std::io;
7use std::io::prelude::*;
8
9use super::super::TermInfo;
10
11#[cfg(test)]
12mod tests;
13
14// These are the orders ncurses uses in its compiled format (as of 5.9). Not sure if portable.
15
16#[rustfmt::skip]
17pub(crate) static boolfnames: &[&str] = &["auto_left_margin", "auto_right_margin",
18    "no_esc_ctlc", "ceol_standout_glitch", "eat_newline_glitch", "erase_overstrike", "generic_type",
19    "hard_copy", "has_meta_key", "has_status_line", "insert_null_glitch", "memory_above",
20    "memory_below", "move_insert_mode", "move_standout_mode", "over_strike", "status_line_esc_ok",
21    "dest_tabs_magic_smso", "tilde_glitch", "transparent_underline", "xon_xoff", "needs_xon_xoff",
22    "prtr_silent", "hard_cursor", "non_rev_rmcup", "no_pad_char", "non_dest_scroll_region",
23    "can_change", "back_color_erase", "hue_lightness_saturation", "col_addr_glitch",
24    "cr_cancels_micro_mode", "has_print_wheel", "row_addr_glitch", "semi_auto_right_margin",
25    "cpi_changes_res", "lpi_changes_res", "backspaces_with_bs", "crt_no_scrolling",
26    "no_correctly_working_cr", "gnu_has_meta_key", "linefeed_is_newline", "has_hardware_tabs",
27    "return_does_clr_eol"];
28
29#[rustfmt::skip]
30pub(crate) static boolnames: &[&str] = &["bw", "am", "xsb", "xhp", "xenl", "eo",
31    "gn", "hc", "km", "hs", "in", "db", "da", "mir", "msgr", "os", "eslok", "xt", "hz", "ul", "xon",
32    "nxon", "mc5i", "chts", "nrrmc", "npc", "ndscr", "ccc", "bce", "hls", "xhpa", "crxm", "daisy",
33    "xvpa", "sam", "cpix", "lpix", "OTbs", "OTns", "OTnc", "OTMT", "OTNL", "OTpt", "OTxr"];
34
35#[rustfmt::skip]
36pub(crate) static numfnames: &[&str] = &[ "columns", "init_tabs", "lines",
37    "lines_of_memory", "magic_cookie_glitch", "padding_baud_rate", "virtual_terminal",
38    "width_status_line", "num_labels", "label_height", "label_width", "max_attributes",
39    "maximum_windows", "max_colors", "max_pairs", "no_color_video", "buffer_capacity",
40    "dot_vert_spacing", "dot_horz_spacing", "max_micro_address", "max_micro_jump", "micro_col_size",
41    "micro_line_size", "number_of_pins", "output_res_char", "output_res_line",
42    "output_res_horz_inch", "output_res_vert_inch", "print_rate", "wide_char_size", "buttons",
43    "bit_image_entwining", "bit_image_type", "magic_cookie_glitch_ul", "carriage_return_delay",
44    "new_line_delay", "backspace_delay", "horizontal_tab_delay", "number_of_function_keys"];
45
46#[rustfmt::skip]
47pub(crate) static numnames: &[&str] = &[ "cols", "it", "lines", "lm", "xmc", "pb",
48    "vt", "wsl", "nlab", "lh", "lw", "ma", "wnum", "colors", "pairs", "ncv", "bufsz", "spinv",
49    "spinh", "maddr", "mjump", "mcs", "mls", "npins", "orc", "orl", "orhi", "orvi", "cps", "widcs",
50    "btns", "bitwin", "bitype", "UTug", "OTdC", "OTdN", "OTdB", "OTdT", "OTkn"];
51
52#[rustfmt::skip]
53pub(crate) static stringfnames: &[&str] = &[ "back_tab", "bell", "carriage_return",
54    "change_scroll_region", "clear_all_tabs", "clear_screen", "clr_eol", "clr_eos",
55    "column_address", "command_character", "cursor_address", "cursor_down", "cursor_home",
56    "cursor_invisible", "cursor_left", "cursor_mem_address", "cursor_normal", "cursor_right",
57    "cursor_to_ll", "cursor_up", "cursor_visible", "delete_character", "delete_line",
58    "dis_status_line", "down_half_line", "enter_alt_charset_mode", "enter_blink_mode",
59    "enter_bold_mode", "enter_ca_mode", "enter_delete_mode", "enter_dim_mode", "enter_insert_mode",
60    "enter_secure_mode", "enter_protected_mode", "enter_reverse_mode", "enter_standout_mode",
61    "enter_underline_mode", "erase_chars", "exit_alt_charset_mode", "exit_attribute_mode",
62    "exit_ca_mode", "exit_delete_mode", "exit_insert_mode", "exit_standout_mode",
63    "exit_underline_mode", "flash_screen", "form_feed", "from_status_line", "init_1string",
64    "init_2string", "init_3string", "init_file", "insert_character", "insert_line",
65    "insert_padding", "key_backspace", "key_catab", "key_clear", "key_ctab", "key_dc", "key_dl",
66    "key_down", "key_eic", "key_eol", "key_eos", "key_f0", "key_f1", "key_f10", "key_f2", "key_f3",
67    "key_f4", "key_f5", "key_f6", "key_f7", "key_f8", "key_f9", "key_home", "key_ic", "key_il",
68    "key_left", "key_ll", "key_npage", "key_ppage", "key_right", "key_sf", "key_sr", "key_stab",
69    "key_up", "keypad_local", "keypad_xmit", "lab_f0", "lab_f1", "lab_f10", "lab_f2", "lab_f3",
70    "lab_f4", "lab_f5", "lab_f6", "lab_f7", "lab_f8", "lab_f9", "meta_off", "meta_on", "newline",
71    "pad_char", "parm_dch", "parm_delete_line", "parm_down_cursor", "parm_ich", "parm_index",
72    "parm_insert_line", "parm_left_cursor", "parm_right_cursor", "parm_rindex", "parm_up_cursor",
73    "pkey_key", "pkey_local", "pkey_xmit", "print_screen", "prtr_off", "prtr_on", "repeat_char",
74    "reset_1string", "reset_2string", "reset_3string", "reset_file", "restore_cursor",
75    "row_address", "save_cursor", "scroll_forward", "scroll_reverse", "set_attributes", "set_tab",
76    "set_window", "tab", "to_status_line", "underline_char", "up_half_line", "init_prog", "key_a1",
77    "key_a3", "key_b2", "key_c1", "key_c3", "prtr_non", "char_padding", "acs_chars", "plab_norm",
78    "key_btab", "enter_xon_mode", "exit_xon_mode", "enter_am_mode", "exit_am_mode", "xon_character",
79    "xoff_character", "ena_acs", "label_on", "label_off", "key_beg", "key_cancel", "key_close",
80    "key_command", "key_copy", "key_create", "key_end", "key_enter", "key_exit", "key_find",
81    "key_help", "key_mark", "key_message", "key_move", "key_next", "key_open", "key_options",
82    "key_previous", "key_print", "key_redo", "key_reference", "key_refresh", "key_replace",
83    "key_restart", "key_resume", "key_save", "key_suspend", "key_undo", "key_sbeg", "key_scancel",
84    "key_scommand", "key_scopy", "key_screate", "key_sdc", "key_sdl", "key_select", "key_send",
85    "key_seol", "key_sexit", "key_sfind", "key_shelp", "key_shome", "key_sic", "key_sleft",
86    "key_smessage", "key_smove", "key_snext", "key_soptions", "key_sprevious", "key_sprint",
87    "key_sredo", "key_sreplace", "key_sright", "key_srsume", "key_ssave", "key_ssuspend",
88    "key_sundo", "req_for_input", "key_f11", "key_f12", "key_f13", "key_f14", "key_f15", "key_f16",
89    "key_f17", "key_f18", "key_f19", "key_f20", "key_f21", "key_f22", "key_f23", "key_f24",
90    "key_f25", "key_f26", "key_f27", "key_f28", "key_f29", "key_f30", "key_f31", "key_f32",
91    "key_f33", "key_f34", "key_f35", "key_f36", "key_f37", "key_f38", "key_f39", "key_f40",
92    "key_f41", "key_f42", "key_f43", "key_f44", "key_f45", "key_f46", "key_f47", "key_f48",
93    "key_f49", "key_f50", "key_f51", "key_f52", "key_f53", "key_f54", "key_f55", "key_f56",
94    "key_f57", "key_f58", "key_f59", "key_f60", "key_f61", "key_f62", "key_f63", "clr_bol",
95    "clear_margins", "set_left_margin", "set_right_margin", "label_format", "set_clock",
96    "display_clock", "remove_clock", "create_window", "goto_window", "hangup", "dial_phone",
97    "quick_dial", "tone", "pulse", "flash_hook", "fixed_pause", "wait_tone", "user0", "user1",
98    "user2", "user3", "user4", "user5", "user6", "user7", "user8", "user9", "orig_pair",
99    "orig_colors", "initialize_color", "initialize_pair", "set_color_pair", "set_foreground",
100    "set_background", "change_char_pitch", "change_line_pitch", "change_res_horz",
101    "change_res_vert", "define_char", "enter_doublewide_mode", "enter_draft_quality",
102    "enter_italics_mode", "enter_leftward_mode", "enter_micro_mode", "enter_near_letter_quality",
103    "enter_normal_quality", "enter_shadow_mode", "enter_subscript_mode", "enter_superscript_mode",
104    "enter_upward_mode", "exit_doublewide_mode", "exit_italics_mode", "exit_leftward_mode",
105    "exit_micro_mode", "exit_shadow_mode", "exit_subscript_mode", "exit_superscript_mode",
106    "exit_upward_mode", "micro_column_address", "micro_down", "micro_left", "micro_right",
107    "micro_row_address", "micro_up", "order_of_pins", "parm_down_micro", "parm_left_micro",
108    "parm_right_micro", "parm_up_micro", "select_char_set", "set_bottom_margin",
109    "set_bottom_margin_parm", "set_left_margin_parm", "set_right_margin_parm", "set_top_margin",
110    "set_top_margin_parm", "start_bit_image", "start_char_set_def", "stop_bit_image",
111    "stop_char_set_def", "subscript_characters", "superscript_characters", "these_cause_cr",
112    "zero_motion", "char_set_names", "key_mouse", "mouse_info", "req_mouse_pos", "get_mouse",
113    "set_a_foreground", "set_a_background", "pkey_plab", "device_type", "code_set_init",
114    "set0_des_seq", "set1_des_seq", "set2_des_seq", "set3_des_seq", "set_lr_margin",
115    "set_tb_margin", "bit_image_repeat", "bit_image_newline", "bit_image_carriage_return",
116    "color_names", "define_bit_image_region", "end_bit_image_region", "set_color_band",
117    "set_page_length", "display_pc_char", "enter_pc_charset_mode", "exit_pc_charset_mode",
118    "enter_scancode_mode", "exit_scancode_mode", "pc_term_options", "scancode_escape",
119    "alt_scancode_esc", "enter_horizontal_hl_mode", "enter_left_hl_mode", "enter_low_hl_mode",
120    "enter_right_hl_mode", "enter_top_hl_mode", "enter_vertical_hl_mode", "set_a_attributes",
121    "set_pglen_inch", "termcap_init2", "termcap_reset", "linefeed_if_not_lf", "backspace_if_not_bs",
122    "other_non_function_keys", "arrow_key_map", "acs_ulcorner", "acs_llcorner", "acs_urcorner",
123    "acs_lrcorner", "acs_ltee", "acs_rtee", "acs_btee", "acs_ttee", "acs_hline", "acs_vline",
124    "acs_plus", "memory_lock", "memory_unlock", "box_chars_1"];
125
126#[rustfmt::skip]
127pub(crate) static stringnames: &[&str] = &[ "cbt", "_", "cr", "csr", "tbc", "clear",
128    "_", "_", "hpa", "cmdch", "cup", "cud1", "home", "civis", "cub1", "mrcup", "cnorm", "cuf1",
129    "ll", "cuu1", "cvvis", "dch1", "dl1", "dsl", "hd", "smacs", "blink", "bold", "smcup", "smdc",
130    "dim", "smir", "invis", "prot", "rev", "smso", "smul", "ech", "rmacs", "sgr0", "rmcup", "rmdc",
131    "rmir", "rmso", "rmul", "flash", "ff", "fsl", "is1", "is2", "is3", "if", "ich1", "il1", "ip",
132    "kbs", "ktbc", "kclr", "kctab", "_", "_", "kcud1", "_", "_", "_", "_", "_", "_", "_", "_", "_",
133    "_", "_", "_", "_", "_", "khome", "_", "_", "kcub1", "_", "knp", "kpp", "kcuf1", "_", "_",
134    "khts", "_", "rmkx", "smkx", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "rmm", "_",
135    "_", "pad", "dch", "dl", "cud", "ich", "indn", "il", "cub", "cuf", "rin", "cuu", "pfkey",
136    "pfloc", "pfx", "mc0", "mc4", "_", "rep", "rs1", "rs2", "rs3", "rf", "rc", "vpa", "sc", "ind",
137    "ri", "sgr", "_", "wind", "_", "tsl", "uc", "hu", "iprog", "_", "_", "_", "_", "_", "mc5p",
138    "rmp", "acsc", "pln", "kcbt", "smxon", "rmxon", "smam", "rmam", "xonc", "xoffc", "_", "smln",
139    "rmln", "_", "kcan", "kclo", "kcmd", "kcpy", "kcrt", "_", "kent", "kext", "kfnd", "khlp",
140    "kmrk", "kmsg", "kmov", "knxt", "kopn", "kopt", "kprv", "kprt", "krdo", "kref", "krfr", "krpl",
141    "krst", "kres", "ksav", "kspd", "kund", "kBEG", "kCAN", "kCMD", "kCPY", "kCRT", "_", "_",
142    "kslt", "kEND", "kEOL", "kEXT", "kFND", "kHLP", "kHOM", "_", "kLFT", "kMSG", "kMOV", "kNXT",
143    "kOPT", "kPRV", "kPRT", "kRDO", "kRPL", "kRIT", "kRES", "kSAV", "kSPD", "kUND", "rfi", "_", "_",
144    "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_",
145    "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_",
146    "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_",
147    "dclk", "rmclk", "cwin", "wingo", "_", "dial", "qdial", "_", "_", "hook", "pause", "wait", "_",
148    "_", "_", "_", "_", "_", "_", "_", "_", "_", "op", "oc", "initc", "initp", "scp", "setf",
149    "setb", "cpi", "lpi", "chr", "cvr", "defc", "swidm", "sdrfq", "sitm", "slm", "smicm", "snlq",
150    "snrmq", "sshm", "ssubm", "ssupm", "sum", "rwidm", "ritm", "rlm", "rmicm", "rshm", "rsubm",
151    "rsupm", "rum", "mhpa", "mcud1", "mcub1", "mcuf1", "mvpa", "mcuu1", "porder", "mcud", "mcub",
152    "mcuf", "mcuu", "scs", "smgb", "smgbp", "smglp", "smgrp", "smgt", "smgtp", "sbim", "scsd",
153    "rbim", "rcsd", "subcs", "supcs", "docr", "zerom", "csnm", "kmous", "minfo", "reqmp", "getm",
154    "setaf", "setab", "pfxl", "devt", "csin", "s0ds", "s1ds", "s2ds", "s3ds", "smglr", "smgtb",
155    "birep", "binel", "bicr", "colornm", "defbi", "endbi", "setcolor", "slines", "dispc", "smpch",
156    "rmpch", "smsc", "rmsc", "pctrm", "scesc", "scesa", "ehhlm", "elhlm", "elohlm", "erhlm",
157    "ethlm", "evhlm", "sgr1", "slength", "OTi2", "OTrs", "OTnl", "OTbs", "OTko", "OTma", "OTG2",
158    "OTG3", "OTG1", "OTG4", "OTGR", "OTGL", "OTGU", "OTGD", "OTGH", "OTGV", "OTGC", "meml", "memu",
159    "box1"];
160
161fn read_le_u16(r: &mut dyn io::Read) -> io::Result<u16> {
162    let mut b = [0; 2];
163    r.read_exact(&mut b)?;
164    Ok((b[0] as u16) | ((b[1] as u16) << 8))
165}
166
167fn read_le_u32(r: &mut dyn io::Read) -> io::Result<u32> {
168    let mut b = [0; 4];
169    r.read_exact(&mut b)?;
170    Ok((b[0] as u32) | ((b[1] as u32) << 8) | ((b[2] as u32) << 16) | ((b[3] as u32) << 24))
171}
172
173fn read_byte(r: &mut dyn io::Read) -> io::Result<u8> {
174    match r.bytes().next() {
175        Some(s) => s,
176        None => Err(io::const_error!(io::ErrorKind::Other, "end of file")),
177    }
178}
179
180/// Parse a compiled terminfo entry, using long capability names if `longnames`
181/// is true
182pub(crate) fn parse(file: &mut dyn io::Read, longnames: bool) -> Result<TermInfo, String> {
183    macro_rules! t( ($e:expr) => (
184        match $e {
185            Ok(e) => e,
186            Err(e) => return Err(e.to_string())
187        }
188    ) );
189
190    let (bnames, snames, nnames) = if longnames {
191        (boolfnames, stringfnames, numfnames)
192    } else {
193        (boolnames, stringnames, numnames)
194    };
195
196    // Check magic number
197    let magic = t!(read_le_u16(file));
198
199    let extended = match magic {
200        0o0432 => false,
201        0o01036 => true,
202        _ => return Err(format!("invalid magic number, found {magic:o}")),
203    };
204
205    // According to the spec, these fields must be >= -1 where -1 means that the feature is not
206    // supported. Using 0 instead of -1 works because we skip sections with length 0.
207    macro_rules! read_nonneg {
208        () => {{
209            match t!(read_le_u16(file)) as i16 {
210                n if n >= 0 => n as usize,
211                -1 => 0,
212                _ => return Err("incompatible file: length fields must be  >= -1".to_string()),
213            }
214        }};
215    }
216
217    let names_bytes = read_nonneg!();
218    let bools_bytes = read_nonneg!();
219    let numbers_count = read_nonneg!();
220    let string_offsets_count = read_nonneg!();
221    let string_table_bytes = read_nonneg!();
222
223    if names_bytes == 0 {
224        return Err("incompatible file: names field must be at least 1 byte wide".to_string());
225    }
226
227    if bools_bytes > boolnames.len() {
228        return Err("incompatible file: more booleans than expected".to_string());
229    }
230
231    if numbers_count > numnames.len() {
232        return Err("incompatible file: more numbers than expected".to_string());
233    }
234
235    if string_offsets_count > stringnames.len() {
236        return Err("incompatible file: more string offsets than expected".to_string());
237    }
238
239    // don't read NUL
240    let mut bytes = Vec::new();
241    t!(file.take((names_bytes - 1) as u64).read_to_end(&mut bytes));
242    let names_str = match String::from_utf8(bytes) {
243        Ok(s) => s,
244        Err(_) => return Err("input not utf-8".to_string()),
245    };
246
247    let term_names: Vec<String> = names_str.split('|').map(|s| s.to_string()).collect();
248    // consume NUL
249    if t!(read_byte(file)) != b'\0' {
250        return Err("incompatible file: missing null terminator for names section".to_string());
251    }
252
253    let bools_map: HashMap<String, bool> = t! {
254        (0..bools_bytes).filter_map(|i| match read_byte(file) {
255            Err(e) => Some(Err(e)),
256            Ok(1) => Some(Ok((bnames[i].to_string(), true))),
257            Ok(_) => None
258        }).collect()
259    };
260
261    if (bools_bytes + names_bytes) % 2 == 1 {
262        t!(read_byte(file)); // compensate for padding
263    }
264
265    let numbers_map: HashMap<String, u32> = t! {
266        (0..numbers_count).filter_map(|i| {
267            let number = if extended { read_le_u32(file) } else { read_le_u16(file).map(Into::into) };
268
269            match number {
270                Ok(0xFFFF) => None,
271                Ok(n) => Some(Ok((nnames[i].to_string(), n))),
272                Err(e) => Some(Err(e))
273            }
274        }).collect()
275    };
276
277    let string_map: HashMap<String, Vec<u8>> = if string_offsets_count > 0 {
278        let string_offsets: Vec<u16> =
279            t!((0..string_offsets_count).map(|_| read_le_u16(file)).collect());
280
281        let mut string_table = Vec::new();
282        t!(file.take(string_table_bytes as u64).read_to_end(&mut string_table));
283
284        t!(string_offsets
285            .into_iter()
286            .enumerate()
287            .filter(|&(_, offset)| {
288                // non-entry
289                offset != 0xFFFF
290            })
291            .map(|(i, offset)| {
292                let offset = offset as usize;
293
294                let name = if snames[i] == "_" { stringfnames[i] } else { snames[i] };
295
296                if offset == 0xFFFE {
297                    // undocumented: FFFE indicates cap@, which means the capability is not present
298                    // unsure if the handling for this is correct
299                    return Ok((name.to_string(), Vec::new()));
300                }
301
302                // Find the offset of the NUL we want to go to
303                let nulpos = string_table[offset..string_table_bytes].iter().position(|&b| b == 0);
304                match nulpos {
305                    Some(len) => {
306                        Ok((name.to_string(), string_table[offset..offset + len].to_vec()))
307                    }
308                    None => Err("invalid file: missing NUL in string_table".to_string()),
309                }
310            })
311            .collect())
312    } else {
313        HashMap::new()
314    };
315
316    // And that's all there is to it
317    Ok(TermInfo { names: term_names, bools: bools_map, numbers: numbers_map, strings: string_map })
318}
319
320/// Creates a dummy TermInfo struct for msys terminals
321pub(crate) fn msys_terminfo() -> TermInfo {
322    let mut strings = HashMap::new();
323    strings.insert("sgr0".to_string(), b"\x1B[0m".to_vec());
324    strings.insert("bold".to_string(), b"\x1B[1m".to_vec());
325    strings.insert("setaf".to_string(), b"\x1B[3%p1%dm".to_vec());
326    strings.insert("setab".to_string(), b"\x1B[4%p1%dm".to_vec());
327
328    let mut numbers = HashMap::new();
329    numbers.insert("colors".to_string(), 8);
330
331    TermInfo {
332        names: vec!["cygwin".to_string()], // msys is a fork of an older cygwin version
333        bools: HashMap::new(),
334        numbers,
335        strings,
336    }
337}