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 NUMBER_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 STRING_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    /// Boolean value not 0 or 1, probably invalid terminfo database
94    #[error("Invalid boolean value {0}")]
95    InvalidBooleanValue(u8),
96    /// Input/output error, probably truncated terminfo database
97    #[error("I/O error")]
98    IO(#[from] std::io::Error),
99    /// A string is not valid UTF-8
100    #[error("Invalid UTF-8 string")]
101    Utf8(#[from] std::str::Utf8Error),
102}
103
104/// Parse terminfo database from the supplied buffer
105///
106/// Returns `Terminfo` instance with data populated from the buffer.
107pub fn parse(buffer: &[u8]) -> Result<Terminfo<'_>, Error> {
108    let mut terminfo = Terminfo::new();
109    let mut reader = Cursor::new(buffer);
110    terminfo.parse_base(&mut reader)?;
111    match terminfo.parse_extended(&mut reader) {
112        Ok(()) | Err(Error::IO(_)) => {} // missing extended data is OK
113        Err(err) => return Err(err),
114    }
115    Ok(terminfo)
116}
117
118fn read_u8(reader: &mut impl Read) -> Result<u8, Error> {
119    let mut buffer = [0u8; 1];
120    reader.read_exact(&mut buffer)?;
121    Ok(buffer[0])
122}
123
124fn read_le16(reader: &mut impl Read) -> Result<u16, Error> {
125    let mut buffer = [0u8; 2];
126    reader.read_exact(&mut buffer)?;
127    let value = u16::from_le_bytes(buffer);
128    Ok(value)
129}
130
131fn read_slice<'a>(reader: &mut Cursor<&'a [u8]>, size: usize) -> Result<&'a [u8], Error> {
132    let start = reader.position() as usize;
133    let end = reader.seek(SeekFrom::Current(size as i64))? as usize;
134    let buffer = &reader.get_ref();
135    match buffer.get(start..end) {
136        Some(slice) => Ok(slice),
137        None => Err(Error::UnsupportedFormat),
138    }
139}
140
141fn get_string(string_table: &[u8], offset: usize) -> Result<&[u8], Error> {
142    let Some(string_slice) = string_table.get(offset..) else {
143        return Err(Error::UnsupportedFormat);
144    };
145    if let Some(string_length) = &string_slice.iter().position(|c| *c == b'\0') {
146        Ok(&string_table[offset..offset + string_length])
147    } else {
148        Err(Error::UnterminatedString)
149    }
150}
151
152/// Convert ABSENT and CANCELED to None
153fn check_offset(size: u16) -> Option<usize> {
154    match i32::from(size as i16) {
155        ABSENT_ENTRY | CANCELED_ENTRY => None,
156        _ => Some(usize::from(size)),
157    }
158}
159
160/// Skip a byte if needed to ensure 2-byte alignment
161fn align_cursor(reader: &mut Cursor<&[u8]>) -> Result<(), Error> {
162    let position = reader.position();
163    if position & 1 == 1 {
164        reader.seek_relative(1)?;
165    }
166    Ok(())
167}
168
169/// Parsed terminfo entry
170#[derive(Debug)]
171pub struct Terminfo<'a> {
172    pub booleans: BTreeSet<&'a str>,
173    pub numbers: BTreeMap<&'a str, i32>,
174    pub strings: BTreeMap<&'a str, &'a [u8]>,
175    number_size: usize,
176}
177
178impl<'a> Terminfo<'a> {
179    fn new() -> Self {
180        Self {
181            booleans: BTreeSet::default(),
182            numbers: BTreeMap::default(),
183            strings: BTreeMap::default(),
184            number_size: 0,
185        }
186    }
187
188    fn read_number(&self, reader: &mut Cursor<&'a [u8]>) -> Result<Option<i32>, Error> {
189        let value = if self.number_size == 4 {
190            let mut buffer = [0u8; 4];
191            reader.read_exact(&mut buffer)?;
192            i32::from_le_bytes(buffer)
193        } else {
194            let mut buffer = [0u8; 2];
195            reader.read_exact(&mut buffer)?;
196            i32::from(i16::from_le_bytes(buffer))
197        };
198        if value > 0 { Ok(Some(value)) } else { Ok(None) }
199    }
200
201    /// Parse base capabilities
202    fn parse_base(&mut self, mut reader: &mut Cursor<&'a [u8]>) -> Result<(), Error> {
203        let magic = read_le16(&mut reader)?;
204        let name_size = usize::from(read_le16(&mut reader)?);
205        let bool_count = usize::from(read_le16(&mut reader)?);
206        let num_count = usize::from(read_le16(&mut reader)?);
207        let str_count = usize::from(read_le16(&mut reader)?);
208        let str_size = usize::from(read_le16(&mut reader)?);
209
210        self.number_size = match magic {
211            val if val == TerminfoMagic::Magic1 as u16 => 2,
212            val if val == TerminfoMagic::Magic2 as u16 => 4,
213            _ => return Err(Error::BadMagic),
214        };
215
216        if bool_count > BOOL_NAMES.len()
217            || num_count > NUMBER_NAMES.len()
218            || str_count > STRING_NAMES.len()
219        {
220            return Err(Error::UnsupportedFormat);
221        }
222
223        // Skip terminal names/aliases, we are not using them
224        reader.seek_relative(name_size as i64)?;
225
226        for name in BOOL_NAMES.iter().take(bool_count) {
227            let value = read_u8(&mut reader)?;
228            match value {
229                0 => continue,
230                1 => {}
231                value => return Err(Error::InvalidBooleanValue(value)),
232            }
233            self.booleans.insert(*name);
234        }
235
236        align_cursor(reader)?;
237
238        for name in NUMBER_NAMES.iter().take(num_count) {
239            if let Some(number) = self.read_number(reader)? {
240                self.numbers.insert(*name, number);
241            }
242        }
243
244        let str_offsets = read_slice(reader, mem::size_of::<u16>() * str_count)?;
245        let mut str_offsets_reader = Cursor::new(str_offsets);
246
247        let str_table = read_slice(reader, str_size)?;
248
249        for name in STRING_NAMES.iter().take(str_count) {
250            let offset = read_le16(&mut str_offsets_reader)?;
251            let Some(offset) = check_offset(offset) else {
252                continue;
253            };
254            let value = get_string(str_table, offset)?;
255            self.strings.insert(*name, value);
256        }
257
258        Ok(())
259    }
260
261    /// Parse extended capabilities
262    fn parse_extended(&mut self, mut reader: &mut Cursor<&'a [u8]>) -> Result<(), Error> {
263        align_cursor(reader)?;
264
265        let bool_count = usize::from(read_le16(&mut reader)?);
266        let num_count = usize::from(read_le16(&mut reader)?);
267        let str_count = usize::from(read_le16(&mut reader)?);
268        let _ext_str_usage = usize::from(read_le16(&mut reader)?);
269        let str_limit = usize::from(read_le16(&mut reader)?);
270
271        let bools = read_slice(reader, bool_count)?;
272        let mut bools_reader = Cursor::new(bools);
273        align_cursor(reader)?;
274
275        let nums = read_slice(reader, self.number_size * num_count)?;
276        let mut nums_reader = Cursor::new(nums);
277
278        let strs = read_slice(reader, mem::size_of::<u16>() * str_count)?;
279        let mut strs_reader = Cursor::new(strs);
280
281        let name_count = bool_count + num_count + str_count;
282        let names = read_slice(reader, mem::size_of::<u16>() * name_count)?;
283        let mut names_reader = Cursor::new(names);
284
285        let str_table = read_slice(reader, str_limit)?;
286
287        let mut names_base = 0;
288        loop {
289            let Ok(offset) = read_le16(&mut strs_reader) else {
290                break;
291            };
292            let Some(offset) = check_offset(offset) else {
293                continue;
294            };
295            names_base += get_string(str_table, offset)?.len() + 1;
296        }
297
298        let Some(names_table) = &str_table.get(names_base..) else {
299            return Err(Error::UnsupportedFormat);
300        };
301
302        loop {
303            let Ok(value) = read_u8(&mut bools_reader) else {
304                break;
305            };
306            let Ok(name_offset) = read_le16(&mut names_reader) else {
307                return Err(Error::UnsupportedFormat);
308            };
309            match value {
310                0 => continue,
311                1 => {}
312                value => return Err(Error::InvalidBooleanValue(value)),
313            }
314            let Some(name_offset) = check_offset(name_offset) else {
315                return Err(Error::UnsupportedFormat);
316            };
317            let name = get_string(names_table, name_offset)?;
318            self.booleans.insert(str::from_utf8(name)?);
319        }
320
321        loop {
322            let Ok(value) = self.read_number(&mut nums_reader) else {
323                break;
324            };
325            let Ok(name_offset) = read_le16(&mut names_reader) else {
326                return Err(Error::UnsupportedFormat);
327            };
328            let Some(value) = value else {
329                continue;
330            };
331            let Some(name_offset) = check_offset(name_offset) else {
332                return Err(Error::UnsupportedFormat);
333            };
334            let name = get_string(names_table, name_offset)?;
335            self.numbers.insert(str::from_utf8(name)?, value);
336        }
337
338        strs_reader.set_position(0);
339        loop {
340            let Ok(str_offset) = read_le16(&mut strs_reader) else {
341                break;
342            };
343            let Ok(name_offset) = read_le16(&mut names_reader) else {
344                return Err(Error::UnsupportedFormat);
345            };
346            if let (Some(str_offset), Some(name_offset)) =
347                (check_offset(str_offset), check_offset(name_offset))
348            {
349                let value = get_string(str_table, str_offset)?;
350                let name = get_string(names_table, name_offset)?;
351                self.strings.insert(str::from_utf8(name)?, value);
352            }
353        }
354
355        Ok(())
356    }
357}
358
359#[cfg(test)]
360mod test {
361    use collection_literals::collection;
362
363    use super::*;
364
365    #[derive(Clone, Copy, PartialEq)]
366    enum NumberType {
367        U16,
368        U32,
369    }
370
371    #[derive(Clone, PartialEq)]
372    enum StringValue {
373        Present(Vec<u8>),
374        Absent,
375        Canceled,
376    }
377
378    // Allow conversion from `StringValue` to `Option`
379    impl<'a> From<&'a StringValue> for Option<&'a [u8]> {
380        fn from(value: &'a StringValue) -> Self {
381            match value {
382                StringValue::Present(value) => Some(value),
383                _ => None,
384            }
385        }
386    }
387
388    // Allow iteration over `StringValue` by converting it to `Option`
389    impl<'a> IntoIterator for &'a StringValue {
390        type Item = &'a [u8];
391        type IntoIter = std::option::IntoIter<Self::Item>;
392
393        fn into_iter(self) -> Self::IntoIter {
394            Option::<&'a [u8]>::from(self).into_iter()
395        }
396    }
397
398    impl<const N: usize> From<&[u8; N]> for StringValue {
399        fn from(value: &[u8; N]) -> Self {
400            Self::Present(value.to_vec())
401        }
402    }
403
404    // Size of byte string in memory with terminating NUL
405    fn memlen(byte_string: &[u8]) -> u16 {
406        byte_string.len() as u16 + 1
407    }
408
409    struct DataSet {
410        number_type: NumberType,
411        term_name: Vec<u8>,
412        base_booleans: Vec<u8>,
413        base_numbers: Vec<i32>,
414        base_strings: Vec<StringValue>,
415        ext_booleans: Vec<(&'static [u8], u8)>,
416        ext_numbers: Vec<(&'static [u8], i32)>,
417        ext_strings: Vec<(&'static [u8], StringValue)>,
418    }
419
420    impl Default for DataSet {
421        fn default() -> Self {
422            Self {
423                number_type: NumberType::U16,
424                term_name: b"myterm".to_vec(),
425                base_booleans: vec![1, 0, 0, 0, 1],
426                base_numbers: vec![80, -2, 25, -1, -10, 0x10005],
427                base_strings: vec![
428                    StringValue::Absent,
429                    StringValue::from(b"Hello"),
430                    StringValue::Canceled,
431                    StringValue::Absent,
432                    StringValue::from(b"World!"),
433                ],
434                ext_booleans: vec![
435                    (b"Curly", 1),
436                    (b"Italic", 1),
437                    (b"Invisible", 0),
438                    (b"Semi-bold", 1),
439                ],
440                ext_numbers: vec![(b"Shades", 1100), (b"Variants", 2200)],
441                ext_strings: vec![
442                    (b"Colors", StringValue::from(b"A lot")),
443                    (b"Ideas", StringValue::Absent),
444                    (b"Luminosity", StringValue::from(b"Positive")),
445                ],
446            }
447        }
448    }
449
450    fn make_buffer(data_set: &DataSet, add_ext: bool) -> Vec<u8> {
451        let magic = match data_set.number_type {
452            NumberType::U16 => 0x011a,
453            NumberType::U32 => 0x021e,
454        };
455        let str_size = data_set.base_strings.iter().flatten().map(memlen).sum();
456
457        let mut buffer = vec![];
458        buffer.extend_from_slice(&u16::to_le_bytes(magic));
459        buffer.extend_from_slice(&u16::to_le_bytes(memlen(&data_set.term_name)));
460        buffer.extend_from_slice(&u16::to_le_bytes(data_set.base_booleans.len() as u16));
461        buffer.extend_from_slice(&u16::to_le_bytes(data_set.base_numbers.len() as u16));
462        buffer.extend_from_slice(&u16::to_le_bytes(data_set.base_strings.len() as u16));
463        buffer.extend_from_slice(&u16::to_le_bytes(str_size));
464        buffer.extend_from_slice(&data_set.term_name);
465        buffer.push(0);
466        buffer.extend_from_slice(&data_set.base_booleans);
467        if !buffer.len().is_multiple_of(2) {
468            buffer.push(0);
469        }
470        for number in &data_set.base_numbers {
471            match data_set.number_type {
472                NumberType::U16 => buffer.extend_from_slice(&u16::to_le_bytes(*number as u16)),
473                NumberType::U32 => buffer.extend_from_slice(&u32::to_le_bytes(*number as u32)),
474            }
475        }
476        let mut offset = 0;
477        for string in &data_set.base_strings {
478            match string {
479                StringValue::Present(string) => {
480                    buffer.extend_from_slice(&u16::to_le_bytes(offset));
481                    offset += memlen(string);
482                }
483                StringValue::Absent => buffer.extend_from_slice(&u16::to_le_bytes(0xffff)),
484                StringValue::Canceled => buffer.extend_from_slice(&u16::to_le_bytes(0xfffe)),
485            }
486        }
487        for string in data_set.base_strings.iter().flatten() {
488            buffer.extend_from_slice(string);
489            buffer.push(0);
490        }
491        if add_ext {
492            if !buffer.len().is_multiple_of(2) {
493                buffer.push(0);
494            }
495            buffer.append(&mut make_ext_buffer(data_set));
496        }
497        buffer
498    }
499
500    fn make_ext_buffer(data_set: &DataSet) -> Vec<u8> {
501        let booleans = &data_set.ext_booleans;
502        let numbers = &data_set.ext_numbers;
503        let strings = &data_set.ext_strings;
504
505        let boolean_name_size: u16 = booleans.iter().map(|x| memlen(x.0)).sum();
506        let number_name_size: u16 = numbers.iter().map(|x| memlen(x.0)).sum();
507        let string_name_size: u16 = strings.iter().map(|x| memlen(x.0)).sum();
508        let string_value_size: u16 = strings.iter().flat_map(|x| &x.1).map(memlen).sum();
509        let name_size = boolean_name_size + number_name_size + string_name_size;
510        let string_size = name_size + string_value_size;
511
512        let mut buffer = vec![];
513
514        // The layout is:
515        //
516        // extended header, boolean values, align(2), number values, string value offsets,
517        // name offsets, string values, boolean names, number names, string names.
518
519        buffer.extend_from_slice(&u16::to_le_bytes(booleans.len() as u16));
520        buffer.extend_from_slice(&u16::to_le_bytes(numbers.len() as u16));
521        buffer.extend_from_slice(&u16::to_le_bytes(strings.len() as u16));
522        buffer.extend_from_slice(&u16::to_le_bytes(0u16)); // unused `ext_str_usage`
523        buffer.extend_from_slice(&u16::to_le_bytes(string_size));
524
525        for boolean in booleans {
526            buffer.push(boolean.1);
527        }
528        if !buffer.len().is_multiple_of(2) {
529            buffer.push(0);
530        }
531        for number in numbers {
532            match data_set.number_type {
533                NumberType::U16 => buffer.extend_from_slice(&u16::to_le_bytes(number.1 as u16)),
534                NumberType::U32 => buffer.extend_from_slice(&u32::to_le_bytes(number.1 as u32)),
535            }
536        }
537        let mut offset = 0;
538        for string in strings {
539            match &string.1 {
540                StringValue::Present(string) => {
541                    buffer.extend_from_slice(&u16::to_le_bytes(offset));
542                    offset += memlen(string);
543                }
544                StringValue::Absent => buffer.extend_from_slice(&u16::to_le_bytes(0xffff)),
545                StringValue::Canceled => buffer.extend_from_slice(&u16::to_le_bytes(0xfffe)),
546            }
547        }
548
549        offset = 0;
550        for boolean in booleans {
551            buffer.extend_from_slice(&u16::to_le_bytes(offset));
552            offset += memlen(boolean.0);
553        }
554        for number in numbers {
555            buffer.extend_from_slice(&u16::to_le_bytes(offset));
556            offset += memlen(number.0);
557        }
558        for string in strings {
559            buffer.extend_from_slice(&u16::to_le_bytes(offset));
560            offset += memlen(string.0);
561        }
562
563        for string in strings {
564            if let StringValue::Present(string) = &string.1 {
565                buffer.extend_from_slice(string);
566                buffer.push(0);
567            }
568        }
569
570        for boolean in booleans {
571            buffer.extend_from_slice(boolean.0);
572            buffer.push(0);
573        }
574        for number in numbers {
575            buffer.extend_from_slice(number.0);
576            buffer.push(0);
577        }
578        for string in strings {
579            buffer.extend_from_slice(string.0);
580            buffer.push(0);
581        }
582
583        buffer
584    }
585
586    #[test]
587    fn empty_buffer() {
588        let terminfo = parse(b"");
589        assert!(matches!(terminfo.unwrap_err(), Error::IO(_)));
590    }
591
592    #[test]
593    fn base_16_bit() {
594        let data_set = DataSet::default();
595        let buffer = make_buffer(&data_set, false);
596        let terminfo = parse(buffer.as_slice()).unwrap();
597        assert_eq!(terminfo.booleans, collection!("bw", "xenl"));
598        assert_eq!(
599            terminfo.numbers,
600            collection!(
601                "cols" => 80,
602                "lines" => 25,
603                "pb" => 5,
604            )
605        );
606        assert_eq!(
607            terminfo.strings,
608            collection!(
609                "bel" => b"Hello".as_slice(),
610                "tbc" => b"World!",
611            )
612        );
613    }
614
615    #[test]
616    fn base_32_bit() {
617        let mut data_set = DataSet {
618            number_type: NumberType::U32,
619            ..Default::default()
620        };
621        data_set.base_numbers[5] = 0x7fff_ffff;
622
623        let buffer = make_buffer(&data_set, false);
624        let terminfo = parse(buffer.as_slice()).unwrap();
625        assert_eq!(terminfo.booleans, collection!("bw", "xenl"));
626        assert_eq!(
627            terminfo.numbers,
628            collection!(
629                "cols" => 80,
630                "lines" => 25,
631                "pb" => 0x7fff_ffff,
632            )
633        );
634        assert_eq!(
635            terminfo.strings,
636            collection!(
637                "bel" => b"Hello".as_slice(),
638                "tbc" => b"World!",
639            )
640        );
641    }
642
643    #[test]
644    fn bad_magic() {
645        let data_set = DataSet::default();
646        let mut buffer = make_buffer(&data_set, false);
647        buffer[1] = 3;
648        let terminfo = parse(buffer.as_slice());
649        assert!(matches!(terminfo.unwrap_err(), Error::BadMagic));
650    }
651
652    #[test]
653    fn base_truncated() {
654        let data_set = DataSet::default();
655        let mut buffer = make_buffer(&data_set, false);
656        buffer.pop();
657        let terminfo = parse(buffer.as_slice());
658        assert!(matches!(terminfo.unwrap_err(), Error::UnsupportedFormat));
659    }
660
661    #[test]
662    fn base_unterminated_string() {
663        let data_set = DataSet::default();
664        let mut buffer = make_buffer(&data_set, false);
665        let buffer_size = buffer.len();
666        buffer[buffer_size - 1] = b'!';
667        let terminfo = parse(buffer.as_slice());
668        assert!(matches!(terminfo.unwrap_err(), Error::UnterminatedString));
669    }
670
671    #[test]
672    fn base_bad_string_offset() {
673        let data_set = DataSet {
674            term_name: b"123".to_vec(),
675            base_booleans: vec![],
676            base_numbers: vec![],
677            base_strings: vec![StringValue::from(b"Hello")],
678            ..Default::default()
679        };
680        let mut buffer = make_buffer(&data_set, false);
681        let mut offset = 6 * mem::size_of::<u16>();
682        offset += &data_set.term_name.len() + 1;
683        buffer[offset] = 0xff;
684        let terminfo = parse(buffer.as_slice());
685        assert!(matches!(terminfo.unwrap_err(), Error::UnsupportedFormat));
686    }
687
688    #[test]
689    fn base_bad_boolean_count() {
690        let data_set = DataSet::default();
691        let mut buffer = make_buffer(&data_set, false);
692        let offset = 2 * mem::size_of::<u16>();
693        let patch = u16::to_le_bytes(BOOL_NAMES.len() as u16 + 1);
694        buffer[offset] = patch[0];
695        buffer[offset + 1] = patch[1];
696        let terminfo = parse(buffer.as_slice());
697        assert!(matches!(terminfo.unwrap_err(), Error::UnsupportedFormat));
698    }
699
700    #[test]
701    fn base_bad_number_count() {
702        let data_set = DataSet::default();
703        let mut buffer = make_buffer(&data_set, false);
704        let offset = 3 * mem::size_of::<u16>();
705        let patch = u16::to_le_bytes(NUMBER_NAMES.len() as u16 + 1);
706        buffer[offset] = patch[0];
707        buffer[offset + 1] = patch[1];
708        let terminfo = parse(buffer.as_slice());
709        assert!(matches!(terminfo.unwrap_err(), Error::UnsupportedFormat));
710    }
711
712    #[test]
713    fn base_bad_string_count() {
714        let data_set = DataSet::default();
715        let mut buffer = make_buffer(&data_set, false);
716        let offset = 4 * mem::size_of::<u16>();
717        let patch = u16::to_le_bytes(STRING_NAMES.len() as u16 + 1);
718        buffer[offset] = patch[0];
719        buffer[offset + 1] = patch[1];
720        let terminfo = parse(buffer.as_slice());
721        assert!(matches!(terminfo.unwrap_err(), Error::UnsupportedFormat));
722    }
723
724    #[test]
725    fn base_bad_boolean() {
726        let data_set = DataSet {
727            base_booleans: vec![1, 0, 42],
728            ..Default::default()
729        };
730        let buffer = make_buffer(&data_set, false);
731        let terminfo = parse(buffer.as_slice());
732        assert!(matches!(
733            terminfo.unwrap_err(),
734            Error::InvalidBooleanValue(42)
735        ));
736    }
737
738    #[test]
739    fn extended_16_bit() {
740        let data_set = DataSet::default();
741        let buffer = make_buffer(&data_set, true);
742        let terminfo = parse(buffer.as_slice()).unwrap();
743        assert_eq!(
744            terminfo.booleans,
745            collection!("Curly", "Italic", "Semi-bold", "bw", "xenl")
746        );
747        assert_eq!(
748            terminfo.numbers,
749            collection!(
750                "Shades" => 1100,
751                "Variants" => 2200,
752                "cols" => 80,
753                "lines" => 25,
754                "pb" => 5,
755            )
756        );
757        assert_eq!(
758            terminfo.strings,
759            collection!(
760                "Colors" => b"A lot".as_slice(),
761                "Luminosity" => b"Positive",
762                "bel" => b"Hello",
763                "tbc" => b"World!",
764            )
765        );
766    }
767
768    #[test]
769    fn extended_32_bit() {
770        let mut data_set = DataSet {
771            number_type: NumberType::U32,
772            ..Default::default()
773        };
774        data_set.base_numbers[5] = 0x7fff_ffff;
775
776        let buffer = make_buffer(&data_set, true);
777        let terminfo = parse(buffer.as_slice()).unwrap();
778        assert_eq!(
779            terminfo.booleans,
780            collection!("Curly", "Italic", "Semi-bold", "bw", "xenl")
781        );
782        assert_eq!(
783            terminfo.numbers,
784            collection!(
785                "Shades" => 1100,
786                "Variants" => 2200,
787                "cols" => 80,
788                "lines" => 25,
789                "pb" => 0x7fff_ffff,
790            )
791        );
792        assert_eq!(
793            terminfo.strings,
794            collection!(
795                "Colors" => b"A lot".as_slice(),
796                "Luminosity" => b"Positive",
797                "bel" => b"Hello",
798                "tbc" => b"World!",
799            )
800        );
801    }
802
803    #[test]
804    fn extended_unterminated_string() {
805        let data_set = DataSet::default();
806        let mut buffer = make_buffer(&data_set, true);
807        let buffer_size = buffer.len();
808        buffer[buffer_size - 1] = b'!';
809        let terminfo = parse(buffer.as_slice());
810        assert!(matches!(terminfo.unwrap_err(), Error::UnterminatedString));
811    }
812
813    #[test]
814    fn extended_bad_boolean() {
815        let data_set = DataSet {
816            ext_booleans: vec![(b"True", 1), (b"False", 0), (b"Incorrect", 67)],
817            ..Default::default()
818        };
819        let buffer = make_buffer(&data_set, true);
820        let terminfo = parse(buffer.as_slice());
821        assert!(matches!(
822            terminfo.unwrap_err(),
823            Error::InvalidBooleanValue(67)
824        ));
825    }
826}