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