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    file_handle: 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 == ModuleVersion::Flight1)?;
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
87        // in no particular order, so we cannot pre-read and just index into them.
88        //
89        // For files from modules that do not contain strings, we get it from the `get_tag_path` function.
90        reader.seek(SeekFrom::Start(strings_offset))?;
91        if self.header.version <= ModuleVersion::CampaignFlight {
92            for file in &mut self.files {
93                reader.seek(SeekFrom::Start(
94                    strings_offset + u64::from(file.name_offset),
95                ))?;
96                file.tag_name = reader.read_null_terminated_string()?;
97            }
98        } else {
99            let tag_paths: Vec<String> = (0..self.files.len())
100                .map(|i| self.get_tag_path(i, 0))
101                .collect::<Result<Vec<_>>>()?;
102
103            for (file, tag_path) in self.files.iter_mut().zip(tag_paths) {
104                file.tag_name = tag_path;
105            }
106        }
107
108        reader.seek(SeekFrom::Start(post_resource_offset))?;
109        self.blocks =
110            reader.read_enumerable::<ModuleBlockEntry>(u64::from(self.header.block_count))?;
111
112        // Align to 0x?????000
113        let stream_position = reader.stream_position()?;
114        reader.seek(SeekFrom::Start((stream_position / 0x1000 + 1) * 0x1000))?;
115        self.file_data_offset = reader.stream_position()?;
116        self.file_handle = Some(reader);
117        Ok(())
118    }
119
120    /// Opens the HD1 file if it exists.
121    fn open_hd1<T: AsRef<Path>>(&mut self, file_path: T) -> Result<()> {
122        if self.header.hd1_delta != 0 {
123            let hd1 = file_path.as_ref().with_extension("module_hd1");
124            if hd1.exists() {
125                self.use_hd1 = true;
126                let file = File::open(hd1)?;
127                self.hd1_file = Some(BufReader::new(file));
128            }
129        }
130        Ok(())
131    }
132
133    /// Gets the tag path of a file entry.
134    ///
135    /// This function returns the tag path of a file entry based on the provided index.
136    /// 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.
137    ///
138    /// # Arguments
139    /// * `index` - The index of the file entry to get the tag path from.
140    /// * `depth` - The depth of the recursion. This is used to prevent infinite recursion.
141    ///
142    /// # Returns
143    /// Returns the tag path of the file entry if the operation is successful.
144    fn get_tag_path(&self, index: usize, depth: usize) -> Result<String> {
145        if depth > 3 {
146            return Err(Error::TagError(TagError::RecursionDepth));
147        }
148        let file = &self.files[index];
149        if file.tag_id == -1 && file.parent_index != -1 {
150            let parent = &self.files[usize::try_from(file.parent_index)?];
151            let mut parent_name: String = String::new();
152            let child_index = self.resource_indices[usize::try_from(parent.resource_index)?
153                ..usize::try_from(parent.resource_index)?
154                    + usize::try_from(parent.resource_count)?]
155                .iter()
156                .map(|&i| &self.files[i as usize])
157                .take_while(|&item| !eq(item, file))
158                .count();
159            if parent.tag_name.is_empty() {
160                parent_name = self.get_tag_path(usize::try_from(file.parent_index)?, depth + 1)?;
161            }
162            if parent.tag_id == -1 {
163                parent_name = self.get_tag_path(usize::try_from(file.parent_index)?, depth + 1)?;
164                Ok(format!("{parent_name}[{child_index}:block]"))
165            } else {
166                Ok(format!("{parent_name}[{child_index}:resource]"))
167            }
168        } else {
169            Ok(format!(
170                "{}/{}.{}",
171                file.tag_group, file.tag_id, file.tag_group
172            ))
173        }
174    }
175
176    /// Reads a specific tag from the module file.
177    ///
178    /// This function reads a specific tag from the module file based on the provided index.
179    /// 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.
180    ///
181    /// # Arguments
182    ///
183    /// * `index` - The index of the file entry to read the tag from. This index corresponds to 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.file_handle {
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 specific tag within the module file.
232    ///
233    /// # Returns
234    ///
235    /// Returns a mutable reference to the file if successful. If the tag is not
236    /// found or couldn't be read, it returns [`None`]. Any I/O error encountered during the operation is also returned
237    /// if it occurs.
238    pub fn read_tag_from_id(&mut self, global_id: i32) -> Result<Option<&mut ModuleFileEntry>> {
239        if let Some(index) = self.files.iter().position(|file| file.tag_id == global_id) {
240            let has_read = self.read_tag(u32::try_from(index)?)?;
241            if let Some(tag) = has_read {
242                Ok(Some(tag))
243            } else {
244                Ok(None)
245            }
246        } else {
247            Ok(None)
248        }
249    }
250}