rom_analyzer/console/
n64.rs

1//! Provides header analysis functionality for Nintendo 64 (N64) ROMs.
2//!
3//! This module can parse N64 ROM headers to extract country code and infer
4//! the geographical region.
5//!
6//! N64 header documentation referenced here:
7//! <https://en64.shoutwiki.com/wiki/ROM>
8
9use serde::Serialize;
10
11use crate::error::RomAnalyzerError;
12use crate::region::{Region, check_region_mismatch};
13
14/// Struct to hold the analysis results for an N64 ROM.
15#[derive(Debug, PartialEq, Clone, Serialize)]
16pub struct N64Analysis {
17    /// The name of the source file.
18    pub source_name: String,
19    /// The identified region(s) as a region::Region bitmask.
20    pub region: Region,
21    /// The identified region name (e.g., "USA (NTSC)").
22    pub region_string: String,
23    /// If the region in the ROM header doesn't match the region in the filename.
24    pub region_mismatch: bool,
25    /// The country code extracted from the ROM header (e.g., "E", "J").
26    pub country_code: String,
27}
28
29impl N64Analysis {
30    /// Returns a printable String of the analysis results.
31    pub fn print(&self) -> String {
32        format!(
33            "{}\n\
34             System:       Nintendo 64 (N64)\n\
35             Region:       {}\n\
36             Code:         {}",
37            self.source_name, self.region, self.country_code
38        )
39    }
40}
41
42/// Determines the N64 game region based on a given country code.
43///
44/// The country code typically comes from the ROM header. This function maps it to a
45/// human-readable region string and a Region bitmask.
46///
47/// # Arguments
48///
49/// * `country_code` - The country code string, usually found in the ROM header.
50///
51/// # Returns
52///
53/// A tuple containing:
54/// - A `&'static str` representing the region (e.g., "USA (NTSC)", "Japan (NTSC)", etc)
55///   or "Unknown" if the country code is not recognized.
56/// - A `Region` bitmask representing the region(s) associated with the code.
57///
58/// # Examples
59///
60/// ```rust
61/// use rom_analyzer::console::n64::map_region;
62/// use rom_analyzer::region::Region;
63///
64/// let (region_str, region_mask) = map_region("E");
65/// assert_eq!(region_str, "USA (NTSC)");
66/// assert_eq!(region_mask, Region::USA);
67///
68/// let (region_str, region_mask) = map_region("J");
69/// assert_eq!(region_str, "Japan (NTSC)");
70/// assert_eq!(region_mask, Region::JAPAN);
71///
72/// let (region_str, region_mask) = map_region("P");
73/// assert_eq!(region_str, "Europe (PAL)");
74/// assert_eq!(region_mask, Region::EUROPE);
75///
76/// let (region_str, region_mask) = map_region("X");
77/// assert_eq!(region_str, "Unknown");
78/// assert_eq!(region_mask, Region::UNKNOWN);
79/// ```
80pub fn map_region(country_code: &str) -> (&'static str, Region) {
81    match country_code {
82        "E" => ("USA (NTSC)", Region::USA),
83        "J" => ("Japan (NTSC)", Region::JAPAN),
84        "P" => ("Europe (PAL)", Region::EUROPE),
85        "D" => ("Germany (PAL)", Region::EUROPE),
86        "F" => ("France (PAL)", Region::EUROPE),
87        "U" => ("USA (Legacy)", Region::USA),
88        _ => ("Unknown", Region::UNKNOWN),
89    }
90}
91
92/// Analyzes N64 ROM data.
93///
94/// This function reads the N64 ROM header to extract the country code.
95/// It then maps the country code to a human-readable region name and performs
96/// a region mismatch check against the `source_name`.
97///
98/// # Arguments
99///
100/// * `data` - A byte slice (`&[u8]`) containing the raw ROM data.
101/// * `source_name` - The name of the ROM file, used for region mismatch checks.
102///
103/// # Returns
104///
105/// A `Result` which is:
106/// - `Ok`([`N64Analysis`]) containing the detailed analysis results.
107/// - `Err`([`RomAnalyzerError`]) if the ROM data is too small to contain a valid N64 header.
108pub fn analyze_n64_data(data: &[u8], source_name: &str) -> Result<N64Analysis, RomAnalyzerError> {
109    // N64 header is at offset 0x0. Country code is at offset 0x3E (2 bytes).
110    const HEADER_SIZE: usize = 0x40;
111    if data.len() < HEADER_SIZE {
112        return Err(RomAnalyzerError::DataTooSmall {
113            file_size: data.len(),
114            required_size: HEADER_SIZE,
115            details: "N64 header".to_string(),
116        });
117    }
118
119    // Extract Country Code (2 bytes, ASCII)
120    // The second byte is often a null terminator, or part of a two-character code.
121    let country_code = String::from_utf8_lossy(&data[0x3E..0x40])
122        .trim_matches(char::from(0))
123        .to_string();
124
125    // Determine region name based on the country code.
126    let (region_name, region) = map_region(&country_code);
127
128    let region_mismatch = check_region_mismatch(source_name, region);
129
130    Ok(N64Analysis {
131        source_name: source_name.to_string(),
132        region,
133        region_string: region_name.to_string(),
134        region_mismatch,
135        country_code,
136    })
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    /// Helper function to generate a minimal N64 header for testing.
144    fn generate_n64_header(country_code: &str) -> Vec<u8> {
145        let mut data = vec![0; 0x40]; // Ensure enough space for header
146
147        // Country Code (2 bytes at 0x3E)
148        let mut cc_bytes = country_code.as_bytes().to_vec();
149        cc_bytes.resize(2, 0);
150        data[0x3E..0x40].copy_from_slice(&cc_bytes);
151
152        data
153    }
154
155    #[test]
156    fn test_analyze_n64_data_usa() -> Result<(), RomAnalyzerError> {
157        let data = generate_n64_header("E"); // USA region
158        let analysis = analyze_n64_data(&data, "test_rom_us.n64")?;
159
160        assert_eq!(analysis.source_name, "test_rom_us.n64");
161        assert_eq!(analysis.region, Region::USA);
162        assert_eq!(analysis.region_string, "USA (NTSC)");
163        assert_eq!(analysis.country_code, "E");
164        assert_eq!(
165            analysis.print(),
166            "test_rom_us.n64\n\
167             System:       Nintendo 64 (N64)\n\
168             Region:       USA\n\
169             Code:         E"
170        );
171        Ok(())
172    }
173
174    #[test]
175    fn test_analyze_n64_data_japan() -> Result<(), RomAnalyzerError> {
176        let data = generate_n64_header("J"); // Japan region
177        let analysis = analyze_n64_data(&data, "test_rom_jp.n64")?;
178
179        assert_eq!(analysis.source_name, "test_rom_jp.n64");
180        assert_eq!(analysis.region, Region::JAPAN);
181        assert_eq!(analysis.region_string, "Japan (NTSC)");
182        assert_eq!(analysis.country_code, "J");
183        Ok(())
184    }
185
186    #[test]
187    fn test_analyze_n64_data_europe() -> Result<(), RomAnalyzerError> {
188        let data = generate_n64_header("P"); // Europe region
189        let analysis = analyze_n64_data(&data, "test_rom_eur.n64")?;
190
191        assert_eq!(analysis.source_name, "test_rom_eur.n64");
192        assert_eq!(analysis.region, Region::EUROPE);
193        assert_eq!(analysis.region_string, "Europe (PAL)");
194        assert_eq!(analysis.country_code, "P");
195        Ok(())
196    }
197
198    #[test]
199    fn test_analyze_n64_data_germany() -> Result<(), RomAnalyzerError> {
200        let data = generate_n64_header("D"); // Germany region
201        let analysis = analyze_n64_data(&data, "test_rom_deu.n64")?;
202
203        assert_eq!(analysis.source_name, "test_rom_deu.n64");
204        assert_eq!(analysis.region, Region::EUROPE);
205        assert_eq!(analysis.region_string, "Germany (PAL)");
206        assert_eq!(analysis.country_code, "D");
207        Ok(())
208    }
209
210    #[test]
211    fn test_analyze_n64_data_france() -> Result<(), RomAnalyzerError> {
212        let data = generate_n64_header("F"); // France region
213        let analysis = analyze_n64_data(&data, "test_rom_fra.n64")?;
214
215        assert_eq!(analysis.source_name, "test_rom_fra.n64");
216        assert_eq!(analysis.region, Region::EUROPE);
217        assert_eq!(analysis.region_string, "France (PAL)");
218        assert_eq!(analysis.country_code, "F");
219        Ok(())
220    }
221
222    #[test]
223    fn test_analyze_n64_data_legacy_usa() -> Result<(), RomAnalyzerError> {
224        let data = generate_n64_header("U"); // Legacy USA region
225        let analysis = analyze_n64_data(&data, "test_rom_usa_legacy.n64")?;
226
227        assert_eq!(analysis.source_name, "test_rom_usa_legacy.n64");
228        assert_eq!(analysis.region, Region::USA);
229        assert_eq!(analysis.region_string, "USA (Legacy)");
230        assert_eq!(analysis.country_code, "U");
231        Ok(())
232    }
233
234    #[test]
235    fn test_analyze_n64_data_unknown() -> Result<(), RomAnalyzerError> {
236        let data = generate_n64_header("X"); // Unknown region
237        let analysis = analyze_n64_data(&data, "test_rom.n64")?;
238
239        assert_eq!(analysis.source_name, "test_rom.n64");
240        assert_eq!(analysis.region, Region::UNKNOWN);
241        assert_eq!(analysis.region_string, "Unknown");
242        assert_eq!(analysis.country_code, "X");
243        Ok(())
244    }
245
246    #[test]
247    fn test_analyze_n64_data_too_small() {
248        // Test with data smaller than the minimum required size for analysis.
249        let data = vec![0; 30]; // Smaller than 0x40
250        let result = analyze_n64_data(&data, "too_small.n64");
251        assert!(result.is_err());
252        assert!(result.unwrap_err().to_string().contains("too small"));
253    }
254}