use flate2::read::ZlibDecoder;
use num::Integer;
+use std::fmt::{Debug, Formatter, Result as FmtResult};
use std::str::from_utf8;
use std::{
collections::VecDeque,
ZLib,
}
+#[derive(Clone, Debug)]
pub enum Record {
Header(Header),
Document(Document),
}
}
+pub struct FallbackEncoding<'a>(&'a [u8]);
+
+impl<'a> Debug for FallbackEncoding<'a> {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ if let Ok(s) = from_utf8(self.0) {
+ let s = s.trim_end();
+ write!(f, "\"{s}\"")
+ } else {
+ let s: String = self
+ .0
+ .iter()
+ .map(|c| char::from(*c).escape_default())
+ .flatten()
+ .collect();
+ let s = s.trim_end();
+ write!(f, "\"{s}\"")
+ }
+ }
+}
+
+#[derive(Clone)]
pub struct Header {
/// Magic number.
pub magic: Magic,
pub endian: Endian,
}
+impl Header {
+ fn debug_field<T: Debug>(&self, f: &mut Formatter, name: &str, value: T) -> FmtResult {
+ writeln!(f, "{name:>17}: {:?}", value)
+ }
+}
+
+impl Debug for Header {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ writeln!(f, "File header record:")?;
+ self.debug_field(f, "Magic", self.magic)?;
+ self.debug_field(f, "Product name", FallbackEncoding(&self.eye_catcher))?;
+ self.debug_field(f, "Layout code", self.layout_code)?;
+ self.debug_field(f, "Nominal case size", self.nominal_case_size)?;
+ self.debug_field(f, "Compression", self.compression)?;
+ self.debug_field(f, "Weight index", self.weight_index)?;
+ self.debug_field(f, "Number of cases", self.n_cases)?;
+ self.debug_field(f, "Compression bias", self.bias)?;
+ self.debug_field(f, "Creation date", FallbackEncoding(&self.creation_date))?;
+ self.debug_field(f, "Creation time", FallbackEncoding(&self.creation_time))?;
+ self.debug_field(f, "File label", FallbackEncoding(&self.file_label))?;
+ self.debug_field(f, "Endianness", self.endian)
+ }
+}
+
impl Header {
fn read<R: Read>(r: &mut R) -> Result<Header, Error> {
let magic: [u8; 4] = read_bytes(r)?;
};
let weight_index: u32 = endian.parse(read_bytes(r)?);
- let weight_index = (weight_index > 0).then_some(weight_index - 1);
+ let weight_index = (weight_index > 0).then(|| weight_index - 1);
let n_cases: u32 = endian.parse(read_bytes(r)?);
let n_cases = (n_cases < i32::MAX as u32 / 2).then_some(n_cases);
pub const EBCDIC: Magic = Magic([0x5b, 0xc6, 0xd3, 0xf2]);
}
+impl Debug for Magic {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ let s = match self {
+ &Magic::SAV => "$FL2",
+ &Magic::ZSAV => "$FL3",
+ &Magic::EBCDIC => "($FL2 in EBCDIC)",
+ _ => return write!(f, "{:?}", self.0),
+ };
+ write!(f, "{s}")
+ }
+}
+
impl TryFrom<[u8; 4]> for Magic {
type Error = Error;
String([u8; 8]),
}
+impl Debug for Value {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ match self {
+ Value::Number(Some(number)) => write!(f, "{number:?}"),
+ Value::Number(None) => write!(f, "SYSMIS"),
+ Value::String(bytes) => write!(f, "{:?}", FallbackEncoding(bytes)),
+ }
+ }
+}
+
impl Value {
+ fn read<R: Read>(r: &mut R, var_type: VarType, endian: Endian) -> Result<Value, IoError> {
+ Ok(Self::from_raw(var_type, read_bytes(r)?, endian))
+ }
+
pub fn from_raw(var_type: VarType, raw: [u8; 8], endian: Endian) -> Value {
match var_type {
VarType::String => Value::String(raw),
impl FusedIterator for Reader {}
+#[derive(Copy, Clone, PartialEq, Eq, Hash)]
+pub struct Format(pub u32);
+
+impl Debug for Format {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ let type_ = format_name(self.0 >> 16);
+ let w = (self.0 >> 8) & 0xff;
+ let d = self.0 & 0xff;
+ write!(f, "{:06x} ({type_}{w}.{d})", self.0)
+ }
+}
+
+fn format_name(type_: u32) -> &'static str {
+ match type_ {
+ 1 => "A",
+ 2 => "AHEX",
+ 3 => "COMMA",
+ 4 => "DOLLAR",
+ 5 => "F",
+ 6 => "IB",
+ 7 => "PIBHEX",
+ 8 => "P",
+ 9 => "PIB",
+ 10 => "PK",
+ 11 => "RB",
+ 12 => "RBHEX",
+ 15 => "Z",
+ 16 => "N",
+ 17 => "E",
+ 20 => "DATE",
+ 21 => "TIME",
+ 22 => "DATETIME",
+ 23 => "ADATE",
+ 24 => "JDATE",
+ 25 => "DTIME",
+ 26 => "WKDAY",
+ 27 => "MONTH",
+ 28 => "MOYR",
+ 29 => "QYR",
+ 30 => "WKYR",
+ 31 => "PCT",
+ 32 => "DOT",
+ 33 => "CCA",
+ 34 => "CCB",
+ 35 => "CCC",
+ 36 => "CCD",
+ 37 => "CCE",
+ 38 => "EDATE",
+ 39 => "SDATE",
+ 40 => "MTIME",
+ 41 => "YMDHMS",
+ _ => "(unknown)",
+ }
+}
+
+#[derive(Clone)]
+pub struct MissingValues {
+ /// Individual missing values, up to 3 of them.
+ pub values: Vec<Value>,
+
+ /// Optional range of missing values.
+ pub range: Option<(Value, Value)>,
+}
+
+impl Debug for MissingValues {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ for (i, value) in self.values.iter().enumerate() {
+ if i > 0 {
+ write!(f, ", ")?;
+ }
+ write!(f, "{value:?}")?;
+ }
+
+ if let Some((low, high)) = self.range {
+ if !self.values.is_empty() {
+ write!(f, ", ")?;
+ }
+ write!(f, "{low:?} THRU {high:?}")?;
+ }
+
+ if self.is_empty() {
+ write!(f, "none")?;
+ }
+
+ Ok(())
+ }
+}
+
+impl MissingValues {
+ fn is_empty(&self) -> bool {
+ self.values.is_empty() && self.range.is_none()
+ }
+
+ fn read<R: Read + Seek>(
+ r: &mut R,
+ offset: u64,
+ width: i32,
+ code: i32,
+ endian: Endian,
+ ) -> Result<MissingValues, Error> {
+ let (n_values, has_range) = match (width, code) {
+ (_, 0..=3) => (code, false),
+ (0, -2) => (0, true),
+ (0, -3) => (1, true),
+ (0, _) => return Err(Error::BadNumericMissingValueCode { offset, code }),
+ (_, _) => return Err(Error::BadStringMissingValueCode { offset, code }),
+ };
+
+ let var_type = VarType::from_width(width);
+
+ let mut values = Vec::new();
+ for _ in 0..n_values {
+ values.push(Value::read(r, var_type, endian)?);
+ }
+ let range = if has_range {
+ let low = Value::read(r, var_type, endian)?;
+ let high = Value::read(r, var_type, endian)?;
+ Some((low, high))
+ } else {
+ None
+ };
+ Ok(MissingValues { values, range })
+ }
+}
+
+#[derive(Clone)]
pub struct Variable {
/// Offset from the start of the file to the start of the record.
pub offset: u64,
/// Write format.
pub write_format: u32,
- /// Missing value code, one of -3, -2, 0, 1, 2, or 3.
- pub missing_value_code: i32,
-
- /// Raw missing values, up to 3 of them.
- pub missing: Vec<[u8; 8]>,
+ /// Missing values.
+ pub missing_values: MissingValues,
/// Optional variable label.
pub label: Option<Vec<u8>>,
}
+impl Debug for Variable {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ writeln!(
+ f,
+ "Width: {} ({})",
+ self.width,
+ if self.width > 0 {
+ "string"
+ } else if self.width == 0 {
+ "numeric"
+ } else {
+ "long string continuation record"
+ }
+ )?;
+ writeln!(f, "Print format: {:?}", Format(self.print_format))?;
+ writeln!(f, "Write format: {:?}", Format(self.write_format))?;
+ writeln!(f, "Name: {:?}", FallbackEncoding(&self.name))?;
+ writeln!(
+ f,
+ "Variable label: {:?}",
+ self.label
+ .as_ref()
+ .map(|label| FallbackEncoding(&label[..]))
+ )?;
+ writeln!(f, "Missing values: {:?}", self.missing_values)
+ }
+}
+
impl Variable {
fn read<R: Read + Seek>(r: &mut R, endian: Endian) -> Result<Variable, Error> {
let offset = r.stream_position()?;
}
};
- let mut missing = Vec::new();
- if missing_value_code != 0 {
- match (width, missing_value_code) {
- (0, -3 | -2 | 1 | 2 | 3) => (),
- (0, _) => {
- return Err(Error::BadNumericMissingValueCode {
- offset,
- code: missing_value_code,
- })
- }
- (_, 0..=3) => (),
- (_, _) => {
- return Err(Error::BadStringMissingValueCode {
- offset,
- code: missing_value_code,
- })
- }
- }
-
- for _ in 0..missing_value_code.abs() {
- missing.push(read_bytes(r)?);
- }
- }
+ let missing_values = MissingValues::read(r, offset, width, missing_value_code, endian)?;
Ok(Variable {
offset,
name,
print_format,
write_format,
- missing_value_code,
- missing,
+ missing_values,
label,
})
}
}
+#[derive(Clone, Debug)]
pub struct ValueLabel {
/// Offset from the start of the file to the start of the record.
pub offset: u64,
}
}
+#[derive(Clone, Debug)]
pub struct VarIndexes {
/// Offset from the start of the file to the start of the record.
pub offset: u64,
}
}
+#[derive(Clone, Debug)]
pub struct Document {
/// Offset from the start of the file to the start of the record.
pub pos: u64,
}
}
+#[derive(Clone, Debug)]
pub struct Extension {
/// Offset from the start of the file to the start of the record.
pub offset: u64,
}
}
+#[derive(Clone, Debug)]
pub struct ZHeader {
/// File offset to the start of the record.
pub offset: u64,
}
}
+#[derive(Clone, Debug)]
pub struct ZTrailer {
/// File offset to the start of the record.
pub offset: u64,
pub blocks: Vec<ZBlock>,
}
+#[derive(Clone, Debug)]
pub struct ZBlock {
/// Offset of block of data if simple compression were used.
pub uncompressed_ofs: u64,