work
authorBen Pfaff <blp@cs.stanford.edu>
Tue, 14 Oct 2025 15:50:34 +0000 (08:50 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Tue, 14 Oct 2025 15:50:34 +0000 (08:50 -0700)
rust/doc/src/commands/set.md
rust/doc/src/spv/light-detail.md
rust/pspp/src/output.rs
rust/pspp/src/output/drivers/html.rs
rust/pspp/src/output/drivers/spv.rs
rust/pspp/src/output/drivers/text.rs
rust/pspp/src/output/spv.rs
rust/pspp/src/output/spv/light.rs

index a7a48a9b59f5191a4fe6104506b729cf42bee13c..774f57e1974d98d2403587dc2112483684cabd68 100644 (file)
@@ -24,7 +24,7 @@ SET
         /SCALEMIN=COUNT
 
 (data output)
-        /CC{A,B,C,D,E}={'NPRE,PRE,SUF,NSUF','NPRE.PRE.SUF.NSUF'}
+        /CC{A,B,C,D,E}='STRING'
         /DECIMAL={DOT,COMMA}
         /FORMAT=FMT_SPEC
         /LEADZERO={ON,OFF}
@@ -46,13 +46,13 @@ SET
         /TVARS={NAMES,LABELS,BOTH}
         /TLOOK={NONE,FILE}
 
-(logging)
+(journal)
         /JOURNAL={ON,OFF} ['FILE_NAME']
 
 (system files)
         /SCOMPRESSION={ON,OFF}
 
-(miscellaneous)
+(security)
         /SAFER=ON
         /LOCALE='STRING'
 
@@ -62,7 +62,7 @@ SET
         /MITERATE=NUMBER
         /MNEST=NUMBER
 
-(settings not yet implemented, but accepted and ignored)
+(not yet implemented)
         /BASETEXTDIRECTION={AUTOMATIC,RIGHTTOLEFT,LEFTTORIGHT}
         /BLOCK='C'
         /BOX={'XXX','XXXXXXXXXXX'}
@@ -84,6 +84,15 @@ synonymous, as are `OFF` and `NO`, when used as subcommand values.
 
 # Data Input
 
+```
+SET
+        /BLANKS={SYSMIS,'.',number}
+        /DECIMAL={DOT,COMMA}
+        /FORMAT=FMT_SPEC
+        /EPOCH={AUTOMATIC,YEAR}
+        /RIB={NATIVE,MSBFIRST,LSBFIRST}
+```
+
 The data input subcommands affect the way that data is read from data
 files.  The data input subcommands are:
 
@@ -128,6 +137,13 @@ files.  The data input subcommands are:
 
 # Interaction
 
+```
+SET
+        /MXERRS=MAX_ERRS
+        /MXWARNS=MAX_WARNINGS
+        /WORKSPACE=WORKSPACE_SIZE
+```
+
 Interaction subcommands affect the way that PSPP interacts with an
 online user.  The interaction subcommands are
 
@@ -142,6 +158,18 @@ online user.  The interaction subcommands are
   are issued, except a single initial warning advising you that
   warnings will not be given.  The default value is 100.
 
+# Syntax Execution
+
+```
+SET
+        /LOCALE='LOCALE'
+        /MXLOOPS=MAX_LOOPS
+        /SEED={RANDOM,SEED_VALUE}
+        /UNDEFINED={WARN,NOWARN}
+        /FUZZBITS=FUZZBITS
+        /SCALEMIN=COUNT
+```
+
 Syntax execution subcommands control the way that PSPP commands
 execute.  The syntax execution subcommands are
 
@@ -192,6 +220,17 @@ execute.  The syntax execution subcommands are
 
 # Data Output
 
+```
+SET
+        /CC{A,B,C,D,E}='STRING'
+        /DECIMAL={DOT,COMMA}
+        /FORMAT=FMT_SPEC
+        /LEADZERO={ON,OFF}
+        /MDISPLAY={TEXT,TABLES}
+        /SMALL=NUMBER
+        /WIB={NATIVE,MSBFIRST,LSBFIRST}
+```
+
 Data output subcommands affect the format of output data.  These
 subcommands are
 
@@ -246,6 +285,14 @@ subcommands are
 
 # Output Routing
 
+```
+SET
+        /ERRORS={ON,OFF,TERMINAL,LISTING,BOTH,NONE}
+        /MESSAGES={ON,OFF,TERMINAL,LISTING,BOTH,NONE}
+        /PRINTBACK={ON,OFF,TERMINAL,LISTING,BOTH,NONE}
+        /RESULTS={ON,OFF,TERMINAL,LISTING,BOTH,NONE}
+```
+
 In the PSPP text-based interface, the output routing subcommands
 affect where output is sent.  The following values are allowed for each
 of these subcommands:
@@ -287,6 +334,16 @@ environment.
 
 # Output Driver
 
+```
+SET
+        /HEADERS={NO,YES,BLANK}
+        /LENGTH={NONE,N_LINES}
+        /WIDTH={NARROW,WIDTH,N_CHARACTERS}
+        /TNUMBERS={VALUES,LABELS,BOTH}
+        /TVARS={NAMES,LABELS,BOTH}
+        /TLOOK={NONE,FILE}
+```
+
 Output driver option subcommands affect output drivers' settings.
 These subcommands are:
 
@@ -327,6 +384,11 @@ These subcommands are:
 
 # Journal
 
+```
+SET
+        /JOURNAL={ON,OFF} ['FILE_NAME']
+```
+
 Journal subcommands affect logging of commands executed to external
 files.  These subcommands are
 
@@ -342,6 +404,13 @@ files.  These subcommands are
   The journal is named `pspp.jnl` by default.  A different name may
   be specified.
 
+# System Files
+
+```
+SET
+        /SCOMPRESSION={ON,OFF}
+```
+
 System file subcommands affect the default format of system files
 produced by PSPP.  These subcommands are
 
@@ -351,6 +420,12 @@ produced by PSPP.  These subcommands are
 
 # Security
 
+```
+SET
+        /SAFER=ON
+        /LOCALE='STRING'
+```
+
 Security subcommands affect the operations that commands are allowed
 to perform.  The security subcommands are
 
@@ -395,6 +470,14 @@ to perform.  The security subcommands are
 
 # Macros
 
+```
+SET
+        /MEXPAND={ON,OFF}
+        /MPRINT={ON,OFF}
+        /MITERATE=NUMBER
+        /MNEST=NUMBER
+```
+
 The following subcommands affect the interpretation of macros.  For
 more information, see [Macro Settings](define.md#macro-settings).
 
@@ -419,6 +502,18 @@ more information, see [Macro Settings](define.md#macro-settings).
 
 # Not Yet Implemented
 
+```
+SET
+        /BASETEXTDIRECTION={AUTOMATIC,RIGHTTOLEFT,LEFTTORIGHT}
+        /BLOCK='C'
+        /BOX={'XXX','XXXXXXXXXXX'}
+        /CACHE={ON,OFF}
+        /CELLSBREAK=NUMBER
+        /COMPRESSION={ON,OFF}
+        /CMPTRANS={ON,OFF}
+        /HEADER={NO,YES,BLANK}
+```
+
 The following subcommands are not yet implemented, but PSPP accepts
 them and ignores the settings:
 
index 6af5c0badcf37294cdfd8a6c4c5d5060d05bc62b..206332ef2141fff4acd2b309dcf4ea3ce1cf5334 100644 (file)
@@ -780,7 +780,7 @@ really categories; the others just serve as grouping constructs.
 
 ```
 Category => Value[name] (Leaf | Group)
-Leaf => 00 00 00 i2 int32[leaf-index] i0
+Leaf => 00 00 bool[x24] i2 int32[leaf-index] i0
 Group =>
     bool[merge] 00 01 int32[x23]
     i-1 int32[n-subcategories] Category*[n-subcategories]
@@ -820,6 +820,8 @@ variable (e.g. in a frequency table or crosstabulation, a group of
 values in a variable being tabulated) and i0 otherwise.  A writer may
 safely write a constant 0 in this field.
 
+`x24` is usually 0.  Its meaning is unexplored.
+
 ## Axes
 
 After the dimensions come assignment of each dimension to one of the
index 1ac438c77a2a3f896c5bee596c79751b69b8bd5b..b275f797ced9b8ff4c1b46c16619c6fbcc52b677 100644 (file)
@@ -274,7 +274,7 @@ impl Details {
 
     pub fn label(&self) -> Cow<'static, str> {
         match self {
-            Details::Chart => todo!(),
+            Details::Chart => Cow::from("chart"),
             Details::Image => Cow::from("Image"),
             Details::Heading(_) => Cow::from("Group"),
             Details::Message(diagnostic) => Cow::from(diagnostic.severity.as_title_str()),
@@ -367,6 +367,15 @@ impl From<Box<Text>> for Details {
     }
 }
 
+#[derive(Clone, Debug, Serialize)]
+pub struct Chart;
+
+impl Chart {
+    pub fn into_item(self) -> Item {
+        Details::Chart.into_item()
+    }
+}
+
 #[derive(Clone, Debug, Serialize)]
 pub struct Text {
     type_: TextType,
@@ -554,16 +563,30 @@ pub enum SpvMembers {
         /// `.png` file.
         String,
     ),
+    /// Chart members.
+    Graph {
+        /// Data member name.
+        data: Option<String>,
+        /// XML member name.
+        xml: String,
+        /// CVS member name.
+        csv: Option<String>,
+    },
 }
 
 impl SpvMembers {
     pub fn iter(&self) -> impl Iterator<Item = &str> {
-        let (a, b) = match self {
-            SpvMembers::Light(a) => (a.as_str(), None),
-            SpvMembers::Legacy { xml: a, binary: b } => (a.as_str(), Some(b.as_str())),
-            SpvMembers::Image(a) => (a.as_str(), None),
+        let (a, b, c) = match self {
+            SpvMembers::Light(a) => (Some(a), None, None),
+            SpvMembers::Legacy { xml, binary } => (Some(xml), Some(binary), None),
+            SpvMembers::Image(a) => (Some(a), None, None),
+            SpvMembers::Graph { data, xml, csv } => (data.as_ref(), Some(xml), csv.as_ref()),
         };
-        once(a).chain(once(b).flatten())
+        once(a)
+            .flatten()
+            .chain(once(b).flatten())
+            .chain(once(c).flatten())
+            .map(|s| s.as_str())
     }
 }
 
index f10f192d37c9ea65f3cf06a55484745afa7279fa..a68304d3a740f0cc8c75282695545118ccade007 100644 (file)
@@ -427,9 +427,7 @@ where
 
     fn write(&mut self, item: &Arc<Item>) {
         match &item.details {
-            Details::Chart => todo!(),
-            Details::Image => todo!(),
-            Details::Heading(_) => todo!(),
+            Details::Chart | Details::Image | Details::Heading(_) => todo!(),
             Details::Message(_diagnostic) => todo!(),
             Details::PageBreak => (),
             Details::Table(pivot_table) => {
index 97f3c2856827191d9c9d7320a6eaddaa66a29a75..2983d4f95d5124c4446420f050efc91989df6613 100644 (file)
@@ -190,8 +190,7 @@ where
         X: Write,
     {
         match &item.details {
-            Details::Chart => todo!(),
-            Details::Image => todo!(),
+            Details::Chart | Details::Image => todo!(),
             Details::Heading(children) => {
                 let mut attributes = Vec::<Attribute>::new();
                 if let Some(command_name) = &item.command_name {
index a2c7be650aecad483dab004e84189e8a96473060..7ec73eb349608b67f2e4c62921eee779831ad4c7 100644 (file)
@@ -382,8 +382,7 @@ impl TextRenderer {
         W: FmtWrite,
     {
         match &item.details {
-            Details::Chart => todo!(),
-            Details::Image => todo!(),
+            Details::Chart | Details::Image => todo!(),
             Details::Heading(children) => {
                 for (index, child) in children.0.iter().enumerate() {
                     if index > 0 {
index 073b94291efe9ecb688eacbead4a852de84d5256..3ab1d0fdb674a8bed0182d20e815c646a6047747 100644 (file)
@@ -166,6 +166,9 @@ impl Heading {
                                 table.decode(archive, structure_member).unwrap(), /* XXX*/
                             );
                         }
+                        ContainerContent::Graph(graph) => {
+                            items.push(graph.decode(structure_member));
+                        }
                         ContainerContent::Text(container_text) => {
                             items.push(
                                 Text::new(
@@ -258,14 +261,37 @@ enum TextAlign {
 enum ContainerContent {
     Table(Table),
     Text(ContainerText),
-    /*
     Graph(Graph),
-    Model(Model),
+    /*    Model(Model),
     Object(Object),
     Image(Image),
     Tree(Tree),*/
 }
 
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Graph {
+    #[serde(rename = "@commandName")]
+    command_name: String,
+    data_path: Option<String>,
+    path: String,
+    csv_path: Option<String>,
+}
+
+impl Graph {
+    fn decode(&self, structure_member: &str) -> Item {
+        crate::output::Chart
+            .into_item()
+            .with_spv_info(
+                SpvInfo::new(structure_member).with_members(SpvMembers::Graph {
+                    data: self.data_path.clone(),
+                    xml: self.path.clone(),
+                    csv: self.csv_path.clone(),
+                }),
+            )
+    }
+}
+
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 struct Table {
index c7be973dad4c7bcbb709a4d2c26b2611c662d169..c65481c81dfdd1e4f10a2682182b96ed6a44dc19 100644 (file)
@@ -7,7 +7,7 @@ use std::{
     sync::Arc,
 };
 
-use binrw::{BinRead, BinResult, Endian, Error as BinError, VecArgs, binread};
+use binrw::{BinRead, BinResult, Endian, Error as BinError, VecArgs, binread, io::TakeSeekExt};
 use chrono::DateTime;
 use displaydoc::Display;
 use encoding_rs::{Encoding, WINDOWS_1252};
@@ -52,22 +52,24 @@ pub struct LightTable {
     header: Header,
     #[br(args(header.version))]
     titles: Titles,
-    #[br(parse_with(parse_counted), args(header.version))]
+    #[br(parse_with(parse_vec), args(header.version))]
     footnotes: Vec<Footnote>,
     #[br(args(header.version))]
     areas: Areas,
-    borders: Counted<Borders>,
-    print_settings: Counted<PrintSettings>,
-    #[br(dbg, if(header.version == Version::V3))]
-    table_settings: Counted<TableSettings>,
+    #[br(parse_with(parse_counted))]
+    borders: Borders,
+    #[br(parse_with(parse_counted))]
+    print_settings: PrintSettings,
+    #[br(dbg, if(header.version == Version::V3), parse_with(parse_counted))]
+    table_settings: TableSettings,
     #[br(if(header.version == Version::V1), temp)]
     _ts: Option<Counted<Sponge>>,
     #[br(args(header.version))]
     formats: Formats,
-    #[br(parse_with(parse_counted), args(header.version))]
+    #[br(parse_with(parse_vec), args(header.version))]
     dimensions: Vec<Dimension>,
     axes: Axes,
-    #[br(parse_with(parse_counted), args(header.version))]
+    #[br(parse_with(parse_vec), args(header.version))]
     cells: Vec<Cell>,
 }
 
@@ -297,7 +299,7 @@ where
 }
 
 #[binrw::parser(reader, endian)]
-fn parse_counted<T, A>(inner: A, ...) -> BinResult<Vec<T>>
+fn parse_vec<T, A>(inner: A, ...) -> BinResult<Vec<T>>
 where
     for<'a> T: BinRead<Args<'a> = A>,
     A: Clone,
@@ -456,7 +458,7 @@ struct Margins {
 #[br(big)]
 #[derive(Debug)]
 struct Borders {
-    #[br(magic(1u32), parse_with(parse_counted))]
+    #[br(magic(1u32), parse_with(parse_vec))]
     borders: Vec<Border>,
 
     #[br(parse_with(parse_bool))]
@@ -572,10 +574,10 @@ struct TableSettings {
     show_alphabetic_markers: bool,
     #[br(parse_with(parse_bool))]
     footnote_marker_subscripts: bool,
-    #[br(temp, parse_with(parse_bool))]
-    _x6: bool,
-    #[br(big)]
-    sizing: Counted<Sizing>,
+    #[br(temp)]
+    _x6: u8,
+    #[br(big, parse_with(parse_counted))]
+    sizing: Sizing,
     notes: U32String,
     table_look: U32String,
     #[br(temp)]
@@ -586,17 +588,17 @@ struct TableSettings {
 #[br(big)]
 #[derive(Debug, Default)]
 struct Sizing {
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     row_breaks: Vec<u32>,
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     column_breaks: Vec<u32>,
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     row_keeps: Vec<(i32, i32)>,
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     column_keeps: Vec<(i32, i32)>,
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     row_point_keeps: Vec<[i32; 3]>,
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     column_point_keeps: Vec<[i32; 3]>,
 }
 
@@ -664,7 +666,7 @@ impl BinRead for Value {
 #[binread]
 #[derive(Default)]
 struct U32String {
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     string: Vec<u8>,
 }
 
@@ -698,7 +700,7 @@ impl Debug for U32String {
 
 #[binread]
 struct CountedInner {
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     data: Vec<u8>,
 }
 
@@ -736,16 +738,18 @@ where
         endian: binrw::Endian,
         args: Self::Args<'_>,
     ) -> BinResult<Self> {
-        let counted = CountedInner::read_options(reader, endian, ())?;
-        let mut cursor = counted.cursor();
-        let result = <T>::read_options(&mut cursor, Endian::Little, args)?;
-        if cursor.position() < cursor.get_ref().len() as u64 {
+        let start = reader.stream_position()?;
+        let count = u32::read_options(reader, endian, ())? as u64;
+        let end = start + count;
+        let mut inner = reader.take_seek(count);
+        let result = <T>::read_options(&mut inner, Endian::Little, args)?;
+        let pos = inner.stream_position()?;
+        if pos < end {
+            let consumed = pos - start;
             return Err(binrw::Error::Custom {
-                pos: cursor.position(),
+                pos,
                 err: Box::new(format!(
-                    "counted data not exhausted (consumed {} bytes out of {})",
-                    cursor.position(),
-                    cursor.get_ref().len()
+                    "counted data not exhausted (consumed {consumed} bytes out of {count})"
                 )),
             });
         }
@@ -753,6 +757,16 @@ where
     }
 }
 
+#[binrw::parser(reader, endian)]
+fn parse_counted<T, A>(args: A, ...) -> BinResult<T>
+where
+    for<'a> T: BinRead<Args<'a> = A>,
+    A: Clone,
+    T: 'static,
+{
+    Ok(<Counted<T>>::read_options(reader, endian, args)?.0)
+}
+
 /// `BinRead` for `Option<T>` always requires the value to be there.  This
 /// instead tries to read it and falls back to None if there's no match.
 #[derive(Clone, Debug)]
@@ -798,7 +812,7 @@ where
 #[br(import(version: Version))]
 #[derive(Debug)]
 struct Formats {
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     column_widths: Vec<i32>,
     locale: U32String,
     current_layer: i32,
@@ -831,11 +845,11 @@ impl Formats {
     }
 
     fn x2(&self) -> Option<&X2> {
-        self.v3.as_ref().map(|v3| &*v3.x1_x2.x2)
+        self.v3.as_ref().map(|v3| &v3.x1_x2.x2)
     }
 
     fn x3(&self) -> Option<&X3> {
-        self.v3.as_ref().map(|v3| &*v3.x3)
+        self.v3.as_ref().map(|v3| &v3.x3)
     }
 
     fn charset(&self) -> Option<&U32String> {
@@ -864,8 +878,10 @@ impl Formats {
 #[derive(Debug)]
 struct FormatsV3 {
     #[br(dbg)]
-    x1_x2: Counted<X1X2>,
-    x3: Counted<X3>,
+    #[br(parse_with(parse_counted))]
+    x1_x2: X1X2,
+    #[br(parse_with(parse_counted))]
+    x3: X3,
 }
 
 #[binread]
@@ -873,7 +889,8 @@ struct FormatsV3 {
 #[derive(Debug)]
 struct X1X2 {
     x1: X1,
-    x2: Counted<X2>,
+    #[br(parse_with(parse_counted))]
+    x2: X2,
 }
 
 #[binread]
@@ -961,13 +978,14 @@ fn parse_show() -> BinResult<Option<Show>> {
 #[br(little)]
 #[derive(Debug)]
 struct X2 {
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     row_heights: Vec<i32>,
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     style_map: Vec<(i64, i16)>,
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     styles: Vec<StylePair>,
-    tail: Counted<Optional<[u8; 8]>>,
+    #[br(parse_with(parse_counted))]
+    tail: Optional<[u8; 8]>,
 }
 
 #[binread]
@@ -980,6 +998,8 @@ struct X3 {
     y1: Y1,
     #[br(dbg)]
     small: f64,
+    #[br(magic = 1u8, temp)]
+    _one: (),
     #[br(dbg)]
     inner: Optional<X3Inner>,
     y2: Y2,
@@ -1039,7 +1059,7 @@ impl Y0 {
 #[br(little)]
 #[derive(Debug)]
 struct CustomCurrency {
-    #[br(parse_with(parse_counted))]
+    #[br(parse_with(parse_vec))]
     ccs: Vec<U32String>,
 }
 
@@ -1059,7 +1079,7 @@ impl CustomCurrency {
 
 #[binread]
 #[br(little)]
-#[br(return_unexpected_error, import(version: Version))]
+#[br( import(version: Version))]
 #[derive(Debug)]
 enum RawValue {
     #[br(magic = 1u8)]
@@ -1122,10 +1142,11 @@ enum RawValue {
         c: U32String,
     },
     Template {
-        #[br(parse_with(parse_optional), args(version))]
+        #[br(dbg, parse_with(parse_optional), args(version))]
         mods: Option<ValueMods>,
+        #[br(dbg)]
         template: U32String,
-        #[br(parse_with(parse_counted), args(version))]
+        #[br(parse_with(parse_vec), args(version))]
         args: Vec<Argument>,
     },
 }
@@ -1234,16 +1255,35 @@ impl RawValue {
     }
 }
 
-#[binread]
-#[br(little)]
-#[br(import(version: Version))]
 #[derive(Debug)]
-enum Argument {
-    Singleton(#[br(magic(0u32), args(version))] Value),
-    Multiple {
-        #[br(magic(0u32), parse_with(parse_counted), args(version))]
-        values: Vec<Value>,
-    },
+struct Argument(Vec<Value>);
+
+impl BinRead for Argument {
+    type Args<'a> = (Version,);
+
+    fn read_options<R: Read + Seek>(
+        reader: &mut R,
+        endian: Endian,
+        (version,): (Version,),
+    ) -> BinResult<Self> {
+        let count = u32::read_options(reader, endian, ())? as usize;
+        dbg!(count);
+        if count == 0 {
+            Ok(Self(vec![Value::read_options(reader, endian, (version,))?]))
+        } else {
+            let zero = u32::read_options(reader, endian, ())?;
+            assert_eq!(zero, 0);
+            let values = <Vec<_>>::read_options(
+                reader,
+                endian,
+                VecArgs {
+                    count,
+                    inner: (version,),
+                },
+            )?;
+            Ok(Self(values))
+        }
+    }
 }
 
 impl Argument {
@@ -1252,13 +1292,10 @@ impl Argument {
         encoding: &'static Encoding,
         footnotes: &pivot::Footnotes,
     ) -> Vec<pivot::Value> {
-        match self {
-            Argument::Singleton(value) => vec![value.decode(encoding, footnotes)],
-            Argument::Multiple { values } => values
-                .iter()
-                .map(|value| value.decode(encoding, footnotes))
-                .collect(),
-        }
+        self.0
+            .iter()
+            .map(|value| value.decode(encoding, footnotes))
+            .collect()
     }
 }
 
@@ -1266,56 +1303,65 @@ impl Argument {
 #[br(little, import(version: Version))]
 #[derive(Debug)]
 struct ValueMods {
-    #[br(parse_with(parse_counted))]
+    #[br(dbg, parse_with(parse_vec))]
     refs: Vec<i16>,
-    #[br(parse_with(parse_counted))]
+    #[br(dbg, parse_with(parse_vec))]
     subscripts: Vec<U32String>,
     #[br(if(version == Version::V1))]
     v1: Option<Sponge>,
-    #[br(if(version == Version::V3))]
-    v3: Counted<Optional<(Counted<TemplateString>, StylePair)>>,
+    #[br(if(version == Version::V3), parse_with(parse_counted))]
+    v3: ValueModsV3,
+}
+
+#[binread]
+#[br(little)]
+#[derive(Debug, Default)]
+struct ValueModsV3 {
+    #[br(parse_with(parse_counted))]
+    template_string: Optional<TemplateString>,
+    style_pair: StylePair,
 }
 
 impl ValueMods {
     fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> ValueStyle {
-        let style_pair = self.v3.as_ref().map(|v3| &v3.1);
-        let font_style = style_pair
-            .and_then(|style_pair| style_pair.font_style.as_ref())
-            .map(|font_style| pivot::FontStyle {
-                bold: font_style.bold,
-                italic: font_style.italic,
-                underline: font_style.underline,
-                markup: false,
-                font: font_style.typeface.decode(encoding),
-                fg: font_style.fg,
-                bg: font_style.bg,
-                size: (font_style.size as i32) * 4 / 3,
-            });
-        let cell_style = style_pair
-            .and_then(|style_pair| style_pair.cell_style.as_ref())
-            .map(|cell_style| {
-                pivot::CellStyle {
-                    horz_align: match cell_style.halign {
-                        0 => Some(HorzAlign::Center),
-                        2 => Some(HorzAlign::Left),
-                        4 => Some(HorzAlign::Right),
-                        6 => Some(HorzAlign::Decimal {
-                            offset: cell_style.decimal_offset,
-                            decimal: Decimal::Dot, /*XXX*/
-                        }),
-                        _ => None,
-                    },
-                    vert_align: match cell_style.valign {
-                        0 => VertAlign::Middle,
-                        3 => VertAlign::Bottom,
-                        _ => VertAlign::Top,
-                    },
-                    margins: enum_map! {
-                        Axis2::X => [cell_style.left_margin as i32, cell_style.right_margin as i32],
-                        Axis2::Y => [cell_style.top_margin as i32, cell_style.bottom_margin as i32],
-                    },
-                }
-            });
+        let font_style =
+            self.v3
+                .style_pair
+                .font_style
+                .as_ref()
+                .map(|font_style| pivot::FontStyle {
+                    bold: font_style.bold,
+                    italic: font_style.italic,
+                    underline: font_style.underline,
+                    markup: false,
+                    font: font_style.typeface.decode(encoding),
+                    fg: font_style.fg,
+                    bg: font_style.bg,
+                    size: (font_style.size as i32) * 4 / 3,
+                });
+        let cell_style = self.v3.style_pair.cell_style.as_ref().map(|cell_style| {
+            pivot::CellStyle {
+                horz_align: match cell_style.halign {
+                    0 => Some(HorzAlign::Center),
+                    2 => Some(HorzAlign::Left),
+                    4 => Some(HorzAlign::Right),
+                    6 => Some(HorzAlign::Decimal {
+                        offset: cell_style.decimal_offset,
+                        decimal: Decimal::Dot, /*XXX*/
+                    }),
+                    _ => None,
+                },
+                vert_align: match cell_style.valign {
+                    0 => VertAlign::Middle,
+                    3 => VertAlign::Bottom,
+                    _ => VertAlign::Top,
+                },
+                margins: enum_map! {
+                    Axis2::X => [cell_style.left_margin as i32, cell_style.right_margin as i32],
+                    Axis2::Y => [cell_style.top_margin as i32, cell_style.bottom_margin as i32],
+                },
+            }
+        });
         ValueStyle {
             cell_style,
             font_style,
@@ -1338,9 +1384,9 @@ impl ValueMods {
     }
     fn template_id(&self, encoding: &'static Encoding) -> Option<String> {
         self.v3
+            .template_string
             .as_ref()
-            .map(|v3| &*v3.0)
-            .and_then(|ts| ts.id.as_ref())
+            .and_then(|template_string| template_string.id.as_ref())
             .map(|s| s.decode(encoding))
     }
 }
@@ -1349,7 +1395,8 @@ impl ValueMods {
 #[br(little)]
 #[derive(Debug)]
 struct TemplateString {
-    _sponge: Counted<Sponge>,
+    #[br(parse_with(parse_counted), temp)]
+    _sponge: Sponge,
     #[br(parse_with(parse_optional))]
     id: Option<U32String>,
 }
@@ -1369,7 +1416,7 @@ impl BinRead for Sponge {
 
 #[binread]
 #[br(little)]
-#[derive(Debug)]
+#[derive(Debug, Default)]
 struct StylePair {
     #[br(parse_with(parse_optional))]
     font_style: Option<FontStyle>,
@@ -1429,7 +1476,7 @@ struct Dimension {
     hide_all_labels: bool,
     #[br(magic(1u8), temp)]
     _dim_index: i32,
-    #[br(parse_with(parse_counted), args(version))]
+    #[br(parse_with(parse_vec), args(version))]
     categories: Vec<Category>,
 }
 
@@ -1477,7 +1524,9 @@ impl Category {
 #[derive(Debug)]
 enum Child {
     Leaf {
-        #[br(magic(b"\0\0\0\x02\0\0\0"))]
+        #[br(magic(0u16), parse_with(parse_bool), temp)]
+        _x24: bool,
+        #[br(magic(b"\x02\0\0\0"))]
         leaf_index: u32,
         #[br(magic(0u32), temp)]
         _tail: (),
@@ -1487,7 +1536,7 @@ enum Child {
         merge: bool,
         #[br(temp, magic(b"\0\x01"))]
         _x23: i32,
-        #[br(magic(-1i32), parse_with(parse_counted), args(version))]
+        #[br(magic(-1i32), parse_with(parse_vec), args(version))]
         subcategories: Vec<Box<Category>>,
     },
 }
@@ -1554,6 +1603,7 @@ impl Axes {
 #[br(little, import(version: Version))]
 #[derive(Debug)]
 struct Cell {
+    #[br(dbg)]
     index: u64,
     #[br(if(version == Version::V1), temp)]
     _zero: Optional<Zero>,