separate warnings and errors
authorBen Pfaff <blp@cs.stanford.edu>
Mon, 29 Jan 2024 00:41:58 +0000 (16:41 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Mon, 29 Jan 2024 00:41:58 +0000 (16:41 -0800)
rust/src/dictionary.rs
rust/src/main.rs
rust/src/raw.rs

index 6662aaa94e5249a3933cba5e42bcb56c9123c093..ace97b03f7db5690d2a0ff565d4b0f2ced0ee242 100644 (file)
@@ -36,7 +36,7 @@ impl PartialOrd for VarWidth {
 impl VarWidth {
     pub const MAX_STRING: u16 = 32767;
 
-    fn n_dict_indexes(self) -> usize {
+    pub fn n_dict_indexes(self) -> usize {
         match self {
             VarWidth::Numeric => 1,
             VarWidth::String(w) => div_ceil(w as usize, 8),
index bf6abe9b8f08500a42bd5453553b8725c4cea042..dece725c65073991a9c08e3570f54f750d1666e7 100644 (file)
@@ -17,7 +17,7 @@
 use anyhow::Result;
 use clap::{Parser, ValueEnum};
 use encoding_rs::Encoding;
-use pspp::raw::{Reader, Record, Magic};
+use pspp::raw::{Magic, Reader, Record};
 use std::fs::File;
 use std::io::BufReader;
 use std::path::{Path, PathBuf};
@@ -78,20 +78,27 @@ fn main() -> Result<()> {
     Ok(())
 }
 
-fn dissect(file_name: &Path, max_cases: u64, mode: Mode, encoding: Option<&'static Encoding>) -> Result<()> {
+fn dissect(
+    file_name: &Path,
+    max_cases: u64,
+    mode: Mode,
+    encoding: Option<&'static Encoding>,
+) -> Result<()> {
     let reader = File::open(file_name)?;
     let reader = BufReader::new(reader);
     let mut reader = Reader::new(reader, |warning| println!("{warning}"))?;
 
     match mode {
         Mode::Identify => {
-            let Record::Header(header) = reader.next().unwrap()? else { unreachable!() };
+            let Record::Header(header) = reader.next().unwrap()? else {
+                unreachable!()
+            };
             match header.magic {
                 Magic::Sav => println!("SPSS System File"),
                 Magic::Zsav => println!("SPSS System File with Zlib compression"),
                 Magic::Ebcdic => println!("EBCDIC-encoded SPSS System File"),
             }
-            return Ok(())
+            return Ok(());
         }
         Mode::Raw => {
             for header in reader {
@@ -108,15 +115,20 @@ fn dissect(file_name: &Path, max_cases: u64, mode: Mode, encoding: Option<&'stat
                 }
             }
         }
-        Mode::Cooked => {
 /*
+        Mode::Decoded => {
             let headers: Vec<Record> = reader.collect::<Result<Vec<_>, _>>()?;
-            let encoding = encoding_from_headers(&headers, &|e| eprintln!("{e}"))?;
-            let (headers, _) = decode(headers, encoding, &|e| eprintln!("{e}"))?;
-            for header in headers {
-                println!("{header:?}");
         }
-            */
+*/
+        Mode::Cooked => {
+            /*
+                let headers: Vec<Record> = reader.collect::<Result<Vec<_>, _>>()?;
+                let encoding = encoding_from_headers(&headers, &|e| eprintln!("{e}"))?;
+                let (headers, _) = decode(headers, encoding, &|e| eprintln!("{e}"))?;
+                for header in headers {
+                    println!("{header:?}");
+            }
+                */
         }
     }
 
index e75e5a05e8e8bfac8adba560c1d4834c79fc6d85..a38422950ad9fc2d1c8d971756a9c33829b89cb1 100644 (file)
@@ -39,9 +39,6 @@ pub enum Error {
     #[error("Invalid ZSAV compression code {0}")]
     InvalidZsavCompression(u32),
 
-    #[error("Variable record at offset {offset:#x} specifies width {width} not in valid range [-1,255).")]
-    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: usize, max: usize },
 
@@ -72,23 +69,6 @@ pub enum Error {
     #[error("At offset {offset:#x}, number of variables indexes for value labels ({n}) is greater than the maximum number ({max}).")]
     TooManyVarIndexes { offset: u64, n: u32, max: u32 },
 
-    #[error("At offset {offset:#x}, at least one valid variable index for value labels is required but none were specified.")]
-    NoVarIndexes { offset: u64 },
-
-    #[error("At offset {offset:#x}, the first variable index is for a {var_type} variable but the following variable indexes are for {} variables: {wrong_types:?}", var_type.opposite())]
-    MixedVarTypes {
-        offset: u64,
-        var_type: VarType,
-        wrong_types: Vec<u32>,
-    },
-
-    #[error("At offset {offset:#x}, one or more variable indexes for value labels were not in the valid range [1,{max}]: {invalid:?}")]
-    InvalidVarIndexes {
-        offset: u64,
-        max: usize,
-        invalid: Vec<u32>,
-    },
-
     #[error("At offset {offset:#x}, record type 7 subtype {subtype} is too large with element size {size} and {count} elements.")]
     ExtensionRecordTooLarge {
         offset: u64,
@@ -125,6 +105,29 @@ pub enum Error {
         expected_n_blocks: u64,
         ztrailer_len: u64,
     },
+}
+
+#[derive(ThisError, Debug)]
+pub enum Warning {
+    #[error("Unexpected end of data inside extension record.")]
+    UnexpectedEndOfData,
+
+    #[error("At offset {offset:#x}, at least one valid variable index for value labels is required but none were specified.")]
+    NoVarIndexes { offset: u64 },
+
+    #[error("At offset {offset:#x}, the first variable index is for a {var_type} variable but the following variable indexes are for {} variables: {wrong_types:?}", var_type.opposite())]
+    MixedVarTypes {
+        offset: u64,
+        var_type: VarType,
+        wrong_types: Vec<u32>,
+    },
+
+    #[error("At offset {offset:#x}, one or more variable indexes for value labels were not in the valid range [1,{max}]: {invalid:?}")]
+    InvalidVarIndexes {
+        offset: u64,
+        max: usize,
+        invalid: Vec<u32>,
+    },
 
     #[error("At offset {offset:#x}, {record} has bad size {size} bytes instead of the expected {expected_size}.")]
     BadRecordSize {
@@ -196,6 +199,12 @@ pub enum Error {
     TBD,
 }
 
+impl From<IoError> for Warning {
+    fn from(_source: IoError) -> Self {
+        Self::UnexpectedEndOfData
+    }
+}
+
 #[derive(Clone, Debug)]
 pub enum Record {
     Header(HeaderRecord<RawString>),
@@ -248,7 +257,7 @@ impl Record {
         reader: &mut R,
         endian: Endian,
         var_types: &[VarType],
-        warn: &Box<dyn Fn(Error)>,
+        warn: &Box<dyn Fn(Warning)>,
     ) -> Result<Option<Record>, Error>
     where
         R: Read + Seek,
@@ -269,7 +278,7 @@ impl Record {
         }
     }
 
-    fn decode<'a>(&'a self, decoder: &Decoder) -> Result<DecodedRecord<'a>, Error> {
+    pub fn decode<'a>(&'a self, decoder: &Decoder) -> Result<DecodedRecord<'a>, Error> {
         Ok(match self {
             Record::Header(record) => record.decode(decoder),
             Record::Variable(record) => record.decode(decoder),
@@ -280,14 +289,14 @@ impl Record {
             Record::VarDisplay(record) => DecodedRecord::VarDisplay(record.clone()),
             Record::MultipleResponse(record) => record.decode(decoder),
             Record::LongStringValueLabels(record) => {
-                DecodedRecord::LongStringValueLabels(record.decode(decoder)?)
+                DecodedRecord::LongStringValueLabels(record.decode(decoder))
             }
             Record::LongStringMissingValues(record) => {
                 DecodedRecord::LongStringMissingValues(record.decode(decoder))
             }
             Record::Encoding(record) => DecodedRecord::Encoding(record.clone()),
             Record::NumberOfCases(record) => DecodedRecord::NumberOfCases(record.clone()),
-            Record::Text(record) => record.decode(decoder)?,
+            Record::Text(record) => record.decode(decoder),
             Record::OtherExtension(record) => DecodedRecord::OtherExtension(record.clone()),
             Record::EndOfHeaders(record) => DecodedRecord::EndOfHeaders(record.clone()),
             Record::ZHeader(record) => DecodedRecord::ZHeader(record.clone()),
@@ -476,17 +485,17 @@ impl HeaderRecord<RawString> {
 
 pub struct Decoder {
     pub encoding: &'static Encoding,
-    pub warn: Box<dyn Fn(Error)>,
+    pub warn: Box<dyn Fn(Warning)>,
 }
 
 impl Decoder {
-    fn warn(&self, error: Error) {
-        (self.warn)(error)
+    fn warn(&self, warning: Warning) {
+        (self.warn)(warning)
     }
     fn decode_slice<'a>(&self, input: &'a [u8]) -> Cow<'a, str> {
         let (output, malformed) = self.encoding.decode_without_bom_handling(input);
         if malformed {
-            self.warn(Error::MalformedString {
+            self.warn(Warning::MalformedString {
                 encoding: self.encoding.name().into(),
                 text: output.clone().into(),
             });
@@ -839,7 +848,7 @@ where
     R: Read + Seek + 'static,
 {
     reader: Option<R>,
-    warn: Box<dyn Fn(Error)>,
+    warn: Box<dyn Fn(Warning)>,
 
     header: HeaderRecord<RawString>,
     var_types: Vec<VarType>,
@@ -853,7 +862,7 @@ where
 {
     pub fn new<F>(mut reader: R, warn: F) -> Result<Self, Error>
     where
-        F: Fn(Error) + 'static,
+        F: Fn(Warning) + 'static,
     {
         let header = HeaderRecord::read(&mut reader)?;
         Ok(Self {
@@ -1431,7 +1440,7 @@ impl ValueLabelRecord<RawStr<8>, RawString> {
         r: &mut R,
         endian: Endian,
         var_types: &[VarType],
-        warn: &Box<dyn Fn(Error)>,
+        warn: &Box<dyn Fn(Warning)>,
     ) -> Result<Option<Record>, Error> {
         let label_offset = r.stream_position()?;
         let n: u32 = endian.parse(read_bytes(r)?);
@@ -1485,7 +1494,7 @@ impl ValueLabelRecord<RawStr<8>, RawString> {
             }
         }
         if !invalid_indexes.is_empty() {
-            warn(Error::InvalidVarIndexes {
+            warn(Warning::InvalidVarIndexes {
                 offset: index_offset,
                 max: var_types.len(),
                 invalid: invalid_indexes,
@@ -1493,7 +1502,7 @@ impl ValueLabelRecord<RawStr<8>, RawString> {
         }
 
         let Some(&first_index) = dict_indexes.first() else {
-            warn(Error::NoVarIndexes {
+            warn(Warning::NoVarIndexes {
                 offset: index_offset,
             });
             return Ok(None);
@@ -1509,7 +1518,7 @@ impl ValueLabelRecord<RawStr<8>, RawString> {
             }
         });
         if !wrong_type_indexes.is_empty() {
-            warn(Error::MixedVarTypes {
+            warn(Warning::MixedVarTypes {
                 offset: index_offset,
                 var_type,
                 wrong_types: wrong_type_indexes,
@@ -1622,7 +1631,7 @@ trait ExtensionRecord {
     const SIZE: Option<u32>;
     const COUNT: Option<u32>;
     const NAME: &'static str;
-    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Error>;
+    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Warning>;
 }
 
 #[derive(Clone, Debug)]
@@ -1642,7 +1651,7 @@ impl ExtensionRecord for IntegerInfoRecord {
     const COUNT: Option<u32> = Some(8);
     const NAME: &'static str = "integer record";
 
-    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Error> {
+    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Warning> {
         ext.check_size::<Self>()?;
 
         let mut input = &ext.data[..];
@@ -1674,7 +1683,7 @@ impl ExtensionRecord for FloatInfoRecord {
     const COUNT: Option<u32> = Some(3);
     const NAME: &'static str = "floating point record";
 
-    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Error> {
+    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Warning> {
         ext.check_size::<Self>()?;
 
         let mut input = &ext.data[..];
@@ -1705,7 +1714,7 @@ pub enum MultipleResponseType {
 }
 
 impl MultipleResponseType {
-    fn parse(input: &[u8]) -> Result<(MultipleResponseType, &[u8]), Error> {
+    fn parse(input: &[u8]) -> Result<(MultipleResponseType, &[u8]), Warning> {
         let (mr_type, input) = match input.split_first() {
             Some((b'C', input)) => (MultipleResponseType::MultipleCategory, input),
             Some((b'D', input)) => {
@@ -1724,7 +1733,7 @@ impl MultipleResponseType {
                 } else if let Some(rest) = input.strip_prefix(b" 11 ") {
                     (CategoryLabels::VarLabels, rest)
                 } else {
-                    return Err(Error::TBD);
+                    return Err(Warning::TBD);
                 };
                 let (value, input) = parse_counted_string(input)?;
                 (
@@ -1732,7 +1741,7 @@ impl MultipleResponseType {
                     input,
                 )
             }
-            _ => return Err(Error::TBD),
+            _ => return Err(Warning::TBD),
         };
         Ok((mr_type, input))
     }
@@ -1751,14 +1760,14 @@ where
 }
 
 impl MultipleResponseSet<RawString, RawString> {
-    fn parse(input: &[u8]) -> Result<(Self, &[u8]), Error> {
+    fn parse(input: &[u8]) -> Result<(Self, &[u8]), Warning> {
         let Some(equals) = input.iter().position(|&b| b == b'=') else {
-            return Err(Error::TBD);
+            return Err(Warning::TBD);
         };
         let (name, input) = input.split_at(equals);
         let (mr_type, input) = MultipleResponseType::parse(input)?;
         let Some(input) = input.strip_prefix(b" ") else {
-            return Err(Error::TBD);
+            return Err(Warning::TBD);
         };
         let (label, mut input) = parse_counted_string(input)?;
         let mut vars = Vec::new();
@@ -1766,7 +1775,7 @@ impl MultipleResponseSet<RawString, RawString> {
             match input.split_first() {
                 Some((b' ', rest)) => {
                     let Some(length) = rest.iter().position(|b| b" \n".contains(b)) else {
-                        return Err(Error::TBD);
+                        return Err(Warning::TBD);
                     };
                     let (var, rest) = rest.split_at(length);
                     if !var.is_empty() {
@@ -1774,7 +1783,7 @@ impl MultipleResponseSet<RawString, RawString> {
                     }
                     input = rest;
                 }
-                _ => return Err(Error::TBD),
+                _ => return Err(Warning::TBD),
             }
         }
         while input.first() == Some(&b'\n') {
@@ -1794,13 +1803,13 @@ impl MultipleResponseSet<RawString, RawString> {
     fn decode<'a>(
         &'a self,
         decoder: &Decoder,
-    ) -> Result<MultipleResponseSet<Identifier, Cow<'a, str>>, Error> {
+    ) -> Result<MultipleResponseSet<Identifier, Cow<'a, str>>, Warning> {
         let mut short_names = Vec::with_capacity(self.short_names.len());
         for short_name in self.short_names.iter() {
             if let Some(short_name) = decoder
                 .decode_identifier(short_name)
-                .map_err(|err| Error::InvalidMrSetName(err))
-                .warn_on_error(&decoder.warn)
+                .map_err(|err| Warning::InvalidMrSetName(err))
+                .issue_warning(&decoder.warn)
             {
                 short_names.push(short_name);
             }
@@ -1808,7 +1817,7 @@ impl MultipleResponseSet<RawString, RawString> {
         Ok(MultipleResponseSet {
             name: decoder
                 .decode_identifier(&self.name)
-                .map_err(|err| Error::InvalidMrSetVariableName(err))?,
+                .map_err(|err| Warning::InvalidMrSetVariableName(err))?,
             label: decoder.decode(&self.label),
             mr_type: self.mr_type.clone(),
             short_names: short_names,
@@ -1828,7 +1837,7 @@ impl ExtensionRecord for MultipleResponseRecord<RawString, RawString> {
     const COUNT: Option<u32> = None;
     const NAME: &'static str = "multiple response set record";
 
-    fn parse(ext: &Extension, _endian: Endian) -> Result<Record, Error> {
+    fn parse(ext: &Extension, _endian: Endian) -> Result<Record, Warning> {
         ext.check_size::<Self>()?;
 
         let mut input = &ext.data[..];
@@ -1846,7 +1855,7 @@ impl MultipleResponseRecord<RawString, RawString> {
     fn decode<'a>(&'a self, decoder: &Decoder) -> DecodedRecord {
         let mut sets = Vec::new();
         for set in self.0.iter() {
-            if let Some(set) = set.decode(decoder).warn_on_error(&decoder.warn) {
+            if let Some(set) = set.decode(decoder).issue_warning(&decoder.warn) {
                 sets.push(set);
             }
         }
@@ -1854,20 +1863,20 @@ impl MultipleResponseRecord<RawString, RawString> {
     }
 }
 
-fn parse_counted_string(input: &[u8]) -> Result<(RawString, &[u8]), Error> {
+fn parse_counted_string(input: &[u8]) -> Result<(RawString, &[u8]), Warning> {
     let Some(space) = input.iter().position(|&b| b == b' ') else {
-        return Err(Error::TBD);
+        return Err(Warning::TBD);
     };
     let Ok(length) = from_utf8(&input[..space]) else {
-        return Err(Error::TBD);
+        return Err(Warning::TBD);
     };
     let Ok(length): Result<usize, _> = length.parse() else {
-        return Err(Error::TBD);
+        return Err(Warning::TBD);
     };
 
     let input = &input[space + 1..];
     if input.len() < length {
-        return Err(Error::TBD);
+        return Err(Warning::TBD);
     };
 
     let (string, rest) = input.split_at(length);
@@ -1889,13 +1898,13 @@ impl Measure {
         }
     }
 
-    fn try_decode(source: u32) -> Result<Option<Measure>, Error> {
+    fn try_decode(source: u32) -> Result<Option<Measure>, Warning> {
         match source {
             0 => Ok(None),
             1 => Ok(Some(Measure::Nominal)),
             2 => Ok(Some(Measure::Ordinal)),
             3 => Ok(Some(Measure::Scale)),
-            _ => Err(Error::InvalidMeasurement(source)),
+            _ => Err(Warning::InvalidMeasurement(source)),
         }
     }
 }
@@ -1908,13 +1917,13 @@ pub enum Alignment {
 }
 
 impl Alignment {
-    fn try_decode(source: u32) -> Result<Option<Alignment>, Error> {
+    fn try_decode(source: u32) -> Result<Option<Alignment>, Warning> {
         match source {
             0 => Ok(None),
             1 => Ok(Some(Alignment::Left)),
             2 => Ok(Some(Alignment::Right)),
             3 => Ok(Some(Alignment::Center)),
-            _ => Err(Error::InvalidAlignment(source)),
+            _ => Err(Warning::InvalidAlignment(source)),
         }
     }
 
@@ -1943,10 +1952,10 @@ impl VarDisplayRecord {
         ext: &Extension,
         n_vars: usize,
         endian: Endian,
-        warn: &Box<dyn Fn(Error)>,
-    ) -> Result<Record, Error> {
+        warn: &Box<dyn Fn(Warning)>,
+    ) -> Result<Record, Warning> {
         if ext.size != 4 {
-            return Err(Error::BadRecordSize {
+            return Err(Warning::BadRecordSize {
                 offset: ext.offsets.start,
                 record: String::from("variable display record"),
                 size: ext.size,
@@ -1959,18 +1968,18 @@ impl VarDisplayRecord {
         } else if ext.count as usize == 2 * n_vars {
             false
         } else {
-            return Err(Error::TBD);
+            return Err(Warning::TBD);
         };
 
         let mut var_displays = Vec::new();
         let mut input = &ext.data[..];
         for _ in 0..n_vars {
             let measure = Measure::try_decode(endian.parse(read_bytes(&mut input).unwrap()))
-                .warn_on_error(&warn)
+                .issue_warning(&warn)
                 .flatten();
             let width = has_width.then(|| endian.parse(read_bytes(&mut input).unwrap()));
             let alignment = Alignment::try_decode(endian.parse(read_bytes(&mut input).unwrap()))
-                .warn_on_error(&warn)
+                .issue_warning(&warn)
                 .flatten();
             var_displays.push(VarDisplay {
                 measure,
@@ -2019,7 +2028,7 @@ impl ExtensionRecord for LongStringMissingValueRecord<RawString, RawStr<8>> {
     const COUNT: Option<u32> = None;
     const NAME: &'static str = "long string missing values record";
 
-    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Error> {
+    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Warning> {
         ext.check_size::<Self>()?;
 
         let mut input = &ext.data[..];
@@ -2030,7 +2039,7 @@ impl ExtensionRecord for LongStringMissingValueRecord<RawString, RawStr<8>> {
             let value_len: u32 = endian.parse(read_bytes(&mut input)?);
             if value_len != 8 {
                 let offset = (ext.data.len() - input.len() - 8) as u64 + ext.offsets.start;
-                return Err(Error::BadLongMissingValueLength {
+                return Err(Warning::BadLongMissingValueLength {
                     record_offset: ext.offsets.start,
                     offset,
                     value_len,
@@ -2074,8 +2083,8 @@ impl LongStringMissingValueRecord<RawString, RawStr<8>> {
         for mv in self.0.iter() {
             if let Some(mv) = mv
                 .decode(decoder)
-                .map_err(|err| Error::InvalidLongStringMissingValueVariableName(err))
-                .warn_on_error(&decoder.warn)
+                .map_err(|err| Warning::InvalidLongStringMissingValueVariableName(err))
+                .issue_warning(&decoder.warn)
             {
                 mvs.push(mv);
             }
@@ -2093,11 +2102,11 @@ impl ExtensionRecord for EncodingRecord {
     const COUNT: Option<u32> = None;
     const NAME: &'static str = "encoding record";
 
-    fn parse(ext: &Extension, _endian: Endian) -> Result<Record, Error> {
+    fn parse(ext: &Extension, _endian: Endian) -> Result<Record, Warning> {
         ext.check_size::<Self>()?;
 
         Ok(Record::Encoding(EncodingRecord(
-            String::from_utf8(ext.data.clone()).map_err(|_| Error::BadEncodingName {
+            String::from_utf8(ext.data.clone()).map_err(|_| Warning::BadEncodingName {
                 offset: ext.offsets.start,
             })?,
         )))
@@ -2119,7 +2128,7 @@ impl ExtensionRecord for NumberOfCasesRecord {
     const COUNT: Option<u32> = Some(2);
     const NAME: &'static str = "extended number of cases record";
 
-    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Error> {
+    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Warning> {
         ext.check_size::<Self>()?;
 
         let mut input = &ext.data[..];
@@ -2159,26 +2168,26 @@ impl TextRecord {
             text: extension.data.into(),
         }
     }
-    pub fn decode<'a>(&self, decoder: &Decoder) -> Result<DecodedRecord, Error> {
+    pub fn decode<'a>(&self, decoder: &Decoder) -> DecodedRecord {
         match self.rec_type {
-            TextRecordType::VariableSets => Ok(DecodedRecord::VariableSets(
-                VariableSetRecord::decode(self, decoder),
-            )),
-            TextRecordType::ProductInfo => Ok(DecodedRecord::ProductInfo(
-                ProductInfoRecord::decode(self, decoder),
-            )),
-            TextRecordType::LongNames => Ok(DecodedRecord::LongNames(LongNamesRecord::decode(
-                self, decoder,
-            ))),
-            TextRecordType::VeryLongStrings => Ok(DecodedRecord::VeryLongStrings(
-                VeryLongStringsRecord::decode(self, decoder),
-            )),
-            TextRecordType::FileAttributes => Ok(DecodedRecord::FileAttributes(
-                FileAttributeRecord::decode(self, decoder),
-            )),
-            TextRecordType::VariableAttributes => Ok(DecodedRecord::VariableAttributes(
-                VariableAttributeRecord::decode(self, decoder),
-            )),
+            TextRecordType::VariableSets => {
+                DecodedRecord::VariableSets(VariableSetRecord::decode(self, decoder))
+            }
+            TextRecordType::ProductInfo => {
+                DecodedRecord::ProductInfo(ProductInfoRecord::decode(self, decoder))
+            }
+            TextRecordType::LongNames => {
+                DecodedRecord::LongNames(LongNamesRecord::decode(self, decoder))
+            }
+            TextRecordType::VeryLongStrings => {
+                DecodedRecord::VeryLongStrings(VeryLongStringsRecord::decode(self, decoder))
+            }
+            TextRecordType::FileAttributes => {
+                DecodedRecord::FileAttributes(FileAttributeRecord::decode(self, decoder))
+            }
+            TextRecordType::VariableAttributes => {
+                DecodedRecord::VariableAttributes(VariableAttributeRecord::decode(self, decoder))
+            }
         }
     }
 }
@@ -2190,14 +2199,14 @@ pub struct VeryLongString {
 }
 
 impl VeryLongString {
-    fn parse(decoder: &Decoder, input: &str) -> Result<VeryLongString, Error> {
+    fn parse(decoder: &Decoder, input: &str) -> Result<VeryLongString, Warning> {
         let Some((short_name, length)) = input.split_once('=') else {
-            return Err(Error::TBD);
+            return Err(Warning::TBD);
         };
         let short_name = decoder
             .new_identifier(short_name)
-            .map_err(Error::InvalidLongStringName)?;
-        let length = length.parse().map_err(|_| Error::TBD)?;
+            .map_err(Warning::InvalidLongStringName)?;
+        let length = length.parse().map_err(|_| Warning::TBD)?;
         Ok(VeryLongString { short_name, length })
     }
 }
@@ -2214,7 +2223,7 @@ impl VeryLongStringsRecord {
             .map(|s| s.trim_end_matches('\t'))
             .filter(|s| !s.is_empty())
         {
-            if let Some(vls) = VeryLongString::parse(decoder, tuple).warn_on_error(&decoder.warn) {
+            if let Some(vls) = VeryLongString::parse(decoder, tuple).issue_warning(&decoder.warn) {
                 very_long_strings.push(vls)
             }
         }
@@ -2229,17 +2238,17 @@ pub struct Attribute {
 }
 
 impl Attribute {
-    fn parse<'a>(decoder: &Decoder, input: &'a str) -> Result<(Attribute, &'a str), Error> {
+    fn parse<'a>(decoder: &Decoder, input: &'a str) -> Result<(Attribute, &'a str), Warning> {
         let Some((name, mut input)) = input.split_once('(') else {
-            return Err(Error::TBD);
+            return Err(Warning::TBD);
         };
         let name = decoder
             .new_identifier(name)
-            .map_err(Error::InvalidAttributeName)?;
+            .map_err(Warning::InvalidAttributeName)?;
         let mut values = Vec::new();
         loop {
             let Some((value, rest)) = input.split_once('\n') else {
-                return Err(Error::TBD);
+                return Err(Warning::TBD);
             };
             if let Some(stripped) = value
                 .strip_prefix('\'')
@@ -2247,7 +2256,7 @@ impl Attribute {
             {
                 values.push(stripped.into());
             } else {
-                decoder.warn(Error::TBD);
+                decoder.warn(Warning::TBD);
                 values.push(value.into());
             }
             if let Some(rest) = rest.strip_prefix(')') {
@@ -2267,7 +2276,7 @@ impl AttributeSet {
         decoder: &Decoder,
         mut input: &'a str,
         sentinel: Option<char>,
-    ) -> Result<(AttributeSet, &'a str), Error> {
+    ) -> Result<(AttributeSet, &'a str), Warning> {
         let mut attributes = HashMap::new();
         let rest = loop {
             match input.chars().next() {
@@ -2297,10 +2306,10 @@ pub struct FileAttributeRecord(AttributeSet);
 impl FileAttributeRecord {
     fn decode(source: &TextRecord, decoder: &Decoder) -> Self {
         let input = decoder.decode(&source.text);
-        match AttributeSet::parse(decoder, &input, None).warn_on_error(&decoder.warn) {
+        match AttributeSet::parse(decoder, &input, None).issue_warning(&decoder.warn) {
             Some((set, rest)) => {
                 if !rest.is_empty() {
-                    decoder.warn(Error::TBD);
+                    decoder.warn(Warning::TBD);
                 }
                 FileAttributeRecord(set)
             }
@@ -2322,13 +2331,13 @@ pub struct VarAttributeSet {
 }
 
 impl VarAttributeSet {
-    fn parse<'a>(decoder: &Decoder, input: &'a str) -> Result<(VarAttributeSet, &'a str), Error> {
+    fn parse<'a>(decoder: &Decoder, input: &'a str) -> Result<(VarAttributeSet, &'a str), Warning> {
         let Some((long_var_name, rest)) = input.split_once(':') else {
-            return Err(Error::TBD);
+            return Err(Warning::TBD);
         };
         let long_var_name = decoder
             .new_identifier(long_var_name)
-            .map_err(Error::InvalidAttributeVariableName)?;
+            .map_err(Warning::InvalidAttributeVariableName)?;
         let (attributes, rest) = AttributeSet::parse(decoder, rest, Some('/'))?;
         let var_attribute = VarAttributeSet {
             long_var_name,
@@ -2348,7 +2357,7 @@ impl VariableAttributeRecord {
         let mut var_attribute_sets = Vec::new();
         while !input.is_empty() {
             let Some((var_attribute, rest)) =
-                VarAttributeSet::parse(decoder, &input).warn_on_error(&decoder.warn)
+                VarAttributeSet::parse(decoder, &input).issue_warning(&decoder.warn)
             else {
                 break;
             };
@@ -2366,16 +2375,16 @@ pub struct LongName {
 }
 
 impl LongName {
-    fn parse(input: &str, decoder: &Decoder) -> Result<Self, Error> {
+    fn parse(input: &str, decoder: &Decoder) -> Result<Self, Warning> {
         let Some((short_name, long_name)) = input.split_once('=') else {
-            return Err(Error::TBD);
+            return Err(Warning::TBD);
         };
         let short_name = decoder
             .new_identifier(short_name)
-            .map_err(Error::InvalidShortName)?;
+            .map_err(Warning::InvalidShortName)?;
         let long_name = decoder
             .new_identifier(long_name)
-            .map_err(Error::InvalidLongName)?;
+            .map_err(Warning::InvalidLongName)?;
         Ok(LongName {
             short_name,
             long_name,
@@ -2391,7 +2400,7 @@ impl LongNamesRecord {
         let input = decoder.decode(&source.text);
         let mut names = Vec::new();
         for pair in input.split('\t').filter(|s| !s.is_empty()) {
-            if let Some(long_name) = LongName::parse(pair, decoder).warn_on_error(&decoder.warn) {
+            if let Some(long_name) = LongName::parse(pair, decoder).issue_warning(&decoder.warn) {
                 names.push(long_name);
             }
         }
@@ -2403,7 +2412,6 @@ impl LongNamesRecord {
 pub struct ProductInfoRecord(pub String);
 
 impl ProductInfoRecord {
-    const NAME: &'static str = "extra product info";
     fn decode(source: &TextRecord, decoder: &Decoder) -> Self {
         Self(decoder.decode(&source.text).into())
     }
@@ -2415,14 +2423,14 @@ pub struct VariableSet {
 }
 
 impl VariableSet {
-    fn parse(input: &str, decoder: &Decoder) -> Result<Self, Error> {
-        let (name, input) = input.split_once('=').ok_or(Error::TBD)?;
+    fn parse(input: &str, decoder: &Decoder) -> Result<Self, Warning> {
+        let (name, input) = input.split_once('=').ok_or(Warning::TBD)?;
         let mut vars = Vec::new();
         for var in input.split_ascii_whitespace() {
             if let Some(identifier) = decoder
                 .new_identifier(var)
-                .map_err(Error::InvalidVariableSetName)
-                .warn_on_error(&decoder.warn)
+                .map_err(Warning::InvalidVariableSetName)
+                .issue_warning(&decoder.warn)
             {
                 vars.push(identifier);
             }
@@ -2445,7 +2453,7 @@ impl VariableSetRecord {
         let mut sets = Vec::new();
         let input = decoder.decode(&source.text);
         for line in input.lines() {
-            if let Some(set) = VariableSet::parse(line, decoder).warn_on_error(&decoder.warn) {
+            if let Some(set) = VariableSet::parse(line, decoder).issue_warning(&decoder.warn) {
                 sets.push(set)
             }
         }
@@ -2456,11 +2464,16 @@ impl VariableSetRecord {
     }
 }
 
-trait WarnOnError<T> {
-    fn warn_on_error<F: Fn(Error)>(self, warn: &F) -> Option<T>;
+trait IssueWarning<T> {
+    fn issue_warning<F>(self, warn: &F) -> Option<T>
+    where
+        F: Fn(Warning);
 }
-impl<T> WarnOnError<T> for Result<T, Error> {
-    fn warn_on_error<F: Fn(Error)>(self, warn: &F) -> Option<T> {
+impl<T> IssueWarning<T> for Result<T, Warning> {
+    fn issue_warning<F>(self, warn: &F) -> Option<T>
+    where
+        F: Fn(Warning),
+    {
         match self {
             Ok(result) => Some(result),
             Err(error) => {
@@ -2489,10 +2502,10 @@ pub struct Extension {
 }
 
 impl Extension {
-    fn check_size<E: ExtensionRecord>(&self) -> Result<(), Error> {
+    fn check_size<E: ExtensionRecord>(&self) -> Result<(), Warning> {
         if let Some(expected_size) = E::SIZE {
             if self.size != expected_size {
-                return Err(Error::BadRecordSize {
+                return Err(Warning::BadRecordSize {
                     offset: self.offsets.start,
                     record: E::NAME.into(),
                     size: self.size,
@@ -2502,7 +2515,7 @@ impl Extension {
         }
         if let Some(expected_count) = E::COUNT {
             if self.count != expected_count {
-                return Err(Error::BadRecordCount {
+                return Err(Warning::BadRecordCount {
                     offset: self.offsets.start,
                     record: E::NAME.into(),
                     count: self.count,
@@ -2517,7 +2530,7 @@ impl Extension {
         r: &mut R,
         endian: Endian,
         n_vars: usize,
-        warn: &Box<dyn Fn(Error)>,
+        warn: &Box<dyn Fn(Warning)>,
     ) -> Result<Option<Record>, Error> {
         let subtype = endian.parse(read_bytes(r)?);
         let header_offset = r.stream_position()?;
@@ -2751,10 +2764,10 @@ impl LongStringValueLabels<RawString, RawString> {
     fn decode<'a>(
         &'a self,
         decoder: &Decoder,
-    ) -> Result<LongStringValueLabels<Identifier, Cow<'a, str>>, Error> {
+    ) -> Result<LongStringValueLabels<Identifier, Cow<'a, str>>, Warning> {
         let var_name = decoder.decode(&self.var_name);
         let var_name = Identifier::new(var_name.trim_end(), decoder.encoding)
-            .map_err(Error::InvalidLongStringValueLabelName)?;
+            .map_err(Warning::InvalidLongStringValueLabelName)?;
 
         let mut labels = Vec::with_capacity(self.labels.len());
         for (value, label) in self.labels.iter() {
@@ -2783,7 +2796,7 @@ impl ExtensionRecord for LongStringValueLabelRecord<RawString, RawString> {
     const COUNT: Option<u32> = None;
     const NAME: &'static str = "long string value labels record";
 
-    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Error> {
+    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Warning> {
         ext.check_size::<Self>()?;
 
         let mut input = &ext.data[..];
@@ -2814,7 +2827,7 @@ impl LongStringValueLabelRecord<RawString, RawString> {
     fn decode<'a>(
         &'a self,
         decoder: &Decoder,
-    ) -> Result<LongStringValueLabelRecord<Identifier, Cow<'a, str>>, Error> {
+    ) -> LongStringValueLabelRecord<Identifier, Cow<'a, str>> {
         let mut labels = Vec::with_capacity(self.0.len());
         for label in &self.0 {
             match label.decode(decoder) {
@@ -2822,6 +2835,6 @@ impl LongStringValueLabelRecord<RawString, RawString> {
                 Err(error) => decoder.warn(error),
             }
         }
-        Ok(LongStringValueLabelRecord(labels))
+        LongStringValueLabelRecord(labels)
     }
 }