terminfo_lean/
parse.rs

1// Copyright 2025 Pavel Roskin
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! Parsing terminfo database files
10
11use std::{
12    collections::{BTreeMap, BTreeSet},
13    io::{Cursor, Read, Seek, SeekFrom},
14    mem,
15};
16
17const ABSENT_ENTRY: i32 = -1;
18const CANCELED_ENTRY: i32 = -2;
19
20const BOOL_NAMES: [&str; 44] = [
21    "bw", "am", "xsb", "xhp", "xenl", "eo", "gn", "hc", "km", "hs", "in", "db", "da", "mir",
22    "msgr", "os", "eslok", "xt", "hz", "ul", "xon", "nxon", "mc5i", "chts", "nrrmc", "npc",
23    "ndscr", "ccc", "bce", "hls", "xhpa", "crxm", "daisy", "xvpa", "sam", "cpix", "lpix", "OTbs",
24    "OTns", "OTnc", "OTMT", "OTNL", "OTpt", "OTxr",
25];
26
27const NUM_NAMES: [&str; 39] = [
28    "cols", "it", "lines", "lm", "xmc", "pb", "vt", "wsl", "nlab", "lh", "lw", "ma", "wnum",
29    "colors", "pairs", "ncv", "bufsz", "spinv", "spinh", "maddr", "mjump", "mcs", "mls", "npins",
30    "orc", "orl", "orhi", "orvi", "cps", "widcs", "btns", "bitwin", "bitype", "UTug", "OTdC",
31    "OTdN", "OTdB", "OTdT", "OTkn",
32];
33
34const STR_NAMES: [&str; 414] = [
35    "cbt", "bel", "cr", "csr", "tbc", "clear", "el", "ed", "hpa", "cmdch", "cup", "cud1", "home",
36    "civis", "cub1", "mrcup", "cnorm", "cuf1", "ll", "cuu1", "cvvis", "dch1", "dl1", "dsl", "hd",
37    "smacs", "blink", "bold", "smcup", "smdc", "dim", "smir", "invis", "prot", "rev", "smso",
38    "smul", "ech", "rmacs", "sgr0", "rmcup", "rmdc", "rmir", "rmso", "rmul", "flash", "ff", "fsl",
39    "is1", "is2", "is3", "if", "ich1", "il1", "ip", "kbs", "ktbc", "kclr", "kctab", "kdch1",
40    "kdl1", "kcud1", "krmir", "kel", "ked", "kf0", "kf1", "kf10", "kf2", "kf3", "kf4", "kf5",
41    "kf6", "kf7", "kf8", "kf9", "khome", "kich1", "kil1", "kcub1", "kll", "knp", "kpp", "kcuf1",
42    "kind", "kri", "khts", "kcuu1", "rmkx", "smkx", "lf0", "lf1", "lf10", "lf2", "lf3", "lf4",
43    "lf5", "lf6", "lf7", "lf8", "lf9", "rmm", "smm", "nel", "pad", "dch", "dl", "cud", "ich",
44    "indn", "il", "cub", "cuf", "rin", "cuu", "pfkey", "pfloc", "pfx", "mc0", "mc4", "mc5", "rep",
45    "rs1", "rs2", "rs3", "rf", "rc", "vpa", "sc", "ind", "ri", "sgr", "hts", "wind", "ht", "tsl",
46    "uc", "hu", "iprog", "ka1", "ka3", "kb2", "kc1", "kc3", "mc5p", "rmp", "acsc", "pln", "kcbt",
47    "smxon", "rmxon", "smam", "rmam", "xonc", "xoffc", "enacs", "smln", "rmln", "kbeg", "kcan",
48    "kclo", "kcmd", "kcpy", "kcrt", "kend", "kent", "kext", "kfnd", "khlp", "kmrk", "kmsg", "kmov",
49    "knxt", "kopn", "kopt", "kprv", "kprt", "krdo", "kref", "krfr", "krpl", "krst", "kres", "ksav",
50    "kspd", "kund", "kBEG", "kCAN", "kCMD", "kCPY", "kCRT", "kDC", "kDL", "kslt", "kEND", "kEOL",
51    "kEXT", "kFND", "kHLP", "kHOM", "kIC", "kLFT", "kMSG", "kMOV", "kNXT", "kOPT", "kPRV", "kPRT",
52    "kRDO", "kRPL", "kRIT", "kRES", "kSAV", "kSPD", "kUND", "rfi", "kf11", "kf12", "kf13", "kf14",
53    "kf15", "kf16", "kf17", "kf18", "kf19", "kf20", "kf21", "kf22", "kf23", "kf24", "kf25", "kf26",
54    "kf27", "kf28", "kf29", "kf30", "kf31", "kf32", "kf33", "kf34", "kf35", "kf36", "kf37", "kf38",
55    "kf39", "kf40", "kf41", "kf42", "kf43", "kf44", "kf45", "kf46", "kf47", "kf48", "kf49", "kf50",
56    "kf51", "kf52", "kf53", "kf54", "kf55", "kf56", "kf57", "kf58", "kf59", "kf60", "kf61", "kf62",
57    "kf63", "el1", "mgc", "smgl", "smgr", "fln", "sclk", "dclk", "rmclk", "cwin", "wingo", "hup",
58    "dial", "qdial", "tone", "pulse", "hook", "pause", "wait", "u0", "u1", "u2", "u3", "u4", "u5",
59    "u6", "u7", "u8", "u9", "op", "oc", "initc", "initp", "scp", "setf", "setb", "cpi", "lpi",
60    "chr", "cvr", "defc", "swidm", "sdrfq", "sitm", "slm", "smicm", "snlq", "snrmq", "sshm",
61    "ssubm", "ssupm", "sum", "rwidm", "ritm", "rlm", "rmicm", "rshm", "rsubm", "rsupm", "rum",
62    "mhpa", "mcud1", "mcub1", "mcuf1", "mvpa", "mcuu1", "porder", "mcud", "mcub", "mcuf", "mcuu",
63    "scs", "smgb", "smgbp", "smglp", "smgrp", "smgt", "smgtp", "sbim", "scsd", "rbim", "rcsd",
64    "subcs", "supcs", "docr", "zerom", "csnm", "kmous", "minfo", "reqmp", "getm", "setaf", "setab",
65    "pfxl", "devt", "csin", "s0ds", "s1ds", "s2ds", "s3ds", "smglr", "smgtb", "birep", "binel",
66    "bicr", "colornm", "defbi", "endbi", "setcolor", "slines", "dispc", "smpch", "rmpch", "smsc",
67    "rmsc", "pctrm", "scesc", "scesa", "ehhlm", "elhlm", "elohlm", "erhlm", "ethlm", "evhlm",
68    "sgr1", "slength", "OTi2", "OTrs", "OTnl", "OTbs", "OTko", "OTma", "OTG2", "OTG3", "OTG1",
69    "OTG4", "OTGR", "OTGL", "OTGU", "OTGD", "OTGH", "OTGV", "OTGC", "meml", "memu", "box1",
70];
71
72#[repr(u16)]
73enum TerminfoMagic {
74    /// Original format, 16-bit numbers
75    Magic1 = 0x011a,
76    /// 32-bit numbers
77    Magic2 = 0x021e,
78}
79
80/// Errors reported when parsing a terminfo database
81#[derive(thiserror::Error, Debug)]
82#[non_exhaustive]
83pub enum Error {
84    /// The magic number is invalid or unsupported
85    #[error("Unknown magic number")]
86    BadMagic,
87    /// A string is not terminated by the NUL byte
88    #[error("String without final NUL")]
89    UnterminatedString,
90    /// Unexpected condition, probably invalid terminfo database
91    #[error("Unsupported terminfo format")]
92    UnsupportedFormat,
93    /// Input/output error, probably truncated terminfo database
94    #[error("I/O error")]
95    IO(#[from] std::io::Error),
96    /// A string is not valid UTF-8
97    #[error("Invalid UTF-8 string")]
98    Utf8(#[from] std::str::Utf8Error),
99}
100
101/// Parse terminfo database from the supplied buffer
102///
103/// Returns `Terminfo` instance with data populated from the buffer.
104pub fn parse(buffer: &[u8]) -> Result<Terminfo<'_>, Error> {
105    let mut terminfo = Terminfo::new();
106    let mut reader = Cursor::new(buffer);
107    terminfo.parse_base(&mut reader)?;
108    match terminfo.parse_extended(&mut reader) {
109        Ok(()) | Err(Error::IO(_)) => {} // missing extended data is OK
110        Err(err) => return Err(err),
111    }
112    Ok(terminfo)
113}
114
115fn read_u8(reader: &mut impl Read) -> Result<u8, Error> {
116    let mut buffer = [0u8; 1];
117    reader.read_exact(&mut buffer)?;
118    Ok(buffer[0])
119}
120
121fn read_le16(reader: &mut impl Read) -> Result<u16, Error> {
122    let mut buffer = [0u8; 2];
123    reader.read_exact(&mut buffer)?;
124    let value = u16::from_le_bytes(buffer);
125    Ok(value)
126}
127
128fn read_slice<'a>(reader: &mut Cursor<&'a [u8]>, size: usize) -> Result<&'a [u8], Error> {
129    let start = reader.position() as usize;
130    let end = reader.seek(SeekFrom::Current(size as i64))? as usize;
131    let buffer = &reader.get_ref();
132    match buffer.get(start..end) {
133        Some(slice) => Ok(slice),
134        None => Err(Error::UnsupportedFormat),
135    }
136}
137
138fn get_string(string_table: &[u8], offset: usize) -> Result<&[u8], Error> {
139    let Some(string_slice) = string_table.get(offset..) else {
140        return Err(Error::UnsupportedFormat);
141    };
142    if let Some(string_length) = &string_slice.iter().position(|c| *c == b'\0') {
143        Ok(&string_table[offset..offset + string_length])
144    } else {
145        Err(Error::UnterminatedString)
146    }
147}
148
149/// Convert ABSENT and CANCELED to None
150fn check_offset(size: u16) -> Option<usize> {
151    match i32::from(size as i16) {
152        ABSENT_ENTRY | CANCELED_ENTRY => None,
153        _ => Some(usize::from(size)),
154    }
155}
156
157/// Skip a byte if needed to ensure 2-byte alignment
158fn align_cursor(reader: &mut Cursor<&[u8]>) -> Result<(), Error> {
159    let position = reader.position();
160    if position & 1 == 1 {
161        reader.seek_relative(1)?;
162    }
163    Ok(())
164}
165
166/// Parsed terminfo entry
167#[derive(Debug)]
168pub struct Terminfo<'a> {
169    pub booleans: BTreeSet<&'a str>,
170    pub numbers: BTreeMap<&'a str, i32>,
171    pub strings: BTreeMap<&'a str, &'a [u8]>,
172    number_size: usize,
173}
174
175impl<'a> Terminfo<'a> {
176    fn new() -> Self {
177        Self {
178            booleans: BTreeSet::default(),
179            numbers: BTreeMap::default(),
180            strings: BTreeMap::default(),
181            number_size: 0,
182        }
183    }
184
185    fn read_number(&self, reader: &mut Cursor<&'a [u8]>) -> Result<Option<i32>, Error> {
186        let value = if self.number_size == 4 {
187            let mut buffer = [0u8; 4];
188            reader.read_exact(&mut buffer)?;
189            i32::from_le_bytes(buffer)
190        } else {
191            let mut buffer = [0u8; 2];
192            reader.read_exact(&mut buffer)?;
193            i32::from(i16::from_le_bytes(buffer))
194        };
195        if value > 0 { Ok(Some(value)) } else { Ok(None) }
196    }
197
198    /// Parse base capabilities
199    fn parse_base(&mut self, mut reader: &mut Cursor<&'a [u8]>) -> Result<(), Error> {
200        let magic = read_le16(&mut reader)?;
201        let name_size = usize::from(read_le16(&mut reader)?);
202        let bool_count = usize::from(read_le16(&mut reader)?);
203        let num_count = usize::from(read_le16(&mut reader)?);
204        let str_count = usize::from(read_le16(&mut reader)?);
205        let str_size = usize::from(read_le16(&mut reader)?);
206
207        self.number_size = match magic {
208            val if val == TerminfoMagic::Magic1 as u16 => 2,
209            val if val == TerminfoMagic::Magic2 as u16 => 4,
210            _ => return Err(Error::BadMagic),
211        };
212
213        if bool_count > BOOL_NAMES.len()
214            || num_count > NUM_NAMES.len()
215            || str_count > STR_NAMES.len()
216        {
217            return Err(Error::UnsupportedFormat);
218        }
219
220        // Skip terminal names/aliases, we are not using them
221        reader.seek_relative(name_size as i64)?;
222
223        for name in BOOL_NAMES.iter().take(bool_count) {
224            let value = read_u8(&mut reader)?;
225            match value {
226                0 => {}
227                1 => {
228                    self.booleans.insert(*name);
229                }
230                _ => return Err(Error::UnsupportedFormat),
231            }
232        }
233
234        align_cursor(reader)?;
235
236        for name in NUM_NAMES.iter().take(num_count) {
237            if let Some(number) = self.read_number(reader)? {
238                self.numbers.insert(*name, number);
239            }
240        }
241
242        let str_offsets = read_slice(reader, mem::size_of::<u16>() * str_count)?;
243        let mut str_offsets_reader = Cursor::new(str_offsets);
244
245        let str_table = read_slice(reader, str_size)?;
246
247        for name in STR_NAMES.iter().take(str_count) {
248            let offset = read_le16(&mut str_offsets_reader)?;
249            let Some(offset) = check_offset(offset) else {
250                continue;
251            };
252            let value = get_string(str_table, offset)?;
253            self.strings.insert(*name, value);
254        }
255
256        Ok(())
257    }
258
259    /// Parse extended capabilities
260    fn parse_extended(&mut self, mut reader: &mut Cursor<&'a [u8]>) -> Result<(), Error> {
261        align_cursor(reader)?;
262
263        let bool_count = usize::from(read_le16(&mut reader)?);
264        let num_count = usize::from(read_le16(&mut reader)?);
265        let str_count = usize::from(read_le16(&mut reader)?);
266        let _ext_str_usage = usize::from(read_le16(&mut reader)?);
267        let str_limit = usize::from(read_le16(&mut reader)?);
268
269        let bools = read_slice(reader, bool_count)?;
270        let mut bools_reader = Cursor::new(bools);
271        align_cursor(reader)?;
272
273        let nums = read_slice(reader, self.number_size * num_count)?;
274        let mut nums_reader = Cursor::new(nums);
275
276        let strs = read_slice(reader, mem::size_of::<u16>() * str_count)?;
277        let mut strs_reader = Cursor::new(strs);
278
279        let name_count = bool_count + num_count + str_count;
280        let names = read_slice(reader, mem::size_of::<u16>() * name_count)?;
281        let mut names_reader = Cursor::new(names);
282
283        let str_table = read_slice(reader, str_limit)?;
284
285        let mut names_base = 0;
286        loop {
287            let Ok(offset) = read_le16(&mut strs_reader) else {
288                break;
289            };
290            let Some(offset) = check_offset(offset) else {
291                continue;
292            };
293            names_base += get_string(str_table, offset)?.len() + 1;
294        }
295
296        let Some(names_table) = &str_table.get(names_base..) else {
297            return Err(Error::UnsupportedFormat);
298        };
299
300        loop {
301            let Ok(value) = read_u8(&mut bools_reader) else {
302                break;
303            };
304            if value != 1 {
305                return Err(Error::UnsupportedFormat);
306            }
307            let Ok(name_offset) = read_le16(&mut names_reader) else {
308                return Err(Error::UnsupportedFormat);
309            };
310            let Some(name_offset) = check_offset(name_offset) else {
311                return Err(Error::UnsupportedFormat);
312            };
313            let name = get_string(names_table, name_offset)?;
314            self.booleans.insert(str::from_utf8(name)?);
315        }
316
317        loop {
318            let Ok(value) = self.read_number(&mut nums_reader) else {
319                break;
320            };
321            let Some(value) = value else {
322                return Err(Error::UnsupportedFormat);
323            };
324            let Ok(name_offset) = read_le16(&mut names_reader) else {
325                return Err(Error::UnsupportedFormat);
326            };
327            let Some(name_offset) = check_offset(name_offset) else {
328                return Err(Error::UnsupportedFormat);
329            };
330            let name = get_string(names_table, name_offset)?;
331            self.numbers.insert(str::from_utf8(name)?, value);
332        }
333
334        strs_reader.set_position(0);
335        loop {
336            let Ok(str_offset) = read_le16(&mut strs_reader) else {
337                break;
338            };
339            let Ok(name_offset) = read_le16(&mut names_reader) else {
340                return Err(Error::UnsupportedFormat);
341            };
342            if let (Some(str_offset), Some(name_offset)) =
343                (check_offset(str_offset), check_offset(name_offset))
344            {
345                let value = get_string(str_table, str_offset)?;
346                let name = get_string(names_table, name_offset)?;
347                self.strings.insert(str::from_utf8(name)?, value);
348            }
349        }
350
351        Ok(())
352    }
353}
354
355#[cfg(test)]
356mod test {
357    use collection_literals::collection;
358
359    use super::*;
360
361    #[repr(i32)]
362    #[derive(Clone, Copy, PartialEq)]
363    enum NumberType {
364        U16 = 2,
365        U32 = 4,
366    }
367
368    fn make_buffer(number_type: NumberType, add_ext: bool) -> Vec<u8> {
369        let (magic, numbers) = match number_type {
370            NumberType::U16 => (0x011a, &[80, 0xFFFE, 25, 0xFFFF, 0x8000, 82]),
371            NumberType::U32 => (
372                0x021e,
373                &[120, 0xFFFF_FFFE, 42, 0xFFFF_FFFF, 0x8000_0000, 82000],
374            ),
375        };
376        let term_name = b"myterm";
377        let booleans = &[1, 0, 0, 0, 1];
378        let strings: &[Option<&[u8]>] = &[None, Some(b"Hello"), None, None, Some(b"World!")];
379        let str_size = strings.iter().flatten().map(|x| x.len() as u16 + 1).sum();
380
381        let mut buffer = vec![];
382        buffer.extend_from_slice(&u16::to_le_bytes(magic));
383        buffer.extend_from_slice(&u16::to_le_bytes(term_name.len() as u16 + 1));
384        buffer.extend_from_slice(&u16::to_le_bytes(booleans.len() as u16));
385        buffer.extend_from_slice(&u16::to_le_bytes(numbers.len() as u16));
386        buffer.extend_from_slice(&u16::to_le_bytes(strings.len() as u16));
387        buffer.extend_from_slice(&u16::to_le_bytes(str_size));
388        buffer.extend_from_slice(term_name);
389        buffer.push(0);
390        buffer.extend_from_slice(booleans);
391        if !buffer.len().is_multiple_of(2) {
392            buffer.push(0);
393        }
394        for number in numbers {
395            match number_type {
396                NumberType::U16 => buffer.extend_from_slice(&u16::to_le_bytes(*number as u16)),
397                NumberType::U32 => buffer.extend_from_slice(&u32::to_le_bytes(*number)),
398            }
399        }
400        let mut offset = 0;
401        for string in strings {
402            if let Some(string) = string {
403                buffer.extend_from_slice(&u16::to_le_bytes(offset));
404                offset += string.len() as u16 + 1;
405            } else {
406                buffer.extend_from_slice(&u16::to_le_bytes(0xFFFF));
407            }
408        }
409        for string in strings.iter().flatten() {
410            buffer.extend_from_slice(string);
411            buffer.push(0);
412        }
413        if add_ext {
414            if !buffer.len().is_multiple_of(2) {
415                buffer.push(0);
416            }
417            buffer.append(&mut make_ext_buffer(number_type));
418        }
419        buffer
420    }
421
422    fn make_ext_buffer(number_type: NumberType) -> Vec<u8> {
423        let booleans: &[&[u8]] = &[b"Curly", b"Italic", b"Semi-bold"];
424        let numbers: &[(&[u8], u32)] = &[(b"Shades", 1100), (b"Variants", 2200)];
425        let strings: &[(&[u8], Option<&[u8]>)] = &[
426            (b"Colors", Some(b"A lot")),
427            (b"Luminocity", Some(b"Positive")),
428            (b"Ideas", None),
429        ];
430
431        let boolean_name_size: u16 = booleans.iter().map(|x| x.len() as u16 + 1).sum();
432        let number_name_size: u16 = numbers.iter().map(|x| x.0.len() as u16 + 1).sum();
433        let string_name_size: u16 = strings.iter().map(|x| x.0.len() as u16 + 1).sum();
434        let string_value_size: u16 = strings
435            .iter()
436            .filter_map(|x| x.1)
437            .map(|x| x.len() as u16 + 1)
438            .sum();
439        let name_size = boolean_name_size + number_name_size + string_name_size;
440        let string_size = name_size + string_value_size;
441
442        let mut buffer = vec![];
443        buffer.extend_from_slice(&u16::to_le_bytes(booleans.len() as u16));
444        buffer.extend_from_slice(&u16::to_le_bytes(numbers.len() as u16));
445        buffer.extend_from_slice(&u16::to_le_bytes(strings.len() as u16));
446        buffer.extend_from_slice(&u16::to_le_bytes(0u16)); // unused `ext_str_usage`
447        buffer.extend_from_slice(&u16::to_le_bytes(string_size));
448
449        // boolean values, align(2), number values, string value offsets
450        // name offsets, string value table, boolean names, number names, string names
451
452        for _boolean in booleans {
453            buffer.push(1);
454        }
455        if !buffer.len().is_multiple_of(2) {
456            buffer.push(0);
457        }
458        for number in numbers {
459            match number_type {
460                NumberType::U16 => buffer.extend_from_slice(&u16::to_le_bytes(number.1 as u16)),
461                NumberType::U32 => buffer.extend_from_slice(&u32::to_le_bytes(number.1)),
462            }
463        }
464        let mut offset = 0;
465        for string in strings {
466            if let Some(string) = string.1 {
467                buffer.extend_from_slice(&u16::to_le_bytes(offset));
468                offset += string.len() as u16 + 1;
469            } else {
470                buffer.extend_from_slice(&u16::to_le_bytes(0xFFFF));
471            }
472        }
473
474        offset = 0;
475        for boolean in booleans {
476            buffer.extend_from_slice(&u16::to_le_bytes(offset));
477            offset += boolean.len() as u16 + 1;
478        }
479        for number in numbers {
480            buffer.extend_from_slice(&u16::to_le_bytes(offset));
481            offset += number.0.len() as u16 + 1;
482        }
483        for string in strings {
484            buffer.extend_from_slice(&u16::to_le_bytes(offset));
485            offset += string.0.len() as u16 + 1;
486        }
487
488        for string in strings {
489            if let Some(string) = string.1 {
490                buffer.extend_from_slice(string);
491                buffer.push(0);
492            }
493        }
494
495        for boolean in booleans {
496            buffer.extend_from_slice(boolean);
497            buffer.push(0);
498        }
499        for number in numbers {
500            buffer.extend_from_slice(number.0);
501            buffer.push(0);
502        }
503        for string in strings {
504            buffer.extend_from_slice(string.0);
505            buffer.push(0);
506        }
507
508        buffer
509    }
510
511    #[test]
512    fn empty_buffer() {
513        let terminfo = parse(b"");
514        assert!(matches!(terminfo.unwrap_err(), Error::IO(_)));
515    }
516
517    #[test]
518    fn base_16_bit() {
519        let buffer = make_buffer(NumberType::U16, false);
520        let terminfo = parse(buffer.as_slice()).unwrap();
521        assert_eq!(terminfo.booleans, collection!("bw", "xenl"));
522        assert_eq!(
523            terminfo.numbers,
524            collection!(
525                "cols" => 80,
526                "lines" => 25,
527                "pb" => 82,
528            )
529        );
530        assert_eq!(
531            terminfo.strings,
532            collection!(
533                "bel" => b"Hello".as_slice(),
534                "tbc" => b"World!",
535            )
536        );
537    }
538
539    #[test]
540    fn base_32_bit() {
541        let buffer = make_buffer(NumberType::U32, false);
542        let terminfo = parse(buffer.as_slice()).unwrap();
543        assert_eq!(terminfo.booleans, collection!("bw", "xenl"));
544        assert_eq!(
545            terminfo.numbers,
546            collection!(
547                "cols" => 120,
548                "lines" => 42,
549                "pb" => 82000,
550            )
551        );
552        assert_eq!(
553            terminfo.strings,
554            collection!(
555                "bel" => b"Hello".as_slice(),
556                "tbc" => b"World!",
557            )
558        );
559    }
560
561    #[test]
562    fn bad_magic() {
563        let mut buffer = make_buffer(NumberType::U16, false);
564        buffer[1] = 3;
565        let terminfo = parse(buffer.as_slice());
566        assert!(matches!(terminfo.unwrap_err(), Error::BadMagic));
567    }
568
569    #[test]
570    fn base_truncated() {
571        let mut buffer = make_buffer(NumberType::U16, false);
572        buffer.pop();
573        let terminfo = parse(buffer.as_slice());
574        assert!(matches!(terminfo.unwrap_err(), Error::UnsupportedFormat));
575    }
576
577    #[test]
578    fn base_unterminated_string() {
579        let mut buffer = make_buffer(NumberType::U16, false);
580        let buffer_size = buffer.len();
581        buffer[buffer_size - 1] = b'!';
582        let terminfo = parse(buffer.as_slice());
583        assert!(matches!(terminfo.unwrap_err(), Error::UnterminatedString));
584    }
585
586    #[test]
587    fn extended_16_bit() {
588        let buffer = make_buffer(NumberType::U16, true);
589        let terminfo = parse(buffer.as_slice()).unwrap();
590        assert_eq!(
591            terminfo.booleans,
592            collection!("Curly", "Italic", "Semi-bold", "bw", "xenl")
593        );
594        assert_eq!(
595            terminfo.numbers,
596            collection!(
597                "Shades" => 1100,
598                "Variants" => 2200,
599                "cols" => 80,
600                "lines" => 25,
601                "pb" => 82,
602            )
603        );
604        assert_eq!(
605            terminfo.strings,
606            collection!(
607                "Colors" => b"A lot".as_slice(),
608                "Luminocity" => b"Positive",
609                "bel" => b"Hello",
610                "tbc" => b"World!",
611            )
612        );
613    }
614
615    #[test]
616    fn extended_32_bit() {
617        let buffer = make_buffer(NumberType::U32, true);
618        let terminfo = parse(buffer.as_slice()).unwrap();
619        assert_eq!(
620            terminfo.booleans,
621            collection!("Curly", "Italic", "Semi-bold", "bw", "xenl")
622        );
623        assert_eq!(
624            terminfo.numbers,
625            collection!(
626                "Shades" => 1100,
627                "Variants" => 2200,
628                "cols" => 120,
629                "lines" => 42,
630                "pb" => 82000,
631            )
632        );
633        assert_eq!(
634            terminfo.strings,
635            collection!(
636                "Colors" => b"A lot".as_slice(),
637                "Luminocity" => b"Positive",
638                "bel" => b"Hello",
639                "tbc" => b"World!",
640            )
641        );
642    }
643}