warning improvements
authorBen Pfaff <blp@cs.stanford.edu>
Sun, 13 Jul 2025 20:23:42 +0000 (13:23 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Sun, 13 Jul 2025 20:25:55 +0000 (13:25 -0700)
33 files changed:
rust/pspp/src/identifier.rs
rust/pspp/src/sys/cooked.rs
rust/pspp/src/sys/raw.rs
rust/pspp/src/sys/raw/records.rs
rust/pspp/src/sys/testdata/bad_machine_float_info_size.expected
rust/pspp/src/sys/testdata/bad_machine_integer_info_count.expected
rust/pspp/src/sys/testdata/bad_variable_name_in_variable_value_pair.expected
rust/pspp/src/sys/testdata/compressed_data_other_bias.expected
rust/pspp/src/sys/testdata/duplicate_attribute_name.expected
rust/pspp/src/sys/testdata/duplicate_long_variable_name.expected
rust/pspp/src/sys/testdata/integer_overflows_in_long_string_missing_values.expected
rust/pspp/src/sys/testdata/invalid_long_string_missing_values.expected
rust/pspp/src/sys/testdata/missing_attribute_value.expected
rust/pspp/src/sys/testdata/multiple_response_sets_bad_counted_string.expected
rust/pspp/src/sys/testdata/multiple_response_sets_counted_string_bad_length.expected
rust/pspp/src/sys/testdata/multiple_response_sets_counted_string_missing_space.expected
rust/pspp/src/sys/testdata/multiple_response_sets_missing_label_source.expected
rust/pspp/src/sys/testdata/multiple_response_sets_missing_newline_after_variable_name.expected
rust/pspp/src/sys/testdata/multiple_response_sets_missing_space_after_c.expected
rust/pspp/src/sys/testdata/multiple_response_sets_missing_space_after_counted_string.expected
rust/pspp/src/sys/testdata/multiple_response_sets_missing_space_after_e.expected
rust/pspp/src/sys/testdata/multiple_response_sets_unexpected_label_source.expected
rust/pspp/src/sys/testdata/null_dereference_skipping_bad_extension_record_18.expected
rust/pspp/src/sys/testdata/unquoted_attribute_value.expected
rust/pspp/src/sys/testdata/value_label_variable_indexes_must_be_in_correct_range.expected
rust/pspp/src/sys/testdata/value_label_variable_indexes_must_not_be_long_string_continuation.expected
rust/pspp/src/sys/testdata/value_label_with_no_associated_variables.expected
rust/pspp/src/sys/testdata/variables_for_value_label_must_all_be_same_type.expected
rust/pspp/src/sys/testdata/wrong_display_alignment.expected
rust/pspp/src/sys/testdata/wrong_display_measurement_level.expected
rust/pspp/src/sys/testdata/wrong_display_parameter_count.expected
rust/pspp/src/sys/testdata/wrong_display_parameter_size.expected
rust/pspp/src/sys/testdata/zcompressed_data_uncompressed_size_block_size.expected

index b81ddfe5f3f4b24da24e4d186e9642b6fa921570..cc9b1f213efbb7965f9139ec2fcad450d9960ad4 100644 (file)
@@ -120,13 +120,13 @@ pub enum Error {
     #[error("\"!\" is not a valid identifier.")]
     Bang,
 
-    #[error("\"{0}\" may not be used as an identifier because it begins with disallowed character {1:?}.")]
-    BadFirstCharacter(String, char),
+    #[error("{string:?} may not be used as an identifier because it begins with disallowed character {c:?}.")]
+    BadFirstCharacter { string: String, c: char },
 
     #[error(
-        "\"{0}\" may not be used as an identifier because it contains disallowed character {1:?}."
+        "{string:?} may not be used as an identifier because it contains disallowed character {c:?}."
     )]
