halbu 0.3.0

Diablo II save file parsing library.
Documentation
use crate::character::decode_for_format as decode_character_for_format;
use crate::{IssueKind, IssueSeverity, ParseHardError, ParseIssue, SaveSummary, Strictness};

use super::layout::{
    expansion_type_from_decoded_character, layout_for_decode, push_issue, range_readable,
    read_version, section_name_option, IssueContext, SIGNATURE, SIGNATURE_RANGE, VERSION_RANGE,
};
use super::FormatId;

/// Summarize top-level header and character fields for list views.
pub(crate) fn summarize(
    bytes: &[u8],
    strictness: Strictness,
) -> Result<SaveSummary, ParseHardError> {
    let mut summary = SaveSummary::default();
    let mut issues: Vec<ParseIssue> = Vec::new();

    if !range_readable(bytes, &SIGNATURE_RANGE, "signature", &mut issues) {
        return if strictness == Strictness::Strict {
            Err(ParseHardError {
                message: "Cannot summarize save: signature section is truncated.".to_string(),
            })
        } else {
            summary.issues = issues;
            Ok(summary)
        };
    }

    if bytes[SIGNATURE_RANGE.start..SIGNATURE_RANGE.end] != SIGNATURE {
        push_issue(
            &mut issues,
            IssueSeverity::Error,
            IssueKind::InvalidSignature,
            IssueContext {
                section_name: section_name_option("signature"),
                message: format!(
                    "Invalid signature: expected {SIGNATURE:X?}, found {:X?}.",
                    &bytes[SIGNATURE_RANGE.start..SIGNATURE_RANGE.end]
                ),
                offset: Some(SIGNATURE_RANGE.start),
                expected: Some(SIGNATURE_RANGE.end - SIGNATURE_RANGE.start),
                found: Some(SIGNATURE_RANGE.end - SIGNATURE_RANGE.start),
            },
        );

        if strictness == Strictness::Strict {
            return Err(ParseHardError {
                message: "Cannot summarize save: invalid signature in strict mode.".to_string(),
            });
        }
    }

    if !range_readable(bytes, &VERSION_RANGE, "version", &mut issues) {
        return if strictness == Strictness::Strict {
            Err(ParseHardError {
                message: "Cannot summarize save: version section is truncated.".to_string(),
            })
        } else {
            summary.issues = issues;
            Ok(summary)
        };
    }

    let version = read_version(bytes);
    let detected_format = FormatId::from_version(version).unwrap_or(FormatId::Unknown(version));
    summary.version = Some(version);
    summary.format = Some(detected_format);
    summary.edition = detected_format.edition();

    let selected_layout = layout_for_decode(detected_format, bytes, strictness, &mut issues)?;
    let character_range = selected_layout.character_range();
    if !range_readable(bytes, &character_range, "character", &mut issues) {
        return if strictness == Strictness::Strict {
            Err(ParseHardError {
                message: "Cannot summarize save: character section is truncated.".to_string(),
            })
        } else {
            summary.issues = issues;
            Ok(summary)
        };
    }

    match decode_character_for_format(
        selected_layout.format_id(),
        &bytes[character_range.start..character_range.end],
    ) {
        Ok(character) => {
            let expansion_type =
                expansion_type_from_decoded_character(selected_layout.format_id(), &character);
            if character.mercenary.has_data_without_hire() {
                push_issue(
                    &mut issues,
                    IssueSeverity::Warning,
                    IssueKind::InvalidValue,
                    IssueContext {
                        section_name: section_name_option("mercenary"),
                        message: "Mercenary block contains non-zero fields even though mercenary id is 0.".to_string(),
                        offset: Some(character_range.start),
                        expected: Some(14),
                        found: Some(14),
                    },
                );
            }
            let title = character.title_d2r(expansion_type).map(str::to_string);
            let class = character.class;
            let level = character.level();
            let name = character.name;
            summary.expansion_type = Some(expansion_type);
            summary.name = Some(name);
            summary.class = Some(class);
            summary.level = Some(level);
            summary.title = title;
        }
        Err(parse_error) => {
            push_issue(
                &mut issues,
                IssueSeverity::Error,
                IssueKind::InvalidValue,
                IssueContext {
                    section_name: section_name_option("character"),
                    message: parse_error.to_string(),
                    offset: Some(character_range.start),
                    expected: Some(selected_layout.character_length()),
                    found: Some(selected_layout.character_length()),
                },
            );

            if strictness == Strictness::Strict {
                return Err(ParseHardError {
                    message: "Cannot summarize save: character section payload is invalid."
                        .to_string(),
                });
            }
        }
    }

    summary.issues = issues;
    Ok(summary)
}