use log::error;
use serde::Serialize;
use crate::error::RomAnalyzerError;
use crate::region::{Region, check_region_mismatch};
#[derive(Debug, PartialEq, Clone, Serialize)]
pub struct SegaCdAnalysis {
pub source_name: String,
pub region: Region,
pub region_string: String,
pub region_mismatch: bool,
pub region_code: u8,
pub signature: String,
}
impl SegaCdAnalysis {
pub fn print(&self) -> String {
format!(
"{}\n\
System: Sega CD / Mega CD\n\
Signature: {}\n\
Region Code: 0x{:02X}\n\
Region: {}",
self.source_name, self.signature, self.region_code, self.region
)
}
}
pub fn map_region(region_byte: u8) -> (&'static str, Region) {
match region_byte {
0x40 => ("Japan (NTSC-J)", Region::JAPAN),
0x80 => ("Europe (PAL)", Region::EUROPE),
0xC0 => ("USA (NTSC-U)", Region::USA),
0x00 => (
"Unrestricted/BIOS region",
Region::USA | Region::EUROPE | Region::JAPAN,
),
_ => ("Unknown", Region::UNKNOWN),
}
}
pub fn analyze_segacd_data(
data: &[u8],
source_name: &str,
) -> Result<SegaCdAnalysis, RomAnalyzerError> {
const REQUIRED_SIZE: usize = 0x200;
if data.len() < REQUIRED_SIZE {
return Err(RomAnalyzerError::DataTooSmall {
file_size: data.len(),
required_size: REQUIRED_SIZE,
details: "Sega CD boot file header".to_string(),
});
}
let signature_bytes = &data[0x100..0x109];
let signature = String::from_utf8_lossy(signature_bytes)
.trim_matches(char::from(0))
.trim()
.to_string();
let region_code = data[0x10B];
let (region_name, region) = map_region(region_code);
if signature != "SEGA CD" && signature != "SEGA MEGA" {
error!(
"[!] Warning: File does not appear to be a standard Sega CD boot file (no SEGA CD or SEGA MEGA signature at 0x100) for {}. Found: '{}'",
source_name, signature
);
}
let region_mismatch = check_region_mismatch(source_name, region);
Ok(SegaCdAnalysis {
source_name: source_name.to_string(),
region,
region_string: region_name.to_string(),
region_mismatch,
region_code,
signature,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_segacd_header(signature_str: &str, region_byte: u8) -> Vec<u8> {
let mut data = vec![0; 0x200];
const SIG_MAX_LEN: usize = 9;
let mut signature_bytes = signature_str.as_bytes().to_vec();
if signature_bytes.len() > SIG_MAX_LEN {
panic!("Signature must be <= 9 bytes");
}
signature_bytes.resize(SIG_MAX_LEN, 0);
data[0x100..0x109].copy_from_slice(&signature_bytes);
data[0x10B] = region_byte;
data
}
#[test]
fn test_analyze_segacd_data_japan() -> Result<(), RomAnalyzerError> {
let data = generate_segacd_header("SEGA CD", 0x40); let analysis = analyze_segacd_data(&data, "test_rom_jp.iso")?;
assert_eq!(analysis.source_name, "test_rom_jp.iso");
assert_eq!(analysis.signature, "SEGA CD");
assert_eq!(analysis.region_code, 0x40);
assert_eq!(analysis.region, Region::JAPAN);
assert_eq!(analysis.region_string, "Japan (NTSC-J)");
assert_eq!(
analysis.print(),
"test_rom_jp.iso\n\
System: Sega CD / Mega CD\n\
Signature: SEGA CD\n\
Region Code: 0x40\n\
Region: Japan"
);
Ok(())
}
#[test]
fn test_analyze_segacd_data_europe() -> Result<(), RomAnalyzerError> {
let data = generate_segacd_header("SEGA CD", 0x80); let analysis = analyze_segacd_data(&data, "test_rom_eur.iso")?;
assert_eq!(analysis.source_name, "test_rom_eur.iso");
assert_eq!(analysis.signature, "SEGA CD");
assert_eq!(analysis.region_code, 0x80);
assert_eq!(analysis.region, Region::EUROPE);
assert_eq!(analysis.region_string, "Europe (PAL)");
Ok(())
}
#[test]
fn test_analyze_segacd_data_usa() -> Result<(), RomAnalyzerError> {
let data = generate_segacd_header("SEGA CD", 0xC0); let analysis = analyze_segacd_data(&data, "test_rom_us.iso")?;
assert_eq!(analysis.source_name, "test_rom_us.iso");
assert_eq!(analysis.signature, "SEGA CD");
assert_eq!(analysis.region_code, 0xC0);
assert_eq!(analysis.region, Region::USA);
assert_eq!(analysis.region_string, "USA (NTSC-U)");
Ok(())
}
#[test]
fn test_analyze_segacd_data_unrestricted() -> Result<(), RomAnalyzerError> {
let data = generate_segacd_header("SEGA CD", 0x00); let analysis = analyze_segacd_data(&data, "test_rom_unrestricted.iso")?;
assert_eq!(analysis.source_name, "test_rom_unrestricted.iso");
assert_eq!(analysis.signature, "SEGA CD");
assert_eq!(analysis.region_code, 0x00);
assert_eq!(
analysis.region,
Region::USA | Region::EUROPE | Region::JAPAN
);
assert_eq!(analysis.region_string, "Unrestricted/BIOS region");
Ok(())
}
#[test]
fn test_analyze_segacd_data_mega_signature() -> Result<(), RomAnalyzerError> {
let data = generate_segacd_header("SEGA MEGA", 0x40); let analysis = analyze_segacd_data(&data, "test_rom_mega_jp.iso")?;
assert_eq!(analysis.source_name, "test_rom_mega_jp.iso");
assert_eq!(analysis.signature, "SEGA MEGA");
assert_eq!(analysis.region_code, 0x40);
assert_eq!(analysis.region, Region::JAPAN);
assert_eq!(analysis.region_string, "Japan (NTSC-J)");
Ok(())
}
#[test]
fn test_analyze_segacd_data_unknown_code() -> Result<(), RomAnalyzerError> {
let data = generate_segacd_header("SEGA CD", 0xFF); let analysis = analyze_segacd_data(&data, "test_rom_unknown.iso")?;
assert_eq!(analysis.source_name, "test_rom_unknown.iso");
assert_eq!(analysis.signature, "SEGA CD");
assert_eq!(analysis.region_code, 0xFF);
assert_eq!(analysis.region, Region::UNKNOWN);
assert_eq!(analysis.region_string, "Unknown");
Ok(())
}
#[test]
fn test_analyze_segacd_data_too_small() {
let data = vec![0; 100]; let result = analyze_segacd_data(&data, "too_small.iso");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too small"));
}
}