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