3370 lines
129 KiB
Rust
3370 lines
129 KiB
Rust
|
|
use std::fs;
|
||
|
|
use std::path::Path;
|
||
|
|
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
use sha2::{Digest, Sha256};
|
||
|
|
|
||
|
|
pub const SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION: u32 = 0x03ec;
|
||
|
|
const PREAMBLE_U32_WORD_COUNT: usize = 16;
|
||
|
|
const MIN_ASCII_RUN_LEN: usize = 8;
|
||
|
|
const ASCII_PREVIEW_CHAR_LIMIT: usize = 160;
|
||
|
|
const TAG_OFFSET_SAMPLE_LIMIT: usize = 8;
|
||
|
|
const EARLY_ZERO_RUN_THRESHOLD: usize = 16;
|
||
|
|
const EARLY_PREVIEW_BYTE_LIMIT: usize = 32;
|
||
|
|
const EARLY_ALIGNED_WORD_WINDOW_COUNT: usize = 8;
|
||
|
|
const SHARED_SIGNATURE_WORDS_1_TO_7: [u32; 7] = [
|
||
|
|
0x00002ee0, 0x00040001, 0x00028000, 0x00010000, 0x00000771, 0x00000771, 0x00000771,
|
||
|
|
];
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
|
|
struct KnownTagDefinition {
|
||
|
|
tag_id: u16,
|
||
|
|
label: &'static str,
|
||
|
|
grounded_meaning: &'static str,
|
||
|
|
}
|
||
|
|
|
||
|
|
const KNOWN_TAG_DEFINITIONS: [KnownTagDefinition; 4] = [
|
||
|
|
KnownTagDefinition {
|
||
|
|
tag_id: 0x2cee,
|
||
|
|
label: "overlay_mask_plane_primary",
|
||
|
|
grounded_meaning: "Primary one-byte overlay mask plane restored into world offset +0x1655.",
|
||
|
|
},
|
||
|
|
KnownTagDefinition {
|
||
|
|
tag_id: 0x2d51,
|
||
|
|
label: "overlay_mask_plane_secondary",
|
||
|
|
grounded_meaning: "Secondary one-byte overlay mask plane restored into world offset +0x1659.",
|
||
|
|
},
|
||
|
|
KnownTagDefinition {
|
||
|
|
tag_id: 0x9471,
|
||
|
|
label: "sidecar_byte_plane_family_low",
|
||
|
|
grounded_meaning: "Lower bound of the grounded sidecar byte-plane chunk family.",
|
||
|
|
},
|
||
|
|
KnownTagDefinition {
|
||
|
|
tag_id: 0x9472,
|
||
|
|
label: "sidecar_byte_plane_family_high",
|
||
|
|
grounded_meaning: "Upper bound of the grounded sidecar byte-plane chunk family.",
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpKnownTagHit {
|
||
|
|
pub tag_id: u16,
|
||
|
|
pub tag_hex: String,
|
||
|
|
pub label: String,
|
||
|
|
pub grounded_meaning: String,
|
||
|
|
pub hit_count: usize,
|
||
|
|
pub sample_offsets: Vec<usize>,
|
||
|
|
pub last_offset: Option<usize>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpPreambleWord {
|
||
|
|
pub index: usize,
|
||
|
|
pub offset: usize,
|
||
|
|
pub value_le: u32,
|
||
|
|
pub value_hex: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpPreamble {
|
||
|
|
pub byte_len: usize,
|
||
|
|
pub word_count: usize,
|
||
|
|
pub words: Vec<SmpPreambleWord>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpAsciiPreview {
|
||
|
|
pub offset: usize,
|
||
|
|
pub byte_len: usize,
|
||
|
|
pub preview: String,
|
||
|
|
pub truncated: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpSharedHeader {
|
||
|
|
pub byte_len: usize,
|
||
|
|
pub root_kind_word: u32,
|
||
|
|
pub root_kind_word_hex: String,
|
||
|
|
pub primary_family_tag: u32,
|
||
|
|
pub primary_family_tag_hex: String,
|
||
|
|
pub shared_signature_words_1_to_7: Vec<u32>,
|
||
|
|
pub shared_signature_hex_words_1_to_7: Vec<String>,
|
||
|
|
pub matches_grounded_common_signature: bool,
|
||
|
|
pub payload_window_words_8_to_9: Vec<u32>,
|
||
|
|
pub payload_window_hex_words_8_to_9: Vec<String>,
|
||
|
|
pub reserved_words_10_to_14: Vec<u32>,
|
||
|
|
pub reserved_words_10_to_14_all_zero: bool,
|
||
|
|
pub final_flag_word: u32,
|
||
|
|
pub final_flag_word_hex: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpHeaderVariantProbe {
|
||
|
|
pub variant_family: String,
|
||
|
|
pub variant_evidence: Vec<String>,
|
||
|
|
pub is_known_family: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpEarlyContentProbe {
|
||
|
|
pub first_post_text_nonzero_offset: usize,
|
||
|
|
pub zero_pad_after_text_len: usize,
|
||
|
|
pub first_post_text_block_len: usize,
|
||
|
|
pub first_post_text_block_hex: String,
|
||
|
|
pub trailing_zero_pad_after_first_block_len: usize,
|
||
|
|
pub secondary_nonzero_offset: Option<usize>,
|
||
|
|
pub secondary_aligned_word_window_offset: Option<usize>,
|
||
|
|
pub secondary_aligned_word_window_words: Vec<u32>,
|
||
|
|
pub secondary_aligned_word_window_hex_words: Vec<String>,
|
||
|
|
pub secondary_preview_hex: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpSecondaryVariantProbe {
|
||
|
|
pub aligned_window_offset: usize,
|
||
|
|
pub words: Vec<u32>,
|
||
|
|
pub hex_words: Vec<String>,
|
||
|
|
pub variant_family: String,
|
||
|
|
pub variant_evidence: Vec<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpContainerProfile {
|
||
|
|
pub profile_family: String,
|
||
|
|
pub profile_evidence: Vec<String>,
|
||
|
|
pub is_known_profile: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpSaveBootstrapBlock {
|
||
|
|
pub profile_family: String,
|
||
|
|
pub aligned_window_offset: usize,
|
||
|
|
pub leading_word: u32,
|
||
|
|
pub leading_word_hex: String,
|
||
|
|
pub anchor_word: u32,
|
||
|
|
pub anchor_word_hex: String,
|
||
|
|
pub descriptor_word_2: u32,
|
||
|
|
pub descriptor_word_2_hex: String,
|
||
|
|
pub descriptor_word_3: u32,
|
||
|
|
pub descriptor_word_3_hex: String,
|
||
|
|
pub descriptor_word_4: u32,
|
||
|
|
pub descriptor_word_4_hex: String,
|
||
|
|
pub descriptor_word_5: u32,
|
||
|
|
pub descriptor_word_5_hex: String,
|
||
|
|
pub descriptor_word_6: u32,
|
||
|
|
pub descriptor_word_6_hex: String,
|
||
|
|
pub descriptor_word_7: u32,
|
||
|
|
pub descriptor_word_7_hex: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpSaveAnchorRunBlock {
|
||
|
|
pub profile_family: String,
|
||
|
|
pub cycle_start_offset: usize,
|
||
|
|
pub cycle_words: Vec<u32>,
|
||
|
|
pub cycle_hex_words: Vec<String>,
|
||
|
|
pub full_cycle_count: usize,
|
||
|
|
pub partial_cycle_word_count: usize,
|
||
|
|
pub trailer_offset: usize,
|
||
|
|
pub trailer_words: Vec<u32>,
|
||
|
|
pub trailer_hex_words: Vec<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpRuntimeAnchorCycleBlock {
|
||
|
|
pub profile_family: String,
|
||
|
|
pub cycle_start_offset: usize,
|
||
|
|
pub cycle_words: Vec<u32>,
|
||
|
|
pub cycle_hex_words: Vec<String>,
|
||
|
|
pub full_cycle_count: usize,
|
||
|
|
pub partial_cycle_word_count: usize,
|
||
|
|
pub trailer_offset: usize,
|
||
|
|
pub trailer_words: Vec<u32>,
|
||
|
|
pub trailer_hex_words: Vec<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpRuntimeTrailerBlock {
|
||
|
|
pub profile_family: String,
|
||
|
|
pub trailer_family: String,
|
||
|
|
pub trailer_evidence: Vec<String>,
|
||
|
|
pub trailer_offset: usize,
|
||
|
|
pub prefix_words_0_to_5: Vec<u32>,
|
||
|
|
pub prefix_hex_words_0_to_5: Vec<String>,
|
||
|
|
pub tag_word_6: u32,
|
||
|
|
pub tag_word_6_hex: String,
|
||
|
|
pub tag_chunk_id_u16: u16,
|
||
|
|
pub tag_chunk_id_hex: String,
|
||
|
|
pub tag_chunk_id_grounded_alignment: Option<String>,
|
||
|
|
pub length_word_7: u32,
|
||
|
|
pub length_word_7_hex: String,
|
||
|
|
pub length_high_u16: u16,
|
||
|
|
pub length_high_hex: String,
|
||
|
|
pub selector_word_8: u32,
|
||
|
|
pub selector_word_8_hex: String,
|
||
|
|
pub selector_high_u16: u16,
|
||
|
|
pub selector_high_hex: String,
|
||
|
|
pub layout_word_9: u32,
|
||
|
|
pub layout_word_9_hex: String,
|
||
|
|
pub descriptor_word_10: u32,
|
||
|
|
pub descriptor_word_10_hex: String,
|
||
|
|
pub descriptor_high_u16: u16,
|
||
|
|
pub descriptor_high_hex: String,
|
||
|
|
pub descriptor_word_11: u32,
|
||
|
|
pub descriptor_word_11_hex: String,
|
||
|
|
pub counter_word_12: u32,
|
||
|
|
pub counter_word_12_hex: String,
|
||
|
|
pub offset_word_13: u32,
|
||
|
|
pub offset_word_13_hex: String,
|
||
|
|
pub span_word_14: u32,
|
||
|
|
pub span_word_14_hex: String,
|
||
|
|
pub mode_word_15: u32,
|
||
|
|
pub mode_word_15_hex: String,
|
||
|
|
pub words: Vec<u32>,
|
||
|
|
pub hex_words: Vec<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpRuntimePostSpanProbe {
|
||
|
|
pub profile_family: String,
|
||
|
|
pub span_target_offset: usize,
|
||
|
|
pub next_nonzero_offset: Option<usize>,
|
||
|
|
pub next_aligned_candidate_offset: Option<usize>,
|
||
|
|
pub next_aligned_candidate_words: Vec<u32>,
|
||
|
|
pub next_aligned_candidate_hex_words: Vec<String>,
|
||
|
|
pub header_candidates: Vec<SmpRuntimePostSpanHeaderCandidate>,
|
||
|
|
pub grounded_progress_hits: Vec<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpRuntimePostSpanHeaderCandidate {
|
||
|
|
pub offset: usize,
|
||
|
|
pub words: Vec<u32>,
|
||
|
|
pub hex_words: Vec<String>,
|
||
|
|
pub dense_word_count: usize,
|
||
|
|
pub high_u16_words: Vec<u16>,
|
||
|
|
pub high_hex_words: Vec<String>,
|
||
|
|
pub grounded_alignments: Vec<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpRt3105PostSpanBridgeProbe {
|
||
|
|
pub profile_family: String,
|
||
|
|
pub bridge_family: String,
|
||
|
|
pub bridge_evidence: Vec<String>,
|
||
|
|
pub span_target_offset: usize,
|
||
|
|
pub next_candidate_offset: Option<usize>,
|
||
|
|
pub next_candidate_delta_from_span_target: Option<usize>,
|
||
|
|
pub packed_profile_offset: usize,
|
||
|
|
pub packed_profile_delta_from_span_target: usize,
|
||
|
|
pub next_candidate_delta_from_packed_profile: Option<i64>,
|
||
|
|
pub selector_high_u16: u16,
|
||
|
|
pub selector_high_hex: String,
|
||
|
|
pub descriptor_high_u16: u16,
|
||
|
|
pub descriptor_high_hex: String,
|
||
|
|
pub next_candidate_high_u16_words: Vec<u16>,
|
||
|
|
pub next_candidate_high_hex_words: Vec<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpRt3105SaveBridgePayloadProbe {
|
||
|
|
pub profile_family: String,
|
||
|
|
pub bridge_family: String,
|
||
|
|
pub primary_block_offset: usize,
|
||
|
|
pub primary_block_len: usize,
|
||
|
|
pub primary_block_len_hex: String,
|
||
|
|
pub primary_words: Vec<u32>,
|
||
|
|
pub primary_hex_words: Vec<String>,
|
||
|
|
pub secondary_block_offset: usize,
|
||
|
|
pub secondary_block_delta_from_primary: usize,
|
||
|
|
pub secondary_block_delta_from_primary_hex: String,
|
||
|
|
pub secondary_block_end_offset: usize,
|
||
|
|
pub secondary_block_len: usize,
|
||
|
|
pub secondary_block_len_hex: String,
|
||
|
|
pub secondary_preview_word_count: usize,
|
||
|
|
pub secondary_words: Vec<u32>,
|
||
|
|
pub secondary_hex_words: Vec<String>,
|
||
|
|
pub evidence: Vec<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpRt3105SaveNameTableProbe {
|
||
|
|
pub profile_family: String,
|
||
|
|
pub source_kind: String,
|
||
|
|
pub semantic_family: String,
|
||
|
|
pub semantic_alignment: Vec<String>,
|
||
|
|
pub header_offset: usize,
|
||
|
|
pub header_word_0: u32,
|
||
|
|
pub header_word_0_hex: String,
|
||
|
|
pub header_word_1: u32,
|
||
|
|
pub header_word_1_hex: String,
|
||
|
|
pub header_word_2: u32,
|
||
|
|
pub header_word_2_hex: String,
|
||
|
|
pub entry_stride: usize,
|
||
|
|
pub entry_stride_hex: String,
|
||
|
|
pub header_prefix_word_count: usize,
|
||
|
|
pub observed_entry_capacity: usize,
|
||
|
|
pub observed_entry_count: usize,
|
||
|
|
pub zero_trailer_entry_count: usize,
|
||
|
|
pub nonzero_trailer_entry_count: usize,
|
||
|
|
pub distinct_trailer_words: Vec<u32>,
|
||
|
|
pub distinct_trailer_hex_words: Vec<String>,
|
||
|
|
pub zero_trailer_entry_names: Vec<String>,
|
||
|
|
pub entries_offset: usize,
|
||
|
|
pub entries_end_offset: usize,
|
||
|
|
pub trailing_footer_hex: String,
|
||
|
|
pub footer_progress_word_0: u32,
|
||
|
|
pub footer_progress_word_0_hex: String,
|
||
|
|
pub footer_progress_word_1: u32,
|
||
|
|
pub footer_progress_word_1_hex: String,
|
||
|
|
pub footer_trailing_byte: u8,
|
||
|
|
pub footer_trailing_byte_hex: String,
|
||
|
|
pub footer_grounded_alignments: Vec<String>,
|
||
|
|
pub entries: Vec<SmpRt3105SaveNameTableEntry>,
|
||
|
|
pub evidence: Vec<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpRt3105SaveNameTableEntry {
|
||
|
|
pub index: usize,
|
||
|
|
pub offset: usize,
|
||
|
|
pub text: String,
|
||
|
|
pub availability_dword: u32,
|
||
|
|
pub availability_dword_hex: String,
|
||
|
|
pub trailer_word: u32,
|
||
|
|
pub trailer_word_hex: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpClassicRehydrateProfileProbe {
|
||
|
|
pub profile_family: String,
|
||
|
|
pub progress_32dc_offset: usize,
|
||
|
|
pub progress_3714_offset: usize,
|
||
|
|
pub progress_3715_offset: usize,
|
||
|
|
pub packed_profile_offset: usize,
|
||
|
|
pub packed_profile_len: usize,
|
||
|
|
pub packed_profile_len_hex: String,
|
||
|
|
pub packed_profile_block: SmpClassicPackedProfileBlock,
|
||
|
|
pub ascii_runs: Vec<SmpAsciiPreview>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpClassicPackedProfileBlock {
|
||
|
|
pub relative_len: usize,
|
||
|
|
pub relative_len_hex: String,
|
||
|
|
pub leading_word_0: u32,
|
||
|
|
pub leading_word_0_hex: String,
|
||
|
|
pub trailing_zero_word_count_after_leading_word: usize,
|
||
|
|
pub map_path_offset: usize,
|
||
|
|
pub map_path: Option<String>,
|
||
|
|
pub display_name_offset: usize,
|
||
|
|
pub display_name: Option<String>,
|
||
|
|
pub profile_byte_0x77: u8,
|
||
|
|
pub profile_byte_0x77_hex: String,
|
||
|
|
pub profile_byte_0x82: u8,
|
||
|
|
pub profile_byte_0x82_hex: String,
|
||
|
|
pub profile_byte_0x97: u8,
|
||
|
|
pub profile_byte_0x97_hex: String,
|
||
|
|
pub profile_byte_0xc5: u8,
|
||
|
|
pub profile_byte_0xc5_hex: String,
|
||
|
|
pub stable_nonzero_words: Vec<SmpPackedProfileWordLane>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpRt3105PackedProfileProbe {
|
||
|
|
pub profile_family: String,
|
||
|
|
pub packed_profile_offset: usize,
|
||
|
|
pub packed_profile_len: usize,
|
||
|
|
pub packed_profile_len_hex: String,
|
||
|
|
pub packed_profile_block: SmpRt3105PackedProfileBlock,
|
||
|
|
pub ascii_runs: Vec<SmpAsciiPreview>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpRt3105PackedProfileBlock {
|
||
|
|
pub relative_len: usize,
|
||
|
|
pub relative_len_hex: String,
|
||
|
|
pub leading_word_0: u32,
|
||
|
|
pub leading_word_0_hex: String,
|
||
|
|
pub trailing_zero_word_count_after_leading_word: usize,
|
||
|
|
pub header_flag_word_3: u32,
|
||
|
|
pub header_flag_word_3_hex: String,
|
||
|
|
pub map_path_offset: usize,
|
||
|
|
pub map_path: Option<String>,
|
||
|
|
pub display_name_offset: usize,
|
||
|
|
pub display_name: Option<String>,
|
||
|
|
pub profile_byte_0x77: u8,
|
||
|
|
pub profile_byte_0x77_hex: String,
|
||
|
|
pub profile_byte_0x82: u8,
|
||
|
|
pub profile_byte_0x82_hex: String,
|
||
|
|
pub profile_byte_0x97: u8,
|
||
|
|
pub profile_byte_0x97_hex: String,
|
||
|
|
pub profile_byte_0xc5: u8,
|
||
|
|
pub profile_byte_0xc5_hex: String,
|
||
|
|
pub stable_nonzero_words: Vec<SmpPackedProfileWordLane>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpPackedProfileWordLane {
|
||
|
|
pub relative_offset: usize,
|
||
|
|
pub relative_offset_hex: String,
|
||
|
|
pub value: u32,
|
||
|
|
pub value_hex: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub struct SmpInspectionReport {
|
||
|
|
pub inspection_mode: String,
|
||
|
|
pub file_extension_hint: Option<String>,
|
||
|
|
pub file_size: usize,
|
||
|
|
pub sha256: String,
|
||
|
|
pub preamble: SmpPreamble,
|
||
|
|
pub shared_header: Option<SmpSharedHeader>,
|
||
|
|
pub header_variant_probe: Option<SmpHeaderVariantProbe>,
|
||
|
|
pub first_ascii_run: Option<SmpAsciiPreview>,
|
||
|
|
pub early_content_probe: Option<SmpEarlyContentProbe>,
|
||
|
|
pub secondary_variant_probe: Option<SmpSecondaryVariantProbe>,
|
||
|
|
pub container_profile: Option<SmpContainerProfile>,
|
||
|
|
pub save_bootstrap_block: Option<SmpSaveBootstrapBlock>,
|
||
|
|
pub save_anchor_run_block: Option<SmpSaveAnchorRunBlock>,
|
||
|
|
pub runtime_anchor_cycle_block: Option<SmpRuntimeAnchorCycleBlock>,
|
||
|
|
pub runtime_trailer_block: Option<SmpRuntimeTrailerBlock>,
|
||
|
|
pub runtime_post_span_probe: Option<SmpRuntimePostSpanProbe>,
|
||
|
|
pub rt3_105_post_span_bridge_probe: Option<SmpRt3105PostSpanBridgeProbe>,
|
||
|
|
pub rt3_105_save_bridge_payload_probe: Option<SmpRt3105SaveBridgePayloadProbe>,
|
||
|
|
pub rt3_105_save_name_table_probe: Option<SmpRt3105SaveNameTableProbe>,
|
||
|
|
pub classic_rehydrate_profile_probe: Option<SmpClassicRehydrateProfileProbe>,
|
||
|
|
pub rt3_105_packed_profile_probe: Option<SmpRt3105PackedProfileProbe>,
|
||
|
|
pub contains_grounded_runtime_tags: bool,
|
||
|
|
pub known_tag_hits: Vec<SmpKnownTagHit>,
|
||
|
|
pub notes: Vec<String>,
|
||
|
|
pub warnings: Vec<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn inspect_smp_file(path: &Path) -> Result<SmpInspectionReport, Box<dyn std::error::Error>> {
|
||
|
|
let bytes = fs::read(path)?;
|
||
|
|
Ok(inspect_bundle_bytes(
|
||
|
|
&bytes,
|
||
|
|
path.extension()
|
||
|
|
.and_then(|extension| extension.to_str())
|
||
|
|
.map(|extension| extension.to_ascii_lowercase()),
|
||
|
|
))
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn inspect_smp_bytes(bytes: &[u8]) -> SmpInspectionReport {
|
||
|
|
inspect_bundle_bytes(bytes, None)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn inspect_bundle_bytes(bytes: &[u8], file_extension_hint: Option<String>) -> SmpInspectionReport {
|
||
|
|
let known_tag_hits = KNOWN_TAG_DEFINITIONS
|
||
|
|
.iter()
|
||
|
|
.filter_map(|definition| {
|
||
|
|
let offsets = find_u16_le_offsets(bytes, definition.tag_id);
|
||
|
|
if offsets.is_empty() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
Some(SmpKnownTagHit {
|
||
|
|
tag_id: definition.tag_id,
|
||
|
|
tag_hex: format!("0x{:04x}", definition.tag_id),
|
||
|
|
label: definition.label.to_string(),
|
||
|
|
grounded_meaning: definition.grounded_meaning.to_string(),
|
||
|
|
hit_count: offsets.len(),
|
||
|
|
sample_offsets: offsets
|
||
|
|
.iter()
|
||
|
|
.copied()
|
||
|
|
.take(TAG_OFFSET_SAMPLE_LIMIT)
|
||
|
|
.collect(),
|
||
|
|
last_offset: offsets.last().copied(),
|
||
|
|
})
|
||
|
|
})
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
|
||
|
|
let shared_header = parse_shared_header(bytes);
|
||
|
|
let header_variant_probe = shared_header.as_ref().map(classify_header_variant_probe);
|
||
|
|
let first_ascii_run = find_first_ascii_run(bytes);
|
||
|
|
let early_content_probe = first_ascii_run
|
||
|
|
.as_ref()
|
||
|
|
.and_then(|ascii_run| probe_early_content_layout(bytes, ascii_run));
|
||
|
|
let secondary_variant_probe = early_content_probe
|
||
|
|
.as_ref()
|
||
|
|
.and_then(classify_secondary_variant_probe);
|
||
|
|
let container_profile = classify_container_profile(
|
||
|
|
file_extension_hint.as_deref(),
|
||
|
|
header_variant_probe.as_ref(),
|
||
|
|
secondary_variant_probe.as_ref(),
|
||
|
|
);
|
||
|
|
let runtime_anchor_cycle_block = parse_runtime_anchor_cycle_block(
|
||
|
|
bytes,
|
||
|
|
container_profile.as_ref(),
|
||
|
|
secondary_variant_probe.as_ref(),
|
||
|
|
);
|
||
|
|
let save_bootstrap_block =
|
||
|
|
parse_save_bootstrap_block(container_profile.as_ref(), secondary_variant_probe.as_ref());
|
||
|
|
let save_anchor_run_block = parse_save_anchor_run_block(
|
||
|
|
bytes,
|
||
|
|
container_profile.as_ref(),
|
||
|
|
save_bootstrap_block.as_ref(),
|
||
|
|
);
|
||
|
|
let runtime_trailer_block = parse_runtime_trailer_block(
|
||
|
|
container_profile.as_ref(),
|
||
|
|
runtime_anchor_cycle_block.as_ref(),
|
||
|
|
);
|
||
|
|
let runtime_post_span_probe =
|
||
|
|
parse_runtime_post_span_probe(bytes, runtime_trailer_block.as_ref());
|
||
|
|
let rt3_105_packed_profile_probe = parse_rt3_105_packed_profile_probe(
|
||
|
|
bytes,
|
||
|
|
file_extension_hint.as_deref(),
|
||
|
|
header_variant_probe.as_ref(),
|
||
|
|
container_profile.as_ref(),
|
||
|
|
);
|
||
|
|
let rt3_105_post_span_bridge_probe = parse_rt3_105_post_span_bridge_probe(
|
||
|
|
runtime_trailer_block.as_ref(),
|
||
|
|
runtime_post_span_probe.as_ref(),
|
||
|
|
rt3_105_packed_profile_probe.as_ref(),
|
||
|
|
);
|
||
|
|
let rt3_105_save_bridge_payload_probe =
|
||
|
|
parse_rt3_105_save_bridge_payload_probe(bytes, rt3_105_post_span_bridge_probe.as_ref());
|
||
|
|
let rt3_105_save_name_table_probe = parse_rt3_105_save_name_table_probe(
|
||
|
|
bytes,
|
||
|
|
file_extension_hint.as_deref(),
|
||
|
|
container_profile.as_ref(),
|
||
|
|
rt3_105_save_bridge_payload_probe.as_ref(),
|
||
|
|
);
|
||
|
|
let classic_rehydrate_profile_probe =
|
||
|
|
parse_classic_rehydrate_profile_probe(bytes, runtime_post_span_probe.as_ref());
|
||
|
|
let mut warnings = Vec::new();
|
||
|
|
if bytes.is_empty() {
|
||
|
|
warnings
|
||
|
|
.push("File is empty, so no `.smp` container structure could be observed.".to_string());
|
||
|
|
}
|
||
|
|
|
||
|
|
if known_tag_hits.is_empty() {
|
||
|
|
warnings.push(
|
||
|
|
"No grounded runtime bundle tags were found in little-endian form. This does not prove the file is invalid."
|
||
|
|
.to_string(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if shared_header.is_none() && !bytes.is_empty() {
|
||
|
|
warnings.push(
|
||
|
|
"File is shorter than the observed 64-byte common RT3 bundle preamble.".to_string(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if let Some(shared_header) = &shared_header {
|
||
|
|
let header_family_is_known = header_variant_probe
|
||
|
|
.as_ref()
|
||
|
|
.map(|probe| probe.is_known_family)
|
||
|
|
.unwrap_or(false);
|
||
|
|
if !shared_header.matches_grounded_common_signature && !header_family_is_known {
|
||
|
|
warnings.push(
|
||
|
|
"The first 64-byte preamble does not match the currently observed shared RT3 bundle signature."
|
||
|
|
.to_string(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if first_ascii_run.is_some() && early_content_probe.is_none() {
|
||
|
|
warnings.push(
|
||
|
|
"Found early text content but could not resolve the next stable nonzero region after its zero padding."
|
||
|
|
.to_string(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if container_profile
|
||
|
|
.as_ref()
|
||
|
|
.is_some_and(|profile| !profile.is_known_profile)
|
||
|
|
{
|
||
|
|
warnings.push(
|
||
|
|
"The current probes did not match any known composite container profile.".to_string(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if known_tag_hits
|
||
|
|
.iter()
|
||
|
|
.any(|hit| hit.hit_count > hit.sample_offsets.len())
|
||
|
|
{
|
||
|
|
warnings.push(
|
||
|
|
"Known-tag offsets are sampled in this report. Large hit counts usually mean byte-pattern noise, not validated chunk boundaries."
|
||
|
|
.to_string(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
warnings.push(
|
||
|
|
"Inspection scans raw bytes for a small grounded tag set only. It does not validate bundle layout or decode payloads."
|
||
|
|
.to_string(),
|
||
|
|
);
|
||
|
|
|
||
|
|
SmpInspectionReport {
|
||
|
|
inspection_mode: "grounded-tag-scan-plus-preamble".to_string(),
|
||
|
|
file_extension_hint,
|
||
|
|
file_size: bytes.len(),
|
||
|
|
sha256: sha256_hex(bytes),
|
||
|
|
preamble: parse_preamble(bytes),
|
||
|
|
shared_header,
|
||
|
|
header_variant_probe,
|
||
|
|
first_ascii_run,
|
||
|
|
early_content_probe,
|
||
|
|
secondary_variant_probe,
|
||
|
|
container_profile,
|
||
|
|
save_bootstrap_block,
|
||
|
|
save_anchor_run_block,
|
||
|
|
runtime_anchor_cycle_block,
|
||
|
|
runtime_trailer_block,
|
||
|
|
runtime_post_span_probe,
|
||
|
|
rt3_105_post_span_bridge_probe,
|
||
|
|
rt3_105_save_bridge_payload_probe,
|
||
|
|
rt3_105_save_name_table_probe,
|
||
|
|
classic_rehydrate_profile_probe,
|
||
|
|
rt3_105_packed_profile_probe,
|
||
|
|
contains_grounded_runtime_tags: !known_tag_hits.is_empty(),
|
||
|
|
known_tag_hits,
|
||
|
|
notes: vec![
|
||
|
|
"Grounded `.smp` runtime tags currently include mask-plane payload ids 0x2cee and 0x2d51.".to_string(),
|
||
|
|
"Grounded sidecar-byte-plane bundle family currently spans 0x9471..0x9472.".to_string(),
|
||
|
|
"The shared-header parse is intentionally conservative: it only names common preamble lanes and checks the observed RT3 bundle-family signature.".to_string(),
|
||
|
|
"The header-variant probe classifies the preamble into one of the currently observed install-era families when possible."
|
||
|
|
.to_string(),
|
||
|
|
"The early-content probe resolves the first stable nonzero block after the padded scenario text and then captures the next aligned word window."
|
||
|
|
.to_string(),
|
||
|
|
"The secondary-variant probe classifies that aligned word window into one of the currently observed file-family patterns."
|
||
|
|
.to_string(),
|
||
|
|
"The container-profile layer combines extension hint, header family, and second-window family into one observed container classification."
|
||
|
|
.to_string(),
|
||
|
|
"The save-bootstrap reader currently parses one conservative 8-word descriptor only for known save-container profiles."
|
||
|
|
.to_string(),
|
||
|
|
"The save-anchor-run reader follows that descriptor tail into the observed repeated 9-word anchor cycle and captures the first trailer words after the cycle diverges."
|
||
|
|
.to_string(),
|
||
|
|
"The runtime-anchor-cycle reader applies the same cycle/trailer scan across the currently known save and sandbox runtime container profiles."
|
||
|
|
.to_string(),
|
||
|
|
"The runtime-trailer reader classifies the first 16 words after the cycle divergence into the currently observed runtime trailer families."
|
||
|
|
.to_string(),
|
||
|
|
"The runtime post-span probe follows the trailer's high-16 span lane into the later file region and records the next nonzero bytes, the first aligned high-16-dense candidate window, and any grounded progress-id hits found nearby."
|
||
|
|
.to_string(),
|
||
|
|
"The RT3 1.05 post-span bridge probe correlates the trailer selector/descriptor lanes with the next candidate region and the later packed-profile block for the currently observed 1.05 save families."
|
||
|
|
.to_string(),
|
||
|
|
"The RT3 1.05 common-save bridge payload probe captures the two stable bridge-stage blocks currently observed under the base 1.05 save branch."
|
||
|
|
.to_string(),
|
||
|
|
"The RT3 1.05 candidate-availability table probe decodes the fixed-width trailing name table from either the common-save bridge payload or the fixed 0x6a70..0x73c0 source range when that header validates."
|
||
|
|
.to_string(),
|
||
|
|
"The classic rehydrate-profile probe recognizes the grounded 0x32dc -> 0x3714 -> 0x3715 progress-id sequence and captures the exact 0x108-byte block between the latter two ids when that pattern appears."
|
||
|
|
.to_string(),
|
||
|
|
"The classic packed-profile block reader exposes the stable map-path, display-name, atlas-tracked latch bytes, and the small set of nonzero word lanes observed inside that 0x108-byte block."
|
||
|
|
.to_string(),
|
||
|
|
"The RT3 1.05 packed-profile probe recognizes the later string-bearing save block rooted at the first post-header .gmp path and exposes the observed map-path, display-name, atlas-tracked byte lanes, and stable nonzero words."
|
||
|
|
.to_string(),
|
||
|
|
format!(
|
||
|
|
"Restore-side loading of the four sidecar byte planes is only grounded for bundle versions >= 0x{:04x}.",
|
||
|
|
SMP_FOUR_SIDECAR_BYTE_PLANES_MIN_BUNDLE_VERSION
|
||
|
|
),
|
||
|
|
],
|
||
|
|
warnings,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_preamble(bytes: &[u8]) -> SmpPreamble {
|
||
|
|
let byte_len = bytes.len().min(PREAMBLE_U32_WORD_COUNT * 4);
|
||
|
|
let words = bytes[..byte_len]
|
||
|
|
.chunks_exact(4)
|
||
|
|
.enumerate()
|
||
|
|
.map(|(index, chunk)| {
|
||
|
|
let value_le = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
||
|
|
SmpPreambleWord {
|
||
|
|
index,
|
||
|
|
offset: index * 4,
|
||
|
|
value_le,
|
||
|
|
value_hex: format!("0x{value_le:08x}"),
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
|
||
|
|
SmpPreamble {
|
||
|
|
byte_len,
|
||
|
|
word_count: words.len(),
|
||
|
|
words,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_shared_header(bytes: &[u8]) -> Option<SmpSharedHeader> {
|
||
|
|
let words = read_preamble_words(bytes)?;
|
||
|
|
let shared_signature_words_1_to_7 = words[1..=7].to_vec();
|
||
|
|
let payload_window_words_8_to_9 = words[8..=9].to_vec();
|
||
|
|
let reserved_words_10_to_14 = words[10..=14].to_vec();
|
||
|
|
let final_flag_word = words[15];
|
||
|
|
|
||
|
|
Some(SmpSharedHeader {
|
||
|
|
byte_len: PREAMBLE_U32_WORD_COUNT * 4,
|
||
|
|
root_kind_word: words[0],
|
||
|
|
root_kind_word_hex: format!("0x{:08x}", words[0]),
|
||
|
|
primary_family_tag: words[1],
|
||
|
|
primary_family_tag_hex: format!("0x{:08x}", words[1]),
|
||
|
|
shared_signature_hex_words_1_to_7: shared_signature_words_1_to_7
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect(),
|
||
|
|
matches_grounded_common_signature: shared_signature_words_1_to_7
|
||
|
|
== SHARED_SIGNATURE_WORDS_1_TO_7,
|
||
|
|
shared_signature_words_1_to_7,
|
||
|
|
payload_window_hex_words_8_to_9: payload_window_words_8_to_9
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect(),
|
||
|
|
payload_window_words_8_to_9,
|
||
|
|
reserved_words_10_to_14_all_zero: reserved_words_10_to_14.iter().all(|word| *word == 0),
|
||
|
|
reserved_words_10_to_14,
|
||
|
|
final_flag_word,
|
||
|
|
final_flag_word_hex: format!("0x{final_flag_word:08x}"),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn classify_header_variant_probe(shared_header: &SmpSharedHeader) -> SmpHeaderVariantProbe {
|
||
|
|
let words = &shared_header.shared_signature_words_1_to_7;
|
||
|
|
let root = shared_header.root_kind_word;
|
||
|
|
let final_flag = shared_header.final_flag_word;
|
||
|
|
|
||
|
|
let (variant_family, evidence, is_known_family) = match (root, words.as_slice(), final_flag) {
|
||
|
|
(
|
||
|
|
0x00002649,
|
||
|
|
[
|
||
|
|
0x00002ee0,
|
||
|
|
0x00040001,
|
||
|
|
0x00028000,
|
||
|
|
0x00010000,
|
||
|
|
0x00000771,
|
||
|
|
0x00000771,
|
||
|
|
0x00000771,
|
||
|
|
],
|
||
|
|
0x00000001,
|
||
|
|
) => (
|
||
|
|
"rt3-105-gmx-header-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"root kind word 0x00002649".to_string(),
|
||
|
|
"1.05 common signature words 1..7".to_string(),
|
||
|
|
"final flag 0x00000001".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
(
|
||
|
|
0x000025e5,
|
||
|
|
[
|
||
|
|
0x00002ee0,
|
||
|
|
0x00040001,
|
||
|
|
0x00028000,
|
||
|
|
0x00010000,
|
||
|
|
0x00000771,
|
||
|
|
0x00000771,
|
||
|
|
0x00000771,
|
||
|
|
],
|
||
|
|
0x00000000,
|
||
|
|
) => (
|
||
|
|
"rt3-105-common-header-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"root kind word 0x000025e5".to_string(),
|
||
|
|
"1.05 common signature words 1..7".to_string(),
|
||
|
|
"final flag 0x00000000".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
(
|
||
|
|
0x000025e5,
|
||
|
|
[
|
||
|
|
0x00002ee0,
|
||
|
|
0x00040001,
|
||
|
|
0x00018000,
|
||
|
|
0x00010000,
|
||
|
|
0x00000746,
|
||
|
|
0x00000746,
|
||
|
|
0x00000746,
|
||
|
|
],
|
||
|
|
0x00000000,
|
||
|
|
) => (
|
||
|
|
"rt3-105-scenario-save-header-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"root kind word 0x000025e5".to_string(),
|
||
|
|
"1.05 scenario-save signature words 1..7".to_string(),
|
||
|
|
"final flag 0x00000000".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
(
|
||
|
|
0x000025e5,
|
||
|
|
[
|
||
|
|
0x00002ee0,
|
||
|
|
0x0001c001,
|
||
|
|
0x00018000,
|
||
|
|
0x00010000,
|
||
|
|
0x00000754,
|
||
|
|
0x00000754,
|
||
|
|
0x00000754,
|
||
|
|
],
|
||
|
|
0x00000000,
|
||
|
|
) => (
|
||
|
|
"rt3-105-alt-save-header-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"root kind word 0x000025e5".to_string(),
|
||
|
|
"1.05 alternate-save signature words 1..7".to_string(),
|
||
|
|
"final flag 0x00000000".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
(
|
||
|
|
0x000026ad,
|
||
|
|
[
|
||
|
|
0x00002ee0,
|
||
|
|
0x00014001,
|
||
|
|
0x00020000,
|
||
|
|
0x00010000,
|
||
|
|
0x00000725,
|
||
|
|
0x00000725,
|
||
|
|
0x00000725,
|
||
|
|
],
|
||
|
|
0x00000100,
|
||
|
|
) => (
|
||
|
|
"rt3-classic-gms-header-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"root kind word 0x000026ad".to_string(),
|
||
|
|
"classic save signature words 1..7".to_string(),
|
||
|
|
"final flag 0x00000100".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
(
|
||
|
|
0x000026ad,
|
||
|
|
[
|
||
|
|
0x00002ee0,
|
||
|
|
0x0001c001,
|
||
|
|
0x00018000,
|
||
|
|
0x00010000,
|
||
|
|
0x00000765,
|
||
|
|
0x00000765,
|
||
|
|
0x00000765,
|
||
|
|
],
|
||
|
|
0x00000001,
|
||
|
|
) => (
|
||
|
|
"rt3-classic-gmx-header-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"root kind word 0x000026ad".to_string(),
|
||
|
|
"classic sandbox signature words 1..7".to_string(),
|
||
|
|
"final flag 0x00000001".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
(0x000025e5, [0x00002ee0, _, _, 0x00010000, _, _, _], 0x00000000 | 0x00000100) => (
|
||
|
|
"rt3-map-header-family".to_string(),
|
||
|
|
vec![
|
||
|
|
"root kind word 0x000025e5".to_string(),
|
||
|
|
"map-family anchor 0x00002ee0".to_string(),
|
||
|
|
"word4 0x00010000".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
_ => (
|
||
|
|
"unknown".to_string(),
|
||
|
|
vec![format!(
|
||
|
|
"root=0x{root:08x}, words1..7={}, final=0x{final_flag:08x}",
|
||
|
|
words
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect::<Vec<_>>()
|
||
|
|
.join(", ")
|
||
|
|
)],
|
||
|
|
false,
|
||
|
|
),
|
||
|
|
};
|
||
|
|
|
||
|
|
SmpHeaderVariantProbe {
|
||
|
|
variant_family,
|
||
|
|
variant_evidence: evidence,
|
||
|
|
is_known_family,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn read_preamble_words(bytes: &[u8]) -> Option<[u32; PREAMBLE_U32_WORD_COUNT]> {
|
||
|
|
if bytes.len() < PREAMBLE_U32_WORD_COUNT * 4 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut words = [0u32; PREAMBLE_U32_WORD_COUNT];
|
||
|
|
for (index, chunk) in bytes[..PREAMBLE_U32_WORD_COUNT * 4]
|
||
|
|
.chunks_exact(4)
|
||
|
|
.enumerate()
|
||
|
|
{
|
||
|
|
words[index] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
||
|
|
}
|
||
|
|
Some(words)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn probe_early_content_layout(
|
||
|
|
bytes: &[u8],
|
||
|
|
ascii_run: &SmpAsciiPreview,
|
||
|
|
) -> Option<SmpEarlyContentProbe> {
|
||
|
|
let search_start = ascii_run.offset + ascii_run.byte_len;
|
||
|
|
let first_post_text_nonzero_offset = find_next_nonzero_offset(bytes, search_start)?;
|
||
|
|
let zero_pad_after_text_len = first_post_text_nonzero_offset.saturating_sub(search_start);
|
||
|
|
let first_zero_run_after_block = find_zero_run(
|
||
|
|
bytes,
|
||
|
|
first_post_text_nonzero_offset,
|
||
|
|
EARLY_ZERO_RUN_THRESHOLD,
|
||
|
|
)
|
||
|
|
.unwrap_or(bytes.len());
|
||
|
|
let first_post_text_block = &bytes[first_post_text_nonzero_offset..first_zero_run_after_block];
|
||
|
|
let secondary_nonzero_offset = find_next_nonzero_offset(bytes, first_zero_run_after_block);
|
||
|
|
let trailing_zero_pad_after_first_block_len = secondary_nonzero_offset
|
||
|
|
.map(|offset| offset.saturating_sub(first_zero_run_after_block))
|
||
|
|
.unwrap_or_else(|| bytes.len().saturating_sub(first_zero_run_after_block));
|
||
|
|
let secondary_aligned_word_window_offset = secondary_nonzero_offset.map(|offset| offset & !0x3);
|
||
|
|
let secondary_aligned_word_window_words = secondary_aligned_word_window_offset
|
||
|
|
.map(|offset| read_u32_window(bytes, offset, EARLY_ALIGNED_WORD_WINDOW_COUNT))
|
||
|
|
.unwrap_or_default();
|
||
|
|
let secondary_preview_hex = secondary_nonzero_offset
|
||
|
|
.map(|offset| {
|
||
|
|
hex_encode(&bytes[offset..bytes.len().min(offset + EARLY_PREVIEW_BYTE_LIMIT)])
|
||
|
|
})
|
||
|
|
.unwrap_or_default();
|
||
|
|
|
||
|
|
Some(SmpEarlyContentProbe {
|
||
|
|
first_post_text_nonzero_offset,
|
||
|
|
zero_pad_after_text_len,
|
||
|
|
first_post_text_block_len: first_post_text_block.len(),
|
||
|
|
first_post_text_block_hex: hex_encode(first_post_text_block),
|
||
|
|
trailing_zero_pad_after_first_block_len,
|
||
|
|
secondary_nonzero_offset,
|
||
|
|
secondary_aligned_word_window_offset,
|
||
|
|
secondary_aligned_word_window_hex_words: secondary_aligned_word_window_words
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect(),
|
||
|
|
secondary_aligned_word_window_words,
|
||
|
|
secondary_preview_hex,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn classify_secondary_variant_probe(
|
||
|
|
probe: &SmpEarlyContentProbe,
|
||
|
|
) -> Option<SmpSecondaryVariantProbe> {
|
||
|
|
let aligned_window_offset = probe.secondary_aligned_word_window_offset?;
|
||
|
|
let words = probe.secondary_aligned_word_window_words.clone();
|
||
|
|
if words.is_empty() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut evidence = Vec::new();
|
||
|
|
let variant_family = match words.as_slice() {
|
||
|
|
[0x001e0000, 0x86a00100, 0x03000001, 0xf0000100, ..] => {
|
||
|
|
evidence.push("leading word 0x001e0000".to_string());
|
||
|
|
evidence.push("anchor word 0x86a00100".to_string());
|
||
|
|
evidence.push("third/fourth words 0x03000001 and 0xf0000100".to_string());
|
||
|
|
"rt3-gms-family-v1".to_string()
|
||
|
|
}
|
||
|
|
[0x000a0000, 0x49f00100, 0x00000002, 0xa0000000, ..] => {
|
||
|
|
evidence.push("leading word 0x000a0000".to_string());
|
||
|
|
evidence.push("anchor word 0x49f00100".to_string());
|
||
|
|
evidence.push("third/fourth words 0x00000002 and 0xa0000000".to_string());
|
||
|
|
"rt3-gmx-family-v1".to_string()
|
||
|
|
}
|
||
|
|
[0x001c0000, 0x86a00100, 0x00000001, 0xa0000000, ..] => {
|
||
|
|
evidence.push("leading word 0x001c0000".to_string());
|
||
|
|
evidence.push("anchor word 0x86a00100".to_string());
|
||
|
|
evidence.push("third/fourth words 0x00000001 and 0xa0000000".to_string());
|
||
|
|
"rt3-105-gms-family-v1".to_string()
|
||
|
|
}
|
||
|
|
[0x00190000, 0x86a00100, 0x00000001, 0xa0000000, ..] => {
|
||
|
|
evidence.push("leading word 0x00190000".to_string());
|
||
|
|
evidence.push("anchor word 0x86a00100".to_string());
|
||
|
|
evidence.push("third/fourth words 0x00000001 and 0xa0000000".to_string());
|
||
|
|
"rt3-105-gmx-family-v1".to_string()
|
||
|
|
}
|
||
|
|
[0x00130000, 0x86a00100, 0x21000001, 0xa0000100, ..] => {
|
||
|
|
evidence.push("leading word 0x00130000".to_string());
|
||
|
|
evidence.push("anchor word 0x86a00100".to_string());
|
||
|
|
evidence.push("third/fourth words 0x21000001 and 0xa0000100".to_string());
|
||
|
|
"rt3-105-gms-scenario-family-v1".to_string()
|
||
|
|
}
|
||
|
|
[0x00010000, 0x49f00100, 0x00000002, 0xa0000000, ..] => {
|
||
|
|
evidence.push("leading word 0x00010000".to_string());
|
||
|
|
evidence.push("anchor word 0x49f00100".to_string());
|
||
|
|
evidence.push("third/fourth words 0x00000002 and 0xa0000000".to_string());
|
||
|
|
"rt3-105-gms-alt-family-v1".to_string()
|
||
|
|
}
|
||
|
|
[0x86a00100, 0x00000001, 0xa0000000, 0x00000186, ..] => {
|
||
|
|
evidence.push("window starts directly on 0x86a00100".to_string());
|
||
|
|
evidence.push("likely same family with missing leading unaligned word".to_string());
|
||
|
|
"rt3-family-unaligned-anchor".to_string()
|
||
|
|
}
|
||
|
|
_ => {
|
||
|
|
evidence.push(format!(
|
||
|
|
"unrecognized leading words: {}",
|
||
|
|
words
|
||
|
|
.iter()
|
||
|
|
.take(4)
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect::<Vec<_>>()
|
||
|
|
.join(", ")
|
||
|
|
));
|
||
|
|
"unknown".to_string()
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
Some(SmpSecondaryVariantProbe {
|
||
|
|
aligned_window_offset,
|
||
|
|
hex_words: words.iter().map(|word| format!("0x{word:08x}")).collect(),
|
||
|
|
words,
|
||
|
|
variant_family,
|
||
|
|
variant_evidence: evidence,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn classify_container_profile(
|
||
|
|
file_extension_hint: Option<&str>,
|
||
|
|
header_variant_probe: Option<&SmpHeaderVariantProbe>,
|
||
|
|
secondary_variant_probe: Option<&SmpSecondaryVariantProbe>,
|
||
|
|
) -> Option<SmpContainerProfile> {
|
||
|
|
let header_family = header_variant_probe.map(|probe| probe.variant_family.as_str())?;
|
||
|
|
let secondary_family = secondary_variant_probe.map(|probe| probe.variant_family.as_str())?;
|
||
|
|
let extension = file_extension_hint.unwrap_or("");
|
||
|
|
|
||
|
|
let (profile_family, profile_evidence, is_known_profile) =
|
||
|
|
match (extension, header_family, secondary_family) {
|
||
|
|
("gms", "rt3-classic-gms-header-v1", "rt3-gms-family-v1") => (
|
||
|
|
"rt3-classic-save-container-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"extension .gms".to_string(),
|
||
|
|
"classic save header family".to_string(),
|
||
|
|
"classic save secondary window family".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
("gmx", "rt3-classic-gmx-header-v1", "rt3-gmx-family-v1") => (
|
||
|
|
"rt3-classic-sandbox-container-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"extension .gmx".to_string(),
|
||
|
|
"classic sandbox header family".to_string(),
|
||
|
|
"classic sandbox secondary window family".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
("gms", "rt3-105-common-header-v1", "rt3-105-gms-family-v1") => (
|
||
|
|
"rt3-105-save-container-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"extension .gms".to_string(),
|
||
|
|
"1.05 common header family".to_string(),
|
||
|
|
"1.05 save secondary window family".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
("gms", "rt3-105-scenario-save-header-v1", "rt3-105-gms-scenario-family-v1") => (
|
||
|
|
"rt3-105-scenario-save-container-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"extension .gms".to_string(),
|
||
|
|
"1.05 scenario-save header family".to_string(),
|
||
|
|
"1.05 scenario-save secondary window family".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
("gms", "rt3-105-alt-save-header-v1", "rt3-105-gms-alt-family-v1") => (
|
||
|
|
"rt3-105-alt-save-container-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"extension .gms".to_string(),
|
||
|
|
"1.05 alternate-save header family".to_string(),
|
||
|
|
"1.05 alternate-save secondary window family".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
("gmx", "rt3-105-gmx-header-v1", "rt3-105-gmx-family-v1") => (
|
||
|
|
"rt3-105-sandbox-container-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"extension .gmx".to_string(),
|
||
|
|
"1.05 sandbox header family".to_string(),
|
||
|
|
"1.05 sandbox secondary window family".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
("gmp", "rt3-105-common-header-v1", "rt3-family-unaligned-anchor") => (
|
||
|
|
"rt3-105-map-container-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"extension .gmp".to_string(),
|
||
|
|
"1.05 common header family".to_string(),
|
||
|
|
"map-style secondary unaligned anchor".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
("gmp", "rt3-105-scenario-save-header-v1", "unknown") => (
|
||
|
|
"rt3-105-scenario-map-container-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"extension .gmp".to_string(),
|
||
|
|
"1.05 scenario-map header family".to_string(),
|
||
|
|
"fixed candidate-availability table range present despite unknown early secondary window".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
("gmp", "rt3-105-alt-save-header-v1", "unknown") => (
|
||
|
|
"rt3-105-alt-map-container-v1".to_string(),
|
||
|
|
vec![
|
||
|
|
"extension .gmp".to_string(),
|
||
|
|
"1.05 alternate-map header family".to_string(),
|
||
|
|
"fixed candidate-availability table range present despite unknown early secondary window".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
("gmp", "rt3-map-header-family", "rt3-family-unaligned-anchor") => (
|
||
|
|
"rt3-map-container-family".to_string(),
|
||
|
|
vec![
|
||
|
|
"extension .gmp".to_string(),
|
||
|
|
"map header family".to_string(),
|
||
|
|
"map-style secondary unaligned anchor".to_string(),
|
||
|
|
],
|
||
|
|
true,
|
||
|
|
),
|
||
|
|
(_, header_family, secondary_family) => (
|
||
|
|
"unknown".to_string(),
|
||
|
|
vec![
|
||
|
|
format!(
|
||
|
|
"extension {}",
|
||
|
|
if extension.is_empty() {
|
||
|
|
"<none>"
|
||
|
|
} else {
|
||
|
|
extension
|
||
|
|
}
|
||
|
|
),
|
||
|
|
format!("header family {header_family}"),
|
||
|
|
format!("secondary family {secondary_family}"),
|
||
|
|
],
|
||
|
|
false,
|
||
|
|
),
|
||
|
|
};
|
||
|
|
|
||
|
|
Some(SmpContainerProfile {
|
||
|
|
profile_family,
|
||
|
|
profile_evidence,
|
||
|
|
is_known_profile,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_save_bootstrap_block(
|
||
|
|
container_profile: Option<&SmpContainerProfile>,
|
||
|
|
secondary_variant_probe: Option<&SmpSecondaryVariantProbe>,
|
||
|
|
) -> Option<SmpSaveBootstrapBlock> {
|
||
|
|
let profile = container_profile?;
|
||
|
|
let secondary = secondary_variant_probe?;
|
||
|
|
let words = &secondary.words;
|
||
|
|
if words.len() < 8 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let supported = matches!(
|
||
|
|
profile.profile_family.as_str(),
|
||
|
|
"rt3-classic-save-container-v1"
|
||
|
|
| "rt3-105-save-container-v1"
|
||
|
|
| "rt3-105-scenario-save-container-v1"
|
||
|
|
| "rt3-105-alt-save-container-v1"
|
||
|
|
);
|
||
|
|
if !supported {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
Some(SmpSaveBootstrapBlock {
|
||
|
|
profile_family: profile.profile_family.clone(),
|
||
|
|
aligned_window_offset: secondary.aligned_window_offset,
|
||
|
|
leading_word: words[0],
|
||
|
|
leading_word_hex: format!("0x{:08x}", words[0]),
|
||
|
|
anchor_word: words[1],
|
||
|
|
anchor_word_hex: format!("0x{:08x}", words[1]),
|
||
|
|
descriptor_word_2: words[2],
|
||
|
|
descriptor_word_2_hex: format!("0x{:08x}", words[2]),
|
||
|
|
descriptor_word_3: words[3],
|
||
|
|
descriptor_word_3_hex: format!("0x{:08x}", words[3]),
|
||
|
|
descriptor_word_4: words[4],
|
||
|
|
descriptor_word_4_hex: format!("0x{:08x}", words[4]),
|
||
|
|
descriptor_word_5: words[5],
|
||
|
|
descriptor_word_5_hex: format!("0x{:08x}", words[5]),
|
||
|
|
descriptor_word_6: words[6],
|
||
|
|
descriptor_word_6_hex: format!("0x{:08x}", words[6]),
|
||
|
|
descriptor_word_7: words[7],
|
||
|
|
descriptor_word_7_hex: format!("0x{:08x}", words[7]),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_runtime_anchor_cycle_block(
|
||
|
|
bytes: &[u8],
|
||
|
|
container_profile: Option<&SmpContainerProfile>,
|
||
|
|
secondary_variant_probe: Option<&SmpSecondaryVariantProbe>,
|
||
|
|
) -> Option<SmpRuntimeAnchorCycleBlock> {
|
||
|
|
let profile = container_profile?;
|
||
|
|
let secondary = secondary_variant_probe?;
|
||
|
|
let supported = matches!(
|
||
|
|
profile.profile_family.as_str(),
|
||
|
|
"rt3-classic-save-container-v1"
|
||
|
|
| "rt3-classic-sandbox-container-v1"
|
||
|
|
| "rt3-105-save-container-v1"
|
||
|
|
| "rt3-105-scenario-save-container-v1"
|
||
|
|
| "rt3-105-alt-save-container-v1"
|
||
|
|
| "rt3-105-sandbox-container-v1"
|
||
|
|
);
|
||
|
|
if !supported {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let cycle_start_offset = secondary.aligned_window_offset + 0x1c;
|
||
|
|
let cycle_words = read_u32_window(bytes, cycle_start_offset, 9);
|
||
|
|
if cycle_words.len() < 9 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut full_cycle_count = 0usize;
|
||
|
|
let mut cursor = cycle_start_offset;
|
||
|
|
while read_u32_window(bytes, cursor, cycle_words.len()) == cycle_words {
|
||
|
|
full_cycle_count += 1;
|
||
|
|
cursor += cycle_words.len() * 4;
|
||
|
|
}
|
||
|
|
|
||
|
|
if full_cycle_count == 0 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut partial_cycle_word_count = 0usize;
|
||
|
|
while partial_cycle_word_count < cycle_words.len() {
|
||
|
|
let offset = cursor + partial_cycle_word_count * 4;
|
||
|
|
if read_u32_at(bytes, offset) == Some(cycle_words[partial_cycle_word_count]) {
|
||
|
|
partial_cycle_word_count += 1;
|
||
|
|
} else {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let trailer_offset = cursor + partial_cycle_word_count * 4;
|
||
|
|
let trailer_words = read_u32_window(bytes, trailer_offset, 16);
|
||
|
|
|
||
|
|
Some(SmpRuntimeAnchorCycleBlock {
|
||
|
|
profile_family: profile.profile_family.clone(),
|
||
|
|
cycle_start_offset,
|
||
|
|
cycle_hex_words: cycle_words
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect(),
|
||
|
|
cycle_words,
|
||
|
|
full_cycle_count,
|
||
|
|
partial_cycle_word_count,
|
||
|
|
trailer_offset,
|
||
|
|
trailer_hex_words: trailer_words
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect(),
|
||
|
|
trailer_words,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_save_anchor_run_block(
|
||
|
|
bytes: &[u8],
|
||
|
|
container_profile: Option<&SmpContainerProfile>,
|
||
|
|
save_bootstrap_block: Option<&SmpSaveBootstrapBlock>,
|
||
|
|
) -> Option<SmpSaveAnchorRunBlock> {
|
||
|
|
let profile = container_profile?;
|
||
|
|
let bootstrap = save_bootstrap_block?;
|
||
|
|
let supported = matches!(
|
||
|
|
profile.profile_family.as_str(),
|
||
|
|
"rt3-classic-save-container-v1"
|
||
|
|
| "rt3-105-save-container-v1"
|
||
|
|
| "rt3-105-scenario-save-container-v1"
|
||
|
|
| "rt3-105-alt-save-container-v1"
|
||
|
|
);
|
||
|
|
if !supported {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let cycle_start_offset = bootstrap.aligned_window_offset + 0x1c;
|
||
|
|
let cycle_words = read_u32_window(bytes, cycle_start_offset, 9);
|
||
|
|
if cycle_words.len() < 9 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut full_cycle_count = 0usize;
|
||
|
|
let mut cursor = cycle_start_offset;
|
||
|
|
while read_u32_window(bytes, cursor, cycle_words.len()) == cycle_words {
|
||
|
|
full_cycle_count += 1;
|
||
|
|
cursor += cycle_words.len() * 4;
|
||
|
|
}
|
||
|
|
|
||
|
|
if full_cycle_count == 0 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut partial_cycle_word_count = 0usize;
|
||
|
|
while partial_cycle_word_count < cycle_words.len() {
|
||
|
|
let offset = cursor + partial_cycle_word_count * 4;
|
||
|
|
if read_u32_at(bytes, offset) == Some(cycle_words[partial_cycle_word_count]) {
|
||
|
|
partial_cycle_word_count += 1;
|
||
|
|
} else {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let trailer_offset = cursor + partial_cycle_word_count * 4;
|
||
|
|
let trailer_words = read_u32_window(bytes, trailer_offset, 12);
|
||
|
|
|
||
|
|
Some(SmpSaveAnchorRunBlock {
|
||
|
|
profile_family: profile.profile_family.clone(),
|
||
|
|
cycle_start_offset,
|
||
|
|
cycle_hex_words: cycle_words
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect(),
|
||
|
|
cycle_words,
|
||
|
|
full_cycle_count,
|
||
|
|
partial_cycle_word_count,
|
||
|
|
trailer_offset,
|
||
|
|
trailer_hex_words: trailer_words
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect(),
|
||
|
|
trailer_words,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_runtime_trailer_block(
|
||
|
|
container_profile: Option<&SmpContainerProfile>,
|
||
|
|
runtime_anchor_cycle_block: Option<&SmpRuntimeAnchorCycleBlock>,
|
||
|
|
) -> Option<SmpRuntimeTrailerBlock> {
|
||
|
|
let profile = container_profile?;
|
||
|
|
let anchor = runtime_anchor_cycle_block?;
|
||
|
|
let words = &anchor.trailer_words;
|
||
|
|
if words.len() < 16 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let trailer_family = match profile.profile_family.as_str() {
|
||
|
|
"rt3-classic-save-container-v1"
|
||
|
|
if words[..6]
|
||
|
|
== [
|
||
|
|
0x00020000, 0x00030000, 0x00010000, 0x00010000, 0x00010000, 0x00020000,
|
||
|
|
] =>
|
||
|
|
{
|
||
|
|
"rt3-classic-save-trailer-v1"
|
||
|
|
}
|
||
|
|
"rt3-classic-sandbox-container-v1"
|
||
|
|
if words[..6]
|
||
|
|
== [
|
||
|
|
0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000,
|
||
|
|
] =>
|
||
|
|
{
|
||
|
|
"rt3-classic-sandbox-trailer-v1"
|
||
|
|
}
|
||
|
|
"rt3-105-save-container-v1"
|
||
|
|
| "rt3-105-scenario-save-container-v1"
|
||
|
|
| "rt3-105-alt-save-container-v1"
|
||
|
|
if words[..6]
|
||
|
|
== [
|
||
|
|
0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000,
|
||
|
|
] =>
|
||
|
|
{
|
||
|
|
"rt3-105-save-trailer-v1"
|
||
|
|
}
|
||
|
|
"rt3-105-sandbox-container-v1"
|
||
|
|
if words[..6]
|
||
|
|
== [
|
||
|
|
0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000,
|
||
|
|
] =>
|
||
|
|
{
|
||
|
|
"rt3-105-sandbox-trailer-v1"
|
||
|
|
}
|
||
|
|
_ => "unknown",
|
||
|
|
}
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
let tag_chunk_id_u16 = (words[6] >> 16) as u16;
|
||
|
|
let length_high_u16 = (words[7] >> 16) as u16;
|
||
|
|
let selector_high_u16 = (words[8] >> 16) as u16;
|
||
|
|
let descriptor_high_u16 = (words[10] >> 16) as u16;
|
||
|
|
let tag_chunk_id_grounded_alignment =
|
||
|
|
classify_runtime_trailer_chunk_id_grounded_alignment(tag_chunk_id_u16).map(str::to_string);
|
||
|
|
|
||
|
|
let mut trailer_evidence = vec![
|
||
|
|
format!("container profile {}", profile.profile_family),
|
||
|
|
format!(
|
||
|
|
"prefix words {}",
|
||
|
|
words[..6]
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect::<Vec<_>>()
|
||
|
|
.join(", ")
|
||
|
|
),
|
||
|
|
format!("high-16 chunk id 0x{tag_chunk_id_u16:04x} from trailer word 6"),
|
||
|
|
format!("high-16 span 0x{length_high_u16:04x} from trailer word 7"),
|
||
|
|
format!("high-16 selector 0x{selector_high_u16:04x} from trailer word 8"),
|
||
|
|
format!("high-16 descriptor 0x{descriptor_high_u16:04x} from trailer word 10"),
|
||
|
|
];
|
||
|
|
if let Some(alignment) = &tag_chunk_id_grounded_alignment {
|
||
|
|
trailer_evidence.push(alignment.clone());
|
||
|
|
}
|
||
|
|
|
||
|
|
Some(SmpRuntimeTrailerBlock {
|
||
|
|
profile_family: profile.profile_family.clone(),
|
||
|
|
trailer_family,
|
||
|
|
trailer_evidence,
|
||
|
|
trailer_offset: anchor.trailer_offset,
|
||
|
|
prefix_words_0_to_5: words[..6].to_vec(),
|
||
|
|
prefix_hex_words_0_to_5: words[..6]
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect(),
|
||
|
|
tag_word_6: words[6],
|
||
|
|
tag_word_6_hex: format!("0x{:08x}", words[6]),
|
||
|
|
tag_chunk_id_u16,
|
||
|
|
tag_chunk_id_hex: format!("0x{tag_chunk_id_u16:04x}"),
|
||
|
|
tag_chunk_id_grounded_alignment,
|
||
|
|
length_word_7: words[7],
|
||
|
|
length_word_7_hex: format!("0x{:08x}", words[7]),
|
||
|
|
length_high_u16,
|
||
|
|
length_high_hex: format!("0x{length_high_u16:04x}"),
|
||
|
|
selector_word_8: words[8],
|
||
|
|
selector_word_8_hex: format!("0x{:08x}", words[8]),
|
||
|
|
selector_high_u16,
|
||
|
|
selector_high_hex: format!("0x{selector_high_u16:04x}"),
|
||
|
|
layout_word_9: words[9],
|
||
|
|
layout_word_9_hex: format!("0x{:08x}", words[9]),
|
||
|
|
descriptor_word_10: words[10],
|
||
|
|
descriptor_word_10_hex: format!("0x{:08x}", words[10]),
|
||
|
|
descriptor_high_u16,
|
||
|
|
descriptor_high_hex: format!("0x{descriptor_high_u16:04x}"),
|
||
|
|
descriptor_word_11: words[11],
|
||
|
|
descriptor_word_11_hex: format!("0x{:08x}", words[11]),
|
||
|
|
counter_word_12: words[12],
|
||
|
|
counter_word_12_hex: format!("0x{:08x}", words[12]),
|
||
|
|
offset_word_13: words[13],
|
||
|
|
offset_word_13_hex: format!("0x{:08x}", words[13]),
|
||
|
|
span_word_14: words[14],
|
||
|
|
span_word_14_hex: format!("0x{:08x}", words[14]),
|
||
|
|
mode_word_15: words[15],
|
||
|
|
mode_word_15_hex: format!("0x{:08x}", words[15]),
|
||
|
|
words: words.to_vec(),
|
||
|
|
hex_words: words.iter().map(|word| format!("0x{word:08x}")).collect(),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn classify_runtime_trailer_chunk_id_grounded_alignment(
|
||
|
|
tag_chunk_id_u16: u16,
|
||
|
|
) -> Option<&'static str> {
|
||
|
|
match tag_chunk_id_u16 {
|
||
|
|
0x2ee1 => Some(
|
||
|
|
"High-16 chunk id 0x2ee1 matches the disassembly-grounded map-style bundle family already read by shell_setup_load_selected_profile_bundle_into_payload_record.",
|
||
|
|
),
|
||
|
|
_ => None,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_runtime_post_span_probe(
|
||
|
|
bytes: &[u8],
|
||
|
|
runtime_trailer_block: Option<&SmpRuntimeTrailerBlock>,
|
||
|
|
) -> Option<SmpRuntimePostSpanProbe> {
|
||
|
|
let trailer = runtime_trailer_block?;
|
||
|
|
let span_target_offset = trailer.trailer_offset + trailer.length_high_u16 as usize;
|
||
|
|
let next_nonzero_offset = find_next_nonzero_offset(bytes, span_target_offset);
|
||
|
|
let header_candidates =
|
||
|
|
collect_runtime_post_span_header_candidates(bytes, span_target_offset, 0x8000);
|
||
|
|
let next_aligned_candidate_offset = header_candidates.first().map(|candidate| candidate.offset);
|
||
|
|
let next_aligned_candidate_words = header_candidates
|
||
|
|
.first()
|
||
|
|
.map(|candidate| candidate.words.clone())
|
||
|
|
.unwrap_or_default();
|
||
|
|
let grounded_progress_hits =
|
||
|
|
find_grounded_progress_high16_hits(bytes, span_target_offset, 0x8000);
|
||
|
|
|
||
|
|
Some(SmpRuntimePostSpanProbe {
|
||
|
|
profile_family: trailer.profile_family.clone(),
|
||
|
|
span_target_offset,
|
||
|
|
next_nonzero_offset,
|
||
|
|
next_aligned_candidate_offset,
|
||
|
|
next_aligned_candidate_hex_words: next_aligned_candidate_words
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect(),
|
||
|
|
next_aligned_candidate_words,
|
||
|
|
header_candidates,
|
||
|
|
grounded_progress_hits,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_classic_rehydrate_profile_probe(
|
||
|
|
bytes: &[u8],
|
||
|
|
runtime_post_span_probe: Option<&SmpRuntimePostSpanProbe>,
|
||
|
|
) -> Option<SmpClassicRehydrateProfileProbe> {
|
||
|
|
let post_span = runtime_post_span_probe?;
|
||
|
|
if post_span.profile_family != "rt3-classic-save-container-v1" {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let progress_32dc_offset =
|
||
|
|
parse_grounded_progress_hit_offset(&post_span.grounded_progress_hits, 0x32dc)?;
|
||
|
|
let progress_3714_offset =
|
||
|
|
parse_grounded_progress_hit_offset(&post_span.grounded_progress_hits, 0x3714)?;
|
||
|
|
let progress_3715_offset =
|
||
|
|
parse_grounded_progress_hit_offset(&post_span.grounded_progress_hits, 0x3715)?;
|
||
|
|
let packed_profile_offset = progress_3714_offset + 4;
|
||
|
|
let packed_profile_len = progress_3715_offset.checked_sub(packed_profile_offset)?;
|
||
|
|
if packed_profile_len != 0x108 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let ascii_runs =
|
||
|
|
collect_ascii_previews_in_range(bytes, packed_profile_offset, progress_3715_offset, 4);
|
||
|
|
let packed_profile_block =
|
||
|
|
parse_classic_packed_profile_block(bytes, packed_profile_offset, packed_profile_len)?;
|
||
|
|
|
||
|
|
Some(SmpClassicRehydrateProfileProbe {
|
||
|
|
profile_family: post_span.profile_family.clone(),
|
||
|
|
progress_32dc_offset,
|
||
|
|
progress_3714_offset,
|
||
|
|
progress_3715_offset,
|
||
|
|
packed_profile_offset,
|
||
|
|
packed_profile_len,
|
||
|
|
packed_profile_len_hex: format!("0x{packed_profile_len:03x}"),
|
||
|
|
packed_profile_block,
|
||
|
|
ascii_runs,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_classic_packed_profile_block(
|
||
|
|
bytes: &[u8],
|
||
|
|
packed_profile_offset: usize,
|
||
|
|
packed_profile_len: usize,
|
||
|
|
) -> Option<SmpClassicPackedProfileBlock> {
|
||
|
|
let block_end = packed_profile_offset.checked_add(packed_profile_len)?;
|
||
|
|
if block_end > bytes.len() || packed_profile_len != 0x108 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let leading_word_0 = read_u32_at(bytes, packed_profile_offset)?;
|
||
|
|
let trailing_zero_word_count_after_leading_word = (1..4)
|
||
|
|
.take_while(|index| {
|
||
|
|
read_u32_at(bytes, packed_profile_offset + (index * 4)).is_some_and(|word| word == 0)
|
||
|
|
})
|
||
|
|
.count();
|
||
|
|
let map_path_offset = 0x13;
|
||
|
|
let display_name_offset = 0x46;
|
||
|
|
let stable_nonzero_word_offsets = [0x00usize, 0x10, 0x78, 0x7c, 0x84, 0x88];
|
||
|
|
let stable_nonzero_words = stable_nonzero_word_offsets
|
||
|
|
.iter()
|
||
|
|
.filter_map(|relative_offset| {
|
||
|
|
let value = read_u32_at(bytes, packed_profile_offset + relative_offset)?;
|
||
|
|
if value == 0 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
Some(SmpPackedProfileWordLane {
|
||
|
|
relative_offset: *relative_offset,
|
||
|
|
relative_offset_hex: format!("0x{relative_offset:02x}"),
|
||
|
|
value,
|
||
|
|
value_hex: format!("0x{value:08x}"),
|
||
|
|
})
|
||
|
|
})
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
|
||
|
|
Some(SmpClassicPackedProfileBlock {
|
||
|
|
relative_len: packed_profile_len,
|
||
|
|
relative_len_hex: format!("0x{packed_profile_len:03x}"),
|
||
|
|
leading_word_0,
|
||
|
|
leading_word_0_hex: format!("0x{leading_word_0:08x}"),
|
||
|
|
trailing_zero_word_count_after_leading_word,
|
||
|
|
map_path_offset,
|
||
|
|
map_path: read_c_string_in_range(bytes, packed_profile_offset + map_path_offset, block_end),
|
||
|
|
display_name_offset,
|
||
|
|
display_name: read_c_string_in_range(
|
||
|
|
bytes,
|
||
|
|
packed_profile_offset + display_name_offset,
|
||
|
|
block_end,
|
||
|
|
),
|
||
|
|
profile_byte_0x77: bytes[packed_profile_offset + 0x77],
|
||
|
|
profile_byte_0x77_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x77]),
|
||
|
|
profile_byte_0x82: bytes[packed_profile_offset + 0x82],
|
||
|
|
profile_byte_0x82_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x82]),
|
||
|
|
profile_byte_0x97: bytes[packed_profile_offset + 0x97],
|
||
|
|
profile_byte_0x97_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x97]),
|
||
|
|
profile_byte_0xc5: bytes[packed_profile_offset + 0xc5],
|
||
|
|
profile_byte_0xc5_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0xc5]),
|
||
|
|
stable_nonzero_words,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_rt3_105_packed_profile_probe(
|
||
|
|
bytes: &[u8],
|
||
|
|
file_extension_hint: Option<&str>,
|
||
|
|
header_variant_probe: Option<&SmpHeaderVariantProbe>,
|
||
|
|
container_profile: Option<&SmpContainerProfile>,
|
||
|
|
) -> Option<SmpRt3105PackedProfileProbe> {
|
||
|
|
let profile_family = if container_profile.is_some_and(|profile| {
|
||
|
|
matches!(
|
||
|
|
profile.profile_family.as_str(),
|
||
|
|
"rt3-105-save-container-v1"
|
||
|
|
| "rt3-105-scenario-save-container-v1"
|
||
|
|
| "rt3-105-alt-save-container-v1"
|
||
|
|
)
|
||
|
|
}) {
|
||
|
|
container_profile
|
||
|
|
.expect("checked above")
|
||
|
|
.profile_family
|
||
|
|
.clone()
|
||
|
|
} else if file_extension_hint == Some("gms")
|
||
|
|
&& header_variant_probe.is_some_and(|probe| {
|
||
|
|
matches!(
|
||
|
|
probe.variant_family.as_str(),
|
||
|
|
"rt3-105-common-header-v1"
|
||
|
|
| "rt3-105-scenario-save-header-v1"
|
||
|
|
| "rt3-105-alt-save-header-v1"
|
||
|
|
| "rt3-map-header-family"
|
||
|
|
)
|
||
|
|
})
|
||
|
|
{
|
||
|
|
"rt3-105-save-analog-block-inferred".to_string()
|
||
|
|
} else {
|
||
|
|
return None;
|
||
|
|
};
|
||
|
|
|
||
|
|
if file_extension_hint != Some("gms") {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let map_path_offset = find_c_string_with_suffix_in_range(bytes, 0x7000, 0x9000, ".gmp")?;
|
||
|
|
let packed_profile_offset = map_path_offset.checked_sub(0x10)?;
|
||
|
|
let packed_profile_len = 0x108usize;
|
||
|
|
let block_end = packed_profile_offset.checked_add(packed_profile_len)?;
|
||
|
|
if block_end > bytes.len() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let packed_profile_block =
|
||
|
|
parse_rt3_105_packed_profile_block(bytes, packed_profile_offset, packed_profile_len)?;
|
||
|
|
let ascii_runs = collect_ascii_previews_in_range(bytes, packed_profile_offset, block_end, 4);
|
||
|
|
|
||
|
|
Some(SmpRt3105PackedProfileProbe {
|
||
|
|
profile_family,
|
||
|
|
packed_profile_offset,
|
||
|
|
packed_profile_len,
|
||
|
|
packed_profile_len_hex: format!("0x{packed_profile_len:03x}"),
|
||
|
|
packed_profile_block,
|
||
|
|
ascii_runs,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_rt3_105_post_span_bridge_probe(
|
||
|
|
runtime_trailer_block: Option<&SmpRuntimeTrailerBlock>,
|
||
|
|
runtime_post_span_probe: Option<&SmpRuntimePostSpanProbe>,
|
||
|
|
rt3_105_packed_profile_probe: Option<&SmpRt3105PackedProfileProbe>,
|
||
|
|
) -> Option<SmpRt3105PostSpanBridgeProbe> {
|
||
|
|
let trailer = runtime_trailer_block?;
|
||
|
|
let post_span = runtime_post_span_probe?;
|
||
|
|
let packed_profile = rt3_105_packed_profile_probe?;
|
||
|
|
let supported = matches!(
|
||
|
|
trailer.profile_family.as_str(),
|
||
|
|
"rt3-105-save-container-v1"
|
||
|
|
| "rt3-105-scenario-save-container-v1"
|
||
|
|
| "rt3-105-alt-save-container-v1"
|
||
|
|
| "rt3-105-save-analog-block-inferred"
|
||
|
|
);
|
||
|
|
if !supported || trailer.profile_family != post_span.profile_family {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let next_candidate_high_u16_words = post_span
|
||
|
|
.header_candidates
|
||
|
|
.first()
|
||
|
|
.map(|candidate| candidate.high_u16_words.clone())
|
||
|
|
.unwrap_or_default();
|
||
|
|
let next_candidate_high_hex_words = next_candidate_high_u16_words
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:04x}"))
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
let next_candidate_offset = post_span.next_aligned_candidate_offset;
|
||
|
|
let next_candidate_delta_from_span_target =
|
||
|
|
next_candidate_offset.and_then(|offset| offset.checked_sub(post_span.span_target_offset));
|
||
|
|
let packed_profile_delta_from_span_target = packed_profile
|
||
|
|
.packed_profile_offset
|
||
|
|
.checked_sub(post_span.span_target_offset)?;
|
||
|
|
let next_candidate_delta_from_packed_profile = next_candidate_offset
|
||
|
|
.map(|offset| offset as i64 - packed_profile.packed_profile_offset as i64);
|
||
|
|
|
||
|
|
let mut bridge_evidence = vec![
|
||
|
|
format!("profile family {}", trailer.profile_family),
|
||
|
|
format!("selector high {}", trailer.selector_high_hex),
|
||
|
|
format!("descriptor high {}", trailer.descriptor_high_hex),
|
||
|
|
format!(
|
||
|
|
"packed profile sits +0x{packed_profile_delta_from_span_target:x} from span target"
|
||
|
|
),
|
||
|
|
];
|
||
|
|
if let Some(delta) = next_candidate_delta_from_span_target {
|
||
|
|
bridge_evidence.push(format!("next candidate sits +0x{delta:x} from span target"));
|
||
|
|
}
|
||
|
|
if let Some(delta) = next_candidate_delta_from_packed_profile {
|
||
|
|
bridge_evidence.push(format!(
|
||
|
|
"next candidate is {delta:+#x} relative to packed profile"
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
let bridge_family = match (
|
||
|
|
trailer.selector_high_u16,
|
||
|
|
trailer.descriptor_high_u16,
|
||
|
|
next_candidate_high_u16_words.as_slice(),
|
||
|
|
) {
|
||
|
|
(0x7110, 0x7801 | 0x7401, [0x6200, 0x0000, 0xfff7, 0x5515, ..]) => {
|
||
|
|
bridge_evidence.push(format!(
|
||
|
|
"selector/descriptor pair 0x7110 -> 0x{:04x}",
|
||
|
|
trailer.descriptor_high_u16
|
||
|
|
));
|
||
|
|
bridge_evidence.push(
|
||
|
|
"next candidate begins with high-16 lanes 0x6200/0x0000/0xfff7/0x5515"
|
||
|
|
.to_string(),
|
||
|
|
);
|
||
|
|
"rt3-105-save-post-span-bridge-v1"
|
||
|
|
}
|
||
|
|
(0x54cd, 0x5901, [0x1500, 0x0100, 0x4100, 0x0200, ..]) => {
|
||
|
|
bridge_evidence.push("selector/descriptor pair 0x54cd -> 0x5901".to_string());
|
||
|
|
bridge_evidence.push(
|
||
|
|
"next candidate begins with high-16 lanes 0x1500/0x0100/0x4100/0x0200"
|
||
|
|
.to_string(),
|
||
|
|
);
|
||
|
|
"rt3-105-alt-save-post-span-bridge-v1"
|
||
|
|
}
|
||
|
|
(0x0001, 0x0186, [0x0186, 0x0006, 0x0006, 0x0001, ..]) => {
|
||
|
|
bridge_evidence.push("selector/descriptor pair 0x0001 -> 0x0186".to_string());
|
||
|
|
bridge_evidence.push(
|
||
|
|
"next candidate remains in the local cycle neighborhood with 0x0186/0x0006/0x0006/0x0001"
|
||
|
|
.to_string(),
|
||
|
|
);
|
||
|
|
"rt3-105-scenario-post-span-bridge-v1"
|
||
|
|
}
|
||
|
|
_ => "unknown",
|
||
|
|
}
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
Some(SmpRt3105PostSpanBridgeProbe {
|
||
|
|
profile_family: trailer.profile_family.clone(),
|
||
|
|
bridge_family,
|
||
|
|
bridge_evidence,
|
||
|
|
span_target_offset: post_span.span_target_offset,
|
||
|
|
next_candidate_offset,
|
||
|
|
next_candidate_delta_from_span_target,
|
||
|
|
packed_profile_offset: packed_profile.packed_profile_offset,
|
||
|
|
packed_profile_delta_from_span_target,
|
||
|
|
next_candidate_delta_from_packed_profile,
|
||
|
|
selector_high_u16: trailer.selector_high_u16,
|
||
|
|
selector_high_hex: trailer.selector_high_hex.clone(),
|
||
|
|
descriptor_high_u16: trailer.descriptor_high_u16,
|
||
|
|
descriptor_high_hex: trailer.descriptor_high_hex.clone(),
|
||
|
|
next_candidate_high_u16_words,
|
||
|
|
next_candidate_high_hex_words,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_rt3_105_save_bridge_payload_probe(
|
||
|
|
bytes: &[u8],
|
||
|
|
bridge_probe: Option<&SmpRt3105PostSpanBridgeProbe>,
|
||
|
|
) -> Option<SmpRt3105SaveBridgePayloadProbe> {
|
||
|
|
let bridge = bridge_probe?;
|
||
|
|
if bridge.bridge_family != "rt3-105-save-post-span-bridge-v1" {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let primary_block_offset = bridge.next_candidate_offset?;
|
||
|
|
let primary_block_word_count = 8usize;
|
||
|
|
let primary_words = read_u32_window(bytes, primary_block_offset, primary_block_word_count);
|
||
|
|
if primary_words.len() < primary_block_word_count {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let secondary_block_delta_from_primary = 0x1808usize;
|
||
|
|
let secondary_block_offset = primary_block_offset + secondary_block_delta_from_primary;
|
||
|
|
let secondary_block_end_offset = bridge.packed_profile_offset;
|
||
|
|
let secondary_block_len = secondary_block_end_offset.checked_sub(secondary_block_offset)?;
|
||
|
|
let secondary_preview_word_count = 32usize;
|
||
|
|
let secondary_words =
|
||
|
|
read_u32_window(bytes, secondary_block_offset, secondary_preview_word_count);
|
||
|
|
if secondary_words.len() < secondary_preview_word_count {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let primary_signature_matches = primary_words
|
||
|
|
== [
|
||
|
|
0x62000000, 0x00000000, 0xfff70000, 0x55150000, 0x55550000, 0x00000000, 0xfff70000,
|
||
|
|
0x54550000,
|
||
|
|
];
|
||
|
|
let secondary_prefix_matches = secondary_words.starts_with(&[
|
||
|
|
0x00050000, 0x00050005, 0xfff70000, 0x54540000, 0x545400f9, 0x00f900f9, 0x00f94008,
|
||
|
|
0x00001555,
|
||
|
|
]);
|
||
|
|
|
||
|
|
let mut evidence = vec![
|
||
|
|
"bridge family rt3-105-save-post-span-bridge-v1".to_string(),
|
||
|
|
format!("primary block offset 0x{primary_block_offset:08x}"),
|
||
|
|
format!("secondary block offset 0x{secondary_block_offset:08x}"),
|
||
|
|
format!("secondary block delta from primary 0x{secondary_block_delta_from_primary:x}"),
|
||
|
|
format!("secondary block end offset 0x{secondary_block_end_offset:08x}"),
|
||
|
|
format!("secondary block span 0x{secondary_block_len:x} bytes"),
|
||
|
|
];
|
||
|
|
if primary_signature_matches {
|
||
|
|
evidence.push(
|
||
|
|
"primary 8-word bridge block matches the observed 0x6200/0xfff7/0x5515/0x5555 spine"
|
||
|
|
.to_string(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if secondary_prefix_matches {
|
||
|
|
evidence.push(
|
||
|
|
"secondary preview matches the observed 0x0005/0xfff7/0x5454 dense block prefix"
|
||
|
|
.to_string(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Some(SmpRt3105SaveBridgePayloadProbe {
|
||
|
|
profile_family: bridge.profile_family.clone(),
|
||
|
|
bridge_family: bridge.bridge_family.clone(),
|
||
|
|
primary_block_offset,
|
||
|
|
primary_block_len: primary_block_word_count * 4,
|
||
|
|
primary_block_len_hex: format!("0x{:02x}", primary_block_word_count * 4),
|
||
|
|
primary_hex_words: primary_words
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect(),
|
||
|
|
primary_words,
|
||
|
|
secondary_block_offset,
|
||
|
|
secondary_block_delta_from_primary,
|
||
|
|
secondary_block_delta_from_primary_hex: format!("0x{secondary_block_delta_from_primary:x}"),
|
||
|
|
secondary_block_end_offset,
|
||
|
|
secondary_block_len,
|
||
|
|
secondary_block_len_hex: format!("0x{secondary_block_len:x}"),
|
||
|
|
secondary_preview_word_count,
|
||
|
|
secondary_hex_words: secondary_words
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect(),
|
||
|
|
secondary_words,
|
||
|
|
evidence,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_rt3_105_save_name_table_probe(
|
||
|
|
bytes: &[u8],
|
||
|
|
file_extension_hint: Option<&str>,
|
||
|
|
container_profile: Option<&SmpContainerProfile>,
|
||
|
|
bridge_payload_probe: Option<&SmpRt3105SaveBridgePayloadProbe>,
|
||
|
|
) -> Option<SmpRt3105SaveNameTableProbe> {
|
||
|
|
let (
|
||
|
|
profile_family,
|
||
|
|
source_kind,
|
||
|
|
header_offset,
|
||
|
|
entries_offset,
|
||
|
|
block_end_offset,
|
||
|
|
mut evidence,
|
||
|
|
) = if let Some(payload) = bridge_payload_probe {
|
||
|
|
(
|
||
|
|
payload.profile_family.clone(),
|
||
|
|
"save-bridge-secondary-block".to_string(),
|
||
|
|
payload.secondary_block_offset + 0x354,
|
||
|
|
payload.secondary_block_offset + 0x3b5,
|
||
|
|
payload.secondary_block_end_offset,
|
||
|
|
vec![
|
||
|
|
"common-save bridge payload branch".to_string(),
|
||
|
|
format!(
|
||
|
|
"secondary block span 0x{:x}..0x{:x}",
|
||
|
|
payload.secondary_block_offset, payload.secondary_block_end_offset
|
||
|
|
),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
} else {
|
||
|
|
let profile_family = container_profile
|
||
|
|
.map(|profile| profile.profile_family.clone())
|
||
|
|
.unwrap_or_else(|| "unknown".to_string());
|
||
|
|
let extension = file_extension_hint.unwrap_or("");
|
||
|
|
let source_kind = match extension {
|
||
|
|
"gmp" => "map-fixed-catalog-range",
|
||
|
|
"gms" => "save-fixed-catalog-range",
|
||
|
|
"gmx" => "sandbox-fixed-catalog-range",
|
||
|
|
_ => "fixed-catalog-range",
|
||
|
|
}
|
||
|
|
.to_string();
|
||
|
|
(
|
||
|
|
profile_family,
|
||
|
|
source_kind,
|
||
|
|
0x6a70,
|
||
|
|
0x6ad1,
|
||
|
|
0x73c0,
|
||
|
|
vec![
|
||
|
|
"fixed catalog range branch".to_string(),
|
||
|
|
"using observed shared 1.05 candidate-availability table offsets".to_string(),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
};
|
||
|
|
let entry_stride = 0x22usize;
|
||
|
|
if block_end_offset > bytes.len() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
if !matches_candidate_availability_table_header(bytes, header_offset) {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
let observed_entry_capacity = read_u32_at(bytes, header_offset + 0x1c)? as usize;
|
||
|
|
let observed_entry_count = read_u32_at(bytes, header_offset + 0x20)? as usize;
|
||
|
|
let entries_len = observed_entry_count.checked_mul(entry_stride)?;
|
||
|
|
let entries_end_offset = entries_offset.checked_add(entries_len)?;
|
||
|
|
if observed_entry_count == 0 || observed_entry_capacity < observed_entry_count {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
if entries_end_offset > block_end_offset || entries_end_offset > bytes.len() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut entries = Vec::with_capacity(observed_entry_count);
|
||
|
|
for index in 0..observed_entry_count {
|
||
|
|
let offset = entries_offset + index * entry_stride;
|
||
|
|
let chunk = &bytes[offset..offset + entry_stride];
|
||
|
|
let nul_index = chunk
|
||
|
|
.iter()
|
||
|
|
.position(|byte| *byte == 0)
|
||
|
|
.unwrap_or(entry_stride);
|
||
|
|
let text = std::str::from_utf8(&chunk[..nul_index]).ok()?.to_string();
|
||
|
|
let trailer_word = read_u32_at(bytes, offset + entry_stride - 4)?;
|
||
|
|
entries.push(SmpRt3105SaveNameTableEntry {
|
||
|
|
index,
|
||
|
|
offset,
|
||
|
|
text,
|
||
|
|
availability_dword: trailer_word,
|
||
|
|
availability_dword_hex: format!("0x{trailer_word:08x}"),
|
||
|
|
trailer_word,
|
||
|
|
trailer_word_hex: format!("0x{trailer_word:08x}"),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
let zero_trailer_entry_names = entries
|
||
|
|
.iter()
|
||
|
|
.filter(|entry| entry.trailer_word == 0)
|
||
|
|
.map(|entry| entry.text.clone())
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
let zero_trailer_entry_count = zero_trailer_entry_names.len();
|
||
|
|
let nonzero_trailer_entry_count = entries.len().saturating_sub(zero_trailer_entry_count);
|
||
|
|
let mut distinct_trailer_words = entries
|
||
|
|
.iter()
|
||
|
|
.map(|entry| entry.trailer_word)
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
distinct_trailer_words.sort_unstable();
|
||
|
|
distinct_trailer_words.dedup();
|
||
|
|
let distinct_trailer_hex_words = distinct_trailer_words
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:08x}"))
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
let trailing_footer_hex = hex_encode(&bytes[entries_end_offset..block_end_offset]);
|
||
|
|
let footer = &bytes[entries_end_offset..block_end_offset];
|
||
|
|
if footer.len() != 9 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
let footer_progress_word_0 = u32::from_le_bytes([footer[0], footer[1], footer[2], footer[3]]);
|
||
|
|
let footer_progress_word_1 = u32::from_le_bytes([footer[4], footer[5], footer[6], footer[7]]);
|
||
|
|
let footer_trailing_byte = footer[8];
|
||
|
|
let mut footer_grounded_alignments = Vec::new();
|
||
|
|
for value in [footer_progress_word_0, footer_progress_word_1] {
|
||
|
|
if let Some(alignment) = classify_name_table_footer_progress_alignment(value) {
|
||
|
|
footer_grounded_alignments.push(alignment.to_string());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
evidence.extend([
|
||
|
|
format!("header offset 0x{header_offset:08x}"),
|
||
|
|
format!("entries offset 0x{entries_offset:08x}"),
|
||
|
|
format!("entry stride 0x{entry_stride:x}"),
|
||
|
|
format!("observed entry capacity {}", observed_entry_capacity),
|
||
|
|
format!("observed entry count {}", observed_entry_count),
|
||
|
|
format!("zero-trailer entries {}", zero_trailer_entry_count),
|
||
|
|
format!(
|
||
|
|
"trailing footer {} bytes after last entry",
|
||
|
|
block_end_offset - entries_end_offset
|
||
|
|
),
|
||
|
|
]);
|
||
|
|
let semantic_alignment = vec![
|
||
|
|
"Matches the grounded scenario-side named candidate-availability table shape under 0x00437743.".to_string(),
|
||
|
|
"Entry layout matches 0x00434ea0/0x00434f20: name slot at +0x00..+0x1d and availability dword at +0x1e.".to_string(),
|
||
|
|
"The shared map/save range suggests this catalog is bundled in source map content and later mirrored into scenario state [state+0x66b2].".to_string(),
|
||
|
|
];
|
||
|
|
|
||
|
|
Some(SmpRt3105SaveNameTableProbe {
|
||
|
|
profile_family,
|
||
|
|
source_kind,
|
||
|
|
semantic_family: "scenario-named-candidate-availability-table".to_string(),
|
||
|
|
semantic_alignment,
|
||
|
|
header_offset,
|
||
|
|
header_word_0: read_u32_at(bytes, header_offset)?,
|
||
|
|
header_word_0_hex: format!("0x{:08x}", read_u32_at(bytes, header_offset)?),
|
||
|
|
header_word_1: read_u32_at(bytes, header_offset + 4)?,
|
||
|
|
header_word_1_hex: format!("0x{:08x}", read_u32_at(bytes, header_offset + 4)?),
|
||
|
|
header_word_2: read_u32_at(bytes, header_offset + 8)?,
|
||
|
|
header_word_2_hex: format!("0x{:08x}", read_u32_at(bytes, header_offset + 8)?),
|
||
|
|
entry_stride,
|
||
|
|
entry_stride_hex: format!("0x{entry_stride:x}"),
|
||
|
|
header_prefix_word_count: 11,
|
||
|
|
observed_entry_capacity,
|
||
|
|
observed_entry_count,
|
||
|
|
zero_trailer_entry_count,
|
||
|
|
nonzero_trailer_entry_count,
|
||
|
|
distinct_trailer_words,
|
||
|
|
distinct_trailer_hex_words,
|
||
|
|
zero_trailer_entry_names,
|
||
|
|
entries_offset,
|
||
|
|
entries_end_offset,
|
||
|
|
trailing_footer_hex,
|
||
|
|
footer_progress_word_0,
|
||
|
|
footer_progress_word_0_hex: format!("0x{footer_progress_word_0:08x}"),
|
||
|
|
footer_progress_word_1,
|
||
|
|
footer_progress_word_1_hex: format!("0x{footer_progress_word_1:08x}"),
|
||
|
|
footer_trailing_byte,
|
||
|
|
footer_trailing_byte_hex: format!("0x{footer_trailing_byte:02x}"),
|
||
|
|
footer_grounded_alignments,
|
||
|
|
entries,
|
||
|
|
evidence,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn matches_candidate_availability_table_header(bytes: &[u8], header_offset: usize) -> bool {
|
||
|
|
matches!(
|
||
|
|
(
|
||
|
|
read_u32_at(bytes, header_offset + 0x08),
|
||
|
|
read_u32_at(bytes, header_offset + 0x0c),
|
||
|
|
read_u32_at(bytes, header_offset + 0x10),
|
||
|
|
read_u32_at(bytes, header_offset + 0x14),
|
||
|
|
read_u32_at(bytes, header_offset + 0x18),
|
||
|
|
read_u32_at(bytes, header_offset + 0x1c),
|
||
|
|
read_u32_at(bytes, header_offset + 0x20),
|
||
|
|
read_u32_at(bytes, header_offset + 0x24),
|
||
|
|
read_u32_at(bytes, header_offset + 0x28),
|
||
|
|
),
|
||
|
|
(
|
||
|
|
Some(0x0000332e),
|
||
|
|
Some(0x00000001),
|
||
|
|
Some(0x00000022),
|
||
|
|
Some(0x00000002),
|
||
|
|
Some(0x00000002),
|
||
|
|
Some(_),
|
||
|
|
Some(_),
|
||
|
|
Some(0x00000000),
|
||
|
|
Some(0x00000001),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn classify_name_table_footer_progress_alignment(value: u32) -> Option<&'static str> {
|
||
|
|
match value {
|
||
|
|
0x32dc => Some(
|
||
|
|
"Footer progress word 0x000032dc matches the grounded late rehydrate progress id 0x32dc.",
|
||
|
|
),
|
||
|
|
0x3714 => Some(
|
||
|
|
"Footer progress word 0x00003714 matches the grounded late rehydrate progress id 0x3714.",
|
||
|
|
),
|
||
|
|
_ => None,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_rt3_105_packed_profile_block(
|
||
|
|
bytes: &[u8],
|
||
|
|
packed_profile_offset: usize,
|
||
|
|
packed_profile_len: usize,
|
||
|
|
) -> Option<SmpRt3105PackedProfileBlock> {
|
||
|
|
let block_end = packed_profile_offset.checked_add(packed_profile_len)?;
|
||
|
|
if block_end > bytes.len() || packed_profile_len != 0x108 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let leading_word_0 = read_u32_at(bytes, packed_profile_offset)?;
|
||
|
|
let trailing_zero_word_count_after_leading_word = (1..4)
|
||
|
|
.take_while(|index| {
|
||
|
|
read_u32_at(bytes, packed_profile_offset + (index * 4)).is_some_and(|word| word == 0)
|
||
|
|
})
|
||
|
|
.count();
|
||
|
|
let header_flag_word_3 = read_u32_at(bytes, packed_profile_offset + 0x0c)?;
|
||
|
|
let map_path_offset = 0x10usize;
|
||
|
|
let display_name_offset = 0x43usize;
|
||
|
|
let stable_nonzero_word_offsets = [0x00usize, 0x0c, 0x78, 0x7c, 0x80, 0x84];
|
||
|
|
let stable_nonzero_words = stable_nonzero_word_offsets
|
||
|
|
.iter()
|
||
|
|
.filter_map(|relative_offset| {
|
||
|
|
let value = read_u32_at(bytes, packed_profile_offset + relative_offset)?;
|
||
|
|
if value == 0 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
Some(SmpPackedProfileWordLane {
|
||
|
|
relative_offset: *relative_offset,
|
||
|
|
relative_offset_hex: format!("0x{relative_offset:02x}"),
|
||
|
|
value,
|
||
|
|
value_hex: format!("0x{value:08x}"),
|
||
|
|
})
|
||
|
|
})
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
|
||
|
|
Some(SmpRt3105PackedProfileBlock {
|
||
|
|
relative_len: packed_profile_len,
|
||
|
|
relative_len_hex: format!("0x{packed_profile_len:03x}"),
|
||
|
|
leading_word_0,
|
||
|
|
leading_word_0_hex: format!("0x{leading_word_0:08x}"),
|
||
|
|
trailing_zero_word_count_after_leading_word,
|
||
|
|
header_flag_word_3,
|
||
|
|
header_flag_word_3_hex: format!("0x{header_flag_word_3:08x}"),
|
||
|
|
map_path_offset,
|
||
|
|
map_path: read_c_string_in_range(bytes, packed_profile_offset + map_path_offset, block_end),
|
||
|
|
display_name_offset,
|
||
|
|
display_name: read_c_string_in_range(
|
||
|
|
bytes,
|
||
|
|
packed_profile_offset + display_name_offset,
|
||
|
|
block_end,
|
||
|
|
),
|
||
|
|
profile_byte_0x77: bytes[packed_profile_offset + 0x77],
|
||
|
|
profile_byte_0x77_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x77]),
|
||
|
|
profile_byte_0x82: bytes[packed_profile_offset + 0x82],
|
||
|
|
profile_byte_0x82_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x82]),
|
||
|
|
profile_byte_0x97: bytes[packed_profile_offset + 0x97],
|
||
|
|
profile_byte_0x97_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0x97]),
|
||
|
|
profile_byte_0xc5: bytes[packed_profile_offset + 0xc5],
|
||
|
|
profile_byte_0xc5_hex: format!("0x{:02x}", bytes[packed_profile_offset + 0xc5]),
|
||
|
|
stable_nonzero_words,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn collect_runtime_post_span_header_candidates(
|
||
|
|
bytes: &[u8],
|
||
|
|
start: usize,
|
||
|
|
search_len: usize,
|
||
|
|
) -> Vec<SmpRuntimePostSpanHeaderCandidate> {
|
||
|
|
let end = bytes.len().min(start + search_len);
|
||
|
|
let mut offset = start & !0x3;
|
||
|
|
let mut candidates = Vec::new();
|
||
|
|
|
||
|
|
while offset + 16 <= end && candidates.len() < 8 {
|
||
|
|
if let Some(candidate) = build_runtime_post_span_header_candidate(bytes, offset) {
|
||
|
|
let mut cluster_end = offset + 4;
|
||
|
|
while cluster_end + 16 <= end
|
||
|
|
&& build_runtime_post_span_header_candidate(bytes, cluster_end).is_some()
|
||
|
|
{
|
||
|
|
cluster_end += 4;
|
||
|
|
}
|
||
|
|
candidates.push(candidate);
|
||
|
|
offset = cluster_end;
|
||
|
|
} else {
|
||
|
|
offset += 4;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
candidates
|
||
|
|
}
|
||
|
|
|
||
|
|
fn build_runtime_post_span_header_candidate(
|
||
|
|
bytes: &[u8],
|
||
|
|
offset: usize,
|
||
|
|
) -> Option<SmpRuntimePostSpanHeaderCandidate> {
|
||
|
|
let words = read_u32_window(bytes, offset, 4);
|
||
|
|
if words.len() < 4 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let dense_words = words
|
||
|
|
.iter()
|
||
|
|
.copied()
|
||
|
|
.filter(|word| (word & 0xffff) == 0 && (word >> 16) != 0)
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
if dense_words.len() < 3 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let high_u16_words = words
|
||
|
|
.iter()
|
||
|
|
.map(|word| (word >> 16) as u16)
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
let mut grounded_alignments = Vec::new();
|
||
|
|
for high in &high_u16_words {
|
||
|
|
if let Some(alignment) = classify_runtime_post_span_high16_grounded_alignment(*high) {
|
||
|
|
if !grounded_alignments
|
||
|
|
.iter()
|
||
|
|
.any(|existing| existing == alignment)
|
||
|
|
{
|
||
|
|
grounded_alignments.push(alignment.to_string());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Some(SmpRuntimePostSpanHeaderCandidate {
|
||
|
|
offset,
|
||
|
|
hex_words: words.iter().map(|word| format!("0x{word:08x}")).collect(),
|
||
|
|
dense_word_count: dense_words.len(),
|
||
|
|
high_hex_words: high_u16_words
|
||
|
|
.iter()
|
||
|
|
.map(|word| format!("0x{word:04x}"))
|
||
|
|
.collect(),
|
||
|
|
high_u16_words,
|
||
|
|
grounded_alignments,
|
||
|
|
words,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn classify_runtime_post_span_high16_grounded_alignment(high_u16: u16) -> Option<&'static str> {
|
||
|
|
match high_u16 {
|
||
|
|
0x32dc => Some(
|
||
|
|
"High-16 value 0x32dc matches the grounded late rehydrate progress id posted during world_entry_transition_and_runtime_bringup.",
|
||
|
|
),
|
||
|
|
0x3714 => Some(
|
||
|
|
"High-16 value 0x3714 matches the grounded late rehydrate progress id posted during world_entry_transition_and_runtime_bringup.",
|
||
|
|
),
|
||
|
|
0x3715 => Some(
|
||
|
|
"High-16 value 0x3715 matches the grounded late rehydrate progress id posted during world_entry_transition_and_runtime_bringup.",
|
||
|
|
),
|
||
|
|
_ => None,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn find_grounded_progress_high16_hits(
|
||
|
|
bytes: &[u8],
|
||
|
|
start: usize,
|
||
|
|
search_len: usize,
|
||
|
|
) -> Vec<String> {
|
||
|
|
let end = bytes.len().min(start + search_len);
|
||
|
|
let mut hits = Vec::new();
|
||
|
|
let mut offset = start & !0x3;
|
||
|
|
while offset + 4 <= end {
|
||
|
|
if let Some(word) = read_u32_at(bytes, offset) {
|
||
|
|
let high = (word >> 16) as u16;
|
||
|
|
if matches!(high, 0x32dc | 0x3714 | 0x3715) {
|
||
|
|
hits.push(format!("0x{high:04x}@0x{offset:08x}"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
offset += 4;
|
||
|
|
}
|
||
|
|
hits
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_grounded_progress_hit_offset(hits: &[String], high_u16: u16) -> Option<usize> {
|
||
|
|
let needle = format!("0x{high_u16:04x}@0x");
|
||
|
|
let hit = hits.iter().find(|hit| hit.starts_with(&needle))?;
|
||
|
|
let offset_hex = hit.split("@0x").nth(1)?;
|
||
|
|
usize::from_str_radix(offset_hex, 16).ok()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn collect_ascii_previews_in_range(
|
||
|
|
bytes: &[u8],
|
||
|
|
start: usize,
|
||
|
|
end: usize,
|
||
|
|
min_len: usize,
|
||
|
|
) -> Vec<SmpAsciiPreview> {
|
||
|
|
let mut previews = Vec::new();
|
||
|
|
let mut run_start = None;
|
||
|
|
let end = end.min(bytes.len());
|
||
|
|
|
||
|
|
for index in start..end {
|
||
|
|
let byte = bytes[index];
|
||
|
|
if is_ascii_preview_byte(byte) {
|
||
|
|
run_start.get_or_insert(index);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(current_start) = run_start.take() {
|
||
|
|
if index - current_start >= min_len {
|
||
|
|
previews.push(build_ascii_preview(bytes, current_start, index));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(current_start) = run_start {
|
||
|
|
if end - current_start >= min_len {
|
||
|
|
previews.push(build_ascii_preview(bytes, current_start, end));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
previews
|
||
|
|
}
|
||
|
|
|
||
|
|
fn find_c_string_with_suffix_in_range(
|
||
|
|
bytes: &[u8],
|
||
|
|
start: usize,
|
||
|
|
end: usize,
|
||
|
|
suffix: &str,
|
||
|
|
) -> Option<usize> {
|
||
|
|
let end = end.min(bytes.len());
|
||
|
|
let suffix = suffix.as_bytes();
|
||
|
|
let mut offset = start.min(end);
|
||
|
|
|
||
|
|
while offset < end {
|
||
|
|
if !is_ascii_preview_byte(bytes[offset]) {
|
||
|
|
offset += 1;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
let run_start = offset;
|
||
|
|
while offset < end && is_ascii_preview_byte(bytes[offset]) {
|
||
|
|
offset += 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
let run = &bytes[run_start..offset];
|
||
|
|
if run.ends_with(suffix) {
|
||
|
|
return Some(run_start);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
None
|
||
|
|
}
|
||
|
|
|
||
|
|
fn read_c_string_in_range(bytes: &[u8], start: usize, end: usize) -> Option<String> {
|
||
|
|
if start >= end || start >= bytes.len() {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let end = end.min(bytes.len());
|
||
|
|
let mut cursor = start;
|
||
|
|
while cursor < end && bytes[cursor] != 0 {
|
||
|
|
cursor += 1;
|
||
|
|
}
|
||
|
|
if cursor == start {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::str::from_utf8(&bytes[start..cursor])
|
||
|
|
.ok()
|
||
|
|
.map(ToString::to_string)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn find_u16_le_offsets(bytes: &[u8], needle: u16) -> Vec<usize> {
|
||
|
|
let pattern = needle.to_le_bytes();
|
||
|
|
bytes
|
||
|
|
.windows(pattern.len())
|
||
|
|
.enumerate()
|
||
|
|
.filter_map(|(offset, window)| (window == pattern).then_some(offset))
|
||
|
|
.collect()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn find_next_nonzero_offset(bytes: &[u8], start: usize) -> Option<usize> {
|
||
|
|
bytes
|
||
|
|
.iter()
|
||
|
|
.enumerate()
|
||
|
|
.skip(start)
|
||
|
|
.find_map(|(offset, byte)| (*byte != 0).then_some(offset))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn find_zero_run(bytes: &[u8], start: usize, min_len: usize) -> Option<usize> {
|
||
|
|
let mut run_start = None;
|
||
|
|
let mut run_len = 0usize;
|
||
|
|
|
||
|
|
for (offset, byte) in bytes.iter().enumerate().skip(start) {
|
||
|
|
if *byte == 0 {
|
||
|
|
run_start.get_or_insert(offset);
|
||
|
|
run_len += 1;
|
||
|
|
if run_len >= min_len {
|
||
|
|
return run_start;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
run_start = None;
|
||
|
|
run_len = 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
None
|
||
|
|
}
|
||
|
|
|
||
|
|
fn find_first_ascii_run(bytes: &[u8]) -> Option<SmpAsciiPreview> {
|
||
|
|
let mut start = None;
|
||
|
|
|
||
|
|
for (index, byte) in bytes.iter().copied().enumerate() {
|
||
|
|
if is_ascii_preview_byte(byte) {
|
||
|
|
start.get_or_insert(index);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(run_start) = start.take() {
|
||
|
|
if index - run_start >= MIN_ASCII_RUN_LEN {
|
||
|
|
return Some(build_ascii_preview(bytes, run_start, index));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
start.and_then(|run_start| {
|
||
|
|
if bytes.len() - run_start >= MIN_ASCII_RUN_LEN {
|
||
|
|
Some(build_ascii_preview(bytes, run_start, bytes.len()))
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn build_ascii_preview(bytes: &[u8], start: usize, end: usize) -> SmpAsciiPreview {
|
||
|
|
let byte_len = end - start;
|
||
|
|
let preview_bytes = &bytes[start..end];
|
||
|
|
let preview = String::from_utf8_lossy(
|
||
|
|
&preview_bytes[..preview_bytes.len().min(ASCII_PREVIEW_CHAR_LIMIT)],
|
||
|
|
)
|
||
|
|
.into_owned();
|
||
|
|
|
||
|
|
SmpAsciiPreview {
|
||
|
|
offset: start,
|
||
|
|
byte_len,
|
||
|
|
truncated: byte_len > ASCII_PREVIEW_CHAR_LIMIT,
|
||
|
|
preview,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_ascii_preview_byte(byte: u8) -> bool {
|
||
|
|
matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | 0x21..=0x7e)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn read_u32_window(bytes: &[u8], offset: usize, count: usize) -> Vec<u32> {
|
||
|
|
let mut words = Vec::new();
|
||
|
|
let end = bytes.len().min(offset + count * 4);
|
||
|
|
for chunk in bytes[offset..end].chunks_exact(4) {
|
||
|
|
words.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
|
||
|
|
}
|
||
|
|
words
|
||
|
|
}
|
||
|
|
|
||
|
|
fn read_u32_at(bytes: &[u8], offset: usize) -> Option<u32> {
|
||
|
|
let chunk = bytes.get(offset..offset + 4)?;
|
||
|
|
Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn hex_encode(bytes: &[u8]) -> String {
|
||
|
|
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn sha256_hex(bytes: &[u8]) -> String {
|
||
|
|
let digest = Sha256::digest(bytes);
|
||
|
|
format!("{digest:x}")
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn reports_grounded_tag_hits_and_offsets() {
|
||
|
|
let bytes = [
|
||
|
|
0x34, 0x12, 0x00, 0x00, 0xe0, 0x2e, 0x00, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x80,
|
||
|
|
0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x71, 0x07, 0x00, 0x00, 0x71, 0x07, 0x00, 0x00,
|
||
|
|
0x71, 0x07, 0x00, 0x00, 0xaa, 0xbb, 0x00, 0x00, 0xcc, 0xdd, 0x00, 0x00, 0x00, 0x00,
|
||
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, b'H', b'e', b'l',
|
||
|
|
b'l', b'o', b' ', b'R', b'R', b'T', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||
|
|
0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78,
|
||
|
|
0x9a, 0xbc, 0xde, 0xf0, 0x00, 0x00, 0x00, 0x00, 0xee, 0x2c, 0x11, 0x51, 0x2d, 0x22,
|
||
|
|
0x71, 0x94, 0x33, 0x72, 0x94,
|
||
|
|
];
|
||
|
|
let report = inspect_smp_bytes(&bytes);
|
||
|
|
|
||
|
|
assert!(report.contains_grounded_runtime_tags);
|
||
|
|
assert_eq!(report.known_tag_hits.len(), 4);
|
||
|
|
assert_eq!(report.preamble.word_count, 16);
|
||
|
|
assert_eq!(report.preamble.words[0].value_le, 0x00001234);
|
||
|
|
let shared_header = report
|
||
|
|
.shared_header
|
||
|
|
.as_ref()
|
||
|
|
.expect("shared header should parse");
|
||
|
|
assert!(shared_header.matches_grounded_common_signature);
|
||
|
|
let header_variant = report
|
||
|
|
.header_variant_probe
|
||
|
|
.as_ref()
|
||
|
|
.expect("header variant probe should exist");
|
||
|
|
assert_eq!(header_variant.variant_family, "unknown");
|
||
|
|
assert!(!header_variant.is_known_family);
|
||
|
|
assert_eq!(shared_header.primary_family_tag, 0x00002ee0);
|
||
|
|
assert_eq!(
|
||
|
|
shared_header.payload_window_words_8_to_9,
|
||
|
|
vec![0x0000bbaa, 0x0000ddcc]
|
||
|
|
);
|
||
|
|
assert!(shared_header.reserved_words_10_to_14_all_zero);
|
||
|
|
assert_eq!(shared_header.final_flag_word, 0);
|
||
|
|
let ascii_run = report
|
||
|
|
.first_ascii_run
|
||
|
|
.as_ref()
|
||
|
|
.expect("ascii run should exist");
|
||
|
|
assert_eq!(ascii_run.offset, 67);
|
||
|
|
assert_eq!(ascii_run.byte_len, 9);
|
||
|
|
assert_eq!(ascii_run.preview, "Hello RRT");
|
||
|
|
let early_probe = report
|
||
|
|
.early_content_probe
|
||
|
|
.as_ref()
|
||
|
|
.expect("early content probe should exist");
|
||
|
|
assert_eq!(early_probe.first_post_text_nonzero_offset, 88);
|
||
|
|
assert_eq!(early_probe.zero_pad_after_text_len, 12);
|
||
|
|
assert_eq!(early_probe.first_post_text_block_len, 4);
|
||
|
|
assert_eq!(early_probe.first_post_text_block_hex, "11223344");
|
||
|
|
assert_eq!(early_probe.trailing_zero_pad_after_first_block_len, 16);
|
||
|
|
assert_eq!(early_probe.secondary_nonzero_offset, Some(108));
|
||
|
|
assert_eq!(early_probe.secondary_aligned_word_window_offset, Some(108));
|
||
|
|
assert_eq!(
|
||
|
|
&early_probe.secondary_aligned_word_window_words[..2],
|
||
|
|
&[0x78563412, 0xf0debc9a]
|
||
|
|
);
|
||
|
|
assert!(
|
||
|
|
early_probe
|
||
|
|
.secondary_preview_hex
|
||
|
|
.starts_with("123456789abcdef0")
|
||
|
|
);
|
||
|
|
let secondary_variant = report
|
||
|
|
.secondary_variant_probe
|
||
|
|
.as_ref()
|
||
|
|
.expect("secondary variant probe should exist");
|
||
|
|
assert_eq!(secondary_variant.variant_family, "unknown");
|
||
|
|
let container_profile = report
|
||
|
|
.container_profile
|
||
|
|
.as_ref()
|
||
|
|
.expect("container profile should exist");
|
||
|
|
assert_eq!(container_profile.profile_family, "unknown");
|
||
|
|
assert!(!container_profile.is_known_profile);
|
||
|
|
assert!(report.save_bootstrap_block.is_none());
|
||
|
|
assert!(report.save_anchor_run_block.is_none());
|
||
|
|
assert!(report.runtime_anchor_cycle_block.is_none());
|
||
|
|
assert!(report.runtime_trailer_block.is_none());
|
||
|
|
assert!(report.runtime_post_span_probe.is_none());
|
||
|
|
assert!(report.classic_rehydrate_profile_probe.is_none());
|
||
|
|
assert_eq!(report.known_tag_hits[0].tag_id, 0x2cee);
|
||
|
|
assert_eq!(report.known_tag_hits[0].hit_count, 1);
|
||
|
|
assert_eq!(report.known_tag_hits[0].sample_offsets, vec![120]);
|
||
|
|
assert_eq!(report.known_tag_hits[1].tag_id, 0x2d51);
|
||
|
|
assert_eq!(report.known_tag_hits[1].sample_offsets, vec![123]);
|
||
|
|
assert_eq!(report.known_tag_hits[2].tag_id, 0x9471);
|
||
|
|
assert_eq!(report.known_tag_hits[2].sample_offsets, vec![126]);
|
||
|
|
assert_eq!(report.known_tag_hits[3].tag_id, 0x9472);
|
||
|
|
assert_eq!(report.known_tag_hits[3].sample_offsets, vec![129]);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn warns_when_no_grounded_tags_are_present() {
|
||
|
|
let report = inspect_smp_bytes(&[0xaa, 0xbb, 0xcc]);
|
||
|
|
|
||
|
|
assert!(!report.contains_grounded_runtime_tags);
|
||
|
|
assert!(report.known_tag_hits.is_empty());
|
||
|
|
assert_eq!(report.preamble.word_count, 0);
|
||
|
|
assert!(report.shared_header.is_none());
|
||
|
|
assert!(report.header_variant_probe.is_none());
|
||
|
|
assert!(report.first_ascii_run.is_none());
|
||
|
|
assert!(report.early_content_probe.is_none());
|
||
|
|
assert!(report.secondary_variant_probe.is_none());
|
||
|
|
assert!(report.container_profile.is_none());
|
||
|
|
assert!(report.save_bootstrap_block.is_none());
|
||
|
|
assert!(report.save_anchor_run_block.is_none());
|
||
|
|
assert!(report.runtime_anchor_cycle_block.is_none());
|
||
|
|
assert!(report.runtime_trailer_block.is_none());
|
||
|
|
assert!(report.runtime_post_span_probe.is_none());
|
||
|
|
assert!(report.classic_rehydrate_profile_probe.is_none());
|
||
|
|
assert!(
|
||
|
|
report
|
||
|
|
.warnings
|
||
|
|
.iter()
|
||
|
|
.any(|warning| warning.contains("No grounded runtime bundle tags were found"))
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parses_save_anchor_cycle_and_trailer() {
|
||
|
|
let cycle_words: [u32; 9] = [
|
||
|
|
0x00000000, 0x0186a000, 0x00000000, 0x86a00000, 0x00000001, 0xa0000000, 0x00000186,
|
||
|
|
0x00000000, 0x000186a0,
|
||
|
|
];
|
||
|
|
let trailer_words: [u32; 3] = [0x00020000, 0x00030000, 0x2ee10000];
|
||
|
|
let mut bytes = vec![0u8; 0x1c + (cycle_words.len() * 2 + 2 + trailer_words.len()) * 4];
|
||
|
|
|
||
|
|
let mut cursor = 0x1c;
|
||
|
|
for _ in 0..2 {
|
||
|
|
for word in cycle_words {
|
||
|
|
bytes[cursor..cursor + 4].copy_from_slice(&word.to_le_bytes());
|
||
|
|
cursor += 4;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for word in &cycle_words[..2] {
|
||
|
|
bytes[cursor..cursor + 4].copy_from_slice(&(*word).to_le_bytes());
|
||
|
|
cursor += 4;
|
||
|
|
}
|
||
|
|
for word in trailer_words {
|
||
|
|
bytes[cursor..cursor + 4].copy_from_slice(&word.to_le_bytes());
|
||
|
|
cursor += 4;
|
||
|
|
}
|
||
|
|
|
||
|
|
let container_profile = SmpContainerProfile {
|
||
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
||
|
|
profile_evidence: vec!["test".to_string()],
|
||
|
|
is_known_profile: true,
|
||
|
|
};
|
||
|
|
let bootstrap = SmpSaveBootstrapBlock {
|
||
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
||
|
|
aligned_window_offset: 0,
|
||
|
|
leading_word: 0,
|
||
|
|
leading_word_hex: "0x00000000".to_string(),
|
||
|
|
anchor_word: 0,
|
||
|
|
anchor_word_hex: "0x00000000".to_string(),
|
||
|
|
descriptor_word_2: 0,
|
||
|
|
descriptor_word_2_hex: "0x00000000".to_string(),
|
||
|
|
descriptor_word_3: 0,
|
||
|
|
descriptor_word_3_hex: "0x00000000".to_string(),
|
||
|
|
descriptor_word_4: 0,
|
||
|
|
descriptor_word_4_hex: "0x00000000".to_string(),
|
||
|
|
descriptor_word_5: 0,
|
||
|
|
descriptor_word_5_hex: "0x00000000".to_string(),
|
||
|
|
descriptor_word_6: 0,
|
||
|
|
descriptor_word_6_hex: "0x00000000".to_string(),
|
||
|
|
descriptor_word_7: 0,
|
||
|
|
descriptor_word_7_hex: "0x00000000".to_string(),
|
||
|
|
};
|
||
|
|
|
||
|
|
let parsed =
|
||
|
|
parse_save_anchor_run_block(&bytes, Some(&container_profile), Some(&bootstrap))
|
||
|
|
.expect("cycle block should parse");
|
||
|
|
|
||
|
|
assert_eq!(parsed.cycle_start_offset, 0x1c);
|
||
|
|
assert_eq!(parsed.cycle_words, cycle_words);
|
||
|
|
assert_eq!(parsed.full_cycle_count, 2);
|
||
|
|
assert_eq!(parsed.partial_cycle_word_count, 2);
|
||
|
|
assert_eq!(
|
||
|
|
parsed.trailer_offset,
|
||
|
|
0x1c + (cycle_words.len() * 2 + 2) * 4
|
||
|
|
);
|
||
|
|
assert_eq!(parsed.trailer_words, trailer_words);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classifies_runtime_trailer_family() {
|
||
|
|
let runtime_anchor_cycle_block = SmpRuntimeAnchorCycleBlock {
|
||
|
|
profile_family: "rt3-classic-sandbox-container-v1".to_string(),
|
||
|
|
cycle_start_offset: 0x33c,
|
||
|
|
cycle_words: vec![0; 9],
|
||
|
|
cycle_hex_words: vec!["0x00000000".to_string(); 9],
|
||
|
|
full_cycle_count: 3,
|
||
|
|
partial_cycle_word_count: 2,
|
||
|
|
trailer_offset: 0x3b0,
|
||
|
|
trailer_words: vec![
|
||
|
|
0x00010000, 0x00010000, 0x00010000, 0x00010000, 0x00000000, 0x00000000, 0x2ee10000,
|
||
|
|
0x32c80000, 0x0dcd0000, 0x01010107, 0x26010000, 0x01010107, 0x00010000, 0x0334c68c,
|
||
|
|
0x03000000, 0x01000000,
|
||
|
|
],
|
||
|
|
trailer_hex_words: Vec::new(),
|
||
|
|
};
|
||
|
|
let container_profile = SmpContainerProfile {
|
||
|
|
profile_family: "rt3-classic-sandbox-container-v1".to_string(),
|
||
|
|
profile_evidence: vec!["test".to_string()],
|
||
|
|
is_known_profile: true,
|
||
|
|
};
|
||
|
|
|
||
|
|
let trailer = parse_runtime_trailer_block(
|
||
|
|
Some(&container_profile),
|
||
|
|
Some(&runtime_anchor_cycle_block),
|
||
|
|
)
|
||
|
|
.expect("runtime trailer should parse");
|
||
|
|
|
||
|
|
assert_eq!(trailer.trailer_family, "rt3-classic-sandbox-trailer-v1");
|
||
|
|
assert_eq!(trailer.prefix_words_0_to_5[0], 0x00010000);
|
||
|
|
assert_eq!(trailer.tag_word_6, 0x2ee10000);
|
||
|
|
assert_eq!(trailer.tag_chunk_id_u16, 0x2ee1);
|
||
|
|
assert_eq!(trailer.selector_word_8, 0x0dcd0000);
|
||
|
|
assert_eq!(trailer.selector_high_u16, 0x0dcd);
|
||
|
|
assert_eq!(trailer.mode_word_15, 0x01000000);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn probes_runtime_post_span_region() {
|
||
|
|
let mut bytes = vec![0u8; 0x200];
|
||
|
|
bytes[0x90..0x94].copy_from_slice(&0x32dc0000u32.to_le_bytes());
|
||
|
|
bytes[0x94..0x98].copy_from_slice(&0x37140000u32.to_le_bytes());
|
||
|
|
bytes[0x98..0x9c].copy_from_slice(&0x03000000u32.to_le_bytes());
|
||
|
|
bytes[0xa0..0xa4].copy_from_slice(&0x37150000u32.to_le_bytes());
|
||
|
|
bytes[0xa4..0xa8].copy_from_slice(&0x00010000u32.to_le_bytes());
|
||
|
|
bytes[0xa8..0xac].copy_from_slice(&0x00410000u32.to_le_bytes());
|
||
|
|
|
||
|
|
let trailer = SmpRuntimeTrailerBlock {
|
||
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
||
|
|
trailer_family: "test".to_string(),
|
||
|
|
trailer_evidence: Vec::new(),
|
||
|
|
trailer_offset: 0x40,
|
||
|
|
prefix_words_0_to_5: Vec::new(),
|
||
|
|
prefix_hex_words_0_to_5: Vec::new(),
|
||
|
|
tag_word_6: 0x2ee10000,
|
||
|
|
tag_word_6_hex: "0x2ee10000".to_string(),
|
||
|
|
tag_chunk_id_u16: 0x2ee1,
|
||
|
|
tag_chunk_id_hex: "0x2ee1".to_string(),
|
||
|
|
tag_chunk_id_grounded_alignment: None,
|
||
|
|
length_word_7: 0x00200000,
|
||
|
|
length_word_7_hex: "0x00200000".to_string(),
|
||
|
|
length_high_u16: 0x0020,
|
||
|
|
length_high_hex: "0x0020".to_string(),
|
||
|
|
selector_word_8: 0,
|
||
|
|
selector_word_8_hex: "0x00000000".to_string(),
|
||
|
|
selector_high_u16: 0,
|
||
|
|
selector_high_hex: "0x0000".to_string(),
|
||
|
|
layout_word_9: 0,
|
||
|
|
layout_word_9_hex: "0x00000000".to_string(),
|
||
|
|
descriptor_word_10: 0,
|
||
|
|
descriptor_word_10_hex: "0x00000000".to_string(),
|
||
|
|
descriptor_high_u16: 0,
|
||
|
|
descriptor_high_hex: "0x0000".to_string(),
|
||
|
|
descriptor_word_11: 0,
|
||
|
|
descriptor_word_11_hex: "0x00000000".to_string(),
|
||
|
|
counter_word_12: 0,
|
||
|
|
counter_word_12_hex: "0x00000000".to_string(),
|
||
|
|
offset_word_13: 0,
|
||
|
|
offset_word_13_hex: "0x00000000".to_string(),
|
||
|
|
span_word_14: 0,
|
||
|
|
span_word_14_hex: "0x00000000".to_string(),
|
||
|
|
mode_word_15: 0,
|
||
|
|
mode_word_15_hex: "0x00000000".to_string(),
|
||
|
|
words: Vec::new(),
|
||
|
|
hex_words: Vec::new(),
|
||
|
|
};
|
||
|
|
|
||
|
|
let probe = parse_runtime_post_span_probe(&bytes, Some(&trailer))
|
||
|
|
.expect("post-span probe should parse");
|
||
|
|
|
||
|
|
assert_eq!(probe.span_target_offset, 0x60);
|
||
|
|
assert_eq!(probe.next_nonzero_offset, Some(0x92));
|
||
|
|
assert_eq!(probe.next_aligned_candidate_offset, Some(0x8c));
|
||
|
|
assert_eq!(probe.header_candidates.len(), 1);
|
||
|
|
assert_eq!(probe.header_candidates[0].dense_word_count, 3);
|
||
|
|
assert_eq!(probe.header_candidates[0].grounded_alignments.len(), 2);
|
||
|
|
assert_eq!(probe.grounded_progress_hits[0], "0x32dc@0x00000090");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parses_classic_rehydrate_profile_probe() {
|
||
|
|
let mut bytes = vec![0u8; 0x220];
|
||
|
|
bytes[0x90..0x94].copy_from_slice(&0x32dc0000u32.to_le_bytes());
|
||
|
|
bytes[0x94..0x98].copy_from_slice(&0x37140000u32.to_le_bytes());
|
||
|
|
bytes[0x1a0..0x1a4].copy_from_slice(&0x37150000u32.to_le_bytes());
|
||
|
|
bytes[0xab..0xb7].copy_from_slice(b"test-map.gmp");
|
||
|
|
bytes[0xde..0xe6].copy_from_slice(b"Test Map");
|
||
|
|
|
||
|
|
let post_span = SmpRuntimePostSpanProbe {
|
||
|
|
profile_family: "rt3-classic-save-container-v1".to_string(),
|
||
|
|
span_target_offset: 0,
|
||
|
|
next_nonzero_offset: Some(0x92),
|
||
|
|
next_aligned_candidate_offset: Some(0x8c),
|
||
|
|
next_aligned_candidate_words: vec![0, 0x32dc0000, 0x37140000, 0x03000000],
|
||
|
|
next_aligned_candidate_hex_words: vec![],
|
||
|
|
header_candidates: vec![],
|
||
|
|
grounded_progress_hits: vec![
|
||
|
|
"0x32dc@0x00000090".to_string(),
|
||
|
|
"0x3714@0x00000094".to_string(),
|
||
|
|
"0x3715@0x000001a0".to_string(),
|
||
|
|
],
|
||
|
|
};
|
||
|
|
|
||
|
|
let probe = parse_classic_rehydrate_profile_probe(&bytes, Some(&post_span))
|
||
|
|
.expect("classic rehydrate probe should parse");
|
||
|
|
|
||
|
|
assert_eq!(probe.packed_profile_offset, 0x98);
|
||
|
|
assert_eq!(probe.packed_profile_len, 0x108);
|
||
|
|
assert_eq!(probe.ascii_runs[0].preview, "test-map.gmp");
|
||
|
|
assert_eq!(probe.packed_profile_block.leading_word_0, 0x00000000);
|
||
|
|
assert_eq!(
|
||
|
|
probe.packed_profile_block.map_path.as_deref(),
|
||
|
|
Some("test-map.gmp")
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
probe.packed_profile_block.display_name.as_deref(),
|
||
|
|
Some("Test Map")
|
||
|
|
);
|
||
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0x77, 0x00);
|
||
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0x82, 0x00);
|
||
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0x97, 0x00);
|
||
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0xc5, 0x00);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parses_rt3_105_packed_profile_probe() {
|
||
|
|
let mut bytes = vec![0u8; 0x9000];
|
||
|
|
let block = 0x73c0usize;
|
||
|
|
bytes[block..block + 4].copy_from_slice(&0x00000003u32.to_le_bytes());
|
||
|
|
bytes[block + 0x0c..block + 0x10].copy_from_slice(&0x01000000u32.to_le_bytes());
|
||
|
|
bytes[block + 0x10..block + 0x1d].copy_from_slice(b"test-105.gmp\0");
|
||
|
|
bytes[block + 0x43..block + 0x4c].copy_from_slice(b"Test 105\0");
|
||
|
|
bytes[block + 0x77] = 0x07;
|
||
|
|
bytes[block + 0x82] = 0x4d;
|
||
|
|
bytes[block + 0x84..block + 0x88].copy_from_slice(&0x65010000u32.to_le_bytes());
|
||
|
|
|
||
|
|
let header_variant_probe = SmpHeaderVariantProbe {
|
||
|
|
variant_family: "rt3-105-common-header-v1".to_string(),
|
||
|
|
variant_evidence: vec![],
|
||
|
|
is_known_family: true,
|
||
|
|
};
|
||
|
|
let probe = parse_rt3_105_packed_profile_probe(
|
||
|
|
&bytes,
|
||
|
|
Some("gms"),
|
||
|
|
Some(&header_variant_probe),
|
||
|
|
None,
|
||
|
|
)
|
||
|
|
.expect("1.05 packed profile probe should parse");
|
||
|
|
|
||
|
|
assert_eq!(probe.profile_family, "rt3-105-save-analog-block-inferred");
|
||
|
|
assert_eq!(probe.packed_profile_offset, 0x73c0);
|
||
|
|
assert_eq!(probe.packed_profile_len, 0x108);
|
||
|
|
assert_eq!(
|
||
|
|
probe.packed_profile_block.map_path.as_deref(),
|
||
|
|
Some("test-105.gmp")
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
probe.packed_profile_block.display_name.as_deref(),
|
||
|
|
Some("Test 105")
|
||
|
|
);
|
||
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0x77, 0x07);
|
||
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0x82, 0x4d);
|
||
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0x97, 0x00);
|
||
|
|
assert_eq!(probe.packed_profile_block.profile_byte_0xc5, 0x00);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classifies_rt3_105_post_span_bridge_variants() {
|
||
|
|
let base_trailer = SmpRuntimeTrailerBlock {
|
||
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
||
|
|
trailer_family: "rt3-105-save-trailer-v1".to_string(),
|
||
|
|
trailer_evidence: vec![],
|
||
|
|
trailer_offset: 944,
|
||
|
|
prefix_words_0_to_5: vec![],
|
||
|
|
prefix_hex_words_0_to_5: vec![],
|
||
|
|
tag_word_6: 0,
|
||
|
|
tag_word_6_hex: String::new(),
|
||
|
|
tag_chunk_id_u16: 0x2ee1,
|
||
|
|
tag_chunk_id_hex: "0x2ee1".to_string(),
|
||
|
|
tag_chunk_id_grounded_alignment: None,
|
||
|
|
length_word_7: 0x32c8_0000,
|
||
|
|
length_word_7_hex: "0x32c80000".to_string(),
|
||
|
|
length_high_u16: 0x32c8,
|
||
|
|
length_high_hex: "0x32c8".to_string(),
|
||
|
|
selector_word_8: 0x7110_0000,
|
||
|
|
selector_word_8_hex: "0x71100000".to_string(),
|
||
|
|
selector_high_u16: 0x7110,
|
||
|
|
selector_high_hex: "0x7110".to_string(),
|
||
|
|
layout_word_9: 0,
|
||
|
|
layout_word_9_hex: String::new(),
|
||
|
|
descriptor_word_10: 0x7801_0000,
|
||
|
|
descriptor_word_10_hex: "0x78010000".to_string(),
|
||
|
|
descriptor_high_u16: 0x7801,
|
||
|
|
descriptor_high_hex: "0x7801".to_string(),
|
||
|
|
descriptor_word_11: 0,
|
||
|
|
descriptor_word_11_hex: String::new(),
|
||
|
|
counter_word_12: 0,
|
||
|
|
counter_word_12_hex: String::new(),
|
||
|
|
offset_word_13: 0,
|
||
|
|
offset_word_13_hex: String::new(),
|
||
|
|
span_word_14: 0,
|
||
|
|
span_word_14_hex: String::new(),
|
||
|
|
mode_word_15: 0,
|
||
|
|
mode_word_15_hex: String::new(),
|
||
|
|
words: vec![],
|
||
|
|
hex_words: vec![],
|
||
|
|
};
|
||
|
|
let base_post_span = SmpRuntimePostSpanProbe {
|
||
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
||
|
|
span_target_offset: 13944,
|
||
|
|
next_nonzero_offset: Some(14795),
|
||
|
|
next_aligned_candidate_offset: Some(20244),
|
||
|
|
next_aligned_candidate_words: vec![],
|
||
|
|
next_aligned_candidate_hex_words: vec![],
|
||
|
|
header_candidates: vec![SmpRuntimePostSpanHeaderCandidate {
|
||
|
|
offset: 20244,
|
||
|
|
words: vec![],
|
||
|
|
hex_words: vec![],
|
||
|
|
dense_word_count: 3,
|
||
|
|
high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515],
|
||
|
|
high_hex_words: vec![],
|
||
|
|
grounded_alignments: vec![],
|
||
|
|
}],
|
||
|
|
grounded_progress_hits: vec![],
|
||
|
|
};
|
||
|
|
let base_profile = SmpRt3105PackedProfileProbe {
|
||
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
||
|
|
packed_profile_offset: 29632,
|
||
|
|
packed_profile_len: 0x108,
|
||
|
|
packed_profile_len_hex: "0x108".to_string(),
|
||
|
|
packed_profile_block: SmpRt3105PackedProfileBlock {
|
||
|
|
relative_len: 0x108,
|
||
|
|
relative_len_hex: "0x108".to_string(),
|
||
|
|
leading_word_0: 3,
|
||
|
|
leading_word_0_hex: "0x00000003".to_string(),
|
||
|
|
trailing_zero_word_count_after_leading_word: 2,
|
||
|
|
header_flag_word_3: 0x0100_0000,
|
||
|
|
header_flag_word_3_hex: "0x01000000".to_string(),
|
||
|
|
map_path_offset: 0x10,
|
||
|
|
map_path: Some("Alternate USA.gmp".to_string()),
|
||
|
|
display_name_offset: 0x43,
|
||
|
|
display_name: Some("Alternate USA".to_string()),
|
||
|
|
profile_byte_0x77: 0x07,
|
||
|
|
profile_byte_0x77_hex: "0x07".to_string(),
|
||
|
|
profile_byte_0x82: 0x4d,
|
||
|
|
profile_byte_0x82_hex: "0x4d".to_string(),
|
||
|
|
profile_byte_0x97: 0,
|
||
|
|
profile_byte_0x97_hex: "0x00".to_string(),
|
||
|
|
profile_byte_0xc5: 0,
|
||
|
|
profile_byte_0xc5_hex: "0x00".to_string(),
|
||
|
|
stable_nonzero_words: vec![],
|
||
|
|
},
|
||
|
|
ascii_runs: vec![],
|
||
|
|
};
|
||
|
|
let base_bridge = parse_rt3_105_post_span_bridge_probe(
|
||
|
|
Some(&base_trailer),
|
||
|
|
Some(&base_post_span),
|
||
|
|
Some(&base_profile),
|
||
|
|
)
|
||
|
|
.expect("base bridge should parse");
|
||
|
|
assert_eq!(
|
||
|
|
base_bridge.bridge_family,
|
||
|
|
"rt3-105-save-post-span-bridge-v1"
|
||
|
|
);
|
||
|
|
assert_eq!(base_bridge.packed_profile_delta_from_span_target, 15688);
|
||
|
|
assert_eq!(
|
||
|
|
base_bridge.next_candidate_delta_from_packed_profile,
|
||
|
|
Some(-9388)
|
||
|
|
);
|
||
|
|
let base_variant_trailer = SmpRuntimeTrailerBlock {
|
||
|
|
descriptor_word_10: 0x7401_0000,
|
||
|
|
descriptor_word_10_hex: "0x74010000".to_string(),
|
||
|
|
descriptor_high_u16: 0x7401,
|
||
|
|
descriptor_high_hex: "0x7401".to_string(),
|
||
|
|
..base_trailer.clone()
|
||
|
|
};
|
||
|
|
let base_variant_bridge = parse_rt3_105_post_span_bridge_probe(
|
||
|
|
Some(&base_variant_trailer),
|
||
|
|
Some(&base_post_span),
|
||
|
|
Some(&base_profile),
|
||
|
|
)
|
||
|
|
.expect("base bridge variant should parse");
|
||
|
|
assert_eq!(
|
||
|
|
base_variant_bridge.bridge_family,
|
||
|
|
"rt3-105-save-post-span-bridge-v1"
|
||
|
|
);
|
||
|
|
|
||
|
|
let alt_trailer = SmpRuntimeTrailerBlock {
|
||
|
|
profile_family: "rt3-105-alt-save-container-v1".to_string(),
|
||
|
|
selector_word_8: 0x54cd_0000,
|
||
|
|
selector_word_8_hex: "0x54cd0000".to_string(),
|
||
|
|
selector_high_u16: 0x54cd,
|
||
|
|
selector_high_hex: "0x54cd".to_string(),
|
||
|
|
descriptor_word_10: 0x5901_0000,
|
||
|
|
descriptor_word_10_hex: "0x59010000".to_string(),
|
||
|
|
descriptor_high_u16: 0x5901,
|
||
|
|
descriptor_high_hex: "0x5901".to_string(),
|
||
|
|
..base_trailer.clone()
|
||
|
|
};
|
||
|
|
let alt_post_span = SmpRuntimePostSpanProbe {
|
||
|
|
profile_family: "rt3-105-alt-save-container-v1".to_string(),
|
||
|
|
next_aligned_candidate_offset: Some(29892),
|
||
|
|
header_candidates: vec![SmpRuntimePostSpanHeaderCandidate {
|
||
|
|
offset: 29892,
|
||
|
|
words: vec![],
|
||
|
|
hex_words: vec![],
|
||
|
|
dense_word_count: 3,
|
||
|
|
high_u16_words: vec![0x1500, 0x0100, 0x4100, 0x0200],
|
||
|
|
high_hex_words: vec![],
|
||
|
|
grounded_alignments: vec![],
|
||
|
|
}],
|
||
|
|
..base_post_span.clone()
|
||
|
|
};
|
||
|
|
let alt_profile = SmpRt3105PackedProfileProbe {
|
||
|
|
profile_family: "rt3-105-alt-save-container-v1".to_string(),
|
||
|
|
packed_profile_block: SmpRt3105PackedProfileBlock {
|
||
|
|
map_path: Some("Spanish Mainline.gmp".to_string()),
|
||
|
|
display_name: Some("Spanish Mainline".to_string()),
|
||
|
|
profile_byte_0x82: 0xa3,
|
||
|
|
profile_byte_0x82_hex: "0xa3".to_string(),
|
||
|
|
..base_profile.packed_profile_block.clone()
|
||
|
|
},
|
||
|
|
..base_profile.clone()
|
||
|
|
};
|
||
|
|
let alt_bridge = parse_rt3_105_post_span_bridge_probe(
|
||
|
|
Some(&alt_trailer),
|
||
|
|
Some(&alt_post_span),
|
||
|
|
Some(&alt_profile),
|
||
|
|
)
|
||
|
|
.expect("alt bridge should parse");
|
||
|
|
assert_eq!(
|
||
|
|
alt_bridge.bridge_family,
|
||
|
|
"rt3-105-alt-save-post-span-bridge-v1"
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
alt_bridge.next_candidate_delta_from_packed_profile,
|
||
|
|
Some(260)
|
||
|
|
);
|
||
|
|
|
||
|
|
let scenario_trailer = SmpRuntimeTrailerBlock {
|
||
|
|
profile_family: "rt3-105-scenario-save-container-v1".to_string(),
|
||
|
|
trailer_family: "unknown".to_string(),
|
||
|
|
trailer_offset: 864,
|
||
|
|
length_word_7: 0,
|
||
|
|
length_word_7_hex: "0x00000000".to_string(),
|
||
|
|
length_high_u16: 0,
|
||
|
|
length_high_hex: "0x0000".to_string(),
|
||
|
|
selector_word_8: 0x0001_86a0,
|
||
|
|
selector_word_8_hex: "0x000186a0".to_string(),
|
||
|
|
selector_high_u16: 0x0001,
|
||
|
|
selector_high_hex: "0x0001".to_string(),
|
||
|
|
descriptor_word_10: 0x0186_a000,
|
||
|
|
descriptor_word_10_hex: "0x0186a000".to_string(),
|
||
|
|
descriptor_high_u16: 0x0186,
|
||
|
|
descriptor_high_hex: "0x0186".to_string(),
|
||
|
|
..base_trailer.clone()
|
||
|
|
};
|
||
|
|
let scenario_post_span = SmpRuntimePostSpanProbe {
|
||
|
|
profile_family: "rt3-105-scenario-save-container-v1".to_string(),
|
||
|
|
span_target_offset: 864,
|
||
|
|
next_aligned_candidate_offset: Some(940),
|
||
|
|
header_candidates: vec![SmpRuntimePostSpanHeaderCandidate {
|
||
|
|
offset: 940,
|
||
|
|
words: vec![],
|
||
|
|
hex_words: vec![],
|
||
|
|
dense_word_count: 3,
|
||
|
|
high_u16_words: vec![0x0186, 0x0006, 0x0006, 0x0001],
|
||
|
|
high_hex_words: vec![],
|
||
|
|
grounded_alignments: vec![],
|
||
|
|
}],
|
||
|
|
..base_post_span.clone()
|
||
|
|
};
|
||
|
|
let scenario_profile = SmpRt3105PackedProfileProbe {
|
||
|
|
profile_family: "rt3-105-scenario-save-container-v1".to_string(),
|
||
|
|
packed_profile_block: SmpRt3105PackedProfileBlock {
|
||
|
|
map_path: Some("Southern Pacific.gmp".to_string()),
|
||
|
|
display_name: Some("Southern Pacific".to_string()),
|
||
|
|
profile_byte_0x82: 0x90,
|
||
|
|
profile_byte_0x82_hex: "0x90".to_string(),
|
||
|
|
..base_profile.packed_profile_block.clone()
|
||
|
|
},
|
||
|
|
..base_profile.clone()
|
||
|
|
};
|
||
|
|
let scenario_bridge = parse_rt3_105_post_span_bridge_probe(
|
||
|
|
Some(&scenario_trailer),
|
||
|
|
Some(&scenario_post_span),
|
||
|
|
Some(&scenario_profile),
|
||
|
|
)
|
||
|
|
.expect("scenario bridge should parse");
|
||
|
|
assert_eq!(
|
||
|
|
scenario_bridge.bridge_family,
|
||
|
|
"rt3-105-scenario-post-span-bridge-v1"
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
scenario_bridge.next_candidate_delta_from_packed_profile,
|
||
|
|
Some(-28692)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parses_rt3_105_save_bridge_payload_probe() {
|
||
|
|
let mut bytes = vec![0u8; 0x7000];
|
||
|
|
let primary = 0x4f14usize;
|
||
|
|
let secondary = 0x671cusize;
|
||
|
|
let primary_words: [u32; 8] = [
|
||
|
|
0x62000000, 0x00000000, 0xfff70000, 0x55150000, 0x55550000, 0x00000000, 0xfff70000,
|
||
|
|
0x54550000,
|
||
|
|
];
|
||
|
|
for (index, word) in primary_words.iter().enumerate() {
|
||
|
|
bytes[primary + index * 4..primary + (index + 1) * 4]
|
||
|
|
.copy_from_slice(&(*word).to_le_bytes());
|
||
|
|
}
|
||
|
|
|
||
|
|
let secondary_words: [u32; 8] = [
|
||
|
|
0x00050000, 0x00050005, 0xfff70000, 0x54540000, 0x545400f9, 0x00f900f9, 0x00f94008,
|
||
|
|
0x00001555,
|
||
|
|
];
|
||
|
|
for (index, word) in secondary_words.iter().enumerate() {
|
||
|
|
bytes[secondary + index * 4..secondary + (index + 1) * 4]
|
||
|
|
.copy_from_slice(&(*word).to_le_bytes());
|
||
|
|
}
|
||
|
|
|
||
|
|
let bridge_probe = SmpRt3105PostSpanBridgeProbe {
|
||
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
||
|
|
bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(),
|
||
|
|
bridge_evidence: vec![],
|
||
|
|
span_target_offset: 0x3678,
|
||
|
|
next_candidate_offset: Some(primary),
|
||
|
|
next_candidate_delta_from_span_target: Some(primary - 0x3678),
|
||
|
|
packed_profile_offset: 0x73c0,
|
||
|
|
packed_profile_delta_from_span_target: 0x3d48,
|
||
|
|
next_candidate_delta_from_packed_profile: Some(primary as i64 - 0x73c0),
|
||
|
|
selector_high_u16: 0x7110,
|
||
|
|
selector_high_hex: "0x7110".to_string(),
|
||
|
|
descriptor_high_u16: 0x7801,
|
||
|
|
descriptor_high_hex: "0x7801".to_string(),
|
||
|
|
next_candidate_high_u16_words: vec![0x6200, 0x0000, 0xfff7, 0x5515],
|
||
|
|
next_candidate_high_hex_words: vec![],
|
||
|
|
};
|
||
|
|
|
||
|
|
let probe = parse_rt3_105_save_bridge_payload_probe(&bytes, Some(&bridge_probe))
|
||
|
|
.expect("save bridge payload probe should parse");
|
||
|
|
|
||
|
|
assert_eq!(probe.primary_block_offset, primary);
|
||
|
|
assert_eq!(probe.primary_block_len, 0x20);
|
||
|
|
assert_eq!(probe.secondary_block_offset, secondary);
|
||
|
|
assert_eq!(probe.secondary_block_delta_from_primary, 0x1808);
|
||
|
|
assert_eq!(probe.secondary_block_end_offset, 0x73c0);
|
||
|
|
assert_eq!(probe.secondary_block_len, 0xca4);
|
||
|
|
assert_eq!(probe.primary_words[..4], primary_words[..4]);
|
||
|
|
assert_eq!(probe.secondary_words[..8], secondary_words[..8]);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parses_rt3_105_save_name_table_probe() {
|
||
|
|
let mut bytes = vec![0u8; 0x7400];
|
||
|
|
let secondary = 0x671cusize;
|
||
|
|
let header = secondary + 0x354;
|
||
|
|
let entries = secondary + 0x3b5;
|
||
|
|
let stride = 0x22usize;
|
||
|
|
let names = ["AluminumMill", "Nuclear Power Plant", "Bakery"];
|
||
|
|
|
||
|
|
bytes[header..header + 4].copy_from_slice(&0x10000000u32.to_le_bytes());
|
||
|
|
bytes[header + 4..header + 8].copy_from_slice(&0x00009000u32.to_le_bytes());
|
||
|
|
bytes[header + 8..header + 12].copy_from_slice(&0x0000332eu32.to_le_bytes());
|
||
|
|
bytes[header + 0x1c..header + 0x20].copy_from_slice(&4u32.to_le_bytes());
|
||
|
|
bytes[header + 0x20..header + 0x24].copy_from_slice(&(names.len() as u32).to_le_bytes());
|
||
|
|
bytes[header + 12..header + 16].copy_from_slice(&1u32.to_le_bytes());
|
||
|
|
bytes[header + 16..header + 20].copy_from_slice(&0x22u32.to_le_bytes());
|
||
|
|
bytes[header + 20..header + 24].copy_from_slice(&2u32.to_le_bytes());
|
||
|
|
bytes[header + 24..header + 28].copy_from_slice(&2u32.to_le_bytes());
|
||
|
|
bytes[header + 0x28..header + 0x2c].copy_from_slice(&1u32.to_le_bytes());
|
||
|
|
|
||
|
|
for (index, name) in names.iter().enumerate() {
|
||
|
|
let off = entries + index * stride;
|
||
|
|
let raw = &mut bytes[off..off + stride];
|
||
|
|
raw[..name.len()].copy_from_slice(name.as_bytes());
|
||
|
|
let trailer = if *name == "Nuclear Power Plant" {
|
||
|
|
0u32
|
||
|
|
} else {
|
||
|
|
1u32
|
||
|
|
};
|
||
|
|
raw[stride - 4..stride].copy_from_slice(&trailer.to_le_bytes());
|
||
|
|
}
|
||
|
|
let footer = entries + names.len() * stride;
|
||
|
|
bytes[footer..footer + 4].copy_from_slice(&0x32dcu32.to_le_bytes());
|
||
|
|
bytes[footer + 4..footer + 8].copy_from_slice(&0x3714u32.to_le_bytes());
|
||
|
|
bytes[footer + 8] = 0x00;
|
||
|
|
|
||
|
|
let payload = SmpRt3105SaveBridgePayloadProbe {
|
||
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
||
|
|
bridge_family: "rt3-105-save-post-span-bridge-v1".to_string(),
|
||
|
|
primary_block_offset: 0x4f14,
|
||
|
|
primary_block_len: 0x20,
|
||
|
|
primary_block_len_hex: "0x20".to_string(),
|
||
|
|
primary_words: vec![],
|
||
|
|
primary_hex_words: vec![],
|
||
|
|
secondary_block_offset: secondary,
|
||
|
|
secondary_block_delta_from_primary: 0x1808,
|
||
|
|
secondary_block_delta_from_primary_hex: "0x1808".to_string(),
|
||
|
|
secondary_block_end_offset: footer + 9,
|
||
|
|
secondary_block_len: footer + 9 - secondary,
|
||
|
|
secondary_block_len_hex: format!("0x{:x}", footer + 9 - secondary),
|
||
|
|
secondary_preview_word_count: 32,
|
||
|
|
secondary_words: vec![],
|
||
|
|
secondary_hex_words: vec![],
|
||
|
|
evidence: vec![],
|
||
|
|
};
|
||
|
|
|
||
|
|
let probe = parse_rt3_105_save_name_table_probe(
|
||
|
|
&bytes,
|
||
|
|
Some("gms"),
|
||
|
|
Some(&SmpContainerProfile {
|
||
|
|
profile_family: "rt3-105-save-container-v1".to_string(),
|
||
|
|
profile_evidence: vec![],
|
||
|
|
is_known_profile: true,
|
||
|
|
}),
|
||
|
|
Some(&payload),
|
||
|
|
)
|
||
|
|
.expect("save name table probe should parse");
|
||
|
|
|
||
|
|
assert_eq!(probe.source_kind, "save-bridge-secondary-block");
|
||
|
|
assert_eq!(
|
||
|
|
probe.semantic_family,
|
||
|
|
"scenario-named-candidate-availability-table"
|
||
|
|
);
|
||
|
|
assert_eq!(probe.header_offset, header);
|
||
|
|
assert_eq!(probe.entry_stride, stride);
|
||
|
|
assert_eq!(probe.observed_entry_capacity, 4);
|
||
|
|
assert_eq!(probe.observed_entry_count, names.len());
|
||
|
|
assert_eq!(probe.entries[0].text, "AluminumMill");
|
||
|
|
assert_eq!(probe.entries[0].availability_dword, 1);
|
||
|
|
assert_eq!(probe.entries[2].text, "Bakery");
|
||
|
|
assert_eq!(probe.zero_trailer_entry_count, 1);
|
||
|
|
assert_eq!(
|
||
|
|
probe.zero_trailer_entry_names,
|
||
|
|
vec!["Nuclear Power Plant".to_string()]
|
||
|
|
);
|
||
|
|
assert_eq!(probe.trailing_footer_hex, "dc3200001437000000");
|
||
|
|
assert_eq!(probe.footer_progress_word_0, 0x32dc);
|
||
|
|
assert_eq!(probe.footer_progress_word_1, 0x3714);
|
||
|
|
assert_eq!(probe.footer_trailing_byte, 0x00);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parses_rt3_105_map_name_table_probe_from_fixed_offsets() {
|
||
|
|
let mut bytes = vec![0u8; 0x7400];
|
||
|
|
let header = 0x6a70usize;
|
||
|
|
let entries = 0x6ad1usize;
|
||
|
|
let stride = 0x22usize;
|
||
|
|
let observed_entry_count = 67usize;
|
||
|
|
|
||
|
|
bytes[header..header + 4].copy_from_slice(&0x00000000u32.to_le_bytes());
|
||
|
|
bytes[header + 4..header + 8].copy_from_slice(&0x00000000u32.to_le_bytes());
|
||
|
|
bytes[header + 8..header + 12].copy_from_slice(&0x0000332eu32.to_le_bytes());
|
||
|
|
bytes[header + 12..header + 16].copy_from_slice(&1u32.to_le_bytes());
|
||
|
|
bytes[header + 16..header + 20].copy_from_slice(&0x22u32.to_le_bytes());
|
||
|
|
bytes[header + 20..header + 24].copy_from_slice(&2u32.to_le_bytes());
|
||
|
|
bytes[header + 24..header + 28].copy_from_slice(&2u32.to_le_bytes());
|
||
|
|
bytes[header + 0x1c..header + 0x20].copy_from_slice(&0x44u32.to_le_bytes());
|
||
|
|
bytes[header + 0x20..header + 0x24]
|
||
|
|
.copy_from_slice(&(observed_entry_count as u32).to_le_bytes());
|
||
|
|
bytes[header + 0x28..header + 0x2c].copy_from_slice(&1u32.to_le_bytes());
|
||
|
|
|
||
|
|
for index in 0..observed_entry_count {
|
||
|
|
let name = match index {
|
||
|
|
0 => "AutoPlant".to_string(),
|
||
|
|
1 => "Nuclear Power Plant".to_string(),
|
||
|
|
66 => "Warehouse11".to_string(),
|
||
|
|
_ => format!("Entry{index:02}"),
|
||
|
|
};
|
||
|
|
let off = entries + index * stride;
|
||
|
|
let raw = &mut bytes[off..off + stride];
|
||
|
|
raw[..name.len()].copy_from_slice(name.as_bytes());
|
||
|
|
let trailer = if name == "Nuclear Power Plant" {
|
||
|
|
0u32
|
||
|
|
} else {
|
||
|
|
1u32
|
||
|
|
};
|
||
|
|
raw[stride - 4..stride].copy_from_slice(&trailer.to_le_bytes());
|
||
|
|
}
|
||
|
|
|
||
|
|
let footer = entries + observed_entry_count * stride;
|
||
|
|
bytes[footer..footer + 4].copy_from_slice(&0x32dcu32.to_le_bytes());
|
||
|
|
bytes[footer + 4..footer + 8].copy_from_slice(&0x3714u32.to_le_bytes());
|
||
|
|
bytes[footer + 8] = 0x00;
|
||
|
|
|
||
|
|
let probe = parse_rt3_105_save_name_table_probe(
|
||
|
|
&bytes,
|
||
|
|
Some("gmp"),
|
||
|
|
Some(&SmpContainerProfile {
|
||
|
|
profile_family: "rt3-105-map-container-v1".to_string(),
|
||
|
|
profile_evidence: vec![],
|
||
|
|
is_known_profile: true,
|
||
|
|
}),
|
||
|
|
None,
|
||
|
|
)
|
||
|
|
.expect("map name table probe should parse");
|
||
|
|
|
||
|
|
assert_eq!(probe.profile_family, "rt3-105-map-container-v1");
|
||
|
|
assert_eq!(probe.source_kind, "map-fixed-catalog-range");
|
||
|
|
assert_eq!(probe.header_offset, header);
|
||
|
|
assert_eq!(probe.entries_offset, entries);
|
||
|
|
assert_eq!(probe.observed_entry_count, observed_entry_count);
|
||
|
|
assert_eq!(probe.entries[0].text, "AutoPlant");
|
||
|
|
assert_eq!(probe.entries[66].text, "Warehouse11");
|
||
|
|
assert_eq!(
|
||
|
|
probe.zero_trailer_entry_names,
|
||
|
|
vec!["Nuclear Power Plant".to_string()]
|
||
|
|
);
|
||
|
|
assert_eq!(probe.footer_progress_word_0, 0x32dc);
|
||
|
|
assert_eq!(probe.footer_progress_word_1, 0x3714);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classifies_rt3_105_alt_save_container_profile() {
|
||
|
|
let shared_header = SmpSharedHeader {
|
||
|
|
byte_len: 64,
|
||
|
|
root_kind_word: 0x000025e5,
|
||
|
|
root_kind_word_hex: "0x000025e5".to_string(),
|
||
|
|
primary_family_tag: 0x00002ee0,
|
||
|
|
primary_family_tag_hex: "0x00002ee0".to_string(),
|
||
|
|
shared_signature_words_1_to_7: vec![
|
||
|
|
0x00002ee0, 0x0001c001, 0x00018000, 0x00010000, 0x00000754, 0x00000754, 0x00000754,
|
||
|
|
],
|
||
|
|
shared_signature_hex_words_1_to_7: vec![
|
||
|
|
"0x00002ee0".to_string(),
|
||
|
|
"0x0001c001".to_string(),
|
||
|
|
"0x00018000".to_string(),
|
||
|
|
"0x00010000".to_string(),
|
||
|
|
"0x00000754".to_string(),
|
||
|
|
"0x00000754".to_string(),
|
||
|
|
"0x00000754".to_string(),
|
||
|
|
],
|
||
|
|
matches_grounded_common_signature: false,
|
||
|
|
payload_window_words_8_to_9: vec![0x007a5978, 0x007a9022],
|
||
|
|
payload_window_hex_words_8_to_9: vec![
|
||
|
|
"0x007a5978".to_string(),
|
||
|
|
"0x007a9022".to_string(),
|
||
|
|
],
|
||
|
|
reserved_words_10_to_14: vec![0; 5],
|
||
|
|
reserved_words_10_to_14_all_zero: true,
|
||
|
|
final_flag_word: 0,
|
||
|
|
final_flag_word_hex: "0x00000000".to_string(),
|
||
|
|
};
|
||
|
|
let early_content_probe = SmpEarlyContentProbe {
|
||
|
|
first_post_text_nonzero_offset: 722,
|
||
|
|
zero_pad_after_text_len: 431,
|
||
|
|
first_post_text_block_len: 35,
|
||
|
|
first_post_text_block_hex:
|
||
|
|
"0101010000010000000000000100000000000000010000000000000000010100000001".to_string(),
|
||
|
|
trailing_zero_pad_after_first_block_len: 45,
|
||
|
|
secondary_nonzero_offset: Some(802),
|
||
|
|
secondary_aligned_word_window_offset: Some(800),
|
||
|
|
secondary_aligned_word_window_words: vec![
|
||
|
|
0x00010000, 0x49f00100, 0x00000002, 0xa0000000, 0x00000186, 0x00000000, 0x000186a0,
|
||
|
|
0x00000000,
|
||
|
|
],
|
||
|
|
secondary_aligned_word_window_hex_words: vec![
|
||
|
|
"0x00010000".to_string(),
|
||
|
|
"0x49f00100".to_string(),
|
||
|
|
"0x00000002".to_string(),
|
||
|
|
"0xa0000000".to_string(),
|
||
|
|
"0x00000186".to_string(),
|
||
|
|
"0x00000000".to_string(),
|
||
|
|
"0x000186a0".to_string(),
|
||
|
|
"0x00000000".to_string(),
|
||
|
|
],
|
||
|
|
secondary_preview_hex:
|
||
|
|
"01000001f04902000000000000a08601000000000000a08601000000000000a0".to_string(),
|
||
|
|
};
|
||
|
|
|
||
|
|
let header_variant = classify_header_variant_probe(&shared_header);
|
||
|
|
let secondary_variant =
|
||
|
|
classify_secondary_variant_probe(&early_content_probe).expect("secondary probe");
|
||
|
|
let container_profile = classify_container_profile(
|
||
|
|
Some("gms"),
|
||
|
|
Some(&header_variant),
|
||
|
|
Some(&secondary_variant),
|
||
|
|
)
|
||
|
|
.expect("container profile");
|
||
|
|
|
||
|
|
assert_eq!(header_variant.variant_family, "rt3-105-alt-save-header-v1");
|
||
|
|
assert_eq!(
|
||
|
|
secondary_variant.variant_family,
|
||
|
|
"rt3-105-gms-alt-family-v1"
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
container_profile.profile_family,
|
||
|
|
"rt3-105-alt-save-container-v1"
|
||
|
|
);
|
||
|
|
assert!(container_profile.is_known_profile);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classifies_rt3_105_map_container_profiles_from_header_families() {
|
||
|
|
let scenario_profile = classify_container_profile(
|
||
|
|
Some("gmp"),
|
||
|
|
Some(&SmpHeaderVariantProbe {
|
||
|
|
variant_family: "rt3-105-scenario-save-header-v1".to_string(),
|
||
|
|
variant_evidence: vec![],
|
||
|
|
is_known_family: true,
|
||
|
|
}),
|
||
|
|
Some(&SmpSecondaryVariantProbe {
|
||
|
|
aligned_window_offset: 0,
|
||
|
|
words: vec![1, 0, 0, 0],
|
||
|
|
hex_words: vec![],
|
||
|
|
variant_family: "unknown".to_string(),
|
||
|
|
variant_evidence: vec![],
|
||
|
|
}),
|
||
|
|
)
|
||
|
|
.expect("scenario map profile");
|
||
|
|
|
||
|
|
let alt_profile = classify_container_profile(
|
||
|
|
Some("gmp"),
|
||
|
|
Some(&SmpHeaderVariantProbe {
|
||
|
|
variant_family: "rt3-105-alt-save-header-v1".to_string(),
|
||
|
|
variant_evidence: vec![],
|
||
|
|
is_known_family: true,
|
||
|
|
}),
|
||
|
|
Some(&SmpSecondaryVariantProbe {
|
||
|
|
aligned_window_offset: 0,
|
||
|
|
words: vec![0x49f00100, 2, 0xa0000000, 0x186],
|
||
|
|
hex_words: vec![],
|
||
|
|
variant_family: "unknown".to_string(),
|
||
|
|
variant_evidence: vec![],
|
||
|
|
}),
|
||
|
|
)
|
||
|
|
.expect("alt map profile");
|
||
|
|
|
||
|
|
assert_eq!(
|
||
|
|
scenario_profile.profile_family,
|
||
|
|
"rt3-105-scenario-map-container-v1"
|
||
|
|
);
|
||
|
|
assert!(scenario_profile.is_known_profile);
|
||
|
|
assert_eq!(alt_profile.profile_family, "rt3-105-alt-map-container-v1");
|
||
|
|
assert!(alt_profile.is_known_profile);
|
||
|
|
}
|
||
|
|
}
|