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 = Self::JAPAN.bits() | Self::USA.bits() | Self::EUROPE.bits() | Self::RUSSIA.bits();
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
73pub fn infer_region_from_filename(name: &str) -> Region {
98 let lower_name = name.to_lowercase();
99 let mut region = Region::UNKNOWN;
100
101 let region_patterns = [
103 (vec!["jap", "jp", "(j)", "[j]", "ntsc-j"], Region::JAPAN),
104 (vec!["usa", "(u)", "[u]", "ntsc-u", "ntsc-us"], Region::USA),
105 (vec!["eur", "(e)", "[e]", "pal", "ntsc-e"], Region::EUROPE),
106 (vec!["russia", "dendy"], Region::RUSSIA),
107 (vec!["(world)", "[world]", "(w)", "[w]"], Region::WORLD),
108 ];
109
110 for (patterns, flag) in region_patterns {
112 for pattern in patterns {
113 if lower_name.contains(pattern) {
114 region |= flag;
115 break;
116 }
117 }
118 }
119
120 region
121}
122
123pub fn check_region_mismatch(source_name: &str, header_region: Region) -> bool {
154 let inferred_region = infer_region_from_filename(source_name);
155
156 if inferred_region.is_empty() || header_region.is_empty() {
158 return false;
159 }
160
161 !inferred_region.intersects(header_region)
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_infer_region_from_filename_japan() {
170 assert_eq!(infer_region_from_filename("game (J).zip"), Region::JAPAN);
171 assert_eq!(infer_region_from_filename("game [J].zip"), Region::JAPAN);
172 assert_eq!(
173 infer_region_from_filename("game (Japan).zip"),
174 Region::JAPAN
175 );
176 assert_eq!(
177 infer_region_from_filename("game (NTSC-J).zip"),
178 Region::JAPAN
179 );
180 }
181
182 #[test]
183 fn test_infer_region_from_filename_usa() {
184 assert_eq!(infer_region_from_filename("game (U).zip"), Region::USA);
185 assert_eq!(infer_region_from_filename("game [U].zip"), Region::USA);
186 assert_eq!(infer_region_from_filename("game (USA).zip"), Region::USA);
187 assert_eq!(infer_region_from_filename("game (NTSC-U).zip"), Region::USA);
188 assert_eq!(
189 infer_region_from_filename("game (NTSC-US).zip"),
190 Region::USA
191 );
192 }
193
194 #[test]
195 fn test_infer_region_from_filename_europe() {
196 assert_eq!(infer_region_from_filename("game (E).zip"), Region::EUROPE);
197 assert_eq!(infer_region_from_filename("game [E].zip"), Region::EUROPE);
198 assert_eq!(
199 infer_region_from_filename("game (Europe).zip"),
200 Region::EUROPE
201 );
202 assert_eq!(infer_region_from_filename("game (PAL).zip"), Region::EUROPE);
203 assert_eq!(
204 infer_region_from_filename("game (NTSC-E).zip"),
205 Region::EUROPE
206 );
207 }
208
209 #[test]
210 fn test_infer_region_from_filename_none() {
211 assert_eq!(
212 infer_region_from_filename("game (unmarked).zip"),
213 Region::UNKNOWN
214 );
215 assert_eq!(
216 infer_region_from_filename("another game.zip"),
217 Region::UNKNOWN
218 );
219 }
220
221 #[test]
222 fn test_check_region_mismatch_no_mismatch_japan() {
223 assert_eq!(check_region_mismatch("game (J).zip", Region::JAPAN), false);
225 assert_eq!(
226 check_region_mismatch("game (Japan).zip", Region::JAPAN),
227 false
228 );
229 assert_eq!(check_region_mismatch("game (J).zip", Region::JAPAN), false);
230 }
231
232 #[test]
233 fn test_check_region_mismatch_no_mismatch_usa() {
234 assert_eq!(check_region_mismatch("game (U).zip", Region::USA), false);
236 assert_eq!(check_region_mismatch("game (USA).zip", Region::USA), false);
237 assert_eq!(check_region_mismatch("game (U).zip", Region::USA), false);
238 }
239
240 #[test]
241 fn test_check_region_mismatch_no_mismatch_europe() {
242 assert_eq!(check_region_mismatch("game (E).zip", Region::EUROPE), false);
244 assert_eq!(
245 check_region_mismatch("game (Europe).zip", Region::EUROPE),
246 false
247 );
248 assert_eq!(check_region_mismatch("game (E).zip", Region::EUROPE), false);
249 }
250
251 #[test]
252 fn test_check_region_mismatch_mismatch_japan_usa() {
253 assert_eq!(check_region_mismatch("game (J).zip", Region::USA), true);
255 assert_eq!(check_region_mismatch("game (Japan).zip", Region::USA), true);
256 }
257
258 #[test]
259 fn test_check_region_mismatch_mismatch_usa_europe() {
260 assert_eq!(check_region_mismatch("game (U).zip", Region::EUROPE), true);
262 assert_eq!(
263 check_region_mismatch("game (USA).zip", Region::EUROPE),
264 true
265 );
266 }
267
268 #[test]
269 fn test_check_region_mismatch_mismatch_europe_japan() {
270 assert_eq!(check_region_mismatch("game (E).zip", Region::JAPAN), true);
272 assert_eq!(
273 check_region_mismatch("game (Europe).zip", Region::JAPAN),
274 true
275 );
276 }
277
278 #[test]
279 fn test_check_region_mismatch_filename_has_region_header_unknown() {
280 assert_eq!(
282 check_region_mismatch("game (J).zip", Region::UNKNOWN),
283 false
284 );
285 assert_eq!(
286 check_region_mismatch("game (U).zip", Region::UNKNOWN),
287 false
288 );
289 assert_eq!(
290 check_region_mismatch("game (E).zip", Region::UNKNOWN),
291 false
292 );
293 }
294
295 #[test]
296 fn test_check_region_mismatch_filename_unknown_header_has_region() {
297 assert_eq!(check_region_mismatch("game.zip", Region::JAPAN), false);
299 assert_eq!(
300 check_region_mismatch("another game.zip", Region::USA),
301 false
302 );
303 assert_eq!(check_region_mismatch("game_title", Region::EUROPE), false);
304 }
305
306 #[test]
307 fn test_check_region_mismatch_both_unknown() {
308 assert_eq!(check_region_mismatch("game.zip", Region::UNKNOWN), false);
310 assert_eq!(
311 check_region_mismatch("another game.zip", Region::UNKNOWN),
312 false
313 );
314 assert_eq!(check_region_mismatch("game_title", Region::UNKNOWN), false);
315 }
316
317 #[test]
318 fn test_check_region_mismatch_case_insensitivity_filename() {
319 assert_eq!(
321 check_region_mismatch("game (JapAn).zip", Region::JAPAN),
322 false
323 );
324 assert_eq!(check_region_mismatch("game (uSa).zip", Region::USA), false);
325 assert_eq!(
326 check_region_mismatch("game (EuRoPe).zip", Region::EUROPE),
327 false
328 );
329 }
330
331 #[test]
332 fn test_overlap_logic() {
333 let filename_region = infer_region_from_filename("Contra (U).nes"); let header_region = Region::USA | Region::JAPAN;
336
337 assert!(filename_region.intersects(header_region));
339 assert_eq!(check_region_mismatch("Contra (U).nes", Region::USA), false);
340 }
341
342 #[test]
343 fn test_strict_mismatch() {
344 let filename_region = infer_region_from_filename("Contra (E).nes"); let header_region = Region::USA | Region::JAPAN;
347
348 assert!(!filename_region.intersects(header_region));
350 assert_eq!(check_region_mismatch("Contra (E).nes", Region::USA), true);
351 }
352
353 #[test]
354 fn test_world_rom() {
355 assert_eq!(check_region_mismatch("Game (W).bin", Region::USA), false);
358 }
359
360 #[test]
361 fn test_multiple_region_filename_display() {
362 let filename = "Super Game (U) (J).nes";
363 let region = infer_region_from_filename(filename).to_string();
364 assert_eq!(region, "Japan/USA")
365 }
366}