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 `'`
`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`.
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);
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.")),
};
/// Reads `.tlo` or `.stt` TableLook and outputs as `.stt` format.
ConvertTableLook,
+ /// Print data values in legacy tables.
+ LegacyData,
+
/// Prints contents.
View,
}
Mode::Directory => "directory",
Mode::GetTableLook => "get-table-look",
Mode::ConvertTableLook => "convert-table-look",
+ Mode::LegacyData => "legacy-data",
Mode::View => "view",
}
}
}
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!(),
}
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 {
}
/// An encrypted file.
+#[derive(Clone)]
pub struct EncryptedFile<R> {
reader: R,
file_type: FileType,
/// `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()),
///
/// 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] = &[
];
// Truncate password to at most 10 bytes.
+ let password = password.as_ref();
let password = password.get(..10).unwrap_or(password);
let n = password.len();
}
}
-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)
}
/// 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,
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,
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))
/// 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
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();
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());
use crate::{
output::{
- Details, Item, ItemCursor, TextType,
+ Item, ItemCursor, TextType,
drivers::{
Driver,
cairo::{
Column (All Layers)
-b1
+b: b1
╭──┬──┬──╮
│a1│a2│a3│
├──┼──┼──┤
╰──┴──┴──╯
Column (All Layers)
-b2
+b: b2
╭──┬──┬──╮
│a1│a2│a3│
├──┼──┼──┤
╰──┴──┴──╯
Column (All Layers)
-b3
+b: b3
╭──┬──┬──╮
│a1│a2│a3│
├──┼──┼──┤
Column x b1
-b1
+b: b1
╭──┬──┬──╮
│a1│a2│a3│
├──┼──┼──┤
Column x b2
-b2
+b: b2
╭──┬──┬──╮
│a1│a2│a3│
├──┼──┼──┤
Row (All Layers)
-b1
+b: b1
╭──┬─╮
│a1│0│
│a2│1│
╰──┴─╯
Row (All Layers)
-b2
+b: b2
╭──┬─╮
│a1│3│
│a2│4│
╰──┴─╯
Row (All Layers)
-b3
+b: b3
╭──┬─╮
│a1│6│
│a2│7│
Row x b1
-b1
+b: b1
╭──┬─╮
│a1│0│
│a2│1│
Row x b2
-b2
+b: b2
╭──┬─╮
│a1│3│
│a2│4│
Column x b1 x a1
-b1
-a1
+b: b1
+a: a1
╭──┬──┬──┬──┬──╮
│c1│c2│c3│c4│c5│
├──┼──┼──┼──┼──┤
Column x b2 x a1
-b2
-a1
+b: b2
+a: a1
╭──┬──┬──┬──┬──╮
│c1│c2│c3│c4│c5│
├──┼──┼──┼──┼──┤
Column x b3 x a2
-b3
-a2
+b: b3
+a: a2
╭──┬──┬──┬──┬──╮
│c1│c2│c3│c4│c5│
├──┼──┼──┼──┼──┤
rc::Rc,
};
-use anyhow::{Context, anyhow};
+use anyhow::Context;
use binrw::{BinRead, error::ContextExt};
use cairo::ImageSurface;
use displaydoc::Display;
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},
}
}
+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")
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);
}
Ok(SpvFile {
items: items.into_iter().collect(),
page_setup,
+ archive,
})
}
}
/// 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)
}
/// Not an SPV file.
NotSpv,
+ /// {0}
+ EncryptionError(#[from] crate::crypto::Error),
+
/// {0}
ZipError(#[from] ZipError),
.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))?;
}
&& 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
}
let document = Document::from_html(&content);
assert_eq!(
document.to_html(),
- r##"<p align="center"><font color="#000000"><font face="sans-serif">&[PageTitle]</font></font></p>"##
+ r##"<p align="center"><font color="#000000"><font size="10pt"><font face="sans-serif">&[PageTitle]</font></font></font></p>"##
);
assert_eq!(
document.0[0]
let html = Document::from_html(&content);
assert_eq!(
html.to_html(),
- r##"<p align="right"><font color="#000000"><font face="sans-serif">Page &[Page]</font></font></p>"##
+ r##"<p align="right"><font color="#000000"><font size="10pt"><font face="sans-serif">Page &[Page]</font></font></font></p>"##
);
}
match Decimal::try_from(c) {
Ok(decimal) => decimal,
Err(_) => {
- warn(LightWarning::InvalidDecimal(c));
+ if c != '\0' {
+ warn(LightWarning::InvalidDecimal(c));
+ }
Decimal::default()
}
}
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
};
use crate::{
- crypto::EncryptedFile,
+ crypto::EncryptedReader,
data::{ByteString, Case, Datum, MutRawString, RawString},
dictionary::{
DictIndexMultipleResponseSet, DictIndexVariableSet, Dictionary, MrSetError,
},
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};
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,
)
.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();