Files
RetroArch/tasks/task_database_cue.c
T
LibretroAdmin 0b7c12251d Scanner: Wii U discs, WUX images and Wii U archives
Wii U content did not scan. It now takes the serial lookup by its
product code:

- a WUD, or a Wii U disc by another name (.iso, probed only once the
  other detectors find nothing, so other discs read nothing more), by
  the "WUP-P-XXXX" its header starts with;
- a WUX, through the new formats/wux, the same octets of the disc it
  stands for;
- a Wii U archive (.wua), by the product and company codes of its base
  title's meta/meta.xml, read through formats/zar.

formats/wux reads WUX, the Wii U image format that keeps each distinct
sector once, as the WUD it stands for. Like the other readers it does
no file I/O: it asks for each range it needs. Opening reads the
32-octet header alone; the sector index is read a window at a time,
only where a read needs it; a read's data lands in its own destination,
one request per run of sectors the file keeps in order.

formats/zar gains a directory listing, zar_dir_begin() and
zar_dir_step(), each name read whole, the listing's end ZAR_END.

A Wii U release's serial in the database is the product code and the
maker's two characters, or the code alone for a title sold as a
download; a disc does not say its maker. The code is therefore looked
up as a stem, in the Wii U database alone - glob() in a libretrodb
query now matches binary fields, as a serial is kept - and the release
picked is the one of the maker an archive names, else a download for
an archive and a disc release for a disc. The playlist names the
release found.

wux_test is new, zar_test lists directories, and the scan test gains a
Wii U lane: a WUD, a WUX, a disc named .iso, two archives whose update
is listed before the game, and another database holding the codes.
2026-11-07 13:49:21 +01:00

2826 lines
85 KiB
C

