infinite_rs/module/
loader.rs

1//! Main abstraction file for modules.
2
3use byteorder::{LE, ReadBytesExt};
4use std::{
5    fs::File,
6    io::{BufReader, Seek, SeekFrom},
7    path::Path,
8    ptr::eq,
9};
10
11use super::{
12    block::ModuleBlockEntry,
13    file::{DataOffsetType, ModuleFileEntry},
14    header::{ModuleHeader, ModuleVersion},
15};
16use crate::Result;
17use crate::{
18    Error,
19    common::{errors::TagError, extensions::BufReaderExt},
20};
21
22#[derive(Default, Debug)]
23/// Module structure which contains the layout of the entire module file.
24pub struct ModuleFile {
25    /// Information relating to how the other fields should be read.
26    pub header: ModuleHeader,
27    /// Metadata regarding compression and layout of files (tags).
28    pub files: Vec<ModuleFileEntry>,
29    /// Indices of resource files present in the module.
30    pub resource_indices: Vec<u32>,
31    /// Uncompressed/compressed blocks making up a file.
32    blocks: Vec<ModuleBlockEntry>,
33    /// Offset in [`BufReader`] where file data starts.
34    file_data_offset: u64,
35    /// Reference to the module file buffer.
36    module_file: Option<BufReader<File>>,
37    /// Reference to HD1 buffer if it exists.
38    hd1_file: Option<BufReader<File>>,
39    /// Whether to use the HD1 module or not.
40    pub use_hd1: bool,
41}
42
43impl ModuleFile {
44    /// Instantiates a [`ModuleFile`] object from the given file path.
45    pub fn from_path<T: AsRef<Path>>(file_path: T) -> Result<Self> {
46        let mut module = Self::default();
47        module.read(file_path)?;
48        Ok(module)
49    }
50
51    /// Reads the module file from the given file path.
52    /// This function reads the entire structure of the module file.
53    /// It also calculates and stores important offsets within the file.
54    ///
55    /// # Arguments
56    ///
57    /// * `file_path` - A reference to a type that implements [`Path`] that holds the path to the module file.
58    ///
59    /// # Errors
60    /// - If the reader fails to read the exact number of bytes [`ReadError`](`crate::Error::ReadError`)
61    /// - If the string table has invalid UTF-8 [`Utf8ReadingError`](`crate::Error::Utf8ReadingError`)
62    pub fn read<T: AsRef<Path>>(&mut self, file_path: T) -> Result<()> {
63        let file = File::open(&file_path)?;
64        let mut reader = BufReader::new(file);
65
66        self.header.read(&mut reader)?;
67        self.open_hd1(file_path)?;
68
69        for _ in 0..self.header.file_count {
70            let mut file = ModuleFileEntry::default();
71            file.read(&mut reader, &self.header.version)?;
72            self.files.push(file);
73        }
74
75        let strings_offset = reader.stream_position()?;
76        reader.seek(SeekFrom::Start(
77            strings_offset + u64::from(self.header.strings_size),
78        ))?;
79        self.resource_indices = (0..self.header.resource_count)
80            .map(|_| -> Result<u32> { Ok(reader.read_u32::<LE>()?) })
81            .collect::<Result<Vec<_>>>()?;
82        let post_resource_offset = reader.stream_position()?;
83
84        // Read strings contained in the file. A stringlist only exists in files before Season 3.
85        // Each entry is separated by a null terminator, and files specify their offset themselves
86        // in no particular order, so we cannot pre-read and just index into them.
87        //
88        // For files from modules that do not contain strings, we get it from the `get_tag_path` function.
89        reader.seek(SeekFrom::Start(strings_offset))?;
90        if self.header.version <= ModuleVersion::CampaignFlight {
91            for file in &mut self.files {
92                reader.seek(SeekFrom::Start(
93                    strings_offset + u64::from(file.name_offset),
94                ))?;
95                file.tag_name = reader.read_null_terminated_string()?;
96            }
97        } else {
98            let tag_paths: Vec<String> = (0..self.files.len())
99                .map(|i| self.get_tag_path(i, 0))
100                .collect::<Result<Vec<_>>>()?;
101
102            for (file, tag_path) in self.files.iter_mut().zip(tag_paths) {
103                file.tag_name = tag_path;
104            }
105        }
106
107        reader.seek(SeekFrom::Start(post_resource_offset))?;
108        self.blocks =
109            reader.read_enumerable::<ModuleBlockEntry>(u64::from(self.header.block_count))?;
110
111        // Align to 0x?????000
112        let stream_position = reader.stream_position()?;
113        reader.seek(SeekFrom::Start((stream_position / 0x1000 + 1) * 0x1000))?;
114        self.file_data_offset = reader.stream_position()?;
115        self.module_file = Some(reader);
116        Ok(())
117    }
118
119    /// Opens the HD1 file if it exists.
120    fn open_hd1<T: AsRef<Path>>(&mut self, file_path: T) -> Result<()> {
121        if self.header.hd1_delta != 0 {
122            let hd1 = file_path.as_ref().with_extension("module_hd1");
123            if hd1.exists() {
124                self.use_hd1 = true;
125                let file = File::open(hd1)?;
126                self.hd1_file = Some(BufReader::new(file));
127            }
128        }
129        Ok(())
130    }
131
132    /// Gets the tag path of a file entry.
133    ///
134    /// This function returns the tag path of a file entry based on the provided index.
135    /// For file entries that have a parent, the function recursively gets the tag path of the parent and appends the child index to the path.
136    ///
137    /// # Arguments
138    /// * `index` - The index of the file entry to get the tag path from.
139    /// * `depth` - The depth of the recursion. This is used to prevent infinite recursion.
140    ///
141    /// # Returns
142    /// Returns the tag path of the file entry if the operation is successful.
143    fn get_tag_path(&self, index: usize, depth: usize) -> Result<String> {
144        if depth > 3 {
145            return Err(Error::TagError(TagError::RecursionDepth));
146        }
147        let file = &self.files[index];
148        if file.tag_id == -1 && file.parent_index != -1 {
149            let parent = &self.files[usize::try_from(file.parent_index)?];
150            let mut parent_name: String = String::new();
151            let child_index = self.resource_indices[usize::try_from(parent.resource_index)?
152                ..usize::try_from(parent.resource_index)?
153                    + usize::try_from(parent.resource_count)?]
154                .iter()
155                .map(|&i| &self.files[i as usize])
156                .take_while(|&item| !eq(item, file))
157                .count();
158            if parent.tag_name.is_empty() {
159                parent_name = self.get_tag_path(usize::try_from(file.parent_index)?, depth + 1)?;
160            }
161            if parent.tag_id == -1 {
162                parent_name = self.get_tag_path(usize::try_from(file.parent_index)?, depth + 1)?;
163                Ok(format!("{parent_name}[{child_index}:block]"))
164            } else {
165                Ok(format!("{parent_name}[{child_index}:resource]"))
166            }
167        } else {
168            Ok(format!(
169                "{}/{}.{}",
170                file.tag_group, file.tag_id, file.tag_group
171            ))
172        }
173    }
174
175    /// Reads a specific tag from the module file.
176    ///
177    /// This function reads a specific tag from the module file based on the provided index.
178    /// It also utilizes the HD1 stream if the file entry has the flag set for it and the stream is loaded, and returns `None` if the tag offset is invalid.
179    ///
180    /// # Arguments
181    ///
182    /// * `index` - The index of the file entry to read the tag from. This index corresponds to
183    ///             the position of the file entry in the [`files`](`ModuleFile::files`) vector.
184    ///
185    /// # Returns
186    ///
187    /// Returns a mutable reference to the file if the read operation is successful, or an [`Error`](`crate::Error`), a [`None`] if the file was not read (if tag offset is specified as invalid) or the containing the I/O error if any reading operation fails.
188    pub fn read_tag(&mut self, index: u32) -> Result<Option<&mut ModuleFileEntry>> {
189        let file = &mut self.files[index as usize];
190        if file.data_offset_flags.contains(DataOffsetType::DEBUG) {
191            return Ok(None); // Currently not reading debug modules because we don't have an
192            // example.
193        }
194
195        let mut offset = self.header.hd1_delta;
196        if file.data_offset_flags.contains(DataOffsetType::USE_HD1) {
197            if let Some(ref mut module_file) = self.hd1_file {
198                if self.header.version <= ModuleVersion::CampaignFlight {
199                    offset += self.header.hd1_delta;
200                }
201                file.read_tag(
202                    module_file,
203                    offset,
204                    &self.blocks,
205                    &self.header.version,
206                    true,
207                )?;
208            } else {
209                return Ok(None);
210            }
211        } else if let Some(ref mut module_file) = self.module_file {
212            file.read_tag(
213                module_file,
214                self.file_data_offset,
215                &self.blocks,
216                &self.header.version,
217                false,
218            )?;
219        }
220        Ok(Some(file))
221    }
222
223    /// Searches for the index of the tag given the `global_id`.
224    ///
225    /// This function searches for the index of a tag in the [`files`](`ModuleFile::files`) vector using the provided
226    /// `global_id`. If the tag is found, it reads the tag using the [`read_tag`](`ModuleFile::read_tag`) function and
227    /// stores it in the index.
228    ///
229    /// # Arguments
230    ///
231    /// * `global_id` - The global tag ID of the file to find. This ID is used to identify the
232    ///                 specific tag within the module file.
233    ///
234    /// # Returns
235    ///
236    /// Returns a mutable reference to the file if successful. If the tag is not
237    /// found or couldn't be read, it returns [`None`]. Any I/O error encountered during the operation is also returned
238    /// if it occurs.
239    pub fn read_tag_from_id(&mut self, global_id: i32) -> Result<Option<&mut ModuleFileEntry>> {
240        if let Some(index) = self.files.iter().position(|file| file.tag_id == global_id) {
241            let has_read = self.read_tag(u32::try_from(index)?)?;
242            if let Some(tag) = has_read {
243                Ok(Some(tag))
244            } else {
245                Ok(None)
246            }
247        } else {
248            Ok(None)
249        }
250    }
251}