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