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