nx_pkg4/
file.rs

1use core::str;
2use std::{fs::File, path::Path};
3
4use memmap2::Mmap;
5
6use crate::{
7    node::{NxNode, NxNodeData},
8    NxError, NxTryGet,
9};
10
11/// A memory mapped NX file.
12pub struct NxFile {
13    pub(crate) data: Mmap,
14    pub(crate) header: NxHeader,
15    pub(crate) root: NxNodeData,
16}
17
18impl NxFile {
19    /// Opens and memory maps an NX file, then validates its header.
20    pub fn open(path: &Path) -> Result<Self, NxError> {
21        let file = File::open(path)?;
22
23        // Safety: a memory mapped file is unsafe as undefined behaviour can occur if the file is
24        // ever modified by another process while in use. This crate aims to provide a fully safe
25        // api so this shouldn't be a concern, however modifying the file will result in either
26        // `NxError`s or values that don't make sense.
27        let data = unsafe { Mmap::map(&file)? };
28        let header = NxHeader::new(&data)?;
29        let root = data.try_get_node_data(header.node_offset)?;
30
31        Ok(Self { data, header, root })
32    }
33
34    /// Gets the total number of nodes in the file.
35    pub fn node_count(&self) -> u32 {
36        self.header.node_count
37    }
38
39    /// Gets the total number of strings in the file.
40    pub fn string_count(&self) -> u32 {
41        self.header.string_count
42    }
43
44    /// Gets the total number of bitmaps in the file.
45    pub fn bitmap_count(&self) -> u32 {
46        self.header.bitmap_count
47    }
48
49    /// Gets the total number of audio tracks in the file.
50    pub fn audio_count(&self) -> u32 {
51        self.header.audio_count
52    }
53
54    /// Gets the root node.
55    pub fn root(&self) -> NxNode {
56        NxNode {
57            data: self.root,
58            file: self,
59        }
60    }
61
62    /// Gets a string from the file at the given index.
63    pub(crate) fn get_str(&self, index: u32) -> Result<&str, NxError> {
64        let offset = self
65            .data
66            .try_get_u64(self.header.string_offset + (index as u64 * size_of::<u64>() as u64))?;
67
68        let len = self.data.try_get_u16(offset)?;
69        Ok(self.data.try_get_str(offset + 2, len)?)
70    }
71
72    /// Gets a bitmap from the file at the given index.
73    pub(crate) fn get_bitmap(&self, index: u32) -> Result<&[u8], NxError> {
74        let offset = self
75            .data
76            .try_get_u64(self.header.bitmap_offset + (index as u64 * size_of::<u64>() as u64))?;
77
78        let len = self.data.try_get_u32(offset)?;
79        Ok(self.data.try_get_bytes(offset + 4, len as usize)?)
80    }
81}
82
83pub(crate) struct NxHeader {
84    node_count: u32,
85    pub(crate) node_offset: u64,
86    string_count: u32,
87    pub(crate) string_offset: u64,
88    bitmap_count: u32,
89    pub(crate) bitmap_offset: u64,
90    audio_count: u32,
91    pub(crate) audio_offset: u64,
92}
93
94impl NxHeader {
95    pub fn new(data: &Mmap) -> Result<Self, NxError> {
96        // Validate that the first 4 bytes equals "PKG4".
97        if data.try_get_u32(0)? != 0x34474B50 {
98            return Err(NxError::InvalidHeader);
99        }
100
101        Ok(Self {
102            node_count: data.try_get_u32(4).map_err(|_| NxError::InvalidHeader)?,
103            node_offset: data.try_get_u64(8).map_err(|_| NxError::InvalidHeader)?,
104            string_count: data.try_get_u32(16).map_err(|_| NxError::InvalidHeader)?,
105            string_offset: data.try_get_u64(20).map_err(|_| NxError::InvalidHeader)?,
106            bitmap_count: data.try_get_u32(28).map_err(|_| NxError::InvalidHeader)?,
107            bitmap_offset: data.try_get_u64(32).map_err(|_| NxError::InvalidHeader)?,
108            audio_count: data.try_get_u32(40).map_err(|_| NxError::InvalidHeader)?,
109            audio_offset: data.try_get_u64(44).map_err(|_| NxError::InvalidHeader)?,
110        })
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn open_file_does_not_exist() {
120        let result = NxFile::open(Path::new("data/file_that_does_not_exist.nx"));
121        assert!(result.is_err());
122    }
123
124    #[test]
125    fn open_file_with_invalid_header() {
126        let result = NxFile::open(Path::new("data/invalid_header.nx"));
127        assert!(result.is_err());
128        assert!(matches!(result.err().unwrap(), NxError::InvalidHeader));
129    }
130
131    #[test]
132    fn open_valid_file() {
133        let result = NxFile::open(Path::new("data/valid.nx"));
134        assert!(result.is_ok());
135
136        let file = result.unwrap();
137        assert_eq!(file.node_count(), 432);
138        assert_eq!(file.string_count(), 227);
139        assert_eq!(file.bitmap_count(), 0);
140        assert_eq!(file.audio_count(), 0);
141    }
142}