1use 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 Magic1 = 0x011a,
76 Magic2 = 0x021e,
78}
79
80#[derive(thiserror::Error, Debug)]
82#[non_exhaustive]
83pub enum Error {
84 #[error("Unknown magic number")]
86 BadMagic,
87 #[error("String without final NUL")]
89 UnterminatedString,
90 #[error("Unsupported terminfo format")]
92 UnsupportedFormat,
93 #[error("I/O error")]
95 IO(#[from] std::io::Error),
96 #[error("Invalid UTF-8 string")]
98 Utf8(#[from] std::str::Utf8Error),
99}
100
101pub 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(_)) => {} 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
149fn 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
157fn 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#[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 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 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 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)); buffer.extend_from_slice(&u16::to_le_bytes(string_size));
448
449 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}