work
authorBen Pfaff <blp@cs.stanford.edu>
Sun, 21 Dec 2025 01:11:13 +0000 (17:11 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Sun, 21 Dec 2025 01:11:13 +0000 (17:11 -0800)
20 files changed:
rust/doc/src/spv/light-detail.md
rust/pspp/src/cli/convert.rs
rust/pspp/src/cli/decrypt.rs
rust/pspp/src/cli/show_spv.rs
rust/pspp/src/crypto.rs
rust/pspp/src/output/drivers/cairo/driver.rs
rust/pspp/src/output/pivot/testdata/d2_cl-all_layers.expected
rust/pspp/src/output/pivot/testdata/d2_cl-layer0.expected
rust/pspp/src/output/pivot/testdata/d2_cl-layer1.expected
rust/pspp/src/output/pivot/testdata/d2_rl-all_layers.expected
rust/pspp/src/output/pivot/testdata/d2_rl-layer0.expected
rust/pspp/src/output/pivot/testdata/d2_rl-layer1.expected
rust/pspp/src/output/pivot/testdata/d3-layer0_0.expected
rust/pspp/src/output/pivot/testdata/d3-layer0_1.expected
rust/pspp/src/output/pivot/testdata/d3-layer1_2.expected
rust/pspp/src/spv/read.rs
rust/pspp/src/spv/read/html.rs
rust/pspp/src/spv/read/light.rs
rust/pspp/src/sys/cooked.rs
rust/pspp/src/sys/tests.rs

index 0cb32535649dc79c02da7888b1106949eb43d365..554a9b83ef74556b71b316e5766178d005cafe25 100644 (file)
@@ -522,7 +522,7 @@ this field by default contains 1948.  In the corpus, `epoch` ranges from
 1943 to 1948, plus some contain -1.
 
 `decimal` is the decimal point character.  The observed values are
-`.` and `,`.
+`.`, `,`, and 0.
 
 `grouping` is the grouping character.  Usually, it is `,` if
 `decimal` is `.`, and vice versa.  Other observed values are `'`
@@ -532,7 +532,7 @@ should not be grouped).
 `n-ccs` is observed as either 0 or 5.  When it is 5, the following
 strings are [CCA through
 CCE](../language/datasets/formats/custom-currency.md) format strings.
-Most commonly these are all `-,,,` but other strings occur.
+Most commonly these are all empty or `-,,,`, but other strings occur.
 
 A writer may safely use false for `x7`, `x8`, and `x9`.
 
index e124c3bbc7e65611e9e5b5399c579e564eb9cbdc..ae414aeb16ac86deaea1a00de4c2f39c34582c3e 100644 (file)
@@ -146,7 +146,7 @@ impl Convert {
                 let (items, page_setup) = pspp::spv::ReadOptions::new(|e| eprintln!("{e}"))
                     .with_password(self.password.clone())
                     .open_file(&self.input)?
-                    .into_parts();
+                    .into_contents();
                 let mut output = self.open_driver("text")?;
                 if let Some(page_setup) = &page_setup {
                     output.setup(page_setup);
index 50e0629cae819199bc2613c04017e8f7a1a0100d..2765855a13bee5b954ecc2266142268cc951e109 100644 (file)
@@ -46,7 +46,7 @@ impl Decrypt {
                 readpass::from_tty().unwrap()
             }
         };
-        let mut reader = match input.unlock(password.as_bytes()) {
+        let mut reader = match input.unlock(password) {
             Ok(reader) => reader,
             Err(_) => return Err(anyhow!("Incorrect password.")),
         };
index 547aed110f230918a7c9eaa49cb720aa7cd945af..03acc6c0d48da4a7efbc3a95ef84beff06467aea 100644 (file)
@@ -65,6 +65,9 @@ enum Mode {
     /// Reads `.tlo` or `.stt` TableLook and outputs as `.stt` format.
     ConvertTableLook,
 
+    /// Print data values in legacy tables.
+    LegacyData,
+
     /// Prints contents.
     View,
 }
@@ -75,6 +78,7 @@ impl Mode {
             Mode::Directory => "directory",
             Mode::GetTableLook => "get-table-look",
             Mode::ConvertTableLook => "convert-table-look",
+            Mode::LegacyData => "legacy-data",
             Mode::View => "view",
         }
     }
@@ -111,6 +115,15 @@ impl ShowSpv {
                 }
                 Ok(())
             }
