1use super::{DECOMPRESSED_TABLE_ENTRY_LENGTH, TABLE_ENTRY_PATH_LENGTH};
2use crate::{
3 bg3::{raw::Save, ModPakLsx},
4 error::{Error, Result},
5 format::Parser,
6 reader::ReadSeek,
7};
8use flate2::read::ZlibDecoder;
9use rc_zip_sync::{rc_zip::parse::EntryKind, ReadZip};
10use std::{
11 ffi::OsStr,
12 io::{Cursor, Read, Seek, SeekFrom},
13 os::unix::ffi::OsStrExt,
14 path::{Path, PathBuf},
15};
16
17pub type Reader<R> = crate::reader::Reader<LspkParser, R>;
19
20pub struct DecompressedLspkZip {
22 pub sanitized_name: String,
23 pub lspk: DecompressedLspk,
24}
25
26pub fn entry_from_zipfile(reader: &impl ReadZip) -> Result<(String, DecompressedLspk)> {
28 let (name, bytes) = reader
29 .read_zip()?
30 .entries()
31 .find_map(|e| {
32 e.sanitized_name()
33 .filter(|_| matches!(e.kind(), EntryKind::File))
34 .filter(|path| {
35 Path::new(path)
36 .extension()
37 .map_or(false, |e| e.eq_ignore_ascii_case("pak"))
38 })
39 .map(|name| (name.into(), e.bytes()))
40 })
41 .ok_or_else(|| Error::MissingLspkFileInZip)?;
42
43 let bytes = bytes?;
44
45 let lspk = Reader::new(Cursor::new(bytes))?.read()?;
46 Ok((name, lspk))
47}
48
49#[derive(Debug, Clone)]
51pub struct DecompressedFile {
52 pub path: PathBuf,
53 pub decompressed_bytes: Vec<u8>,
54}
55
56fn preprocess(s: &mut String) {
57 let mut start = 0;
58
59 loop {
60 let remaining = &s[start..];
61
62 let Some(mut end_of_current_line) = remaining.find('\n') else {
63 return;
64 };
65
66 let current_line = &remaining[..end_of_current_line];
67
68 if current_line.ends_with('\r') {
69 end_of_current_line -= 1;
70 }
71
72 let Some(end_of_current_element) = current_line.find("/>") else {
73 start += end_of_current_line + 1;
74 continue;
75 };
76
77 let extra_to_remove = (start + end_of_current_element + 2)..(start + end_of_current_line);
78 let replacement = " ".repeat(extra_to_remove.len());
79 s.replace_range(extra_to_remove, &replacement);
80
81 start += end_of_current_line + 1;
82 }
83}
84
85impl DecompressedFile {
86 pub fn deserialize_as_mod_pak(self) -> Result<ModPakLsx> {
88 let mut raw_xml_str = String::from_utf8_lossy(&self.decompressed_bytes).into_owned();
89 preprocess(&mut raw_xml_str);
90
91 let save: Save =
92 quick_xml::de::from_str(&raw_xml_str).map_err(|_| Error::InvalidMetadataFile)?;
93 save.try_into()
94 }
95}
96
97#[derive(Debug, Clone)]
99pub struct DecompressedLspk {
100 pub original_bytes: Vec<u8>,
101 pub files: Vec<DecompressedFile>,
102}
103
104impl DecompressedLspk {
105 pub fn extract_meta_lsx(self) -> Result<DecompressedFile> {
108 self.files
109 .into_iter()
110 .find(|d| {
111 let mut path_components = d.path.components();
112
113 path_components
114 .next()
115 .map(|c| c.as_os_str().to_string_lossy()) ==
116 Some("Mods".into()) &&
117 path_components.next().is_some() &&
118 path_components
119 .next()
120 .map(|c| c.as_os_str().to_string_lossy()) ==
121 Some("meta.lsx".into())
122 })
123 .ok_or(Error::MissingMetadataFile)
124 }
125}
126
127#[derive(Debug)]
129pub struct LspkParser {
130 footer_offset: SeekFrom,
131 num_compressed_files: usize,
132 decompressed_table_length: usize,
133 compressed_table_length: usize,
134}
135
136impl Default for LspkParser {
137 fn default() -> Self {
138 Self {
139 footer_offset: SeekFrom::Start(0),
140 num_compressed_files: Default::default(),
141 decompressed_table_length: Default::default(),
142 compressed_table_length: Default::default(),
143 }
144 }
145}
146
147impl Parser for LspkParser {
148 const ID_BYTES: [u8; 4] = super::ID_BYTES;
149
150 const MIN_SUPPORTED_VERSION: u32 = super::MIN_SUPPORTED_VERSION;
151
152 type Output = DecompressedLspk;
153
154 fn read(&mut self, reader: &mut ReadSeek<impl Read + Seek>) -> Result<Self::Output> {
155 let footer_offset_unsigned = reader.read_u64_from_le_bytes()?;
156 let footer_offset_signed =
157 footer_offset_unsigned
158 .try_into()
159 .map_err(|_| Error::InvalidFooterOffset {
160 footer_offset_found: footer_offset_unsigned,
161 })?;
162
163 self.footer_offset = SeekFrom::Current(footer_offset_signed);
164
165 self.seek_footer(reader)?;
166
167 self.num_compressed_files = reader.read_usize_from_u32_le_bytes()?;
168 self.compressed_table_length = reader.read_usize_from_u32_le_bytes()?;
169 self.decompressed_table_length =
170 self.num_compressed_files * DECOMPRESSED_TABLE_ENTRY_LENGTH;
171
172 let table = self.read_table(reader)?;
173
174 let result: Result<_> = self.parse_file_entries(table, reader).collect();
175
176 let original_bytes = reader.read_all_bytes()?;
177
178 Ok(DecompressedLspk {
179 files: result?,
180 original_bytes,
181 })
182 }
183}
184
185impl LspkParser {
186 fn seek_footer(&self, reader: &mut ReadSeek<impl Read + Seek>) -> Result<()> {
187 reader.seek_starting_position()?;
188 reader.seek(self.footer_offset)?;
189
190 Ok(())
191 }
192
193 fn read_table(&self, reader: &mut ReadSeek<impl Read + Seek>) -> Result<Vec<u8>> {
194 self.seek_footer(reader)?;
195 reader.seek(SeekFrom::Current(8))?;
196
197 let mut compressed_bytes = vec![0; self.compressed_table_length];
198 reader.read_exact(&mut compressed_bytes)?;
199
200 lz4_flex::block::decompress(&compressed_bytes, self.decompressed_table_length)
201 .map_err(Error::TableEntryDecompressionFailed)
202 }
203
204 fn parse_file_entries<'a>(
205 &'a self,
206 mut table: Vec<u8>,
207 reader: &'a mut ReadSeek<impl Read + Seek>,
208 ) -> impl Iterator<Item = Result<DecompressedFile>> + 'a {
209 (0..self.num_compressed_files).map(move |_| {
210 let bytes = if table.is_empty() {
211 return Err(Error::TooShort);
212 } else if table.len() <= DECOMPRESSED_TABLE_ENTRY_LENGTH {
213 std::mem::take(&mut table)
214 } else {
215 let path = table.split_off(DECOMPRESSED_TABLE_ENTRY_LENGTH);
216
217 std::mem::replace(&mut table, path)
218 };
219
220 let end = bytes
221 .iter()
222 .take(TABLE_ENTRY_PATH_LENGTH)
223 .copied()
224 .enumerate()
225 .find_map(|(i, byte)| (byte == 0).then_some(i))
226 .unwrap_or(TABLE_ENTRY_PATH_LENGTH);
227
228 let path = PathBuf::from(OsStr::from_bytes(&bytes[..end]));
229
230 let offset_upper_bytes = bytes[256..260]
232 .try_into()
233 .map(u32::from_le_bytes)
234 .map_err(|_| Error::TooShort)?;
235
236 let offset_lower_bytes = bytes[260..262]
237 .try_into()
238 .map(u16::from_le_bytes)
239 .map_err(|_| Error::TooShort)?;
240
241 let offset = u64::from(offset_upper_bytes) | (u64::from(offset_lower_bytes) << 32);
242
243 let compression_type = match bytes[263] & 0x0F {
244 0 => CompressionType::None,
245 1 => CompressionType::Zlib,
246 2 => CompressionType::Lz4,
247 3 => CompressionType::Zstd,
248
249 other => unreachable!("compression type lower four bit shouldn't ever be {other}"),
251 };
252
253 let num_bytes_compressed = bytes[264..268]
254 .try_into()
255 .map(u32::from_le_bytes)
256 .map_err(|_| Error::TooShort)?;
257
258 let num_bytes_decompressed = bytes[268..272]
259 .try_into()
260 .map(u32::from_le_bytes)
261 .map_err(|_| Error::TooShort)?;
262
263 let offset = offset & 0x000f_ffff_ffff_ffff;
264
265 decompress_file(
266 compression_type,
267 path,
268 offset,
269 usize_from_u32!(num_bytes_compressed),
270 usize_from_u32!(num_bytes_decompressed),
271 reader,
272 )
273 })
274 }
275}
276
277#[derive(Debug, PartialEq, Eq, Clone, Copy)]
278enum CompressionType {
279 None,
280 Zlib,
281 Lz4,
282 Zstd,
283}
284
285fn decompress_file(
286 compression_type: CompressionType,
287 path: PathBuf,
288 offset: u64,
289 num_bytes_compressed: usize,
290 num_bytes_decompressed: usize,
291 reader: &mut ReadSeek<impl Read + Seek>,
292) -> Result<DecompressedFile> {
293 let zlib_compressed = match compression_type {
294 CompressionType::Zlib => true,
295 CompressionType::Zstd => return Err(Error::ZstdNotSupported),
296 _anything_else => false,
297 };
298
299 let offset_signed = offset
300 .try_into()
301 .map_err(|_| Error::InvalidCompressedFileOffset {
302 compressed_file_offset_found: offset,
303 })?;
304
305 reader.seek_starting_position()?;
306 reader.seek(SeekFrom::Current(offset_signed))?;
307
308 let mut compressed_bytes = vec![0_u8; num_bytes_compressed];
309 reader.read_exact(&mut compressed_bytes).unwrap();
310
311 if num_bytes_decompressed == 0 || compression_type == CompressionType::None {
314 return Ok(DecompressedFile {
315 path,
316 decompressed_bytes: compressed_bytes,
317 });
318 }
319
320 let decompressed_bytes = if zlib_compressed {
321 let mut compressed_bytes = compressed_bytes.as_slice();
322 let mut decoder = ZlibDecoder::new(&mut compressed_bytes);
323 let mut decompressed_bytes = vec![0; num_bytes_decompressed];
324
325 match decoder.read_exact(&mut decompressed_bytes) {
326 Ok(()) => Ok(decompressed_bytes),
327 Err(e) => Err(Error::ZlibDecompressionFailed {
328 path: path.clone(),
329 error: e,
330 }),
331 }
332 } else {
333 lz4_flex::block::decompress(&compressed_bytes, num_bytes_decompressed).map_err(|e| {
334 Error::Lz4DecompressionFailed {
335 path: path.clone(),
336 error: e,
337 }
338 })
339 };
340
341 decompressed_bytes.map(|decompressed_bytes| DecompressedFile {
342 path,
343 decompressed_bytes,
344 })
345}