successfully read just an outline
authorBen Pfaff <blp@cs.stanford.edu>
Thu, 1 Jan 2026 00:28:16 +0000 (16:28 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Thu, 1 Jan 2026 00:28:16 +0000 (16:28 -0800)
rust/pspp/src/cli/show_spv.rs
rust/pspp/src/output.rs
rust/pspp/src/spv/read/structure.rs

index 3a4c7f05f5b68cc122f9c3ee68cb1996c032b1d4..475aaa83413bf14c2734511979a6192af0e40957 100644 (file)
 // this program.  If not, see <http://www.gnu.org/licenses/>.
 
 use anyhow::Result;
+use binrw::{BinRead, error::ContextExt};
 use clap::{Args, ValueEnum};
 use pspp::{
-    output::{Criteria, Item, Itemlike},
-    spv::SpvArchive,
+    output::{
+        Criteria, Item, Itemlike, SpvMembers,
+        pivot::{Axis3, Dimension, Group, Leaf, PivotTable, value::Value},
+    },
+    spv::{SpvArchive, legacy_bin::LegacyBin},
+};
+use std::{
+    collections::HashMap,
+    fmt::Display,
+    io::{Cursor, Read},
+    path::PathBuf,
+    sync::Arc,
 };
-use std::{fmt::Display, path::PathBuf, sync::Arc};
 
 /// Show information about SPSS viewer files (SPV files).
 #[derive(Args, Clone, Debug)]
@@ -127,65 +137,64 @@ impl ShowSpv {
     }
 
     fn legacy_data(self) -> Result<()> {
-        todo!() /*
-        let spv = SpvArchive::open_file(&self.input, self.password.as_deref())?;
-        let outline = spv.read_outline(|w| eprintln!("{w}"))?;
-        for item in self.read()? {
-        for item in ItemRefIterator::new(&item) {
-        if let Some(spv_info) = &item.spv_info
-        && let Some(members) = &spv_info.members
-        && let SpvMembers::LegacyTable { xml: _, binary } = &members
-        {
-        let mut bin_member = spv_file.archive.by_name(&binary)?;
-        let mut bin_data = Vec::with_capacity(bin_member.size() as usize);
-        bin_member.read_to_end(&mut bin_data)?;
-        let mut cursor = Cursor::new(bin_data);
-        let legacy_bin = LegacyBin::read(&mut cursor).map_err(|e| {
-        e.with_message(format!(
-        "While parsing {binary:?} as legacy binary SPV member"
-        ))
-        })?;
-        let data = legacy_bin.decode();
-        let n_values = data
-        .values()
-        .flat_map(|map| map.values())
-        .map(|values| values.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)]);
-        let formats = HashMap::new();
-        for (variable_index, (variable_name, values)) in
-        data.values().flat_map(|map| map.iter()).enumerate()
-        {
-        for (value_index, data_value) in values.iter().enumerate() {
-        let value = Value::new_datum(&data_value.value).with_value_label(
-        (variable_name == "cellFormat")
-        .then(|| data_value.as_format(&formats).to_string()),
-        );
-        pivot_table.insert([value_index, variable_index], value);
-        }
-        }
-        println!("{pivot_table}");
+        let mut archive = SpvArchive::open_file(&self.input, self.password.as_deref())?;
+        let outline = archive.read_outline(|w| eprintln!("{w}"))?;
+        for item in outline.items {
+            for item in item.iter_in_order() {
+                if let Some(spv_info) = item.spv_info()
+                    && let Some(members) = &spv_info.members
+                    && let SpvMembers::LegacyTable { xml: _, binary } = &members
+                {
+                    let mut bin_member = archive.0.by_name(&binary)?;
+                    let mut bin_data = Vec::with_capacity(bin_member.size() as usize);
+                    bin_member.read_to_end(&mut bin_data)?;
+                    let mut cursor = Cursor::new(bin_data);
+                    let legacy_bin = LegacyBin::read(&mut cursor).map_err(|e| {
+                        e.with_message(format!(
+                            "While parsing {binary:?} as legacy binary SPV member"
+                        ))
+                    })?;
+                    let data = legacy_bin.decode();
+                    let n_values = data
+                        .values()
+                        .flat_map(|map| map.values())
+                        .map(|values| values.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)]);
+                    let formats = HashMap::new();
+                    for (variable_index, (variable_name, values)) in
+                        data.values().flat_map(|map| map.iter()).enumerate()
+                    {
+                        for (value_index, data_value) in values.iter().enumerate() {
+                            let value = Value::new_datum(&data_value.value).with_value_label(
+                                (variable_name == "cellFormat")
+                                    .then(|| data_value.as_format(&formats).to_string()),
+                            );
+                            pivot_table.insert([value_index, variable_index], value);
+                        }
+                    }
+                    println!("{pivot_table}");
+                }
+            }
         }
-        }
-        }
-        Ok(())*/
+        Ok(())
     }
 }
 
