summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJakob L. Kreuze <jakob@memeware.net>2018-05-23 22:01:48 -0400
committerJakob L. Kreuze <jakob@memeware.net>2018-05-23 22:01:48 -0400
commit8f109356234421cbf22c14976be363ef3d08c4ee (patch)
tree67d315aa92ef77349eb14178d5bdee68ffa4f082 /src
parent7802dcd0df8a6f0e88bbfb8f9a06999be2afc919 (diff)
Finalized GRP loader - Added path resolution, tests, and a cleaner API.
Diffstat (limited to 'src')
-rw-r--r--src/fmt.rs190
-rw-r--r--src/main.rs2
2 files changed, 188 insertions, 4 deletions
diff --git a/src/fmt.rs b/src/fmt.rs
index 2ce342e..5b38f85 100644
--- a/src/fmt.rs
+++ b/src/fmt.rs
@@ -2,12 +2,57 @@ extern crate byteorder;
extern crate simple_error;
use std::collections::HashMap;
+use std::env;
use std::error::Error;
use std::fs::File;
use std::io::Read;
+use std::path::Path;
use self::byteorder::{ByteOrder, LittleEndian};
+
+// FIXME: This doesn't follow any non-*NIX conventions.
+
+/// Means of locating a group file that may be located in any number of file
+/// system locations. Begins by searching the current working directory, and
+/// then moves onto standard *NIX home directory locations, such as
+/// '~/.rebuild'.
+
+fn find_file(filename: &str) -> Option<String> {
+ // Initial base paths that don't need to be expanded.
+ let directories = vec!["./"];
+
+ let mut directories: Vec<String> = directories.iter()
+ .map(|s| String::from(*s))
+ .collect();
+
+ match env::home_dir() {
+ Some(path) => {
+ match path.to_str() {
+ Some(path) => {
+ let mut path = String::from(path);
+ path.push_str("/.rebuild/");
+ directories.push(path);
+ }
+ None => (),
+ }
+ }
+ None => (),
+ }
+
+ for root in directories.iter() {
+ let mut path = root.clone();
+ path.push_str(filename);
+
+ if Path::new(&path).exists() {
+ return Some(path);
+ }
+ }
+
+ None
+}
+
+
// 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
@@ -16,6 +61,11 @@ use self::byteorder::{ByteOrder, LittleEndian};
// 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 various
+/// group files are loaded. This is only somewhat reminiscent of the way
+/// Silverman's original code goes about loading game data.
+
#[derive(Debug)]
pub struct GroupManager {
files: HashMap<String, Vec<u8>>,
@@ -26,7 +76,8 @@ impl GroupManager {
GroupManager { files: HashMap::new() }
}
- pub fn load_from_slice(&mut self, data: &[u8]) -> Result<(), Box<Error>> {
+ /// Loads the contents of an in-memory group file into the cache.
+ pub fn load_data(&mut self, data: &[u8]) -> Result<(), Box<Error>> {
let len = data.len();
if len < 16 {
@@ -76,22 +127,155 @@ impl GroupManager {
Ok(())
}
- pub fn load_from_file(&mut self, filename: &str) -> Result<(), Box<Error>> {
+ /// Loads the contents of an on-disk group file into the cache.
+ pub fn load_file(&mut self, filename: &str) -> Result<(), Box<Error>> {
+ let filename = match find_file(filename) {
+ Some(filename) => filename,
+ None => bail!("File not found in any search paths."),
+ };
+
let mut file = File::open(filename)?;
let mut bytes: Vec<u8> = Vec::new();
file.read_to_end(&mut bytes)?;
- self.load_from_slice(&bytes)?;
+ self.load_data(&bytes)?;
Ok(())
}
+ /// 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 mut group_manager = GroupManager::new();
+
+ match group_manager.load_data(&data) {
+ Err(e) => panic!("{}", e),
+ Ok(_) => (),
+ }
+
+ 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 mut group_manager = GroupManager::new();
+
+ match group_manager.load_data(&data) {
+ Ok(_) => panic!("Accepted incomplete header."),
+ Err(_) => (),
+ }
+ }
+
+ #[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 mut group_manager = GroupManager::new();
+
+ match group_manager.load_data(&data) {
+ Ok(_) => panic!("Accepted invalid header."),
+ Err(_) => (),
+ }
+ }
+
+ #[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 mut group_manager = GroupManager::new();
+
+ match group_manager.load_data(&data) {
+ Ok(_) => panic!("Accepted invalid header."),
+ Err(_) => (),
+ }
+ }
+
+ // TODO: Add checks for data_off and table_off going out of bounds.
+}
+
+
// What's the .MAP / .ART file format?
//
// Go to my Build Source Code Page and download BUILDSRC.ZIP. I have a text file
diff --git a/src/main.rs b/src/main.rs
index 7bfdd82..95925e0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -38,7 +38,7 @@ fn main() {
let filename = "DUKE3D.GRP";
let mut group_manager = fmt::GroupManager::new();
- if let Err(e) = group_manager.load_from_file(filename) {
+ if let Err(e) = group_manager.load_file(filename) {
println!("Couldn't open {}: {}", filename, e);
process::exit(1);
}