-    BadLaterCharacter(String, char),
+    BadLaterCharacter { string: String, c: char },
 
     #[error("Identifier \"{id}\" is {length} bytes in the encoding in use ({encoding}), which exceeds the {max}-byte limit.")]
     TooLong {
@@ -270,11 +270,17 @@ impl Identifier {
         let mut i = s.chars();
         let first = i.next().unwrap();
         if !first.may_start_id() {
-            return Err(Error::BadFirstCharacter(s.into(), first));
+            return Err(Error::BadFirstCharacter {
+                string: s.into(),
+                c: first,
+            });
         }
         for c in i {
             if !c.may_continue_id() {
-                return Err(Error::BadLaterCharacter(s.into(), c));
+                return Err(Error::BadLaterCharacter {
+                    string: s.into(),
+                    c,
+                });
             }
         }
         Ok(())
@@ -305,7 +311,10 @@ impl Identifier {
             _ => {
                 let s = self.0.into_inner();
                 let first = s.chars().next().unwrap();
-                Err(Error::BadFirstCharacter(s, first))
+                Err(Error::BadFirstCharacter {
+                    string: s,
+                    c: first,
+                })
             }
         }
     }
index 8a00f5ddd361d580b486d96692d4b7c27559db8e..2c87f74322f7204955ce2bc1b72e74eaf3379115 100644 (file)
@@ -757,7 +757,7 @@ impl Headers {
         for record in self
             .multiple_response
             .iter()
-            .flat_map(|record| record.0.iter())
+            .flat_map(|record| record.sets.iter())
         {
             match MultipleResponseSet::decode(&dictionary, record, &mut warn) {
                 Ok(mrset) => {
@@ -889,7 +889,7 @@ impl Headers {
         for record in self
             .long_string_value_labels
             .drain(..)
-            .flat_map(|record| record.0.into_iter())
+            .flat_map(|record| record.labels.into_iter())
         {
             let Some((_, variable)) = dictionary.variables.get_full_mut2(&record.var_name.0) else {
                 warn(Error::UnknownLongStringValueLabelVariable(
@@ -914,7 +914,7 @@ impl Headers {
         for mut record in self
             .long_string_missing_values
             .drain(..)
-            .flat_map(|record| record.0.into_iter())
+            .flat_map(|record| record.values.into_iter())
         {
             let Some((_, variable)) = dictionary.variables.get_full_mut2(&record.var_name.0) else {
                 warn(Error::LongStringMissingValueUnknownVariable {
index 57143033d5dde9768b9f50c97f60941205ad8192..bc599c5af7b314d68cdd9fb09bf4e3407e341e8a 100644 (file)
@@ -253,65 +253,84 @@ pub enum ErrorDetails {
 ///
 /// Warnings indicate that something may be amiss, but they do not prevent
 /// reading further records.
+#[derive(Debug)]
+pub struct Warning {
+    /// Range of file offsets where the warning occurred.
+    offsets: Option<Range<u64>>,
+
+    /// Details of the warning.
+    details: WarningDetails,
+}
+
+impl std::error::Error for Warning {}
+
+impl Warning {
+    pub fn new(offsets: Option<Range<u64>>, details: WarningDetails) -> Self {
+        Self { offsets, details }
+    }
+}
+
+impl Display for Warning {
+    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+        if let Some(offsets) = &self.offsets
+            && !offsets.is_empty()
+        {
+            if offsets.end > offsets.start.wrapping_add(1) {
+                write!(
+                    f,
+                    "Warning at file offsets {:#x} to {:#x}: ",
+                    offsets.start, offsets.end
+                )?;
+            } else {
+                write!(f, "Warning at file offset {:#x}: ", offsets.start)?;
+            }
+        }
+        write!(f, "{}", &self.details)
+    }
+}
+
 #[derive(ThisError, Debug)]
-pub enum Warning {
+pub enum WarningDetails {
     #[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."
+        "At least one valid variable index for value labels is required but none were specified."
     )]
-    NoVarIndexes { offset: u64 },
+    NoVarIndexes,
 
-    #[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)]
+    #[error("The first variable index is for a {var_type} variable but the following variable indexes are for {} variables: {wrong_types:?}", !var_type)]
     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}] or referred to string continuations: {invalid:?}"
+        "One or more variable indexes for value labels were not in the valid range [1,{max}] or referred to string continuations: {invalid:?}"
     )]
-    InvalidVarIndexes {
-        offset: u64,
-        max: usize,
-        invalid: Vec<u32>,
-    },
+    InvalidVarIndexes { max: usize, invalid: Vec<u32> },
 
-    #[error(
-        "At offset {offset:#x}, {record} has bad size {size} bytes instead of the expected {expected_size}."
-    )]
+    #[error("{record} has bad size {size} bytes instead of the expected {expected_size}.")]
     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}."
-    )]
+    #[error("{record} has bad count {count} instead of the expected {expected_count}.")]
     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."
+        "In long string missing values record, value length at offset {offset:#x} is {value_len} instead of the expected 8."
     )]
-    BadLongMissingValueLength {
-        record_offset: u64,
-        offset: u64,
-        value_len: u32,
-    },
+    BadLongMissingValueLength { 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 },
+    #[error("Encoding record contains an encoding name that is not valid UTF-8.")]
+    BadEncodingName,
 
     // XXX This is risky because `text` might be arbitarily long.
     #[error("Text string contains invalid bytes for {encoding} encoding: {text:?}")]
@@ -447,7 +466,7 @@ pub enum Warning {
     TBD,
 }
 
-impl From<IoError> for Warning {
+impl From<IoError> for WarningDetails {
     fn from(_source: IoError) -> Self {
         Self::UnexpectedEndOfData
     }
@@ -720,7 +739,7 @@ pub fn infer_encoding(
         Ok(encoding) => Ok(encoding),
         Err(err @ EncodingError::Ebcdic) => Err(Error::new(None, ErrorDetails::EncodingError(err))),
         Err(err) => {
-            warn(Warning::EncodingError(err));
+            warn(Warning::new(None, WarningDetails::EncodingError(err)));
             // Warn that we're using the default encoding.
             Ok(default_encoding())
         }
@@ -772,10 +791,13 @@ impl<'de> Decoder<'de> {
     fn decode_slice<'a>(&mut self, input: &'a [u8]) -> Cow<'a, str> {
         let (output, malformed) = self.encoding.decode_without_bom_handling(input);
         if malformed {
-            self.warn(Warning::MalformedString {
-                encoding: self.encoding.name().into(),
-                text: output.clone().into(),
-            });
+            self.warn(Warning::new(
+                None,
+                WarningDetails::MalformedString {
+                    encoding: self.encoding.name().into(),
+                    text: output.clone().into(),
+                },
+            ));
         }
         output
     }
index 063912b89e57aa06816c04a94c0ada7fc42c8fb3..bf7e7cdfc8054c37fe20b0cfb3befb9f81c7146a 100644 (file)
@@ -21,7 +21,7 @@ use crate::{
     identifier::{Error as IdError, Identifier},
     sys::raw::{
         read_bytes, read_string, read_vec, DecodedRecord, Decoder, Error, ErrorDetails, Magic,
-        RawDatum, RawStrArray, RawWidth, Record, UntypedDatum, VarTypes, Warning,
+        RawDatum, RawStrArray, RawWidth, Record, UntypedDatum, VarTypes, Warning, WarningDetails,
     },
 };
 
@@ -179,7 +179,10 @@ impl HeaderRecord<RawString> {
         let n_cases = (header.n_cases < i32::MAX as u32 / 2).then_some(header.n_cases);
 
         if header.bias != 100.0 && header.bias != 0.0 {
-            warn(Warning::UnexpectedBias(header.bias));
+            warn(Warning::new(
+                Some(84..92),
+                WarningDetails::UnexpectedBias(header.bias),
+            ));
         }
 
         let creation_date = RawString(header.creation_date.into());
@@ -310,21 +313,29 @@ impl MissingValues {
             }
         };
 
-        Self::read_inner(r, raw_width, individual_values, has_range, endian, warn).map_err(
-            |details| {
-                Error::new(
-                    {
-                        let n = individual_values + if has_range { 2 } else { 0 };
-                        Some(offsets.start..offsets.end + 8 * n as u64)
-                    },
-                    details,
-                )
-            },
+        Self::read_inner(
+            r,
+            offsets.clone(),
+            raw_width,
+            individual_values,
+            has_range,
+            endian,
+            warn,
         )
+        .map_err(|details| {
+            Error::new(
+                {
+                    let n = individual_values + if has_range { 2 } else { 0 };
+                    Some(offsets.start..offsets.end + 8 * n as u64)
+                },
+                details,
+            )
+        })
     }
 
     fn read_inner<R>(
         r: &mut R,
+        offsets: Range<u64>,
         raw_width: RawWidth,
         individual_values: usize,
         has_range: bool,
@@ -358,7 +369,10 @@ impl MissingValues {
                 });
                 return Ok(Self::new(values, range).unwrap());
             }
-            Ok(VarWidth::String(_)) if range.is_some() => warn(Warning::MissingValueStringRange),
+            Ok(VarWidth::String(_)) if range.is_some() => warn(Warning::new(
+                Some(offsets),
+                WarningDetails::MissingValueStringRange,
+            )),
             Ok(VarWidth::String(width)) => {
                 let width = width.min(8) as usize;
                 let values = values
@@ -367,7 +381,10 @@ impl MissingValues {
                     .collect();
                 return Ok(Self::new(values, None).unwrap());
             }
-            Err(()) => warn(Warning::MissingValueContinuation),
+            Err(()) => warn(Warning::new(
+                Some(offsets),
+                WarningDetails::MissingValueContinuation,
+            )),
         }
         Ok(Self::default())
     }
@@ -602,18 +619,17 @@ impl ValueLabelRecord<RawDatum, RawString> {
         }
 
         let n: u32 = endian.parse(read_bytes(r)?);
+        let n_offsets = index_offset + 4..index_offset + 8;
         if n > Self::MAX_INDEXES {
             return Err(Error::new(
-                Some(index_offset + 4..index_offset + 8),
+                Some(n_offsets),
                 ErrorDetails::TooManyVarIndexes {
                     n,
                     max: Self::MAX_INDEXES,
                 },
             ));
         } else if n == 0 {
-            warn(Warning::NoVarIndexes {
-                offset: index_offset,
-            });
+            warn(Warning::new(Some(n_offsets), WarningDetails::NoVarIndexes));
             return Ok(None);
         }
 
@@ -628,12 +644,15 @@ impl ValueLabelRecord<RawDatum, RawString> {
                 invalid_indexes.push(index);
             }
         }
+        let index_offsets = index_offset..r.stream_position()?;
         if !invalid_indexes.is_empty() {
-            warn(Warning::InvalidVarIndexes {
-                offset: index_offset,
-                max: var_types.n_values(),
-                invalid: invalid_indexes,
-            });
+            warn(Warning::new(
+                Some(index_offsets.clone()),
+                WarningDetails::InvalidVarIndexes {
+                    max: var_types.n_values(),
+                    invalid: invalid_indexes,
+                },
+            ));
         }
 
         let Some(&first_index) = dict_indexes.first() else {
@@ -650,11 +669,13 @@ impl ValueLabelRecord<RawDatum, RawString> {
             }
         });
         if !wrong_type_indexes.is_empty() {
-            warn(Warning::MixedVarTypes {
-                offset: index_offset,
-                var_type,
-                wrong_types: wrong_type_indexes,
-            });
+            warn(Warning::new(
+                Some(index_offsets),
+                WarningDetails::MixedVarTypes {
+                    var_type,
+                    wrong_types: wrong_type_indexes,
+                },
+            ));
         }
 
         let labels = labels
@@ -780,7 +801,7 @@ static INTEGER_INFO_RECORD: ExtensionRecord = ExtensionRecord {
 };
 
 impl IntegerInfoRecord {
-    pub fn parse(ext: &Extension, endian: Endian) -> Result<Record, Warning> {
+    pub fn parse(ext: &Extension, endian: Endian) -> Result<Record, WarningDetails> {
         ext.check_size(&INTEGER_INFO_RECORD)?;
 
         let mut input = &ext.data[..];
@@ -806,7 +827,7 @@ static FLOAT_INFO_RECORD: ExtensionRecord = ExtensionRecord {
 };
 
 impl FloatInfoRecord {
-    pub fn parse(ext: &Extension, endian: Endian) -> Result<Record, Warning> {
+    pub fn parse(ext: &Extension, endian: Endian) -> Result<Record, WarningDetails> {
         ext.check_size(&FLOAT_INFO_RECORD)?;
 
         let mut input = &ext.data[..];
@@ -832,7 +853,7 @@ pub struct FloatInfoRecord {
 pub struct RawLongNamesRecord(TextRecord);
 
 impl RawLongNamesRecord {
-    pub fn parse(extension: Extension) -> Result<Record, Warning> {
+    pub fn parse(extension: Extension) -> Result<Record, WarningDetails> {
         Ok(Record::LongNames(Self(TextRecord::parse(
             extension,
             "long names record",
@@ -842,7 +863,8 @@ impl RawLongNamesRecord {
         let input = decoder.decode(&self.0.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).issue_warning(&mut decoder.warn)
+            if let Some(long_name) =
+                LongName::parse(pair, decoder).issue_warning(&self.0.offsets, &mut decoder.warn)
             {
                 names.push(long_name);
             }
@@ -860,7 +882,7 @@ pub struct TextRecord {
 }
 
 impl TextRecord {
-    pub fn parse(extension: Extension, name: &str) -> Result<TextRecord, Warning> {
+    pub fn parse(extension: Extension, name: &str) -> Result<TextRecord, WarningDetails> {
         extension.check_size(&ExtensionRecord {
             size: Some(1),
             count: None,
@@ -880,17 +902,17 @@ pub struct VeryLongString {
 }
 
 impl VeryLongString {
-    fn parse(decoder: &Decoder, input: &str) -> Result<VeryLongString, Warning> {
+    fn parse(decoder: &Decoder, input: &str) -> Result<VeryLongString, WarningDetails> {
         let Some((short_name, length)) = input.split_once('=') else {
-            return Err(Warning::VeryLongStringMissingDelimiter(input.into()));
+            return Err(WarningDetails::VeryLongStringMissingDelimiter(input.into()));
         };
         let short_name = decoder
             .new_identifier(short_name)
             .and_then(Identifier::must_be_ordinary)
-            .map_err(Warning::InvalidLongStringName)?;
+            .map_err(WarningDetails::InvalidLongStringName)?;
         let length = length
             .parse()
-            .map_err(|_| Warning::VeryLongStringInvalidLength(input.into()))?;
+            .map_err(|_| WarningDetails::VeryLongStringInvalidLength(input.into()))?;
         Ok(VeryLongString { short_name, length })
     }
 }
@@ -902,7 +924,7 @@ pub struct RawVeryLongStringsRecord(TextRecord);
 pub struct VeryLongStringsRecord(pub Vec<VeryLongString>);
 
 impl RawVeryLongStringsRecord {
-    pub fn parse(extension: Extension) -> Result<Record, Warning> {
+    pub fn parse(extension: Extension) -> Result<Record, WarningDetails> {
         Ok(Record::VeryLongStrings(Self(TextRecord::parse(
             extension,
             "very long strings record",
@@ -916,8 +938,8 @@ impl RawVeryLongStringsRecord {
             .map(|s| s.trim_start_matches('\t'))
             .filter(|s| !s.is_empty())
         {
-            if let Some(vls) =
-                VeryLongString::parse(decoder, tuple).issue_warning(&mut decoder.warn)
+            if let Some(vls) = VeryLongString::parse(decoder, tuple)
+                .issue_warning(&self.0.offsets, &mut decoder.warn)
             {
                 very_long_strings.push(vls)
             }
@@ -936,7 +958,7 @@ pub enum MultipleResponseType {
 }
 
 impl MultipleResponseType {
-    fn parse(input: &[u8]) -> Result<(MultipleResponseType, &[u8]), Warning> {
+    fn parse(input: &[u8]) -> Result<(MultipleResponseType, &[u8]), WarningDetails> {
         let (mr_type, input) = match input.split_first() {
             Some((b'C', input)) => (MultipleResponseType::MultipleCategory, input),
             Some((b'D', input)) => {
@@ -955,7 +977,7 @@ impl MultipleResponseType {
                 } else if let Some(rest) = input.strip_prefix(b" 11 ") {
                     (CategoryLabels::VarLabels, rest)
                 } else {
-                    return Err(Warning::InvalidMultipleDichotomyLabelType);
+                    return Err(WarningDetails::InvalidMultipleDichotomyLabelType);
                 };
                 let (value, input) = parse_counted_string(input)?;
                 (
@@ -963,7 +985,7 @@ impl MultipleResponseType {
                     input,
                 )
             }
-            _ => return Err(Warning::InvalidMultipleResponseType),
+            _ => return Err(WarningDetails::InvalidMultipleResponseType),
         };
         Ok((mr_type, input))
     }
@@ -982,15 +1004,15 @@ where
 }
 
 impl MultipleResponseSet<RawString, RawString> {
-    fn parse(input: &[u8]) -> Result<(Self, &[u8]), Warning> {
+    fn parse(input: &[u8]) -> Result<(Self, &[u8]), WarningDetails> {
         let Some(equals) = input.iter().position(|&b| b == b'=') else {
-            return Err(Warning::MultipleResponseSyntaxError("missing `=`"));
+            return Err(WarningDetails::MultipleResponseSyntaxError("missing `=`"));
         };
         let (name, input) = input.split_at(equals);
         let input = input.strip_prefix(b"=").unwrap();
         let (mr_type, input) = MultipleResponseType::parse(input)?;
         let Some(input) = input.strip_prefix(b" ") else {
-            return Err(Warning::MultipleResponseSyntaxError(
+            return Err(WarningDetails::MultipleResponseSyntaxError(
                 "missing space after multiple response type",
             ));
         };
@@ -1000,7 +1022,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(Warning::MultipleResponseSyntaxError(
+                        return Err(WarningDetails::MultipleResponseSyntaxError(
                             "missing variable name delimiter",
                         ));
                     };
@@ -1011,7 +1033,7 @@ impl MultipleResponseSet<RawString, RawString> {
                     input = rest;
                 }
                 _ => {
-                    return Err(Warning::MultipleResponseSyntaxError(
+                    return Err(WarningDetails::MultipleResponseSyntaxError(
                         "missing space preceding variable name",
                     ));
                 }
@@ -1033,14 +1055,15 @@ impl MultipleResponseSet<RawString, RawString> {
 
     fn decode(
         &self,
+        offsets: &Range<u64>,
         decoder: &mut Decoder,
-    ) -> Result<MultipleResponseSet<Identifier, String>, Warning> {
+    ) -> Result<MultipleResponseSet<Identifier, String>, WarningDetails> {
         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(Warning::InvalidMrSetName)
-                .issue_warning(&mut decoder.warn)
+                .map_err(WarningDetails::InvalidMrSetName)
+                .issue_warning(offsets, &mut decoder.warn)
             {
                 short_names.push(short_name);
             }
@@ -1048,7 +1071,7 @@ impl MultipleResponseSet<RawString, RawString> {
         Ok(MultipleResponseSet {
             name: decoder
                 .decode_identifier(&self.name)
-                .map_err(Warning::InvalidMrSetVariableName)?,
+                .map_err(WarningDetails::InvalidMrSetVariableName)?,
             label: decoder.decode(&self.label).to_string(),
             mr_type: self.mr_type.clone(),
             short_names,
@@ -1057,10 +1080,14 @@ impl MultipleResponseSet<RawString, RawString> {
 }
 
 #[derive(Clone, Debug)]
-pub struct MultipleResponseRecord<I, S>(pub Vec<MultipleResponseSet<I, S>>)
+pub struct MultipleResponseRecord<I, S>
 where
     I: Debug,
-    S: Debug;
+    S: Debug,
+{
+    pub offsets: Range<u64>,
+    pub sets: Vec<MultipleResponseSet<I, S>>,
+}
 
 static MULTIPLE_RESPONSE_RECORD: ExtensionRecord = ExtensionRecord {
     size: Some(1),
@@ -1069,7 +1096,7 @@ static MULTIPLE_RESPONSE_RECORD: ExtensionRecord = ExtensionRecord {
 };
 
 impl MultipleResponseRecord<RawString, RawString> {
-    fn parse(ext: &Extension, _endian: Endian) -> Result<Record, Warning> {
+    fn parse(ext: &Extension, _endian: Endian) -> Result<Record, WarningDetails> {
         ext.check_size(&MULTIPLE_RESPONSE_RECORD)?;
 
         let mut input = &ext.data[..];
@@ -1085,58 +1112,67 @@ impl MultipleResponseRecord<RawString, RawString> {
             sets.push(set);
             input = rest;
         }
-        Ok(Record::MultipleResponse(MultipleResponseRecord(sets)))
+        Ok(Record::MultipleResponse(MultipleResponseRecord {
+            offsets: ext.offsets.clone(),
+            sets,
+        }))
     }
 }
 
 impl MultipleResponseRecord<RawString, RawString> {
     pub fn decode(self, decoder: &mut Decoder) -> DecodedRecord {
         let mut sets = Vec::new();
-        for set in self.0.iter() {
-            if let Some(set) = set.decode(decoder).issue_warning(&mut decoder.warn) {
+        for set in self.sets.iter() {
+            if let Some(set) = set
+                .decode(&self.offsets, decoder)
+                .issue_warning(&self.offsets, &mut decoder.warn)
+            {
                 sets.push(set);
             }
         }
-        DecodedRecord::MultipleResponse(MultipleResponseRecord(sets))
+        DecodedRecord::MultipleResponse(MultipleResponseRecord {
+            offsets: self.offsets,
+            sets,
+        })
     }
 }
 
-fn parse_counted_string(input: &[u8]) -> Result<(RawString, &[u8]), Warning> {
+fn parse_counted_string(input: &[u8]) -> Result<(RawString, &[u8]), WarningDetails> {
     let Some(space) = input.iter().position(|&b| b == b' ') else {
-        return Err(Warning::CountedStringMissingSpace);
+        return Err(WarningDetails::CountedStringMissingSpace);
     };
     let Ok(length) = from_utf8(&input[..space]) else {
-        return Err(Warning::CountedStringInvalidUTF8);
+        return Err(WarningDetails::CountedStringInvalidUTF8);
     };
     let Ok(length): Result<usize, _> = length.parse() else {
-        return Err(Warning::CountedStringInvalidLength(length.into()));
+        return Err(WarningDetails::CountedStringInvalidLength(length.into()));
     };
 
     let Some((string, rest)) = input[space + 1..].split_at_checked(length) else {
-        return Err(Warning::CountedStringTooLong(length));
+        return Err(WarningDetails::CountedStringTooLong(length));
     };
     Ok((string.into(), rest))
 }
 
 impl Measure {
-    fn try_decode(source: u32) -> Result<Option<Measure>, Warning> {
+    fn try_decode(source: u32) -> Result<Option<Measure>, WarningDetails> {
         match source {
             0 => Ok(None),
             1 => Ok(Some(Measure::Nominal)),
             2 => Ok(Some(Measure::Ordinal)),
             3 => Ok(Some(Measure::Scale)),
-            _ => Err(Warning::InvalidMeasurement(source)),
+            _ => Err(WarningDetails::InvalidMeasurement(source)),
         }
     }
 }
 
 impl Alignment {
-    fn try_decode(source: u32) -> Result<Option<Alignment>, Warning> {
+    fn try_decode(source: u32) -> Result<Option<Alignment>, WarningDetails> {
         match source {
             0 => Ok(Some(Alignment::Left)),
             1 => Ok(Some(Alignment::Right)),
             2 => Ok(Some(Alignment::Center)),
-            _ => Err(Warning::InvalidAlignment(source)),
+            _ => Err(WarningDetails::InvalidAlignment(source)),
         }
     }
 }
@@ -1157,10 +1193,9 @@ impl VarDisplayRecord {
         var_types: &VarTypes,
         endian: Endian,
         warn: &mut dyn FnMut(Warning),
-    ) -> Result<Record, Warning> {
+    ) -> Result<Record, WarningDetails> {
         if ext.size != 4 {
-            return Err(Warning::BadRecordSize {
-                offset: ext.offsets.start,
+            return Err(WarningDetails::BadRecordSize {
                 record: String::from("variable display record"),
                 size: ext.size,
                 expected_size: 4,
@@ -1173,7 +1208,7 @@ impl VarDisplayRecord {
         } else if ext.count as usize == 2 * n_vars {
             false
         } else {
-            return Err(Warning::InvalidVariableDisplayCount {
+            return Err(WarningDetails::InvalidVariableDisplayCount {
                 count: ext.count as usize,
                 first: 2 * n_vars,
                 second: 3 * n_vars,
@@ -1184,11 +1219,11 @@ impl VarDisplayRecord {
         let mut input = &ext.data[..];
         for _ in 0..n_vars {
             let measure = Measure::try_decode(endian.parse(read_bytes(&mut input).unwrap()))
-                .issue_warning(warn)
+                .issue_warning(&ext.offsets, 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()))
-                .issue_warning(warn)
+                .issue_warning(&ext.offsets, warn)
                 .flatten();
             var_displays.push(VarDisplay {
                 measure,
@@ -1225,9 +1260,13 @@ impl LongStringMissingValues<RawString> {
 }
 
 #[derive(Clone, Debug)]
-pub struct LongStringMissingValueRecord<N>(pub Vec<LongStringMissingValues<N>>)
+pub struct LongStringMissingValueRecord<N>
 where
-    N: Debug;
+    N: Debug,
+{
+    pub offsets: Range<u64>,
+    pub values: Vec<LongStringMissingValues<N>>,
+}
 
 static LONG_STRING_MISSING_VALUE_RECORD: ExtensionRecord = ExtensionRecord {
     size: Some(1),
@@ -1240,7 +1279,7 @@ impl LongStringMissingValueRecord<RawString> {
         ext: &Extension,
         endian: Endian,
         warn: &mut dyn FnMut(Warning),
-    ) -> Result<Record, Warning> {
+    ) -> Result<Record, WarningDetails> {
         ext.check_size(&LONG_STRING_MISSING_VALUE_RECORD)?;
 
         let mut input = &ext.data[..];
@@ -1251,11 +1290,10 @@ impl LongStringMissingValueRecord<RawString> {
             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;
-                warn(Warning::BadLongMissingValueLength {
-                    record_offset: ext.offsets.start,
-                    offset,
-                    value_len,
-                });
+                warn(Warning::new(
+                    Some(ext.offsets.clone()),
+                    WarningDetails::BadLongMissingValueLength { offset, value_len },
+                ));
                 read_vec(&mut input, value_len as usize * n_missing_values as usize)?;
                 continue;
             }
@@ -1281,24 +1319,30 @@ impl LongStringMissingValueRecord<RawString> {
             });
         }
         Ok(Record::LongStringMissingValues(
-            LongStringMissingValueRecord(missing_value_set),
+            LongStringMissingValueRecord {
+                offsets: ext.offsets.clone(),
+                values: missing_value_set,
+            },
         ))
     }
 }
 
 impl LongStringMissingValueRecord<RawString> {
     pub fn decode(self, decoder: &mut Decoder) -> LongStringMissingValueRecord<Identifier> {
-        let mut mvs = Vec::with_capacity(self.0.len());
-        for mv in self.0.iter() {
+        let mut mvs = Vec::with_capacity(self.values.len());
+        for mv in self.values.iter() {
             if let Some(mv) = mv
                 .decode(decoder)
-                .map_err(Warning::InvalidLongStringMissingValueVariableName)
-                .issue_warning(&mut decoder.warn)
+                .map_err(WarningDetails::InvalidLongStringMissingValueVariableName)
+                .issue_warning(&self.offsets, &mut decoder.warn)
             {
                 mvs.push(mv);
             }
         }
-        LongStringMissingValueRecord(mvs)
+        LongStringMissingValueRecord {
+            offsets: self.offsets,
+            values: mvs,
+        }
     }
 }
 
@@ -1312,13 +1356,11 @@ static ENCODING_RECORD: ExtensionRecord = ExtensionRecord {
 };
 
 impl EncodingRecord {
-    fn parse(ext: &Extension, _endian: Endian) -> Result<Record, Warning> {
+    fn parse(ext: &Extension, _endian: Endian) -> Result<Record, WarningDetails> {
         ext.check_size(&ENCODING_RECORD)?;
 
         Ok(Record::Encoding(EncodingRecord(
-            String::from_utf8(ext.data.clone()).map_err(|_| Warning::BadEncodingName {
-                offset: ext.offsets.start,
-            })?,
+            String::from_utf8(ext.data.clone()).map_err(|_| WarningDetails::BadEncodingName)?,
         )))
     }
 }
@@ -1339,7 +1381,7 @@ static NUMBER_OF_CASES_RECORD: ExtensionRecord = ExtensionRecord {
 };
 
 impl NumberOfCasesRecord {
-    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Warning> {
+    fn parse(ext: &Extension, endian: Endian) -> Result<Record, WarningDetails> {
         ext.check_size(&NUMBER_OF_CASES_RECORD)?;
 
         let mut input = &ext.data[..];
@@ -1354,7 +1396,7 @@ impl NumberOfCasesRecord {
 pub struct RawVariableSetRecord(TextRecord);
 
 impl RawVariableSetRecord {
-    fn parse(extension: Extension) -> Result<Record, Warning> {
+    fn parse(extension: Extension) -> Result<Record, WarningDetails> {
         Ok(Record::VariableSets(Self(TextRecord::parse(
             extension,
             "variable sets record",
@@ -1364,7 +1406,9 @@ impl RawVariableSetRecord {
         let mut sets = Vec::new();
         let input = decoder.decode(&self.0.text);
         for line in input.lines() {
-            if let Some(set) = VariableSet::parse(line, decoder).issue_warning(&mut decoder.warn) {
+            if let Some(set) = VariableSet::parse(line, decoder, &self.0.offsets)
+                .issue_warning(&self.0.offsets, &mut decoder.warn)
+            {
                 sets.push(set)
             }
         }
@@ -1379,7 +1423,7 @@ impl RawVariableSetRecord {
 pub struct RawProductInfoRecord(TextRecord);
 
 impl RawProductInfoRecord {
-    fn parse(extension: Extension) -> Result<Record, Warning> {
+    fn parse(extension: Extension) -> Result<Record, WarningDetails> {
         Ok(Record::ProductInfo(Self(TextRecord::parse(
             extension,
             "product info record",
@@ -1397,17 +1441,21 @@ pub struct Attribute {
 }
 
 impl Attribute {
-    fn parse<'a>(decoder: &mut Decoder, input: &'a str) -> Result<(Attribute, &'a str), Warning> {
+    fn parse<'a>(
+        decoder: &mut Decoder,
+        offsets: &Range<u64>,
+        input: &'a str,
+    ) -> Result<(Attribute, &'a str), WarningDetails> {
         let Some((name, mut input)) = input.split_once('(') else {
-            return Err(Warning::AttributeMissingLParen(input.into()));
+            return Err(WarningDetails::AttributeMissingLParen(input.into()));
         };
         let name = decoder
             .new_identifier(name)
-            .map_err(Warning::InvalidAttributeName)?;
+            .map_err(WarningDetails::InvalidAttributeName)?;
         let mut values = Vec::new();
         loop {
             let Some((value, rest)) = input.split_once('\n') else {
-                return Err(Warning::AttributeMissingValue {
+                return Err(WarningDetails::AttributeMissingValue {
                     name: name.clone(),
                     index: values.len(),
                 });
@@ -1418,10 +1466,13 @@ impl Attribute {
             {
                 values.push(stripped.into());
             } else {
-                decoder.warn(Warning::AttributeMissingQuotes {
-                    name: name.clone(),
-                    index: values.len(),
-                });
+                decoder.warn(Warning::new(
+                    Some(offsets.clone()),
+                    WarningDetails::AttributeMissingQuotes {
+                        name: name.clone(),
+                        index: values.len(),
+                    },
+                ));
                 values.push(value.into());
             }
             if let Some(rest) = rest.strip_prefix(')') {
@@ -1436,9 +1487,10 @@ impl Attribute {
 impl Attributes {
     fn parse<'a>(
         decoder: &mut Decoder,
+        offsets: &Range<u64>,
         mut input: &'a str,
         sentinel: Option<char>,
-    ) -> Result<(Attributes, &'a str, Vec<Identifier>), Warning> {
+    ) -> Result<(Attributes, &'a str, Vec<Identifier>), WarningDetails> {
         let mut attributes = BTreeMap::new();
         let mut duplicates = Vec::new();
         let rest = loop {
@@ -1446,7 +1498,7 @@ impl Attributes {
                 None => break input,
                 c if c == sentinel => break &input[1..],
                 _ => {
-                    let (attribute, rest) = Attribute::parse(decoder, input)?;
+                    let (attribute, rest) = Attribute::parse(decoder, offsets, input)?;
                     if attributes.contains_key(&attribute.name) {
                         duplicates.push(attribute.name.clone());
                     }
@@ -1466,7 +1518,7 @@ pub struct RawFileAttributesRecord(TextRecord);
 pub struct FileAttributesRecord(pub Attributes);
 
 impl RawFileAttributesRecord {
-    fn parse(extension: Extension) -> Result<Record, Warning> {
+    fn parse(extension: Extension) -> Result<Record, WarningDetails> {
         Ok(Record::FileAttributes(Self(TextRecord::parse(
             extension,
             "file attributes record",
@@ -1474,15 +1526,23 @@ impl RawFileAttributesRecord {
     }
     pub fn decode(self, decoder: &mut Decoder) -> FileAttributesRecord {
         let input = decoder.decode(&self.0.text);
-        match Attributes::parse(decoder, &input, None).issue_warning(&mut decoder.warn) {
+        match Attributes::parse(decoder, &self.0.offsets, &input, None)
+            .issue_warning(&self.0.offsets, &mut decoder.warn)
+        {
             Some((set, rest, duplicates)) => {
                 if !duplicates.is_empty() {
-                    decoder.warn(Warning::DuplicateFileAttributes {
-                        attributes: duplicates,
-                    });
+                    decoder.warn(Warning::new(
+                        Some(self.0.offsets.clone()),
+                        WarningDetails::DuplicateFileAttributes {
+                            attributes: duplicates,
+                        },
+                    ));
                 }
                 if !rest.is_empty() {
-                    decoder.warn(dbg!(Warning::TBD));
+                    decoder.warn(Warning::new(
+                        Some(self.0.offsets.clone()),
+                        WarningDetails::TBD,
+                    ));
                 }
                 FileAttributesRecord(set)
             }
@@ -1500,21 +1560,25 @@ pub struct VarAttributes {
 impl VarAttributes {
     fn parse<'a>(
         decoder: &mut Decoder,
+        offsets: &Range<u64>,
         input: &'a str,
-    ) -> Result<(VarAttributes, &'a str), Warning> {
+    ) -> Result<(VarAttributes, &'a str), WarningDetails> {
         let Some((long_var_name, rest)) = input.split_once(':') else {
-            return Err(dbg!(Warning::TBD));
+            return Err(dbg!(WarningDetails::TBD));
         };
         let long_var_name = decoder
             .new_identifier(long_var_name)
             .and_then(Identifier::must_be_ordinary)
-            .map_err(Warning::InvalidAttributeVariableName)?;
-        let (attributes, rest, duplicates) = Attributes::parse(decoder, rest, Some('/'))?;
+            .map_err(WarningDetails::InvalidAttributeVariableName)?;
+        let (attributes, rest, duplicates) = Attributes::parse(decoder, offsets, rest, Some('/'))?;
         if !duplicates.is_empty() {
-            decoder.warn(Warning::DuplicateVariableAttributes {
-                variable: long_var_name.clone(),
-                attributes: duplicates,
-            });
+            decoder.warn(Warning::new(
+                Some(offsets.clone()),
+                WarningDetails::DuplicateVariableAttributes {
+                    variable: long_var_name.clone(),
+                    attributes: duplicates,
+                },
+            ));
         }
         let var_attribute = VarAttributes {
             long_var_name,
@@ -1531,7 +1595,7 @@ pub struct RawVariableAttributesRecord(TextRecord);
 pub struct VariableAttributesRecord(pub Vec<VarAttributes>);
 
 impl RawVariableAttributesRecord {
-    fn parse(extension: Extension) -> Result<Record, Warning> {
+    fn parse(extension: Extension) -> Result<Record, WarningDetails> {
         Ok(Record::VariableAttributes(Self(TextRecord::parse(
             extension,
             "variable attributes record",
@@ -1542,8 +1606,8 @@ impl RawVariableAttributesRecord {
         let mut input = decoded.as_ref();
         let mut var_attribute_sets = Vec::new();
         while !input.is_empty() {
-            let Some((var_attribute, rest)) =
-                VarAttributes::parse(decoder, input).issue_warning(&mut decoder.warn)
+            let Some((var_attribute, rest)) = VarAttributes::parse(decoder, &self.0.offsets, input)
+                .issue_warning(&self.0.offsets, &mut decoder.warn)
             else {
                 break;
             };
@@ -1561,18 +1625,18 @@ pub struct LongName {
 }
 
 impl LongName {
-    fn parse(input: &str, decoder: &Decoder) -> Result<Self, Warning> {
+    fn parse(input: &str, decoder: &Decoder) -> Result<Self, WarningDetails> {
         let Some((short_name, long_name)) = input.split_once('=') else {
-            return Err(Warning::LongNameMissingEquals);
+            return Err(WarningDetails::LongNameMissingEquals);
         };
         let short_name = decoder
             .new_identifier(short_name)
             .and_then(Identifier::must_be_ordinary)
-            .map_err(Warning::InvalidShortName)?;
+            .map_err(WarningDetails::InvalidShortName)?;
         let long_name = decoder
             .new_identifier(long_name)
             .and_then(Identifier::must_be_ordinary)
-            .map_err(Warning::InvalidLongName)?;
+            .map_err(WarningDetails::InvalidLongName)?;
         Ok(LongName {
             short_name,
             long_name,
@@ -1593,17 +1657,21 @@ pub struct VariableSet {
 }
 
 impl VariableSet {
-    fn parse(input: &str, decoder: &mut Decoder) -> Result<Self, Warning> {
+    fn parse(
+        input: &str,
+        decoder: &mut Decoder,
+        offsets: &Range<u64>,
+    ) -> Result<Self, WarningDetails> {
         let (name, input) = input
             .split_once('=')
-            .ok_or(Warning::VariableSetMissingEquals)?;
+            .ok_or(WarningDetails::VariableSetMissingEquals)?;
         let mut vars = Vec::new();
         for var in input.split_ascii_whitespace() {
             if let Some(identifier) = decoder
                 .new_identifier(var)
                 .and_then(Identifier::must_be_ordinary)
-                .map_err(Warning::InvalidVariableSetName)
-                .issue_warning(&mut decoder.warn)
+                .map_err(WarningDetails::InvalidVariableSetName)
+                .issue_warning(offsets, &mut decoder.warn)
             {
                 vars.push(identifier);
             }
@@ -1622,14 +1690,14 @@ pub struct VariableSetRecord {
 }
 
 trait IssueWarning<T> {
-    fn issue_warning(self, warn: &mut dyn FnMut(Warning)) -> Option<T>;
+    fn issue_warning(self, offsets: &Range<u64>, warn: &mut dyn FnMut(Warning)) -> Option<T>;
 }
-impl<T> IssueWarning<T> for Result<T, Warning> {
-    fn issue_warning(self, warn: &mut dyn FnMut(Warning)) -> Option<T> {
+impl<T> IssueWarning<T> for Result<T, WarningDetails> {
+    fn issue_warning(self, offsets: &Range<u64>, warn: &mut dyn FnMut(Warning)) -> Option<T> {
         match self {
             Ok(result) => Some(result),
             Err(error) => {
-                warn(error);
+                warn(Warning::new(Some(offsets.clone()), error));
                 None
             }
         }
@@ -1654,11 +1722,10 @@ pub struct Extension {
 }
 
 impl Extension {
-    pub fn check_size(&self, expected: &ExtensionRecord) -> Result<(), Warning> {
+    pub fn check_size(&self, expected: &ExtensionRecord) -> Result<(), WarningDetails> {
         match expected.size {
             Some(expected_size) if self.size != expected_size => {
-                return Err(Warning::BadRecordSize {
-                    offset: self.offsets.start,
+                return Err(WarningDetails::BadRecordSize {
                     record: expected.name.into(),
                     size: self.size,
                     expected_size,
@@ -1668,8 +1735,7 @@ impl Extension {
         }
         match expected.count {
             Some(expected_count) if self.count != expected_count => {
-                return Err(Warning::BadRecordCount {
-                    offset: self.offsets.start,
+                return Err(WarningDetails::BadRecordCount {
                     record: expected.name.into(),
                     count: self.count,
                     expected_count,
@@ -1703,8 +1769,9 @@ impl Extension {
         let start_offset = r.stream_position()?;
         let data = read_vec(r, product as usize)?;
         let end_offset = start_offset + product as u64;
+        let offsets = start_offset..end_offset;
         let extension = Extension {
-            offsets: start_offset..end_offset,
+            offsets: offsets.clone(),
             subtype,
             size,
             count,
@@ -1729,8 +1796,8 @@ impl Extension {
         };
         match result {
             Ok(result) => Ok(Some(result)),
-            Err(error) => {
-                warn(error);
+            Err(details) => {
+                warn(Warning::new(Some(offsets), details));
                 Ok(None)
             }
         }
@@ -1753,10 +1820,10 @@ impl LongStringValueLabels<RawString, RawString> {
     fn decode(
         &self,
         decoder: &mut Decoder,
-    ) -> Result<LongStringValueLabels<Identifier, String>, Warning> {
+    ) -> Result<LongStringValueLabels<Identifier, String>, WarningDetails> {
         let var_name = decoder.decode(&self.var_name);
         let var_name = Identifier::from_encoding(var_name.trim_end(), decoder.encoding)
-            .map_err(Warning::InvalidLongStringValueLabelName)?;
+            .map_err(WarningDetails::InvalidLongStringValueLabelName)?;
 
         let mut labels = Vec::with_capacity(self.labels.len());
         for (value, label) in self.labels.iter() {
@@ -1773,10 +1840,14 @@ impl LongStringValueLabels<RawString, RawString> {
 }
 
 #[derive(Clone, Debug)]
-pub struct LongStringValueLabelRecord<N, S>(pub Vec<LongStringValueLabels<N, S>>)
+pub struct LongStringValueLabelRecord<N, S>
 where
     N: Debug,
-    S: Debug;
+    S: Debug,
+{
+    pub offsets: Range<u64>,
+    pub labels: Vec<LongStringValueLabels<N, S>>,
+}
 
 static LONG_STRING_VALUE_LABEL_RECORD: ExtensionRecord = ExtensionRecord {
     size: Some(1),
@@ -1785,7 +1856,7 @@ static LONG_STRING_VALUE_LABEL_RECORD: ExtensionRecord = ExtensionRecord {
 };
 
 impl LongStringValueLabelRecord<RawString, RawString> {
-    fn parse(ext: &Extension, endian: Endian) -> Result<Record, Warning> {
+    fn parse(ext: &Extension, endian: Endian) -> Result<Record, WarningDetails> {
         ext.check_size(&LONG_STRING_VALUE_LABEL_RECORD)?;
 
         let mut input = &ext.data[..];
@@ -1806,22 +1877,26 @@ impl LongStringValueLabelRecord<RawString, RawString> {
                 labels,
             })
         }
-        Ok(Record::LongStringValueLabels(LongStringValueLabelRecord(
-            label_set,
-        )))
+        Ok(Record::LongStringValueLabels(LongStringValueLabelRecord {
+            offsets: ext.offsets.clone(),
+            labels: label_set,
+        }))
     }
 }
 
 impl LongStringValueLabelRecord<RawString, RawString> {
     pub fn decode(self, decoder: &mut Decoder) -> LongStringValueLabelRecord<Identifier, String> {
-        let mut labels = Vec::with_capacity(self.0.len());
-        for label in &self.0 {
+        let mut labels = Vec::with_capacity(self.labels.len());
+        for label in &self.labels {
             match label.decode(decoder) {
                 Ok(set) => labels.push(set),
-                Err(error) => decoder.warn(error),
+                Err(error) => decoder.warn(Warning::new(Some(self.offsets.clone()), error)),
             }
         }
-        LongStringValueLabelRecord(labels)
+        LongStringValueLabelRecord {
+            offsets: self.offsets,
+            labels,
+        }
     }
 }
 
@@ -1976,6 +2051,9 @@ impl ZTrailer {
         let mut expected_uncmp_ofs = zheader.zheader_offset;
         let mut expected_cmp_ofs = zheader.zheader_offset + 24;
         for (index, block) in blocks.iter().enumerate() {
+            let block_start = start_offset + 24 + 24 * index as u64;
+            let block_offsets = block_start..block_start + 24;
+
             if block.uncompressed_ofs != expected_uncmp_ofs {
                 Err(ErrorDetails::ZlibTrailerBlockWrongUncmpOfs {
                     index,
@@ -1997,31 +2075,29 @@ impl ZTrailer {
             } else {
                 Ok(())
             }
-            .map_err(|details| {
-                Error::new(
-                    {
-                        let block_start = start_offset + 24 + 24 * index as u64;
-                        Some(block_start..block_start + 24)
-                    },
-                    details,
-                )
-            })?;
+            .map_err(|details| Error::new(Some(block_offsets.clone()), details))?;
 
             if index < blocks.len() - 1 {
                 if block.uncompressed_size != block_size {
-                    warn(Warning::ZlibTrailerBlockWrongSize {
-                        index,
-                        actual: block.uncompressed_size,
-                        expected: block_size,
-                    });
+                    warn(Warning::new(
+                        Some(block_offsets),
+                        WarningDetails::ZlibTrailerBlockWrongSize {
+                            index,
+                            actual: block.uncompressed_size,
+                            expected: block_size,
+                        },
+                    ));
                 }
             } else {
                 if block.uncompressed_size > block_size {
-                    warn(Warning::ZlibTrailerBlockTooBig {
-                        index,
-                        actual: block.uncompressed_size,
-                        max_expected: block_size,
-                    });
+                    warn(Warning::new(
+                        Some(block_offsets),
+                        WarningDetails::ZlibTrailerBlockTooBig {
+                            index,
+                            actual: block.uncompressed_size,
+                            max_expected: block_size,
+                        },
+                    ));
                 }
             }
 
index 973ebaac59ee5cee447533b189bf488ee81f06cb..5ebd48f3700abe71020c7bb089e8eb17cad1c929 100644 (file)
@@ -1,4 +1,4 @@
-At offset 0xe0, floating point record has bad count 4 instead of the expected 3.
+Warning at file offsets 0xe0 to 0x100: floating point record has bad count 4 instead of the expected 3.
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 72bda6df5185c7713b88ea920f8e05ea763eecd2..d08b9606ee46f001034109a7470510726657d813 100644 (file)
@@ -1,4 +1,4 @@
-At offset 0xe0, integer record has bad count 9 instead of the expected 8.
+Warning at file offsets 0xe0 to 0x104: integer record has bad count 9 instead of the expected 8.
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 2c10b1cc7516eeee068ed312e234df33a337f669..14471714749a3aa6734d1d5acc58580a31d03fe3 100644 (file)
@@ -1,4 +1,4 @@
-Missing `=` separator in long variable name record.
+Warning at file offsets 0xe0 to 0xe5: Missing `=` separator in long variable name record.
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index a2a09ddddd25f173fc6b20208b58121e999c1d7a..7b7f43ebd9fa5da1ac35858b0cf8b5641832dd15 100644 (file)
@@ -1,4 +1,4 @@
-Compression bias is 50 instead of the usual values of 0 or 100.
+Warning at file offsets 0x54 to 0x5c: Compression bias is 50 instead of the usual values of 0 or 100.
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 8de0af1193a33196307b583e26037f2dbe654c4b..ec072796535b8e3e40b55d9fcbbccb3e619d43dd 100644 (file)
@@ -1,6 +1,6 @@
-Duplicate dataset attributes with names: Attr1.
+Warning at file offsets 0xe0 to 0xfe: Duplicate dataset attributes with names: Attr1.
 
-Duplicate attributes for variable FIRSTVAR: fred.
+Warning at file offsets 0x10e to 0x12d: Duplicate attributes for variable FIRSTVAR: fred.
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 14947fd8677b3cf0d252d86e6abfb3e059948ba7..7a00f13ff310fbc061ff7d935629c0e62f436fc2 100644 (file)
@@ -1,8 +1,8 @@
-Invalid name in long variable name record.  "_Invalid" may not be used as an identifier because it begins with disallowed character '_'.
+Warning at file offsets 0x140 to 0x1aa: Invalid name in long variable name record.  "_Invalid" may not be used as an identifier because it begins with disallowed character '_'.
 
-Invalid name in long variable name record.  "$Invalid" may not be used as an identifier because it begins with disallowed character '$'.
+Warning at file offsets 0x140 to 0x1aa: Invalid name in long variable name record.  "$Invalid" may not be used as an identifier because it begins with disallowed character '$'.
 
-Invalid name in long variable name record.  "#Invalid" may not be used as an identifier because it begins with disallowed character '#'.
+Warning at file offsets 0x140 to 0x1aa: Invalid name in long variable name record.  "#Invalid" may not be used as an identifier because it begins with disallowed character '#'.
 
 Duplicate long variable name LONGVARIABLENAME.
 
index 10f60064aaa59c281b714805cac34b2bf77c8644..4dd9691195a69074db86c436397a483fb9016f23 100644 (file)
Binary files a/rust/pspp/src/sys/testdata/integer_overflows_in_long_string_missing_values.expected and b/rust/pspp/src/sys/testdata/integer_overflows_in_long_string_missing_values.expected differ
index 37499e8d596612d1e71119a8a284535c91656556..f749ae8de9ea888917bbe837846dca3a47050884 100644 (file)
@@ -1,4 +1,4 @@
-In long string missing values record starting at offset 0x238, value length at offset 0x2a8 is 12 instead of the expected 8.
+Warning at file offsets 0x238 to 0x2f2: In long string missing values record, value length at offset 0x2a8 is 12 instead of the expected 8.
 
 File header claims 8 variable positions but 9 were read from file.
 
index 3ced2e811c9128738337c32abf8cfbc7a21279d4..980b0dcd25c534b27c43ac8c72bd585d77a65a67 100644 (file)
@@ -1,6 +1,6 @@
-Attribute for Attr1[1] lacks value.
+Warning at file offsets 0xe0 to 0xe6: Attribute for Attr1[1] lacks value.
 
-Attribute for fred[2] lacks value.
+Warning at file offsets 0xf6 to 0x109: Attribute for fred[2] lacks value.
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 9b5bb88724ee0064982d011edaa884b6a516c669..477dc239682adac6c73632438688887fd21e9325 100644 (file)
@@ -1,4 +1,4 @@
-Syntax error parsing counted string (missing trailing space)
+Warning at file offsets 0xe0 to 0xe5: Syntax error parsing counted string (missing trailing space)
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 3a7ad8c3f6f189dbbcd9bc40d5bdd8a5af70c75a..ccd09bde382a166efa9a733206483e4fb1c5f939 100644 (file)
@@ -1,4 +1,4 @@
-Syntax error parsing counted string (length 4 goes past end of input)
+Warning at file offsets 0xe0 to 0xe9: Syntax error parsing counted string (length 4 goes past end of input)
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 9b5bb88724ee0064982d011edaa884b6a516c669..03591009150e86d423fdf78452955de83d28b5e3 100644 (file)
@@ -1,4 +1,4 @@
-Syntax error parsing counted string (missing trailing space)
+Warning at file offsets 0xe0 to 0xe6: Syntax error parsing counted string (missing trailing space)
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 01359aea993d74a1142b4abdb173214b94d1c7e8..19c5be342f25e70d3b2f48156240aede21a023f7 100644 (file)
@@ -1,4 +1,4 @@
-Invalid multiple dichotomy label type
+Warning at file offsets 0xe0 to 0xe5: Invalid multiple dichotomy label type
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 16dd47ddf178711e522f0258ba3937ec051a1b3a..94929e747c9e9a5474d7b7b056503adb42ee719b 100644 (file)
@@ -1,4 +1,4 @@
-Syntax error in multiple response record (missing variable name delimiter)
+Warning at file offsets 0xe0 to 0xec: Syntax error in multiple response record (missing variable name delimiter)
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 4fcc82f57f32214859680c3157991631fd3d774a..021c411872bb0b404ac368ce64895e3ea733a4c1 100644 (file)
@@ -1,4 +1,4 @@
-Syntax error in multiple response record (missing space after multiple response type)
+Warning at file offsets 0xe0 to 0xe5: Syntax error in multiple response record (missing space after multiple response type)
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 4fcc82f57f32214859680c3157991631fd3d774a..208826b8ba4e30f35ac34212b366e68e7b9e7842 100644 (file)
@@ -1,4 +1,4 @@
-Syntax error in multiple response record (missing space after multiple response type)
+Warning at file offsets 0xe0 to 0xea: Syntax error in multiple response record (missing space after multiple response type)
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 01359aea993d74a1142b4abdb173214b94d1c7e8..19c5be342f25e70d3b2f48156240aede21a023f7 100644 (file)
@@ -1,4 +1,4 @@
-Invalid multiple dichotomy label type
+Warning at file offsets 0xe0 to 0xe5: Invalid multiple dichotomy label type
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 01359aea993d74a1142b4abdb173214b94d1c7e8..f7f3dd6f0c0622aa412a508a4f552a46a78cefbd 100644 (file)
@@ -1,4 +1,4 @@
-Invalid multiple dichotomy label type
+Warning at file offsets 0xe0 to 0xe6: Invalid multiple dichotomy label type
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 950b17e8b60353e2e983bcfcd27bebb05d8f128f..5ed52fce1af9d2f18a110d0660a0feac08ee6e76 100644 (file)
@@ -1,4 +1,4 @@
-At offset 0x1c8, variable attributes record has bad size 4 bytes instead of the expected 1.
+Warning at file offsets 0x1c8 to 0x1e8: variable attributes record has bad size 4 bytes instead of the expected 1.
 
 This system file does not indicate its own character encoding.  For best results, specify an encoding explicitly.  Use SYSFILE INFO with ENCODING="DETECT" to analyze the possible encodings.
 
index 37611c1b4109aca9b69a646485cd190ee7566934..b6db9e1bc27968645a68ded0438383c3b98f3e36 100644 (file)
@@ -1,6 +1,6 @@
-Attribute for Attr1[1] missing quotations.
+Warning at file offsets 0xe0 to 0xed: Attribute for Attr1[1] missing quotations.
 
-Attribute for fred[1] missing quotations.
+Warning at file offsets 0xfd to 0x10f: Attribute for fred[1] missing quotations.
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index d7a6ca5e5e049a0925017facddf0ac0861874207..7b5bbb9115c7fe0c972f99fa6ba87bea0ea4c2cd 100644 (file)
@@ -1,8 +1,8 @@
-At offset 0x110, one or more variable indexes for value labels were not in the valid range [1,2] or referred to string continuations: [3, 4]
+Warning at file offsets 0x110 to 0x118: One or more variable indexes for value labels were not in the valid range [1,2] or referred to string continuations: [3, 4]
 
-At offset 0x138, one or more variable indexes for value labels were not in the valid range [1,2] or referred to string continuations: [5, 6]
+Warning at file offsets 0x138 to 0x140: One or more variable indexes for value labels were not in the valid range [1,2] or referred to string continuations: [5, 6]
 
-At offset 0x160, one or more variable indexes for value labels were not in the valid range [1,2] or referred to string continuations: [7, 8]
+Warning at file offsets 0x160 to 0x168: One or more variable indexes for value labels were not in the valid range [1,2] or referred to string continuations: [7, 8]
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index dd7424e7d781f4f87e3e302f2c431ef835f22117..54dba1a367362e3703295669af7d4a8ae9bc635e 100644 (file)
@@ -1,4 +1,4 @@
-At offset 0x110, one or more variable indexes for value labels were not in the valid range [1,2] or referred to string continuations: [2]
+Warning at file offsets 0x110 to 0x114: One or more variable indexes for value labels were not in the valid range [1,2] or referred to string continuations: [2]
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 283dbd7f1e5ce8af8472d42bb750f7c37d997e24..60cd0f5597e75357e7daaca35905ba0217ab7a12 100644 (file)
@@ -1,4 +1,4 @@
-At offset 0xe8, at least one valid variable index for value labels is required but none were specified.
+Warning at file offsets 0xec to 0xf0: At least one valid variable index for value labels is required but none were specified.
 
 This system file does not indicate its own character encoding.  For best results, specify an encoding explicitly.  Use SYSFILE INFO with ENCODING="DETECT" to analyze the possible encodings.
 
index d71b23b5ca5a1b5959225dc8bdb5bbac83f64750..749b0e9d0829c0d4b636c572f3f87d2a55741254 100644 (file)
@@ -1,4 +1,4 @@
-At offset 0x110, the first variable index is for a string variable but the following variable indexes are for numeric variables: [2]
+Warning at file offsets 0x110 to 0x118: The first variable index is for a string variable but the following variable indexes are for numeric variables: [2]
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 5a47058218264258a023c9b1085f4d3102a02007..866fd2adb464ddcb4914a17f18c65797feac5ecf 100644 (file)
@@ -1,4 +1,4 @@
-Invalid variable display alignment value 4294967295
+Warning at file offsets 0xe0 to 0xe8: Invalid variable display alignment value 4294967295
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 4dc2ab1937ecb40ff08de4cf2976ae74c902b284..a3878bcbd215c21ff46a97240f5f6b10cfe4ffa0 100644 (file)
@@ -1,4 +1,4 @@
-Invalid variable measurement level value 4
+Warning at file offsets 0xe0 to 0xe8: Invalid variable measurement level value 4
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 99a2db97cbbdb440e2a1bba24b00d4234b07dc85..7137241dfa564c35518360a3d44742ebc1d0cf6c 100644 (file)
@@ -1,4 +1,4 @@
-Variable display record contains 4 items but should contain either 2 or 3.
+Warning at file offsets 0xe0 to 0xf0: Variable display record contains 4 items but should contain either 2 or 3.
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 1fc8ac5f5a5044685093fe10749d34d5ad4ed55d..cff6b227e259d9b2ba315f6d4281c1c81d60cc12 100644 (file)
@@ -1,4 +1,4 @@
-At offset 0xe0, variable display record has bad size 8 bytes instead of the expected 4.
+Warning at file offsets 0xe0 to 0xf0: variable display record has bad size 8 bytes instead of the expected 4.
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│
index 0591a35cd75ebeaeace89c09134d8f2dc09fd1ff..d79d528952447e51bb3ae4de0e9f6380ccb94361 100644 (file)
@@ -1,4 +1,4 @@
-ZLIB block descriptor 0 reported block size 0x400000, when at most 0x3ff000 was expected.
+Warning at file offsets 0x1c4 to 0x1dc: ZLIB block descriptor 0 reported block size 0x400000, when at most 0x3ff000 was expected.
 
 ╭──────────────────────┬────────────────────────╮
 │       Created        │    01-JAN-2011 20:53:52│