index c1f59874506b144fdeeb3a1f123297d41a517383..a2f1d6953203451c1f2c8d768acfcf51e4b2c0a8 100644 (file)
@@ -84,12 +84,12 @@ pub struct Item {
 }
 
 pub trait Itemlike {
-    fn label(&self) -> Cow<'static, str>;
+    fn label(&self) -> Cow<'_, str>;
     fn command_name(&self) -> Option<&str>;
     fn subtype(&self) -> Option<String>;
     fn is_shown(&self) -> bool;
     fn page_break_before(&self) -> bool;
-    fn spv_info(&self) -> Option<&SpvInfo>;
+    fn spv_info(&self) -> Option<Cow<'_, SpvInfo>>;
     fn iter_in_order(&self) -> ItemRefIterator<'_, Self>
     where
         Self: Sized;
@@ -122,12 +122,14 @@ pub trait Itemlike {
                 "Page Title" => Class::PageTitle,
                 _ => Class::Texts,
             },
+            ItemKind::Model => Class::Models,
+            ItemKind::Tree => Class::Trees,
         }
     }
 }
 
 impl Itemlike for Item {
-    fn label(&self) -> Cow<'static, str> {
+    fn label(&self) -> Cow<'_, str> {
         match &self.label {
             Some(label) => Cow::from(label.clone()),
             None => self.details.label(),
@@ -156,8 +158,10 @@ impl Itemlike for Item {
         self.page_break_before
     }
 
-    fn spv_info(&self) -> Option<&SpvInfo> {
-        self.spv_info.as_deref()
+    fn spv_info(&self) -> Option<Cow<'_, SpvInfo>> {
+        self.spv_info
+            .as_ref()
+            .map(|spv_info| Cow::Borrowed(&**spv_info))
     }
 
     fn iter_in_order(&self) -> ItemRefIterator<'_, Item> {
@@ -296,6 +300,8 @@ pub enum ItemKind {
     Message(Severity),
     Table,
     Text,
+    Model,
+    Tree,
 }
 
 impl ItemKind {
@@ -307,6 +313,8 @@ impl ItemKind {
             ItemKind::Message(_) => "message",
             ItemKind::Table => "table",
             ItemKind::Text => "text",
+            ItemKind::Model => "model",
+            ItemKind::Tree => "tree",
         }
     }
 }
@@ -681,7 +689,7 @@ where
     // Returns the label for the heading with the given `level` in the stack
     // above the current item.  Level 0 is the top level.  Levels without a
     // label are skipped.
-    pub fn heading(&self, level: usize) -> Option<Cow<'static, str>> {
+    pub fn heading(&self, level: usize) -> Option<Cow<'_, str>> {
         self.stack
             .iter()
             .map(|(item, _index)| item.as_ref().label())
index 14d9ab6c447cd0869aa4d42edbefddade400ba30..231c1e906fc8f891bec9eebc239c6f7a1900f2bd 100644 (file)
@@ -1,6 +1,8 @@
 use std::{
+    borrow::Cow,
     io::{BufRead, BufReader, Cursor, Read, Seek},
     mem::take,
+    sync::Arc,
 };
 
 use anyhow::Context;
@@ -10,7 +12,7 @@ use zip::ZipArchive;
 
 use crate::{
     output::{
-        Details, Item, SpvInfo, SpvMembers, Text,
+        Details, Item, ItemKind, ItemRefIterator, Itemlike, SpvInfo, SpvMembers, Text,
         page::PageSetup,
         pivot::{
             Length,
@@ -68,7 +70,7 @@ pub struct OutlineHeading {
     structure_member: String,
     expand: bool,
     label: String,
-    children: Vec<OutlineItem>,
+    children: Vec<Arc<OutlineItem>>,
     command_name: Option<String>,
 }
 
@@ -92,7 +94,7 @@ impl OutlineItem {
     {
         match self {
             OutlineItem::Container(container) => {
-                let mut spv_info = container.spv_info();
+                let mut spv_info = container.spv_info().clone();
                 let result = match container.content {
                     Content::Table(table) => table.decode(archive, &mut *warn),
                     Content::Graph(_) => Err(Error::GraphTodo),
@@ -120,7 +122,7 @@ impl OutlineItem {
                 heading
                     .children
                     .into_iter()
-                    .map(|child| child.read_item(archive, &mut *warn))
+                    .map(|child| Arc::unwrap_or_clone(child).read_item(archive, &mut *warn))
                     .collect::<Item>()
                     .with_show(expand)
                     .with_label(label)
@@ -131,6 +133,79 @@ impl OutlineItem {
     }
 }
 
+impl Itemlike for OutlineItem {
+    fn label(&self) -> Cow<'_, str> {
+        match self {
+            OutlineItem::Heading(outline_heading) => Cow::from(outline_heading.label.as_str()),
+            OutlineItem::Container(container) => Cow::from(container.label.as_str()),
+        }
+    }
+
+    fn command_name(&self) -> Option<&str> {
+        match self {
+            OutlineItem::Heading(outline_heading) => outline_heading.command_name.as_deref(),
+            OutlineItem::Container(container) => Some(&container.command_name),
+        }
+    }
+
+    fn subtype(&self) -> Option<String> {
+        match self {
+            OutlineItem::Heading(_) => None,
+            OutlineItem::Container(container) => container.content.subtype().map(|s| s.into()),
+        }
+    }
+
+    fn is_shown(&self) -> bool {
+        match self {
+            OutlineItem::Heading(_) => true,
+            OutlineItem::Container(container) => container.show,
+        }
+    }
+
+    fn page_break_before(&self) -> bool {
+        match self {
+            OutlineItem::Heading(_) => false,
+            OutlineItem::Container(container) => container.page_break_before,
+        }
+    }
+
+    fn spv_info(&self) -> Option<Cow<'_, SpvInfo>> {
+        let spv_info = match self {
+            OutlineItem::Heading(outline_heading) => outline_heading.spv_info(),
+            OutlineItem::Container(container) => container.spv_info(),
+        };
+        Some(Cow::Owned(spv_info))
+    }
+
+    fn iter_in_order(&self) -> ItemRefIterator<'_, Self>
+    where
+        Self: Sized,
+    {
+        ItemRefIterator::new(self)
+    }
+
+    fn children(&self) -> &[Arc<Self>] {
+        match self {
+            OutlineItem::Heading(outline_heading) => &outline_heading.children,
+            OutlineItem::Container(_) => &[],
+        }
+    }
+
+    fn children_mut(&mut self) -> Option<&mut Vec<Arc<Self>>> {
+        match self {
+            OutlineItem::Heading(outline_heading) => Some(&mut outline_heading.children),
+            OutlineItem::Container(_) => None,
+        }
+    }
+
+    fn kind(&self) -> ItemKind {
+        match self {
+            OutlineItem::Heading(_) => ItemKind::Heading,
+            OutlineItem::Container(container) => container.content.kind(),
+        }
+    }
+}
+
 #[derive(Clone, Debug)]
 pub struct Container {
     structure_member: String,
@@ -160,6 +235,24 @@ pub enum Content {
 }
 
 impl Content {
+    fn kind(&self) -> ItemKind {
+        match self {
+            Content::Text(_) => ItemKind::Text,
+            Content::Table(_) => ItemKind::Table,
+            Content::Image(_) => ItemKind::Image,
+            Content::Graph(_) => ItemKind::Graph,
+            Content::Tree => ItemKind::Tree,
+            Content::Model => ItemKind::Model,
+        }
+    }
+
+    fn subtype(&self) -> Option<&str> {
+        match self {
+            Content::Table(table) => Some(&table.subtype),
+            _ => None,
+        }
+    }
+
     fn members(&self) -> Option<SpvMembers> {
         match self {
             Content::Text(_text) => None,
@@ -284,7 +377,7 @@ impl Graph {
 }
 
 mod raw {
-    use std::mem::take;
+    use std::{mem::take, sync::Arc};
 
     use paper_sizes::PaperSize;
     use serde::Deserialize;
@@ -400,7 +493,11 @@ mod raw {
                             expand: !heading.visibility.is_some(),
                             label: take(&mut heading.label.text),
                             command_name: heading.command_name.take(),
-                            children: heading.decode(structure_member, warn),
+                            children: heading
+                                .decode(structure_member, warn)
+                                .into_iter()
+                                .map(Arc::new)
+                                .collect(),
                         }));
                     }
                 }