tiger_pkg/
d2_shared.rs

1use std::{
2    borrow::Cow,
3    collections::hash_map::Entry,
4    fs::File,
5    io::{Read, Seek, SeekFrom},
6    sync::Arc,
7};
8
9use ahash::AHashMap;
10use anyhow::Context;
11use binrw::{BinRead, BinReaderExt, NullString};
12use bitflags::bitflags;
13use parking_lot::RwLock;
14
15use crate::{
16    block_cache::BlockCache,
17    crypto::PkgGcmState,
18    oodle,
19    package::{PackageLanguage, ReadSeek, UEntryHeader},
20    DestinyVersion, GameVersion, TagHash,
21};
22
23#[derive(BinRead, Debug, Clone)]
24pub struct EntryHeader {
25    pub reference: u32,
26
27    _type_info: u32,
28
29    #[br(calc = (_type_info >> 9) as u8 & 0x7f)]
30    pub file_type: u8,
31    #[br(calc = (_type_info >> 6) as u8 & 0x7)]
32    pub file_subtype: u8,
33
34    _block_info: u64,
35
36    #[br(calc = _block_info as u32 & 0x3fff)]
37    pub starting_block: u32,
38
39    #[br(calc = ((_block_info >> 14) as u32 & 0x3FFF) << 4)]
40    pub starting_block_offset: u32,
41
42    #[br(calc = (_block_info >> 28) as u32)]
43    pub file_size: u32,
44}
45
46#[derive(BinRead, Debug, Clone)]
47pub struct BlockHeader {
48    pub offset: u32,
49    pub size: u32,
50    pub patch_id: u16,
51    #[br(map(|v: u16| BlockFlags::from_bits_retain(v)))]
52    pub flags: BlockFlags,
53    pub _hash: [u8; 20],
54    pub gcm_tag: [u8; 16],
55}
56
57bitflags! {
58    #[derive(Debug, Clone, Copy)]
59    pub struct BlockFlags: u16 {
60        const COMPRESSED = 0x1;
61        const ENCRYPTED = 0x2;
62        const ALT_CIPHER = 0x4;
63        const REDACTED = 0x8;
64    }
65}
66
67#[derive(BinRead, Debug, Clone)]
68pub struct HashTableEntry {
69    pub hash64: u64,
70    pub hash32: TagHash,
71    pub reference: TagHash,
72}
73
74pub const BLOCK_SIZE: usize = 0x40000;
75
76pub struct CommonPackageData {
77    pub pkg_id: u16,
78    pub patch_id: u16,
79    pub group_id: u64,
80    pub entries: Vec<EntryHeader>,
81    pub blocks: Vec<BlockHeader>,
82    pub wide_hashes: Vec<HashTableEntry>,
83    pub language: PackageLanguage,
84}
85
86pub struct PackageCommonD2 {
87    pub(crate) version: DestinyVersion,
88    pub(crate) pkg_id: u16,
89    pub(crate) patch_id: u16,
90    pub(crate) language: PackageLanguage,
91
92    pub(crate) gcm: RwLock<PkgGcmState>,
93    pub(crate) _entries: Vec<EntryHeader>,
94    pub(crate) entries_unified: Arc<[UEntryHeader]>,
95    pub(crate) blocks: Vec<BlockHeader>,
96    pub(crate) wide_hashes: Vec<HashTableEntry>,
97
98    pub(crate) reader: RwLock<Box<dyn ReadSeek>>,
99    pub(crate) path_base: String,
100
101    block_cache: BlockCache,
102    pub(crate) file_handles: RwLock<AHashMap<usize, File>>,
103}
104
105impl PackageCommonD2 {
106    pub fn new<R: ReadSeek + 'static>(
107        reader: R,
108        version: DestinyVersion,
109        path: String,
110        data: CommonPackageData,
111    ) -> anyhow::Result<PackageCommonD2> {
112        let CommonPackageData {
113            pkg_id,
114            patch_id,
115            group_id,
116            entries,
117            blocks,
118            wide_hashes,
119            language,
120        } = data;
121
122        let last_underscore_pos = path.rfind('_').unwrap();
123        let path_base = path[..last_underscore_pos].to_owned();
124
125        let entries_unified: Vec<UEntryHeader> = entries
126            .iter()
127            .map(|e| UEntryHeader {
128                reference: e.reference,
129                file_type: e.file_type,
130                file_subtype: e.file_subtype,
131                starting_block: e.starting_block,
132                starting_block_offset: e.starting_block_offset,
133                file_size: e.file_size,
134            })
135            .collect();
136
137        Ok(PackageCommonD2 {
138            version,
139            pkg_id,
140            patch_id,
141            language,
142            gcm: RwLock::new(PkgGcmState::new(
143                pkg_id,
144                GameVersion::Destiny(version),
145                group_id,
146            )),
147            _entries: entries,
148            entries_unified: entries_unified.into(),
149            blocks,
150            wide_hashes,
151            reader: RwLock::new(Box::new(reader)),
152            path_base,
153            block_cache: BlockCache::new(),
154            file_handles: Default::default(),
155        })
156    }
157
158    fn get_block_raw(&self, block_index: usize) -> anyhow::Result<Cow<'_, [u8]>> {
159        let bh = &self.blocks[block_index];
160        let mut data = vec![0u8; bh.size as usize];
161
162        if self.patch_id == bh.patch_id {
163            self.reader
164                .write()
165                .seek(SeekFrom::Start(bh.offset as u64))?;
166            self.reader.write().read_exact(&mut data)?;
167        } else {
168            match self.file_handles.write().entry(bh.patch_id as _) {
169                Entry::Occupied(mut f) => {
170                    let f = f.get_mut();
171                    f.seek(SeekFrom::Start(bh.offset as u64))?;
172                    f.read_exact(&mut data)?;
173                }
174                Entry::Vacant(e) => {
175                    let f = File::open(format!("{}_{}.pkg", self.path_base, bh.patch_id))
176                        .with_context(|| {
177                            format!(
178                                "Failed to open package file {}_{}.pkg",
179                                self.path_base, bh.patch_id
180                            )
181                        })?;
182
183                    let f = e.insert(f);
184                    f.seek(SeekFrom::Start(bh.offset as u64))?;
185                    f.read_exact(&mut data)?;
186                }
187            };
188        };
189
190        Ok(Cow::Owned(data))
191    }
192
193    /// Reads, decrypts and decompresses the specified block
194    fn read_block(&self, block_index: usize) -> anyhow::Result<Vec<u8>> {
195        let bh = self.blocks[block_index].clone();
196
197        let mut block_data = self.get_block_raw(block_index)?.to_vec();
198
199        if bh.flags.contains(BlockFlags::ENCRYPTED) {
200            self.gcm
201                .write()
202                .decrypt_block_in_place(bh.flags, &bh.gcm_tag, &mut block_data)?;
203        };
204
205        let decompressed_data = if bh.flags.contains(BlockFlags::COMPRESSED) {
206            let mut buffer = vec![0u8; BLOCK_SIZE];
207            let _decompressed_size = match self.version {
208                // Destiny 1
209                DestinyVersion::DestinyInternalAlpha
210                | DestinyVersion::DestinyFirstLookAlpha
211                | DestinyVersion::DestinyTheTakenKing
212                | DestinyVersion::DestinyRiseOfIron => oodle::decompress_3,
213
214                // Destiny 2 (Red War - Beyond Light)
215                DestinyVersion::Destiny2Beta
216                | DestinyVersion::Destiny2Forsaken
217                | DestinyVersion::Destiny2Shadowkeep => oodle::decompress_3,
218
219                // Destiny 2 (Beyond Light - Latest)
220                DestinyVersion::Destiny2BeyondLight
221                | DestinyVersion::Destiny2WitchQueen
222                | DestinyVersion::Destiny2Lightfall
223                | DestinyVersion::Destiny2TheFinalShape
224                | DestinyVersion::Destiny2TheEdgeOfFate => oodle::decompress_9,
225            }(&block_data, &mut buffer)?;
226
227            buffer
228        } else {
229            block_data
230        };
231
232        Ok(decompressed_data)
233    }
234
235    pub fn get_block(&self, block_index: usize) -> anyhow::Result<Arc<Vec<u8>>> {
236        self.block_cache.get(block_index, |i| self.read_block(i))
237    }
238}
239
240#[derive(Debug, Clone, bincode::Decode, bincode::Encode)]
241pub struct PackageNamedTagEntry {
242    pub hash: TagHash,
243    pub class_hash: u32,
244    pub name: String,
245}
246
247impl BinRead for PackageNamedTagEntry {
248    type Args<'a> = ();
249
250    fn read_options<R: Read + Seek>(
251        reader: &mut R,
252        endian: binrw::Endian,
253        _args: Self::Args<'_>,
254    ) -> binrw::BinResult<Self> {
255        let hash = reader.read_type(endian)?;
256        let class_hash = reader.read_type(endian)?;
257
258        let name_offset: u64 = reader.read_type(endian)?;
259        let pos_save = reader.stream_position()?;
260
261        reader.seek(SeekFrom::Start(pos_save - 8 + name_offset))?;
262        let name_cstring: NullString = reader.read_type(endian)?;
263        reader.seek(SeekFrom::Start(pos_save))?;
264
265        Ok(Self {
266            hash,
267            class_hash,
268            name: name_cstring.to_string(),
269        })
270    }
271}