// Copyright (C) 2018 Jakob L. Kreuze, All Rights Reserved.
//
// This file is part of rebuild.
//
// rebuild is free software: you can redistribute it and/or modify it under the
// terms of the GNU General Public License as published by the Free Software
// Foundation, either version 3 of the License, or (at your option) any later
// version.
//
// rebuild is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// rebuild. If not, see .
extern crate byteorder;
extern crate simple_error;
use std::collections::HashMap;
use std::error::Error;
use std::fs::File;
use std::io::Read;
use self::byteorder::{ByteOrder, LE};
use path::PathManager;
// What's the .GRP file format?
//
// The ".grp" file format is just a collection of a lot of files stored into 1 big
// one. I tried to make the format as simple as possible: The first 12 bytes
// contains my name, "KenSilverman". The next 4 bytes is the number of files that
// were compacted into the group file. Then for each file, there is a 16 byte
// structure, where the first 12 bytes are the filename, and the last 4 bytes are
// the file's size. The rest of the group file is just the raw data packed one
// after the other in the same order as the list of files.
/// Implementation of a group file "cache", into which the contents of several
/// group files can be loaded. This is somewhat similar to the way that
/// Silverman's original code goes about loading game data.
#[derive(Debug)]
pub struct GroupManager {
paths: PathManager,
files: HashMap>,
}
impl GroupManager {
/// Creates a new GroupManager that uses the given PathManager to resolve
/// the locations of named GRP files.
pub fn new(paths: PathManager) -> GroupManager {
GroupManager { paths, files: HashMap::new() }
}
/// Loads the contents of an in-memory group file into the cache.
///
/// # Errors
///
/// The operation will fail on any sort of parsing error - such as an
/// invalid header, or sizes that would cause an invalid read.
pub fn load_data(&mut self, data: &[u8]) -> Result<(), Box> {
let len = data.len();
if len < 16 {
bail!("'data' is too small to contain the GRP header.");
}
let header = String::from_utf8(data[..12].to_vec())?;
if header.as_str() != "KenSilverman" {
bail!("Invalid GRP header.");
}
let file_count = LE::read_u32(&data[12..16]) as usize;
// 16 bytes for the header, and 16 bytes for each table entry. The raw
// data will follow.
let data_start = 16 * (file_count + 1) as usize;
if data_start >= len {
bail!(format!("Invalid number of files. (given: {})", file_count));
}
let mut data_off = data_start;
for i in 0..file_count {
// Similar to how 'data_start' was calculated - 16 bytes for the
// header, and 16 bytes for each table entry.
let table_off = 16 * (i + 1);
let name = &data[table_off..table_off+12];
let name = String::from_utf8(name.to_vec())?;
let name = if let Some(j) = name.find('\x00') {
String::from(&name[..j])
} else {
name
};
let size = &data[table_off+12..table_off+16];
let size = LE::read_u32(size) as usize;
if data_off + size > len {
bail!("`data_off >= len` - Table was likely corrupted.");
}
let data = data[data_off..data_off+size].to_vec();
data_off += size;
self.files.insert(name, data);
}
Ok(())
}
/// Queries the associated PathManager for the location of a file with the
/// given name, and if found, loads its contents.
///
/// # Errors
///
/// A return value of 'Err' indicates that the given path did not exist.
pub fn load_file(&mut self, name: &str) -> Result<(), Box> {
if let Some(path) = self.paths.find(name) {
let mut file = File::open(path)?;
let mut bytes: Vec = Vec::new();
file.read_to_end(&mut bytes)?;
self.load_data(&bytes)?;
return Ok(());
}
bail!("File not found in any search paths.")
}
/// Obtains binary data associated with the given filename from the cache.
pub fn get(&self, filename: &str) -> Option<&[u8]> {
Some(&self.files.get(filename)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_slice() {
// Binary blob containing a GRP test vector, made by me. Contains the
// "KenSilverman" header, and a table consisting of 3 files:
//
// - 'TESTFILEA': A single byte, 0x01.
// - 'TESTFILEB': 0x02, repeated twice.
// - 'TESTFILEC': 0x03, repeated three times.
//
// The sizes listed in the table accurately represent this.
let data = vec![
b'K', b'e', b'n', b'S', b'i', b'l', b'v', b'e',
b'r', b'm', b'a', b'n', 0x03, 0x00, 0x00, 0x00,
b'T', b'E', b'S', b'T', b'F', b'I', b'L', b'E',
b'A', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
b'T', b'E', b'S', b'T', b'F', b'I', b'L', b'E',
b'B', 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
b'T', b'E', b'S', b'T', b'F', b'I', b'L', b'E',
b'C', 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
0x01, 0x02, 0x02, 0x03, 0x03, 0x03,
];
let paths = PathManager::new();
let mut group_manager = GroupManager::new(paths);
if let Err(e) = group_manager.load_data(&data) {
panic!("{}", e);
}
let data = match group_manager.get("TESTFILEA") {
Some(data) => data,
None => panic!("TESTFILEA wasn't found in the archive"),
};
assert_eq!(data.len(), 1);
assert_eq!(data[0], 0x01);
let data = match group_manager.get("TESTFILEB") {
Some(data) => data,
None => panic!("TESTFILEB wasn't found in the archive"),
};
assert_eq!(data.len(), 2);
assert_eq!(data[0], 0x02);
assert_eq!(data[1], 0x02);
let data = match group_manager.get("TESTFILEC") {
Some(data) => data,
None => panic!("TESTFILEC wasn't found in the archive"),
};
assert_eq!(data.len(), 3);
assert_eq!(data[0], 0x03);
assert_eq!(data[1], 0x03);
assert_eq!(data[2], 0x03);
}
#[test]
fn test_incomplete_header() {
// Binary blob similar to the GRP test vector above, but with a header
// that would be too small to be valid.
let data = vec![
b'J', b'a', b'k', b'o', b'b',
];
let paths = PathManager::new();
let mut group_manager = GroupManager::new(paths);
if let Ok(_) = group_manager.load_data(&data) {
panic!("Accepted invalid header.");
}
}
#[test]
fn test_invalid_header() {
// Binary blob similar to the GRP test vector above, but with an invalid
// "magic" header.
let data = vec![
b'J', b'a', b'k', b'o', b'b', b'L', b'K', b'r',
b'e', b'u', b'z', b'e', 0x01, 0x00, 0x00, 0x00,
b'T', b'E', b'S', b'T', b'F', b'I', b'L', b'E',
b'A', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x01,
];
let paths = PathManager::new();
let mut group_manager = GroupManager::new(paths);
if let Ok(_) = group_manager.load_data(&data) {
panic!("Accepted invalid header.");
}
}
#[test]
fn test_invalid_file_count() {
// Binary blob similar to the GRP test vector above, but with a header
// indicating that there are more files than could possibly be contained
// in the table.
let data = vec![
b'K', b'e', b'n', b'S', b'i', b'l', b'v', b'e',
b'r', b'm', b'a', b'n', 0x69, 0x00, 0x00, 0x00,
b'T', b'E', b'S', b'T', b'F', b'I', b'L', b'E',
b'A', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x01,
];
let paths = PathManager::new();
let mut group_manager = GroupManager::new(paths);
if let Ok(_) = group_manager.load_data(&data) {
panic!("Accepted invalid header.");
}
}
#[test]
fn test_not_enough_data() {
// Binary blob similar to the GRP test vector above, but with a file
// entry larger than the data following the table.
let data = vec![
b'K', b'e', b'n', b'S', b'i', b'l', b'v', b'e',
b'r', b'm', b'a', b'n', 0x69, 0x00, 0x00, 0x00,
b'T', b'E', b'S', b'T', b'F', b'I', b'L', b'E',
b'A', 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0x01,
];
let paths = PathManager::new();
let mut group_manager = GroupManager::new(paths);
if let Ok(_) = group_manager.load_data(&data) {
panic!("Accepted invalid header.");
}
}
}