+            Mode::LegacyData => {
+                let item = pspp::spv::ReadOptions::new(|e| eprintln!("{e}"))
+                    .with_password(self.password)
+                    .open_file(&self.input)?
+                    .into_items();
+                let items = self.criteria.apply(item);
+                for child in items {}
+                todo!()
+            }
             Mode::GetTableLook => todo!(),
             Mode::ConvertTableLook => todo!(),
         }
index cce9825632e674bc6cbf07793eeb8fc01f06d31b..ac7a2c4aecd26b1803a168a920ad5e5ae92e10a7 100644 (file)
@@ -16,37 +16,35 @@ use aes::{
     cipher::{BlockDecrypt, KeyInit, generic_array::GenericArray},
 };
 use cmac::{Cmac, Mac};
+use displaydoc::Display;
 use smallvec::SmallVec;
 use std::{
     fmt::Debug,
     io::{BufRead, Error as IoError, ErrorKind, Read, Seek, SeekFrom},
 };
-use thiserror::Error as ThisError;
 
 use binrw::{BinRead, io::NoSeek};
 
 /// Error reading an encrypted file.
-#[derive(Clone, Debug, ThisError)]
+#[derive(Clone, Debug, thiserror::Error, Display)]
 pub enum Error {
-    /// I/O error.
-    #[error("I/O error reading encrypted file wrapper ({0})")]
+    /// I/O error reading encrypted file wrapper ({0}).
     IoError(ErrorKind),
 
     /// Invalid padding in final encrypted data block.
-    #[error("Invalid padding in final encrypted data block")]
     InvalidPadding,
 
     /// Not an encrypted file.
-    #[error("Not an encrypted file")]
     NotEncrypted,
 
-    /// Encrypted file has invalid length.
-    #[error("Encrypted file has invalid length {0} (expected 4 more than a multiple of 16).")]
+    /// Encrypted file has invalid length {0} (expected 4 more than a multiple of 16).
     InvalidLength(u64),
 
-    /// Unknown file type.
-    #[error("Unknown file type {0:?}.")]
+    /// Unknown file type {0:?}.
     UnknownFileType(String),
+
+    /// Incorrect password.
+    WrongPassword,
 }
 
 impl From<std::io::Error> for Error {
@@ -69,6 +67,7 @@ struct EncryptedHeader {
 }
 
 /// An encrypted file.
+#[derive(Clone)]
 pub struct EncryptedFile<R> {
     reader: R,
     file_type: FileType,
@@ -155,7 +154,11 @@ where
     /// `password` decoded with [EncodedPassword::decode].  If successful,
     /// returns an [EncryptedReader] for the file; on failure, returns the
     /// [EncryptedFile] again for another try.
-    pub fn unlock(self, password: &[u8]) -> Result<EncryptedReader<R>, Self> {
+    pub fn unlock<P>(self, password: P) -> Result<EncryptedReader<R>, Self>
+    where
+        P: AsRef<[u8]>,
+    {
+        let password = password.as_ref();
         self.unlock_literal(password).or_else(|this| {
             match EncodedPassword::from_encoded(password) {
                 Some(encoded) => this.unlock_literal(&encoded.decode()),
@@ -170,7 +173,10 @@ where
     ///
     /// If the password itself might be encoded ("encrypted"), instead use
     /// [Self::unlock] to try it both ways.
-    pub fn unlock_literal(self, password: &[u8]) -> Result<EncryptedReader<R>, Self> {
+    pub fn unlock_literal<P>(self, password: P) -> Result<EncryptedReader<R>, Self>
+    where
+        P: AsRef<[u8]>,
+    {
         // NIST SP 800-108 fixed data.
         #[rustfmt::skip]
         static  FIXED: &[u8] = &[
@@ -197,6 +203,7 @@ where
         ];
 
         // Truncate password to at most 10 bytes.
+        let password = password.as_ref();
         let password = password.get(..10).unwrap_or(password);
         let n = password.len();
 
@@ -265,10 +272,7 @@ fn parse_padding(block: &[u8; 16]) -> Option<usize> {
     }
 }
 
-impl<R> Debug for EncryptedFile<R>
-where
-    R: Read,
-{
+impl<R> Debug for EncryptedFile<R> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "EncryptedFile({:?})", &self.file_type)
     }
@@ -276,8 +280,7 @@ where
 
 /// Encrypted file reader.
 ///
-/// This implements [Read] and [Seek] for SPSS encrypted files.  To construct an
-/// [EncryptedReader], call [EncryptedFile::new], then [EncryptedFile::unlock].
+/// This implements [Read] and [Seek] for SPSS encrypted files.
 pub struct EncryptedReader<R> {
     /// Underlying reader.
     reader: R,
@@ -305,7 +308,26 @@ pub struct EncryptedReader<R> {
     tail: usize,
 }
 
+/// The [Read] and [Seek] traits together, for use as `dyn ReadSeek`.
+pub trait ReadSeek: Read + Seek {}
+impl<T> ReadSeek for T where T: Read + Seek {}
+
 impl<R> EncryptedReader<R> {
+    /// Opens `reader` and unlocks it with the given password in one step.
+    ///
+    /// This fails if the password is wrong.  To allow for multiple password
+    /// tries, use [EncryptedFile::new] followed by [EncryptedFile::unlock]
+    /// instead.
+    pub fn open<P>(reader: R, password: P) -> Result<Self, Error>
+    where
+        R: Read + Seek,
+        P: AsRef<[u8]>,
+    {
+        EncryptedFile::new(reader)?
+            .unlock(password)
+            .map_err(|_| Error::WrongPassword)
+    }
+
     fn new(reader: R, aes: Aes256Dec, file_type: FileType, length: u64) -> Self {
         Self {
             reader,
@@ -515,7 +537,11 @@ pub struct EncodedPassword(Vec<Vec<char>>);
 impl EncodedPassword {
     /// Creates an [EncodedPassword] from an already-encoded password `encoded`.
     /// Returns `None` if `encoded` is not a valid encoded password.
-    pub fn from_encoded(encoded: &[u8]) -> Option<Self> {
+    pub fn from_encoded<P>(encoded: P) -> Option<Self>
+    where
+        P: AsRef<[u8]>,
+    {
+        let encoded = encoded.as_ref();
         if encoded.len() > 20
             || encoded.len() % 2 != 0
             || !encoded.iter().all(|byte| (32..=127).contains(byte))
@@ -531,7 +557,8 @@ impl EncodedPassword {
     /// Returns an [EncodedPassword] as an encoded version of the given
     /// `plaintext` password.  Only the first 10 bytes, at most, of the
     /// plaintext password is used.
-    pub fn from_plaintext(plaintext: &[u8]) -> EncodedPassword {
+    pub fn from_plaintext<P: AsRef<[u8]>>(plaintext: P) -> EncodedPassword {
+        let plaintext = plaintext.as_ref();
         let input = plaintext.get(..10).unwrap_or(plaintext);
         EncodedPassword(
             input
@@ -592,7 +619,7 @@ mod tests {
         let mut cursor = Cursor::new(&input);
         let file = EncryptedFile::new(&mut cursor).unwrap();
         assert_eq!(file.file_type(), file_type);
-        let mut reader = file.unlock_literal(password.as_bytes()).unwrap();
+        let mut reader = file.unlock_literal(password).unwrap();
         assert_eq!(reader.file_type(), file_type);
         let mut actual = Vec::new();
         std::io::copy(&mut reader, &mut actual).unwrap();
@@ -658,7 +685,7 @@ mod tests {
             let encoded = EncodedPassword::from_plaintext(&[plaintext]);
             for variant in 0..encoded.n_variants() {
                 let encoded_variant = encoded.variant(variant);
-                let decoded = EncodedPassword::from_encoded(encoded_variant.as_bytes())
+                let decoded = EncodedPassword::from_encoded(encoded_variant)
                     .unwrap()
                     .decode();
                 assert_eq!(&[plaintext], decoded.as_slice());
index 14cf853d9acd22c86b180134729c60d9cefdcdae..fe783a0b04e3c33c17eedde95e6a84f9829c7aea 100644 (file)
@@ -29,7 +29,7 @@ use serde::{Deserialize, Serialize};
 
 use crate::{
     output::{
-        Details, Item, ItemCursor, TextType,
+        Item, ItemCursor, TextType,
         drivers::{
             Driver,
             cairo::{
index cdb9255969de7af8fcb869f4a6a07e2655c4189f..842bf2702ab459e04ae9361a426264b2466c85f6 100644 (file)
@@ -1,5 +1,5 @@
 Column (All Layers)
-b1
+b: b1
 ╭──┬──┬──╮
 │a1│a2│a3│
 ├──┼──┼──┤
@@ -7,7 +7,7 @@ b1
 ╰──┴──┴──╯
 
 Column (All Layers)
-b2
+b: b2
 ╭──┬──┬──╮
 │a1│a2│a3│
 ├──┼──┼──┤
@@ -15,7 +15,7 @@ b2
 ╰──┴──┴──╯
 
 Column (All Layers)
-b3
+b: b3
 ╭──┬──┬──╮
 │a1│a2│a3│
 ├──┼──┼──┤
index 48a28c4991a7cd7483e03650c1158acd45623766..9a3c09891edf054cee5fdd2bdb6264729af9433f 100644 (file)
@@ -1,5 +1,5 @@
 Column x b1
-b1
+b: b1
 ╭──┬──┬──╮
 │a1│a2│a3│
 ├──┼──┼──┤
index 9e9323c81034e34a90b9af5d3cc0a54f23561fa6..6611a174d564861722626d23078fc90b0a9f192c 100644 (file)
@@ -1,5 +1,5 @@
 Column x b2
-b2
+b: b2
 ╭──┬──┬──╮
 │a1│a2│a3│
 ├──┼──┼──┤
index 0aa4682594747a279a48f853399b1acdeee23c57..2a83533b22920293d2f2b73f3b573c93f84d5277 100644 (file)
@@ -1,5 +1,5 @@
 Row (All Layers)
-b1
+b: b1
 ╭──┬─╮
 │a1│0│
 │a2│1│
@@ -7,7 +7,7 @@ b1
 ╰──┴─╯
 
 Row (All Layers)
-b2
+b: b2
 ╭──┬─╮
 │a1│3│
 │a2│4│
@@ -15,7 +15,7 @@ b2
 ╰──┴─╯
 
 Row (All Layers)
-b3
+b: b3
 ╭──┬─╮
 │a1│6│
 │a2│7│
index 1f7667590a143d7fcdd1b5f7e7b55875e34dc4a8..843d15dbbcc8897a048c228e038c86cd86362acc 100644 (file)
@@ -1,5 +1,5 @@
 Row x b1
-b1
+b: b1
 ╭──┬─╮
 │a1│0│
 │a2│1│
index 051797d19295c582ee6bcf7c440baf6e82041029..53ad394014ef93bd7f0099e80603b5719f0b84fe 100644 (file)
@@ -1,5 +1,5 @@
 Row x b2
-b2
+b: b2
 ╭──┬─╮
 │a1│3│
 │a2│4│
index c2eefe00c439ee1b88df3514aa857649ef6ca943..3ddf5ab679ee73cc45005316da4974db55f66a02 100644 (file)
@@ -1,6 +1,6 @@
 Column x b1 x a1
-b1
-a1
+b: b1
+a: a1
 ╭──┬──┬──┬──┬──╮
 │c1│c2│c3│c4│c5│
 ├──┼──┼──┼──┼──┤
index aaa4395528f3f32f01fd77993940890ee33dde60..8f4dd26678576a5ca12b3a391845c9c3a0651def 100644 (file)
@@ -1,6 +1,6 @@
 Column x b2 x a1
-b2
-a1
+b: b2
+a: a1
 ╭──┬──┬──┬──┬──╮
 │c1│c2│c3│c4│c5│
 ├──┼──┼──┼──┼──┤
index 55fb1525e238ac6b1d7f5f243890fc0812e157d4..bc679a3ebed7f5bb891e333c648dc118f617ad35 100644 (file)
@@ -1,6 +1,6 @@
 Column x b3 x a2
-b3
-a2
+b: b3
+a: a2
 ╭──┬──┬──┬──┬──╮
 │c1│c2│c3│c4│c5│
 ├──┼──┼──┼──┼──┤
index 158e31c10b4d3e0f231e927500bcc44b2172305f..611dd70ff264b11dc85a19f662ca958b983a38c8 100644 (file)
@@ -23,7 +23,7 @@ use std::{
     rc::Rc,
 };
 
-use anyhow::{Context, anyhow};
+use anyhow::Context;
 use binrw::{BinRead, error::ContextExt};
 use cairo::ImageSurface;
 use displaydoc::Display;
@@ -32,7 +32,7 @@ use serde::Deserialize;
 use zip::{ZipArchive, result::ZipError};
 
 use crate::{
-    crypto::EncryptedFile,
+    crypto::EncryptedReader,
     output::{
         Details, Item, SpvInfo, SpvMembers, Text, page,
         pivot::{Axis2, Length, TableProperties, look::Look, value::Value},
@@ -109,64 +109,49 @@ impl<F> ReadOptions<F> {
     }
 }
 
+pub trait ReadSeek: Read + Seek {}
+impl<T> ReadSeek for T where T: Read + Seek {}
+
 impl<F> ReadOptions<F>
 where
     F: FnMut(Warning) + 'static,
 {
     /// Opens the file at `path`.
-    pub fn open_file<P>(mut self, path: P) -> Result<SpvFile, anyhow::Error>
+    pub fn open_file<P>(self, path: P) -> Result<SpvFile, Error>
     where
         P: AsRef<Path>,
     {
-        let file = File::open(path)?;
-        if let Some(password) = self.password.take() {
-            self.open_reader_encrypted(file, password)
-        } else {
-            Self::open_reader_inner(file, self.warn)
-        }
+        self.open_reader(File::open(path)?)
     }
 
     /// Opens the file read from `reader`.
-    fn open_reader_encrypted<R>(self, reader: R, password: String) -> Result<SpvFile, anyhow::Error>
+    pub fn open_reader<R>(self, reader: R) -> Result<SpvFile, Error>
     where
         R: Read + Seek + 'static,
     {
-        Self::open_reader_inner(
-            EncryptedFile::new(reader)?
-                .unlock(password.as_bytes())
-                .map_err(|_| anyhow!("Incorrect password."))?,
-            self.warn,
-        )
+        let reader = match &self.password {
+            None => Box::new(reader) as Box<dyn ReadSeek>,
+            Some(password) => Box::new(EncryptedReader::open(reader, password)?),
+        };
+        self.open_reader_inner(Box::new(reader))
     }
 
-    /// Opens the file read from `reader`.
-    pub fn open_reader<R>(mut self, reader: R) -> Result<SpvFile, anyhow::Error>
-    where
-        R: Read + Seek + 'static,
-    {
-        if let Some(password) = self.password.take() {
-            self.open_reader_encrypted(reader, password)
-        } else {
-            Self::open_reader_inner(reader, self.warn)
-        }
-    }
-
-    fn open_reader_inner<R>(reader: R, warn: F) -> Result<SpvFile, anyhow::Error>
-    where
-        R: Read + Seek + 'static,
-    {
-        // Open archive.
-        let mut archive = ZipArchive::new(reader).map_err(|error| match error {
+    fn open_reader_inner(self, reader: Box<dyn ReadSeek>) -> Result<SpvFile, Error> {
+        let archive = ZipArchive::new(reader).map_err(|error| match error {
             ZipError::InvalidArchive(_) => Error::NotSpv,
             other => other.into(),
         })?;
-        Ok(Self::from_spv_zip_archive(&mut archive, warn)?)
+        Ok(self.open_zip_archive(archive)?)
     }
 
-    fn from_spv_zip_archive<R>(archive: &mut ZipArchive<R>, warn: F) -> Result<SpvFile, Error>
-    where
-        R: Read + Seek,
-    {
+    /// Opens the provided Zip `archive`.
+    ///
+    /// Any password provided for reading the file is unused, because if one was
+    /// needed then it must have already been used to open the archive.
+    pub fn open_zip_archive(
+        self,
+        mut archive: ZipArchive<Box<dyn ReadSeek>>,
+    ) -> Result<SpvFile, Error> {
         // Check manifest.
         let mut file = archive
             .by_name("META-INF/MANIFEST.MF")
@@ -179,13 +164,13 @@ where
         drop(file);
 
         // Read all the items.
-        let warn = Rc::new(RefCell::new(Box::new(warn) as Box<dyn FnMut(Warning)>));
+        let warn = Rc::new(RefCell::new(Box::new(self.warn) as Box<dyn FnMut(Warning)>));
         let mut items = Vec::new();
         let mut page_setup = None;
         for i in 0..archive.len() {
             let name = String::from(archive.name_for_index(i).unwrap());
             if name.starts_with("outputViewer") && name.ends_with(".xml") {
-                let (mut new_items, ps) = read_heading(archive, i, &name, &warn)?;
+                let (mut new_items, ps) = read_heading(&mut archive, i, &name, &warn)?;
                 items.append(&mut new_items);
                 page_setup = page_setup.or(ps);
             }
@@ -194,6 +179,7 @@ where
         Ok(SpvFile {
             items: items.into_iter().collect(),
             page_setup,
+            archive,
         })
     }
 }
@@ -205,11 +191,14 @@ pub struct SpvFile {
 
     /// The page setup in the SPV file, if any.
     pub page_setup: Option<page::PageSetup>,
+
+    /// The Zip archive that the file was read from.
+    pub archive: ZipArchive<Box<dyn ReadSeek>>,
 }
 
 impl SpvFile {
-    /// Returns the individual parts of the `SpvFile`.
-    pub fn into_parts(self) -> (Vec<Item>, Option<page::PageSetup>) {
+    /// Returns the contents of the `SpvFile`.
+    pub fn into_contents(self) -> (Vec<Item>, Option<page::PageSetup>) {
         (self.items, self.page_setup)
     }
 
@@ -227,6 +216,9 @@ pub enum Error {
     /// Not an SPV file.
     NotSpv,
 
+    /// {0}
+    EncryptionError(#[from] crate::crypto::Error),
+
     /// {0}
     ZipError(#[from] ZipError),
 
index f334ecc95ae8a5201653c8383e7871aa54789d2b..c4cb7ba7c4eaf2743c7abf715fdb0e97b266e0fc 100644 (file)
@@ -284,7 +284,7 @@ impl Markup {
                         .with_attribute(("color", color.display_css().to_string().as_str())),
                     Style::Size(points) => writer
                         .create_element("font")
-                        .with_attribute(("size", format!("{points}pt").as_str())),
+                        .with_attribute(("size", format!("{}pt", *points / 0.75).as_str())),
                 }
                 .write_inner_content(|w| child.write_html(w))?;
             }
@@ -765,7 +765,7 @@ fn parse_nodes(nodes: &[Node]) -> Markup {
                             && let Some(points) =
                                 [6.0, 7.5, 9.0, 10.5, 13.5, 18.0, 27.0].get(index).copied()
                         {
-                            apply_style(&mut inner, Style::Size(points));
+                            apply_style(&mut inner, Style::Size(points * 0.75));
                         }
                         None
                     }
@@ -1007,7 +1007,7 @@ mod tests {
         let document = Document::from_html(&content);
         assert_eq!(
             document.to_html(),
-            r##"<p align="center"><font color="#000000"><font face="sans-serif">&amp;[PageTitle]</font></font></p>"##
+            r##"<p align="center"><font color="#000000"><font size="10pt"><font face="sans-serif">&amp;[PageTitle]</font></font></font></p>"##
         );
         assert_eq!(
             document.0[0]
@@ -1042,7 +1042,7 @@ mod tests {
         let html = Document::from_html(&content);
         assert_eq!(
             html.to_html(),
-            r##"<p align="right"><font color="#000000"><font face="sans-serif">Page &amp;[Page]</font></font></p>"##
+            r##"<p align="right"><font color="#000000"><font size="10pt"><font face="sans-serif">Page &amp;[Page]</font></font></font></p>"##
         );
     }
 
index 321255a99759dda10e7f0b9fe1c890988e48ca74..251743e00b17d00631f46b8b70089c53b97edd49 100644 (file)
@@ -1074,7 +1074,9 @@ impl Y0 {
         match Decimal::try_from(c) {
             Ok(decimal) => decimal,
             Err(_) => {
-                warn(LightWarning::InvalidDecimal(c));
+                if c != '\0' {
+                    warn(LightWarning::InvalidDecimal(c));
+                }
                 Decimal::default()
             }
         }
@@ -1098,10 +1100,12 @@ impl CustomCurrency {
         let mut ccs = EnumMap::default();
         for (cc, string) in enum_iterator::all().zip(&self.ccs) {
             let string = string.decode(encoding);
-            if let Ok(style) = NumberStyle::from_str(&string) {
-                ccs[cc] = Some(Box::new(style));
-            } else {
-                warn(LightWarning::InvalidCustomCurrency(string));
+            if !string.is_empty() {
+                if let Ok(style) = NumberStyle::from_str(&string) {
+                    ccs[cc] = Some(Box::new(style));
+                } else {
+                    warn(LightWarning::InvalidCustomCurrency(string));
+                }
             }
         }
         ccs
index 2b39006329469f593ede602551890f9a114ae8f2..cee6929d874a02f659168022610cd89e23961d95 100644 (file)
@@ -24,7 +24,7 @@ use std::{
 };
 
 use crate::{
-    crypto::EncryptedFile,
+    crypto::EncryptedReader,
     data::{ByteString, Case, Datum, MutRawString, RawString},
     dictionary::{
         DictIndexMultipleResponseSet, DictIndexVariableSet, Dictionary, MrSetError,
@@ -50,7 +50,7 @@ use crate::{
     },
     variable::{InvalidRole, MissingValues, MissingValuesError, VarType, VarWidth, Variable},
 };
-use anyhow::{Error as AnyError, anyhow};
+use anyhow::Error as AnyError;
 use binrw::{BinRead, BinWrite, Endian};
 use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
 use encoding_rs::{Encoding, UTF_8};
@@ -542,9 +542,7 @@ impl<F> ReadOptions<F> {
         F: FnMut(AnyError),
     {
         Self::open_reader_inner(
-            EncryptedFile::new(reader)?
-                .unlock(password.as_bytes())
-                .map_err(|_| anyhow!("Incorrect password."))?,
+            EncryptedReader::open(reader, password)?,
             self.encoding,
             self.warn,
         )
index 3e2afa36614c016b5c530f5c89017dc7605199fb..1fb63e4f9a47bb15b64184efc4f53e979e883241 100644 (file)
@@ -732,7 +732,7 @@ fn test_encrypted_sysfile(name: &str, password: &str) {
         .with_extension("sav");
     let sysfile = EncryptedFile::new(File::open(&input_filename).unwrap())
         .unwrap()
-        .unlock(password.as_bytes())
+        .unlock(password)
         .unwrap();
     let expected_filename = input_filename.with_extension("expected");
     let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();