/* RetroArch - A frontend for libretro.
* Copyright (C) 2011-2017 - Daniel De Matteis
* Copyright (C) 2014-2017 - Jean-André Santoni
* Copyright (C) 2016-2019 - Brad Parker
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include <ctype.h>
#include <string.h>
#include <retro_miscellaneous.h>
#include <retro_endianness.h>
#include <compat/strcasestr.h>
#include <compat/strl.h>
#include <file/file_path.h>
#include <retro_endianness.h>
#include <streams/file_stream.h>
#include <streams/interface_stream.h>
#include <encodings/crc32.h>
#include <formats/ciso.h>
#include <formats/xdvdfs.h>
#include <formats/zar.h>
#include <formats/wux.h>
#include <string/stdstring.h>
#include "task_database_cue.h"
#ifdef HAVE_CONFIG_H
#include "../config.h"
#endif
#include "../database_info.h"
#include "tasks_internal.h"
#include "../msg_hash.h"
#include "../verbosity.h"
#define MAX_TOKEN_LEN 255
#ifdef MSB_FIRST
#define MODETEST_VAL 0x00ffffff
#else
#define MODETEST_VAL 0xffffff00
#endif
/* Upper bound on how far into a track any serial/system detector
* reads: DISC_DATA_SIZE_PS2 (600000) is the largest probe and 0x9320
* the deepest magic-number offset. Keep this in sync when adding
* detectors that reach further. */
#define SERIAL_PROBE_SPAN (600 * 1024)
static struct magic_entry MAGIC_NUMBERS[] = {
{ "Nintendo - GameCube", "\xc2\x33\x9f\x3d", 0x00001c},
{ "Nintendo - GameCube", "\xc2\x33\x9f\x3d", 0x000074}, /* RVZ, WIA */
{ "Nintendo - Wii", "\x5d\x1c\x9e\xa3", 0x000018},
{ "Nintendo - Wii", "\x5d\x1c\x9e\xa3", 0x000218}, /* WBFS */
{ "Nintendo - Wii", "\x5d\x1c\x9e\xa3", 0x000070}, /* RVZ, WIA */
{ "Sega - Dreamcast", "SEGA SEGAKATANA", 0x000010},
{ "Sega - Mega-CD - Sega CD", "SEGADISCSYSTEM", 0x000010},
{ "Sega - Saturn", "SEGA SEGASATURN", 0x000010},
{ "Sony - PlayStation", "Sony Computer ", 0x0024f8}, /* PS1 CD license string, PS2 CD doesnt have this string */
{ "Sony - PlayStation 2", "PLAYSTATION", 0x009320}, /* PS1 CD and PS2 CD */
{ "Sony - PlayStation 2", "PLAYSTATION", 0x008008}, /* PS2 DVD */
{ "Sony - PlayStation 2", " ", 0x008008}, /* PS2 DVD */
{ "Sony - PlayStation Portable", "PSP GAME", 0x008008},
/* CD-i magic number should start with \0 at position 0 but it throws off later strlen() */
{ "Philips - CD-i", "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00", 0x000001},
{ NULL, NULL, 0}
};
/**
* Given a filename and position, find the associated disc number.
*/
static int cue_find_disc_number(const char* str1, char disc)
{
switch (disc)
{
case 'a':
case 'A':
return 1;
case 'b':
case 'B':
return 2;
case 'c':
case 'C':
return 3;
case 'd':
case 'D':
return 4;
case 'e':
case 'E':
return 5;
case 'f':
case 'F':
return 6;
case 'g':
case 'G':
return 7;
case 'h':
case 'H':
return 8;
case 'i':
case 'I':
return 9;
default:
if ((disc - '0') >= 1)
return (disc - '0');
break;
}
return 0;
}
/**
* Given a title and filename, append the appropriate disc number to it.
*/
/* @len is the size of the @str1 buffer. This used to bound the
* append with "PATH_MAX_LENGTH - __len" regardless of what was
* actually passed: detect_gc_game() hands it a char[20], so the
* snprintf() was told it had four kilobytes of a twenty byte buffer.
* Only the single-character disc index kept that from overflowing. */
static void cue_append_multi_disc_suffix(char * str1, size_t len,
const char *filename)
{
/* Check multi-disc and insert suffix */
int result = string_find_index_substring_string(filename, "(Disc ");
if (result < 0)
result = string_find_index_substring_string(filename, "(disc ");
if (result < 0)
result = string_find_index_substring_string(filename, "(Disk ");
if (result < 0)
result = string_find_index_substring_string(filename, "(disk ");
if (result >= 0)
{
int disc_number = cue_find_disc_number(filename, filename[result + 6]);
if (disc_number > 0)
{
char *dest = str1;
size_t __len = strlen(dest);
if (__len < len)
snprintf(dest + __len, len - __len, "-%i", disc_number - 1);
}
}
}
/* Cue/GDI sheets are tokenized character by character, and each
* character used to be obtained with intfstream_read(fd, c, 1): one
* VFS round trip per byte, multiplied by every sheet in a library
* scan. The sheets themselves are KB-scale text, so the read cost
* was almost entirely call overhead.
*
* Pull bytes through a small refill window instead. The window is
* deliberately modest (1 KiB) - a typical single-bin cue fits in one
* fill and a large multi-track sheet in a handful - and lives on the
* caller's stack, so the parse allocates nothing and holds no more
* than the window regardless of sheet size.
*
* Entry points that receive a caller-owned stream (cue_next_file,
* gdi_next_file) must leave the stream position exactly where
* byte-at-a-time tokenizing would have left it, because callers
* re-enter with the same handle and gdi_next_file keys off
* intfstream_tell(). cue_tok_detach() rewinds whatever the window
* read ahead before control returns to such callers. */
#define CUE_TOK_BUF_LEN 1024
typedef struct
{
intfstream_t *fd;
size_t pos; /* next unconsumed byte in buf */
size_t end; /* one past the last valid byte in buf */
bool fail; /* underlying stream reported an error */
char buf[CUE_TOK_BUF_LEN];
} cue_tok_stream_t;
static void cue_tok_init(cue_tok_stream_t *t, intfstream_t *fd)
{
t->fd = fd;
t->pos = 0;
t->end = 0;
t->fail = false;
}
/* Returns 1 when bytes are available, 0 on EOF, -1 on stream error. */
static int cue_tok_fill(cue_tok_stream_t *t)
{
int64_t got;
if (t->fail)
return -1;
got = intfstream_read(t->fd, t->buf, CUE_TOK_BUF_LEN);
if (got < 0)
{
t->fail = true;
return -1;
}
t->pos = 0;
t->end = (size_t)got;
return (got == 0) ? 0 : 1;
}
/* Reposition the underlying stream on the first unconsumed byte so
* the caller-visible position matches unbuffered tokenizing. Must
* be called before returning a caller-owned stream. */
static void cue_tok_detach(cue_tok_stream_t *t)
{
size_t ahead = t->end - t->pos;
if (ahead == 0 || t->fail)
return;
if (intfstream_seek(t->fd, -(int64_t)ahead, SEEK_CUR) >= 0)
t->pos = t->end;
}
static int64_t task_database_cue_get_token(cue_tok_stream_t *t, char *s,
uint64_t len)
{
int64_t _len = 0;
int in_string = 0;
if (len < 2)
return -1;
for (;;)
{
char ch;
if (t->pos == t->end)
{
int rv = cue_tok_fill(t);
if (rv < 0)
return -1;
/* EOF with a partial token accumulated discards the token,
* as the unbuffered tokenizer always has. A sheet whose
* final line lacks a trailing newline loses its last token
* either way; changing that is a parsing-behavior decision,
* not an I/O one. */
if (rv == 0)
return 0;
}
ch = t->buf[t->pos++];
switch (ch)
{
case ' ':
case '\t':
case '\r':
case '\n':
if (_len == 0)
continue;
if (!in_string)
{
s[_len] = '\0';
return _len;
}
break;
case '\"':
if (_len == 0)
{
in_string = 1;
continue;
}
s[_len] = '\0';
return _len;
}
s[_len++] = ch;
if (_len == (int64_t)len - 1)
{
s[_len] = '\0';
return _len;
}
}
}
#define DISC_DATA_SIZE_PS1 60000
/* Whether the disc boots PSX.EXE, so carries no serial: a SYSTEM.CNF
* BOOT line names it ("cdrom:", any backslashes, then PSX.EXE), or
* @data lists PSX.EXE and no SYSTEM.CNF, which the BIOS then boots. */
static bool ps1_boots_psx_exe(const char *data, int len)
{
int pos;
bool psx_exe = false;
bool system_cnf = false;
for (pos = 0; pos + 7 <= len; pos++)
{
int boot;
if (memcmp(&data[pos], "PSX.EXE", 7) == 0)
psx_exe = true;
if (pos + 10 <= len && memcmp(&data[pos], "SYSTEM.CNF", 10) == 0)
system_cnf = true;
if (!string_starts_with_case_insensitive(&data[pos], "cdrom:"))
continue;
boot = pos + 6;
while (boot < len && data[boot] == '\\')
boot++;
if ( boot + 7 <= len
&& string_starts_with_case_insensitive(&data[boot], "PSX.EXE"))
return true;
}
return psx_exe && !system_cnf;
}
int detect_ps1_game(intfstream_t *fd, char *s, size_t len,
const char *filename)
{
int pos;
int data_len;
int64_t got = 0;
char raw_game_id[50];
char *disc_data = (char*)malloc(DISC_DATA_SIZE_PS1);
if (!disc_data)
return 0;
if (intfstream_seek(fd, 0, SEEK_SET) >= 0)
got = intfstream_read(fd, disc_data, DISC_DATA_SIZE_PS1);
if (got <= 0)
{
free(disc_data);
return 0;
}
/* A short read leaves the rest of the buffer unset. */
data_len = (int)got;
if (data_len < DISC_DATA_SIZE_PS1)
disc_data[data_len] = '\0';
else
disc_data[DISC_DATA_SIZE_PS1 - 1] = '\0';
if (ps1_boots_psx_exe(disc_data, data_len))
{
strlcpy(s, "PSX.EXE", len);
cue_append_multi_disc_suffix(s, len, filename);
free(disc_data);
return 0;
}
for (pos = 0; pos < data_len; pos++)
{
strlcpy(raw_game_id, &disc_data[pos], sizeof(raw_game_id));
raw_game_id[12] = '\0';
if (raw_game_id[0] == 'S' || raw_game_id[0] == 'E')
{
if ( memcmp(raw_game_id, "SCUS_", 5) == 0
|| memcmp(raw_game_id, "SLUS_", 5) == 0
|| memcmp(raw_game_id, "SLES_", 5) == 0
|| memcmp(raw_game_id, "SCED_", 5) == 0
|| memcmp(raw_game_id, "SLPS_", 5) == 0
|| memcmp(raw_game_id, "SLPM_", 5) == 0
|| memcmp(raw_game_id, "SCPS_", 5) == 0
|| memcmp(raw_game_id, "SLED_", 5) == 0
|| memcmp(raw_game_id, "SIPS_", 5) == 0
|| memcmp(raw_game_id, "ESPM_", 5) == 0
|| memcmp(raw_game_id, "SCES_", 5) == 0
|| memcmp(raw_game_id, "SLKA_", 5) == 0
|| memcmp(raw_game_id, "SCAJ_", 5) == 0
)
{
raw_game_id[4] = '-';
if (raw_game_id[8] == '.')
{
raw_game_id[8] = raw_game_id[9];
raw_game_id[9] = raw_game_id[10];
}
else if (raw_game_id[7] == '.')
{
raw_game_id[7] = raw_game_id[8];
raw_game_id[8] = raw_game_id[9];
raw_game_id[9] = raw_game_id[10];
}
raw_game_id[10] = '\0';
string_remove_all_whitespace(s, raw_game_id);
cue_append_multi_disc_suffix(s, len, filename);
free(disc_data);
return 1;
}
}
else if (memcmp(raw_game_id, "LSP-", 4) == 0)
{
raw_game_id[10] = '\0';
string_remove_all_whitespace(s, raw_game_id);
cue_append_multi_disc_suffix(s, len, filename);
free(disc_data);
return 1;
}
}
s[0 ] = 'X';
s[1 ] = 'X';
s[2 ] = 'X';
s[3 ] = 'X';
s[4 ] = 'X';
s[5 ] = 'X';
s[6 ] = 'X';
s[7 ] = 'X';
s[8 ] = 'X';
s[9 ] = 'X';
s[10] = '\0';
cue_append_multi_disc_suffix(s, len, filename);
free(disc_data);
return 0;
}
int detect_ps2_game(intfstream_t *fd, char *s, size_t len,
const char *filename)
{
#define DISC_DATA_SIZE_PS2 600000
int pos;
char raw_game_id[50];
char *disc_data;
/* Load data into buffer and use pointers */
if (intfstream_seek(fd, 0, SEEK_SET) < 0)
return 0;
disc_data = (char*)malloc(DISC_DATA_SIZE_PS2);
/* NULL-check: intfstream_read writes into disc_data via
* filestream_read (which calls fread with no NULL-guard on the
* dest). Matches the NULL-checked pattern in the PS1 / PSP
* variants above/below. */
if (!disc_data)
return 0;
if (intfstream_read(fd, disc_data, DISC_DATA_SIZE_PS2) <= 0)
{
free(disc_data);
return 0;
}
disc_data[DISC_DATA_SIZE_PS2 - 1] = '\0';
for (pos = 0; pos < DISC_DATA_SIZE_PS2 - 12; pos++)
{
/* Quick first-character check to avoid unnecessary work */
char c = disc_data[pos];
if ( c != 'S' && c != 'P' && c != 'T' && c != 'C'
&& c != 'H' && c != 'A' && c != 'V' && c != 'L'
&& c != 'M' && c != 'N' && c != 'U' && c != 'W'
&& c != 'G' && c != 'K' && c != 'R')
continue;
strlcpy(raw_game_id, &disc_data[pos], 13);
if ( memcmp(raw_game_id, "SLPM_", STRLEN_CONST("SLPM_")) == 0
|| memcmp(raw_game_id, "SLES_", STRLEN_CONST("SLES_")) == 0
|| memcmp(raw_game_id, "SCES_", STRLEN_CONST("SCES_")) == 0
|| memcmp(raw_game_id, "SLUS_", STRLEN_CONST("SLUS_")) == 0
|| memcmp(raw_game_id, "SLPS_", STRLEN_CONST("SLPS_")) == 0
|| memcmp(raw_game_id, "SCED_", STRLEN_CONST("SCED_")) == 0
|| memcmp(raw_game_id, "SCUS_", STRLEN_CONST("SCUS_")) == 0
|| memcmp(raw_game_id, "SCPS_", STRLEN_CONST("SCPS_")) == 0
|| memcmp(raw_game_id, "SCAJ_", STRLEN_CONST("SCAJ_")) == 0
|| memcmp(raw_game_id, "SLKA_", STRLEN_CONST("SLKA_")) == 0
|| memcmp(raw_game_id, "SCKA_", STRLEN_CONST("SCKA_")) == 0
|| memcmp(raw_game_id, "SLAJ_", STRLEN_CONST("SLAJ_")) == 0
|| memcmp(raw_game_id, "TCPS_", STRLEN_CONST("TCPS_")) == 0
|| memcmp(raw_game_id, "KOEI_", STRLEN_CONST("KOEI_")) == 0
|| memcmp(raw_game_id, "PBPX_", STRLEN_CONST("PBPX_")) == 0
|| memcmp(raw_game_id, "PCPX_", STRLEN_CONST("PCPX_")) == 0
|| memcmp(raw_game_id, "PAPX_", STRLEN_CONST("PAPX_")) == 0
|| memcmp(raw_game_id, "SCCS_", STRLEN_CONST("SCCS_")) == 0
|| memcmp(raw_game_id, "ALCH_", STRLEN_CONST("ALCH_")) == 0
|| memcmp(raw_game_id, "TCES_", STRLEN_CONST("TCES_")) == 0
|| memcmp(raw_game_id, "CPCS_", STRLEN_CONST("CPCS_")) == 0
|| memcmp(raw_game_id, "SLED_", STRLEN_CONST("SLED_")) == 0
|| memcmp(raw_game_id, "TLES_", STRLEN_CONST("TLES_")) == 0
|| memcmp(raw_game_id, "GUST_", STRLEN_CONST("GUST_")) == 0
|| memcmp(raw_game_id, "CF00_", STRLEN_CONST("CF00_")) == 0
|| memcmp(raw_game_id, "SCPN_", STRLEN_CONST("SCPN_")) == 0
|| memcmp(raw_game_id, "SCPM_", STRLEN_CONST("SCPM_")) == 0
|| memcmp(raw_game_id, "PSXC_", STRLEN_CONST("PSXC_")) == 0
|| memcmp(raw_game_id, "SLPN_", STRLEN_CONST("SLPN_")) == 0
|| memcmp(raw_game_id, "ULKS_", STRLEN_CONST("ULKS_")) == 0
|| memcmp(raw_game_id, "LDTL_", STRLEN_CONST("LDTL_")) == 0
|| memcmp(raw_game_id, "PKP2_", STRLEN_CONST("PKP2_")) == 0
|| memcmp(raw_game_id, "WLFD_", STRLEN_CONST("WLFD_")) == 0
|| memcmp(raw_game_id, "CZP2_", STRLEN_CONST("CZP2_")) == 0
|| memcmp(raw_game_id, "HAKU_", STRLEN_CONST("HAKU_")) == 0
|| memcmp(raw_game_id, "SRPM_", STRLEN_CONST("SRPM_")) == 0
|| memcmp(raw_game_id, "MTP2_", STRLEN_CONST("MTP2_")) == 0
|| memcmp(raw_game_id, "NMP2_", STRLEN_CONST("NMP2_")) == 0
|| memcmp(raw_game_id, "ARZE_", STRLEN_CONST("ARZE_")) == 0
|| memcmp(raw_game_id, "VUGJ_", STRLEN_CONST("VUGJ_")) == 0
|| memcmp(raw_game_id, "ARP2_", STRLEN_CONST("ARP2_")) == 0
|| memcmp(raw_game_id, "ROSE_", STRLEN_CONST("ROSE_")) == 0
)
{
raw_game_id[4] = '-';
if (raw_game_id[8] == '.')
{
raw_game_id[8] = raw_game_id[9];
raw_game_id[9] = raw_game_id[10];
raw_game_id[10] = '\0';
}
/* A few games have their serial in the form of xx.xxx */
/* Tanaka Torahiko no Ultra-ryuu Shougi - Ibisha Anaguma-hen (Japan) -> SLPS_02.261 */
else if (raw_game_id[7] == '.')
{
raw_game_id[7] = raw_game_id[8];
raw_game_id[8] = raw_game_id[9];
raw_game_id[9] = raw_game_id[10];
raw_game_id[10] = '\0';
}
else
raw_game_id[10] = '\0';
/* Fix corrupted digits (control char 0x12 -> '3') */
if (raw_game_id[8] == '\x12')
raw_game_id[8] = '3';
if (raw_game_id[9] == '\x12')
raw_game_id[9] = '3';
string_remove_all_whitespace(s, raw_game_id);
cue_append_multi_disc_suffix(s, len, filename);
free(disc_data);
return 1;
}
}
s[0 ] = 'X';
s[1 ] = 'X';
s[2 ] = 'X';
s[3 ] = 'X';
s[4 ] = 'X';
s[5 ] = 'X';
s[6 ] = 'X';
s[7 ] = 'X';
s[8 ] = 'X';
s[9 ] = 'X';
s[10] = '\0';
cue_append_multi_disc_suffix(s, len, filename);
free(disc_data);
return 0;
}
int detect_psp_game(intfstream_t *fd, char *s, size_t len,
const char *filename)
{
#define DISC_DATA_SIZE_PSP 300000
int pos;
ssize_t bytes_read;
char *disc_data = (char*)malloc(DISC_DATA_SIZE_PSP);
if (!disc_data)
return 0;
if (intfstream_seek(fd, 0, SEEK_SET) < 0)
{
free(disc_data);
return 0;
}
bytes_read = intfstream_read(fd, disc_data, DISC_DATA_SIZE_PSP);
if (bytes_read <= 0)
{
free(disc_data);
return 0;
}
for (pos = 0; pos <= bytes_read - 10; pos++)
{
const char *p = &disc_data[pos];
if (*p != 'U' && *p != 'N')
continue;
if (
memcmp(p, "ULES-", 5) == 0
|| memcmp(p, "ULUS-", 5) == 0
|| memcmp(p, "ULJS-", 5) == 0
|| memcmp(p, "ULEM-", 5) == 0
|| memcmp(p, "ULUM-", 5) == 0
|| memcmp(p, "ULJM-", 5) == 0
|| memcmp(p, "UCES-", 5) == 0
|| memcmp(p, "UCUS-", 5) == 0
|| memcmp(p, "UCJS-", 5) == 0
|| memcmp(p, "UCAS-", 5) == 0
|| memcmp(p, "UCKS-", 5) == 0
|| memcmp(p, "ULKS-", 5) == 0
|| memcmp(p, "ULAS-", 5) == 0
|| memcmp(p, "NPEH-", 5) == 0
|| memcmp(p, "NPUH-", 5) == 0
|| memcmp(p, "NPJH-", 5) == 0
|| memcmp(p, "NPHH-", 5) == 0
|| memcmp(p, "NPEG-", 5) == 0
|| memcmp(p, "NPUG-", 5) == 0
|| memcmp(p, "NPJG-", 5) == 0
|| memcmp(p, "NPHG-", 5) == 0
|| memcmp(p, "NPEZ-", 5) == 0
|| memcmp(p, "NPUZ-", 5) == 0
|| memcmp(p, "NPJZ-", 5) == 0
)
{
strlcpy(s, p, MIN(len, 11));
cue_append_multi_disc_suffix(s, len, filename);
free(disc_data);
return 1;
}
}
free(disc_data);
return 0;
}
/* Reads a little-endian uint32 from a 4-byte buffer. PBP/SFO header
* fields are always stored little-endian regardless of host. */
static uint32_t pbp_read_u32(const uint8_t *p)
{
return (uint32_t)p[0]
| ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16)
| ((uint32_t)p[3] << 24);
}
/* detect_pbp_game:
*
* PSP and PS1-Classic content is distributed as EBOOT.PBP. The disc
* image inside DATA.PSAR is encrypted (PGD/AMCTRL) on official titles,
* so the on-disc license string and serial cannot be located by the
* raw linear scan used for CUE/CHD/ISO. The plaintext PARAM.SFO at the
* head of the PBP, however, carries the serial in its DISC_ID key
* (PS1-Classics) or TITLE_ID key (PSP), so we parse that instead.
*
* PBP header layout (little-endian):
* 0x00 u32 magic = 0x50425000 ('\0','P','B','P')
* 0x04 u32 version
* 0x08 u32 offset of PARAM.SFO
* 0x0C u32 offset of ICON0.PNG (== PARAM.SFO end)
* ... five further sub-file offsets ...
*
* PARAM.SFO layout (little-endian):
* 0x00 u32 magic = 0x46535000 ('\0','P','S','F')
* 0x08 u32 key_table_start (relative to SFO start)
* 0x0C u32 data_table_start (relative to SFO start)
* 0x10 u32 num_entries
* 0x14 .. index entries, 16 bytes each:
* u16 key_offset (relative to key_table_start)
* u16 data_fmt
* u32 data_len
* u32 data_max_len
* u32 data_offset (relative to data_table_start)
*
* The DISC_ID/TITLE_ID value is stored without a separator
* (e.g. "SCUS94900"); we re-insert the hyphen after the 4-letter
* prefix to produce the "SCUS-94900" form the RDB is keyed on.
*/
int detect_pbp_game(intfstream_t *fd, char *s, size_t len,
const char *filename)
{
#define PBP_SFO_MAX 65536
uint8_t pbp_header[0x28];
uint8_t *sfo = NULL;
uint32_t sfo_off = 0;
uint32_t sfo_end = 0;
uint32_t sfo_size = 0;
uint32_t key_tbl = 0;
uint32_t data_tbl = 0;
uint32_t entries = 0;
uint32_t i;
char raw_id[16];
int found = 0;
if (intfstream_seek(fd, 0, SEEK_SET) < 0)
return 0;
if (intfstream_read(fd, pbp_header, sizeof(pbp_header))
!= (int64_t)sizeof(pbp_header))
return 0;
/* Verify '\0PBP' magic. */
if (pbp_read_u32(pbp_header) != 0x50425000)
return 0;
sfo_off = pbp_read_u32(pbp_header + 0x08);
sfo_end = pbp_read_u32(pbp_header + 0x0C);
if (sfo_end <= sfo_off)
return 0;
sfo_size = sfo_end - sfo_off;
if (sfo_size < 0x14 || sfo_size > PBP_SFO_MAX)
return 0;
if (intfstream_seek(fd, (int64_t)sfo_off, SEEK_SET) < 0)
return 0;
sfo = (uint8_t*)malloc(sfo_size);
/* NULL-check: intfstream_read writes into sfo via filestream_read
* (fread, no NULL-guard on the dest). */
if (!sfo)
return 0;
if (intfstream_read(fd, sfo, sfo_size) != (int64_t)sfo_size)
{
free(sfo);
return 0;
}
/* Verify '\0PSF' magic. */
if (pbp_read_u32(sfo) != 0x46535000)
{
free(sfo);
return 0;
}
key_tbl = pbp_read_u32(sfo + 0x08);
data_tbl = pbp_read_u32(sfo + 0x0C);
entries = pbp_read_u32(sfo + 0x10);
if ( key_tbl >= sfo_size
|| data_tbl >= sfo_size
|| entries == 0
|| entries > 1024)
{
free(sfo);
return 0;
}
/* First pass prefers DISC_ID (PS1-Classics), second pass falls
* back to TITLE_ID (PSP). DISC_ID matches the RDB serial form
* directly; TITLE_ID is the PSP product code, also RDB-keyed. */
for (i = 0; i < entries && !found; i++)
{
uint32_t idx = 0x14 + i * 0x10;
uint16_t koff;
uint32_t dlen;
uint32_t doff;
const char *key;
const char *val;
if (idx + 0x10 > sfo_size)
break;
koff = (uint16_t)(sfo[idx] | (sfo[idx + 1] << 8));
dlen = pbp_read_u32(sfo + idx + 0x04);
doff = pbp_read_u32(sfo + idx + 0x0C);
if ( (uint32_t)key_tbl + koff >= sfo_size
|| (uint32_t)data_tbl + doff >= sfo_size)
continue;
key = (const char*)(sfo + key_tbl + koff);
val = (const char*)(sfo + data_tbl + doff);
if ( strcmp(key, "DISC_ID") != 0
&& strcmp(key, "TITLE_ID") != 0)
continue;
/* dlen counts the trailing NUL for UTF8 SFO strings; require a
* sane product-code length (e.g. "SCUS94900" -> 9 + NUL). */
if (dlen < 9 || (uint32_t)data_tbl + doff + dlen > sfo_size)
continue;
/* Copy the 9-char product code (4 alpha + 5 digit). */
strlcpy(raw_id, val, sizeof(raw_id));
raw_id[9] = '\0';
/* Re-insert the separator: "SCUS94900" -> "SCUS-94900". */
memmove(raw_id + 5, raw_id + 4, 5);
raw_id[4] = '-';
raw_id[10] = '\0';
string_remove_all_whitespace(s, raw_id);
cue_append_multi_disc_suffix(s, len, filename);
found = 1;
}
free(sfo);
return found;
#undef PBP_SFO_MAX
}
/* detect_3ds_game:
*
* The product code ("CTR-P-AMKE") is at 0x150 of an NCCH header: the
* start of a bare .cxi, or partition 0 of a cart image (NCSD), whose
* partition table counts media units of 0x200 << flags[6]. Layouts
* from 3dbrew's NCSD and NCCH pages. The NCCH header is plaintext in
* encrypted dumps too.
*/
int detect_3ds_game(intfstream_t *fd, char *s, size_t len)
{
uint8_t hdr[0x200];
size_t i;
if ( intfstream_seek(fd, 0, SEEK_SET) < 0
|| intfstream_read(fd, hdr, sizeof(hdr)) != (int64_t)sizeof(hdr))
return 0;
if (memcmp(hdr + 0x100, "NCSD", 4) == 0)
{
uint64_t ncch;
if (hdr[0x18E] > 15)
return 0;
ncch = (uint64_t)pbp_read_u32(hdr + 0x120) << (9 + hdr[0x18E]);
if ( !ncch
|| intfstream_seek(fd, (int64_t)ncch, SEEK_SET) < 0
|| intfstream_read(fd, hdr, sizeof(hdr)) != (int64_t)sizeof(hdr))
return 0;
}
if (memcmp(hdr + 0x100, "NCCH", 4) != 0)
return 0;
for (i = 0; i < 0x10 && hdr[0x150 + i]; i++)
if (hdr[0x150 + i] <= ' ' || hdr[0x150 + i] > '~')
return 0;
if (!i || i >= len)
return 0;
memcpy(s, hdr + 0x150, i);
s[i] = '\0';
return 1;
}
size_t detect_gc_game(intfstream_t *fd, char *s, size_t len,
const char *filename)
{
char region_id;
char pre_game_id[20];
char raw_game_id[20];
size_t _len = 0;
/* Load raw serial or quit */
if (intfstream_seek(fd, 0, SEEK_SET) < 0)
return 0;
if (intfstream_read(fd, raw_game_id, 4) <= 0)
return 0;
if ( memcmp(raw_game_id, "RVZ", STRLEN_CONST("RVZ")) == 0
|| memcmp(raw_game_id, "WIA", STRLEN_CONST("WIA")) == 0
)
{
if (intfstream_seek(fd, 0x0058, SEEK_SET) < 0)
return 0;
if (intfstream_read(fd, raw_game_id, 4) <= 0)
return 0;
}
raw_game_id[4] = '\0';
/** Scrub files with bad data and log **/
if (raw_game_id[0] == '\0' || raw_game_id[0] == ' ')
{
#ifdef DEBUG
RARCH_LOG("[Scanner] Scrubbing: \"%s\".\n", filename);
#endif
return 0;
}
/** convert raw gamecube serial to redump serial.
not enough is known about the disc data to properly
convert every raw serial to redump serial. it will
only fail with the following exceptions: the
subregions of europe P-UKV, P-AUS, X-UKV, X-EUU
will not match redump.**/
/** insert prefix **/
_len = strlcpy_lit(pre_game_id, "DL-DOL-", sizeof(pre_game_id));
/** add raw serial **/
strlcpy(pre_game_id + _len, raw_game_id, sizeof(pre_game_id) - _len);
/** check region **/
region_id = pre_game_id[10];
/** check multi-disc and insert suffix **/
cue_append_multi_disc_suffix(pre_game_id, sizeof(pre_game_id), filename);
_len = strlcpy(s, pre_game_id, len);
switch (region_id)
{
case 'E':
_len += strlcpy_lit(s + _len, "-USA", len - _len);
break;
case 'J':
_len += strlcpy_lit(s + _len, "-JPN", len - _len);
break;
case 'P': /** NYI: P can also be P-UKV, P-AUS **/
case 'X': /** NYI: X can also be X-UKV, X-EUU **/
_len += strlcpy_lit(s + _len, "-EUR", len - _len);
break;
case 'Y':
_len += strlcpy_lit(s + _len, "-FAH", len - _len);
break;
case 'D':
_len += strlcpy_lit(s + _len, "-NOE", len - _len);
break;
case 'S':
_len += strlcpy_lit(s + _len, "-ESP", len - _len);
break;
case 'F':
_len += strlcpy_lit(s + _len, "-FRA", len - _len);
break;
case 'I':
_len += strlcpy_lit(s + _len, "-ITA", len - _len);
break;
case 'H':
_len += strlcpy_lit(s + _len, "-HOL", len - _len);
break;
default:
return 0;
}
return _len;
}
int detect_scd_game(intfstream_t *fd, char *s, size_t len,
const char *filename)
{
#define SCD_SERIAL_OFFSET 0x0193
#define SCD_SERIAL_LEN 11
#define SCD_REGION_OFFSET 0x0200
int index;
size_t _len;
char pre_game_id[SCD_SERIAL_LEN+1];
char raw_game_id[SCD_SERIAL_LEN+1];
char check_suffix_50[10];
char region_id;
char lgame_id[10];
/* Load raw serial or quit */
if (intfstream_seek(fd, SCD_SERIAL_OFFSET, SEEK_SET) < 0)
return 0;
if (intfstream_read(fd, raw_game_id, SCD_SERIAL_LEN) <= 0)
return 0;
raw_game_id[SCD_SERIAL_LEN] = '\0';
/* Load raw region id or quit */
if (intfstream_seek(fd, SCD_REGION_OFFSET, SEEK_SET) < 0)
return 0;
if (intfstream_read(fd, &region_id, 1) <= 0)
return 0;
#ifdef DEBUG
/** Scrub files with bad data and log **/
if (raw_game_id[0] == '\0' || raw_game_id[0] == ' ' || raw_game_id[0] == '0')
RARCH_LOG("[Scanner] Scrubbing: \"%s\".\n", filename);
#endif
/** Convert raw Sega - Mega-CD - Sega CD serial to redump serial. **/
/** process raw serial to a pre serial without spaces **/
_len = string_remove_all_whitespace(pre_game_id, raw_game_id);
/** Force minimum serial length since it is assumed **/
if (_len < 2)
_len = 2;
strlcpy(check_suffix_50, &pre_game_id[_len - 2], sizeof(check_suffix_50));
check_suffix_50[2] = '\0';
/** redump serials are built differently for each prefix **/
if ( pre_game_id[0] == 'T'
&& pre_game_id[1] == '-')
{
if (region_id == 'U' || region_id == 'J')
{
if ((index = string_index_last_occurance(pre_game_id, '-')) == -1)
return 0;
strlcpy(s, pre_game_id, (size_t)(index + 1));
cue_append_multi_disc_suffix(s, len, filename);
return 1;
}
if ((index = string_index_last_occurance(pre_game_id, '-')) == -1)
return 0;
strlcpy(lgame_id, pre_game_id, (size_t)(index + 1));
_len = strlcpy(s, lgame_id, len);
s[ _len] = '-';
s[++_len] = '5';
s[++_len] = '0';
s[++_len] = '\0';
cue_append_multi_disc_suffix(s, len, filename);
}
else if (pre_game_id[0] == 'G'
&& pre_game_id[1] == '-')
{
if ((index = string_index_last_occurance(pre_game_id, '-')) == -1)
return 0;
strlcpy(s, pre_game_id, (size_t)(index + 1));
cue_append_multi_disc_suffix(s, len, filename);
}
else if (pre_game_id[0] == 'M'
&& pre_game_id[1] == 'K'
&& pre_game_id[2] == '-')
{
if ( check_suffix_50[0] == '5'
&& check_suffix_50[1] == '0')
{
strlcpy(lgame_id, &pre_game_id[3], 5);
_len = strlcpy(s, lgame_id, len);
s[ _len] = '-';
s[++_len] = '5';
s[++_len] = '0';
s[++_len] = '\0';
}
else
strlcpy(s, &pre_game_id[3], 5);
cue_append_multi_disc_suffix(s, len, filename);
}
else
{
string_trim_whitespace_right(raw_game_id);
string_trim_whitespace_left(raw_game_id);
strlcpy(s, raw_game_id, len);
}
return 1;
}
int detect_sat_game(intfstream_t *fd, char *s, size_t len,
const char *filename)
{
#define SAT_SERIAL_OFFSET 0x0030
#define SAT_SERIAL_LEN 9
#define SAT_REGION_OFFSET 0x0050
size_t _len, raw_len;
char raw_game_id[SAT_SERIAL_LEN+1];
char region_id;
char check_suffix[3];
char lgame_id[3];
char rgame_id[10];
/* Load raw serial or quit */
if (intfstream_seek(fd, SAT_SERIAL_OFFSET, SEEK_SET) < 0)
return 0;
if (intfstream_read(fd, raw_game_id, SAT_SERIAL_LEN) <= 0)
return 0;
raw_game_id[SAT_SERIAL_LEN] = '\0';
/* Load raw region id or quit */
if (intfstream_seek(fd, SAT_REGION_OFFSET, SEEK_SET) < 0)
return 0;
if (intfstream_read(fd, &region_id, 1) <= 0)
return 0;
/** Scrub files with bad data and log **/
if (raw_game_id[0] == '\0' || raw_game_id[0] == ' ')
{
#ifdef DEBUG
RARCH_LOG("[Scanner] Scrubbing: \"%s\".\n", filename);
#endif
return 0;
}
string_trim_whitespace_right(raw_game_id);
string_trim_whitespace_left(raw_game_id);
/** Dissect this raw serial into parts **/
raw_len = strlen(raw_game_id);
if (raw_len < 2)
{
strlcpy(s, raw_game_id, len);
return 1;
}
strlcpy(check_suffix, &raw_game_id[raw_len - 2], sizeof(check_suffix));
/** redump serials are built differently for each region **/
switch (region_id)
{
case 'B': /* Brazil/multi-region (BKUT) */
case 'T': /* Taiwan/Asia */
case 'K': /* Korea */
case 'A': /* Americas */
case 'U': /* USA */
if ( raw_game_id[0] == 'M'
&& raw_game_id[1] == 'K'
&& raw_game_id[2] == '-')
{
const char *serial_start = &raw_game_id[3];
/* Strip leading zeros from serial number */
while (*serial_start == '0' && *(serial_start + 1) != '\0')
serial_start++;
strlcpy(s, serial_start, len);
}
else
strlcpy(s, raw_game_id, len);
cue_append_multi_disc_suffix(s, len, filename);
break;
case 'E':
strlcpy(lgame_id, raw_game_id, sizeof(lgame_id));
if ( !strcmp(check_suffix, "-5")
|| !strcmp(check_suffix, "50"))
{
/* Copy chars from index 2, excluding last 2 chars of raw serial.
* Clamp to rgame_id buffer size to prevent overflow. */
size_t copy_len = raw_len - 4; /* chars to copy (excluding index 0-1 and last 2) */
if (copy_len >= sizeof(rgame_id))
copy_len = sizeof(rgame_id) - 1;
memcpy(rgame_id, &raw_game_id[2], copy_len);
rgame_id[copy_len] = '\0';
}
else
strlcpy(rgame_id, &raw_game_id[2], sizeof(rgame_id));
_len = strlcpy(s, lgame_id, len);
_len += strlcpy(s + _len, rgame_id, len - _len);
if (_len + 3 < len)
{
s[ _len] = '-';
s[++_len] = '5';
s[++_len] = '0';
s[++_len] = '\0';
}
else
s[_len] = '\0';
cue_append_multi_disc_suffix(s, len, filename);
break;
case 'J':
strlcpy(s, raw_game_id, len);
cue_append_multi_disc_suffix(s, len, filename);
break;
default:
strlcpy(s, raw_game_id, len);
break;
}
return 1;
}
int detect_dc_game(intfstream_t *fd, char *s, size_t len,
const char *filename)
{
int index;
int total_hyphens;
int total_hyphens_recalc;
size_t _len, __len, ___len;
char pre_game_id[50];
char raw_game_id[50];
char lgame_id[20];
char rgame_id[20];
char region_id;
/* Load raw serial or quit */
if (intfstream_seek(fd, 0x0050, SEEK_SET) < 0)
return 0;
if (intfstream_read(fd, raw_game_id, 10) <= 0)
return 0;
if (intfstream_seek(fd, 0x0042, SEEK_SET) < 0)
return false;
if (intfstream_read(fd, &region_id, 1) <= 0)
return false;
raw_game_id[10] = '\0';
/** Scrub files with bad data and log **/
if ( raw_game_id[0] == '\0'
|| raw_game_id[0] == ' ')
{
#ifdef DEBUG
RARCH_LOG("[Scanner] Scrubbing: \"%s\".\n", filename);
#endif
return 0;
}
string_trim_whitespace_right(raw_game_id);
string_trim_whitespace_left(raw_game_id);
string_replace_multi_space_with_single_space(raw_game_id);
string_replace_whitespace_with_single_character(raw_game_id, '-');
__len = strlen(raw_game_id);
total_hyphens = string_count_occurrences_single_character(raw_game_id, '-');
/** redump serials are built differently for each prefix **/
if ( raw_game_id[0] == 'T'
&& raw_game_id[1] == '-')
{
if (total_hyphens >= 2)
{
index = string_index_last_occurance(raw_game_id, '-');
if (index < 0)
return 0;
strlcpy(lgame_id, &raw_game_id[0], (size_t)index + 1);
strlcpy(rgame_id, &raw_game_id[index + 1], sizeof(rgame_id));
_len = strlcpy(s, lgame_id, len);
s[ _len] = '-';
s[++_len] = '\0';
strlcpy(s + _len, rgame_id, len - _len);
}
else if (__len <= 7)
strlcpy(s, raw_game_id, 8);
else
{
strlcpy(lgame_id, raw_game_id, 8);
strlcpy(rgame_id, &raw_game_id[__len - 2], sizeof(rgame_id));
_len = strlcpy(s, lgame_id, len);
s[ _len] = '-';
s[++_len] = '\0';
strlcpy(s + _len, rgame_id, len - _len);
}
cue_append_multi_disc_suffix(s, len, filename);
}
else if (raw_game_id[0] == 'T')
{
strlcpy(lgame_id, raw_game_id, 2);
strlcpy(rgame_id, &raw_game_id[1], sizeof(rgame_id));
_len = strlcpy(pre_game_id, lgame_id, sizeof(pre_game_id) - 2);
pre_game_id[ _len] = '-';
pre_game_id[++_len] = '\0';
strlcpy(pre_game_id + _len, rgame_id, sizeof(pre_game_id) - _len);
total_hyphens_recalc = string_count_occurrences_single_character(pre_game_id, '-');
if (total_hyphens_recalc >= 2)
{
index = string_index_last_occurance(pre_game_id, '-');
if (index < 0)
return 0;
strlcpy(lgame_id, pre_game_id, (size_t)index + 1);
___len = strlen(pre_game_id);
}
else
{
___len = strlen(pre_game_id) - 1;
if (___len <= 8)
{
strlcpy(s, pre_game_id, 9);
cue_append_multi_disc_suffix(s, len, filename);
return 1;
}
strlcpy(lgame_id, pre_game_id, 8);
}
strlcpy(rgame_id, &pre_game_id[___len - 2], sizeof(rgame_id));
_len = strlcpy(s, lgame_id, len);
s[ _len] = '-';
s[++_len] = '\0';
strlcpy(s + _len, rgame_id, len - _len);
cue_append_multi_disc_suffix(s, len, filename);
}
else if (raw_game_id[0] == 'H'
&& raw_game_id[1] == 'D'
&& raw_game_id[2] == 'R'
&& raw_game_id[3] == '-')
{
if (total_hyphens >= 2)
{
index = string_index_last_occurance(raw_game_id, '-');
if (index < 0)
return 0;
strlcpy(lgame_id, raw_game_id, (size_t)index);
strlcpy(rgame_id, &raw_game_id[__len - 4], sizeof(rgame_id));
_len = strlcpy(s, lgame_id, len);
s[ _len] = '-';
s[++_len] = '\0';
strlcpy(s + _len, rgame_id, len - _len);
}
else
strlcpy(s, raw_game_id, len);
cue_append_multi_disc_suffix(s, len, filename);
}
else if (raw_game_id[0] == 'M'
&& raw_game_id[1] == 'K'
&& raw_game_id[2] == '-')
{
if (__len <= 8)
{
/* For 8 character serials in 'MK-xxxxx' format,
* we need to remove 'MK-' to match Redump database
* Sega GT being the only exception (MK-51053),
* we have to check if it's not that game first */
if (memcmp(raw_game_id, "MK-51053", STRLEN_CONST("MK-51053")) != 0)
{
/* Europe region serials need the MK- prefix and -50 postfix for database match. */
if (region_id == 'E')
{
strlcpy(s, raw_game_id, 9);
s[ 8] = '-';
s[ 9] = '5';
s[10] = '0';
s[11] = '\0';
}
else
{
strlcpy(s, raw_game_id + 3, 6);
}
}
else
strlcpy(s, raw_game_id, 9);
}
else
{
strlcpy(lgame_id, raw_game_id, 9);
strlcpy(rgame_id, &raw_game_id[__len - 2], sizeof(rgame_id));
_len = strlcpy(s, lgame_id, len);
s[ _len] = '-';
s[++_len] = '\0';
strlcpy(s + _len, rgame_id, len - _len);
}
cue_append_multi_disc_suffix(s, len, filename);
}
else
strlcpy(s, raw_game_id, len);
return 1;
}
size_t detect_wii_game(intfstream_t *fd, char *s, size_t len,
const char *filename)
{
char raw_game_id[15];
/* Load raw serial or quit */
if (intfstream_seek(fd, 0x0000, SEEK_SET) < 0)
return 0;
if (intfstream_read(fd, raw_game_id, 6) <= 0)
return 0;
if (memcmp(raw_game_id, "WBFS", STRLEN_CONST("WBFS")) == 0)
{
if (intfstream_seek(fd, 0x0200, SEEK_SET) < 0)
return 0;
if (intfstream_read(fd, raw_game_id, 6) <= 0)
return 0;
}
if ( memcmp(raw_game_id, "RVZ", STRLEN_CONST("RVZ")) == 0
|| memcmp(raw_game_id, "WIA", STRLEN_CONST("WIA")) == 0
)
{
if (intfstream_seek(fd, 0x0058, SEEK_SET) < 0)
return 0;
if (intfstream_read(fd, raw_game_id, 6) <= 0)
return 0;
}
raw_game_id[6] = '\0';
/** Scrub files with bad data and log **/
if ( raw_game_id[0] == '\0'
|| raw_game_id[0] == ' ')
{
#ifdef DEBUG
RARCH_LOG("[Scanner] Scrubbing: \"%s\".\n", filename);
#endif
return 0;
}
/* The suffix was appended to 's' and then immediately destroyed by
* the strlcpy() below, so Wii multi-disc titles never received one.
* Every other detector writes the serial first and appends after. */
{
size_t _len = strlcpy(s, raw_game_id, len);
cue_append_multi_disc_suffix(s, len, filename);
return _len;
}
}
int detect_system(intfstream_t *fd, const char **system_name,
const char * filename)
{
int i;
char magic[50];
#ifdef DEBUG
RARCH_LOG("[Scanner] %s\n", msg_hash_to_str(MSG_COMPARING_WITH_KNOWN_MAGIC_NUMBERS));
#endif
for (i = 0; MAGIC_NUMBERS[i].system_name != NULL; i++)
{
if (intfstream_seek(fd, MAGIC_NUMBERS[i].offset, SEEK_SET) >= 0)
{
size_t _len = strlen(MAGIC_NUMBERS[i].magic);
if (intfstream_read(fd, magic, _len) > 0)
{
magic[_len] = '\0';
if (memcmp(MAGIC_NUMBERS[i].magic, magic, _len) == 0)
{
*system_name = MAGIC_NUMBERS[i].system_name;
#ifdef DEBUG
RARCH_LOG("[Scanner] Name: %s\n", filename);
RARCH_LOG("[Scanner] System: %s\n", MAGIC_NUMBERS[i].system_name);
#endif
return 1;
}
}
}
}
#ifdef DEBUG
RARCH_LOG("[Scanner] Name: %s\n", filename);
RARCH_LOG("[Scanner] System: Unknown\n");
#endif
return 0;
}
/* Open @path once and report its size through the same handle, so the
* handle can go on to serve the serial/CRC probe instead of being
* closed and the file re-opened. Returns NULL with *file_size = -1
* when the file cannot be opened, matching the old
* open-for-size-and-close helper. */
static intfstream_t *cue_track_stream_open(const char *path,
int64_t *file_size)
{
intfstream_t *fd = intfstream_open_file(path,
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (!fd)
{
*file_size = -1;
return NULL;
}
*file_size = intfstream_get_size(fd);
return fd;
}
static void cue_track_stream_close(intfstream_t *fd)
{
if (!fd)
return;
intfstream_close(fd);
free(fd);
}
/* The candidate data track just became last_file; move last_file's
* stream (if one is held) into the candidate slot so the caller's
* serial/CRC probe can run on it without a fresh open.
*
* Three states are possible at an adoption point:
* - *cur_fd holds last_file's stream: hand it over.
* - *cur_fd is NULL and *cur_transferred is set: an earlier
* adoption already moved this same file's stream into *cand_fd;
* nothing to do.
* - *cur_fd is NULL and *cur_transferred is clear: last_file never
* opened. Whatever *cand_fd holds belongs to some earlier file,
* so drop it rather than pair the wrong stream with the path. */
static void cue_cand_adopt_stream(intfstream_t **cand_fd,
int64_t *cand_fsize, intfstream_t **cur_fd, int64_t cur_fsize,
bool *cur_transferred)
{
if (*cur_fd)
{
cue_track_stream_close(*cand_fd);
*cand_fd = *cur_fd;
*cand_fsize = cur_fsize;
*cur_fd = NULL;
*cur_transferred = true;
}
else if (!*cur_transferred)
{
cue_track_stream_close(*cand_fd);
*cand_fd = NULL;
*cand_fsize = -1;
}
}
static bool update_cand(int64_t *cand_index, int64_t *last_index,
uint64_t *largest, char *last_file, uint64_t *offset,
size_t *size, char *s, size_t len)
{
if (*cand_index != -1)
{
/* A well-formed sheet lists indices in ascending order, so the
* track span is last_index - cand_index. A malformed or
* truncated sheet - e.g. one missing its final newline, which
* makes the tokenizer drop the last INDEX token - can leave
* last_index behind cand_index. Signed subtraction then yields
* a negative span that would wrap to a huge size_t and drive a
* multi-exabyte staging malloc downstream, so treat any track
* whose index runs backwards as having no span. */
if ( *last_index >= *cand_index
&& (uint64_t)(*last_index - *cand_index) > *largest)
{
size_t _len;
*largest = *last_index - *cand_index;
_len = strlcpy(s, last_file, len);
*offset = *cand_index;
*size = (size_t)*largest;
*cand_index = -1;
return _len;
}
*cand_index = -1;
}
return 0;
}
/* When @track_fd is non-NULL, a successful return additionally hands
* back an open stream on the winning data track (or NULL if that file
* could not be opened during the parse) together with its size, so
* the caller's serial/CRC probe does not pay a second open of a file
* this parse already opened to learn its size. */
static int cue_find_track_internal(const char *cue_path, bool first,
uint64_t *offset, size_t *size, char *s, size_t len,
intfstream_t **track_fd, int64_t *track_fsize)
{
int rv;
intfstream_info_t info;
cue_tok_stream_t tok;
char tmp_token[MAX_TOKEN_LEN];
char last_file[PATH_MAX_LENGTH];
char cue_dir[DIR_MAX_LENGTH];
intfstream_t *fd = NULL;
intfstream_t *cur_fd = NULL; /* stream of last_file */
intfstream_t *cand_fd = NULL; /* stream of current candidate */
int64_t cand_fsize = -1;
bool cur_transferred = false;
int64_t last_index = -1;
int64_t cand_index = -1;
int32_t cand_track = -1;
int32_t track = 0;
uint64_t largest = 0;
int64_t volatile file_size = -1;
bool is_data = false;
cue_dir[0] = last_file[0] = '\0';
fill_pathname_basedir(cue_dir, cue_path, sizeof(cue_dir));
info.type = INTFSTREAM_FILE;
if (!(fd = (intfstream_t*)intfstream_init(&info)))
goto error;
if (!intfstream_open(fd, cue_path,
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE))
{
#ifdef DEBUG
RARCH_LOG("[Scanner] Could not open CUE file \"%s\".\n", cue_path);
#endif
goto error;
}
#ifdef DEBUG
RARCH_LOG("[Scanner] Parsing CUE file \"%s\"...\n", cue_path);
#endif
cue_tok_init(&tok, fd);
tmp_token[0] = '\0';
rv = -1;
while (task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)) > 0)
{
if (string_is_equal_noncase(tmp_token, "FILE"))
{
/* Set last index to last EOF */
if (file_size != -1)
last_index = file_size;
/* We're changing files since the candidate, update it */
if (update_cand(&cand_index, &last_index,
&largest, last_file, offset,
size, s, len))
{
cue_cand_adopt_stream(&cand_fd, &cand_fsize,
&cur_fd, file_size, &cur_transferred);
rv = 0;
if (first)
goto clean;
}
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token));
fill_pathname_join_special(last_file, cue_dir,
tmp_token, sizeof(last_file));
/* One open serves the size here, the CD-i magic probe below,
* and - if this file wins - the caller's serial/CRC pass. */
cue_track_stream_close(cur_fd);
{
int64_t sz = -1;
cur_fd = cue_track_stream_open(last_file, &sz);
file_size = sz;
}
cur_transferred = false;
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token));
}
else if (string_is_equal_noncase(tmp_token, "TRACK"))
{
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token));
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token));
is_data = !string_is_equal_noncase(tmp_token, "AUDIO");
++track;
/* Special case: CD-i stores data in AUDIO-labeled tracks */
/* Check if track 1 is AUDIO but contains CD-i magic bytes.
* The FILE line above already opened this track; probe the
* magic through whichever slot currently holds that stream
* instead of opening the file again. Later consumers seek
* before reading, so disturbing the position is fine. */
if (!is_data && track == 1 && last_file[0] != '\0')
{
uint8_t magic_buf[12];
const uint8_t cdi_magic[] = {0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00};
intfstream_t *probe = cur_fd
? cur_fd : (cur_transferred ? cand_fd : NULL);
if ( probe
&& intfstream_seek(probe, 0, SEEK_SET) >= 0
&& intfstream_read(probe, magic_buf,
sizeof(magic_buf)) == sizeof(magic_buf)
&& memcmp(magic_buf, cdi_magic, sizeof(cdi_magic)) == 0)
{
is_data = true; /* CD-i AUDIO track contains data */
#ifdef DEBUG
RARCH_LOG("[Scanner] Detected CD-i data in AUDIO track 1\n");
#endif
}
}
}
else if (string_is_equal_noncase(tmp_token, "INDEX"))
{
int _m, _s, _f;
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token));
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token));
{
const char *ptr = tmp_token;
char *end = NULL;
_m = (int)strtol(ptr, &end, 10);
if (!end || *end != ':')
{
#ifdef DEBUG
RARCH_LOG("[Scanner] Error parsing time stamp \"%s\".\n", tmp_token);
#endif
goto error;
}
ptr = end + 1;
_s = (int)strtol(ptr, &end, 10);
if (!end || *end != ':')
{
#ifdef DEBUG
RARCH_LOG("[Scanner] Error parsing time stamp \"%s\".\n", tmp_token);
#endif
goto error;
}
ptr = end + 1;
_f = (int)strtol(ptr, &end, 10);
if (!end || (*end != '\0' && *end != ' ' && *end != '\t'))
{
#ifdef DEBUG
RARCH_LOG("[Scanner] Error parsing time stamp \"%s\".\n", tmp_token);
#endif
goto error;
}
}
last_index = (size_t)(((_m * 60 + _s) * 75) + _f) * 2352;
/* If we've changed tracks since the candidate, update it */
if ( (cand_track != -1)
&& (track != cand_track)
&& update_cand(&cand_index, &last_index, &largest,
last_file, offset,
size, s, len))
{
cue_cand_adopt_stream(&cand_fd, &cand_fsize,
&cur_fd, file_size, &cur_transferred);
rv = 0;
if (first)
goto clean;
}
if (!is_data)
continue;
if (cand_index == -1)
{
cand_index = last_index;
cand_track = track;
}
}
}
if (file_size != -1)
last_index = file_size;
if (update_cand(&cand_index, &last_index,
&largest, last_file, offset,
size, s, len))
{
cue_cand_adopt_stream(&cand_fd, &cand_fsize,
&cur_fd, file_size, &cur_transferred);
rv = 0;
}
clean:
intfstream_close(fd);
free(fd);
cue_track_stream_close(cur_fd);
if (rv == 0 && track_fd)
{
*track_fd = cand_fd;
*track_fsize = cand_fsize;
}
else
cue_track_stream_close(cand_fd);
return rv;
error:
if (fd)
{
intfstream_close(fd);
free(fd);
}
cue_track_stream_close(cur_fd);
cue_track_stream_close(cand_fd);
return -1;
}
int cue_find_track(const char *cue_path, bool first,
uint64_t *offset, size_t *size, char *s, size_t len)
{
return cue_find_track_internal(cue_path, first, offset, size,
s, len, NULL, NULL);
}
bool cue_next_file(intfstream_t *fd,
const char *cue_path, char *s, uint64_t len)
{
cue_tok_stream_t tok;
char tmp_token[MAX_TOKEN_LEN];
tmp_token[0] = '\0';
cue_tok_init(&tok, fd);
while (task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)) > 0)
{
if (string_is_equal_noncase(tmp_token, "FILE"))
{
char cue_dir[DIR_MAX_LENGTH];
fill_pathname_basedir(cue_dir, cue_path, sizeof(cue_dir));
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token));
fill_pathname_join_special(s, cue_dir, tmp_token, (size_t)len);
/* The caller re-enters with this stream; put it where the
* tokenizer logically stopped, not where the window read to. */
cue_tok_detach(&tok);
return 1;
}
}
cue_tok_detach(&tok);
return 0;
}
/* As cue_find_track_internal(): a non-NULL @track_fd receives an open
* stream on the winning track plus its size on success. */
static int gdi_find_track_internal(const char *gdi_path, bool first,
char *s, size_t len, intfstream_t **track_fd, int64_t *track_fsize)
{
intfstream_info_t info;
cue_tok_stream_t tok;
char tmp_token[MAX_TOKEN_LEN];
intfstream_t *fd = NULL;
intfstream_t *cand_fd = NULL;
int64_t cand_fsize = -1;
uint64_t largest = 0;
int rv = -1;
int size = -1;
int mode = -1;
int64_t file_size = -1;
info.type = INTFSTREAM_FILE;
if (!(fd = (intfstream_t*)intfstream_init(&info)))
goto error;
if (!intfstream_open(fd, gdi_path,
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE))
{
#ifdef DEBUG
RARCH_LOG("[Scanner] Could not open GDI file \"%s\".\n", gdi_path);
#endif
goto error;
}
#ifdef DEBUG
RARCH_LOG("[Scanner] Parsing GDI file \"%s\"...\n", gdi_path);
#endif
cue_tok_init(&tok, fd);
tmp_token[0] = '\0';
/* Skip track count */
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token));
/* Track number */
while (task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)) > 0)
{
/* Offset */
if (task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)) <= 0)
goto error;
/* Mode */
if (task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)) <= 0)
goto error;
mode = atoi(tmp_token);
/* Sector size */
if (task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)) <= 0)
goto error;
size = atoi(tmp_token);
/* File name */
if (task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)) <= 0)
goto error;
/* Check for data track */
if (!(mode == 0 && size == 2352))
{
char last_file[PATH_MAX_LENGTH];
char gdi_dir[DIR_MAX_LENGTH];
intfstream_t *this_fd = NULL;
fill_pathname_basedir(gdi_dir, gdi_path, sizeof(gdi_dir));
fill_pathname_join_special(last_file,
gdi_dir, tmp_token, sizeof(last_file));
/* One open learns the size and, if this track wins, goes on
* to serve the caller's serial/CRC probe. */
if (!(this_fd = cue_track_stream_open(last_file, &file_size)))
goto error;
if ((uint64_t)file_size > largest)
{
strlcpy(s, last_file, len);
rv = 0;
largest = file_size;
cue_track_stream_close(cand_fd);
cand_fd = this_fd;
cand_fsize = file_size;
if (first)
goto clean;
}
else
cue_track_stream_close(this_fd);
}
/* Disc offset (not used?) */
if (task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)) <= 0)
goto error;
}
clean:
intfstream_close(fd);
free(fd);
if (rv == 0 && track_fd)
{
*track_fd = cand_fd;
*track_fsize = cand_fsize;
}
else
cue_track_stream_close(cand_fd);
return rv;
error:
if (fd)
{
intfstream_close(fd);
free(fd);
}
cue_track_stream_close(cand_fd);
return -1;
}
int gdi_find_track(const char *gdi_path, bool first, char *s, size_t len)
{
return gdi_find_track_internal(gdi_path, first, s, len, NULL, NULL);
}
size_t gdi_next_file(intfstream_t *fd, const char *gdi_path,
char *s, size_t len)
{
cue_tok_stream_t tok;
char tmp_token[MAX_TOKEN_LEN];
tmp_token[0] = '\0';
cue_tok_init(&tok, fd);
/* Skip initial track count */
if (intfstream_tell(fd) == 0)
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token));
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)); /* Track number */
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)); /* Offset */
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)); /* Mode */
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)); /* Sector size */
/* File name */
if (task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token)) > 0)
{
size_t _len;
char gdi_dir[DIR_MAX_LENGTH];
fill_pathname_basedir(gdi_dir, gdi_path, sizeof(gdi_dir));
_len = fill_pathname_join_special(s, gdi_dir, tmp_token, len);
/* Disc offset */
task_database_cue_get_token(&tok, tmp_token, sizeof(tmp_token));
/* Caller re-enters with this stream and keys the header skip
* off intfstream_tell(); rewind the window's lookahead. */
cue_tok_detach(&tok);
return _len;
}
cue_tok_detach(&tok);
return 0;
}
int intfstream_get_serial(intfstream_t *fd, char *s, size_t len,
const char *filename)
{
const char *system_name = NULL;
if (detect_system(fd, &system_name, filename) >= 1)
{
size_t system_len = strlen(system_name);
if (string_starts_with_size(system_name, "Sony", STRLEN_CONST("Sony")))
{
if ( STRLEN_CONST("Sony - PlayStation Portable") == system_len
&& memcmp(system_name, "Sony - PlayStation Portable", system_len) == 0)
{
if (detect_psp_game(fd, s, len, filename) != 0)
return 1;
}
else if ( STRLEN_CONST("Sony - PlayStation") == system_len
&& memcmp(system_name, "Sony - PlayStation", system_len) == 0)
{
if (detect_ps1_game(fd, s, len, filename) != 0)
return 1;
}
else if ( STRLEN_CONST("Sony - PlayStation 2") == system_len
&& memcmp(system_name, "Sony - PlayStation 2", system_len) == 0)
{
if (detect_ps2_game(fd, s, len, filename) != 0)
return 1;
}
}
else if (string_starts_with_size(system_name, "Nintendo", STRLEN_CONST("Nintendo")))
{
if ( STRLEN_CONST("Nintendo - GameCube") == system_len
&& memcmp(system_name, "Nintendo - GameCube", system_len) == 0)
{
if (detect_gc_game(fd, s, len, filename) != 0)
return 1;
}
else if ( STRLEN_CONST("Nintendo - Wii") == system_len
&& memcmp(system_name, "Nintendo - Wii", system_len) == 0)
{
if (detect_wii_game(fd, s, len, filename) != 0)
return 1;
}
}
else if (string_starts_with_size(system_name, "Sega", STRLEN_CONST("Sega")))
{
if ( STRLEN_CONST("Sega - Mega-CD - Sega CD") == system_len
&& memcmp(system_name, "Sega - Mega-CD - Sega CD", system_len) == 0)
{
if (detect_scd_game(fd, s, len, filename) != 0)
return 1;
}
else if ( STRLEN_CONST("Sega - Saturn") == system_len
&& memcmp(system_name, "Sega - Saturn", system_len) == 0)
{
if (detect_sat_game(fd, s, len, filename) != 0)
return 1;
}
else if ( STRLEN_CONST("Sega - Dreamcast") == system_len
&& memcmp(system_name, "Sega - Dreamcast", system_len) == 0)
{
if (detect_dc_game(fd, s, len, filename) != 0)
return 1;
}
}
/* Philips CD-i has no serial entry on disc,
* use default fallback to CRC */
}
return 0;
}
/* Serial probe over an already-open stream. @file_size may be passed
* in when the caller learned it at open time (the cue/GDI parse
* does); -1 means measure it here. The stream is left open for the
* caller and its position is unspecified on return. */
static bool intfstream_file_get_serial_fd(intfstream_t *fd,
int64_t file_size, uint64_t offset, int64_t size,
char *s, size_t len, const char *name, uint64_t *fsize)
{
int rv;
uint8_t *data = NULL;
if (file_size < 0)
{
if (intfstream_seek(fd, 0, SEEK_END) == -1)
return false;
file_size = intfstream_tell(fd);
}
if (fsize)
*fsize = (uint64_t)file_size;
if (file_size < 0)
return false;
if (intfstream_seek(fd, 0, SEEK_SET) == -1)
return false;
if (offset != 0 || size < file_size)
{
/* A bounded span is a data track inside a larger image. Every
* serial/system detector resolves within the first 600000
* bytes of the track (DISC_DATA_SIZE_PS2 is the largest probe,
* 0x9320 the deepest magic-number offset), so stage at most
* that much. Tracks shorter than the cap stage what exists; a
* detector seeking or reading past the window's end sees a
* short read, the same as on a short track. */
intfstream_t *mem = NULL;
if (size > SERIAL_PROBE_SPAN)
size = SERIAL_PROBE_SPAN;
if (intfstream_seek(fd, (int64_t)offset, SEEK_SET) == -1)
return false;
data = (uint8_t*)malloc(size);
/* NULL-check: intfstream_read below would NULL-deref on
* OOM via filestream_read's fread. */
if (!data)
return false;
if (intfstream_read(fd, data, size) != (int64_t) size)
{
free(data);
return false;
}
if (!(mem = intfstream_open_memory(data, RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE,
size)))
{
free(data);
return false;
}
rv = intfstream_get_serial(mem, s, len, name);
intfstream_close(mem);
free(mem);
free(data);
return rv != 0;
}
rv = intfstream_get_serial(fd, s, len, name);
return rv != 0;
}
bool intfstream_file_get_serial(const char *name,
uint64_t offset, int64_t size, char *s, size_t len, uint64_t *fsize)
{
bool rv;
intfstream_t *fd = intfstream_open_file(name,
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (!fd)
return false;
rv = intfstream_file_get_serial_fd(fd, -1, offset, size,
s, len, name, fsize);
intfstream_close(fd);
free(fd);
return rv;
}
int task_database_cue_get_serial(const char *name, char *s, size_t len,
uint64_t *filesize)
{
char track_path[PATH_MAX_LENGTH];
uint64_t offset = 0;
size_t _len = 0;
intfstream_t *track = NULL;
int64_t track_fsize = -1;
track_path[0] = '\0';
if (cue_find_track_internal(name, true, &offset, &_len,
track_path, sizeof(track_path), &track, &track_fsize) < 0)
{
#ifdef DEBUG
RARCH_LOG("[Scanner] %s\n",
msg_hash_to_str(MSG_COULD_NOT_FIND_VALID_DATA_TRACK));
#endif
return 0;
}
if (track)
{
int rv = intfstream_file_get_serial_fd(track, track_fsize,
offset, _len, s, len, track_path, filesize);
cue_track_stream_close(track);
return rv;
}
/* The parse could not open the winning track, so no stream was
* retained; the by-path probe fails the same way it always has. */
return intfstream_file_get_serial(track_path, offset, _len, s, len,
filesize);
}
int task_database_gdi_get_serial(const char *name, char *s, size_t len,
uint64_t *filesize)
{
char track_path[PATH_MAX_LENGTH];
intfstream_t *track = NULL;
int64_t track_fsize = -1;
track_path[0] = '\0';
if (gdi_find_track_internal(name, true,
track_path, sizeof(track_path), &track, &track_fsize) < 0)
{
#ifdef DEBUG
RARCH_LOG("[Scanner] %s\n",
msg_hash_to_str(MSG_COULD_NOT_FIND_VALID_DATA_TRACK));
#endif
return 0;
}
if (track)
{
int rv = intfstream_file_get_serial_fd(track, track_fsize,
0, INT64_MAX, s, len, track_path, filesize);
cue_track_stream_close(track);
return rv;
}
return intfstream_file_get_serial(track_path, 0, INT64_MAX, s, len,
filesize);
}
int task_database_pbp_get_serial(const char *name, char *s, size_t len,
uint64_t *filesize)
{
int rv;
int64_t file_size = -1;
intfstream_t *fd = intfstream_open_file(name,
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (!fd)
return 0;
if (intfstream_seek(fd, 0, SEEK_END) != -1)
{
file_size = intfstream_tell(fd);
if (filesize)
*filesize = (uint64_t)file_size;
}
if (intfstream_seek(fd, 0, SEEK_SET) == -1)
{
intfstream_close(fd);
free(fd);
return 0;
}
rv = detect_pbp_game(fd, s, len, name);
intfstream_close(fd);
free(fd);
return rv;
}
int task_database_3ds_get_serial(const char *name, char *s, size_t len,
uint64_t *filesize)
{
int rv;
intfstream_t *fd = intfstream_open_file(name,
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (!fd)
return 0;
if (filesize && intfstream_seek(fd, 0, SEEK_END) != -1)
*filesize = (uint64_t)intfstream_tell(fd);
rv = detect_3ds_game(fd, s, len);
intfstream_close(fd);
free(fd);
return rv;
}
/* Helper function to detect if a CHD file is a CD-i disc
* CD-i discs store data in AUDIO-labeled tracks, so we need
* to explicitly open track 1 for scanning */
bool is_chd_file_cdi(const char *path)
{
uint8_t magic[12];
const uint8_t cdi_magic[] = {0x00, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0x00};
bool is_cdi = false;
/* Try to open track 1 explicitly */
intfstream_t *fd = intfstream_open_chd_track(path,
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE,
1); /* Explicit track number, not CHDSTREAM_TRACK_FIRST_DATA */
if (!fd)
return 0;
/* Read and check CD-i magic bytes at offset 0 */
if (intfstream_read(fd, magic, sizeof(magic)) == sizeof(magic))
is_cdi = (memcmp(magic, cdi_magic, sizeof(cdi_magic)) == 0);
intfstream_close(fd);
free(fd);
return is_cdi;
}
static size_t detect_xbox_game(intfstream_t *fd, uint64_t pos, bool disc,
char *s, size_t len, uint8_t *regions);
int task_database_chd_get_serial(const char *name, char *serial,
size_t len, uint64_t *filesize, uint8_t *regions)
{
int result;
/* CD-i discs store data in AUDIO-labeled tracks, so we must
* explicitly open track 1 instead of using CHDSTREAM_TRACK_FIRST_DATA */
int32_t track = is_chd_file_cdi(name) ? 1 : CHDSTREAM_TRACK_FIRST_DATA;
intfstream_t *fd = intfstream_open_chd_track(
name,
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE,
track);
if (!fd)
return 0;
/* TODO/FIXME: get the full CHD size instead */
*filesize = intfstream_get_size(fd);
result = intfstream_get_serial(fd, serial, len, name);
/* An Xbox DVD is looked for only once nothing else has been found,
* so other discs read nothing more for it */
if (!result)
result = detect_xbox_game(fd, ~(uint64_t)0, true, serial, len,
regions) != 0;
intfstream_close(fd);
free(fd);
return result;
}
/* The disc header the GameCube and Wii detectors read */
#define SCAN_CISO_PROBE_SIZE 0x440
/* An image read for a reader that does no I/O of its own: each range
* goes straight to where the reader asks, and the file is only sought
* when a range does not start where the last one ended. */
typedef struct
{
intfstream_t *fd;
uint64_t pos;
} task_database_src_t;
static size_t task_database_pull(task_database_src_t *src,
uint64_t offset, uint8_t *dst, size_t length)
{
int64_t got;
if ( src->pos != offset
&& intfstream_seek(src->fd, (int64_t)offset, SEEK_SET) < 0)
return 0;
src->pos = offset;
if ((got = intfstream_read(src->fd, dst, length)) <= 0)
{
src->pos = ~(uint64_t)0;
return 0;
}
src->pos += (uint64_t)got;
return (size_t)got;
}
int task_database_ciso_get_serial(const char *name, char *s, size_t len,
uint64_t *filesize)
{
task_database_src_t src;
ciso_request_t req;
int rv = 0;
int err;
uint8_t *head = NULL;
ciso_t *c = NULL;
intfstream_t *mem = NULL;
/* The descriptor path: every read lands where the reader asked,
* not through a stdio buffer. */
intfstream_t *fd = intfstream_open_file(name,
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK);
if (!fd)
return 0;
*filesize = (uint64_t)intfstream_get_size(fd);
src.fd = fd;
src.pos = 0;
if ( !(c = ciso_new())
|| !(head = (uint8_t*)malloc(SCAN_CISO_PROBE_SIZE)))
goto end;
while ((err = ciso_open_step(c, &req)) == CISO_PENDING)
ciso_feed(c, task_database_pull(&src, req.offset, req.dst, req.length));
if ( err != CISO_OK
|| ciso_read_begin(c, 0, head, SCAN_CISO_PROBE_SIZE) != CISO_OK)
goto end;
while ((err = ciso_read_step(c, &req)) == CISO_PENDING)
ciso_feed(c, task_database_pull(&src, req.offset, req.dst, req.length));
if (err != CISO_OK)
goto end;
/* the serial detectors read the disc header where it was put */
if ((mem = intfstream_open_memory(head, RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE, ciso_read_progress(c))))
{
rv = intfstream_get_serial(mem, s, len, name);
intfstream_close(mem);
free(mem);
}
end:
ciso_free(c);
free(head);
intfstream_close(fd);
free(fd);
return rv;
}
/* The Redump serial of an Xbox or Xbox 360 title: the publisher's two
* letters and the title's number, "MS-004" for 0x4D530004; an Xbox
* 360 disc of a set adds its place in it from 0, "FO-2046-1" */
static size_t xbox_title_serial(const xdvdfs_title_t *t, char *s,
size_t len)
{
char serial[24];
char digits[8];
unsigned n = t->id & 0xFFFF;
size_t nd = 0;
size_t _len = 0;
int i;
for (i = 0; i < 2; i++)
{
char c = (char)(t->id >> (24 - 8 * i));
if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')))
return 0;
serial[_len++] = c;
}
serial[_len++] = '-';
do
{
digits[nd++] = (char)('0' + n % 10);
n /= 10;
} while (n);
while (nd < 3)
digits[nd++] = '0';
while (nd)
serial[_len++] = digits[--nd];
if ( t->platform == XDVDFS_XBOX360
&& t->discs > 1 && t->disc >= 1 && t->disc <= t->discs)
{
n = t->disc - 1u;
serial[_len++] = '-';
do
{
digits[nd++] = (char)('0' + n % 10);
n /= 10;
} while (n);
while (nd)
serial[_len++] = digits[--nd];
}
serial[_len] = '\0';
return strlcpy(s, serial, len);
}
/* An Xbox or Xbox 360 serial from the title of the executable @fd
* holds, or with @disc of the one the disc image boots, and the
* regions it is for. @pos is where the stream stands. */
static size_t detect_xbox_game(intfstream_t *fd, uint64_t pos, bool disc,
char *s, size_t len, uint8_t *regions)
{
task_database_src_t src;
xdvdfs_request_t req;
xdvdfs_extent_t exe;
xdvdfs_title_t title;
size_t _len = 0;
int err = XDVDFS_OK;
int64_t size = intfstream_get_size(fd);
xdvdfs_t *x;
if (size <= 0 || !(x = xdvdfs_new((uint64_t)size)))
return 0;
src.fd = fd;
src.pos = pos;
exe.offset = 0;
exe.size = size > 0xFFFFFFFF ? 0xFFFFFFFFu : (uint32_t)size;
if (disc)
while ((err = xdvdfs_open_step(x, &req)) == XDVDFS_PENDING)
xdvdfs_feed(x, task_database_pull(&src, req.offset, req.dst,
req.length));
if ( err == XDVDFS_OK
&& xdvdfs_title_begin(x, disc ? NULL : &exe) == XDVDFS_OK)
{
while ((err = xdvdfs_title_step(x, &req, &title)) == XDVDFS_PENDING)
xdvdfs_feed(x, task_database_pull(&src, req.offset, req.dst,
req.length));
if (err == XDVDFS_OK && (_len = xbox_title_serial(&title, s, len)))
*regions = title.regions;
}
xdvdfs_free(x);
return _len;
}
int task_database_xbox_get_serial(const char *name, bool disc, char *s,
size_t len, uint64_t *filesize, uint8_t *regions)
{
size_t _len;
/* The descriptor path: the few ranges the probe needs land where
* it asks, with no stdio buffer filled around each one. */
intfstream_t *fd = intfstream_open_file(name,
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK);
if (!fd)
return 0;
*filesize = (uint64_t)intfstream_get_size(fd);
_len = detect_xbox_game(fd, 0, disc, s, len, regions);
intfstream_close(fd);
free(fd);
return _len != 0;
}
/* An Xbox 360 game in a ZArchive: the title of its default.xex, read
* through the archive's blocks */
typedef struct
{
task_database_src_t src;
zar_file_t file;
zar_t *z;
} task_database_zar_src_t;
static size_t task_database_zar_pull(task_database_zar_src_t *zs,
uint64_t offset, uint8_t *dst, size_t length)
{
zar_request_t req;
int err;
if (zar_read_begin(zs->z, &zs->file, offset, dst, length) != ZAR_OK)
return 0;
while ((err = zar_read_step(zs->z, &req)) == ZAR_PENDING)
zar_feed(zs->z, task_database_pull(&zs->src, req.offset, req.dst,
req.length));
/* what came before a failure is still read */
return zar_read_progress(zs->z);
}
int task_database_zar_get_serial(const char *name, char *s, size_t len,
uint64_t *filesize, uint8_t *regions)
{
task_database_zar_src_t zs;
zar_request_t req;
xdvdfs_request_t xreq;
xdvdfs_extent_t exe;
xdvdfs_title_t title;
xdvdfs_t *x = NULL;
zar_t *z = NULL;
size_t _len = 0;
int err;
int64_t size;
/* The descriptor path: each range lands where the readers ask */
intfstream_t *fd = intfstream_open_file(name,
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK);
if (!fd)
return 0;
size = intfstream_get_size(fd);
*filesize = (uint64_t)size;
zs.src.fd = fd;
zs.src.pos = 0;
if (size <= 0 || !(z = zar_new((uint64_t)size)))
goto end;
zs.z = z;
while ((err = zar_open_step(zs.z, &req)) == ZAR_PENDING)
zar_feed(zs.z, task_database_pull(&zs.src, req.offset, req.dst,
req.length));
if (err != ZAR_OK || zar_lookup_begin(zs.z, "default.xex") != ZAR_OK)
goto end;
while ((err = zar_lookup_step(zs.z, &req, &zs.file)) == ZAR_PENDING)
zar_feed(zs.z, task_database_pull(&zs.src, req.offset, req.dst,
req.length));
if (err != ZAR_OK || !(x = xdvdfs_new(zs.file.size)))
goto end;
exe.offset = 0;
exe.size = zs.file.size > 0xFFFFFFFFu
? 0xFFFFFFFFu : (uint32_t)zs.file.size;
if (xdvdfs_title_begin(x, &exe) != XDVDFS_OK)
goto end;
while ((err = xdvdfs_title_step(x, &xreq, &title)) == XDVDFS_PENDING)
xdvdfs_feed(x, task_database_zar_pull(&zs, xreq.offset, xreq.dst,
xreq.length));
if (err == XDVDFS_OK && (_len = xbox_title_serial(&title, s, len)))
*regions = title.regions;
end:
xdvdfs_free(x);
zar_free(z);
intfstream_close(fd);
free(fd);
return _len != 0;
}
/* A Wii U product code's four characters, from "WUP-?-" and them;
* @code gets them, terminated */
static bool wiiu_product_code(const uint8_t *p, size_t n, char *code)
{
size_t i;
if ( n < 10
|| p[0] != 'W' || p[1] != 'U' || p[2] != 'P' || p[3] != '-'
|| p[4] < 'A' || p[4] > 'Z' || p[5] != '-')
return false;
for (i = 0; i < 4; i++)
{
uint8_t c = p[6 + i];
if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')))
return false;
code[i] = (char)c;
}
code[4] = '\0';
return true;
}
/* The disc header a Wii U product code is read from */
#define WIIU_HEAD 10
int task_database_wiiu_get_serial(const char *name, char *s, size_t len,
uint64_t *filesize)
{
task_database_src_t src;
wux_request_t req;
uint8_t head[WIIU_HEAD];
char code[5];
size_t got;
int err;
int rv = 0;
int64_t size;
wux_t *w = NULL;
/* The descriptor path: the few octets the probe needs land where
* it asks */
intfstream_t *fd = intfstream_open_file(name,
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK);
if (!fd)
return 0;
size = intfstream_get_size(fd);
*filesize = (uint64_t)size;
src.fd = fd;
src.pos = 0;
if (size < WIIU_HEAD)
goto end;
for (got = 0; got < WIIU_HEAD; )
{
size_t n = task_database_pull(&src, got, head + got, WIIU_HEAD - got);
if (!n)
goto end;
got += n;
}
/* A WUX: the same octets of the disc it stands for */
if (wux_is_wux(head))
{
if (!(w = wux_new((uint64_t)size)))
goto end;
while ((err = wux_open_step(w, &req)) == WUX_PENDING)
wux_feed(w, task_database_pull(&src, req.offset, req.dst,
req.length));
if ( err != WUX_OK
|| wux_read_begin(w, 0, head, WIIU_HEAD) != WUX_OK)
goto end;
while ((err = wux_read_step(w, &req)) == WUX_PENDING)
wux_feed(w, task_database_pull(&src, req.offset, req.dst,
req.length));
if (err != WUX_OK || wux_read_progress(w) != WIIU_HEAD)
goto end;
}
/* a disc says "WUP-P-" */
if (head[4] == 'P' && wiiu_product_code(head, WIIU_HEAD, code))
rv = strlcpy(s, code, len) < len;
end:
wux_free(w);
intfstream_close(fd);
free(fd);
return rv;
}
/* The text of the first <@tag> element of @xml, which is terminated;
* NULL when there is none. *@n gets its length. */
static const char *wiiu_xml_value(const char *xml, const char *tag,
size_t *n)
{
size_t tlen = strlen(tag);
const char *p = xml;
while ((p = strchr(p, '<')))
{
const char *v;
p++;
if (strncmp(p, tag, tlen) || (p[tlen] != '>' && p[tlen] != ' '))
continue;
if (!(v = strchr(p + tlen, '>')))
return NULL;
v++;
for (*n = 0; v[*n] && v[*n] != '<'; (*n)++) { }
return v[*n] ? v : NULL;
}
return NULL;
}
/* meta.xml's head, which names the product and company first */
#define WIIU_META_HEAD 2048
int task_database_wua_get_serial(const char *name, char *s, size_t len,
uint64_t *filesize, char *maker)
{
task_database_zar_src_t zs;
zar_request_t req;
zar_dirent_t ent;
char path[64];
char code[5];
char *xml = NULL;
const char *v;
size_t n;
int err;
int rv = 0;
int64_t size;
zar_t *z = NULL;
/* The descriptor path: each range lands where the reader asks */
intfstream_t *fd = intfstream_open_file(name,
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK);
if (!fd)
return 0;
maker[0] = '\0';
path[0] = '\0';
size = intfstream_get_size(fd);
*filesize = (uint64_t)size;
zs.src.fd = fd;
zs.src.pos = 0;
if (size <= 0 || !(z = zar_new((uint64_t)size)))
goto end;
zs.z = z;
while ((err = zar_open_step(z, &req)) == ZAR_PENDING)
zar_feed(z, task_database_pull(&zs.src, req.offset, req.dst,
req.length));
if (err != ZAR_OK || zar_dir_begin(z, "") != ZAR_OK)
goto end;
/* A title's directory is its ID, 16 hex digits, then "_v" and its
* version. The first base title (00050000...) is the game; any
* other title stands in when there is none, as an update or the
* content of one names the same product. */
for (;;)
{
size_t i;
while ((err = zar_dir_step(z, &req, &ent)) == ZAR_PENDING)
zar_feed(z, task_database_pull(&zs.src, req.offset, req.dst,
req.length));
if (err != ZAR_OK)
break;
if ( !ent.is_dir || ent.len < 18 || ent.len > 40
|| ent.name[16] != '_' || (ent.name[17] | 0x20) != 'v')
continue;
for (i = 0; i < 16; i++)
{
char c = ent.name[i];
if (!( (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')
|| (c >= 'a' && c <= 'f')))
break;
}
if (i < 16)
continue;
if (!path[0] || !strncmp(ent.name, "00050000", 8))
{
memcpy(path, ent.name, ent.len);
path[ent.len] = '\0';
if (!strncmp(ent.name, "00050000", 8))
break;
}
}
if (!path[0])
goto end;
strlcat(path, "/meta/meta.xml", sizeof(path));
if (zar_lookup_begin(z, path) != ZAR_OK)
goto end;
while ((err = zar_lookup_step(z, &req, &zs.file)) == ZAR_PENDING)
zar_feed(z, task_database_pull(&zs.src, req.offset, req.dst,
req.length));
if (err != ZAR_OK || !(xml = (char*)malloc(WIIU_META_HEAD + 1)))
goto end;
n = task_database_zar_pull(&zs, 0, (uint8_t*)xml, WIIU_META_HEAD);
xml[n] = '\0';
if ( !(v = wiiu_xml_value(xml, "product_code", &n))
|| !wiiu_product_code((const uint8_t*)v, n, code))
goto end;
/* the maker: the company code's last two characters */
if ( (v = wiiu_xml_value(xml, "company_code", &n)) && n >= 2
&& ISALNUM((unsigned char)v[n - 2])
&& ISALNUM((unsigned char)v[n - 1]))
{
maker[0] = (char)TOUPPER((unsigned char)v[n - 2]);
maker[1] = (char)TOUPPER((unsigned char)v[n - 1]);
maker[2] = '\0';
}
rv = strlcpy(s, code, len) < len;
end:
free(xml);
zar_free(z);
intfstream_close(fd);
free(fd);
return rv;
}
/* Opens what the scanner hashes for @name: the file itself, the data
* track a CUE or GDI sheet names, or a CHD's data track. The stream
* comes back at the start of the span, *span is its length or -1 to
* read to the end, and *size is the size the lookups compare: the
* track file's for a sheet, the track's for a CHD. */
intfstream_t *task_database_crc_open(const char *name,
enum task_database_crc_kind kind, int64_t *span, uint64_t *size)
{
char track_path[PATH_MAX_LENGTH];
const char *path = name;
intfstream_t *fd = NULL;
int64_t file_size = -1;
int64_t len = -1;
uint64_t offset = 0;
if (kind == TASK_DATABASE_CRC_CHD)
{
/* CD-i discs store data in AUDIO-labeled tracks, so track 1 is
* opened explicitly instead of CHDSTREAM_TRACK_PRIMARY. */
if (!(fd = intfstream_open_chd_track(name,
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE,
is_chd_file_cdi(name) ? 1 : CHDSTREAM_TRACK_PRIMARY)))
return NULL;
*size = intfstream_get_size(fd);
*span = -1;
intfstream_rewind(fd);
return fd;
}
track_path[0] = '\0';
if (kind == TASK_DATABASE_CRC_CUE)
{
size_t _len = 0;
if (cue_find_track_internal(name, false, &offset, &_len,
track_path, sizeof(track_path), &fd, &file_size) < 0)
{
#ifdef DEBUG
RARCH_LOG("[Scanner] %s\n",
msg_hash_to_str(MSG_COULD_NOT_FIND_VALID_DATA_TRACK));
#endif
return NULL;
}
len = (int64_t)_len;
path = track_path;
}
else if (kind == TASK_DATABASE_CRC_GDI)
{
if (gdi_find_track_internal(name, false,
track_path, sizeof(track_path), &fd, &file_size) < 0)
{
#ifdef DEBUG
RARCH_LOG("[Scanner] %s\n",
msg_hash_to_str(MSG_COULD_NOT_FIND_VALID_DATA_TRACK));
#endif
return NULL;
}
path = track_path;
}
/* FREQUENT_ACCESS: the VFS maps the file where it can, and
* intfstream_crc_step() then hashes the mapping in place. */
if (!fd && !(fd = intfstream_open_file(path,
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS)))
return NULL;
if (file_size < 0)
{
if (intfstream_seek(fd, 0, SEEK_END) == -1)
goto error;
file_size = intfstream_tell(fd);
}
if (file_size < 0)
goto error;
*size = (uint64_t)file_size;
/* Only a CUE data track can be part of its file. */
if (len < 0 || (offset == 0 && len >= file_size))
{
offset = 0;
len = -1;
}
if (intfstream_seek(fd, (int64_t)offset, SEEK_SET) == -1)
goto error;
*span = len;
return fd;
error:
intfstream_close(fd);
free(fd);
return NULL;
}