mirror of
https://github.com/libretro/RetroArch
synced 2025-01-31 06:32:48 +00:00
456 lines
12 KiB
C
456 lines
12 KiB
C
/* Copyright (C) 2010-2020 The RetroArch team
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* The following license statement only applies to this file (logiqx_dat.c).
|
|
* ---------------------------------------------------------------------------------------
|
|
*
|
|
* Permission is hereby granted, free of charge,
|
|
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
|
* to deal in the Software without restriction, including without limitation the rights to
|
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
|
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
#include <file/file_path.h>
|
|
#include <string/stdstring.h>
|
|
#include <formats/rxml.h>
|
|
|
|
#include <formats/logiqx_dat.h>
|
|
|
|
/* Holds all internal DAT file data */
|
|
struct logiqx_dat
|
|
{
|
|
rxml_document_t *data;
|
|
rxml_node_t *current_node;
|
|
};
|
|
|
|
/* List of HTML formatting codes that must
|
|
* be replaced when parsing XML data */
|
|
const char *logiqx_dat_html_code_list[][2] = {
|
|
{"&", "&"},
|
|
{"'", "'"},
|
|
{">", ">"},
|
|
{"<", "<"},
|
|
{""", "\""}
|
|
};
|
|
|
|
#define LOGIQX_DAT_HTML_CODE_LIST_SIZE 5
|
|
|
|
/* Validation */
|
|
|
|
/* Performs rudimentary validation of the specified
|
|
* Logiqx XML DAT file path (not rigorous - just
|
|
* enough to prevent obvious errors).
|
|
* Also provides access to file size (DAT files can
|
|
* be very large, so it is useful to have this information
|
|
* on hand - i.e. so we can check that the system has
|
|
* enough free memory to load the file). */
|
|
bool logiqx_dat_path_is_valid(const char *path, uint64_t *file_size)
|
|
{
|
|
const char *file_ext = NULL;
|
|
int32_t file_size_int;
|
|
|
|
if (string_is_empty(path))
|
|
return false;
|
|
|
|
/* Check file extension */
|
|
file_ext = path_get_extension(path);
|
|
|
|
if (string_is_empty(file_ext))
|
|
return false;
|
|
|
|
if (!string_is_equal_noncase(file_ext, "dat") &&
|
|
!string_is_equal_noncase(file_ext, "xml"))
|
|
return false;
|
|
|
|
/* Ensure file exists */
|
|
if (!path_is_valid(path))
|
|
return false;
|
|
|
|
/* Get file size */
|
|
file_size_int = path_get_size(path);
|
|
|
|
if (file_size_int <= 0)
|
|
return false;
|
|
|
|
if (file_size)
|
|
*file_size = (uint64_t)file_size_int;
|
|
|
|
return true;
|
|
}
|
|
|
|
/* File initialisation/de-initialisation */
|
|
|
|
/* Loads specified Logiqx XML DAT file from disk.
|
|
* Returned logiqx_dat_t object must be free'd using
|
|
* logiqx_dat_free().
|
|
* Returns NULL if file is invalid or a read error
|
|
* occurs. */
|
|
logiqx_dat_t *logiqx_dat_init(const char *path)
|
|
{
|
|
logiqx_dat_t *dat_file = NULL;
|
|
rxml_node_t *root_node = NULL;
|
|
|
|
/* Check file path */
|
|
if (!logiqx_dat_path_is_valid(path, NULL))
|
|
goto error;
|
|
|
|
/* Create logiqx_dat_t object */
|
|
dat_file = (logiqx_dat_t*)calloc(1, sizeof(*dat_file));
|
|
|
|
if (!dat_file)
|
|
goto error;
|
|
|
|
/* Read file from disk */
|
|
dat_file->data = rxml_load_document(path);
|
|
|
|
if (!dat_file->data)
|
|
goto error;
|
|
|
|
/* Ensure root node has the correct name */
|
|
root_node = rxml_root_node(dat_file->data);
|
|
|
|
if (!root_node)
|
|
goto error;
|
|
|
|
if (string_is_empty(root_node->name))
|
|
goto error;
|
|
|
|
/* > Logiqx XML uses: 'datafile'
|
|
* > MAME List XML uses: 'mame'
|
|
* > MAME 'Software List' uses: 'softwarelist' */
|
|
if (!string_is_equal(root_node->name, "datafile") &&
|
|
!string_is_equal(root_node->name, "mame") &&
|
|
!string_is_equal(root_node->name, "softwarelist"))
|
|
goto error;
|
|
|
|
/* Get pointer to initial child node */
|
|
dat_file->current_node = root_node->children;
|
|
|
|
if (!dat_file->current_node)
|
|
goto error;
|
|
|
|
/* All is well - return logiqx_dat_t object */
|
|
return dat_file;
|
|
|
|
error:
|
|
logiqx_dat_free(dat_file);
|
|
return NULL;
|
|
}
|
|
|
|
/* Frees specified DAT file */
|
|
void logiqx_dat_free(logiqx_dat_t *dat_file)
|
|
{
|
|
if (!dat_file)
|
|
return;
|
|
|
|
dat_file->current_node = NULL;
|
|
|
|
if (dat_file->data)
|
|
{
|
|
rxml_free_document(dat_file->data);
|
|
dat_file->data = NULL;
|
|
}
|
|
|
|
free(dat_file);
|
|
dat_file = NULL;
|
|
}
|
|
|
|
/* Game information access */
|
|
|
|
/* Returns true if specified node is a 'game' entry */
|
|
static bool logiqx_dat_is_game_node(rxml_node_t *node)
|
|
{
|
|
const char *node_name = NULL;
|
|
|
|
if (!node)
|
|
return false;
|
|
|
|
/* Check node name */
|
|
node_name = node->name;
|
|
|
|
if (string_is_empty(node_name))
|
|
return false;
|
|
|
|
/* > Logiqx XML uses: 'game'
|
|
* > MAME List XML uses: 'machine'
|
|
* > MAME 'Software List' uses: 'software' */
|
|
return string_is_equal(node_name, "game") ||
|
|
string_is_equal(node_name, "machine") ||
|
|
string_is_equal(node_name, "software");
|
|
}
|
|
|
|
/* Returns true if specified node is a game
|
|
* node containing information for a game with
|
|
* the specified name */
|
|
static bool logiqx_dat_game_node_matches_name(
|
|
rxml_node_t *node, const char *game_name)
|
|
{
|
|
const char *node_game_name = NULL;
|
|
|
|
if (!logiqx_dat_is_game_node(node) ||
|
|
string_is_empty(game_name))
|
|
return false;
|
|
|
|
/* Get 'name' attribute of XML node */
|
|
node_game_name = rxml_node_attrib(node, "name");
|
|
|
|
if (string_is_empty(node_game_name))
|
|
return false;
|
|
|
|
return string_is_equal(node_game_name, game_name);
|
|
}
|
|
|
|
/* The XML element data strings returned from
|
|
* DAT files are very 'messy'. This function
|
|
* removes all cruft, replaces formatting strings
|
|
* and copies the result (if valid) to 'str' */
|
|
static void logiqx_dat_sanitise_element_data(
|
|
const char *data, char *str, size_t len)
|
|
{
|
|
char sanitised_data[PATH_MAX_LENGTH];
|
|
size_t i;
|
|
|
|
sanitised_data[0] = '\0';
|
|
|
|
if (string_is_empty(data))
|
|
return;
|
|
|
|
strlcpy(sanitised_data, data, sizeof(sanitised_data));
|
|
|
|
/* Element data includes leading/trailing
|
|
* newline characters - trim them away */
|
|
string_trim_whitespace(sanitised_data);
|
|
|
|
if (string_is_empty(sanitised_data))
|
|
return;
|
|
|
|
/* XML has a number of special characters that
|
|
* are handled using a HTML formatting codes.
|
|
* All of these have to be replaced...
|
|
* & -> &
|
|
* ' -> '
|
|
* > -> >
|
|
* < -> <
|
|
* " -> "
|
|
*/
|
|
for (i = 0; i < LOGIQX_DAT_HTML_CODE_LIST_SIZE; i++)
|
|
{
|
|
const char *find_string = logiqx_dat_html_code_list[i][0];
|
|
const char *replace_string = logiqx_dat_html_code_list[i][1];
|
|
|
|
/* string_replace_substring() is expensive
|
|
* > only invoke if element string contains
|
|
* HTML code */
|
|
if (strstr(sanitised_data, find_string))
|
|
{
|
|
char *tmp = string_replace_substring(
|
|
sanitised_data, find_string, replace_string);
|
|
|
|
if (!string_is_empty(tmp))
|
|
strlcpy(sanitised_data, tmp, sizeof(sanitised_data));
|
|
|
|
if (tmp)
|
|
free(tmp);
|
|
}
|
|
}
|
|
|
|
if (string_is_empty(sanitised_data))
|
|
return;
|
|
|
|
/* All is well - can copy result */
|
|
strlcpy(str, sanitised_data, len);
|
|
}
|
|
|
|
/* Extracts game information from specified node.
|
|
* Returns false if node is invalid */
|
|
static bool logiqx_dat_parse_game_node(
|
|
rxml_node_t *node, logiqx_dat_game_info_t *game_info)
|
|
{
|
|
const char *game_name = NULL;
|
|
const char *is_bios = NULL;
|
|
const char *is_runnable = NULL;
|
|
rxml_node_t *info_node = NULL;
|
|
bool description_found = false;
|
|
bool year_found = false;
|
|
bool manufacturer_found = false;
|
|
|
|
if (!logiqx_dat_is_game_node(node))
|
|
return false;
|
|
|
|
if (!game_info)
|
|
return false;
|
|
|
|
/* Initialise logiqx_dat_game_info_t object */
|
|
game_info->name[0] = '\0';
|
|
game_info->description[0] = '\0';
|
|
game_info->year[0] = '\0';
|
|
game_info->manufacturer[0] = '\0';
|
|
game_info->is_bios = false;
|
|
game_info->is_runnable = true;
|
|
|
|
/* Get game name */
|
|
game_name = rxml_node_attrib(node, "name");
|
|
|
|
if (!string_is_empty(game_name))
|
|
strlcpy(game_info->name, game_name, sizeof(game_info->name));
|
|
|
|
/* Get 'is bios' status */
|
|
is_bios = rxml_node_attrib(node, "isbios");
|
|
|
|
if (!string_is_empty(is_bios))
|
|
game_info->is_bios = string_is_equal(is_bios, "yes");
|
|
|
|
/* Get 'is runnable' status
|
|
* > Note: This attribute only exists in MAME List
|
|
* XML files, but there is no harm in checking for
|
|
* it generally. For normal Logiqx XML files,
|
|
* 'is runnable' is just the inverse of 'is bios' */
|
|
is_runnable = rxml_node_attrib(node, "runnable");
|
|
|
|
if (!string_is_empty(is_runnable))
|
|
game_info->is_runnable = string_is_equal(is_runnable, "yes");
|
|
else
|
|
game_info->is_runnable = !game_info->is_bios;
|
|
|
|
/* Loop over all game info nodes */
|
|
for (info_node = node->children; info_node; info_node = info_node->next)
|
|
{
|
|
const char *info_node_name = info_node->name;
|
|
const char *info_node_data = info_node->data;
|
|
|
|
if (string_is_empty(info_node_name))
|
|
continue;
|
|
|
|
/* Check description */
|
|
if (string_is_equal(info_node_name, "description"))
|
|
{
|
|
logiqx_dat_sanitise_element_data(
|
|
info_node_data, game_info->description,
|
|
sizeof(game_info->description));
|
|
description_found = true;
|
|
}
|
|
/* Check year */
|
|
else if (string_is_equal(info_node_name, "year"))
|
|
{
|
|
logiqx_dat_sanitise_element_data(
|
|
info_node_data, game_info->year,
|
|
sizeof(game_info->year));
|
|
year_found = true;
|
|
}
|
|
/* Check manufacturer */
|
|
else if (string_is_equal(info_node_name, "manufacturer"))
|
|
{
|
|
logiqx_dat_sanitise_element_data(
|
|
info_node_data, game_info->manufacturer,
|
|
sizeof(game_info->manufacturer));
|
|
manufacturer_found = true;
|
|
}
|
|
|
|
/* If all required entries have been found,
|
|
* can end loop */
|
|
if (description_found && year_found && manufacturer_found)
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Sets/resets internal node pointer to the first
|
|
* entry in the DAT file */
|
|
void logiqx_dat_set_first(logiqx_dat_t *dat_file)
|
|
{
|
|
rxml_node_t *root_node = NULL;
|
|
|
|
if (!dat_file)
|
|
return;
|
|
|
|
if (!dat_file->data)
|
|
return;
|
|
|
|
/* Get root node */
|
|
root_node = rxml_root_node(dat_file->data);
|
|
|
|
if (!root_node)
|
|
{
|
|
dat_file->current_node = NULL;
|
|
return;
|
|
}
|
|
|
|
/* Get pointer to initial child node */
|
|
dat_file->current_node = root_node->children;
|
|
}
|
|
|
|
/* Fetches game information for the current entry
|
|
* in the DAT file and increments the internal node
|
|
* pointer.
|
|
* Returns false if the end of the DAT file has been
|
|
* reached (in which case 'game_info' will be invalid) */
|
|
bool logiqx_dat_get_next(
|
|
logiqx_dat_t *dat_file, logiqx_dat_game_info_t *game_info)
|
|
{
|
|
if (!dat_file || !game_info)
|
|
return false;
|
|
|
|
if (!dat_file->data)
|
|
return false;
|
|
|
|
while (dat_file->current_node)
|
|
{
|
|
rxml_node_t *current_node = dat_file->current_node;
|
|
|
|
/* Whatever happens, internal node pointer must
|
|
* be 'incremented' */
|
|
dat_file->current_node = dat_file->current_node->next;
|
|
|
|
/* If this is a game node, extract info
|
|
* and return */
|
|
if (logiqx_dat_is_game_node(current_node))
|
|
return logiqx_dat_parse_game_node(current_node, game_info);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Fetches information for the specified game.
|
|
* Returns false if game does not exist, or arguments
|
|
* are invalid. */
|
|
bool logiqx_dat_search(
|
|
logiqx_dat_t *dat_file, const char *game_name,
|
|
logiqx_dat_game_info_t *game_info)
|
|
{
|
|
rxml_node_t *root_node = NULL;
|
|
rxml_node_t *game_node = NULL;
|
|
|
|
if (!dat_file || !game_info || string_is_empty(game_name))
|
|
return false;
|
|
|
|
if (!dat_file->data)
|
|
return false;
|
|
|
|
/* Get root node */
|
|
root_node = rxml_root_node(dat_file->data);
|
|
|
|
if (!root_node)
|
|
return false;
|
|
|
|
/* Loop over all child nodes of the DAT file */
|
|
for (game_node = root_node->children; game_node; game_node = game_node->next)
|
|
{
|
|
/* If this is the requested game, fetch info and return */
|
|
if (logiqx_dat_game_node_matches_name(game_node, game_name))
|
|
return logiqx_dat_parse_game_node(game_node, game_info);
|
|
}
|
|
|
|
return false;
|
|
}
|