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 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 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 DestinyVersion::DestinyInternalAlpha
200 | DestinyVersion::DestinyFirstLookAlpha
201 | DestinyVersion::DestinyTheTakenKing
202 | DestinyVersion::DestinyRiseOfIron => oodle::decompress_3,
203
204 DestinyVersion::Destiny2Beta
206 | DestinyVersion::Destiny2Forsaken
207 | DestinyVersion::Destiny2Shadowkeep => oodle::decompress_3,
208
209 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}