Commit | Line | Data |
---|---|---|
3719602c PC |
1 | /* Copyright (C) 2010-2020 The RetroArch team |
2 | * | |
3 | * --------------------------------------------------------------------------------------- | |
4 | * The following license statement only applies to this file (media_detect_cd.c). | |
5 | * --------------------------------------------------------------------------------------- | |
6 | * | |
7 | * Permission is hereby granted, free of charge, | |
8 | * to any person obtaining a copy of this software and associated documentation files (the "Software"), | |
9 | * to deal in the Software without restriction, including without limitation the rights to | |
10 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, | |
11 | * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
12 | * | |
13 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |
14 | * | |
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | |
16 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
18 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
19 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
21 | */ | |
22 | ||
23 | #include <media/media_detect_cd.h> | |
24 | #include <streams/file_stream.h> | |
25 | #include <string/stdstring.h> | |
26 | #include <file/file_path.h> | |
27 | #include <retro_miscellaneous.h> | |
28 | ||
29 | /*#define MEDIA_CUE_PARSE_DEBUG*/ | |
30 | ||
31 | static void media_zero_trailing_spaces(char *buf, size_t len) | |
32 | { | |
33 | int i; | |
34 | ||
35 | for (i = len - 1; i >= 0; i--) | |
36 | { | |
37 | if (buf[i] == ' ') | |
38 | buf[i] = '\0'; | |
39 | else if (buf[i] != '\0') | |
40 | break; | |
41 | } | |
42 | } | |
43 | ||
44 | static bool media_skip_spaces(const char **buf, size_t len) | |
45 | { | |
46 | bool found = false; | |
47 | unsigned i; | |
48 | ||
49 | if (!buf || !*buf || !**buf) | |
50 | return false; | |
51 | ||
52 | for (i = 0; i < len; i++) | |
53 | { | |
54 | if ((*buf)[i] == ' ' || (*buf)[i] == '\t') | |
55 | continue; | |
56 | ||
57 | *buf += i; | |
58 | found = true; | |
59 | break; | |
60 | } | |
61 | ||
62 | if (found) | |
63 | return true; | |
64 | ||
65 | return false; | |
66 | } | |
67 | ||
68 | /* Fill in "info" with detected CD info. Use this when you have a cue file and want it parsed to find the first data track and any pregap info. */ | |
69 | bool media_detect_cd_info_cue(const char *path, media_detect_cd_info_t *info) | |
70 | { | |
71 | RFILE *file = NULL; | |
72 | char *line = NULL; | |
73 | char track_path[PATH_MAX_LENGTH] = {0}; | |
74 | char track_abs_path[PATH_MAX_LENGTH] = {0}; | |
75 | char track_mode[11] = {0}; | |
76 | bool found_file = false; | |
77 | bool found_track = false; | |
78 | unsigned first_data_track = 0; | |
79 | uint64_t data_track_pregap_bytes = 0; | |
80 | ||
81 | if (string_is_empty(path) || !info) | |
82 | return false; | |
83 | ||
84 | file = filestream_open(path, RETRO_VFS_FILE_ACCESS_READ, 0); | |
85 | ||
86 | if (!file) | |
87 | { | |
88 | #ifdef MEDIA_CUE_PARSE_DEBUG | |
89 | printf("[MEDIA] Could not open cue path for reading: %s\n", path); | |
90 | fflush(stdout); | |
91 | #endif | |
92 | return false; | |
93 | } | |
94 | ||
95 | while (!filestream_eof(file) && (line = filestream_getline(file))) | |
96 | { | |
97 | size_t len = 0; | |
98 | const char *command = NULL; | |
99 | ||
100 | if (string_is_empty(line)) | |
101 | { | |
102 | free(line); | |
103 | continue; | |
104 | } | |
105 | ||
106 | len = strlen(line); | |
107 | command = line; | |
108 | ||
109 | media_skip_spaces(&command, len); | |
110 | ||
111 | if (!found_file && !strncasecmp(command, "FILE", 4)) | |
112 | { | |
113 | const char *file = command + 4; | |
114 | media_skip_spaces(&file, len - 4); | |
115 | ||
116 | if (!string_is_empty(file)) | |
117 | { | |
118 | const char *file_end = NULL; | |
119 | size_t file_len = 0; | |
120 | bool quoted = false; | |
121 | ||
122 | if (file[0] == '"') | |
123 | { | |
124 | quoted = true; | |
125 | file++; | |
126 | } | |
127 | ||
128 | if (quoted) | |
129 | file_end = strchr(file, '\"'); | |
130 | else | |
131 | file_end = strchr(file, ' '); | |
132 | ||
133 | if (file_end) | |
134 | { | |
135 | file_len = file_end - file; | |
136 | memcpy(track_path, file, file_len); | |
137 | found_file = true; | |
138 | #ifdef MEDIA_CUE_PARSE_DEBUG | |
139 | printf("Found file: %s\n", track_path); | |
140 | fflush(stdout); | |
141 | #endif | |
142 | } | |
143 | } | |
144 | } | |
145 | else if (found_file && !found_track && !strncasecmp(command, "TRACK", 5)) | |
146 | { | |
147 | const char *track = command + 5; | |
148 | media_skip_spaces(&track, len - 5); | |
149 | ||
150 | if (!string_is_empty(track)) | |
151 | { | |
152 | char *ptr = NULL; | |
153 | unsigned track_number = (unsigned)strtol(track, &ptr, 10); | |
154 | #ifdef MEDIA_CUE_PARSE_DEBUG | |
155 | printf("Found track: %d\n", track_number); | |
156 | fflush(stdout); | |
157 | #endif | |
158 | track++; | |
159 | ||
160 | if (track[0] && track[0] != ' ' && track[0] != '\t') | |
161 | track++; | |
162 | ||
163 | if (!string_is_empty(track)) | |
164 | { | |
165 | media_skip_spaces(&track, strlen(track)); | |
166 | #ifdef MEDIA_CUE_PARSE_DEBUG | |
167 | printf("Found track type: %s\n", track); | |
168 | fflush(stdout); | |
169 | #endif | |
170 | if (!strncasecmp(track, "MODE", 4)) | |
171 | { | |
172 | first_data_track = track_number; | |
173 | found_track = true; | |
174 | strlcpy(track_mode, track, sizeof(track_mode)); | |
175 | } | |
176 | else | |
177 | found_file = false; | |
178 | } | |
179 | } | |
180 | } | |
181 | else if (found_file && found_track && first_data_track && !strncasecmp(command, "INDEX", 5)) | |
182 | { | |
183 | const char *index = command + 5; | |
184 | media_skip_spaces(&index, len - 5); | |
185 | ||
186 | if (!string_is_empty(index)) | |
187 | { | |
188 | char *ptr = NULL; | |
189 | unsigned index_number = (unsigned)strtol(index, &ptr, 10); | |
190 | ||
191 | if (index_number == 1) | |
192 | { | |
193 | const char *pregap = index + 1; | |
194 | ||
195 | if (pregap[0] && pregap[0] != ' ' && pregap[0] != '\t') | |
196 | pregap++; | |
197 | ||
198 | if (!string_is_empty(pregap)) | |
199 | { | |
200 | media_skip_spaces(&pregap, strlen(pregap)); | |
201 | found_file = false; | |
202 | found_track = false; | |
203 | ||
204 | if (first_data_track && !string_is_empty(track_mode)) | |
205 | { | |
206 | unsigned track_sector_size = 0; | |
207 | unsigned track_mode_number = 0; | |
208 | ||
209 | if (strlen(track_mode) == 10) | |
210 | { | |
211 | sscanf(track_mode, "MODE%d/%d", (int*)&track_mode_number, (int*)&track_sector_size); | |
212 | #ifdef MEDIA_CUE_PARSE_DEBUG | |
213 | printf("Found track mode %d with sector size %d\n", track_mode_number, track_sector_size); | |
214 | fflush(stdout); | |
215 | #endif | |
216 | if ((track_mode_number == 1 || track_mode_number == 2) && track_sector_size) | |
217 | { | |
218 | unsigned min = 0; | |
219 | unsigned sec = 0; | |
220 | unsigned frame = 0; | |
221 | sscanf(pregap, "%02d:%02d:%02d", (int*)&min, (int*)&sec, (int*)&frame); | |
222 | ||
223 | if (min || sec || frame || strstr(pregap, "00:00:00")) | |
224 | { | |
225 | data_track_pregap_bytes = ((min * 60 + sec) * 75 + frame) * track_sector_size; | |
226 | #ifdef MEDIA_CUE_PARSE_DEBUG | |
227 | printf("Found pregap of %02d:%02d:%02d (bytes: %" PRIu64 ")\n", min, sec, frame, data_track_pregap_bytes); | |
228 | fflush(stdout); | |
229 | #endif | |
230 | break; | |
231 | } | |
232 | } | |
233 | } | |
234 | } | |
235 | } | |
236 | } | |
237 | } | |
238 | } | |
239 | ||
240 | free(line); | |
241 | } | |
242 | ||
243 | filestream_close(file); | |
244 | ||
245 | if (!string_is_empty(track_path)) | |
246 | { | |
247 | if (strstr(track_path, "/") || strstr(track_path, "\\")) | |
248 | { | |
249 | #ifdef MEDIA_CUE_PARSE_DEBUG | |
250 | printf("using path %s\n", track_path); | |
251 | fflush(stdout); | |
252 | #endif | |
253 | return media_detect_cd_info(track_path, data_track_pregap_bytes, info); | |
254 | } | |
255 | ||
256 | fill_pathname_basedir(track_abs_path, path, sizeof(track_abs_path)); | |
257 | strlcat(track_abs_path, track_path, sizeof(track_abs_path)); | |
258 | #ifdef MEDIA_CUE_PARSE_DEBUG | |
259 | printf("using abs path %s\n", track_abs_path); | |
260 | fflush(stdout); | |
261 | #endif | |
262 | return media_detect_cd_info(track_abs_path, data_track_pregap_bytes, info); | |
263 | } | |
264 | ||
265 | return true; | |
266 | } | |
267 | ||
268 | /* Fill in "info" with detected CD info. Use this when you want to open a specific track file directly, and the pregap is known. */ | |
269 | bool media_detect_cd_info(const char *path, uint64_t pregap_bytes, media_detect_cd_info_t *info) | |
270 | { | |
271 | RFILE *file; | |
272 | ||
273 | if (string_is_empty(path) || !info) | |
274 | return false; | |
275 | ||
276 | file = filestream_open(path, RETRO_VFS_FILE_ACCESS_READ, 0); | |
277 | ||
278 | if (!file) | |
279 | { | |
280 | #ifdef MEDIA_CUE_PARSE_DEBUG | |
281 | printf("[MEDIA] Could not open path for reading: %s\n", path); | |
282 | fflush(stdout); | |
283 | #endif | |
284 | return false; | |
285 | } | |
286 | ||
287 | { | |
288 | unsigned offset = 0; | |
289 | unsigned sector_size = 0; | |
290 | unsigned buf_size = 17 * 2352; | |
291 | char *buf = (char*)calloc(1, buf_size); | |
292 | int64_t read_bytes = 0; | |
293 | ||
294 | if (!buf) | |
295 | return false; | |
296 | ||
297 | if (pregap_bytes) | |
298 | filestream_seek(file, pregap_bytes, RETRO_VFS_SEEK_POSITION_START); | |
299 | ||
300 | read_bytes = filestream_read(file, buf, buf_size); | |
301 | ||
302 | if (read_bytes != buf_size) | |
303 | { | |
304 | #ifdef MEDIA_CUE_PARSE_DEBUG | |
305 | printf("[MEDIA] Could not read from media: got %" PRId64 " bytes instead of %d.\n", read_bytes, buf_size); | |
306 | fflush(stdout); | |
307 | #endif | |
308 | filestream_close(file); | |
309 | free(buf); | |
310 | return false; | |
311 | } | |
312 | ||
313 | /* 12-byte sync field at the start of every sector, common to both mode1 and mode2 data tracks | |
314 | * (when at least sync data is requested). This is a CD-ROM standard feature and not specific to any game devices, | |
315 | * and as such should not be part of any system-specific detection or "magic" bytes. | |
316 | * Depending on what parts of a sector were requested from the disc, the user data might start at | |
317 | * byte offset 0, 4, 8, 12, 16 or 24. Cue sheets only specify the total number of bytes requested from the sectors | |
318 | * of a track (like 2048 or 2352) and it is then assumed based on the size/mode as to what fields are present. */ | |
319 | if (!memcmp(buf, "\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00", 12)) | |
320 | { | |
321 | /* Assume track data contains all fields. */ | |
322 | sector_size = 2352; | |
323 | ||
324 | if (buf[15] == 2) | |
325 | { | |
326 | /* assume Mode 2 formed (formless is rarely used) */ | |
327 | offset = 24; | |
328 | } | |
329 | else | |
330 | { | |
331 | /* assume Mode 1 */ | |
332 | offset = 16; | |
333 | } | |
334 | } | |
335 | else | |
336 | { | |
337 | /* Assume sectors only contain user data instead. */ | |
338 | offset = 0; | |
339 | sector_size = 2048; | |
340 | } | |
341 | ||
342 | if (!memcmp(buf + offset, "SEGADISCSYSTEM", | |
343 | STRLEN_CONST("SEGADISCSYSTEM"))) | |
344 | { | |
345 | const char *title_pos = NULL; | |
346 | const char *serial_pos = NULL; | |
347 | ||
348 | /* All discs currently in Redump for MCD start with SEGADISCSYSTEM. There are other strings mentioned elsewhere online, | |
349 | * but I have not seen any real examples of them. */ | |
350 | info->system_id = MEDIA_CD_SYSTEM_MEGA_CD; | |
351 | ||
352 | strcpy_literal(info->system, "Sega CD / Mega CD"); | |
353 | ||
354 | title_pos = buf + offset + 0x150; | |
355 | ||
356 | if (media_skip_spaces(&title_pos, 48)) | |
357 | { | |
358 | memcpy(info->title, title_pos, 48 - (title_pos - (buf + offset + 0x150))); | |
359 | media_zero_trailing_spaces(info->title, sizeof(info->title)); | |
360 | } | |
361 | else | |
362 | { | |
363 | info->title[0] = 'N'; | |
364 | info->title[1] = '/'; | |
365 | info->title[2] = 'A'; | |
366 | info->title[3] = '\0'; | |
367 | } | |
368 | ||
369 | serial_pos = buf + offset + 0x183; | |
370 | ||
371 | if (media_skip_spaces(&serial_pos, 8)) | |
372 | { | |
373 | memcpy(info->serial, serial_pos, 8 - (serial_pos - (buf + offset + 0x183))); | |
374 | media_zero_trailing_spaces(info->serial, sizeof(info->serial)); | |
375 | } | |
376 | else | |
377 | { | |
378 | info->serial[0] = 'N'; | |
379 | info->serial[1] = '/'; | |
380 | info->serial[2] = 'A'; | |
381 | info->serial[3] = '\0'; | |
382 | } | |
383 | } | |
384 | else if (!memcmp(buf + offset, "SEGA SEGASATURN", | |
385 | STRLEN_CONST("SEGA SEGASATURN"))) | |
386 | { | |
387 | const char *title_pos = NULL; | |
388 | const char *serial_pos = NULL; | |
389 | const char *version_pos = NULL; | |
390 | const char *release_date_pos = NULL; | |
391 | ||
392 | info->system_id = MEDIA_CD_SYSTEM_SATURN; | |
393 | ||
394 | strcpy_literal(info->system, "Sega Saturn"); | |
395 | ||
396 | title_pos = buf + offset + 0x60; | |
397 | ||
398 | if (media_skip_spaces(&title_pos, 112)) | |
399 | { | |
400 | memcpy(info->title, title_pos, 112 - (title_pos - (buf + offset + 0x60))); | |
401 | media_zero_trailing_spaces(info->title, sizeof(info->title)); | |
402 | } | |
403 | else | |
404 | { | |
405 | info->title [0] = 'N'; | |
406 | info->title [1] = '/'; | |
407 | info->title [2] = 'A'; | |
408 | info->title [3] = '\0'; | |
409 | } | |
410 | ||
411 | serial_pos = buf + offset + 0x20; | |
412 | ||
413 | if (media_skip_spaces(&serial_pos, 10)) | |
414 | { | |
415 | memcpy(info->serial, serial_pos, 10 - (serial_pos - (buf + offset + 0x20))); | |
416 | media_zero_trailing_spaces(info->serial, sizeof(info->serial)); | |
417 | } | |
418 | else | |
419 | { | |
420 | info->serial[0] = 'N'; | |
421 | info->serial[1] = '/'; | |
422 | info->serial[2] = 'A'; | |
423 | info->serial[3] = '\0'; | |
424 | } | |
425 | ||
426 | version_pos = buf + offset + 0x2a; | |
427 | ||
428 | if (media_skip_spaces(&version_pos, 6)) | |
429 | { | |
430 | memcpy(info->version, version_pos, 6 - (version_pos - (buf + offset + 0x2a))); | |
431 | media_zero_trailing_spaces(info->version, sizeof(info->version)); | |
432 | } | |
433 | else | |
434 | { | |
435 | info->version[0] = 'N'; | |
436 | info->version[1] = '/'; | |
437 | info->version[2] = 'A'; | |
438 | info->version[3] = '\0'; | |
439 | } | |
440 | ||
441 | release_date_pos = buf + offset + 0x30; | |
442 | ||
443 | if (media_skip_spaces(&release_date_pos, 8)) | |
444 | { | |
445 | memcpy(info->release_date, release_date_pos, 8 - (release_date_pos - (buf + offset + 0x30))); | |
446 | media_zero_trailing_spaces(info->release_date, sizeof(info->release_date)); | |
447 | } | |
448 | else | |
449 | { | |
450 | info->release_date[0] = 'N'; | |
451 | info->release_date[1] = '/'; | |
452 | info->release_date[2] = 'A'; | |
453 | info->release_date[3] = '\0'; | |
454 | } | |
455 | } | |
456 | else if (!memcmp(buf + offset, "SEGA SEGAKATANA", STRLEN_CONST("SEGA SEGAKATANA"))) | |
457 | { | |
458 | const char *title_pos = NULL; | |
459 | const char *serial_pos = NULL; | |
460 | const char *version_pos = NULL; | |
461 | const char *release_date_pos = NULL; | |
462 | ||
463 | info->system_id = MEDIA_CD_SYSTEM_DREAMCAST; | |
464 | ||
465 | strcpy_literal(info->system, "Sega Dreamcast"); | |
466 | ||
467 | title_pos = buf + offset + 0x80; | |
468 | ||
469 | if (media_skip_spaces(&title_pos, 96)) | |
470 | { | |
471 | memcpy(info->title, title_pos, 96 - (title_pos - (buf + offset + 0x80))); | |
472 | media_zero_trailing_spaces(info->title, sizeof(info->title)); | |
473 | } | |
474 | else | |
475 | { | |
476 | info->title [0] = 'N'; | |
477 | info->title [1] = '/'; | |
478 | info->title [2] = 'A'; | |
479 | info->title [3] = '\0'; | |
480 | } | |
481 | ||
482 | serial_pos = buf + offset + 0x40; | |
483 | ||
484 | if (media_skip_spaces(&serial_pos, 10)) | |
485 | { | |
486 | memcpy(info->serial, serial_pos, 10 - (serial_pos - (buf + offset + 0x40))); | |
487 | media_zero_trailing_spaces(info->serial, sizeof(info->serial)); | |
488 | } | |
489 | else | |
490 | { | |
491 | info->serial [0] = 'N'; | |
492 | info->serial [1] = '/'; | |
493 | info->serial [2] = 'A'; | |
494 | info->serial [3] = '\0'; | |
495 | } | |
496 | ||
497 | version_pos = buf + offset + 0x4a; | |
498 | ||
499 | if (media_skip_spaces(&version_pos, 6)) | |
500 | { | |
501 | memcpy(info->version, version_pos, 6 - (version_pos - (buf + offset + 0x4a))); | |
502 | media_zero_trailing_spaces(info->version, sizeof(info->version)); | |
503 | } | |
504 | else | |
505 | { | |
506 | info->version [0] = 'N'; | |
507 | info->version [1] = '/'; | |
508 | info->version [2] = 'A'; | |
509 | info->version [3] = '\0'; | |
510 | } | |
511 | ||
512 | release_date_pos = buf + offset + 0x50; | |
513 | ||
514 | if (media_skip_spaces(&release_date_pos, 8)) | |
515 | { | |
516 | memcpy(info->release_date, release_date_pos, 8 - (release_date_pos - (buf + offset + 0x50))); | |
517 | media_zero_trailing_spaces(info->release_date, sizeof(info->release_date)); | |
518 | } | |
519 | else | |
520 | { | |
521 | info->release_date[0] = 'N'; | |
522 | info->release_date[1] = '/'; | |
523 | info->release_date[2] = 'A'; | |
524 | info->release_date[3] = '\0'; | |
525 | } | |
526 | } | |
527 | /* Primary Volume Descriptor fields of ISO9660 */ | |
528 | else if (!memcmp(buf + offset + (16 * sector_size), "\1CD001\1\0PLAYSTATION", 19)) | |
529 | { | |
530 | const char *title_pos = NULL; | |
531 | ||
532 | info->system_id = MEDIA_CD_SYSTEM_PSX; | |
533 | ||
534 | strcpy_literal(info->system, "Sony PlayStation"); | |
535 | ||
536 | title_pos = buf + offset + (16 * sector_size) + 40; | |
537 | ||
538 | if (media_skip_spaces(&title_pos, 32)) | |
539 | { | |
540 | memcpy(info->title, title_pos, 32 - (title_pos - (buf + offset + (16 * sector_size) + 40))); | |
541 | media_zero_trailing_spaces(info->title, sizeof(info->title)); | |
542 | } | |
543 | else | |
544 | { | |
545 | info->title [0] = 'N'; | |
546 | info->title [1] = '/'; | |
547 | info->title [2] = 'A'; | |
548 | info->title [3] = '\0'; | |
549 | } | |
550 | } | |
551 | else if (!memcmp(buf + offset, "\x01\x5a\x5a\x5a\x5a\x5a\x01\x00\x00\x00\x00\x00", 12)) | |
552 | { | |
553 | info->system_id = MEDIA_CD_SYSTEM_3DO; | |
554 | ||
555 | strcpy_literal(info->system, "3DO"); | |
556 | } | |
557 | else if (!memcmp(buf + offset + 0x950, "PC Engine CD-ROM SYSTEM", 23)) | |
558 | { | |
559 | info->system_id = MEDIA_CD_SYSTEM_PC_ENGINE_CD; | |
560 | ||
561 | strcpy_literal(info->system, "TurboGrafx-CD / PC-Engine CD"); | |
562 | } | |
563 | ||
564 | free(buf); | |
565 | } | |
566 | ||
567 | filestream_close(file); | |
568 | ||
569 | return true; | |
570 | } |