s2protocol 3.5.5

A parser for Starcraft II - Replay format, exports to different target formats
Documentation
//! Parsing the DocumentHeader.
//!
//! There are a few fields that are related to MapInfo/Player01 name being Player\ 1
//! these are ignored.
//!

use crate::{S2ProtoResult, S2ProtocolError, dbg_peek_hex};
use nom::bytes::complete::*;
use nom::number::complete::*;
use nom_mpq::MPQ;
use nom_mpq::parser::peek_hex;
use tracing::instrument;

#[derive(Default, Debug, Clone)]
pub struct DocumentHeader {
    pub maybe_dimension_x1: i32,
    pub maybe_dimension_y1: i32,
    // TODO: these are not epochs, probably more like units that are 1000th the value of the
    // maybe_dimension_x1
    pub some_epoch_1: i32,
    pub some_epoch_2: i32,
    pub mod_info: String,
    // Aka the map title.
    pub name: String,
    /// A long description of the map.
    pub description_long: String,
    /// A short description of the map, in the few files I've checked it's empty.
    pub description_short: String,
}

impl DocumentHeader {
    /// Sets a field in the struct being read by the DocumentHeader file.
    pub fn set_field_by_name(&mut self, name: &str, value: String) {
        // Player00: Neutral
        // Player01: Player 1
        // Player02: Player 2
        // Player03: Hostile
        match name {
            "DocInfo/Name" => self.name = value,
            "DocInfo/DescLong" => self.description_long = value,
            "DocInfo/DescShort" => self.description_short = value,
            "MapInfo/Player00/Name"
            | "MapInfo/Player01/Name"
            | "MapInfo/Player02/Name"
            | "MapInfo/Player03/Name"
            | "MapInfo/Player04/Name"
            | "MapInfo/Player05/Name"
            | "MapInfo/Player06/Name"
            | "MapInfo/Player07/Name"
            | "MapInfo/Player08/Name"
            | "MapInfo/Team00/Name"
            | "MapInfo/Team01/Name"
            | "MapInfo/Team02/Name"
            | "MapInfo/Team03/Name"
            // These values are seen on Heart Of The Swarm it seems, may be related to GameHeart.
            // Thus far the information I am looking for appears in the Legacy of the Void and the
            // structs are easy to parse, I hope I don't have to try to understand the Swarm one,
            // seems more complex:
            | "DocInfo/Screenshot01"
            | "DocInfo/Screenshot02"
            | "DocInfo/Screenshot03"
            | "DocInfo/HowToPlayBasic00"
            | "DocInfo/HowToPlayBasic01"
            | "DocInfo/HowToPlayBasic02"
            | "DocInfo/HowToPlayAdvanced00"
            | "DocInfo/HowToPlayAdvanced01"
            | "DocInfo/HowToPlayAdvanced02" => tracing::debug!("Ignoring {name}"),
            _ => {
                tracing::warn!("Unknown field name {name}")
            }
        }
    }

    #[instrument(level = "debug", skip(mpq, file_contents))]
    pub fn from_mpq(mpq: &MPQ, file_contents: &[u8]) -> Result<Self, S2ProtocolError> {
        let (_, document_header_sector) =
            mpq.read_mpq_file_sector("DocumentHeader", false, file_contents)?;
        let (_, document_header) = Self::parse(&document_header_sector)?;
        Ok(document_header)
    }

