1 //! This file contains code to read and write SLF files.
2 //!
3 //! SLF is a file format that holds a collection of files and has the file extension `.slf`.
4 //!
5 //! I'm calling it "Sir-tech Library File" based on the name of STCI/STI.
6 //!
7 //!
8 //! # File Structure
9 //!
10 //! Based on "src/sgp/LibraryDatabase.cc", the file has the following structure:
11 //!
12 //!  * header - 532 bytes, always at the start of the file
13 //!  * data - any size, contains the data of the entries
14 //!  * entries - 280 bytes per entry, always at the end of the file
15 //!
16 //! Each entry represents a file.
17 //!
18 //! Numeric values are in little endian.
19 //!
20 //! Strings are '\0' terminated and have unused bytes zeroed.
21 //!
22 //! The paths are case-insensitive and use the '\\' character as a directory separator.
23 //! Probably the special names for current directory "." and parent directory ".." are not supported.
24 //! The header contains a library path, it is a path relative to the default directory (Data dir).
25 //! Each entry contains a file path, it is a path relative to the library path.
26 //! The encoding of the paths is unknown, but so far I've only seen ASCII.
27 //!
28 //!
29 //! # Header Structure
30 //!
31 //! Based on LIBHEADER in "src/sgp/LibraryDatabase.cc", the header has the following structure (532 bytes):
32 //!
33 //!  * 256 byte string with the library name
34 //!  * 256 byte string with the library path (empty or terminated by '\\', relative to Data dir)
35 //!  * 4 byte signed number with the total number of entries
36 //!  * 4 byte signed number with the total number of entries that have state FILE_OK 0x00
37 //!  * 2 byte unsigned number with name iSort (not used, only saw 0xFFFF, probably means it's sorted)
38 //!  * 2 byte unsigned number with name iVersion (not used, only saw 0x0200, probably means v2.0)
39 //!  * 1 byte unsigned number with name fContainsSubDirectories (not used, saw 0 and 1)
40 //!  * 3 byte padding (4 byte alignment)
41 //!  * 4 byte signed number with name iReserved (not used)
42 //!
43 //!
44 //! # Entry Structure
45 //!
46 //! Based on DIRENTRY in "src/sgp/LibraryDatabase.cc", the header has the following structure (280 bytes):
47 //!
48 //!  * 256 byte string with the file path (relative to the library path)
49 //!  * 4 byte unsigned number with the offset of the file data in the library file
50 //!  * 4 byte unsigned number with the length of the file data in the library file
51 //!  * 1 byte unsigned number with the state of the entry (saw FILE_OK 0x00 and FILE_OLD 0x01)
52 //!  * 1 byte unsigned number with name ubReserved (not used)
53 //!  * 2 byte padding (4 byte alignment)
54 //!  * 8 byte FILETIME (not used, from windows, the number of 10^-7 seconds (100-nanosecond intervals) from 1 Jan 1601)
55 //!  * 2 byte unsigned number with name usReserved2 (not used)
56 //!  * 2 byte padding (4 byte alignment)
57 //!
58 
59 use std::io::ErrorKind::InvalidInput;
60 use std::io::{Cursor, Error, Read, Result, Seek, SeekFrom, Write};
61 use std::time::{Duration, SystemTime, UNIX_EPOCH};
62 
63 use byteorder::{ReadBytesExt, WriteBytesExt, LE};
64 
65 use crate::file_formats::{StracciatellaReadExt, StracciatellaWriteExt};
66 
67 /// Number of bytes of the header in the library file.
68 pub const HEADER_BYTES: u32 = 532;
69 
70 /// Number of bytes of an entry in the library file.
71 pub const ENTRY_BYTES: u32 = 280;
72 
73 /// Unix epoch is 1 Jan 1970.
74 /// FILETIME is the number of 10^-7 seconds (100-nanosecond intervals) from 1 Jan 1601.
75 pub const UNIX_EPOCH_AS_FILETIME: u64 = 116_444_736_000_000_000; // 100-nanoseconds
76 
77 /// Header of the archive.
78 /// The entries are at the end of the archive.
79 #[derive(Debug, Default, Eq, PartialEq)]
80 pub struct SlfHeader {
81     /// Name of the library.
82     ///
83     /// Usually it's the name of the library file in uppercase.
84     /// Nul terminated string of 256 bytes, unused bytes are zeroed, unknown encoding (saw ASCII).
85     pub library_name: String,
86 
87     /// Base path of the files in the library.
88     ///
89     /// Empty or terminated by '\\'.
90     /// Nul terminated string of 256 bytes, unused bytes are zeroed, unknown encoding (saw ASCII).
91     pub library_path: String,
92 
93     /// Number of entries that are available.
94     pub num_entries: i32,
95 
96     /// Number of entries that have state Ok and are used by the game.
97     pub ok_entries: i32,
98 
99     /// TODO 0xFFFF probably means the entries are sorted by file path first, and by state second (Old < Ok)
100     pub sort: u16,
101 
102     /// TODO 0x0200 probably means v2.0
103     pub version: u16,
104 
105     /// TODO 0 when there are 0 '\\' characters in library_path (0 '\\' characters in the file names either, do they count?)
106     ///      1 when there is 1 '\\' character in library_path (0-2 '\\' characters in the file names)
107     pub contains_subdirectories: u8,
108 }
109 
110 /// Entry of the archive.
111 #[derive(Debug, Default, Eq, PartialEq)]
112 pub struct SlfEntry {
113     /// Path of the file from the library path.
114     pub file_path: String,
115 
116     /// Start offset of the file data in the library.
117     pub offset: u32,
118 
119     /// Length of the file data in the library.
120     pub length: u32,
121 
122     /// State of the entry.
123     pub state: SlfEntryState,
124 
125     /// FILETIME, the number of 10^-7 seconds (100-nanosecond intervals) from 1 Jan 1601.
126     pub file_time: u64,
127 }
128 
129 /// State of an entry of the archive.
130 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
131 pub enum SlfEntryState {
132     /// Contains data and the data is up to date.
133     ///
134     /// Only entries with this state are used in the game.
135     Ok,
136 
137     /// The default state, this entry is empty.
138     ///
139     /// Not used in the game, probably used in datalib98 for empty entries.
140     Deleted,
141 
142     /// Contains data and the data is old.
143     ///
144     /// There should be an entry with the same path and state Ok next to this entry.
145     Old,
146 
147     /// Not used here or in the game, probably used in datalib98 during path searches.
148     DoesNotExist,
149 
150     // Unknown state.
151     Unknown(u8),
152 }
153 
154 impl SlfHeader {
155     /// Read the header from input.
156     #[allow(dead_code)]
from_input<T>(input: &mut T) -> Result<Self> where T: Read + Seek,157     pub fn from_input<T>(input: &mut T) -> Result<Self>
158     where
159         T: Read + Seek,
160     {
161         input.seek(SeekFrom::Start(0))?;
162 
163         let mut handle = input.take(u64::from(HEADER_BYTES));
164         let library_name = handle.read_fixed_string(256)?;
165         let library_path = handle.read_fixed_string(256)?;
166         let num_entries = handle.read_i32::<LE>()?;
167         let ok_entries = handle.read_i32::<LE>()?;
168         let sort = handle.read_u16::<LE>()?;
169         let version = handle.read_u16::<LE>()?;
170         let contains_subdirectories = handle.read_u8()?;
171         handle.read_unused(7)?;
172         assert_eq!(handle.limit(), 0);
173 
174         Ok(Self {
175             library_name,
176             library_path,
177             num_entries,
178             ok_entries,
179             sort,
180             version,
181             contains_subdirectories,
182         })
183     }
184 
185     /// Write this header to output.
186     #[allow(dead_code)]
to_output<T>(&self, output: &mut T) -> Result<()> where T: Write + Seek,187     pub fn to_output<T>(&self, output: &mut T) -> Result<()>
188     where
189         T: Write + Seek,
190     {
191         let mut buffer = Vec::with_capacity(HEADER_BYTES as usize);
192         let mut cursor = Cursor::new(&mut buffer);
193         cursor.write_fixed_string(256, &self.library_name)?;
194         cursor.write_fixed_string(256, &self.library_path)?;
195         cursor.write_i32::<LE>(self.num_entries)?;
196         cursor.write_i32::<LE>(self.ok_entries)?;
197         cursor.write_u16::<LE>(self.sort)?;
198         cursor.write_u16::<LE>(self.version)?;
199         cursor.write_u8(self.contains_subdirectories)?;
200         cursor.write_unused(7)?;
201         assert_eq!(buffer.len(), HEADER_BYTES as usize);
202 
203         output.seek(SeekFrom::Start(0))?;
204         output.write_all(&buffer)?;
205 
206         Ok(())
207     }
208 
209     /// Read the entries from the input.
210     #[allow(dead_code)]
entries_from_input<T>(&self, input: &mut T) -> Result<Vec<SlfEntry>> where T: Read + Seek,211     pub fn entries_from_input<T>(&self, input: &mut T) -> Result<Vec<SlfEntry>>
212     where
213         T: Read + Seek,
214     {
215         if self.num_entries <= 0 {
216             return Err(Error::new(
217                 InvalidInput,
218                 format!("unexpected number of entries {}", self.num_entries),
219             ));
220         }
221 
222         let num_entries = self.num_entries as u32;
223         let num_bytes = num_entries * ENTRY_BYTES;
224         input.seek(SeekFrom::End(-(i64::from(num_bytes))))?;
225 
226         let mut handle = input.take(u64::from(num_bytes));
227         let mut entries = Vec::new();
228         for _ in 0..num_entries {
229             let file_path = handle.read_fixed_string(256)?;
230             let offset = handle.read_u32::<LE>()?;
231             let length = handle.read_u32::<LE>()?;
232             let state: SlfEntryState = handle.read_u8()?.into();
233             handle.read_unused(3)?;
234             let file_time = handle.read_u64::<LE>()?;
235             handle.read_unused(4)?;
236 
237             entries.push(SlfEntry {
238                 file_path,
239                 offset,
240                 length,
241                 state,
242                 file_time,
243             });
244         }
245         assert_eq!(handle.limit(), 0);
246 
247         Ok(entries)
248     }
249 
250     /// Write the entries to output.
251     #[allow(dead_code)]
entries_to_output<T>(&self, output: &mut T, entries: &[SlfEntry]) -> Result<()> where T: Write + Seek,252     pub fn entries_to_output<T>(&self, output: &mut T, entries: &[SlfEntry]) -> Result<()>
253     where
254         T: Write + Seek,
255     {
256         if self.num_entries < 0 || self.num_entries as usize != entries.len() {
257             return Err(Error::new(
258                 InvalidInput,
259                 format!(
260                     "unexpected number of entries {} != {}",
261                     self.num_entries,
262                     entries.len()
263                 ),
264             ));
265         }
266 
267         let num_bytes = self.num_entries as u32 * ENTRY_BYTES;
268         let mut buffer = Vec::with_capacity(num_bytes as usize);
269         let mut cursor = Cursor::new(&mut buffer);
270         for entry in entries {
271             cursor.write_fixed_string(256, &entry.file_path)?;
272             cursor.write_u32::<LE>(entry.offset)?;
273             cursor.write_u32::<LE>(entry.length)?;
274             cursor.write_u8(entry.state.into())?;
275             cursor.write_unused(3)?;
276             cursor.write_u64::<LE>(entry.file_time)?;
277             cursor.write_unused(4)?;
278         }
279         assert_eq!(buffer.len(), num_bytes as usize);
280 
281         let mut end_of_data = HEADER_BYTES;
282         for entry in entries {
283             let end_of_entry = entry.offset + entry.length;
284             if end_of_data < end_of_entry {
285                 end_of_data = end_of_entry;
286             }
287         }
288 
289         match output.seek(SeekFrom::End(-(i64::from(num_bytes)))) {
290             Ok(position) if position >= u64::from(end_of_data) => {}
291             _ => {
292                 // will increase the size of output
293                 output.seek(SeekFrom::Start(u64::from(end_of_data)))?;
294             }
295         }
296         output.write_all(&buffer)?;
297 
298         Ok(())
299     }
300 }
301 
302 impl SlfEntry {
303     /// Convert the file time of the entry to system time.
304     #[allow(dead_code)]
to_system_time(&self) -> Option<SystemTime>305     pub fn to_system_time(&self) -> Option<SystemTime> {
306         if self.file_time < UNIX_EPOCH_AS_FILETIME {
307             let n = UNIX_EPOCH_AS_FILETIME - self.file_time; // 100-nanoseconds
308             let secs = Duration::from_secs(n / 10_000_000);
309             let nanos = Duration::from_nanos((n % 10_000_000) * 100);
310             UNIX_EPOCH
311                 .checked_sub(secs)
312                 .and_then(|x| x.checked_sub(nanos))
313         } else {
314             let n = self.file_time - UNIX_EPOCH_AS_FILETIME; // 100-nanoseconds
315             let secs = Duration::from_secs(n / 10_000_000);
316             let nanos = Duration::from_nanos((n % 10_000_000) * 100);
317             UNIX_EPOCH
318                 .checked_add(secs)
319                 .and_then(|x| x.checked_add(nanos))
320         }
321     }
322 
323     /// Read the entry data from the input.
324     #[allow(dead_code)]
data_from_input<T>(&self, input: &mut T) -> Result<Vec<u8>> where T: Read + Seek,325     pub fn data_from_input<T>(&self, input: &mut T) -> Result<Vec<u8>>
326     where
327         T: Read + Seek,
328     {
329         input.seek(SeekFrom::Start(u64::from(self.offset)))?;
330 
331         let mut data = vec![0u8; self.length as usize];
332         input.read_exact(&mut data)?;
333 
334         Ok(data)
335     }
336 
337     /// Write the entry data to output.
338     #[allow(dead_code)]
data_to_output<T>(&self, output: &mut T, data: &[u8]) -> Result<()> where T: Write + Seek,339     pub fn data_to_output<T>(&self, output: &mut T, data: &[u8]) -> Result<()>
340     where
341         T: Write + Seek,
342     {
343         if self.offset < HEADER_BYTES {
344             return Err(Error::new(
345                 InvalidInput,
346                 format!("unexpected data offset {}", self.offset),
347             ));
348         }
349         if self.length as usize != data.len() {
350             return Err(Error::new(
351                 InvalidInput,
352                 format!("unexpected data length {} != {}", self.length, data.len()),
353             ));
354         }
355 
356         output.seek(SeekFrom::Start(u64::from(self.offset)))?;
357         output.write_all(&data)?;
358 
359         Ok(())
360     }
361 }
362 
363 impl Default for SlfEntryState {
364     /// Default value of SlfEntryState
default() -> Self365     fn default() -> Self {
366         SlfEntryState::Deleted
367     }
368 }
369 
370 impl From<SlfEntryState> for u8 {
371     /// All states map to a u8 value.
from(state: SlfEntryState) -> Self372     fn from(state: SlfEntryState) -> Self {
373         match state {
374             SlfEntryState::Ok => 0x00,
375             SlfEntryState::Deleted => 0xFF,
376             SlfEntryState::Old => 0x01,
377             SlfEntryState::DoesNotExist => 0xFE,
378             SlfEntryState::Unknown(value) => value,
379         }
380     }
381 }
382 
383 impl From<u8> for SlfEntryState {
384     /// All u8 values map to a state.
from(value: u8) -> Self385     fn from(value: u8) -> Self {
386         match value {
387             0x00 => SlfEntryState::Ok,
388             0xFF => SlfEntryState::Deleted,
389             0x01 => SlfEntryState::Old,
390             0xFE => SlfEntryState::DoesNotExist,
391             value => SlfEntryState::Unknown(value),
392         }
393     }
394 }
395 
396 #[cfg(test)]
397 mod tests {
398     use std::fmt::Debug;
399     use std::io::Cursor;
400 
401     use crate::file_formats::slf::{
402         SlfEntry, SlfEntryState, SlfHeader, ENTRY_BYTES, HEADER_BYTES, UNIX_EPOCH_AS_FILETIME,
403     };
404 
405     #[inline]
assert_ok<OK, ERR: Debug>(result: Result<OK, ERR>) -> OK406     fn assert_ok<OK, ERR: Debug>(result: Result<OK, ERR>) -> OK {
407         assert!(result.is_ok());
408         result.unwrap()
409     }
410 
411     #[test]
write_and_read_in_memory()412     fn write_and_read_in_memory() {
413         let test_header = SlfHeader {
414             library_name: "test library".to_string(),
415             library_path: "libdir\\".to_string(),
416             num_entries: 1,
417             ok_entries: 1,
418             sort: 0xFFFF,
419             version: 0x0200,
420             contains_subdirectories: 1,
421         };
422         let test_data = "file contents\n".as_bytes().to_vec();
423         let test_data_len = test_data.len() as u32;
424         let test_entries = vec![SlfEntry {
425             file_path: "file.ext".to_string(),
426             offset: HEADER_BYTES,
427             length: test_data_len,
428             state: SlfEntryState::Ok,
429             file_time: UNIX_EPOCH_AS_FILETIME,
430         }];
431         let after_header_pos = u64::from(HEADER_BYTES);
432         let after_data_pos = after_header_pos + u64::from(test_data_len);
433         let after_entries_pos = after_data_pos + u64::from(ENTRY_BYTES);
434         let mut buf: Vec<u8> = Vec::new();
435         let mut f = Cursor::new(&mut buf);
436 
437         // write
438         assert_ok(test_header.to_output(&mut f));
439         assert_eq!(f.position(), after_header_pos);
440         for entry in &test_entries {
441             let after_entry_data_pos = u64::from(entry.offset + entry.length);
442             assert_ok(entry.data_to_output(&mut f, &test_data));
443             assert_eq!(f.position(), after_entry_data_pos);
444         }
445         assert_ok(test_header.entries_to_output(&mut f, &test_entries));
446         assert_eq!(f.position(), after_entries_pos);
447 
448         // read
449         let header = assert_ok(SlfHeader::from_input(&mut f));
450         assert_eq!(f.position(), after_header_pos);
451         assert_eq!(test_header, header);
452         let entries = assert_ok(header.entries_from_input(&mut f));
453         assert_eq!(f.position(), after_entries_pos);
454         assert_eq!(test_entries, entries);
455         for entry in &entries {
456             let after_entry_data_pos = u64::from(entry.offset + entry.length);
457             let data = assert_ok(entry.data_from_input(&mut f));
458             assert_eq!(f.position(), after_entry_data_pos);
459             assert_eq!(test_data, data);
460         }
461     }
462 }
463