espers 0.1.0

Library for reading ESP and ESM files
Documentation
use crate::error::Error;
use binrw::{binrw, BinRead};
use encoding_rs::WINDOWS_1252;
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Seek};
use std::path::PathBuf;
use std::str::from_utf8;

#[binrw]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[brw(little)]
pub struct DirectoryEntry {
    pub string_id: u32,
    pub offset: u32,
}

#[binrw]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[brw(little)]
pub struct RawStringTable {
    pub num_entries: u32,
    pub string_data_size: u32,

    #[br(count = num_entries)]
    pub entries: Vec<DirectoryEntry>,

    #[br(count = string_data_size)]
    pub data: Vec<u8>,
}

impl RawStringTable {
    pub fn parse<T: Read + Seek>(reader: &mut T) -> Result<Self, Error> {
        Ok(Self::read(reader)?)
    }
}

pub struct StringTable {
    pub offsets: HashMap<u32, u32>,
    pub data: Vec<u8>,
}

impl From<RawStringTable> for StringTable {
    fn from(raw: RawStringTable) -> Self {
        Self {
            offsets: raw
                .entries
                .into_iter()
                .map(|de| (de.string_id, de.offset))
                .collect(),
            data: raw.data,
        }
    }
}

impl StringTable {
    pub fn get_str(&self, id: &u32) -> Result<Option<String>, Error> {
        let start = match self.offsets.get(id) {
            Some(&v) => v as usize,
            None => return Ok(None),
        };
        let end = self.data[start..]
            .iter()
            .position(|&c| c == b'\0')
            .ok_or(Error::StringEOF)?;

        let bytes = &self.data[start..start + end];

        let result: Result<String, Error> = match from_utf8(bytes) {
            Ok(s) => Ok(s.into()),
            Err(_) => match WINDOWS_1252.decode(bytes) {
                (s, _, _) => Ok(s.into()),
            },
        };
        Ok(Some(result?))
    }

    pub fn get_slice(&self, id: &u32) -> Result<Option<&[u8]>, Error> {
        let start = match self.offsets.get(id) {
            Some(&v) => v as usize,
            None => return Ok(None),
        };
        let end = self.data[start..]
            .iter()
            .position(|&c| c == b'\0')
            .ok_or(Error::StringEOF)?;

        Ok(Some(&self.data[start..start + end]))
    }
}

pub struct StringTables {
    string_tables: Vec<StringTable>,
}

impl StringTables {
    pub fn load(path: &str, language: &str) -> Result<Self, Error> {
        let path = PathBuf::from(path);
        let plugin_name = path.file_stem().unwrap().to_string_lossy();
        let dir = path.parent().unwrap().join("Strings");

        let mut string_tables = Vec::new();

        for suffix in ["STRINGS", "DLSTRINGS", "ILSTRINGS"] {
            let y = dir.join(format!("{}_{}.{}", plugin_name, language, suffix));

            if let Ok(mut f) = File::open(y) {
                if let Ok(rst) = RawStringTable::parse(&mut f) {
                    string_tables.push(rst.into())
                }
            }
        }

        Ok(Self { string_tables })
    }

    pub fn get_string(&self, id: &u32) -> Result<Option<String>, Error> {
        for st in &self.string_tables {
            if let Some(s) = st.get_str(id)? {
                return Ok(Some(s));
            }
        }

        Ok(None)
    }

    pub fn get_slice(&self, id: &u32) -> Result<Option<&[u8]>, Error> {
        for st in &self.string_tables {
            if let Some(s) = st.get_slice(id)? {
                return Ok(Some(s));
            }
        }

        Ok(None)
    }
}