    #[tracing::instrument(level = "debug", skip(input), fields(input = peek_hex(input)))]
    pub fn parse(input: &[u8]) -> S2ProtoResult<&[u8], Self> {
        let mut res = Self::default();
        let (tail, _) = dbg_peek_hex(tag(&b"H2CS"[..]), "read file magic, H2CS bytes")(input)?;

        let (tail, maybe_file_version_bytes) =
            dbg_peek_hex(take(4usize), "read maybe_file_version, 4 bytes")(tail)?;
        let (_, maybe_file_version) =
            i32(nom::number::Endianness::Little)(maybe_file_version_bytes)?;

        tracing::debug!(
            "maybe_file_version '{maybe_file_version_bytes:?}' '{:?}' tail is {}",
            maybe_file_version,
            peek_hex(tail)
        );

        let (tail, maybe_map_compatibility_bytes) =
            dbg_peek_hex(take(4usize), "read maybe_map_compatibility, 4 bytes")(tail)?;
        let maybe_compatibility =
            String::from_utf8_lossy(maybe_map_compatibility_bytes).to_string();

        tracing::debug!(
            "maybe_compatibility '{maybe_map_compatibility_bytes:?}' '{}', tail is {}",
            maybe_compatibility,
            peek_hex(tail)
        );

        let (tail, _unknown_bytes) =
            dbg_peek_hex(take(4usize), "read _unknown_bytes after S2.., 4 bytes")(tail)?;

        let (tail, maybe_i32_bytes) =
            dbg_peek_hex(take(4usize), "read maybe_i32_bytes, 4 bytes")(tail)?;
        let (_, maybe_dimension_x1) = i32(nom::number::Endianness::Little)(maybe_i32_bytes)?;
        res.maybe_dimension_x1 = maybe_dimension_x1;

        let (tail, maybe_i32_bytes) =
            dbg_peek_hex(take(4usize), "read maybe_i32_bytes, 4 bytes")(tail)?;
        let (_, maybe_dimension_y1) = i32(nom::number::Endianness::Little)(maybe_i32_bytes)?;
        res.maybe_dimension_y1 = maybe_dimension_y1;

        let (tail, _padding_bytes) = dbg_peek_hex(
            take(4usize),
            "read _padding_bytes after maybe dimensions1, 4 bytes",
        )(tail)?;

        let (tail, maybe_i32_bytes) =
            dbg_peek_hex(take(4usize), "read maybe_i32_bytes, 4 bytes")(tail)?;
        let (_, some_epoch_1) = i32(nom::number::Endianness::Little)(maybe_i32_bytes)?;
        res.some_epoch_1 = some_epoch_1;

        let (tail, maybe_i32_bytes) =
            dbg_peek_hex(take(4usize), "read maybe_i32_bytes, 4 bytes")(tail)?;
        let (_, some_epoch_2) = i32(nom::number::Endianness::Little)(maybe_i32_bytes)?;
        res.some_epoch_2 = some_epoch_2;

        // 0200 0000 0000 0000 0100 0000 follows, no idea what these are...
        let (tail, _padding_bytes) = dbg_peek_hex(
            take(12usize),
            "read _padding_bytes after maybe dimensions2, 12 bytes: ",
        )(tail)?;

        let (tail, string_bytes) = dbg_peek_hex(take_while(|x| x != 0u8), "read mod string")(tail)?;
        res.mod_info = String::from_utf8_lossy(string_bytes).to_string();
        if res.mod_info != "bnet:Void (Mod)/0.0/999,file:Mods/Void.SC2Mod" {
            return Err(S2ProtocolError::Map(crate::cache_handles::MapError::Other(
                "Only parsed Void typed.".to_string(),
            )));
        }

        tracing::debug!("Got mod_info: {}", res.mod_info);

        let (tail, _past_the_zero_delim) = dbg_peek_hex(
            take(1usize),
            "read _past_the_zero_delim on mod_info, 1 bytes",
        )(tail)?;

        let (tail, docinfo_field_count_bytes) =
            dbg_peek_hex(take(2usize), "read doc_info field count, 2 bytes")(tail)?;
        let (_, docinfo_field_count) =
            u16(nom::number::Endianness::Little)(docinfo_field_count_bytes)?;
        tracing::debug!(
            "field count field count bytes: {:?} tail is {}",
            docinfo_field_count_bytes,
            peek_hex(tail)
        );

        let (mut new_tail, _) =
            dbg_peek_hex(tag(&[0, 0][..]), "doc_info element count delimiter.")(tail)?;

        tracing::debug!("Expect to read {} fields", docinfo_field_count);
        for field_num in 0..docinfo_field_count {
            let (tail, next_str_size_bytes) =
                dbg_peek_hex(take(2usize), "read string size, 2 bytes")(new_tail)?;
            let (_, next_str_size) = u16(nom::number::Endianness::Little)(next_str_size_bytes)?;

            tracing::debug!(
                "{field_num}: field NAME string size {}, tail is {}",
                next_str_size,
                peek_hex(new_tail)
            );

            let (tail, docinfo_field_name_bytes) =
                dbg_peek_hex(take(next_str_size), "read docinfo_field_name, n bytes")(tail)?;
            let docinfo_field_name = String::from_utf8_lossy(docinfo_field_name_bytes).to_string();
            tracing::debug!(
                "{field_num} field NAME: '{}' {}",
                docinfo_field_name,
                peek_hex(tail)
            );

            // Internationalization str, 4 bytes
            let (tail, i18n_bytes) = dbg_peek_hex(take(4usize), "read i18n bytes, n bytes")(tail)?;
            let i18n_str_rev = String::from_utf8_lossy(i18n_bytes).to_string();
            let i18n_str: String = i18n_str_rev.chars().rev().collect();
            tracing::debug!(
                "{field_num} field language/i8n: '{}' {}",
                i18n_str,
                peek_hex(tail)
            );

            let (tail, next_str_size_bytes) =
                dbg_peek_hex(take(2usize), "read string size, 2 bytes")(tail)?;
            let (_, next_str_size) = u16(nom::number::Endianness::Little)(next_str_size_bytes)?;

            tracing::debug!(
                "{field_num}: field VALUE string size {}, tail is {}",
                next_str_size,
                peek_hex(new_tail)
            );

            let (tail, docinfo_field_value_bytes) =
                dbg_peek_hex(take(next_str_size), "read docinfo_field_value, n bytes")(tail)?;
            let docinfo_field_value =
                String::from_utf8_lossy(docinfo_field_value_bytes).to_string();

            tracing::debug!(
                "{field_num} field VALUE: '{}' {}",
                docinfo_field_value,
                peek_hex(tail)
            );

            if i18n_str == "enUS" {
                res.set_field_by_name(&docinfo_field_name, docinfo_field_value);
            } else {
                tracing::debug!("Skipping language '{i18n_str}'");
            }
            new_tail = tail;
        }

        Ok((tail, res))
    }
}

