git subrepo clone https://github.com/libretro/libretro-common.git deps/libretro-common
[pcsx_rearmed.git] / deps / libretro-common / formats / logiqx_dat / logiqx_dat.c
CommitLineData
3719602c
PC
1/* Copyright (C) 2010-2020 The RetroArch team
2 *
3 * ---------------------------------------------------------------------------------------
4 * The following license statement only applies to this file (logiqx_dat.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 <file/file_path.h>
24#include <string/stdstring.h>
25#include <formats/rxml.h>
26
27#include <formats/logiqx_dat.h>
28
29/* Holds all internal DAT file data */
30struct logiqx_dat
31{
32 rxml_document_t *data;
33 rxml_node_t *current_node;
34};
35
36/* List of HTML formatting codes that must
37 * be replaced when parsing XML data */
38const char *logiqx_dat_html_code_list[][2] = {
39 {"&amp;", "&"},
40 {"&apos;", "'"},
41 {"&gt;", ">"},
42 {"&lt;", "<"},
43 {"&quot;", "\""}
44};
45
46#define LOGIQX_DAT_HTML_CODE_LIST_SIZE 5
47
48/* Validation */
49
50/* Performs rudimentary validation of the specified
51 * Logiqx XML DAT file path (not rigorous - just
52 * enough to prevent obvious errors).
53 * Also provides access to file size (DAT files can
54 * be very large, so it is useful to have this information
55 * on hand - i.e. so we can check that the system has
56 * enough free memory to load the file). */
57bool logiqx_dat_path_is_valid(const char *path, uint64_t *file_size)
58{
59 const char *file_ext = NULL;
60 int32_t file_size_int;
61
62 if (string_is_empty(path))
63 return false;
64
65 /* Check file extension */
66 file_ext = path_get_extension(path);
67
68 if (string_is_empty(file_ext))
69 return false;
70
71 if (!string_is_equal_noncase(file_ext, "dat") &&
72 !string_is_equal_noncase(file_ext, "xml"))
73 return false;
74
75 /* Ensure file exists */
76 if (!path_is_valid(path))
77 return false;
78
79 /* Get file size */
80 file_size_int = path_get_size(path);
81
82 if (file_size_int <= 0)
83 return false;
84
85 if (file_size)
86 *file_size = (uint64_t)file_size_int;
87
88 return true;
89}
90
91/* File initialisation/de-initialisation */
92
93/* Loads specified Logiqx XML DAT file from disk.
94 * Returned logiqx_dat_t object must be free'd using
95 * logiqx_dat_free().
96 * Returns NULL if file is invalid or a read error
97 * occurs. */
98logiqx_dat_t *logiqx_dat_init(const char *path)
99{
100 logiqx_dat_t *dat_file = NULL;
101 rxml_node_t *root_node = NULL;
102
103 /* Check file path */
104 if (!logiqx_dat_path_is_valid(path, NULL))
105 goto error;
106
107 /* Create logiqx_dat_t object */
108 dat_file = (logiqx_dat_t*)calloc(1, sizeof(*dat_file));
109
110 if (!dat_file)
111 goto error;
112
113 /* Read file from disk */
114 dat_file->data = rxml_load_document(path);
115
116 if (!dat_file->data)
117 goto error;
118
119 /* Ensure root node has the correct name */
120 root_node = rxml_root_node(dat_file->data);
121
122 if (!root_node)
123 goto error;
124
125 if (string_is_empty(root_node->name))
126 goto error;
127
128 /* > Logiqx XML uses: 'datafile'
129 * > MAME List XML uses: 'mame'
130 * > MAME 'Software List' uses: 'softwarelist' */
131 if (!string_is_equal(root_node->name, "datafile") &&
132 !string_is_equal(root_node->name, "mame") &&
133 !string_is_equal(root_node->name, "softwarelist"))
134 goto error;
135
136 /* Get pointer to initial child node */
137 dat_file->current_node = root_node->children;
138
139 if (!dat_file->current_node)
140 goto error;
141
142 /* All is well - return logiqx_dat_t object */
143 return dat_file;
144
145error:
146 logiqx_dat_free(dat_file);
147 return NULL;
148}
149
150/* Frees specified DAT file */
151void logiqx_dat_free(logiqx_dat_t *dat_file)
152{
153 if (!dat_file)
154 return;
155
156 dat_file->current_node = NULL;
157
158 if (dat_file->data)
159 {
160 rxml_free_document(dat_file->data);
161 dat_file->data = NULL;
162 }
163
164 free(dat_file);
165 dat_file = NULL;
166}
167
168/* Game information access */
169
170/* Returns true if specified node is a 'game' entry */
171static bool logiqx_dat_is_game_node(rxml_node_t *node)
172{
173 const char *node_name = NULL;
174
175 if (!node)
176 return false;
177
178 /* Check node name */
179 node_name = node->name;
180
181 if (string_is_empty(node_name))
182 return false;
183
184 /* > Logiqx XML uses: 'game'
185 * > MAME List XML uses: 'machine'
186 * > MAME 'Software List' uses: 'software' */
187 return string_is_equal(node_name, "game") ||
188 string_is_equal(node_name, "machine") ||
189 string_is_equal(node_name, "software");
190}
191
192/* Returns true if specified node is a game
193 * node containing information for a game with
194 * the specified name */
195static bool logiqx_dat_game_node_matches_name(
196 rxml_node_t *node, const char *game_name)
197{
198 const char *node_game_name = NULL;
199
200 if (!logiqx_dat_is_game_node(node) ||
201 string_is_empty(game_name))
202 return false;
203
204 /* Get 'name' attribute of XML node */
205 node_game_name = rxml_node_attrib(node, "name");
206
207 if (string_is_empty(node_game_name))
208 return false;
209
210 return string_is_equal(node_game_name, game_name);
211}
212
213/* The XML element data strings returned from
214 * DAT files are very 'messy'. This function
215 * removes all cruft, replaces formatting strings
216 * and copies the result (if valid) to 'str' */
217static void logiqx_dat_sanitise_element_data(
218 const char *data, char *str, size_t len)
219{
220 char sanitised_data[PATH_MAX_LENGTH];
221 size_t i;
222
223 sanitised_data[0] = '\0';
224
225 if (string_is_empty(data))
226 return;
227
228 strlcpy(sanitised_data, data, sizeof(sanitised_data));
229
230 /* Element data includes leading/trailing
231 * newline characters - trim them away */
232 string_trim_whitespace(sanitised_data);
233
234 if (string_is_empty(sanitised_data))
235 return;
236
237 /* XML has a number of special characters that
238 * are handled using a HTML formatting codes.
239 * All of these have to be replaced...
240 * &amp; -> &
241 * &apos; -> '
242 * &gt; -> >
243 * &lt; -> <
244 * &quot; -> "
245 */
246 for (i = 0; i < LOGIQX_DAT_HTML_CODE_LIST_SIZE; i++)
247 {
248 const char *find_string = logiqx_dat_html_code_list[i][0];
249 const char *replace_string = logiqx_dat_html_code_list[i][1];
250
251 /* string_replace_substring() is expensive
252 * > only invoke if element string contains
253 * HTML code */
254 if (strstr(sanitised_data, find_string))
255 {
256 char *tmp = string_replace_substring(
257 sanitised_data,
258 find_string, strlen(find_string),
259 replace_string, strlen(replace_string));
260
261 if (!string_is_empty(tmp))
262 strlcpy(sanitised_data, tmp, sizeof(sanitised_data));
263
264 if (tmp)
265 free(tmp);
266 }
267 }
268
269 if (string_is_empty(sanitised_data))
270 return;
271
272 /* All is well - can copy result */
273 strlcpy(str, sanitised_data, len);
274}
275
276/* Extracts game information from specified node.
277 * Returns false if node is invalid */
278static bool logiqx_dat_parse_game_node(
279 rxml_node_t *node, logiqx_dat_game_info_t *game_info)
280{
281 const char *game_name = NULL;
282 const char *is_bios = NULL;
283 const char *is_runnable = NULL;
284 rxml_node_t *info_node = NULL;
285 bool description_found = false;
286 bool year_found = false;
287 bool manufacturer_found = false;
288
289 if (!logiqx_dat_is_game_node(node))
290 return false;
291
292 if (!game_info)
293 return false;
294
295 /* Initialise logiqx_dat_game_info_t object */
296 game_info->name[0] = '\0';
297 game_info->description[0] = '\0';
298 game_info->year[0] = '\0';
299 game_info->manufacturer[0] = '\0';
300 game_info->is_bios = false;
301 game_info->is_runnable = true;
302
303 /* Get game name */
304 game_name = rxml_node_attrib(node, "name");
305
306 if (!string_is_empty(game_name))
307 strlcpy(game_info->name, game_name, sizeof(game_info->name));
308
309 /* Get 'is bios' status */
310 is_bios = rxml_node_attrib(node, "isbios");
311
312 if (!string_is_empty(is_bios))
313 game_info->is_bios = string_is_equal(is_bios, "yes");
314
315 /* Get 'is runnable' status
316 * > Note: This attribute only exists in MAME List
317 * XML files, but there is no harm in checking for
318 * it generally. For normal Logiqx XML files,
319 * 'is runnable' is just the inverse of 'is bios' */
320 is_runnable = rxml_node_attrib(node, "runnable");
321
322 if (!string_is_empty(is_runnable))
323 game_info->is_runnable = string_is_equal(is_runnable, "yes");
324 else
325 game_info->is_runnable = !game_info->is_bios;
326
327 /* Loop over all game info nodes */
328 for (info_node = node->children; info_node; info_node = info_node->next)
329 {
330 const char *info_node_name = info_node->name;
331 const char *info_node_data = info_node->data;
332
333 if (string_is_empty(info_node_name))
334 continue;
335
336 /* Check description */
337 if (string_is_equal(info_node_name, "description"))
338 {
339 logiqx_dat_sanitise_element_data(
340 info_node_data, game_info->description,
341 sizeof(game_info->description));
342 description_found = true;
343 }
344 /* Check year */
345 else if (string_is_equal(info_node_name, "year"))
346 {
347 logiqx_dat_sanitise_element_data(
348 info_node_data, game_info->year,
349 sizeof(game_info->year));
350 year_found = true;
351 }
352 /* Check manufacturer */
353 else if (string_is_equal(info_node_name, "manufacturer"))
354 {
355 logiqx_dat_sanitise_element_data(
356 info_node_data, game_info->manufacturer,
357 sizeof(game_info->manufacturer));
358 manufacturer_found = true;
359 }
360
361 /* If all required entries have been found,
362 * can end loop */
363 if (description_found && year_found && manufacturer_found)
364 break;
365 }
366
367 return true;
368}
369
370/* Sets/resets internal node pointer to the first
371 * entry in the DAT file */
372void logiqx_dat_set_first(logiqx_dat_t *dat_file)
373{
374 rxml_node_t *root_node = NULL;
375
376 if (!dat_file)
377 return;
378
379 if (!dat_file->data)
380 return;
381
382 /* Get root node */
383 root_node = rxml_root_node(dat_file->data);
384
385 if (!root_node)
386 {
387 dat_file->current_node = NULL;
388 return;
389 }
390
391 /* Get pointer to initial child node */
392 dat_file->current_node = root_node->children;
393}
394
395/* Fetches game information for the current entry
396 * in the DAT file and increments the internal node
397 * pointer.
398 * Returns false if the end of the DAT file has been
399 * reached (in which case 'game_info' will be invalid) */
400bool logiqx_dat_get_next(
401 logiqx_dat_t *dat_file, logiqx_dat_game_info_t *game_info)
402{
403 if (!dat_file || !game_info)
404 return false;
405
406 if (!dat_file->data)
407 return false;
408
409 while (dat_file->current_node)
410 {
411 rxml_node_t *current_node = dat_file->current_node;
412
413 /* Whatever happens, internal node pointer must
414 * be 'incremented' */
415 dat_file->current_node = dat_file->current_node->next;
416
417 /* If this is a game node, extract info
418 * and return */
419 if (logiqx_dat_is_game_node(current_node))
420 return logiqx_dat_parse_game_node(current_node, game_info);
421 }
422
423 return false;
424}
425
426/* Fetches information for the specified game.
427 * Returns false if game does not exist, or arguments
428 * are invalid. */
429bool logiqx_dat_search(
430 logiqx_dat_t *dat_file, const char *game_name,
431 logiqx_dat_game_info_t *game_info)
432{
433 rxml_node_t *root_node = NULL;
434 rxml_node_t *game_node = NULL;
435
436 if (!dat_file || !game_info || string_is_empty(game_name))
437 return false;
438
439 if (!dat_file->data)
440 return false;
441
442 /* Get root node */
443 root_node = rxml_root_node(dat_file->data);
444
445 if (!root_node)
446 return false;
447
448 /* Loop over all child nodes of the DAT file */
449 for (game_node = root_node->children; game_node; game_node = game_node->next)
450 {
451 /* If this is the requested game, fetch info and return */
452 if (logiqx_dat_game_node_matches_name(game_node, game_name))
453 return logiqx_dat_parse_game_node(game_node, game_info);
454 }
455
456 return false;
457}