1use std::fmt;
14
15use bitflags::bitflags;
16use serde::Serialize;
17
18bitflags! {
19 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
25 pub struct Region: u8 {
26
27 const UNKNOWN = 0;
28 const JAPAN = 1 << 0;
29 const USA = 1 << 1;
30 const EUROPE = 1 << 2;
31 const RUSSIA = 1 << 3;
32 const ASIA = 1 << 4;
33 const CHINA = 1 << 5;
34 const KOREA = 1 << 6;
35
36 const WORLD = u8::MAX;
38 }
39}
40
41impl fmt::Display for Region {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 if self.is_empty() {
44 return write!(f, "Unknown");
45 }
46
47 if self.bits() == Region::WORLD.bits() {
49 return write!(f, "World");
50 }
51
52 let regions: Vec<&str> = self
54 .iter()
55 .map(|flag| match flag {
56 Region::JAPAN => "Japan",
57 Region::USA => "USA",
58 Region::EUROPE => "Europe",
59 Region::RUSSIA => "Russia",
60 Region::ASIA => "Asia",
61 Region::CHINA => "China",
62 Region::KOREA => "Korea",
63 _ => "",
64 })
65 .filter(|s| !s.is_empty())
66 .collect();
67
68 write!(f, "{}", regions.join("/"))
70 }
71}
72
73const REGION_PATTERNS: &[(&[&str], Region)] = &[
74 (&["JAP", "JP", "(J)", "[J]", "NTSC-J"], Region::JAPAN),
75 (&["USA", "(U)", "[U]", "NTSC-U", "NTSC-US"], Region::USA),
76 (&["EUR", "(E)", "[E]", "PAL", "NTSC-E"], Region::EUROPE),
77 (&["RUSSIA", "DENDY"], Region::RUSSIA),
78 (&["(WORLD)", "[WORLD]", "(W)", "[W]"], Region::WORLD),
79];
80
81pub fn infer_region_from_filename(name: &str) -> Region {
106 let upper_name = name.to_uppercase();
109 REGION_PATTERNS
110 .iter()
111 .fold(Region::UNKNOWN, |acc, (patterns, flag)| {
112 if patterns.iter().any(|pattern| upper_name.contains(*pattern)) {
113 acc | *flag
114 } else {
115 acc
116 }
117 })
118}
119
120pub fn check_region_mismatch(source_name: &str, header_region: Region) -> bool {
151 let inferred_region = infer_region_from_filename(source_name);
152
153 if inferred_region.is_empty() || header_region.is_empty() {
155 return false;
156 }
157
158 !inferred_region.intersects(header_region)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_infer_region_from_filename_japan() {
167 assert_eq!(infer_region_from_filename("game (J).zip"), Region::JAPAN);
168 assert_eq!(infer_region_from_filename("game [J].zip"), Region::JAPAN);
169 assert_eq!(
170 infer_region_from_filename("game (Japan).zip"),
171 Region::JAPAN
172 );
173 assert_eq!(
174 infer_region_from_filename("game (NTSC-J).zip"),
175 Region::JAPAN
176 );
177 }
178
179 #[test]
180 fn test_infer_region_from_filename_usa() {
181 assert_eq!(infer_region_from_filename("game (U).zip"), Region::USA);
182 assert_eq!(infer_region_from_filename("game [U].zip"), Region::USA);
183 assert_eq!(infer_region_from_filename("game (USA).zip"), Region::USA);
184 assert_eq!(infer_region_from_filename("game (NTSC-U).zip"), Region::USA);
185 assert_eq!(
186 infer_region_from_filename("game (NTSC-US).zip"),
187 Region::USA
188 );
189 }
190
191 #[test]
192 fn test_infer_region_from_filename_europe() {
193 assert_eq!(infer_region_from_filename("game (E).zip"), Region::EUROPE);
194 assert_eq!(infer_region_from_filename("game [E].zip"), Region::EUROPE);
195 assert_eq!(
196 infer_region_from_filename("game (Europe).zip"),
197 Region::EUROPE
198 );
199 assert_eq!(infer_region_from_filename("game (PAL).zip"), Region::EUROPE);
200 assert_eq!(
201 infer_region_from_filename("game (NTSC-E).zip"),
202 Region::EUROPE
203 );
204 }
205
206 #[test]
207 fn test_infer_region_from_filename_russia() {
208 assert_eq!(
209 infer_region_from_filename("game (Russia).zip"),
210 Region::RUSSIA
211 );
212 assert_eq!(infer_region_from_filename("game DENDY.zip"), Region::RUSSIA);
213 }
214
215 #[test]
216 fn test_infer_region_from_filename_world() {
217 assert_eq!(infer_region_from_filename("game (W).zip"), Region::WORLD);
218 assert_eq!(
219 infer_region_from_filename("game (World).zip"),
220 Region::WORLD
221 );
222 }
223
224 #[test]
225 fn test_infer_region_from_filename_none() {
226 assert_eq!(
227 infer_region_from_filename("game (unmarked).zip"),
228 Region::UNKNOWN
229 );
230 assert_eq!(
231 infer_region_from_filename("another game.zip"),
232 Region::UNKNOWN
233 );
234 }
235
236 #[test]
237 fn test_check_region_mismatch_no_mismatch_japan() {
238 assert!(!check_region_mismatch("game (J).zip", Region::JAPAN));
240 assert!(!check_region_mismatch("game (Japan).zip", Region::JAPAN));
241 }
242
243 #[test]
244 fn test_check_region_mismatch_no_mismatch_usa() {
245 assert!(!check_region_mismatch("game (U).zip", Region::USA));
247 assert!(!check_region_mismatch("game (USA).zip", Region::USA));
248 }
249
250 #[test]
251 fn test_check_region_mismatch_no_mismatch_europe() {
252 assert!(!check_region_mismatch("game (E).zip", Region::EUROPE));
254 assert!(!check_region_mismatch("game (Europe).zip", Region::EUROPE));
255 }
256
257 #[test]
258 fn test_check_region_mismatch_mismatch_japan_usa() {
259 assert!(check_region_mismatch("game (J).zip", Region::USA));
261 assert!(check_region_mismatch("game (Japan).zip", Region::USA));
262 }
263
264 #[test]
265 fn test_check_region_mismatch_mismatch_usa_europe() {
266 assert!(check_region_mismatch("game (U).zip", Region::EUROPE));
268 assert!(check_region_mismatch("game (USA).zip", Region::EUROPE));
269 }
270
271 #[test]
272 fn test_check_region_mismatch_mismatch_europe_japan() {
273 assert!(check_region_mismatch("game (E).zip", Region::JAPAN));
275 assert!(check_region_mismatch("game (Europe).zip", Region::JAPAN));
276 }
277
278 #[test]
279 fn test_check_region_mismatch_filename_has_region_header_unknown() {
280 assert!(!check_region_mismatch("game (J).zip", Region::UNKNOWN));
282 assert!(!check_region_mismatch("game (U).zip", Region::UNKNOWN));
283 assert!(!check_region_mismatch("game (E).zip", Region::UNKNOWN));
284 }
285
286 #[test]
287 fn test_check_region_mismatch_filename_unknown_header_has_region() {
288 assert!(!check_region_mismatch("game.zip", Region::JAPAN));
290 assert!(!check_region_mismatch("another game.zip", Region::USA));
291 assert!(!check_region_mismatch("game_title", Region::EUROPE));
292 }
293
294 #[test]
295 fn test_check_region_mismatch_both_unknown() {
296 assert!(!check_region_mismatch("game.zip", Region::UNKNOWN));
298 assert!(!check_region_mismatch("another game.zip", Region::UNKNOWN));
299 assert!(!check_region_mismatch("game_title", Region::UNKNOWN));
300 }
301
302 #[test]
303 fn test_check_region_mismatch_case_insensitivity_filename() {
304 assert!(!check_region_mismatch("game (JapAn).zip", Region::JAPAN));
306 assert!(!check_region_mismatch("game (uSa).zip", Region::USA));
307 assert!(!check_region_mismatch("game (EuRoPe).zip", Region::EUROPE));
308 }
309
310 #[test]
311 fn test_overlap_logic() {
312 let filename_region = infer_region_from_filename("Contra (U).nes"); let header_region = Region::USA | Region::JAPAN;
315
316 assert!(filename_region.intersects(header_region));
318 assert!(!check_region_mismatch("Contra (U).nes", Region::USA));
319 }
320
321 #[test]
322 fn test_strict_mismatch() {
323 let filename_region = infer_region_from_filename("Contra (E).nes"); let header_region = Region::USA | Region::JAPAN;
326
327 assert!(!filename_region.intersects(header_region));
329 assert!(check_region_mismatch("Contra (E).nes", Region::USA));
330 }
331
332 #[test]
333 fn test_world_rom() {
334 assert!(!check_region_mismatch("Game (W).bin", Region::USA));
337 }
338
339 #[test]
340 fn test_multiple_region_filename_display() {
341 let filename = "Super Game (U) (J).nes";
342 let region = infer_region_from_filename(filename).to_string();
343 assert_eq!(region, "Japan/USA")
344 }
345
346 #[test]
347 fn test_region_display_all() {
348 assert_eq!(Region::JAPAN.to_string(), "Japan");
349 assert_eq!(Region::USA.to_string(), "USA");
350 assert_eq!(Region::EUROPE.to_string(), "Europe");
351 assert_eq!(Region::RUSSIA.to_string(), "Russia");
352 assert_eq!(Region::ASIA.to_string(), "Asia");
353 assert_eq!(Region::CHINA.to_string(), "China");
354 assert_eq!(Region::KOREA.to_string(), "Korea");
355 assert_eq!(Region::UNKNOWN.to_string(), "Unknown");
356 assert_eq!(Region::WORLD.to_string(), "World");
357 assert_eq!((Region::JAPAN | Region::USA).to_string(), "Japan/USA");
358 }
359}