From 3f8dc8eb2374f237d2ba7e1ad37982313fde3ee3 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Sun, 26 Apr 2026 11:34:38 -0700 Subject: [PATCH] add support for writing legacy data and a test for it --- rust/pspp/src/cli/spv.rs | 73 ++---- rust/pspp/src/file.rs | 11 + rust/pspp/src/spv/read/graph.rs | 20 +- rust/pspp/src/spv/read/legacy_bin.rs | 239 +++++++++++++++++- rust/pspp/src/spv/testdata/legacy-data1.bin | Bin 0 -> 5580 bytes .../src/spv/testdata/legacy-data1.expected | 39 +++ rust/pspp/src/spv/write.rs | 2 +- 7 files changed, 328 insertions(+), 56 deletions(-) create mode 100644 rust/pspp/src/spv/testdata/legacy-data1.bin create mode 100644 rust/pspp/src/spv/testdata/legacy-data1.expected diff --git a/rust/pspp/src/cli/spv.rs b/rust/pspp/src/cli/spv.rs index 361d0b1427..c48242aa2d 100644 --- a/rust/pspp/src/cli/spv.rs +++ b/rust/pspp/src/cli/spv.rs @@ -17,15 +17,16 @@ use anyhow::{Result, anyhow}; use clap::{Args, Subcommand}; use pspp::{ - file::ReadSeek, + file::{FileType, ReadSeek}, output::{ Criteria, Itemlike, SpvMembers, pivot::{Axis3, Dimension, Group, Leaf, PivotTable, value::Value}, }, spv::{ SpvArchive, + legacy_bin::{LegacyBin, data_pivot_table}, read::{ - legacy_xml::{Visualization, datum_as_format}, + legacy_xml::Visualization, structure::{Content, OutlineItem}, }, }, @@ -122,56 +123,28 @@ pub struct LegacyData { impl LegacyData { fn run(self) -> Result<()> { - let (mut archive, items) = self.input.read_outline()?; - for item in items { - for item in item.iter_preorder() { - if let Some(spv_info) = item.spv_info() - && let Some(members) = &spv_info.members - && let Some(binary) = members.bin_member() - { - let data = archive - .read_legacy_bin(binary, &mut |w| eprintln!("{w}")) - .map_err(|error| { - anyhow::Error::from(error).context(format!( - "While parsing {binary:?} as legacy binary SPV member" - )) - })?; - - let n_values = data - .values() - .flat_map(|map| map.values()) - .map(|values| values.data.len()) - .max() - .unwrap_or(0); - let index = Dimension::new( - Group::new("Index") - .with_multiple(Leaf::numbers(0..n_values)) - .with_label_shown(), - ); - let variables = Dimension::new(Group::new("Variables").with_multiple( - data.iter().map(|(name, contents)| { - Group::new(name.as_str()).with_multiple(contents.keys().map(|name| { - name.replace("categories", "\ncategories") - .replace("labels", "\nlabels") - .replace("group", "\ngroup") - .replace("Label", "\nLabel") - })) - }), - )); - let mut pivot_table = - PivotTable::new([(Axis3::Y, index), (Axis3::X, variables)]); - for (variable_index, (variable_name, values)) in - data.values().flat_map(|map| map.iter()).enumerate() + if let Some(FileType::LegacyBin) = FileType::from_file(&self.input.input)? { + let data = std::fs::read(&self.input.input)?; + let data = LegacyBin::from_data(&data, &mut |w| eprintln!("{w}"))?; + println!("{}", data_pivot_table(&data)); + } else { + let (mut archive, items) = self.input.read_outline()?; + for item in items { + for item in item.iter_preorder() { + if let Some(spv_info) = item.spv_info() + && let Some(members) = &spv_info.members + && let Some(binary) = members.bin_member() { - for (value_index, data_value) in values.data.iter().enumerate() { - let value = Value::new_datum(data_value).with_value_label( - (variable_name == "cellFormat") - .then(|| datum_as_format(data_value).to_string()), - ); - pivot_table.insert([value_index, variable_index], value); - } + let data = archive + .read_legacy_bin(binary, &mut |w| eprintln!("{w}")) + .map_err(|error| { + anyhow::Error::from(error).context(format!( + "While parsing {binary:?} as legacy binary SPV member" + )) + })?; + + println!("{}", data_pivot_table(&data)); } - println!("{pivot_table}"); } } } diff --git a/rust/pspp/src/file.rs b/rust/pspp/src/file.rs index 31f0bb1a40..adcfe2146c 100644 --- a/rust/pspp/src/file.rs +++ b/rust/pspp/src/file.rs @@ -74,6 +74,11 @@ pub enum FileType { /// A standalone graph exported in XML format. Graph, + + /// A legacy data binary file. + /// + /// Normally these are seen only inside SPV files. + LegacyBin, } impl Display for FileType { @@ -208,6 +213,10 @@ impl FileType { return Ok(Some(Self::Viewer { encrypted: false })); } + if buf.starts_with(&[0, 0xaf]) || buf.starts_with(&[0, 0xb0]) { + return Ok(Some(Self::LegacyBin)); + } + if !buf.is_empty() && !buf.contains(&0) { return Ok(Some(Self::Syntax { confident: buf.starts_with(b"* Encoding:"), @@ -233,6 +242,7 @@ impl FileType { FileType::Look(LookFormat::Binary) => "tlo", FileType::Look(LookFormat::Xml) => "stt", FileType::Graph => "xml", + FileType::LegacyBin => "bin", } } @@ -259,6 +269,7 @@ impl FileType { } => "encrypted SPSS syntax", FileType::Look(_) => "SPSS TableLook", FileType::Graph => "SPSS exported chart", + FileType::LegacyBin => "SPSS legacy binary data", } } } diff --git a/rust/pspp/src/spv/read/graph.rs b/rust/pspp/src/spv/read/graph.rs index 238e1fc1d2..1e22f9309d 100644 --- a/rust/pspp/src/spv/read/graph.rs +++ b/rust/pspp/src/spv/read/graph.rs @@ -488,6 +488,11 @@ impl Visualization { mut csv_data: Option>, warn: &mut dyn FnMut(GraphWarning), ) -> Result { + for child in &self.children { + if let Some(style_cycle) = child.style_cycle() { + dbg!(style_cycle); + } + } let _styles = self.decode_styles(); for delimited_source in self .children @@ -1154,6 +1159,12 @@ impl VisChild { _ => None, } } + fn style_cycle(&self) -> Option<&StyleCycle> { + match self { + Self::StyleCycle(style_cycle) => Some(style_cycle), + _ => None, + } + } } #[derive(Deserialize, Debug)] @@ -2380,14 +2391,14 @@ struct ValueMapEntry { #[derive(Clone, Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct StyleCycle { - #[serde(default)] + #[serde(default, rename = "cycle")] cycles: Vec, } #[derive(Clone, Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct Cycle { - #[serde(default)] + #[serde(default, rename = "style")] styles: Vec