41dec4d92fe3c33cad9dec9d511717900d209191
[pspp] / rust / src / main.rs
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2023 Free Software Foundation, Inc.
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
16
17 use anyhow::{anyhow, Result};
18 use clap::Parser;
19 use num::Num;
20 use std::fs::File;
21 use std::io::prelude::*;
22 use std::io::BufReader;
23 use std::path::{Path, PathBuf};
24
25 /// A utility to dissect SPSS system files.
26 #[derive(Parser, Debug)]
27 #[command(author, version, about, long_about = None)]
28 struct Args {
29     /// Maximum number of cases to print.
30     #[arg(long = "data", default_value_t = 0)]
31     max_cases: usize,
32
33     /// Files to dissect.
34     #[arg(required = true)]
35     files: Vec<PathBuf>
36 }
37
38 fn main() -> Result<()> {
39     let Args { max_cases, files } = Args::parse();
40
41     let error = false;
42     for file in files {
43         Dissector::new(file)?;
44     }
45     Ok(())
46 }
47
48 #[derive(Copy, Clone, Debug)]
49 enum Compression {
50     Simple,
51     ZLib
52 }
53
54 #[derive(Copy, Clone, Debug)]
55 enum Endianness {
56     BigEndian,
57     LittleEndian
58 }
59 use Endianness::*;
60
61 trait Parse<T, const N: usize> {
62     fn parse(self, bytes: [u8; N]) -> T;
63 }
64 impl Parse<u64, 8> for Endianness {
65     fn parse(self, bytes: [u8; 8]) -> u64 {
66         match self {
67             BigEndian => u64::from_be_bytes(bytes),
68             LittleEndian => u64::from_le_bytes(bytes)
69         }
70     }
71 }
72 impl Parse<u32, 4> for Endianness {
73     fn parse(self, bytes: [u8; 4]) -> u32 {
74         match self {
75             BigEndian => u32::from_be_bytes(bytes),
76             LittleEndian => u32::from_le_bytes(bytes)
77         }
78     }
79 }
80 impl Parse<u16, 2> for Endianness {
81     fn parse(self, bytes: [u8; 2]) -> u16 {
82         match self {
83             BigEndian => u16::from_be_bytes(bytes),
84             LittleEndian => u16::from_le_bytes(bytes)
85         }
86     }
87 }
88 impl Parse<u8, 1> for Endianness {
89     fn parse(self, bytes: [u8; 1]) -> u8 {
90         match self {
91             BigEndian => u8::from_be_bytes(bytes),
92             LittleEndian => u8::from_le_bytes(bytes)
93         }
94     }
95 }
96 impl Parse<i64, 8> for Endianness {
97     fn parse(self, bytes: [u8; 8]) -> i64 {
98         match self {
99             BigEndian => i64::from_be_bytes(bytes),
100             LittleEndian => i64::from_le_bytes(bytes)
101         }
102     }
103 }
104 impl Parse<i32, 4> for Endianness {
105     fn parse(self, bytes: [u8; 4]) -> i32 {
106         match self {
107             BigEndian => i32::from_be_bytes(bytes),
108             LittleEndian => i32::from_le_bytes(bytes)
109         }
110     }
111 }
112 impl Parse<i16, 2> for Endianness {
113     fn parse(self, bytes: [u8; 2]) -> i16 {
114         match self {
115             BigEndian => i16::from_be_bytes(bytes),
116             LittleEndian => i16::from_le_bytes(bytes)
117         }
118     }
119 }
120 impl Parse<i8, 1> for Endianness {
121     fn parse(self, bytes: [u8; 1]) -> i8 {
122         match self {
123             BigEndian => i8::from_be_bytes(bytes),
124             LittleEndian => i8::from_le_bytes(bytes)
125         }
126     }
127 }
128 impl Parse<f64, 8> for Endianness {
129     fn parse(self, bytes: [u8; 8]) -> f64 {
130         match self {
131             BigEndian => f64::from_be_bytes(bytes),
132             LittleEndian => f64::from_le_bytes(bytes)
133         }
134     }
135 }
136
137 fn read_bytes<const N: usize>(r: &mut BufReader<File>) -> Result<[u8; N]> {
138     let mut buf = [0; N];
139     r.read_exact(&mut buf)?;
140     Ok(buf)
141 }
142
143 fn read_vec(r: &mut BufReader<File>, n: usize) -> Result<Vec<u8>> {
144     let mut vec = Vec::with_capacity(n);
145     vec.resize(n, 0);
146     r.read_exact(&mut vec)?;
147     Ok(vec)
148 }    
149
150 trait ReadSwap<T> {
151     fn read_swap(&mut self) -> Result<T>;
152 }
153
154 impl ReadSwap<u32> for Dissector {
155     fn read_swap(&mut self) -> Result<u32> {
156         Ok(self.endianness.parse(read_bytes(&mut self.r)?))
157     }
158 }
159 impl ReadSwap<u8> for Dissector {
160     fn read_swap(&mut self) -> Result<u8> {
161         Ok(self.endianness.parse(read_bytes(&mut self.r)?))
162     }
163 }
164
165 impl ReadSwap<i32> for Dissector {
166     fn read_swap(&mut self) -> Result<i32> {
167         Ok(self.endianness.parse(read_bytes(&mut self.r)?))
168     }
169 }
170
171 impl ReadSwap<f64> for Dissector {
172     fn read_swap(&mut self) -> Result<f64> {
173         Ok(self.endianness.parse(read_bytes(&mut self.r)?))
174     }
175 }
176
177 struct Dissector {
178     filename: String,
179     r: BufReader<File>,
180     compression: Option<Compression>,
181     endianness: Endianness,
182     fp_format: Endianness,
183     bias: f64,
184     n_variable_records: usize,
185     n_variables: usize,
186     var_widths: Vec<i32>,
187 }
188
189 fn detect_endianness(layout_code: [u8; 4]) -> Option<Endianness> {
190     for endianness in [BigEndian, LittleEndian] {
191         match endianness.parse(layout_code) {
192             2 | 3 => return Some(endianness),
193             _ => ()
194         }
195     }
196     None
197 }
198
199 fn detect_fp_format(bias: [u8; 8]) -> Option<Endianness> {
200     for endianness in [BigEndian, LittleEndian] {
201         let value: f64 = endianness.parse(bias);
202         if value == 100.0 {
203             return Some(endianness)
204         }
205     }
206     None
207 }
208
209 fn trim_end(mut s: Vec<u8>, c: u8) -> Vec<u8> {
210     while s.last() == Some(&c) {
211         s.pop();
212     }
213     s
214 }
215
216 fn format_name(type_: u32) -> &'static str {
217     match type_ {
218         1 => "A",
219         2 => "AHEX",
220         3 => "COMMA",
221         4 => "DOLLAR",
222         5 => "F",
223         6 => "IB",
224         7 => "PIBHEX",
225         8 => "P",
226         9 => "PIB",
227         10 => "PK",
228         11 => "RB",
229         12 => "RBHEX",
230         15 => "Z",
231         16 => "N",
232         17 => "E",
233         20 => "DATE",
234         21 => "TIME",
235         22 => "DATETIME",
236         23 => "ADATE",
237         24 => "JDATE",
238         25 => "DTIME",
239         26 => "WKDAY",
240         27 => "MONTH",
241         28 => "MOYR",
242         29 => "QYR",
243         30 => "WKYR",
244         31 => "PCT",
245         32 => "DOT",
246         33 => "CCA",
247         34 => "CCB",
248         35 => "CCC",
249         36 => "CCD",
250         37 => "CCE",
251         38 => "EDATE",
252         39 => "SDATE",
253         40 => "MTIME",
254         41 => "YMDHMS",
255         _ => "invalid"
256     }
257 }
258
259 fn round_up<T: Num + Copy>(x: T, y: T) -> T
260 {
261     (x + (y - T::one())) / y * y
262 }
263
264 impl UntypedValue {
265     fn new(
266 }
267
268 impl Dissector {
269     fn new<P: AsRef<Path>>(filename: P) -> Result<Dissector> {
270         let mut r = BufReader::new(File::open(&filename)?);
271         let filename = filename.as_ref().to_string_lossy().into_owned();
272         let rec_type: [u8; 4] = read_bytes(&mut r)?;
273         let zmagic = match &rec_type {
274             b"$FL2" => false,
275             b"$FL3" => true,
276             _ => Err(anyhow!("This is not an SPSS system file."))?
277         };
278
279         let eye_catcher: [u8; 60] = read_bytes(&mut r)?;
280         let layout_code: [u8; 4] = read_bytes(&mut r)?;
281         let endianness = detect_endianness(layout_code)
282             .ok_or_else(|| anyhow!("This is not an SPSS system file."))?;
283         let layout_code: u32 = endianness.parse(layout_code);
284         let _nominal_case_size: [u8; 4] = read_bytes(&mut r)?;
285         let compressed: u32 = endianness.parse(read_bytes(&mut r)?);
286         let compression = match (zmagic, compressed) {
287             (false, 0) => None,
288             (false, 1) => Some(Compression::Simple),
289             (true, 2) => Some(Compression::ZLib),
290             _ => Err(anyhow!("{} file header has invalid compression value {compressed}.",
291                              if zmagic { "ZSAV" } else { "SAV" }))?,
292         };
293
294         let weight_index: u32 = endianness.parse(read_bytes(&mut r)?);
295         let n_cases: u32 = endianness.parse(read_bytes(&mut r)?);
296
297         let bias: [u8; 8] = read_bytes(&mut r)?;
298         let fp_format = detect_fp_format(bias)
299             .unwrap_or_else(|| { eprintln!("Compression bias is not the usual value of 100, or system file uses unrecognized floating-point format."); endianness });
300         let bias: f64 = fp_format.parse(bias);
301
302         let mut d = Dissector {
303             filename,
304             r,
305             compression,
306             endianness,
307             fp_format,
308             bias,
309             n_variable_records: 0,
310             n_variables: 0,
311             var_widths: Vec::new(),
312         };
313
314         let creation_date: [u8; 9] = read_bytes(&mut d.r)?;
315         let creation_time: [u8; 8] = read_bytes(&mut d.r)?;
316         let file_label: [u8; 64] = read_bytes(&mut d.r)?;
317         let mut file_label = trim_end(Vec::from(file_label), b' ');
318         d.r.seek_relative(3)?;
319
320         println!("File header record:");
321         println!("{:>17}: {}", "Product name", String::from_utf8_lossy(&eye_catcher));
322         println!("{:>17}: {}", "Layout code", layout_code);
323         println!("{:>17}: {} ({})", "Compressed", compressed, match compression {
324             None => "no compression",
325             Some(Compression::Simple) => "simple compression",
326             Some(Compression::ZLib) => "ZLIB compression",
327         });
328         println!("{:>17}: {}", "Weight index", weight_index);
329         println!("{:>17}: {}", "Number of cases", n_cases);
330         println!("{:>17}: {}", "Compression bias", bias);
331         println!("{:>17}: {}", "Creation date", String::from_utf8_lossy(&creation_date));
332         println!("{:>17}: {}", "Creation time", String::from_utf8_lossy(&creation_time));
333         println!("{:>17}: \"{}\"", "File label", String::from_utf8_lossy(&file_label));
334
335         loop {
336             let rec_type: u32 = d.read_swap()?;
337             match rec_type {
338                 2 => d.read_variable_record()?,
339                 3 => d.read_value_label_record()?,
340                 4 => Err(anyhow!("Misplaced type 4 record."))?,
341                 999 => break,
342                 _ => Err(anyhow!("Unrecognized record type {rec_type}."))?
343             }
344         }
345
346         let pos = d.r.stream_position()?;
347         println!("{:08x}: end-of-dictionary record (first byte of data at {:0x})", pos, pos + 4);
348
349         Ok(d)
350     }
351
352     fn read_variable_record(&mut self) -> Result<()> {
353         self.n_variable_records += 1;
354         println!("{:08x}: variable record {}", self.r.stream_position()?, self.n_variable_records);
355         let width: i32 = self.read_swap()?;
356         let has_variable_label: u32 = self.read_swap()?;
357         let missing_value_code: i32 = self.read_swap()?;
358         let print_format: u32 = self.read_swap()?;
359         let write_format: u32 = self.read_swap()?;
360         let name: [u8; 8] = read_bytes(&mut self.r)?;
361         let name: Vec<u8> = trim_end(Vec::from(name), b'\0');
362
363         if width >= 0 {
364             self.n_variables += 1;
365         }
366         self.var_widths.push(width);
367
368         println!("\tWidth: {width} ({})", match width {
369             _ if width > 0 => "string",
370             _ if width == 0 => "numeric",
371             _ => "long string continuation record"
372         });
373
374         println!("\tVariable label: {has_variable_label}");
375         println!("\tMissing values code: {missing_value_code} ({})",
376                  match missing_value_code {
377                      0 => "no missing values",
378                      1 => "one missing value",
379                      2 => "two missing values",
380                      3 => "three missing values",
381                      -2 => "one missing value range",
382                      -3 => "one missing value, one range",
383                      _ => "bad value"
384                  });
385         for (which, format) in [("Print", print_format),
386                                 ("Worite", write_format)] {
387             let type_ = format_name(format >> 16);
388             let w = (format >> 8) & 0xff;
389             let d = format & 0xff;
390             println!("\t{which} format: {format:06x} ({type_}{w}.{d})");
391         }
392         println!("\tName: {}", String::from_utf8_lossy(&name));
393
394         // Read variable label.
395         match has_variable_label {
396             0 => (),
397             1 => {
398                 let offset = self.r.stream_position()?;
399                 let len: u32 = self.read_swap()?;
400                 let read_len = len.min(65535) as usize;
401                 let label = read_vec(&mut self.r, read_len)?;
402                 println!("\t{offset:08x} Variable label: \"{}\"", String::from_utf8_lossy(&label));
403
404                 self.r.seek_relative((round_up(len, 4) - len).into())?;
405             },
406             _ => Err(anyhow!("Variable label indicator field is not 0 or 1."))?,
407         };
408
409         // Read missing values.
410         if missing_value_code != 0 {
411             print!("\t{:08x} Missing values:", self.r.stream_position()?);
412             if width == 0 {
413                 let (has_range, n_individual) = match missing_value_code {
414                     -3 => (true, 1),
415                     -2 => (true, 0),
416                     1 | 2 | 3 => (false, missing_value_code),
417                     _ => Err(anyhow!("Numeric missing value indicator field is not -3, -2, 0, 1, 2, or 3."))?,
418                 };
419                 if has_range {
420                     let low: f64 = self.read_swap()?;
421                     let high: f64 = self.read_swap()?;
422                     print!(" {low}...{high}");
423                 }
424                 for _i in 0..n_individual {
425                     let value: f64 = self.read_swap()?;
426                     print!(" {value}");
427                 }
428             } else if width > 0 {
429                 if missing_value_code < 1 || missing_value_code > 3 {
430                     Err(anyhow!("String missing value indicator field is not 0, 1, 2, or 3."))?;
431                 }
432                 for _i in 0..missing_value_code {
433                     let string: [u8; 8] = read_bytes(&mut self.r)?;
434                     let string: Vec<u8> = trim_end(Vec::from(string), b'\0');
435                     println!(" {}", String::from_utf8_lossy(&string));
436                 }
437             }
438             println!();
439         }
440
441         Ok(())
442     }
443
444     fn read_value_label_record(&mut self) -> Result<()> {
445         println!("{:08x}: value labels record", self.r.stream_position()?);
446
447         let n_labels: u32 = self.read_swap()?;
448         for _i in 0..n_labels {
449             let raw: [u8; 8] = read_bytes(&mut self.r)?;
450             let label_len: u8 = self.read_swap()?;
451             let padded_len = round_up(label_len as usize + 1, 8);
452
453             let mut label = read_vec(&mut self.r, padded_len)?;
454             label.truncate(label_len as usize);
455             print
456         }
457
458         Ok(())
459     }
460 }