git subrepo pull (merge) --force deps/lightning
[pcsx_rearmed.git] / deps / libretro-common / media / media_detect_cd.c
CommitLineData
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
31static 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
44static 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. */
69bool 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. */
269bool 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}