esl 0.3.1

A library for reading, writing and processing ESM/ESP/ESS files.
Documentation
#![feature(ptr_offset_from)]
#![feature(type_alias_impl_trait)]
#![feature(stmt_expr_attributes)]
#![deny(warnings)]
#![recursion_limit="512"]

#[macro_use]
extern crate enum_derive;
#[macro_use]
extern crate enum_primitive_derive;
#[macro_use]
extern crate macro_attr;
#[macro_use]
extern crate bitflags;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate serde;
#[macro_use]
extern crate derivative;
#[macro_use]
extern crate nameof;

#[macro_use]
mod tag;

pub use crate::tag::*;

#[macro_use]
mod bitflags_ext;

mod field;

pub use crate::field::*;

mod record;

pub use crate::record::*;

pub mod read;

mod strings;

pub use crate::strings::*;

pub mod code;

mod serde_helpers;

#[cfg(test)]
mod tests {
    use crate::*;
    use crate::read::*;
    use crate::code::{self, CodePage};
    use byteorder::{WriteBytesExt, LittleEndian};
    use std::iter::Iterator;
    use std::str::FromStr;
    use std::mem::transmute;

    fn test_author() -> &'static [u8] {
        b"test author\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
    }
    
    fn test_description() -> Vec<u8> {
        let mut v = Vec::new();
        v.extend_from_slice(b"test description");
        v.extend_from_slice(b"\r\nAAA\r\n\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v.extend_from_slice(b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
        v
    }
    
    fn test_file_1_bytes() -> Vec<u8> {
        let mut v = Vec::new();
        v.extend_from_slice(b"TES3");
        v.write_u32::<LittleEndian>(346).unwrap();
        v.write_u64::<LittleEndian>(0).unwrap();
        v.extend_from_slice(b"HEDR");
        v.write_u32::<LittleEndian>(300).unwrap();
        v.write_u32::<LittleEndian>(7).unwrap();
        v.write_u32::<LittleEndian>(32).unwrap();
        v.extend_from_slice(test_author());
        v.extend_from_slice(&test_description());
        v.write_u32::<LittleEndian>(1).unwrap();
        v.extend_from_slice(b"MAST");
        v.write_u32::<LittleEndian>(14).unwrap();
        v.extend_from_slice(b"Mo__o_in_.esm\0");
        v.extend_from_slice(b"DATA");
        v.write_u32::<LittleEndian>(8).unwrap();
        v.write_u64::<LittleEndian>(137).unwrap();
        v.extend_from_slice(b"CLOH");
        v.write_u32::<LittleEndian>(29).unwrap();
        v.write_u64::<LittleEndian>(0).unwrap();
        v.extend_from_slice(b"NAMF");
        v.write_u32::<LittleEndian>(8).unwrap();
        v.extend_from_slice(b"namename");
        v.extend_from_slice(b"IDID");
        v.write_u32::<LittleEndian>(5).unwrap();
        v.extend_from_slice(b"idid\0");
        v
    }

    fn test_file1() -> Vec<Record> {
        vec![
            Record {
                tag: TES3,
                flags: RecordFlags::empty(),
                fields: vec![
                    (HEDR, Field::FileMetadata(FileMetadata {
                        version: 7,
                        file_type: FileType::ESS,
                        author: "test author".into(),
                        description: vec!["test description".into(), "AAA".into(), "".into()],
                        records: 1
                    })),
                    (MAST, Field::StringZ("Mo__o_in_.esm".into())),
                    (DATA, Field::I64(137))
                ]
            },
            Record {
                tag: Tag::from_str("CLOH").unwrap(),
                flags: RecordFlags::empty(),
                fields: vec![
                    (Tag::from_str("NAMF").unwrap(), Field::U8List(b"namename".iter().map(|&x| x).collect())),
                    (Tag::from_str("IDID").unwrap(), Field::U8List(b"idid\0".iter().map(|&x| x).collect())),
                ]
            }
        ]
    }
    
    #[test]
    fn read_file_1() {
        let mut records = &test_file_1_bytes()[..];
        let records = Records::new(CodePage::English, RecordReadMode::Strict, 0, &mut records);
        let records = records.map(|x| x.unwrap()).collect::<Vec<_>>();
        assert_eq!(records, test_file1());
    }

    #[test]
    fn serialize_file_1() {
        let bytes = code::serialize(&test_file1(), CodePage::Russian, true).unwrap();
        assert_eq!(bytes, test_file_1_bytes());
    }

    #[test]
    fn deserialize_file_1() {
        let mut records = &test_file_1_bytes()[..];
        let record1: Record = code::deserialize_from_slice(&mut records, CodePage::Russian, false).unwrap();
        let record2: Record = code::deserialize_from_slice(&mut records, CodePage::Russian, false).unwrap();
        assert!(records.is_empty());
        assert_eq!(vec![record1, record2], test_file1());
    }
  
    #[test]
    fn race_fnam() {
        let mut bytes = &[
            0x52, 0x41, 0x43, 0x45, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x4E, 0x41, 0x4D, 0x45, 0x09, 0x00, 0x00, 0x00, 0x52, 0x65, 0x64, 0x67, 0x75, 0x61, 0x72, 0x64,
            0x00, 0x46, 0x4E, 0x41, 0x4D, 0x08, 0x00, 0x00, 0x00, 0xD0, 0xE5, 0xE4, 0xE3, 0xE0, 0xF0, 0xE4,
            0x00
        ][..];
        let mut records = Records::new(CodePage::Russian, RecordReadMode::Strict, 0, &mut bytes);
        let record = records.next().unwrap().unwrap();
        assert_eq!(record.fields[1].1, Field::StringZ(StringZ::from("Редгард")));
        let yaml = serde_yaml::to_string(&record).unwrap();
        assert!(!yaml.contains("^"));
        assert!(!yaml.contains("\\u"));
    }
    
    #[test]
    fn cell() {
        let record = Record {
            tag: CELL,
            flags: RecordFlags::empty(),
            fields: vec![
                (NAME, Field::StringZ("".into())),
                (DATA, Field::Cell(Cell { flags:CellFlags::HAS_WATER, grid: Grid { x: 20, y: 4} })),
                (RGNN, Field::StringZ("Coast Region".into())),
                (FRMR, Field::I32(347140)),
                (NAME, Field::StringZ("crab".into())),
                (DATA, Field::Position(Position { x: 165898.0, y: 38710.484375, z: 198.8867950439453, x_rot: 0.0, y_rot: 0.0, z_rot: 0.0 }))
            ]
        };
        let bytes = code::serialize(&record, CodePage::English, false).unwrap();
        let read = {
            let mut bytes = &bytes[..];
            let mut records = Records::new(CodePage::Russian, RecordReadMode::Strict, 0, &mut bytes);
            let read = records.next().unwrap().unwrap();
            assert!(records.next().is_none());
            read
        };
        assert_eq!(record, read);
        let deserialized: Record = code::deserialize(&bytes, CodePage::Russian, false).unwrap();
        assert_eq!(record, deserialized);
    }

    #[test]
    fn effect_metadata() {
        let record = Record {
            tag: MGEF,
            flags: RecordFlags::empty(),
            fields: vec![(MEDT, Field::EffectMetadata(EffectMetadata {
                school: School::Destruction,
                base_cost: 100.0,
                flags: EffectFlags::SPELLMAKING | EffectFlags::LIGHT_NEGATIVE,
                color: Color { r: 1, g: 2, b: 255 },
                size_factor: 1.0,
                size_cap: 50.0,
                speed: 1.0
            }))]
        };
        let bytes = code::serialize(&record, CodePage::English, false).unwrap();
        let read = {
            let mut bytes = &bytes[..];
            let mut records = Records::new(CodePage::Russian, RecordReadMode::Strict, 0, &mut bytes);
            let read = records.next().unwrap().unwrap();
            assert!(records.next().is_none());
            read
        };
        assert_eq!(record, read);
        let deserialized: Record = code::deserialize(&bytes, CodePage::Russian, false).unwrap();
        assert_eq!(record, deserialized);
    }

    #[test]
    fn journal_zeros() {
        let record = Record {
            tag: JOUR,
            flags: RecordFlags::empty(),
            fields: vec![(NAME, Field::StringList(vec![
                "\0\0\0".into(),
            ]))]
        };
        let bytes = code::serialize(&record, CodePage::English, false).unwrap();
        let read = {
            let mut bytes = &bytes[..];
            let mut records = Records::new(CodePage::English, RecordReadMode::Strict, 0, &mut bytes);
            let read = records.next().unwrap().unwrap();
            assert!(records.next().is_none());
            read
        };
        assert_eq!(record, read);
        let deserialized: Record = code::deserialize(&bytes, CodePage::Russian, false).unwrap();
        assert_eq!(record, deserialized);
    }

    #[test]
    fn deseralize_float_field() {
        let yaml = "\
- GMST:
    - FLTV: 3.0
    - FLTV: .nan
    - FLTV: nanFFEEEEEE
    - FLTV: 0.1
    - FLTV: -0.0
        ";
        let res: Vec<Record> = serde_yaml::from_str(yaml).unwrap();
        assert_eq!(res.len(), 1);
        assert_eq!(res[0].fields.len(), 5);
        assert_eq!(res[0].fields[0].1, Field::F32(3.0));
        let standard_nan: f32 = unsafe { transmute(0xFFFFFFFFu32) };
        let custom_nan: f32 = unsafe { transmute(0xFFEEEEEEu32) };
        assert!(standard_nan.is_nan());
        assert!(custom_nan.is_nan());
        assert_ne!(Field::F32(standard_nan), Field::F32(custom_nan));
        assert_eq!(res[0].fields[1].1, Field::F32(standard_nan));
        assert_eq!(res[0].fields[2].1, Field::F32(custom_nan));
        assert_eq!(res[0].fields[3].1, Field::F32(0.1));
        assert_eq!(res[0].fields[4].1, Field::F32(0.0_f32.copysign(-1.0)));
        let res_yaml = serde_yaml::to_string(&res).unwrap();
        assert_eq!(res_yaml, format!("---\n{}", yaml).trim());
    }

    #[test]
    fn deserialize_light_field() {
        let yaml = "\
- LIGH:
    - LHDT:
        weight: 0.1
        value: 3
        time: 0
        radius: 128
        color: \"#F58C28\"
        flags: DYNAMIC CAN_CARRY FIRE FLICKER_SLOW
        ";
        let res: Vec<Record> = serde_yaml::from_str(yaml).unwrap();
        assert_eq!(res.len(), 1);
        assert_eq!(res[0].fields.len(), 1);
        if let Field::Light(field) = &res[0].fields[0].1 {
            assert_eq!(field.weight, 0.1);
        } else {
            panic!()
        }
        let res_yaml = serde_yaml::to_string(&res).unwrap();
        assert_eq!(res_yaml, format!("---\n{}", yaml).trim());
    }
}