larian_formats/lspk/
read.rs

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
17/// Reads a .pak file with "magic number" LSPK.
18pub type Reader<R> = crate::reader::Reader<LspkParser, R>;
19
20/// A successfully parsed LSPK .pak file from within a zipfile.
21pub struct DecompressedLspkZip {
22    pub sanitized_name: String,
23    pub lspk: DecompressedLspk,
24}
25
26/// Inspects a zipfile for a file with the .pak extension and reads it.
27pub 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/// A successfully decompressed file from within an LSPK .pak archive.
50#[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    /// Deserializes the contents of the file into the expected XML format of a `meta.lsx` file.
87    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/// A successfully decompressed LSPK .pak archive.
98#[derive(Debug, Clone)]
99pub struct DecompressedLspk {
100    pub original_bytes: Vec<u8>,
101    pub files: Vec<DecompressedFile>,
102}
103
104impl DecompressedLspk {
105    /// Looks for a file a file called `meta.lsx` within a subdirectory of the `Mods` directory
106    /// contained in the archive and attempts to deserialize it.
107    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/// Contains the logic to parse an LSPK .pak file.
128#[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            // Last type bytes are used for metadata, so parse as a u32 and a u16 and then combine.
231            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                // Checking the lower four bits shouldn't ever be possible to find anything else
250                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    // println!("{compressed_bytes:?}");
312
313    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}