uo-rust-libs 0.1.0

Data readers for various Ultima Online client mul files
Documentation
//! Methods for reading from standardized Mul and Idx files
//!
//! IDX files are defined as `|index:u32|size:u32|opt1:u16|opt2:u16|`
//!
//! Where index and size represent references into the equivalent Mul file
//!
//! Index values of `0xFEFEFEFF` and `0xFFFFFFFF` are considered undefined, and should be skipped

use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;

use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};

use crate::error::{MulReaderError, MulReaderResult, MulWriterResult};

const UNDEF_RECORD: u32 = 0xFEFEFEFF;
const INDEX_SIZE: u32 = 12;

/// An individual record, read from a Mul file
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct MulRecord {
    ///Raw Mul data
    pub data: Vec<u8>,
    ///The index position in the Mul of this item
    pub start: u32,
    ///The total length in the Mul of this item
    pub length: u32,
    ///An implementation-specific variable
    pub opt1: u16,
    ///An implementation-specific variable
    pub opt2: u16,
}

///Read Mul records out of an idx and a mul
#[derive(Debug)]
pub struct MulReader<T: Read + Seek> {
    idx_reader: T,
    data_reader: T,
}

impl MulReader<File> {
    /// Create a new mul reader from an index and mul path
    pub fn new(idx_path: &Path, mul_path: &Path) -> MulReaderResult<MulReader<File>> {
        let idx_reader = File::open(idx_path)?;
        let data_reader = File::open(mul_path)?;

        Ok(MulReader {
            idx_reader,
            data_reader,
        })
    }
}

impl<T: Read + Seek> MulReader<T> {
    /// Create a new mul reader from existing index and mul readers
    pub fn from_readables(idx_reader: T, data_reader: T) -> MulReader<T> {
        MulReader {
            idx_reader,
            data_reader,
        }
    }

    /// Read a specific entry from the Mul.
    ///
    /// This method will return OffsetOutOfBounds if the index is marked invalid.
    pub fn read(&mut self, index: u32) -> MulReaderResult<MulRecord> {
        //Wind the idx reader to the index position
        self.idx_reader
            .seek(SeekFrom::Start((index * INDEX_SIZE) as u64))?;

        let start = self.idx_reader.read_u32::<LittleEndian>()?;
        //Check for empty cell
        if start == UNDEF_RECORD || start == u32::MAX {
            return Err(MulReaderError::OffsetOutOfBounds {
                index,
                offset: start,
            });
        }

        let length = self.idx_reader.read_u32::<LittleEndian>()?;
        let mut data = vec![0; length as usize];
        let opt1 = self.idx_reader.read_u16::<LittleEndian>()?;
        let opt2 = self.idx_reader.read_u16::<LittleEndian>()?;

        self.data_reader.seek(SeekFrom::Start(start as u64))?;
        self.data_reader.read_exact(data.as_mut_slice())?;

        Ok(MulRecord {
            data,
            start,
            length,
            opt1,
            opt2,
        })
    }
}

///Write new records onto existing Mul and Idx files
#[derive(Debug)]
pub struct MulWriter<T: Write + Seek> {
    idx_writer: T,
    data_writer: T,
}

pub enum MulWriterMode {
    Append,
    Truncate,
}

impl MulWriter<File> {
    /// Create a new mul writer from an index and mul path
    pub fn new(
        idx_path: &Path,
        mul_path: &Path,
        mode: MulWriterMode,
    ) -> MulWriterResult<MulWriter<File>> {
        let mut options = OpenOptions::new();
        let options = options.write(true).create(true).truncate(match mode {
            MulWriterMode::Append => false,
            MulWriterMode::Truncate => true,
        });

        let idx_writer = options.open(idx_path)?;
        let data_writer = options.open(mul_path)?;

        Ok(MulWriter {
            idx_writer,
            data_writer,
        })
    }
}

impl<T: Write + Seek> MulWriter<T> {
    /// Create a new mul writer from an index and mul path
    pub fn from_writables(idx_writer: T, data_writer: T) -> MulWriter<T> {
        MulWriter {
            idx_writer,
            data_writer,
        }
    }
    /// Append a new value to the mul files
    pub fn append(
        &mut self,
        data: &[u8],
        opt1: Option<u16>,
        opt2: Option<u16>,
    ) -> MulWriterResult<()> {
        //Wind the files to the end
        self.idx_writer.seek(SeekFrom::End(0))?;
        let mul_size = self.data_writer.seek(SeekFrom::End(0))?;

        //Fill up our fields
        let start = mul_size as u32;
        let length = data.len() as u32;
        self.data_writer.write_all(data)?;
        self.idx_writer.write_u32::<LittleEndian>(start)?;
        self.idx_writer.write_u32::<LittleEndian>(length)?;
        self.idx_writer
            .write_u16::<LittleEndian>(opt1.unwrap_or_default())?;
        self.idx_writer
            .write_u16::<LittleEndian>(opt2.unwrap_or_default())?;

        Ok(())
    }
}

#[cfg(test)]
pub mod tests {
    use super::*;
    use std::io::Cursor;

    pub fn simple_from_vecs(vectors: Vec<(Vec<u8>, u16, u16)>) -> MulReader<Cursor<Vec<u8>>> {
        let mut idx_reader = Cursor::new(vec![]);
        let mut mul_reader = Cursor::new(vec![]);
        //For every MUL record, we should have an index record pointing at it
        for (vec, opt1, opt2) in vectors {
            let len = vec.len();
            let mul_size = mul_reader.seek(SeekFrom::End(0)).unwrap();
            let mut idx_cursor = Cursor::new(vec![]);
            idx_cursor
                .write_u32::<LittleEndian>(mul_size as u32)
                .unwrap(); //Position
            idx_cursor.write_u32::<LittleEndian>(len as u32).unwrap(); //Length
            idx_cursor.write_u16::<LittleEndian>(opt1).unwrap(); //Opt1
            idx_cursor.write_u16::<LittleEndian>(opt2).unwrap(); //Opt2
            idx_reader.write_all(idx_cursor.get_ref()).unwrap();
            mul_reader.write_all(&vec).unwrap();
        }
        MulReader::from_readables(idx_reader, mul_reader)
    }

    pub fn simple_from_mul_records(records: Vec<MulRecord>) -> MulReader<Cursor<Vec<u8>>> {
        let mut idx_reader = Cursor::new(vec![]);
        let mut mul_reader = Cursor::new(vec![]);
        //For every MUL record, we should have an index record pointing at it
        for record in records {
            let mul_size = mul_reader.seek(SeekFrom::End(0)).unwrap();
            let mut idx_cursor = Cursor::new(vec![]);
            idx_cursor
                .write_u32::<LittleEndian>(mul_size as u32)
                .unwrap(); //Position
            idx_cursor
                .write_u32::<LittleEndian>(record.data.len() as u32)
                .unwrap(); //Length
            idx_cursor.write_u16::<LittleEndian>(record.opt1).unwrap(); //Opt1
            idx_cursor.write_u16::<LittleEndian>(record.opt2).unwrap(); //Opt2
            idx_reader.write_all(idx_cursor.get_ref()).unwrap();
            mul_reader.write_all(&record.data).unwrap();
        }
        MulReader::from_readables(idx_reader, mul_reader)
    }
}