From 5bd7fb2785dfd48bb2c5b20f8cf0f3249a821109 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Wed, 10 Dec 2025 11:25:10 -0800 Subject: [PATCH] Move spv reader and writer to top-level module. --- rust/pspp/src/cli/convert.rs | 4 +- rust/pspp/src/cli/show_spv.rs | 6 +-- rust/pspp/src/output.rs | 1 - rust/pspp/src/output/drivers/cairo/driver.rs | 22 ++++++----- rust/pspp/src/output/drivers/cairo/fsm.rs | 2 +- rust/pspp/src/output/drivers/cairo/pager.rs | 10 ++--- rust/pspp/src/output/page.rs | 2 +- rust/pspp/src/output/pivot.rs | 13 ++----- rust/pspp/src/output/table.rs | 5 +-- rust/pspp/src/spv.rs | 1 + rust/pspp/src/{output/spv.rs => spv/read.rs} | 38 +++++++++++-------- rust/pspp/src/{output/spv => spv/read}/css.rs | 13 +++---- .../pspp/src/{output/spv => spv/read}/html.rs | 2 +- .../{output/spv => spv/read}/legacy_bin.rs | 16 +++----- .../{output/spv => spv/read}/legacy_xml.rs | 26 +++---------- .../src/{output/spv => spv/read}/light.rs | 29 +++----------- rust/pspp/src/spv/write.rs | 27 ++++++++++--- 17 files changed, 93 insertions(+), 124 deletions(-) rename rust/pspp/src/{output/spv.rs => spv/read.rs} (96%) rename rust/pspp/src/{output/spv => spv/read}/css.rs (98%) rename rust/pspp/src/{output/spv => spv/read}/html.rs (99%) rename rust/pspp/src/{output/spv => spv/read}/legacy_bin.rs (95%) rename rust/pspp/src/{output/spv => spv/read}/legacy_xml.rs (99%) rename rust/pspp/src/{output/spv => spv/read}/light.rs (99%) diff --git a/rust/pspp/src/cli/convert.rs b/rust/pspp/src/cli/convert.rs index 26dc202964..3df207134d 100644 --- a/rust/pspp/src/cli/convert.rs +++ b/rust/pspp/src/cli/convert.rs @@ -23,7 +23,7 @@ use pspp::{ data::{ByteString, Case, Datum}, dictionary::Dictionary, file::FileType, - output::{Criteria, drivers::Driver, spv}, + output::{Criteria, drivers::Driver}, pc::PcFile, por::PortableFile, sys::ReadOptions, @@ -143,7 +143,7 @@ impl Convert { self.write_data(dictionary, cases) } Some(FileType::Viewer { .. }) => { - let (items, page_setup) = spv::ReadOptions::new() + let (items, page_setup) = pspp::spv::read::ReadOptions::new() .with_password(self.password.clone()) .open_file(&self.input)? .into_parts(); diff --git a/rust/pspp/src/cli/show_spv.rs b/rust/pspp/src/cli/show_spv.rs index 631c3177c8..2a068602e6 100644 --- a/rust/pspp/src/cli/show_spv.rs +++ b/rust/pspp/src/cli/show_spv.rs @@ -16,7 +16,7 @@ use anyhow::Result; use clap::{Args, ValueEnum}; -use pspp::output::{Criteria, Item, spv}; +use pspp::output::{Criteria, Item}; use std::{fmt::Display, path::PathBuf}; /// Show information about SPSS viewer files (SPV files). @@ -90,7 +90,7 @@ impl ShowSpv { pub fn run(self) -> Result<()> { match self.mode { Mode::Directory => { - let item = spv::ReadOptions::new() + let item = pspp::spv::read::ReadOptions::new() .with_password(self.password) .open_file(&self.input)? .into_items(); @@ -101,7 +101,7 @@ impl ShowSpv { Ok(()) } Mode::View => { - let item = spv::ReadOptions::new() + let item = pspp::spv::read::ReadOptions::new() .with_password(self.password) .open_file(&self.input)? .into_items(); diff --git a/rust/pspp/src/output.rs b/rust/pspp/src/output.rs index 1253116210..225051e6e2 100644 --- a/rust/pspp/src/output.rs +++ b/rust/pspp/src/output.rs @@ -46,7 +46,6 @@ pub mod drivers; pub mod page; pub mod pivot; pub mod render; -pub mod spv; pub mod table; /// A single output item. diff --git a/rust/pspp/src/output/drivers/cairo/driver.rs b/rust/pspp/src/output/drivers/cairo/driver.rs index 1fa4c3e019..8b8b29089f 100644 --- a/rust/pspp/src/output/drivers/cairo/driver.rs +++ b/rust/pspp/src/output/drivers/cairo/driver.rs @@ -27,18 +27,20 @@ use pango::SCALE; use paper_sizes::Unit; use serde::{Deserialize, Serialize}; -use crate::output::{ - Details, Item, ItemCursor, TextType, - drivers::{ - Driver, - cairo::{ - fsm::{CairoFsmStyle, parse_font_style}, - pager::{CairoPageStyle, CairoPager}, +use crate::{ + output::{ + Details, Item, ItemCursor, TextType, + drivers::{ + Driver, + cairo::{ + fsm::{CairoFsmStyle, parse_font_style}, + pager::{CairoPageStyle, CairoPager}, + }, }, + page::PageSetup, + pivot::{Color, Coord2, FontStyle}, }, - page::PageSetup, - pivot::{Color, Coord2, FontStyle}, - spv::html::Variable, + spv::read::html::Variable, }; use crate::output::pivot::Axis2; diff --git a/rust/pspp/src/output/drivers/cairo/fsm.rs b/rust/pspp/src/output/drivers/cairo/fsm.rs index 5c411ec1d9..1d92e12ec4 100644 --- a/rust/pspp/src/output/drivers/cairo/fsm.rs +++ b/rust/pspp/src/output/drivers/cairo/fsm.rs @@ -29,10 +29,10 @@ use smallvec::{SmallVec, smallvec}; use crate::output::drivers::cairo::{px_to_xr, xr_to_pt}; use crate::output::pivot::{Axis2, BorderStyle, Coord2, FontStyle, HorzAlign, Rect2, Stroke}; use crate::output::render::{Device, Extreme, Pager, Params}; -use crate::output::spv::html::Markup; use crate::output::table::DrawCell; use crate::output::{Details, Item}; use crate::output::{pivot::Color, table::Content}; +use crate::spv::read::html::Markup; /// Width of an ordinary line. const LINE_WIDTH: isize = LINE_SPACE / 2; diff --git a/rust/pspp/src/output/drivers/cairo/pager.rs b/rust/pspp/src/output/drivers/cairo/pager.rs index 6cb1152992..6811646c26 100644 --- a/rust/pspp/src/output/drivers/cairo/pager.rs +++ b/rust/pspp/src/output/drivers/cairo/pager.rs @@ -20,16 +20,12 @@ use cairo::{Context, RecordingSurface}; use enum_map::EnumMap; use pango::Layout; -use crate::output::{ - Item, +use crate::{output::{ drivers::cairo::{ fsm::{CairoFsm, CairoFsmStyle}, xr_to_pt, - }, - pivot::{Axis2, CellStyle, FontStyle, Rect2, ValueOptions}, - spv::html::{Document, Variable}, - table::DrawCell, -}; + }, pivot::{Axis2, CellStyle, FontStyle, Rect2, ValueOptions}, table::DrawCell, Item +}, spv::read::html::{Document, Variable}}; #[derive(Clone, Debug)] pub struct CairoPageStyle { diff --git a/rust/pspp/src/output/page.rs b/rust/pspp/src/output/page.rs index d8ea2e67d9..611a70442a 100644 --- a/rust/pspp/src/output/page.rs +++ b/rust/pspp/src/output/page.rs @@ -20,7 +20,7 @@ use enum_map::{EnumMap, enum_map}; use paper_sizes::{Catalog, Length, PaperSize, Unit}; use serde::{Deserialize, Deserializer, Serialize, de::Error}; -use crate::output::spv::html::Document; +use crate::spv::read::html::Document; use super::pivot::Axis2; diff --git a/rust/pspp/src/output/pivot.rs b/rust/pspp/src/output/pivot.rs index 3a60b2d6d9..2b588301c4 100644 --- a/rust/pspp/src/output/pivot.rs +++ b/rust/pspp/src/output/pivot.rs @@ -72,16 +72,9 @@ pub use tlo::parse_bool; use tlo::parse_tlo; use crate::{ - calendar::date_time_to_pspp, - data::{ByteString, Datum, EncodedString}, - format::{ - DATETIME40_0, Decimal, F8_2, F40, F40_2, F40_3, Format, PCT40_1, - Settings as FormatSettings, Type, UncheckedFormat, - }, - output::spv::html::Markup, - settings::{Settings, Show}, - util::ToSmallString, - variable::{VarType, Variable}, + calendar::date_time_to_pspp, data::{ByteString, Datum, EncodedString}, format::{ + Decimal, Format, Settings as FormatSettings, Type, UncheckedFormat, DATETIME40_0, F40, F40_2, F40_3, F8_2, PCT40_1 + }, settings::{Settings, Show}, spv::read::html::Markup, util::ToSmallString, variable::{VarType, Variable} }; pub mod output; diff --git a/rust/pspp/src/output/table.rs b/rust/pspp/src/output/table.rs index ff7e440d8f..4cc7bf06ca 100644 --- a/rust/pspp/src/output/table.rs +++ b/rust/pspp/src/output/table.rs @@ -35,10 +35,7 @@ use std::{ use enum_map::{EnumMap, enum_map}; use ndarray::{Array, Array2}; -use crate::output::{ - pivot::{CellStyle, DisplayValue, FontStyle, Footnote, HorzAlign, ValueInner}, - spv::html, -}; +use crate::{output::pivot::{CellStyle, DisplayValue, FontStyle, Footnote, HorzAlign, ValueInner}, spv::read::html}; use super::pivot::{ Area, AreaStyle, Axis2, Border, BorderStyle, HeadingRegion, Value, ValueOptions, diff --git a/rust/pspp/src/spv.rs b/rust/pspp/src/spv.rs index 09d05aa05d..f6bee721db 100644 --- a/rust/pspp/src/spv.rs +++ b/rust/pspp/src/spv.rs @@ -27,4 +27,5 @@ pub use write::Writer; +pub mod read; mod write; diff --git a/rust/pspp/src/output/spv.rs b/rust/pspp/src/spv/read.rs similarity index 96% rename from rust/pspp/src/output/spv.rs rename to rust/pspp/src/spv/read.rs index 7be381b96b..8b0c2dca97 100644 --- a/rust/pspp/src/output/spv.rs +++ b/rust/pspp/src/spv/read.rs @@ -34,12 +34,12 @@ use crate::{ Details, Item, SpvInfo, SpvMembers, Text, page::{self}, pivot::{Axis2, Length, Look, TableProperties, Value}, - spv::{ - html::Document, - legacy_bin::LegacyBin, - legacy_xml::Visualization, - light::{LightError, LightTable}, - }, + }, + spv::read::{ + html::Document, + legacy_bin::LegacyBin, + legacy_xml::Visualization, + light::{LightError, LightTable}, }, }; @@ -149,30 +149,36 @@ impl ReadOptions { } Ok(SpvFile { - item: items.into_iter().collect(), + items: items.into_iter().collect(), page_setup, }) } } +/// A SPSS viewer (SPV) file read with [ReadOptions]. pub struct SpvFile { /// SPV file contents. - pub item: Vec, + pub items: Vec, /// The page setup in the SPV file, if any. pub page_setup: Option, } impl SpvFile { + // Returns the individual parts of the `SpvFile`. pub fn into_parts(self) -> (Vec, Option) { - (self.item, self.page_setup) + (self.items, self.page_setup) } + /// Returns just the [Item]s. pub fn into_items(self) -> Vec { - self.item + self.items } } +/// An error reading an SPV file. +/// +/// Returned by [ReadOptions::open_file] and [ReadOptions::open_reader]. #[derive(Debug, Display, thiserror::Error)] pub enum Error { /// Not an SPV file. @@ -218,9 +224,8 @@ where Ok(result) => result, Err(error) => panic!("{error:?}"), }; - let _page_setup = heading.page_setup.take(); - // XXX convert page_setup to the internal format - Ok((heading.decode(archive, structure_member)?, None)) + let page_setup = heading.page_setup.take().map(|ps| ps.decode()); + Ok((heading.decode(archive, structure_member)?, page_setup)) } #[derive(Deserialize, Debug)] @@ -414,7 +419,7 @@ impl PageParagraphText { #[derive(Copy, Clone, Debug, Default, Deserialize)] #[serde(rename = "snake_case")] -pub enum ReferenceOrientation { +enum ReferenceOrientation { #[serde(alias = "0")] #[serde(alias = "0deg")] #[serde(alias = "inherit")] @@ -455,7 +460,7 @@ impl From for page::Orientation { /// Chart size. #[derive(Copy, Clone, Debug, Default, Deserialize)] -pub enum ChartSize { +enum ChartSize { #[default] #[serde(rename = "as-is")] AsIs, @@ -757,7 +762,8 @@ struct TableStructure { /// The `.bin` member name. data_path: String, /// Rarely used, not understood. - csv_path: Option, + #[serde(rename = "csvPath")] + _csv_path: Option, } #[cfg(test)] diff --git a/rust/pspp/src/output/spv/css.rs b/rust/pspp/src/spv/read/css.rs similarity index 98% rename from rust/pspp/src/output/spv/css.rs rename to rust/pspp/src/spv/read/css.rs index 17fbe92435..5733b2869e 100644 --- a/rust/pspp/src/output/spv/css.rs +++ b/rust/pspp/src/spv/read/css.rs @@ -7,9 +7,9 @@ use std::{ use itertools::Itertools; -use crate::output::{ - pivot::{FontStyle, HorzAlign}, - spv::html::Style, +use crate::{ + output::pivot::{FontStyle, HorzAlign}, + spv::read::html::Style, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -19,7 +19,6 @@ enum Token<'a> { RightCurly, Colon, Semicolon, - Error, } struct Lexer<'a>(&'a str); @@ -256,9 +255,9 @@ impl<'a> Display for CssString<'a> { mod tests { use std::borrow::Cow; - use crate::output::{ - pivot::{Color, FontStyle, HorzAlign}, - spv::css::{Lexer, Token}, + use crate::{ + output::pivot::{Color, FontStyle, HorzAlign}, + spv::read::css::{Lexer, Token}, }; #[test] diff --git a/rust/pspp/src/output/spv/html.rs b/rust/pspp/src/spv/read/html.rs similarity index 99% rename from rust/pspp/src/output/spv/html.rs rename to rust/pspp/src/spv/read/html.rs index 4db98918fc..c4c0e66eff 100644 --- a/rust/pspp/src/output/spv/html.rs +++ b/rust/pspp/src/spv/read/html.rs @@ -723,7 +723,7 @@ fn get_element_text(element: &Element, text: &mut String) { mod tests { use std::{borrow::Cow, str::FromStr}; - use crate::output::spv::html::{self, Document, Markup, Variable}; + use crate::spv::read::html::{self, Document, Markup, Variable}; #[test] fn variable() { diff --git a/rust/pspp/src/output/spv/legacy_bin.rs b/rust/pspp/src/spv/read/legacy_bin.rs similarity index 95% rename from rust/pspp/src/output/spv/legacy_bin.rs rename to rust/pspp/src/spv/read/legacy_bin.rs index f1e0e46342..07fca9ee99 100644 --- a/rust/pspp/src/output/spv/legacy_bin.rs +++ b/rust/pspp/src/spv/read/legacy_bin.rs @@ -11,21 +11,20 @@ use crate::{ calendar::{date_time_to_pspp, time_to_pspp}, data::Datum, format::{Category, Format}, - output::{ - pivot::Value, - spv::light::{U32String, decode_format, parse_vec}, - }, + output::pivot::Value, + spv::read::light::{U32String, decode_format, parse_vec}, }; #[binread] #[br(little)] #[derive(Debug)] pub struct LegacyBin { - #[br(magic(0u8))] + #[br(magic(0u8), temp)] version: Version, #[br(temp)] n_sources: u16, - member_size: u32, + #[br(temp)] + _member_size: u32, #[br(count(n_sources), args { inner: (version,) })] metadata: Vec, #[br(parse_with(parse_data), args(metadata.as_slice()))] @@ -36,11 +35,6 @@ pub struct LegacyBin { impl LegacyBin { pub fn decode(&self) -> HashMap>> { - fn decode_asciiz(name: &[u8]) -> String { - let len = name.iter().position(|b| *b == 0).unwrap_or(name.len()); - std::str::from_utf8(&name[..len]).unwrap().into() // XXX unwrap - } - let mut sources = HashMap::new(); for (metadata, data) in self.metadata.iter().zip(&self.data) { let mut variables = HashMap::new(); diff --git a/rust/pspp/src/output/spv/legacy_xml.rs b/rust/pspp/src/spv/read/legacy_xml.rs similarity index 99% rename from rust/pspp/src/output/spv/legacy_xml.rs rename to rust/pspp/src/spv/read/legacy_xml.rs index c1523f5577..97892a5c07 100644 --- a/rust/pspp/src/output/spv/legacy_xml.rs +++ b/rust/pspp/src/spv/read/legacy_xml.rs @@ -34,14 +34,12 @@ use crate::{ calendar::{date_time_to_pspp, time_to_pspp}, data::Datum, format::{self, Decimal::Dot, F8_0, F40_2, Type, UncheckedFormat}, - output::{ - pivot::{ - self, Area, AreaStyle, Axis2, Axis3, Category, CategoryLocator, CellStyle, Color, - Dimension, Group, HeadingRegion, HorzAlign, Leaf, Length, Look, NumberValue, - PivotTable, RowParity, Value, ValueInner, VertAlign, - }, - spv::legacy_bin::DataValue, + output::pivot::{ + self, Area, AreaStyle, Axis2, Axis3, Category, CategoryLocator, CellStyle, Color, + Dimension, Group, HeadingRegion, HorzAlign, Leaf, Length, Look, NumberValue, PivotTable, + RowParity, Value, ValueInner, VertAlign, }, + spv::read::legacy_bin::DataValue, }; #[derive(Debug)] @@ -1048,13 +1046,6 @@ impl Series { value } - fn max_category(&self) -> Option { - self.values - .iter() - .filter_map(|value| value.category()) - .max() - } - fn new_name(&self, dv: &DataValue, footnotes: &pivot::Footnotes) -> Value { let dv = self.map.lookup(dv); let name = Value::new_datum(dv); @@ -1084,13 +1075,6 @@ struct VisualizationExtension { show_gridline: Option, } -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -enum Variable { - SourceVariable(SourceVariable), - DerivedVariable(DerivedVariable), -} - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct SourceVariable { diff --git a/rust/pspp/src/output/spv/light.rs b/rust/pspp/src/spv/read/light.rs similarity index 99% rename from rust/pspp/src/output/spv/light.rs rename to rust/pspp/src/spv/read/light.rs index 263f751004..672667386e 100644 --- a/rust/pspp/src/output/spv/light.rs +++ b/rust/pspp/src/spv/read/light.rs @@ -1,6 +1,6 @@ use std::{ fmt::Debug, - io::{Cursor, Read, Seek}, + io::{Read, Seek}, ops::Deref, str::FromStr, sync::Arc, @@ -239,7 +239,8 @@ struct Header { max_column_heading_width: u32, min_row_heading_width: u32, max_row_heading_width: u32, - table_id: i64, + #[br(temp)] + _table_id: i64, } #[binread] @@ -672,24 +673,6 @@ impl Debug for U32String { } } -#[binread] -struct CountedInner { - #[br(parse_with(parse_vec))] - data: Vec, -} - -impl CountedInner { - fn cursor(self) -> Cursor> { - Cursor::new(self.data) - } -} - -impl Debug for CountedInner { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", &self.data) - } -} - #[derive(Clone, Debug, Default)] struct Counted(T); @@ -870,7 +853,7 @@ struct N0 { #[br(temp)] _bytes: [u8; 14], y1: Y1, - y2: Y2, + _y2: Y2, } #[binread] @@ -890,7 +873,7 @@ struct Y1 { _x12: bool, #[br(temp, parse_with(parse_bool))] _x13: bool, - y0: Y0, + _y0: Y0, } #[binread] @@ -912,7 +895,7 @@ struct N1 { show_title: u8, #[br(temp, parse_with(parse_bool))] _x16: bool, - lang: u8, + _lang: u8, #[br(parse_with(parse_show))] show_variables: Option, #[br(parse_with(parse_show))] diff --git a/rust/pspp/src/spv/write.rs b/rust/pspp/src/spv/write.rs index 08aa5f3c27..93ae30b3d0 100644 --- a/rust/pspp/src/spv/write.rs +++ b/rust/pspp/src/spv/write.rs @@ -40,12 +40,13 @@ use crate::{ Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Leaf, PivotTable, RowColBorder, RowParity, Stroke, Value, ValueInner, ValueStyle, VertAlign, }, - spv::html::Document, }, settings::Show, + spv::read::html::Document, util::ToSmallString, }; +/// SPSS viewer (SPV) file writer. pub struct Writer where W: Write + Seek, @@ -61,6 +62,8 @@ impl Writer where W: Write + Seek, { + /// Creates a new `Writer` to write an SPV file to underlying stream + /// `writer`. pub fn for_writer(writer: W) -> Self { let mut writer = ZipWriter::new(writer); writer @@ -76,11 +79,27 @@ where } } + /// Returns this `Writer` with `page_setup` set up to be written with the + /// next call to [write](Writer::write). + /// + /// Page setup is only significant if it is written before the first call to + /// [write](Writer::writer). pub fn with_page_setup(mut self, page_setup: PageSetup) -> Self { self.set_page_setup(page_setup); self } + /// Sets `page_setup` to be written with the next call to + /// [write](Writer::write). + /// + /// Page setup is only significant if it is written before the first call to + /// [write](Writer::writer). + pub fn set_page_setup(&mut self, page_setup: PageSetup) { + self.page_setup = Some(page_setup); + } + + /// Closes the underlying file and returns the inner writer and the final + /// I/O result. pub fn close(mut self) -> ZipResult { self.writer .start_file("META-INF/MANIFEST.MF", SimpleFileOptions::default())?; @@ -88,10 +107,6 @@ where self.writer.finish() } - pub fn set_page_setup(&mut self, page_setup: PageSetup) { - self.page_setup = Some(page_setup); - } - fn page_break_before(&mut self) -> bool { let page_break_before = self.needs_page_break; self.needs_page_break = false; @@ -523,7 +538,7 @@ impl PivotTable { impl Writer where - W: Write + Seek + 'static, + W: Write + Seek, { pub fn write(&mut self, item: &Item) { if item.details.is_page_break() { -- 2.30.2