tiger-pkg 0.20.1

Tiger engine package reading library for Destiny 1/2 and Marathon
Documentation
use std::{
    fs::File,
    io::{BufReader, Seek, SeekFrom},
    sync::Arc,
};

use anyhow::Context;
use binrw::{BinReaderExt, Endian, VecArgs};

use crate::{
    d2_beyondlight::structs::PackageHeader,
    d2_shared::{
        BlockFlags, CommonPackageData, HashTableEntry, PackageCommonD2, PackageNamedTagEntry,
    },
    package::{
        Package, PackageLanguage, PackagePlatform, ReadSeek, Redaction, UEntryHeader,
        UHashTableEntry,
    },
    DestinyVersion,
};

pub struct PackageD2BeyondLight {
    common: PackageCommonD2,
    pub header: PackageHeader,
    pub named_tags: Vec<PackageNamedTagEntry>,
}

unsafe impl Send for PackageD2BeyondLight {}
unsafe impl Sync for PackageD2BeyondLight {}

impl PackageD2BeyondLight {
    pub fn open(path: &str, version: DestinyVersion) -> anyhow::Result<PackageD2BeyondLight> {
        let reader = File::open(path).with_context(|| format!("Cannot find file '{path}'"))?;

        Self::from_reader(path, reader, version)
    }

    pub fn from_reader<R: ReadSeek + 'static>(
        path: &str,
        reader: R,
        version: DestinyVersion,
    ) -> anyhow::Result<PackageD2BeyondLight> {
        let mut reader = BufReader::new(reader);
        let header: PackageHeader = reader.read_le()?;

        reader.seek(SeekFrom::Start(header.entry_table_offset as _))?;
        let entries = reader.read_le_args(VecArgs {
            count: header.entry_table_size as _,
            inner: (),
        })?;

        reader.seek(SeekFrom::Start(header.block_table_offset as _))?;
        let blocks = reader.read_le_args(VecArgs {
            count: header.block_table_size as _,
            inner: (),
        })?;

        reader.seek(SeekFrom::Start(header.named_tag_table_offset as u64 + 0x30))?;
        let named_tags = reader.read_le_args(VecArgs {
            count: header.named_tag_table_size as _,
            inner: (),
        })?;

        let wide_hashes: Vec<HashTableEntry> = if header.h64_table_size != 0 {
            reader.seek(SeekFrom::Start((header.h64_table_offset + 0x10) as _))?;
            let offset = reader.read_le::<i64>()?;
            reader.seek(SeekFrom::Start(
                (header.h64_table_offset as i64 + 0x20 + offset) as _,
            ))?;
            reader.read_le_args(VecArgs {
                count: header.h64_table_size as _,
                inner: (),
            })?
        } else {
            vec![]
        };

        Ok(PackageD2BeyondLight {
            common: PackageCommonD2::new(
                reader.into_inner(),
                version,
                path.to_string(),
                CommonPackageData {
                    pkg_id: header.pkg_id,
                    patch_id: header.patch_id,
                    group_id: header.group_id,
                    entries,
                    blocks,
                    wide_hashes,
                    language: header.language,
                },
            )?,
            header,
            named_tags,
        })
    }
}

// TODO(cohae): Can we implement this on PackageCommon?
impl Package for PackageD2BeyondLight {
    fn endianness(&self) -> Endian {
        Endian::Little // TODO(cohae): Not necessarily
    }

    fn pkg_id(&self) -> u16 {
        self.common.pkg_id
    }

    fn patch_id(&self) -> u16 {
        self.common.patch_id
    }

    fn language(&self) -> PackageLanguage {
        self.common.language
    }

    fn platform(&self) -> PackagePlatform {
        self.header.platform
    }

    fn hash64_table(&self) -> Vec<UHashTableEntry> {
        self.common
            .wide_hashes
            .iter()
            .map(|h| UHashTableEntry {
                hash64: h.hash64,
                hash32: h.hash32,
                reference: h.reference,
            })
            .collect()
    }

    fn named_tags(&self) -> Vec<PackageNamedTagEntry> {
        self.named_tags.clone()
    }

    fn entries(&self) -> &[UEntryHeader] {
        &self.common.entries_unified
    }

    fn entry(&self, index: usize) -> Option<UEntryHeader> {
        self.common.entries_unified.get(index).cloned()
    }

    fn get_block(&self, index: usize) -> anyhow::Result<Arc<Vec<u8>>> {
        self.common.get_block(index)
    }

    fn redaction_level(&self) -> Redaction {
        let num_redacted = self
            .common
            .blocks
            .iter()
            .filter(|b| b.flags.contains(BlockFlags::REDACTED))
            .count();

        if num_redacted == 0 {
            Redaction::None
        } else if num_redacted == self.common.blocks.len() {
            Redaction::Full
        } else {
            Redaction::Partial
        }
    }
}