rom_analyzer/console/
psx.rs

1//! Provides header analysis functionality for Sony PlayStation (PSX) ROMs, typically in CD image formats.
2//!
3//! This module focuses on identifying the region of PSX games by searching for known
4//! executable prefixes (e.g., "SLUS", "SLES", "SLPS") within the initial data tracks.
5
6use std::error::Error;
7
8use serde::Serialize;
9
10use crate::error::RomAnalyzerError;
11use crate::region::{Region, check_region_mismatch};
12
13/// Struct to hold the analysis results for a PSX ROM.
14#[derive(Debug, PartialEq, Clone, Serialize)]
15pub struct PsxAnalysis {
16    /// The name of the source file.
17    pub source_name: String,
18    /// The identified region(s) as a region::Region bitmask.
19    pub region: Region,
20    /// The identified region name (e.g., "North America (NTSC-U)").
21    pub region_string: String,
22    /// If the region in the ROM header doesn't match the region in the filename.
23    pub region_mismatch: bool,
24    /// The identified region code (e.g., "SLUS").
25    pub code: String,
26}
27
28impl PsxAnalysis {
29    /// Returns a printable String of the analysis results.
30    pub fn print(&self) -> String {
31        let executable_prefix_not_found = if self.code == "N/A" {
32            "\nNote: Executable prefix (SLUS/SLES/SLPS) not found in header area. Requires main data track (.bin or .iso)."
33        } else {
34            ""
35        };
36        format!(
37            "{}\n\
38             System:       Sony PlayStation (PSX)\n\
39             Region:       {}\n\
40             Code:         {}\
41             {}",
42            self.source_name, self.region, self.code, executable_prefix_not_found
43        )
44    }
45}
46
47/// Determines the PSX game region based on a given region code.
48///
49/// The region code typically comes from the ROM data. This function maps it to a
50/// human-readable region string and a Region bitmask.
51///
52/// # Arguments
53///
54/// * `region_code` - The region code string, usually found in the ROM data.
55///
56/// # Returns
57///
58/// A tuple containing:
59/// - A `&'static str` representing the region (e.g., "North America (NTSC-U)", "Europe (PAL)", etc)
60///   or "Unknown" if the region code is not recognized.
61/// - A `Region` bitmask representing the region(s) associated with the code.
62///
63/// # Examples
64///
65/// ```rust
66/// use rom_analyzer::console::psx::map_region;
67/// use rom_analyzer::region::Region;
68///
69/// let (region_str, region_mask) = map_region("SLUS");
70/// assert_eq!(region_str, "North America (NTSC-U)");
71/// assert_eq!(region_mask, Region::USA);
72///
73/// let (region_str, region_mask) = map_region("SLES");
74/// assert_eq!(region_str, "Europe (PAL)");
75/// assert_eq!(region_mask, Region::EUROPE);
76///
77/// let (region_str, region_mask) = map_region("SLPS");
78/// assert_eq!(region_str, "Japan (NTSC-J)");
79/// assert_eq!(region_mask, Region::JAPAN);
80///
81/// let (region_str, region_mask) = map_region("UNKNOWN");
82/// assert_eq!(region_str, "Unknown");
83/// assert_eq!(region_mask, Region::UNKNOWN);
84/// ```
85pub fn map_region(region_code: &str) -> (&'static str, Region) {
86    match region_code {
87        "SLUS" => ("North America (NTSC-U)", Region::USA),
88        "SLES" => ("Europe (PAL)", Region::EUROPE),
89        "SLPS" => ("Japan (NTSC-J)", Region::JAPAN),
90        _ => ("Unknown", Region::UNKNOWN),
91    }
92}
93
94/// Analyzes PlayStation (PSX) ROM data, typically from CD images.
95///
96/// This function scans a portion of the ROM data (up to `0x20000` bytes) for
97/// common PSX executable prefixes like "SLUS", "SLES", or "SLPS". These prefixes
98/// indicate the game's region. If a prefix is found, the corresponding region
99/// and code are extracted. A region mismatch check is also performed against the `source_name`.
100///
101/// # Arguments
102///
103/// * `data` - A byte slice (`&[u8]`) containing the raw ROM data (e.g., from a `.bin` or `.iso` file).
104/// * `source_name` - The name of the ROM file, used for region mismatch checks.
105///
106/// # Returns
107///
108/// A `Result` which is:
109/// - `Ok(PsxAnalysis)` containing the detailed analysis results.
110/// - `Err(Box<dyn Error>)` if the ROM data is too small for reliable analysis.
111pub fn analyze_psx_data(data: &[u8], source_name: &str) -> Result<PsxAnalysis, Box<dyn Error>> {
112    // Check the first 128KB (0x20000 bytes)
113    let check_size = std::cmp::min(data.len(), 0x20000);
114    if check_size < 0x2000 {
115        // Need enough data for Volume Descriptor/Boot file
116        return Err(Box::new(RomAnalyzerError::new(
117            "PSX boot file too small for reliable analysis.",
118        )));
119    }
120
121    let data_sample = &data[..check_size];
122
123    let mut found_code = "N/A".to_string();
124    let mut region_name = "Unknown";
125    let mut region = Region::UNKNOWN;
126
127    // TODO: Consider moving this somewhere else to centralize the logic into map_region()
128    // For now we'll live with these hardcoded prefixes.
129    for prefix in ["SLUS", "SLES", "SLPS"] {
130        // Use windows to check for the prefix anywhere in the sample.
131        if data_sample
132            .windows(prefix.len())
133            .any(|window| window.eq_ignore_ascii_case(prefix.as_bytes()))
134        {
135            found_code = prefix.to_string();
136            let (region_str, region_mask) = map_region(prefix);
137            region_name = region_str;
138            region = region_mask;
139            break;
140        }
141    }
142
143    let region_mismatch = check_region_mismatch(source_name, region);
144
145    Ok(PsxAnalysis {
146        source_name: source_name.to_string(),
147        region,
148        region_string: region_name.to_string(),
149        region_mismatch,
150        code: found_code,
151    })
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use std::error::Error;
158
159    #[test]
160    fn test_analyze_psx_data_slus() -> Result<(), Box<dyn Error>> {
161        // Ensure sufficient data for analysis, at least 0x2000 bytes.
162        let mut data = vec![0; 0x2000];
163        // Place the region code at an offset where it's expected.
164        data[0x100..0x104].copy_from_slice(b"SLUS"); // North America
165        let analysis = analyze_psx_data(&data, "test_rom_us.iso")?;
166
167        assert_eq!(analysis.source_name, "test_rom_us.iso");
168        assert_eq!(analysis.region, Region::USA);
169        assert_eq!(analysis.region_string, "North America (NTSC-U)");
170        assert_eq!(analysis.code, "SLUS");
171        Ok(())
172    }
173
174    #[test]
175    fn test_analyze_psx_data_sles() -> Result<(), Box<dyn Error>> {
176        let mut data = vec![0; 0x2000];
177        data[0x100..0x104].copy_from_slice(b"SLES"); // Europe
178        let analysis = analyze_psx_data(&data, "test_rom_eur.iso")?;
179
180        assert_eq!(analysis.source_name, "test_rom_eur.iso");
181        assert_eq!(analysis.region, Region::EUROPE);
182        assert_eq!(analysis.region_string, "Europe (PAL)");
183        assert_eq!(analysis.code, "SLES");
184        Ok(())
185    }
186
187    #[test]
188    fn test_analyze_psx_data_slps() -> Result<(), Box<dyn Error>> {
189        let mut data = vec![0; 0x2000];
190        data[0x100..0x104].copy_from_slice(b"SLPS"); // Japan
191        let analysis = analyze_psx_data(&data, "test_rom_jp.iso")?;
192
193        assert_eq!(analysis.source_name, "test_rom_jp.iso");
194        assert_eq!(analysis.region, Region::JAPAN);
195        assert_eq!(analysis.region_string, "Japan (NTSC-J)");
196        assert_eq!(analysis.code, "SLPS");
197        Ok(())
198    }
199
200    #[test]
201    fn test_analyze_psx_data_unknown() -> Result<(), Box<dyn Error>> {
202        let data = vec![0; 0x2000];
203        // No known prefix
204        let analysis = analyze_psx_data(&data, "test_rom.iso")?;
205
206        assert_eq!(analysis.source_name, "test_rom.iso");
207        assert_eq!(analysis.region, Region::UNKNOWN);
208        assert_eq!(analysis.region_string, "Unknown");
209        assert_eq!(analysis.code, "N/A");
210        Ok(())
211    }
212
213    #[test]
214    fn test_analyze_psx_data_too_small() {
215        // Test with data smaller than the minimum required size for analysis.
216        let data = vec![0; 100]; // Smaller than 0x2000
217        let result = analyze_psx_data(&data, "too_small.iso");
218        assert!(result.is_err());
219        assert!(result.unwrap_err().to_string().contains("too small"));
220    }
221
222    #[test]
223    fn test_analyze_psx_data_case_insensitivity() -> Result<(), Box<dyn Error>> {
224        // Test that the matching is case-insensitive.
225        let mut data = vec![0; 0x2000];
226        data[0x100..0x104].copy_from_slice(b"sLuS"); // Mixed case
227        let analysis = analyze_psx_data(&data, "test_rom_mixedcase.iso")?;
228
229        assert_eq!(analysis.source_name, "test_rom_mixedcase.iso");
230        assert_eq!(analysis.region, Region::USA);
231        assert_eq!(analysis.region_string, "North America (NTSC-U)");
232        assert_eq!(analysis.code, "SLUS");
233        Ok(())
234    }
235}