#[cfg(test)]
pub mod document_header_tests {
    use super::*;

    pub fn document_header_cache_content() -> Vec<u8> {
        // xxd -i -t < DocumentHeader
        vec![
            0x48, 0x32, 0x43, 0x53, // 4 bytes H2CS header.
            0x08, 0x00, 0x00, 0x00, // Maybe a file version?
            0x32, 0x53, 0x00, 0x00, // S2, maybe compatibility or rolated to Starcraft 2?
            0x01, 0x05, 0x00, 0x0e, 0x95, 0x6c, 0x01, 0x00, // Maybe dimension X1
            0x95, 0x6c, 0x01, 0x00, // Maybe dimension Y1
            0x00, 0x00, 0x00, 0x00, // Padding bytes
            0xe7, 0x9c, 0xc5, 0x58, // Maybe dimension X2
            0xe7, 0x9c, 0xc5, 0x58, // Maybe dimension Y2
            0x02, 0x00, 0x00, 0x00, // No idea.
            0x00, 0x00, 0x00, 0x00, // Padding
            0x01, 0x00, 0x00, 0x00, // No idea.
            0x62, 0x6e, 0x65, 0x74, 0x3a, 0x56, 0x6f, 0x69, 0x64, 0x20, 0x28, 0x4d, 0x6f, 0x64,
            0x29, 0x2f, 0x30, 0x2e, 0x30, 0x2f, 0x39, 0x39, 0x39, 0x2c, 0x66, 0x69, 0x6c, 0x65,
            0x3a, 0x4d, 0x6f, 0x64, 0x73, 0x2f, 0x56, 0x6f, 0x69, 0x64, 0x2e, 0x53, 0x43, 0x32,
            0x4d, 0x6f, 0x64, 0x00, // Until 0x0, a string with battle net SC2Mod information.
            0x07, 0x00, // Maybe doc info field count.
            0x00, 0x00, // Maybe padding.
            0x0c, 0x00, // next string size, 12 characters...
            0x44, 0x6f, 0x63, 0x49, 0x6e, 0x66, 0x6f, 0x2f, 0x4e, 0x61, 0x6d,
            0x65, // 12 characters up to here: DocInfo/Name
            0x53, 0x55, 0x6e,
            0x65, // "SUne" reverse for 4 characters internationalization, enUS
            // - S for String
            0x0a, 0x00, // next string size, 10 characters.
            0x54, 0x6f, 0x6b, 0x61, 0x6d, 0x61, 0x6b, 0x20, 0x4c, 0x45,
            // 10 charaters up to here: Tokamak LE
            0x15, 0x00, 0x4d, 0x61, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x2f, 0x50, 0x6c, 0x61, 0x79,
            0x65, 0x72, 0x30, 0x33, 0x2f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x55, 0x6e, 0x65, 0x07,
            0x00, 0x48, 0x6f, 0x73, 0x74, 0x69, 0x6c, 0x65, 0x10, 0x00, 0x44, 0x6f, 0x63, 0x49,
            0x6e, 0x66, 0x6f, 0x2f, 0x44, 0x65, 0x73, 0x63, 0x4c, 0x6f, 0x6e, 0x67, 0x53, 0x55,
            0x6e, 0x65, 0xd1, 0x00, 0x57, 0x69, 0x64, 0x65, 0x20, 0x6d, 0x61, 0x63, 0x72, 0x6f,
            0x20, 0x6d, 0x61, 0x70, 0x20, 0x77, 0x69, 0x74, 0x63, 0x68, 0x20, 0x31, 0x36, 0x20,
            0x62, 0x61, 0x73, 0x65, 0x73, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x72, 0x65, 0x20, 0x61,
            0x72, 0x65, 0x20, 0x74, 0x77, 0x6f, 0x20, 0x68, 0x79, 0x62, 0x72, 0x69, 0x64, 0x20,
            0x62, 0x61, 0x73, 0x65, 0x73, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x6d, 0x69, 0x6e,
            0x65, 0x72, 0x61, 0x6c, 0x73, 0x20, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67,
            0x20, 0x61, 0x20, 0x70, 0x61, 0x74, 0x68, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x70,
            0x61, 0x74, 0x68, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x70, 0x65,
            0x6e, 0x20, 0x62, 0x79, 0x20, 0x6d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x20, 0x6f, 0x72,
            0x20, 0x62, 0x79, 0x20, 0x64, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x69, 0x6e, 0x67,
            0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x20, 0x74,
            0x6f, 0x77, 0x65, 0x72, 0x73, 0x20, 0x62, 0x65, 0x68, 0x69, 0x6e, 0x64, 0x20, 0x74,
            0x68, 0x65, 0x20, 0x62, 0x61, 0x73, 0x65, 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, 0x20,
            0x63, 0x72, 0x75, 0x73, 0x68, 0x65, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x62, 0x6c,
            0x75, 0x65, 0x20, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x61, 0x6c, 0x20, 0x6e, 0x6f, 0x64,
            0x65, 0x73, 0x2e, 0x15, 0x00, 0x4d, 0x61, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x2f, 0x50,
            0x6c, 0x61, 0x79, 0x65, 0x72, 0x30, 0x30, 0x2f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x55,
            0x6e, 0x65, 0x07, 0x00, 0x4e, 0x65, 0x75, 0x74, 0x72, 0x61, 0x6c, 0x15, 0x00, 0x4d,
            0x61, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x2f, 0x50, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x30,
            0x32, 0x2f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x55, 0x6e, 0x65, 0x08, 0x00, 0x50, 0x6c,
            0x61, 0x79, 0x65, 0x72, 0x20, 0x32, 0x11, 0x00, 0x44, 0x6f, 0x63, 0x49, 0x6e, 0x66,
            0x6f, 0x2f, 0x44, 0x65, 0x73, 0x63, 0x53, 0x68, 0x6f, 0x72, 0x74, 0x53, 0x55, 0x6e,
            0x65, 0x01, 0x00, 0x32, 0x15, 0x00, 0x4d, 0x61, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x2f,
            0x50, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x30, 0x31, 0x2f, 0x4e, 0x61, 0x6d, 0x65, 0x53,
            0x55, 0x6e, 0x65, 0x08, 0x00, 0x50, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x20, 0x31, 0x00,
        ]
    }

    #[test_log::test]
    fn test_parse_document_header() {
        let cache_contents: Vec<u8> = document_header_cache_content();
        let (_, document_header) = DocumentHeader::parse(&cache_contents).unwrap();
        assert_eq!(document_header.name, "Tokamak LE");
        assert_eq!(
            document_header.mod_info,
            "bnet:Void (Mod)/0.0/999,file:Mods/Void.SC2Mod"
        );
    }
}