1 #![allow(unused_variables)]
3 /* PSPP - a program for statistical analysis.
4 * Copyright (C) 2023 Free Software Foundation, Inc.
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>. */
19 use anyhow::{anyhow, Result};
22 use hexplay::HexViewBuilder;
23 use num::{Float, Num};
24 use std::cmp::Ordering;
26 use std::io::prelude::*;
27 use std::io::BufReader;
28 use std::path::{Path, PathBuf};
30 use std::{fmt, num::FpCategory};
32 /// A utility to dissect SPSS system files.
33 #[derive(Parser, Debug)]
34 #[command(author, version, about, long_about = None)]
36 /// Maximum number of cases to print.
37 #[arg(long = "data", default_value_t = 0)]
41 #[arg(required = true)]
45 fn main() -> Result<()> {
46 let Args { max_cases, files } = Args::parse();
49 Dissector::new(file)?;
54 #[derive(Copy, Clone, Debug)]
60 #[derive(Copy, Clone, Debug)]
67 trait Parse<T, const N: usize> {
68 fn parse(self, bytes: [u8; N]) -> T;
70 impl Parse<u64, 8> for Endianness {
71 fn parse(self, bytes: [u8; 8]) -> u64 {
73 BigEndian => u64::from_be_bytes(bytes),
74 LittleEndian => u64::from_le_bytes(bytes),
78 impl Parse<u32, 4> for Endianness {
79 fn parse(self, bytes: [u8; 4]) -> u32 {
81 BigEndian => u32::from_be_bytes(bytes),
82 LittleEndian => u32::from_le_bytes(bytes),
86 impl Parse<u16, 2> for Endianness {
87 fn parse(self, bytes: [u8; 2]) -> u16 {
89 BigEndian => u16::from_be_bytes(bytes),
90 LittleEndian => u16::from_le_bytes(bytes),
94 impl Parse<u8, 1> for Endianness {
95 fn parse(self, bytes: [u8; 1]) -> u8 {
97 BigEndian => u8::from_be_bytes(bytes),
98 LittleEndian => u8::from_le_bytes(bytes),
102 impl Parse<i64, 8> for Endianness {
103 fn parse(self, bytes: [u8; 8]) -> i64 {
105 BigEndian => i64::from_be_bytes(bytes),
106 LittleEndian => i64::from_le_bytes(bytes),
110 impl Parse<i32, 4> for Endianness {
111 fn parse(self, bytes: [u8; 4]) -> i32 {
113 BigEndian => i32::from_be_bytes(bytes),
114 LittleEndian => i32::from_le_bytes(bytes),
118 impl Parse<i16, 2> for Endianness {
119 fn parse(self, bytes: [u8; 2]) -> i16 {
121 BigEndian => i16::from_be_bytes(bytes),
122 LittleEndian => i16::from_le_bytes(bytes),
126 impl Parse<i8, 1> for Endianness {
127 fn parse(self, bytes: [u8; 1]) -> i8 {
129 BigEndian => i8::from_be_bytes(bytes),
130 LittleEndian => i8::from_le_bytes(bytes),
134 impl Parse<f64, 8> for Endianness {
135 fn parse(self, bytes: [u8; 8]) -> f64 {
137 BigEndian => f64::from_be_bytes(bytes),
138 LittleEndian => f64::from_le_bytes(bytes),
143 fn read_bytes<const N: usize>(r: &mut BufReader<File>) -> Result<[u8; N]> {
144 let mut buf = [0; N];
145 r.read_exact(&mut buf)?;
149 fn read_vec(r: &mut BufReader<File>, n: usize) -> Result<Vec<u8>> {
150 let mut vec = vec![0; n];
151 r.read_exact(&mut vec)?;
156 fn read_swap(&mut self) -> Result<T>;
159 impl ReadSwap<u32> for Dissector {
160 fn read_swap(&mut self) -> Result<u32> {
161 Ok(self.endianness.parse(read_bytes(&mut self.r)?))
164 impl ReadSwap<u8> for Dissector {
165 fn read_swap(&mut self) -> Result<u8> {
166 Ok(self.endianness.parse(read_bytes(&mut self.r)?))
170 impl ReadSwap<i32> for Dissector {
171 fn read_swap(&mut self) -> Result<i32> {
172 Ok(self.endianness.parse(read_bytes(&mut self.r)?))
176 impl ReadSwap<f64> for Dissector {
177 fn read_swap(&mut self) -> Result<f64> {
178 Ok(self.endianness.parse(read_bytes(&mut self.r)?))
185 compression: Option<Compression>,
186 endianness: Endianness,
187 fp_format: Endianness,
189 n_variable_records: usize,
191 var_widths: Vec<i32>,
194 fn detect_endianness(layout_code: [u8; 4]) -> Option<Endianness> {
195 for endianness in [BigEndian, LittleEndian] {
196 match endianness.parse(layout_code) {
197 2 | 3 => return Some(endianness),
204 fn detect_fp_format(bias: [u8; 8]) -> Option<Endianness> {
205 for endianness in [BigEndian, LittleEndian] {
206 let value: f64 = endianness.parse(bias);
208 return Some(endianness);
214 fn trim_end(mut s: Vec<u8>, c: u8) -> Vec<u8> {
215 while s.last() == Some(&c) {
221 fn slice_trim_end(mut s: &[u8], c: u8) -> &[u8] {
222 while s.last() == Some(&c) {
223 s = s.split_last().unwrap().1;
228 fn format_name(type_: u32) -> &'static str {
271 fn round_up<T: Num + Copy>(x: T, y: T) -> T {
272 (x + (y - T::one())) / y * y
275 struct UntypedValue {
277 endianness: Endianness,
281 fn new(raw: [u8; 8], endianness: Endianness) -> UntypedValue {
282 UntypedValue { raw, endianness }
286 impl fmt::Display for UntypedValue {
287 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288 let numeric: f64 = self.endianness.parse(self.raw);
289 let n_printable = self
292 .take_while(|&&x| x == b' ' || x.is_ascii_graphic())
294 let printable_prefix = std::str::from_utf8(&self.raw[0..n_printable]).unwrap();
295 write!(f, "{numeric}/\"{printable_prefix}\"")
299 struct HexFloat<T: Float>(T);
301 impl<T: Float> fmt::Display for HexFloat<T> {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 let sign = if self.0.is_sign_negative() { "-" } else { "" };
304 match self.0.classify() {
305 FpCategory::Nan => return write!(f, "NaN"),
306 FpCategory::Infinite => return write!(f, "{sign}Infinity"),
307 FpCategory::Zero => return write!(f, "{sign}0.0"),
310 let (significand, mut exponent, _) = self.0.integer_decode();
311 let mut hex_sig = format!("{:x}", significand);
312 while hex_sig.ends_with('0') {
316 match hex_sig.len() {
317 0 => write!(f, "{sign}0.0"),
318 1 => write!(f, "{sign}0x{hex_sig}.0p{exponent}"),
322 hex_sig.chars().next().unwrap(),
324 exponent + 4 * (len as i16 - 1)
331 mod hex_float_tests {
337 assert_eq!(format!("{}", HexFloat(1.0)), "0x1.0p0");
338 assert_eq!(format!("{}", HexFloat(123.0)), "0x1.ecp6");
339 assert_eq!(format!("{}", HexFloat(1.0 / 16.0)), "0x1.0p-4");
340 assert_eq!(format!("{}", HexFloat(f64::infinity())), "Infinity");
341 assert_eq!(format!("{}", HexFloat(f64::neg_infinity())), "-Infinity");
342 assert_eq!(format!("{}", HexFloat(f64::nan())), "NaN");
343 assert_eq!(format!("{}", HexFloat(0.0)), "0.0");
344 assert_eq!(format!("{}", HexFloat(f64::neg_zero())), "-0.0");
349 fn new<P: AsRef<Path>>(filename: P) -> Result<Dissector> {
350 let mut r = BufReader::new(File::open(&filename)?);
351 let filename = filename.as_ref().to_string_lossy().into_owned();
352 let rec_type: [u8; 4] = read_bytes(&mut r)?;
353 let zmagic = match &rec_type {
356 _ => Err(anyhow!("This is not an SPSS system file."))?,
359 let eye_catcher: [u8; 60] = read_bytes(&mut r)?;
360 let layout_code: [u8; 4] = read_bytes(&mut r)?;
361 let endianness = detect_endianness(layout_code)
362 .ok_or_else(|| anyhow!("This is not an SPSS system file."))?;
363 let layout_code: u32 = endianness.parse(layout_code);
364 let _nominal_case_size: [u8; 4] = read_bytes(&mut r)?;
365 let compressed: u32 = endianness.parse(read_bytes(&mut r)?);
366 let compression = match (zmagic, compressed) {
368 (false, 1) => Some(Compression::Simple),
369 (true, 2) => Some(Compression::ZLib),
371 "{} file header has invalid compression value {compressed}.",
372 if zmagic { "ZSAV" } else { "SAV" }
376 let weight_index: u32 = endianness.parse(read_bytes(&mut r)?);
377 let n_cases: u32 = endianness.parse(read_bytes(&mut r)?);
379 let bias: [u8; 8] = read_bytes(&mut r)?;
380 let fp_format = detect_fp_format(bias)
381 .unwrap_or_else(|| { eprintln!("Compression bias is not the usual value of 100, or system file uses unrecognized floating-point format."); endianness });
382 let bias: f64 = fp_format.parse(bias);
384 let mut d = Dissector {
391 n_variable_records: 0,
393 var_widths: Vec::new(),
396 let creation_date: [u8; 9] = read_bytes(&mut d.r)?;
397 let creation_time: [u8; 8] = read_bytes(&mut d.r)?;
398 let file_label: [u8; 64] = read_bytes(&mut d.r)?;
399 let file_label = trim_end(Vec::from(file_label), b' ');
402 println!("File header record:");
406 String::from_utf8_lossy(&eye_catcher)
408 println!("{:>17}: {}", "Layout code", layout_code);
414 None => "no compression",
415 Some(Compression::Simple) => "simple compression",
416 Some(Compression::ZLib) => "ZLIB compression",
419 println!("{:>17}: {}", "Weight index", weight_index);
420 println!("{:>17}: {}", "Number of cases", n_cases);
421 println!("{:>17}: {}", "Compression bias", bias);
425 String::from_utf8_lossy(&creation_date)
430 String::from_utf8_lossy(&creation_time)
435 String::from_utf8_lossy(&file_label)
439 let rec_type: u32 = d.read_swap()?;
441 2 => d.read_variable_record()?,
442 3 => d.read_value_label_record()?,
443 4 => Err(anyhow!("Misplaced type 4 record."))?,
444 6 => d.read_document_record()?,
445 7 => d.read_extension_record()?,
447 _ => Err(anyhow!("Unrecognized record type {rec_type}."))?,
451 let pos = d.r.stream_position()?;
453 "{:08x}: end-of-dictionary record (first byte of data at {:0x})",
461 fn read_extension_record(&mut self) -> Result<()> {
462 let offset = self.r.stream_position()?;
463 let subtype: u32 = self.read_swap()?;
464 let size: u32 = self.read_swap()?;
465 let count: u32 = self.read_swap()?;
466 println!("{offset:08x}: Record 7, subtype {subtype}, size={size}, count={count}");
468 3 => self.read_machine_integer_info(size, count),
469 4 => self.read_machine_float_info(size, count),
470 5 => self.read_variable_sets(size, count),
472 // DATE variable information. We don't use it yet, but we should.
475 7 | 19 => self.read_mrsets(size, count),
476 10 => self.read_extra_product_info(size, count),
477 11 => self.read_display_parameters(size, count),
478 _ => self.read_unknown_extension(subtype, size, count),
482 fn warn(&mut self, s: String) -> Result<()> {
484 "\"{}\" near offset 0x{:08x}: {s}",
486 self.r.stream_position()?
491 fn skip_bytes(&mut self, mut n: u64) -> Result<()> {
492 let mut buf = [0; 1024];
494 let chunk = u64::min(n, buf.len() as u64);
495 self.r.read_exact(&mut buf[0..chunk as usize])?;
501 fn read_unknown_extension(&mut self, subtype: u32, size: u32, count: u32) -> Result<()> {
502 self.warn(format!("Unrecognized record type 7, subtype {subtype}."))?;
503 if size == 0 || count > 65536 / size {
504 self.skip_bytes(size as u64 * count as u64)?;
505 } else if size != 1 {
508 let vec = read_vec(&mut self.r, size as usize)?;
511 HexViewBuilder::new(&vec).address_offset(offset).finish()
513 offset += size as usize;
519 fn read_variable_record(&mut self) -> Result<()> {
520 self.n_variable_records += 1;
522 "{:08x}: variable record {}",
523 self.r.stream_position()?,
524 self.n_variable_records
526 let width: i32 = self.read_swap()?;
527 let has_variable_label: u32 = self.read_swap()?;
528 let missing_value_code: i32 = self.read_swap()?;
529 let print_format: u32 = self.read_swap()?;
530 let write_format: u32 = self.read_swap()?;
531 let name: [u8; 8] = read_bytes(&mut self.r)?;
532 let name: Vec<u8> = trim_end(Vec::from(name), b'\0');
535 self.n_variables += 1;
537 self.var_widths.push(width);
540 "\tWidth: {width} ({})",
542 _ if width > 0 => "string",
543 _ if width == 0 => "numeric",
544 _ => "long string continuation record",
548 println!("\tVariable label: {has_variable_label}");
550 "\tMissing values code: {missing_value_code} ({})",
551 match missing_value_code {
552 0 => "no missing values",
553 1 => "one missing value",
554 2 => "two missing values",
555 3 => "three missing values",
556 -2 => "one missing value range",
557 -3 => "one missing value, one range",
561 for (which, format) in [("Print", print_format), ("Worite", write_format)] {
562 let type_ = format_name(format >> 16);
563 let w = (format >> 8) & 0xff;
564 let d = format & 0xff;
565 println!("\t{which} format: {format:06x} ({type_}{w}.{d})");
567 println!("\tName: {}", String::from_utf8_lossy(&name));
569 // Read variable label.
570 match has_variable_label {
573 let offset = self.r.stream_position()?;
574 let len: u32 = self.read_swap()?;
575 let read_len = len.min(65535) as usize;
576 let label = read_vec(&mut self.r, read_len)?;
578 "\t{offset:08x} Variable label: \"{}\"",
579 String::from_utf8_lossy(&label)
582 self.skip_bytes((round_up(len, 4) - len).into())?;
584 _ => Err(anyhow!("Variable label indicator field is not 0 or 1."))?,
587 // Read missing values.
588 if missing_value_code != 0 {
589 print!("\t{:08x} Missing values:", self.r.stream_position()?);
590 match width.cmp(&0) {
592 let (has_range, n_individual) = match missing_value_code {
595 1 | 2 | 3 => (false, missing_value_code),
597 "Numeric missing value indicator field is not -3, -2, 0, 1, 2, or 3."
601 let low: f64 = self.read_swap()?;
602 let high: f64 = self.read_swap()?;
603 print!(" {low}...{high}");
605 for _ in 0..n_individual {
606 let value: f64 = self.read_swap()?;
610 Ordering::Greater => {
611 if !(0..=3).contains(&missing_value_code) {
613 "String missing value indicator field is not 0, 1, 2, or 3."
616 for _ in 0..missing_value_code {
617 let string: [u8; 8] = read_bytes(&mut self.r)?;
618 let string: Vec<u8> = trim_end(Vec::from(string), b'\0');
619 println!(" {}", String::from_utf8_lossy(&string));
622 Ordering::Less => (),
630 fn read_value_label_record(&mut self) -> Result<()> {
631 println!("{:08x}: value labels record", self.r.stream_position()?);
634 let n_labels: u32 = self.read_swap()?;
635 for _ in 0..n_labels {
636 let raw: [u8; 8] = read_bytes(&mut self.r)?;
637 let value = UntypedValue::new(raw, self.fp_format);
638 let label_len: u8 = self.read_swap()?;
639 let padded_len = round_up(label_len as usize + 1, 8);
641 let mut label = read_vec(&mut self.r, padded_len)?;
642 label.truncate(label_len as usize);
643 let label = String::from_utf8_lossy(&label);
645 println!("\t{value}: {label}");
648 // Read the type-4 record with the corresponding variable indexes.
649 let rec_type: u32 = self.read_swap()?;
652 "Variable index record (type 4) does not immediately \
653 follow value label record (type 3) as it should."
657 println!("\t{:08x}: apply to variables", self.r.stream_position()?);
658 let n_vars: u32 = self.read_swap()?;
660 let index: u32 = self.read_swap()?;
668 fn read_document_record(&mut self) -> Result<()> {
669 println!("{:08x}: document record", self.r.stream_position()?);
670 let n_lines: u32 = self.read_swap()?;
671 println!("\t{n_lines} lines of documents");
673 for i in 0..n_lines {
674 print!("\t{:08x}: ", self.r.stream_position()?);
675 let line: [u8; 64] = read_bytes(&mut self.r)?;
676 let line = trim_end(Vec::from(line), b' ');
677 println!("line {i}: \"{}\"", String::from_utf8_lossy(&line));
682 fn read_machine_integer_info(&mut self, size: u32, count: u32) -> Result<()> {
683 let offset = self.r.stream_position()?;
684 let version_major: u32 = self.read_swap()?;
685 let version_minor: u32 = self.read_swap()?;
686 let version_revision: u32 = self.read_swap()?;
687 let machine_code: u32 = self.read_swap()?;
688 let float_representation: u32 = self.read_swap()?;
689 let compression_code: u32 = self.read_swap()?;
690 let integer_representation: u32 = self.read_swap()?;
691 let character_code: u32 = self.read_swap()?;
693 println!("{offset:08x}: machine integer info");
694 if size != 4 || count != 8 {
696 "Bad size ({size}) or count ({count}) field on record type 7, subtype 3"
699 println!("\tVersion: {version_major}.{version_minor}.{version_revision}");
700 println!("\tMachine code: {machine_code}");
702 "\tFloating point representation: {float_representation} ({})",
703 match float_representation {
710 println!("\tCompression code: {compression_code}");
712 "\tEndianness: {integer_representation} ({})",
713 match integer_representation {
719 println!("\tCharacter code: {character_code}");
723 fn read_machine_float_info(&mut self, size: u32, count: u32) -> Result<()> {
724 let offset = self.r.stream_position()?;
725 let sysmis: f64 = self.read_swap()?;
726 let highest: f64 = self.read_swap()?;
727 let lowest: f64 = self.read_swap()?;
729 println!("{offset:08x}: machine float info");
730 if size != 4 || count != 8 {
732 "Bad size ({size}) or count ({count}) field on extension 4."
736 println!("\tsysmis: {sysmis} ({})", HexFloat(sysmis));
737 println!("\thighest: {highest} ({})", HexFloat(highest));
738 println!("\tlowest: {lowest} ({})", HexFloat(lowest));
742 fn read_variable_sets(&mut self, size: u32, count: u32) -> Result<()> {
743 println!("{:08x}: variable sets", self.r.stream_position()?);
744 let mut text = self.open_text_record(size, count)?;
746 while text.match_byte(b'\n') {
749 let set = match text.tokenize(b'=') {
750 Some(set) => String::from_utf8_lossy(set).into_owned(),
754 // Always present even for an empty set.
755 text.match_byte(b' ');
757 match text.tokenize(b'\n') {
758 None => println!("\tset \"{set}\" is empty"),
761 "\tset \"{set}\" contains \"{}\"",
762 String::from_utf8_lossy(variables).trim_end_matches('\r')
770 // Read record type 7, subtype 7.
771 fn read_mrsets(&mut self, size: u32, count: u32) -> Result<()> {
772 print!("{:08x}: multiple response sets", self.r.stream_position()?);
773 let mut text = self.open_text_record(size, count)?;
775 #[derive(PartialEq, Eq)]
781 while text.match_byte(b'\n') {}
782 let Some(name) = text.tokenize(b'=') else {
786 let (mrset, cat_label_from_counted_values, label_from_var_label) = if text
789 if !text.match_byte(b' ') {
791 "missing space following 'C' at offset {} in mrsets record",
795 (MrSet::MC, false, false)
796 } else if text.match_byte(b'D') {
797 (MrSet::MD, false, false)
798 } else if text.match_byte(b'E') {
799 if !text.match_byte(b' ') {
801 "missing space following 'E' at offset {} in mrsets record",
807 let Some(number) = text.tokenize(b' ') else {
809 "Missing label source value following `E' at offset {}u in MRSETS record",
814 let label_from_var_label = if number == b"11" {
816 } else if number == b"1" {
819 Err(anyhow!("Unexpected label source value `{}' following `E' at offset {pos} in MRSETS record", String::from_utf8_lossy(number)))?
821 (MrSet::MD, true, label_from_var_label)
824 "missing `C', `D', or `E' at offset {} in mrsets record",
829 let counted_value = if mrset == MrSet::MD {
830 Some(text.parse_counted_string()?)
833 let label = text.parse_counted_string()?;
835 let variables = text.tokenize(b'\n');
837 print!("\t\"{}\": multiple {} set",
838 String::from_utf8_lossy(name),
839 if mrset == MrSet::MC { "category" } else { "dichotomy" });
845 fn read_extra_product_info(&mut self, size: u32, count: u32) -> Result<()> {
846 print!("{:08x}: extra product info", self.r.stream_position()?);
847 let text = self.open_text_record(size, count)?;
848 print_string(&text.buffer);
852 fn read_display_parameters(&mut self, size: u32, count: u32) -> Result<()> {
854 "{:08x}: variable display parameters",
855 self.r.stream_position()?
858 Err(anyhow!("Bad size ({size}) on extension 11."))?;
860 let n_vars = self.n_variables;
861 let includes_width = if count as usize == 3 * n_vars {
863 } else if count as usize == 2 * n_vars {
867 "Extension 11 has bad count {count} (for {n_vars} variables)."
872 let measure: u32 = self.read_swap()?;
874 "\tVar #{i}: measure={measure} ({})",
884 let width: u32 = self.read_swap()?;
885 print!(", width={width}");
888 let align: u32 = self.read_swap()?;
890 ", align={align} ({})",
902 fn open_text_record(&mut self, size: u32, count: u32) -> Result<TextRecord> {
903 let n_bytes = match u32::checked_mul(size, count) {
905 None => Err(anyhow!("Extension record too large."))?,
907 Ok(TextRecord::new(read_vec(&mut self.r, n_bytes as usize)?))
911 fn print_string(s: &[u8]) {
912 if s.contains(&b'\0') {
913 println!("{}", HexView::new(s));
917 b'\\' => print!("\\\\"),
919 c if (b' '..=b'~').contains(&c) => print!("{}", c as char),
920 c => print!("\\{:2x}", c),
932 fn new(buffer: Vec<u8>) -> TextRecord {
933 TextRecord { buffer, pos: 0 }
936 fn tokenize(&mut self, delimiter: u8) -> Option<&[u8]> {
937 let start = self.pos;
938 while self.pos < self.buffer.len()
939 && self.buffer[self.pos] != delimiter
940 && self.buffer[self.pos] != 0
944 if start == self.pos {
947 Some(&self.buffer[start..self.pos])
951 fn match_byte(&mut self, c: u8) -> bool {
952 if self.pos < self.buffer.len() && self.buffer[self.pos] == c {
960 fn parse_usize(&mut self) -> Result<usize> {
961 let n_digits = self.buffer[self.pos..]
963 .take_while(|c| c.is_ascii_digit())
966 Err(anyhow!("expecting digit at offset {} in record", self.pos))?;
968 let start = self.pos;
969 self.pos += n_digits;
971 let digits = str::from_utf8(&self.buffer[start..end]).unwrap();
972 let Ok(number) = digits.parse::<usize>() else {
974 "expecting number in [0,{}] at offset {} in record",
983 fn get_n_bytes(&mut self, n: usize) -> Option<(usize, usize)> {
984 let start = self.pos;
985 let Some(end) = start.checked_add(n) else {
992 fn parse_counted_string(&mut self) -> Result<&[u8]> {
993 let length = self.parse_usize()?;
994 if !self.match_byte(b' ') {
995 Err(anyhow!("expecting space at offset {} in record", self.pos))?;
998 let Some((start, end)) = self.get_n_bytes(length) else {
999 Err(anyhow!("{length}-byte string starting at offset {} exceeds record length {}",
1000 self.pos, self.buffer.len()))?
1002 if !self.match_byte(b' ') {
1004 "expecting space at offset {} following {}-byte string",
1009 Ok(&self.buffer[start..end])