work
[pspp] / rust / src / raw.rs
index 0ae4f53b8c6a3094b246f1b1e67fa0c26dc23e62..a8c7c858e9e0a766243226066023acd6bc1aead4 100644 (file)
@@ -1,6 +1,6 @@
 use crate::endian::{Endian, Parse, ToBytes};
-use crate::{CategoryLabels, Compression};
 
+use encoding_rs::mem::decode_latin1;
 use flate2::read::ZlibDecoder;
 use num::Integer;
 use std::borrow::Cow;
@@ -36,7 +36,7 @@ pub enum Error {
     BadVariableWidth { offset: u64, width: i32 },
 
     #[error("Document record at offset {offset:#x} has document line count ({n}) greater than the maximum number {max}.")]
-    BadDocumentLength { offset: u64, n: u32, max: u32 },
+    BadDocumentLength { offset: u64, n: usize, max: usize },
 
     #[error("At offset {offset:#x}, unrecognized record type {rec_type}.")]
     BadRecordType { offset: u64, rec_type: u32 },
@@ -96,13 +96,27 @@ pub enum Error {
     },
 
     #[error("At offset {offset:#x}, {record} has bad size {size} bytes instead of the expected {expected_size}.")]
-    BadRecordSize { offset: u64, record: String, size: u32, expected_size: u32 },
+    BadRecordSize {
+        offset: u64,
+        record: String,
+        size: u32,
+        expected_size: u32,
+    },
 
     #[error("At offset {offset:#x}, {record} has bad count {count} instead of the expected {expected_count}.")]
-    BadRecordCount { offset: u64, record: String, count: u32, expected_count: u32 },
+    BadRecordCount {
+        offset: u64,
+        record: String,
+        count: u32,
+        expected_count: u32,
+    },
 
     #[error("In long string missing values record starting at offset {record_offset:#x}, value length at offset {offset:#x} is {value_len} instead of the expected 8.")]
-    BadLongMissingValueLength { record_offset: u64, offset: u64, value_len: u32 },
+    BadLongMissingValueLength {
+        record_offset: u64,
+        offset: u64,
+        value_len: u32,
+    },
 
     #[error("The encoding record at offset {offset:#x} contains an encoding name that is not valid UTF-8.")]
     BadEncodingName { offset: u64 },
@@ -113,25 +127,24 @@ pub enum Error {
 
 #[derive(Clone, Debug)]
 pub enum Record {
-    Header(Header),
-    Variable(Variable),
-    ValueLabel(ValueLabel),
-    VarIndexes(VarIndexes),
-    Document(Document),
-    IntegerInfo(IntegerInfo),
-    FloatInfo(FloatInfo),
-    VariableSets(UnencodedString),
+    Header(HeaderRecord),
+    Variable(VariableRecord),
+    ValueLabel(ValueLabelRecord),
+    VarIndexes(VarIndexRecord),
+    Document(DocumentRecord),
+    IntegerInfo(IntegerInfoRecord),
+    FloatInfo(FloatInfoRecord),
+    VariableSets(TextRecord),
     VarDisplay(VarDisplayRecord),
     MultipleResponse(MultipleResponseRecord),
     LongStringValueLabels(LongStringValueLabelRecord),
     Encoding(EncodingRecord),
     NumberOfCases(NumberOfCasesRecord),
-    ProductInfo(UnencodedString),
-    LongNames(UnencodedString),
-    LongStrings(UnencodedString),
-    FileAttributes(UnencodedString),
-    VariableAttributes(UnencodedString),
-    TextExtension(TextExtension),
+    ProductInfo(TextRecord),
+    LongNames(TextRecord),
+    VeryLongStrings(TextRecord),
+    FileAttributes(TextRecord),
+    VariableAttributes(TextRecord),
     OtherExtension(Extension),
     EndOfHeaders(u32),
     ZHeader(ZHeader),
@@ -143,10 +156,10 @@ impl Record {
     fn read<R: Read + Seek>(reader: &mut R, endian: Endian) -> Result<Record, Error> {
         let rec_type: u32 = endian.parse(read_bytes(reader)?);
         match rec_type {
-            2 => Ok(Record::Variable(Variable::read(reader, endian)?)),
-            3 => Ok(Record::ValueLabel(ValueLabel::read(reader, endian)?)),
-            4 => Ok(Record::VarIndexes(VarIndexes::read(reader, endian)?)),
-            6 => Ok(Record::Document(Document::read(reader, endian)?)),
+            2 => Ok(Record::Variable(VariableRecord::read(reader, endian)?)),
+            3 => Ok(Record::ValueLabel(ValueLabelRecord::read(reader, endian)?)),
+            4 => Ok(Record::VarIndexes(VarIndexRecord::read(reader, endian)?)),
+            6 => Ok(Record::Document(DocumentRecord::read(reader, endian)?)),
             7 => Ok(Extension::read(reader, endian)?),
             999 => Ok(Record::EndOfHeaders(endian.parse(read_bytes(reader)?))),
             _ => Err(Error::BadRecordType {
@@ -157,43 +170,26 @@ impl Record {
     }
 }
 
-pub struct FallbackEncoding<'a>(&'a [u8]);
-
-fn fallback_encode<'a>(s: &'a [u8]) -> Cow<'a, str> {
-    if let Ok(s) = from_utf8(s) {
-        s.into()
-    } else {
-        let s: String = s.iter().map(|c| char::from(*c)).collect();
-        s.into()
-    }
+// If `s` is valid UTF-8, returns it decoded as UTF-8, otherwise returns it
+// decoded as Latin-1 (actually bytes interpreted as Unicode code points).
+fn default_decode<'a>(s: &'a [u8]) -> Cow<'a, str> {
+    from_utf8(s).map_or_else(|_| decode_latin1(s), Cow::from)
 }
 
-impl<'a> Debug for FallbackEncoding<'a> {
-    fn fmt(&self, f: &mut Formatter) -> FmtResult {
-        if let Ok(s) = from_utf8(self.0) {
-            let s = s.trim_end();
-            write!(f, "\"{s}\"")
-        } else {
-            let s: String = self
-                .0
-                .iter()
-                .map(|c| char::from(*c).escape_default())
-                .flatten()
-                .collect();
-            let s = s.trim_end();
-            write!(f, "\"{s}\"")
-        }
-    }
+#[derive(Copy, Clone, Debug)]
+pub enum Compression {
+    Simple,
+    ZLib,
 }
 
 #[derive(Clone)]
-pub struct Header {
+pub struct HeaderRecord {
     /// Magic number.
     pub magic: Magic,
 
     /// Eye-catcher string, product name, in the file's encoding.  Padded
     /// on the right with spaces.
-    pub eye_catcher: [u8; 60],
+    pub eye_catcher: UnencodedStr<60>,
 
     /// Layout code, normally either 2 or 3.
     pub layout_code: u32,
@@ -205,7 +201,7 @@ pub struct Header {
     /// Compression type, if any,
     pub compression: Option<Compression>,
 
-    /// 0-based variable index of the weight variable, or `None` if the file is
+    /// 1-based variable index of the weight variable, or `None` if the file is
     /// unweighted.
     pub weight_index: Option<u32>,
 
@@ -216,48 +212,48 @@ pub struct Header {
     pub bias: f64,
 
     /// `dd mmm yy` in the file's encoding.
-    pub creation_date: [u8; 9],
+    pub creation_date: UnencodedStr<9>,
 
     /// `HH:MM:SS` in the file's encoding.
-    pub creation_time: [u8; 8],
+    pub creation_time: UnencodedStr<8>,
 
     /// File label, in the file's encoding.  Padded on the right with spaces.
-    pub file_label: [u8; 64],
+    pub file_label: UnencodedStr<64>,
 
     /// Endianness of the data in the file header.
     pub endian: Endian,
 }
 
-impl Header {
+impl HeaderRecord {
     fn debug_field<T: Debug>(&self, f: &mut Formatter, name: &str, value: T) -> FmtResult {
         writeln!(f, "{name:>17}: {:?}", value)
     }
 }
 
-impl Debug for Header {
+impl Debug for HeaderRecord {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
         writeln!(f, "File header record:")?;
         self.debug_field(f, "Magic", self.magic)?;
-        self.debug_field(f, "Product name", FallbackEncoding(&self.eye_catcher))?;
+        self.debug_field(f, "Product name", &self.eye_catcher)?;
         self.debug_field(f, "Layout code", self.layout_code)?;
         self.debug_field(f, "Nominal case size", self.nominal_case_size)?;
         self.debug_field(f, "Compression", self.compression)?;
         self.debug_field(f, "Weight index", self.weight_index)?;
         self.debug_field(f, "Number of cases", self.n_cases)?;
         self.debug_field(f, "Compression bias", self.bias)?;
-        self.debug_field(f, "Creation date", FallbackEncoding(&self.creation_date))?;
-        self.debug_field(f, "Creation time", FallbackEncoding(&self.creation_time))?;
-        self.debug_field(f, "File label", FallbackEncoding(&self.file_label))?;
+        self.debug_field(f, "Creation date", &self.creation_date)?;
+        self.debug_field(f, "Creation time", &self.creation_time)?;
+        self.debug_field(f, "File label", &self.file_label)?;
         self.debug_field(f, "Endianness", self.endian)
     }
 }
 
-impl Header {
-    fn read<R: Read>(r: &mut R) -> Result<Header, Error> {
+impl HeaderRecord {
+    fn read<R: Read>(r: &mut R) -> Result<HeaderRecord, Error> {
         let magic: [u8; 4] = read_bytes(r)?;
         let magic: Magic = magic.try_into().map_err(|_| Error::NotASystemFile)?;
 
-        let eye_catcher: [u8; 60] = read_bytes(r)?;
+        let eye_catcher = UnencodedStr::<60>(read_bytes(r)?);
         let layout_code: [u8; 4] = read_bytes(r)?;
         let endian = Endian::identify_u32(2, layout_code)
             .or_else(|| Endian::identify_u32(2, layout_code))
@@ -278,19 +274,19 @@ impl Header {
         };
 
         let weight_index: u32 = endian.parse(read_bytes(r)?);
-        let weight_index = (weight_index > 0).then(|| weight_index - 1);
+        let weight_index = (weight_index > 0).then_some(weight_index);
 
         let n_cases: u32 = endian.parse(read_bytes(r)?);
         let n_cases = (n_cases < i32::MAX as u32 / 2).then_some(n_cases);
 
         let bias: f64 = endian.parse(read_bytes(r)?);
 
-        let creation_date: [u8; 9] = read_bytes(r)?;
-        let creation_time: [u8; 8] = read_bytes(r)?;
-        let file_label: [u8; 64] = read_bytes(r)?;
+        let creation_date = UnencodedStr::<9>(read_bytes(r)?);
+        let creation_time = UnencodedStr::<8>(read_bytes(r)?);
+        let file_label = UnencodedStr::<64>(read_bytes(r)?);
         let _: [u8; 3] = read_bytes(r)?;
 
-        Ok(Header {
+        Ok(HeaderRecord {
             magic,
             layout_code,
             nominal_case_size,
@@ -346,16 +342,16 @@ impl TryFrom<[u8; 4]> for Magic {
     }
 }
 
-#[derive(Copy, Clone, PartialEq, Eq, Hash)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub enum VarType {
-    Number,
+    Numeric,
     String,
 }
 
 impl VarType {
     fn from_width(width: i32) -> VarType {
         match width {
-            0 => VarType::Number,
+            0 => VarType::Numeric,
             _ => VarType::String,
         }
     }
@@ -363,8 +359,8 @@ impl VarType {
 
 mod state {
     use super::{
-        Compression, Error, Header, Record, Value, VarType, Variable, ZHeader, ZTrailer,
-        ZlibDecodeMultiple,
+        Compression, Error, HeaderRecord, Record, Value, VarType, VariableRecord, ZHeader,
+        ZTrailer, ZlibDecodeMultiple,
     };
     use crate::endian::Endian;
     use std::{
@@ -395,7 +391,7 @@ mod state {
 
     impl<R: Read + Seek + 'static> State for Start<R> {
         fn read(mut self: Box<Self>) -> Result<Option<(Record, Box<dyn State>)>, Error> {
-            let header = Header::read(&mut self.reader)?;
+            let header = HeaderRecord::read(&mut self.reader)?;
             let next_state = Headers(CommonState {
                 reader: self.reader,
                 endian: header.endian,
@@ -413,7 +409,7 @@ mod state {
         fn read(mut self: Box<Self>) -> Result<Option<(Record, Box<dyn State>)>, Error> {
             let record = Record::read(&mut self.0.reader, self.0.endian)?;
             match record {
-                Record::Variable(Variable { width, .. }) => {
+                Record::Variable(VariableRecord { width, .. }) => {
                     self.0.var_types.push(VarType::from_width(width));
                 }
                 Record::EndOfHeaders(_) => {
@@ -507,7 +503,7 @@ mod state {
 #[derive(Copy, Clone)]
 pub enum Value {
     Number(Option<f64>),
-    String([u8; 8]),
+    String(UnencodedStr<8>),
 }
 
 impl Debug for Value {
@@ -515,21 +511,25 @@ impl Debug for Value {
         match self {
             Value::Number(Some(number)) => write!(f, "{number:?}"),
             Value::Number(None) => write!(f, "SYSMIS"),
-            Value::String(bytes) => write!(f, "{:?}", FallbackEncoding(bytes)),
+            Value::String(bytes) => write!(f, "{:?}", bytes),
         }
     }
 }
 
 impl Value {
     fn read<R: Read>(r: &mut R, var_type: VarType, endian: Endian) -> Result<Value, IoError> {
-        Ok(Self::from_raw(var_type, read_bytes(r)?, endian))
+        Ok(Self::from_raw(
+            UntypedValue(read_bytes(r)?),
+            var_type,
+            endian,
+        ))
     }
 
-    pub fn from_raw(var_type: VarType, raw: [u8; 8], endian: Endian) -> Value {
+    pub fn from_raw(raw: UntypedValue, var_type: VarType, endian: Endian) -> Value {
         match var_type {
-            VarType::String => Value::String(raw),
-            VarType::Number => {
-                let number: f64 = endian.parse(raw);
+            VarType::String => Value::String(UnencodedStr(raw.0)),
+            VarType::Numeric => {
+                let number: f64 = endian.parse(raw.0);
                 Value::Number((number != -f64::MAX).then_some(number))
             }
         }
@@ -555,7 +555,7 @@ impl Value {
                     });
                 }
             };
-            values.push(Value::from_raw(var_type, raw, endian));
+            values.push(Value::from_raw(UntypedValue(raw), var_type, endian));
         }
         Ok(Some(values))
     }
@@ -589,9 +589,9 @@ impl Value {
                 match code {
                     0 => (),
                     1..=251 => match var_type {
-                        VarType::Number => break Value::Number(Some(code as f64 - bias)),
+                        VarType::Numeric => break Value::Number(Some(code as f64 - bias)),
                         VarType::String => {
-                            break Value::String(endian.to_bytes(code as f64 - bias))
+                            break Value::String(UnencodedStr(endian.to_bytes(code as f64 - bias)))
                         }
                     },
                     252 => {
@@ -605,10 +605,12 @@ impl Value {
                             });
                         }
                     }
-                    253 => break Value::from_raw(var_type, read_bytes(reader)?, endian),
+                    253 => {
+                        break Value::from_raw(UntypedValue(read_bytes(reader)?), var_type, endian)
+                    }
                     254 => match var_type {
-                        VarType::String => break Value::String(*b"        "), // XXX EBCDIC
-                        VarType::Number => {
+                        VarType::String => break Value::String(UnencodedStr(*b"        ")), // XXX EBCDIC
+                        VarType::Numeric => {
                             return Err(Error::CompressedStringExpected {
                                 offset: case_start,
                                 case_ofs: reader.stream_position()? - case_start,
@@ -616,7 +618,7 @@ impl Value {
                         }
                     },
                     255 => match var_type {
-                        VarType::Number => break Value::Number(None),
+                        VarType::Numeric => break Value::Number(None),
                         VarType::String => {
                             return Err(Error::CompressedNumberExpected {
                                 offset: case_start,
@@ -716,9 +718,9 @@ impl Iterator for Reader {
 impl FusedIterator for Reader {}
 
 #[derive(Copy, Clone, PartialEq, Eq, Hash)]
-pub struct Format(pub u32);
+pub struct Spec(pub u32);
 
-impl Debug for Format {
+impl Debug for Spec {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
         let type_ = format_name(self.0 >> 16);
         let w = (self.0 >> 8) & 0xff;
@@ -766,8 +768,9 @@ fn format_name(type_: u32) -> Cow<'static, str> {
         39 => "SDATE",
         40 => "MTIME",
         41 => "YMDHMS",
-        _ => return format!("<unknown format {type_}>").into()
-    }.into()
+        _ => return format!("<unknown format {type_}>").into(),
+    }
+    .into()
 }
 
 #[derive(Clone)]
@@ -841,7 +844,7 @@ impl MissingValues {
 }
 
 #[derive(Clone)]
-pub struct Variable {
+pub struct VariableRecord {
     /// Offset from the start of the file to the start of the record.
     pub offset: u64,
 
@@ -849,13 +852,13 @@ pub struct Variable {
     pub width: i32,
 
     /// Variable name, padded on the right with spaces.
-    pub name: [u8; 8],
+    pub name: UnencodedStr<8>,
 
     /// Print format.
-    pub print_format: Format,
+    pub print_format: Spec,
 
     /// Write format.
-    pub write_format: Format,
+    pub write_format: Spec,
 
     /// Missing values.
     pub missing_values: MissingValues,
@@ -864,7 +867,7 @@ pub struct Variable {
     pub label: Option<UnencodedString>,
 }
 
-impl Debug for Variable {
+impl Debug for VariableRecord {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
         writeln!(
             f,
@@ -880,21 +883,21 @@ impl Debug for Variable {
         )?;
         writeln!(f, "Print format: {:?}", self.print_format)?;
         writeln!(f, "Write format: {:?}", self.write_format)?;
-        writeln!(f, "Name: {:?}", FallbackEncoding(&self.name))?;
+        writeln!(f, "Name: {:?}", &self.name)?;
         writeln!(f, "Variable label: {:?}", self.label)?;
         writeln!(f, "Missing values: {:?}", self.missing_values)
     }
 }
 
-impl Variable {
-    fn read<R: Read + Seek>(r: &mut R, endian: Endian) -> Result<Variable, Error> {
+impl VariableRecord {
+    fn read<R: Read + Seek>(r: &mut R, endian: Endian) -> Result<VariableRecord, Error> {
         let offset = r.stream_position()?;
         let width: i32 = endian.parse(read_bytes(r)?);
         let has_variable_label: u32 = endian.parse(read_bytes(r)?);
         let missing_value_code: i32 = endian.parse(read_bytes(r)?);
-        let print_format = Format(endian.parse(read_bytes(r)?));
-        let write_format = Format(endian.parse(read_bytes(r)?));
-        let name: [u8; 8] = read_bytes(r)?;
+        let print_format = Spec(endian.parse(read_bytes(r)?));
+        let write_format = Spec(endian.parse(read_bytes(r)?));
+        let name = UnencodedStr::<8>(read_bytes(r)?);
 
         let label = match has_variable_label {
             0 => None,
@@ -918,7 +921,7 @@ impl Variable {
 
         let missing_values = MissingValues::read(r, offset, width, missing_value_code, endian)?;
 
-        Ok(Variable {
+        Ok(VariableRecord {
             offset,
             width,
             name,
@@ -946,18 +949,18 @@ impl Debug for UntypedValue {
         };
         write!(f, "{number}")?;
 
-        let string = fallback_encode(&self.0);
+        let string = default_decode(&self.0);
         let string = string
             .split(|c: char| c == '\0' || c.is_control())
             .next()
             .unwrap();
-        write!(f, "/\"{string}\"")?;
+        write!(f, "{string:?}")?;
         Ok(())
     }
 }
 
 #[derive(Clone)]
-pub struct UnencodedString(Vec<u8>);
+pub struct UnencodedString(pub Vec<u8>);
 
 impl From<Vec<u8>> for UnencodedString {
     fn from(source: Vec<u8>) -> Self {
@@ -973,12 +976,27 @@ impl From<&[u8]> for UnencodedString {
 
 impl Debug for UnencodedString {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
-        write!(f, "{:?}", FallbackEncoding(self.0.as_slice()))
+        write!(f, "{:?}", default_decode(self.0.as_slice()))
+    }
+}
+
+#[derive(Copy, Clone)]
+pub struct UnencodedStr<const N: usize>(pub [u8; N]);
+
+impl<const N: usize> From<[u8; N]> for UnencodedStr<N> {
+    fn from(source: [u8; N]) -> Self {
+        Self(source)
+    }
+}
+
+impl<const N: usize> Debug for UnencodedStr<N> {
+    fn fmt(&self, f: &mut Formatter) -> FmtResult {
+        write!(f, "{:?}", default_decode(&self.0))
     }
 }
 
 #[derive(Clone)]
-pub struct ValueLabel {
+pub struct ValueLabelRecord {
     /// Offset from the start of the file to the start of the record.
     pub offset: u64,
 
@@ -986,7 +1004,7 @@ pub struct ValueLabel {
     pub labels: Vec<(UntypedValue, UnencodedString)>,
 }
 
-impl Debug for ValueLabel {
+impl Debug for ValueLabelRecord {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
         for (value, label) in self.labels.iter() {
             writeln!(f, "{value:?}: {label:?}")?;
@@ -995,18 +1013,18 @@ impl Debug for ValueLabel {
     }
 }
 
-impl ValueLabel {
+impl ValueLabelRecord {
     /// Maximum number of value labels in a record.
     pub const MAX: u32 = u32::MAX / 8;
 
-    fn read<R: Read + Seek>(r: &mut R, endian: Endian) -> Result<ValueLabel, Error> {
+    fn read<R: Read + Seek>(r: &mut R, endian: Endian) -> Result<ValueLabelRecord, Error> {
         let offset = r.stream_position()?;
         let n: u32 = endian.parse(read_bytes(r)?);
-        if n > ValueLabel::MAX {
+        if n > ValueLabelRecord::MAX {
             return Err(Error::BadNumberOfValueLabels {
                 offset,
                 n,
-                max: ValueLabel::MAX,
+                max: ValueLabelRecord::MAX,
             });
         }
 
@@ -1021,100 +1039,96 @@ impl ValueLabel {
             label.truncate(label_len);
             labels.push((value, UnencodedString(label)));
         }
-        Ok(ValueLabel { offset, labels })
+        Ok(ValueLabelRecord { offset, labels })
     }
 }
 
 #[derive(Clone)]
-pub struct VarIndexes {
+pub struct VarIndexRecord {
     /// Offset from the start of the file to the start of the record.
     pub offset: u64,
 
-    /// The 0-based indexes of the variable indexes.
-    pub var_indexes: Vec<u32>,
+    /// The 1-based indexes of the variable indexes.
+    pub dict_indexes: Vec<u32>,
 }
 
-impl Debug for VarIndexes {
+impl Debug for VarIndexRecord {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
         write!(f, "apply to variables")?;
-        for var_index in self.var_indexes.iter() {
-            write!(f, " #{var_index}")?;
+        for dict_index in self.dict_indexes.iter() {
+            write!(f, " #{dict_index}")?;
         }
         Ok(())
     }
 }
 
-impl VarIndexes {
+impl VarIndexRecord {
     /// Maximum number of variable indexes in a record.
     pub const MAX: u32 = u32::MAX / 8;
 
-    fn read<R: Read + Seek>(r: &mut R, endian: Endian) -> Result<VarIndexes, Error> {
+    fn read<R: Read + Seek>(r: &mut R, endian: Endian) -> Result<VarIndexRecord, Error> {
         let offset = r.stream_position()?;
         let n: u32 = endian.parse(read_bytes(r)?);
-        if n > VarIndexes::MAX {
+        if n > VarIndexRecord::MAX {
             return Err(Error::BadNumberOfVarIndexes {
                 offset,
                 n,
-                max: VarIndexes::MAX,
+                max: VarIndexRecord::MAX,
             });
         }
-        let mut var_indexes = Vec::with_capacity(n as usize);
+        let mut dict_indexes = Vec::with_capacity(n as usize);
         for _ in 0..n {
-            var_indexes.push(endian.parse(read_bytes(r)?));
+            dict_indexes.push(endian.parse(read_bytes(r)?));
         }
 
-        Ok(VarIndexes {
+        Ok(VarIndexRecord {
             offset,
-            var_indexes,
+            dict_indexes,
         })
     }
 }
 
 #[derive(Clone, Debug)]
-pub struct Document {
+pub struct DocumentRecord {
     /// Offset from the start of the file to the start of the record.
     pub pos: u64,
 
     /// The document, as an array of 80-byte lines.
-    pub lines: Vec<[u8; Document::LINE_LEN as usize]>,
+    pub lines: Vec<DocumentLine>,
 }
 
-impl Document {
+pub type DocumentLine = UnencodedStr<{ DocumentRecord::LINE_LEN }>;
+
+impl DocumentRecord {
     /// Length of a line in a document.  Document lines are fixed-length and
     /// padded on the right with spaces.
-    pub const LINE_LEN: u32 = 80;
+    pub const LINE_LEN: usize = 80;
 
     /// Maximum number of lines we will accept in a document.  This is simply
     /// the maximum number that will fit in a 32-bit space.
-    pub const MAX_LINES: u32 = i32::MAX as u32 / Self::LINE_LEN;
+    pub const MAX_LINES: usize = i32::MAX as usize / Self::LINE_LEN;
 
-    fn read<R: Read + Seek>(r: &mut R, endian: Endian) -> Result<Document, Error> {
+    fn read<R: Read + Seek>(r: &mut R, endian: Endian) -> Result<DocumentRecord, Error> {
         let offset = r.stream_position()?;
         let n: u32 = endian.parse(read_bytes(r)?);
-        match n {
-            0..=Self::MAX_LINES => Ok(Document {
-                pos: r.stream_position()?,
-                lines: (0..n)
-                    .map(|_| read_bytes(r))
-                    .collect::<Result<Vec<_>, _>>()?,
-            }),
-            _ => Err(Error::BadDocumentLength {
+        let n = n as usize;
+        if n > Self::MAX_LINES {
+            Err(Error::BadDocumentLength {
                 offset,
                 n,
                 max: Self::MAX_LINES,
-            }),
+            })
+        } else {
+            let pos = r.stream_position()?;
+            let mut lines = Vec::with_capacity(n);
+            for _ in 0..n {
+                lines.push(UnencodedStr::<{ DocumentRecord::LINE_LEN }>(read_bytes(r)?));
+            }
+            Ok(DocumentRecord { pos, lines })
         }
     }
 }
 
-trait TextRecord
-where
-    Self: Sized,
-{
-    const NAME: &'static str;
-    fn parse(input: &str, warn: impl Fn(Error)) -> Result<Self, Error>;
-}
-
 trait ExtensionRecord
 where
     Self: Sized,
@@ -1127,7 +1141,7 @@ where
 }
 
 #[derive(Clone, Debug)]
-pub struct IntegerInfo {
+pub struct IntegerInfoRecord {
     pub version: (i32, i32, i32),
     pub machine_code: i32,
     pub floating_point_rep: i32,
@@ -1136,7 +1150,7 @@ pub struct IntegerInfo {
     pub character_code: i32,
 }
 
-impl ExtensionRecord for IntegerInfo {
+impl ExtensionRecord for IntegerInfoRecord {
     const SUBTYPE: u32 = 3;
     const SIZE: Option<u32> = Some(4);
     const COUNT: Option<u32> = Some(8);
@@ -1149,7 +1163,7 @@ impl ExtensionRecord for IntegerInfo {
         let data: Vec<i32> = (0..8)
             .map(|_| endian.parse(read_bytes(&mut input).unwrap()))
             .collect();
-        Ok(IntegerInfo {
+        Ok(IntegerInfoRecord {
             version: (data[0], data[1], data[2]),
             machine_code: data[3],
             floating_point_rep: data[4],
@@ -1161,13 +1175,13 @@ impl ExtensionRecord for IntegerInfo {
 }
 
 #[derive(Clone, Debug)]
-pub struct FloatInfo {
+pub struct FloatInfoRecord {
     pub sysmis: f64,
     pub highest: f64,
     pub lowest: f64,
 }
 
-impl ExtensionRecord for FloatInfo {
+impl ExtensionRecord for FloatInfoRecord {
     const SUBTYPE: u32 = 4;
     const SIZE: Option<u32> = Some(8);
     const COUNT: Option<u32> = Some(3);
@@ -1180,7 +1194,7 @@ impl ExtensionRecord for FloatInfo {
         let data: Vec<f64> = (0..3)
             .map(|_| endian.parse(read_bytes(&mut input).unwrap()))
             .collect();
-        Ok(FloatInfo {
+        Ok(FloatInfoRecord {
             sysmis: data[0],
             highest: data[1],
             lowest: data[2],
@@ -1188,6 +1202,12 @@ impl ExtensionRecord for FloatInfo {
     }
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum CategoryLabels {
+    VarLabels,
+    CountedValues,
+}
+
 #[derive(Clone, Debug)]
 pub enum MultipleResponseType {
     MultipleDichotomy {
@@ -1196,20 +1216,9 @@ pub enum MultipleResponseType {
     },
     MultipleCategory,
 }
-#[derive(Clone, Debug)]
-pub struct MultipleResponseSet {
-    pub name: UnencodedString,
-    pub label: UnencodedString,
-    pub mr_type: MultipleResponseType,
-    pub vars: Vec<UnencodedString>,
-}
 
-impl MultipleResponseSet {
-    fn parse(input: &[u8]) -> Result<(MultipleResponseSet, &[u8]), Error> {
-        let Some(equals) = input.iter().position(|&b| b == b'=') else {
-            return Err(Error::TBD);
-        };
-        let (name, input) = input.split_at(equals);
+impl MultipleResponseType {
+    fn parse(input: &[u8]) -> Result<(MultipleResponseType, &[u8]), Error> {
         let (mr_type, input) = match input.get(0) {
             Some(b'C') => (MultipleResponseType::MultipleCategory, &input[1..]),
             Some(b'D') => {
@@ -1245,6 +1254,25 @@ impl MultipleResponseSet {
             }
             _ => return Err(Error::TBD),
         };
+        Ok((mr_type, input))
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct MultipleResponseSet {
+    pub name: UnencodedString,
+    pub label: UnencodedString,
+    pub mr_type: MultipleResponseType,
+    pub short_names: Vec<UnencodedString>,
+}
+
+impl MultipleResponseSet {
+    fn parse(input: &[u8]) -> Result<(MultipleResponseSet, &[u8]), Error> {
+        let Some(equals) = input.iter().position(|&b| b == b'=') else {
+            return Err(Error::TBD);
+        };
+        let (name, input) = input.split_at(equals);
+        let (mr_type, input) = MultipleResponseType::parse(input)?;
         let Some(b' ') = input.get(0) else {
             return Err(Error::TBD);
         };
@@ -1271,7 +1299,7 @@ impl MultipleResponseSet {
                 name: name.into(),
                 label: label.into(),
                 mr_type,
-                vars,
+                short_names: vars,
             },
             input,
         ))
@@ -1279,7 +1307,7 @@ impl MultipleResponseSet {
 }
 
 #[derive(Clone, Debug)]
-pub struct MultipleResponseRecord(Vec<MultipleResponseSet>);
+pub struct MultipleResponseRecord(pub Vec<MultipleResponseSet>);
 
 impl ExtensionRecord for MultipleResponseRecord {
     const SUBTYPE: u32 = 7;
@@ -1321,15 +1349,6 @@ fn parse_counted_string(input: &[u8]) -> Result<(UnencodedString, &[u8]), Error>
     Ok((string.into(), rest))
 }
 
-pub struct ProductInfo(String);
-
-impl TextRecord for ProductInfo {
-    const NAME: &'static str = "extra product info";
-    fn parse(input: &str, _warn: impl Fn(Error)) -> Result<Self, Error> {
-        Ok(ProductInfo(input.into()))
-    }
-}
-
 #[derive(Clone, Debug)]
 pub struct VarDisplayRecord(pub Vec<u32>);
 
@@ -1350,145 +1369,6 @@ impl ExtensionRecord for VarDisplayRecord {
     }
 }
 
-pub struct VariableSet {
-    pub name: String,
-    pub vars: Vec<String>,
-}
-
-impl VariableSet {
-    fn parse(input: &str) -> Result<Self, Error> {
-        let (name, input) = input.split_once('=').ok_or(Error::TBD)?;
-        let vars = input.split_ascii_whitespace().map(String::from).collect();
-        Ok(VariableSet {
-            name: name.into(),
-            vars,
-        })
-    }
-}
-
-pub struct VariableSetRecord(Vec<VariableSet>);
-
-impl TextRecord for VariableSetRecord {
-    const NAME: &'static str = "variable set";
-    fn parse(input: &str, warn: impl Fn(Error)) -> Result<Self, Error> {
-        let mut sets = Vec::new();
-        for line in input.lines() {
-            match VariableSet::parse(line) {
-                Ok(set) => sets.push(set),
-                Err(error) => warn(error),
-            }
-        }
-        Ok(VariableSetRecord(sets))
-    }
-}
-
-pub struct LongVariableName {
-    pub short_name: String,
-    pub long_name: String,
-}
-
-pub struct LongVariableNameRecord(Vec<LongVariableName>);
-
-impl TextRecord for LongVariableNameRecord {
-    const NAME: &'static str = "long variable names";
-    fn parse(input: &str, warn: impl Fn(Error)) -> Result<Self, Error> {
-        let mut names = Vec::new();
-        for pair in input.split('\t').filter(|s| !s.is_empty()) {
-            if let Some((short_name, long_name)) = pair.split_once('=') {
-                let name = LongVariableName {
-                    short_name: short_name.into(),
-                    long_name: long_name.into(),
-                };
-                names.push(name);
-            } else {
-                warn(Error::TBD)
-            }
-        }
-        Ok(LongVariableNameRecord(names))
-    }
-}
-
-pub struct VeryLongString {
-    pub short_name: String,
-    pub length: usize,
-}
-
-impl VeryLongString {
-    fn parse(input: &str) -> Result<VeryLongString, Error> {
-        let Some((short_name, length)) = input.split_once('=') else {
-            return Err(Error::TBD);
-        };
-        let length: usize = length.parse().map_err(|_| Error::TBD)?;
-        Ok(VeryLongString {
-            short_name: short_name.into(),
-            length,
-        })
-    }
-}
-
-pub struct VeryLongStringRecord(Vec<VeryLongString>);
-
-impl TextRecord for VeryLongStringRecord {
-    const NAME: &'static str = "very long strings";
-    fn parse(input: &str, warn: impl Fn(Error)) -> Result<Self, Error> {
-        let mut very_long_strings = Vec::new();
-        for tuple in input
-            .split('\0')
-            .map(|s| s.trim_end_matches('\t'))
-            .filter(|s| !s.is_empty())
-        {
-            match VeryLongString::parse(tuple) {
-                Ok(vls) => very_long_strings.push(vls),
-                Err(error) => warn(error),
-            }
-        }
-        Ok(VeryLongStringRecord(very_long_strings))
-    }
-}
-
-#[derive(Clone, Debug)]
-pub struct LongStringValueLabels {
-    pub var_name: UnencodedString,
-    pub width: u32,
-
-    /// `(value, label)` pairs, where each value is `width` bytes.
-    pub labels: Vec<(UnencodedString, UnencodedString)>,
-}
-
-#[derive(Clone, Debug)]
-pub struct LongStringValueLabelRecord(Vec<LongStringValueLabels>);
-
-impl ExtensionRecord for LongStringValueLabelRecord {
-    const SUBTYPE: u32 = 21;
-    const SIZE: Option<u32> = Some(1);
-    const COUNT: Option<u32> = None;
-    const NAME: &'static str = "long string value labels record";
-
-    fn parse(ext: &Extension, endian: Endian, _warn: impl Fn(Error)) -> Result<Self, Error> {
-        ext.check_size::<Self>()?;
-
-        let mut input = &ext.data[..];
-        let mut label_set = Vec::new();
-        while !input.is_empty() {
-            let var_name = read_string(&mut input, endian)?;
-            let width: u32 = endian.parse(read_bytes(&mut input)?);
-            let n_labels: u32 = endian.parse(read_bytes(&mut input)?);
-            let mut labels = Vec::new();
-            for _ in 0..n_labels {
-                let value = read_string(&mut input, endian)?;
-                let label = read_string(&mut input, endian)?;
-                labels.push((value, label));
-            }
-            label_set.push(LongStringValueLabels {
-                var_name,
-                width,
-                labels,
-            })
-        }
-        Ok(LongStringValueLabelRecord(label_set))
-    }
-}
-
 pub struct LongStringMissingValues {
     /// Variable name.
     pub var_name: UnencodedString,
@@ -1534,7 +1414,7 @@ impl ExtensionRecord for LongStringMissingValueSet {
                 } else {
                     value
                 };
-                values.push(Value::String(value));
+                values.push(Value::String(UnencodedStr(value)));
             }
             let missing_values = MissingValues {
                 values,
@@ -1568,127 +1448,6 @@ impl ExtensionRecord for EncodingRecord {
     }
 }
 
-pub struct Attribute {
-    pub name: String,
-    pub values: Vec<String>,
-}
-
-impl Attribute {
-    fn parse<'a>(input: &'a str, warn: &impl Fn(Error)) -> Result<(Attribute, &'a str), Error> {
-        let Some((name, mut input)) = input.split_once('(') else {
-            return Err(Error::TBD);
-        };
-        let mut values = Vec::new();
-        loop {
-            let Some((value, rest)) = input.split_once('\n') else {
-                return Err(Error::TBD);
-            };
-            if let Some(stripped) = value
-                .strip_prefix('\'')
-                .and_then(|value| value.strip_suffix('\''))
-            {
-                values.push(stripped.into());
-            } else {
-                warn(Error::TBD);
-                values.push(value.into());
-            }
-            if let Some(rest) = rest.strip_prefix(')') {
-                return Ok((
-                    Attribute {
-                        name: name.into(),
-                        values,
-                    },
-                    rest,
-                ));
-            }
-            input = rest;
-        }
-    }
-}
-
-pub struct AttributeSet(pub Vec<Attribute>);
-
-impl AttributeSet {
-    fn parse<'a>(
-        mut input: &'a str,
-        sentinel: Option<char>,
-        warn: &impl Fn(Error),
-    ) -> Result<(AttributeSet, &'a str), Error> {
-        let mut attributes = Vec::new();
-        let rest = loop {
-            match input.chars().next() {
-                None => break input,
-                c if c == sentinel => break &input[1..],
-                _ => {
-                    let (attribute, rest) = Attribute::parse(input, &warn)?;
-                    attributes.push(attribute);
-                    input = rest;
-                }
-            }
-        };
-        Ok((AttributeSet(attributes), rest))
-    }
-}
-
-pub struct FileAttributeRecord(AttributeSet);
-
-impl TextRecord for FileAttributeRecord {
-    const NAME: &'static str = "data file attributes";
-    fn parse(input: &str, warn: impl Fn(Error)) -> Result<Self, Error> {
-        let (set, rest) = AttributeSet::parse(input, None, &warn)?;
-        if !rest.is_empty() {
-            warn(Error::TBD);
-        }
-        Ok(FileAttributeRecord(set))
-    }
-}
-
-pub struct VarAttributeSet {
-    pub long_var_name: String,
-    pub attributes: AttributeSet,
-}
-
-impl VarAttributeSet {
-    fn parse<'a>(
-        input: &'a str,
-        warn: &impl Fn(Error),
-    ) -> Result<(VarAttributeSet, &'a str), Error> {
-        let Some((long_var_name, rest)) = input.split_once(':') else {
-            return Err(Error::TBD);
-        };
-        let (attributes, rest) = AttributeSet::parse(rest, Some('/'), warn)?;
-        Ok((
-            VarAttributeSet {
-                long_var_name: long_var_name.into(),
-                attributes,
-            },
-            rest,
-        ))
-    }
-}
-
-pub struct VariableAttributeRecord(Vec<VarAttributeSet>);
-
-impl TextRecord for VariableAttributeRecord {
-    const NAME: &'static str = "variable attributes";
-    fn parse(mut input: &str, warn: impl Fn(Error)) -> Result<Self, Error> {
-        let mut var_attribute_sets = Vec::new();
-        while !input.is_empty() {
-            match VarAttributeSet::parse(input, &warn) {
-                Ok((var_attribute, rest)) => {
-                    var_attribute_sets.push(var_attribute);
-                    input = rest;
-                }
-                Err(error) => {
-                    warn(error);
-                    break;
-                }
-            }
-        }
-        Ok(VariableAttributeRecord(var_attribute_sets))
-    }
-}
-
 #[derive(Clone, Debug)]
 pub struct NumberOfCasesRecord {
     /// Always observed as 1.
@@ -1715,20 +1474,22 @@ impl ExtensionRecord for NumberOfCasesRecord {
     }
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
-pub enum TextExtensionSubtype {
-    VariableSets = 5,
-    ProductInfo = 10,
-    LongNames = 13,
-    LongStrings = 14,
-    FileAttributes = 17,
-    VariableAttributes = 18,
+#[derive(Clone, Debug)]
+pub struct TextRecord {
+    /// Offset from the start of the file to the start of the record.
+    pub offset: u64,
+
+    /// The text content of the record.
+    pub text: UnencodedString,
 }
 
-#[derive(Clone, Debug)]
-pub struct TextExtension {
-    pub subtype: TextExtensionSubtype,
-    pub string: UnencodedString,
+impl From<Extension> for TextRecord {
+    fn from(source: Extension) -> Self {
+        TextRecord {
+            offset: source.offset,
+            text: source.data.into(),
+        }
+    }
 }
 
 #[derive(Clone, Debug)]
@@ -1749,34 +1510,6 @@ pub struct Extension {
     pub data: Vec<u8>,
 }
 
-/*
-fn extension_record_size_requirements(extension: ExtensionType) -> (u32, u32) {
-    match extension {
-        /* Implemented record types. */
-        ExtensionType::Integer => (4, 8),
-        ExtensionType::Float => (8, 3),
-        ExtensionType::VarSets => (1, 0),
-        ExtensionType::Mrsets => (1, 0),
-        ExtensionType::ProductInfo => (1, 0),
-        ExtensionType::Display => (4, 0),
-        ExtensionType::LongNames => (1, 0),
-        ExtensionType::LongStrings => (1, 0),
-        ExtensionType::Ncases => (8, 2),
-        ExtensionType::FileAttrs => (1, 0),
-        ExtensionType::VarAttrs => (1, 0),
-        ExtensionType::Mrsets2 => (1, 0),
-        ExtensionType::Encoding => (1, 0),
-        ExtensionType::LongLabels => (1, 0),
-        ExtensionType::LongMissing => (1, 0),
-
-        /* Ignored record types. */
-        ExtensionType::Date => (0, 0),
-        ExtensionType::DataEntry => (0, 0),
-        ExtensionType::Dataview => (0, 0),
-    }
-}
- */
-
 impl Extension {
     fn check_size<E: ExtensionRecord>(&self) -> Result<(), Error> {
         if let Some(expected_size) = E::SIZE {
@@ -1825,12 +1558,12 @@ impl Extension {
             data,
         };
         match subtype {
-            IntegerInfo::SUBTYPE => Ok(Record::IntegerInfo(IntegerInfo::parse(
+            IntegerInfoRecord::SUBTYPE => Ok(Record::IntegerInfo(IntegerInfoRecord::parse(
                 &extension,
                 endian,
                 |_| (),
             )?)),
-            FloatInfo::SUBTYPE => Ok(Record::FloatInfo(FloatInfo::parse(
+            FloatInfoRecord::SUBTYPE => Ok(Record::FloatInfo(FloatInfoRecord::parse(
                 &extension,
                 endian,
                 |_| (),
@@ -1856,24 +1589,12 @@ impl Extension {
                 endian,
                 |_| (),
             )?)),
-            x if x == TextExtensionSubtype::VariableSets as u32 => {
-                Ok(Record::VariableSets(UnencodedString(extension.data)))
-            }
-            x if x == TextExtensionSubtype::ProductInfo as u32 => {
-                Ok(Record::ProductInfo(UnencodedString(extension.data)))
-            }
-            x if x == TextExtensionSubtype::LongNames as u32 => {
-                Ok(Record::LongNames(UnencodedString(extension.data)))
-            }
-            x if x == TextExtensionSubtype::LongStrings as u32 => {
-                Ok(Record::LongStrings(UnencodedString(extension.data)))
-            }
-            x if x == TextExtensionSubtype::FileAttributes as u32 => {
-                Ok(Record::FileAttributes(UnencodedString(extension.data)))
-            }
-            x if x == TextExtensionSubtype::VariableAttributes as u32 => {
-                Ok(Record::VariableAttributes(UnencodedString(extension.data)))
-            }
+            5 => Ok(Record::VariableSets(extension.into())),
+            10 => Ok(Record::ProductInfo(extension.into())),
+            13 => Ok(Record::LongNames(extension.into())),
+            14 => Ok(Record::VeryLongStrings(extension.into())),
+            17 => Ok(Record::FileAttributes(extension.into())),
+            18 => Ok(Record::VariableAttributes(extension.into())),
             _ => Ok(Record::OtherExtension(extension)),
         }
     }
@@ -2024,3 +1745,46 @@ fn read_string<R: Read>(r: &mut R, endian: Endian) -> Result<UnencodedString, Io
     let length: u32 = endian.parse(read_bytes(r)?);
     Ok(read_vec(r, length as usize)?.into())
 }
+
+#[derive(Clone, Debug)]
+pub struct LongStringValueLabels {
+    pub var_name: UnencodedString,
+    pub width: u32,
+
+    /// `(value, label)` pairs, where each value is `width` bytes.
+    pub labels: Vec<(UnencodedString, UnencodedString)>,
+}
+
+#[derive(Clone, Debug)]
+pub struct LongStringValueLabelRecord(pub Vec<LongStringValueLabels>);
+
+impl ExtensionRecord for LongStringValueLabelRecord {
+    const SUBTYPE: u32 = 21;
+    const SIZE: Option<u32> = Some(1);
+    const COUNT: Option<u32> = None;
+    const NAME: &'static str = "long string value labels record";
+
+    fn parse(ext: &Extension, endian: Endian, _warn: impl Fn(Error)) -> Result<Self, Error> {
+        ext.check_size::<Self>()?;
+
+        let mut input = &ext.data[..];
+        let mut label_set = Vec::new();
+        while !input.is_empty() {
+            let var_name = read_string(&mut input, endian)?;
+            let width: u32 = endian.parse(read_bytes(&mut input)?);
+            let n_labels: u32 = endian.parse(read_bytes(&mut input)?);
+            let mut labels = Vec::new();
+            for _ in 0..n_labels {
+                let value = read_string(&mut input, endian)?;
+                let label = read_string(&mut input, endian)?;
+                labels.push((value, label));
+            }
+            label_set.push(LongStringValueLabels {
+                var_name,
+                width,
+                labels,
+            })
+        }
+        Ok(LongStringValueLabelRecord(label_set))
+    }
+}