2bde62b758b0de13c74896205a3a9a905cd1a717
[pspp] / rust / src / lib.rs
1 #![allow(unused_variables)]
2 use endian::{Endian, Parse};
3 use num::Integer;
4 use num_derive::FromPrimitive;
5 use std::io::{BufReader, Error as IoError, Read, Seek};
6 use thiserror::Error;
7
8 pub mod endian;
9
10 #[derive(Error, Debug)]
11 pub enum Error {
12     #[error("Not an SPSS system file")]
13     NotASystemFile,
14
15     #[error("I/O error ({source})")]
16     Io {
17         #[from]
18         source: IoError,
19     },
20
21     #[error("Invalid SAV compression code {0}")]
22     InvalidSavCompression(u32),
23
24     #[error("Invalid ZSAV compression code {0}")]
25     InvalidZsavCompression(u32),
26
27     #[error("Misplaced type 4 record near offset {0:#x}.")]
28     MisplacedType4Record(u64),
29
30     #[error("Document record at offset {offset:#x} has document line count ({n}) greater than the maximum number {max}.")]
31     BadDocumentLength { offset: u64, n: u32, max: u32 },
32
33     #[error("At offset {offset:#x}, Unrecognized record type {rec_type}.")]
34     BadRecordType { offset: u64, rec_type: u32 },
35
36     #[error("At offset {offset:#x}, variable label code ({code}) is not 0 or 1.")]
37     BadVariableLabelCode { offset: u64, code: u32 },
38
39     #[error(
40         "At offset {offset:#x}, numeric missing value code ({code}) is not -3, -2, 0, 1, 2, or 3."
41     )]
42     BadNumericMissingValueCode { offset: u64, code: i32 },
43
44     #[error("At offset {offset:#x}, string missing value code ({code}) is not 0, 1, 2, or 3.")]
45     BadStringMissingValueCode { offset: u64, code: i32 },
46
47     #[error("At offset {offset:#x}, number of value labels ({n}) is greater than the maximum number {max}.")]
48     BadNumberOfValueLabels { offset: u64, n: u32, max: u32 },
49
50     #[error("At offset {offset:#x}, variable index record (type 4) does not immediately follow value label record (type 3) as it should.")]
51     MissingVariableIndexRecord { offset: u64 },
52
53     #[error("At offset {offset:#x}, number of variables associated with a value label ({n}) is not between 1 and the number of variables ({max}).")]
54     BadNumberOfValueLabelVariables { offset: u64, n: u32, max: u32 },
55
56     #[error("At offset {offset:#x}, record type 7 subtype {subtype} is too large with element size {size} and {count} elements.")]
57     ExtensionRecordTooLarge {
58         offset: u64,
59         subtype: u32,
60         size: u32,
61         count: u32,
62     },
63
64     #[error("Wrong ZLIB data header offset {zheader_offset:#x} (expected {offset:#x}).")]
65     BadZlibHeaderOffset { offset: u64, zheader_offset: u64 },
66
67     #[error("At offset {offset:#x}, impossible ZLIB trailer offset {ztrailer_offset:#x}.")]
68     BadZlibTrailerOffset { offset: u64, ztrailer_offset: u64 },
69
70     #[error("At offset {offset:#x}, impossible ZLIB trailer length {ztrailer_len}.")]
71     BadZlibTrailerLen { offset: u64, ztrailer_len: u64 },
72 }
73
74 #[derive(Error, Debug)]
75 pub enum Warning {
76     #[error("Unexpected floating-point bias {0} or unrecognized floating-point format.")]
77     UnexpectedBias(f64),
78
79     #[error("Duplicate type 6 (document) record.")]
80     DuplicateDocumentRecord,
81 }
82
83 #[derive(Copy, Clone, Debug)]
84 pub enum Compression {
85     Simple,
86     ZLib,
87 }
88
89 pub struct Reader<R: Read> {
90     r: BufReader<R>,
91     documents: Vec<DocumentRecord>,
92     variables: Vec<VariableRecord>,
93     value_labels: Vec<ValueLabelRecord>,
94     extensions: Vec<ExtensionRecord>,
95     zheader: Option<ZHeader>,
96 }
97
98 /// Magic number for a regular system file.
99 pub const ASCII_MAGIC: &[u8; 4] = b"$FL2";
100
101 /// Magic number for a system file that contains zlib-compressed data.
102 pub const ASCII_ZMAGIC: &[u8; 4] = b"$FL3";
103
104 /// Magic number for an EBDIC-encoded system file.  This is `$FL2` encoded in
105 /// EBCDIC.
106 pub const EBCDIC_MAGIC: &[u8; 4] = &[0x5b, 0xc6, 0xd3, 0xf2];
107
108 pub struct FileHeader {
109     /// First 4 bytes of the file, one of `ASCII_MAGIC`, `ASCII_ZMAGIC`, and
110     /// `EBCDIC_MAGIC`.
111     pub magic: [u8; 4],
112
113     /// True if `magic` indicates that this file contained zlib-compressed data.
114     pub is_zsav: bool,
115
116     /// True if `magic` indicates that this file contained EBCDIC data.
117     pub is_ebcdic: bool,
118
119     /// Endianness of the data in the file header.
120     pub endianness: Endian,
121
122     /// 0-based variable index of the weight variable, or `None` if the file is
123     /// unweighted.
124     pub weight_index: Option<u32>,
125
126     /// Number of variable positions, or `None` if the value in the file is
127     /// questionably trustworthy.
128     pub nominal_case_size: Option<u32>,
129
130     /// `dd mmm yy` in the file's encoding.
131     pub creation_date: [u8; 9],
132
133     /// `HH:MM:SS` in the file's encoding.
134     pub creation_time: [u8; 8],
135
136     /// Eye-catcher string, then product name, in the file's encoding.  Padded
137     /// on the right with spaces.
138     pub eye_catcher: [u8; 60],
139
140     /// File label, in the file's encoding.  Padded on the right with spaces.
141     pub file_label: [u8; 64],
142 }
143
144 impl<R: Read + Seek> Reader<R> {
145     pub fn new(r: R, warn: impl Fn(Warning)) -> Result<Reader<R>, Error> {
146         let mut r = BufReader::new(r);
147
148         let header = read_header(&mut r, &warn)?;
149         let e = header.endianness;
150         let mut documents = Vec::new();
151         let mut variables = Vec::new();
152         let mut value_labels = Vec::new();
153         let mut extensions = Vec::new();
154         loop {
155             let offset = r.stream_position()?;
156             let rec_type: u32 = e.parse(read_bytes(&mut r)?);
157             match rec_type {
158                 2 => variables.push(read_variable_record(&mut r, e)?),
159                 3 => value_labels.push(read_value_label_record(&mut r, e, variables.len())?),
160                 4 => return Err(Error::MisplacedType4Record(offset)),
161                 6 => documents.push(read_document_record(&mut r, e)?),
162                 7 => extensions.push(read_extension_record(&mut r, e)?),
163                 999 => break,
164                 _ => return Err(Error::BadRecordType { offset, rec_type }),
165             }
166         }
167         let _: [u8; 4] = read_bytes(&mut r)?;
168         let zheader = match header.is_zsav {
169             true => Some(read_zheader(&mut r, e)?),
170             false => None,
171         };
172
173         Ok(Reader {
174             r,
175             documents,
176             variables,
177             value_labels,
178             extensions,
179             zheader,
180         })
181     }
182 }
183
184 fn read_header<R: Read>(r: &mut R, warn: impl Fn(Warning)) -> Result<FileHeader, Error> {
185     let magic: [u8; 4] = read_bytes(r)?;
186     let (is_zsav, is_ebcdic) = match &magic {
187         ASCII_MAGIC => (false, false),
188         ASCII_ZMAGIC => (true, false),
189         EBCDIC_MAGIC => (false, true),
190         _ => return Err(Error::NotASystemFile),
191     };
192
193     let eye_catcher: [u8; 60] = read_bytes(r)?;
194     let layout_code: [u8; 4] = read_bytes(r)?;
195     let endianness = Endian::identify_u32(2, layout_code)
196         .or_else(|| Endian::identify_u32(2, layout_code))
197         .ok_or_else(|| Error::NotASystemFile)?;
198
199     let nominal_case_size: u32 = endianness.parse(read_bytes(r)?);
200     let nominal_case_size =
201         (nominal_case_size <= i32::MAX as u32 / 16).then_some(nominal_case_size);
202
203     let compression_code: u32 = endianness.parse(read_bytes(r)?);
204     let compression = match (is_zsav, compression_code) {
205         (false, 0) => None,
206         (false, 1) => Some(Compression::Simple),
207         (true, 2) => Some(Compression::ZLib),
208         (false, code) => return Err(Error::InvalidSavCompression(code)),
209         (true, code) => return Err(Error::InvalidZsavCompression(code)),
210     };
211
212     let weight_index: u32 = endianness.parse(read_bytes(r)?);
213     let weight_index = (weight_index > 0).then_some(weight_index - 1);
214
215     let n_cases: u32 = endianness.parse(read_bytes(r)?);
216     let n_cases = (n_cases < i32::MAX as u32 / 2).then_some(n_cases);
217
218     let bias: f64 = endianness.parse(read_bytes(r)?);
219     if bias != 100.0 {
220         warn(Warning::UnexpectedBias(bias))
221     }
222
223     let creation_date: [u8; 9] = read_bytes(r)?;
224     let creation_time: [u8; 8] = read_bytes(r)?;
225     let file_label: [u8; 64] = read_bytes(r)?;
226     let _: [u8; 3] = read_bytes(r)?;
227
228     Ok(FileHeader {
229         magic,
230         is_zsav,
231         is_ebcdic,
232         endianness,
233         weight_index,
234         nominal_case_size,
235         creation_date,
236         creation_time,
237         eye_catcher,
238         file_label,
239     })
240 }
241
242 pub struct VariableRecord {
243     /// Offset from the start of the file to the start of the record.
244     pub offset: u64,
245
246     /// Variable width, in the range -1..=255.
247     pub width: i32,
248
249     /// Variable name, padded on the right with spaces.
250     pub name: [u8; 8],
251
252     /// Print format.
253     pub print_format: u32,
254
255     /// Write format.
256     pub write_format: u32,
257
258     /// Missing value code, one of -3, -2, 0, 1, 2, or 3.
259     pub missing_value_code: i32,
260
261     /// Raw missing values, up to 3 of them.
262     pub missing: Vec<[u8; 8]>,
263
264     /// Optional variable label.
265     pub label: Option<Vec<u8>>,
266 }
267
268 fn read_variable_record<R: Read + Seek>(
269     r: &mut BufReader<R>,
270     e: Endian,
271 ) -> Result<VariableRecord, Error> {
272     let offset = r.stream_position()?;
273     let width: i32 = e.parse(read_bytes(r)?);
274     let has_variable_label: u32 = e.parse(read_bytes(r)?);
275     let missing_value_code: i32 = e.parse(read_bytes(r)?);
276     let print_format: u32 = e.parse(read_bytes(r)?);
277     let write_format: u32 = e.parse(read_bytes(r)?);
278     let name: [u8; 8] = read_bytes(r)?;
279
280     let label = match has_variable_label {
281         0 => None,
282         1 => {
283             let len: u32 = e.parse(read_bytes(r)?);
284             let read_len = len.min(65535) as usize;
285             let label = Some(read_vec(r, read_len)?);
286
287             let padding_bytes = Integer::next_multiple_of(&len, &4) - len;
288             let _ = read_vec(r, padding_bytes as usize)?;
289
290             label
291         }
292         _ => {
293             return Err(Error::BadVariableLabelCode {
294                 offset,
295                 code: has_variable_label,
296             })
297         }
298     };
299
300     let mut missing = Vec::new();
301     if missing_value_code != 0 {
302         match (width, missing_value_code) {
303             (0, -3 | -2 | 1 | 2 | 3) => (),
304             (0, _) => {
305                 return Err(Error::BadNumericMissingValueCode {
306                     offset,
307                     code: missing_value_code,
308                 })
309             }
310             (_, 0..=3) => (),
311             (_, _) => {
312                 return Err(Error::BadStringMissingValueCode {
313                     offset,
314                     code: missing_value_code,
315                 })
316             }
317         }
318
319         for _ in 0..missing_value_code.abs() {
320             missing.push(read_bytes(r)?);
321         }
322     }
323
324     Ok(VariableRecord {
325         offset,
326         width,
327         name,
328         print_format,
329         write_format,
330         missing_value_code,
331         missing,
332         label,
333     })
334 }
335
336 pub struct ValueLabelRecord {
337     /// Offset from the start of the file to the start of the record.
338     pub offset: u64,
339
340     /// The labels.
341     pub labels: Vec<([u8; 8], Vec<u8>)>,
342
343     /// The 0-based indexes of the variables to which the labels are assigned.
344     pub var_indexes: Vec<u32>,
345 }
346
347 pub const MAX_VALUE_LABELS: u32 = u32::MAX / 8;
348
349 fn read_value_label_record<R: Read + Seek>(
350     r: &mut BufReader<R>,
351     e: Endian,
352     n_var_records: usize,
353 ) -> Result<ValueLabelRecord, Error> {
354     let offset = r.stream_position()?;
355     let n: u32 = e.parse(read_bytes(r)?);
356     if n > MAX_VALUE_LABELS {
357         return Err(Error::BadNumberOfValueLabels {
358             offset,
359             n,
360             max: MAX_VALUE_LABELS,
361         });
362     }
363
364     let mut labels = Vec::new();
365     for _ in 0..n {
366         let value: [u8; 8] = read_bytes(r)?;
367         let label_len: u8 = e.parse(read_bytes(r)?);
368         let label_len = label_len as usize;
369         let padded_len = Integer::next_multiple_of(&(label_len + 1), &8);
370
371         let mut label = read_vec(r, padded_len)?;
372         label.truncate(label_len);
373         labels.push((value, label));
374     }
375
376     let rec_type: u32 = e.parse(read_bytes(r)?);
377     if rec_type != 4 {
378         return Err(Error::MissingVariableIndexRecord {
379             offset: r.stream_position()?,
380         });
381     }
382
383     let n_vars: u32 = e.parse(read_bytes(r)?);
384     if n_vars < 1 || n_vars as usize > n_var_records {
385         return Err(Error::BadNumberOfValueLabelVariables {
386             offset: r.stream_position()?,
387             n: n_vars,
388             max: n_var_records as u32,
389         });
390     }
391     let mut var_indexes = Vec::with_capacity(n_vars as usize);
392     for _ in 0..n_vars {
393         var_indexes.push(e.parse(read_bytes(r)?));
394     }
395
396     Ok(ValueLabelRecord {
397         offset,
398         labels,
399         var_indexes,
400     })
401 }
402
403 pub const DOC_LINE_LEN: u32 = 80;
404 pub const DOC_MAX_LINES: u32 = i32::MAX as u32 / DOC_LINE_LEN;
405
406 pub struct DocumentRecord {
407     /// Offset from the start of the file to the start of the record.
408     pub pos: u64,
409
410     /// The document, as an array of 80-byte lines.
411     pub lines: Vec<[u8; DOC_LINE_LEN as usize]>,
412 }
413
414 fn read_document_record<R: Read + Seek>(
415     r: &mut BufReader<R>,
416     e: Endian,
417 ) -> Result<DocumentRecord, Error> {
418     let offset = r.stream_position()?;
419     let n: u32 = e.parse(read_bytes(r)?);
420     match n {
421         0..=DOC_MAX_LINES => {
422             let pos = r.stream_position()?;
423             let mut lines = Vec::with_capacity(n as usize);
424             for _ in 0..n {
425                 let line: [u8; 80] = read_bytes(r)?;
426                 lines.push(line);
427             }
428             Ok(DocumentRecord { pos, lines })
429         }
430         _ => Err(Error::BadDocumentLength {
431             offset,
432             n,
433             max: DOC_MAX_LINES,
434         }),
435     }
436 }
437
438 #[derive(FromPrimitive)]
439 enum Extension {
440     /// Machine integer info.
441     Integer = 3,
442     /// Machine floating-point info.
443     Float = 4,
444     /// Variable sets.
445     VarSets = 5,
446     /// DATE.
447     Date = 6,
448     /// Multiple response sets.
449     Mrsets = 7,
450     /// SPSS Data Entry.
451     DataEntry = 8,
452     /// Extra product info text.
453     ProductInfo = 10,
454     /// Variable display parameters.
455     Display = 11,
456     /// Long variable names.
457     LongNames = 13,
458     /// Long strings.
459     LongStrings = 14,
460     /// Extended number of cases.
461     Ncases = 16,
462     /// Data file attributes.
463     FileAttrs = 17,
464     /// Variable attributes.
465     VarAttrs = 18,
466     /// Multiple response sets (extended).
467     Mrsets2 = 19,
468     /// Character encoding.
469     Encoding = 20,
470     /// Value labels for long strings.
471     LongLabels = 21,
472     /// Missing values for long strings.
473     LongMissing = 22,
474     /// "Format properties in dataview table".
475     Dataview = 24,
476 }
477
478 struct ExtensionRecord {
479     /// Offset from the start of the file to the start of the record.
480     offset: u64,
481
482     /// Record subtype.
483     subtype: u32,
484
485     /// Size of each data element.
486     size: u32,
487
488     /// Number of data elements.
489     count: u32,
490
491     /// `size * count` bytes of data.
492     data: Vec<u8>,
493 }
494
495 fn extension_record_size_requirements(extension: Extension) -> (u32, u32) {
496     match extension {
497         /* Implemented record types. */
498         Extension::Integer => (4, 8),
499         Extension::Float => (8, 3),
500         Extension::VarSets => (1, 0),
501         Extension::Mrsets => (1, 0),
502         Extension::ProductInfo => (1, 0),
503         Extension::Display => (4, 0),
504         Extension::LongNames => (1, 0),
505         Extension::LongStrings => (1, 0),
506         Extension::Ncases => (8, 2),
507         Extension::FileAttrs => (1, 0),
508         Extension::VarAttrs => (1, 0),
509         Extension::Mrsets2 => (1, 0),
510         Extension::Encoding => (1, 0),
511         Extension::LongLabels => (1, 0),
512         Extension::LongMissing => (1, 0),
513
514         /* Ignored record types. */
515         Extension::Date => (0, 0),
516         Extension::DataEntry => (0, 0),
517         Extension::Dataview => (0, 0),
518     }
519 }
520
521 fn read_extension_record<R: Read + Seek>(
522     r: &mut BufReader<R>,
523     e: Endian,
524 ) -> Result<ExtensionRecord, Error> {
525     let subtype = e.parse(read_bytes(r)?);
526     let offset = r.stream_position()?;
527     let size: u32 = e.parse(read_bytes(r)?);
528     let count = e.parse(read_bytes(r)?);
529     let Some(product) = size.checked_mul(count) else {
530         return Err(Error::ExtensionRecordTooLarge {
531             offset,
532             subtype,
533             size,
534             count,
535         });
536     };
537     let offset = r.stream_position()?;
538     let data = read_vec(r, product as usize)?;
539     Ok(ExtensionRecord {
540         offset,
541         subtype,
542         size,
543         count,
544         data,
545     })
546 }
547
548 struct ZHeader {
549     /// File offset to the start of the record.
550     offset: u64,
551
552     /// File offset to the ZLIB data header.
553     zheader_offset: u64,
554
555     /// File offset to the ZLIB trailer.
556     ztrailer_offset: u64,
557
558     /// Length of the ZLIB trailer in bytes.
559     ztrailer_len: u64,
560 }
561
562 fn read_zheader<R: Read + Seek>(r: &mut BufReader<R>, e: Endian) -> Result<ZHeader, Error> {
563     let offset = r.stream_position()?;
564     let zheader_offset: u64 = e.parse(read_bytes(r)?);
565     let ztrailer_offset: u64 = e.parse(read_bytes(r)?);
566     let ztrailer_len: u64 = e.parse(read_bytes(r)?);
567
568     if zheader_offset != offset {
569         return Err(Error::BadZlibHeaderOffset {
570             offset,
571             zheader_offset,
572         });
573     }
574     if ztrailer_offset < offset {
575         return Err(Error::BadZlibTrailerOffset {
576             offset,
577             ztrailer_offset,
578         });
579     }
580     if ztrailer_len < 24 || ztrailer_len % 24 != 0 {
581         return Err(Error::BadZlibTrailerLen {
582             offset,
583             ztrailer_len,
584         });
585     }
586
587     Ok(ZHeader {
588         offset,
589         zheader_offset,
590         ztrailer_offset,
591         ztrailer_len,
592     })
593 }
594
595 fn read_bytes<const N: usize, R: Read>(r: &mut R) -> Result<[u8; N], IoError> {
596     let mut buf = [0; N];
597     r.read_exact(&mut buf)?;
598     Ok(buf)
599 }
600
601 fn read_vec<R: Read>(r: &mut BufReader<R>, n: usize) -> Result<Vec<u8>, IoError> {
602     let mut vec = vec![0; n];
603     r.read_exact(&mut vec)?;
604     Ok(vec)
605 }
606
607 /*
608 fn trim_end(mut s: Vec<u8>, c: u8) -> Vec<u8> {
609     while s.last() == Some(&c) {
610         s.pop();
611     }
612     s
613 }
614
615 fn skip_bytes<R: Read>(r: &mut R, mut n: u64) -> Result<(), IoError> {
616     let mut buf = [0; 1024];
617     while n > 0 {
618         let chunk = u64::min(n, buf.len() as u64);
619         r.read_exact(&mut buf[0..chunk as usize])?;
620         n -= chunk;
621     }
622     Ok(())
623 }
624
625 */