1 /* RetroArch - A frontend for libretro.
2 * Copyright (C) 2010-2014 - Hans-Kristian Arntzen
3 * Copyright (C) 2011-2017 - Daniel De Matteis
4 * Copyright (C) 2016-2019 - Brad Parker
5 *
6 * RetroArch is free software: you can redistribute it and/or modify it under the terms
7 * of the GNU General Public License as published by the Free Software Found-
8 * ation, either version 3 of the License, or (at your option) any later version.
9 *
10 * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
11 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
12 * PURPOSE. See the GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along with RetroArch.
15 * If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <string.h>
21 #include <ctype.h>
22
23 #include <libretro.h>
24 #include <boolean.h>
25 #include <retro_assert.h>
26 #include <retro_miscellaneous.h>
27 #include <compat/posix_string.h>
28 #include <string/stdstring.h>
29 #include <streams/interface_stream.h>
30 #include <file/file_path.h>
31 #include <lists/string_list.h>
32 #include <formats/rjson.h>
33 #include <array/rbuf.h>
34
35 #include "playlist.h"
36 #include "verbosity.h"
37 #include "file_path_special.h"
38 #include "core_info.h"
39
40 #if defined(ANDROID)
41 #include "play_feature_delivery/play_feature_delivery.h"
42 #endif
43
44 #ifndef PLAYLIST_ENTRIES
45 #define PLAYLIST_ENTRIES 6
46 #endif
47
48 #define WINDOWS_PATH_DELIMITER '\\'
49 #define POSIX_PATH_DELIMITER '/'
50
51 #ifdef _WIN32
52 #define LOCAL_FILE_SYSTEM_PATH_DELIMITER WINDOWS_PATH_DELIMITER
53 #define USING_WINDOWS_FILE_SYSTEM
54 #else
55 #define LOCAL_FILE_SYSTEM_PATH_DELIMITER POSIX_PATH_DELIMITER
56 #define USING_POSIX_FILE_SYSTEM
57 #endif
58
59 struct content_playlist
60 {
61 char *default_core_path;
62 char *default_core_name;
63 char *base_content_directory;
64
65 struct playlist_entry *entries;
66
67 playlist_config_t config; /* size_t alignment */
68
69 enum playlist_label_display_mode label_display_mode;
70 enum playlist_thumbnail_mode right_thumbnail_mode;
71 enum playlist_thumbnail_mode left_thumbnail_mode;
72 enum playlist_sort_mode sort_mode;
73
74 bool modified;
75 bool old_format;
76 bool compressed;
77 bool cached_external;
78 };
79
80 typedef struct
81 {
82 struct playlist_entry *current_entry;
83 char **current_string_val;
84 unsigned *current_entry_uint_val;
85 enum playlist_label_display_mode *current_meta_label_display_mode_val;
86 enum playlist_thumbnail_mode *current_meta_thumbnail_mode_val;
87 enum playlist_sort_mode *current_meta_sort_mode_val;
88 playlist_t *playlist;
89
90 unsigned array_depth;
91 unsigned object_depth;
92
93 bool in_items;
94 bool in_subsystem_roms;
95 bool capacity_exceeded;
96 bool out_of_memory;
97 } JSONContext;
98
99 /* TODO/FIXME - global state - perhaps move outside this file */
100 static playlist_t *playlist_cached = NULL;
101
102 typedef int (playlist_sort_fun_t)(
103 const struct playlist_entry *a,
104 const struct playlist_entry *b);
105
106 /* TODO/FIXME - hack for allowing the explore view to switch
107 * over to a playlist item */
playlist_set_cached_external(playlist_t * pl)108 void playlist_set_cached_external(playlist_t* pl)
109 {
110 playlist_free_cached();
111 if (!pl)
112 return;
113
114 playlist_cached = pl;
115 playlist_cached->cached_external = true;
116 }
117
118 /* Convenience function: copies specified playlist
119 * path to specified playlist configuration object */
playlist_config_set_path(playlist_config_t * config,const char * path)120 void playlist_config_set_path(playlist_config_t *config, const char *path)
121 {
122 if (!config)
123 return;
124
125 if (!string_is_empty(path))
126 strlcpy(config->path, path, sizeof(config->path));
127 else
128 config->path[0] = '\0';
129 }
130
131 /* Convenience function: copies base content directory
132 * path to specified playlist configuration object.
133 * Also sets autofix_paths boolean, depending on base
134 * content directory value */
playlist_config_set_base_content_directory(playlist_config_t * config,const char * path)135 void playlist_config_set_base_content_directory(
136 playlist_config_t* config, const char* path)
137 {
138 if (!config)
139 return;
140
141 config->autofix_paths = !string_is_empty(path);
142 if (config->autofix_paths)
143 strlcpy(config->base_content_directory, path,
144 sizeof(config->base_content_directory));
145 else
146 config->base_content_directory[0] = '\0';
147 }
148
149
150 /* Creates a copy of the specified playlist configuration.
151 * Returns false in the event of an error */
playlist_config_copy(const playlist_config_t * src,playlist_config_t * dst)152 bool playlist_config_copy(const playlist_config_t *src,
153 playlist_config_t *dst)
154 {
155 if (!src || !dst)
156 return false;
157
158 strlcpy(dst->path, src->path, sizeof(dst->path));
159 strlcpy(dst->base_content_directory, src->base_content_directory,
160 sizeof(dst->base_content_directory));
161
162 dst->capacity = src->capacity;
163 dst->old_format = src->old_format;
164 dst->compress = src->compress;
165 dst->fuzzy_archive_match = src->fuzzy_archive_match;
166 dst->autofix_paths = src->autofix_paths;
167
168 return true;
169 }
170
171 /* Returns internal playlist configuration object
172 * of specified playlist.
173 * Returns NULL it the event of an error. */
playlist_get_config(playlist_t * playlist)174 playlist_config_t *playlist_get_config(playlist_t *playlist)
175 {
176 if (!playlist)
177 return NULL;
178
179 return &playlist->config;
180 }
181
path_replace_base_path_and_convert_to_local_file_system(char * out_path,const char * in_path,const char * in_oldrefpath,const char * in_refpath,size_t size)182 static void path_replace_base_path_and_convert_to_local_file_system(
183 char *out_path, const char *in_path,
184 const char *in_oldrefpath, const char *in_refpath,
185 size_t size)
186 {
187 size_t in_oldrefpath_length = strlen(in_oldrefpath);
188 size_t in_refpath_length = strlen(in_refpath);
189
190 /* If entry path is inside playlist base path,
191 * replace it with new base content directory */
192 if (string_starts_with_size(in_path, in_oldrefpath, in_oldrefpath_length))
193 {
194 memcpy(out_path, in_refpath, in_refpath_length);
195 memcpy(out_path + in_refpath_length, in_path + in_oldrefpath_length,
196 strlen(in_path) - in_oldrefpath_length + 1);
197
198 #ifdef USING_WINDOWS_FILE_SYSTEM
199 /* If we are running under a Windows filesystem,
200 * '/' characters are not allowed anywhere.
201 * We replace with '\' and hope for the best... */
202 string_replace_all_chars(out_path,
203 POSIX_PATH_DELIMITER, WINDOWS_PATH_DELIMITER);
204 #endif
205
206 #ifdef USING_POSIX_FILE_SYSTEM
207 /* Under POSIX filesystem, we replace '\' characters with '/' */
208 string_replace_all_chars(out_path,
209 WINDOWS_PATH_DELIMITER, POSIX_PATH_DELIMITER);
210 #endif
211 }
212 else
213 strlcpy(out_path, in_path, size);
214 }
215
216 /* Generates a case insensitive hash for the
217 * specified path string */
playlist_path_hash(const char * path)218 static uint32_t playlist_path_hash(const char *path)
219 {
220 unsigned char c;
221 uint32_t hash = (uint32_t)0x811c9dc5;
222 while ((c = (unsigned char)*(path++)) != '\0')
223 hash = ((hash * (uint32_t)0x01000193) ^ (uint32_t)((c >= 'A' && c <= 'Z') ? (c | 0x20) : c));
224 return (hash ? hash : 1);
225 }
226
playlist_path_id_free(playlist_path_id_t * path_id)227 static void playlist_path_id_free(playlist_path_id_t *path_id)
228 {
229 if (!path_id)
230 return;
231
232 if (path_id->archive_path &&
233 (path_id->archive_path != path_id->real_path))
234 free(path_id->archive_path);
235
236 if (path_id->real_path)
237 free(path_id->real_path);
238
239 free(path_id);
240 }
241
playlist_path_id_init(const char * path)242 static playlist_path_id_t *playlist_path_id_init(const char *path)
243 {
244 playlist_path_id_t *path_id = (playlist_path_id_t*)malloc(sizeof(*path_id));
245 const char *archive_delim = NULL;
246 char real_path[PATH_MAX_LENGTH];
247
248 real_path[0] = '\0';
249
250 if (!path_id)
251 return NULL;
252
253 path_id->real_path = NULL;
254 path_id->archive_path = NULL;
255 path_id->real_path_hash = 0;
256 path_id->archive_path_hash = 0;
257 path_id->is_archive = false;
258 path_id->is_in_archive = false;
259
260 if (string_is_empty(path))
261 return path_id;
262
263 /* Get real path */
264 strlcpy(real_path, path, sizeof(real_path));
265 playlist_resolve_path(PLAYLIST_SAVE, false, real_path,
266 sizeof(real_path));
267
268 path_id->real_path = strdup(real_path);
269 path_id->real_path_hash = playlist_path_hash(real_path);
270
271 /* Check archive status */
272 path_id->is_archive = path_is_compressed_file(real_path);
273 archive_delim = path_get_archive_delim(real_path);
274
275 /* If path refers to a file inside an archive,
276 * extract the path of the parent archive */
277 if (archive_delim)
278 {
279 size_t len = (1 + archive_delim - real_path);
280 char archive_path[PATH_MAX_LENGTH] = {0};
281
282 len = (len < PATH_MAX_LENGTH) ? len : PATH_MAX_LENGTH;
283 strlcpy(archive_path, real_path, len * sizeof(char));
284
285 path_id->archive_path = strdup(archive_path);
286 path_id->archive_path_hash = playlist_path_hash(archive_path);
287 path_id->is_in_archive = true;
288 }
289 else if (path_id->is_archive)
290 {
291 path_id->archive_path = path_id->real_path;
292 path_id->archive_path_hash = path_id->real_path_hash;
293 }
294
295 return path_id;
296 }
297
298 /**
299 * playlist_path_equal:
300 * @real_path : 'Real' search path, generated by path_resolve_realpath()
301 * @entry_path : Existing playlist entry 'path' value
302 *
303 * Returns 'true' if real_path matches entry_path
304 * (Taking into account relative paths, case insensitive
305 * filesystems, 'incomplete' archive paths)
306 **/
playlist_path_equal(const char * real_path,const char * entry_path,const playlist_config_t * config)307 static bool playlist_path_equal(const char *real_path,
308 const char *entry_path, const playlist_config_t *config)
309 {
310 bool real_path_is_compressed;
311 bool entry_real_path_is_compressed;
312 char entry_real_path[PATH_MAX_LENGTH];
313
314 entry_real_path[0] = '\0';
315
316 /* Sanity check */
317 if (string_is_empty(real_path) ||
318 string_is_empty(entry_path) ||
319 !config)
320 return false;
321
322 /* Get entry 'real' path */
323 strlcpy(entry_real_path, entry_path, sizeof(entry_real_path));
324 path_resolve_realpath(entry_real_path, sizeof(entry_real_path), true);
325
326 if (string_is_empty(entry_real_path))
327 return false;
328
329 /* First pass comparison */
330 #ifdef _WIN32
331 /* Handle case-insensitive operating systems*/
332 if (string_is_equal_noncase(real_path, entry_real_path))
333 return true;
334 #else
335 if (string_is_equal(real_path, entry_real_path))
336 return true;
337 #endif
338
339 #ifdef RARCH_INTERNAL
340 /* If fuzzy matching is disabled, we can give up now */
341 if (!config->fuzzy_archive_match)
342 return false;
343 #endif
344
345 /* If we reach this point, we have to work
346 * harder...
347 * Need to handle a rather awkward archive file
348 * case where:
349 * - playlist path contains a properly formatted
350 * [archive_path][delimiter][rom_file]
351 * - search path is just [archive_path]
352 * ...or vice versa.
353 * This pretty much always happens when a playlist
354 * is generated via scan content (which handles the
355 * archive paths correctly), but the user subsequently
356 * loads an archive file via the command line or some
357 * external launcher (where the [delimiter][rom_file]
358 * part is almost always omitted) */
359 real_path_is_compressed = path_is_compressed_file(real_path);
360 entry_real_path_is_compressed = path_is_compressed_file(entry_real_path);
361
362 if ((real_path_is_compressed && !entry_real_path_is_compressed) ||
363 (!real_path_is_compressed && entry_real_path_is_compressed))
364 {
365 const char *compressed_path_a = real_path_is_compressed ? real_path : entry_real_path;
366 const char *full_path = real_path_is_compressed ? entry_real_path : real_path;
367 const char *delim = path_get_archive_delim(full_path);
368
369 if (delim)
370 {
371 char compressed_path_b[PATH_MAX_LENGTH] = {0};
372 unsigned len = (unsigned)(1 + delim - full_path);
373
374 strlcpy(compressed_path_b, full_path,
375 (len < PATH_MAX_LENGTH ? len : PATH_MAX_LENGTH) * sizeof(char));
376
377 #ifdef _WIN32
378 /* Handle case-insensitive operating systems*/
379 if (string_is_equal_noncase(compressed_path_a, compressed_path_b))
380 return true;
381 #else
382 if (string_is_equal(compressed_path_a, compressed_path_b))
383 return true;
384 #endif
385 }
386 }
387
388 return false;
389 }
390
391 /**
392 * playlist_path_matches_entry:
393 * @path_id : Path identity, containing 'real' path,
394 * hash and archive status information
395 * @entry : Playlist entry to compare with path_id
396 *
397 * Returns 'true' if 'path_id' matches path information
398 * contained in specified 'entry'. Will update path_id
399 * cache inside specified 'entry', if not already present.
400 **/
playlist_path_matches_entry(playlist_path_id_t * path_id,struct playlist_entry * entry,const playlist_config_t * config)401 static bool playlist_path_matches_entry(playlist_path_id_t *path_id,
402 struct playlist_entry *entry, const playlist_config_t *config)
403 {
404 /* Sanity check */
405 if (!path_id ||
406 !entry ||
407 !config)
408 return false;
409
410 /* Check whether entry contains a path ID cache */
411 if (!entry->path_id)
412 {
413 entry->path_id = playlist_path_id_init(entry->path);
414 if (!entry->path_id)
415 return false;
416 }
417
418 /* Ensure we have valid real_path strings */
419 if (string_is_empty(path_id->real_path) ||
420 string_is_empty(entry->path_id->real_path))
421 return false;
422
423 /* First pass comparison */
424 if (path_id->real_path_hash ==
425 entry->path_id->real_path_hash)
426 {
427 #ifdef _WIN32
428 /* Handle case-insensitive operating systems*/
429 if (string_is_equal_noncase(path_id->real_path,
430 entry->path_id->real_path))
431 return true;
432 #else
433 if (string_is_equal(path_id->real_path,
434 entry->path_id->real_path))
435 return true;
436 #endif
437 }
438
439 #ifdef RARCH_INTERNAL
440 /* If fuzzy matching is disabled, we can give up now */
441 if (!config->fuzzy_archive_match)
442 return false;
443 #endif
444
445 /* If we reach this point, we have to work
446 * harder...
447 * Need to handle a rather awkward archive file
448 * case where:
449 * - playlist path contains a properly formatted
450 * [archive_path][delimiter][rom_file]
451 * - search path is just [archive_path]
452 * ...or vice versa.
453 * This pretty much always happens when a playlist
454 * is generated via scan content (which handles the
455 * archive paths correctly), but the user subsequently
456 * loads an archive file via the command line or some
457 * external launcher (where the [delimiter][rom_file]
458 * part is almost always omitted) */
459 if (((path_id->is_archive && !path_id->is_in_archive) && entry->path_id->is_in_archive) ||
460 ((entry->path_id->is_archive && !entry->path_id->is_in_archive) && path_id->is_in_archive))
461 {
462 /* Ensure we have valid parent archive path
463 * strings */
464 if (string_is_empty(path_id->archive_path) ||
465 string_is_empty(entry->path_id->archive_path))
466 return false;
467
468 if (path_id->archive_path_hash ==
469 entry->path_id->archive_path_hash)
470 {
471 #ifdef _WIN32
472 /* Handle case-insensitive operating systems*/
473 if (string_is_equal_noncase(path_id->archive_path,
474 entry->path_id->archive_path))
475 return true;
476 #else
477 if (string_is_equal(path_id->archive_path,
478 entry->path_id->archive_path))
479 return true;
480 #endif
481 }
482 }
483
484 return false;
485 }
486
487 /**
488 * playlist_core_path_equal:
489 * @real_core_path : 'Real' search path, generated by path_resolve_realpath()
490 * @entry_core_path : Existing playlist entry 'core path' value
491 * @config : Playlist config parameters
492 *
493 * Returns 'true' if real_core_path matches entry_core_path
494 * (Taking into account relative paths, case insensitive
495 * filesystems)
496 **/
playlist_core_path_equal(const char * real_core_path,const char * entry_core_path,const playlist_config_t * config)497 static bool playlist_core_path_equal(const char *real_core_path, const char *entry_core_path, const playlist_config_t *config)
498 {
499 char entry_real_core_path[PATH_MAX_LENGTH];
500
501 entry_real_core_path[0] = '\0';
502
503 /* Sanity check */
504 if (string_is_empty(real_core_path) || string_is_empty(entry_core_path))
505 return false;
506
507 /* Get entry 'real' core path */
508 strlcpy(entry_real_core_path, entry_core_path, sizeof(entry_real_core_path));
509 if (!string_is_equal(entry_real_core_path, FILE_PATH_DETECT) &&
510 !string_is_equal(entry_real_core_path, FILE_PATH_BUILTIN))
511 playlist_resolve_path(PLAYLIST_SAVE, true, entry_real_core_path,
512 sizeof(entry_real_core_path));
513
514 if (string_is_empty(entry_real_core_path))
515 return false;
516
517 #ifdef _WIN32
518 /* Handle case-insensitive operating systems*/
519 if (string_is_equal_noncase(real_core_path, entry_real_core_path))
520 return true;
521 #else
522 if (string_is_equal(real_core_path, entry_real_core_path))
523 return true;
524 #endif
525
526 if (config->autofix_paths &&
527 core_info_core_file_id_is_equal(real_core_path, entry_core_path))
528 return true;
529
530 return false;
531 }
532
playlist_get_size(playlist_t * playlist)533 uint32_t playlist_get_size(playlist_t *playlist)
534 {
535 if (!playlist)
536 return 0;
537 return (uint32_t)RBUF_LEN(playlist->entries);
538 }
539
playlist_get_conf_path(playlist_t * playlist)540 char *playlist_get_conf_path(playlist_t *playlist)
541 {
542 if (!playlist)
543 return NULL;
544 return playlist->config.path;
545 }
546
547 /**
548 * playlist_get_index:
549 * @playlist : Playlist handle.
550 * @idx : Index of playlist entry.
551 * @path : Path of playlist entry.
552 * @core_path : Core path of playlist entry.
553 * @core_name : Core name of playlist entry.
554 *
555 * Gets values of playlist index:
556 **/
playlist_get_index(playlist_t * playlist,size_t idx,const struct playlist_entry ** entry)557 void playlist_get_index(playlist_t *playlist,
558 size_t idx,
559 const struct playlist_entry **entry)
560 {
561 if (!playlist || !entry || (idx >= RBUF_LEN(playlist->entries)))
562 return;
563
564 *entry = &playlist->entries[idx];
565 }
566
567 /**
568 * playlist_free_entry:
569 * @entry : Playlist entry handle.
570 *
571 * Frees playlist entry.
572 **/
playlist_free_entry(struct playlist_entry * entry)573 static void playlist_free_entry(struct playlist_entry *entry)
574 {
575 if (!entry)
576 return;
577
578 if (entry->path)
579 free(entry->path);
580 if (entry->label)
581 free(entry->label);
582 if (entry->core_path)
583 free(entry->core_path);
584 if (entry->core_name)
585 free(entry->core_name);
586 if (entry->db_name)
587 free(entry->db_name);
588 if (entry->crc32)
589 free(entry->crc32);
590 if (entry->subsystem_ident)
591 free(entry->subsystem_ident);
592 if (entry->subsystem_name)
593 free(entry->subsystem_name);
594 if (entry->runtime_str)
595 free(entry->runtime_str);
596 if (entry->last_played_str)
597 free(entry->last_played_str);
598 if (entry->subsystem_roms)
599 string_list_free(entry->subsystem_roms);
600 if (entry->path_id)
601 playlist_path_id_free(entry->path_id);
602
603 entry->path = NULL;
604 entry->label = NULL;
605 entry->core_path = NULL;
606 entry->core_name = NULL;
607 entry->db_name = NULL;
608 entry->crc32 = NULL;
609 entry->subsystem_ident = NULL;
610 entry->subsystem_name = NULL;
611 entry->runtime_str = NULL;
612 entry->last_played_str = NULL;
613 entry->subsystem_roms = NULL;
614 entry->path_id = NULL;
615 entry->runtime_status = PLAYLIST_RUNTIME_UNKNOWN;
616 entry->runtime_hours = 0;
617 entry->runtime_minutes = 0;
618 entry->runtime_seconds = 0;
619 entry->last_played_year = 0;
620 entry->last_played_month = 0;
621 entry->last_played_day = 0;
622 entry->last_played_hour = 0;
623 entry->last_played_minute = 0;
624 entry->last_played_second = 0;
625 }
626
627 /**
628 * playlist_delete_index:
629 * @playlist : Playlist handle.
630 * @idx : Index of playlist entry.
631 *
632 * Delete the entry at the index:
633 **/
playlist_delete_index(playlist_t * playlist,size_t idx)634 void playlist_delete_index(playlist_t *playlist,
635 size_t idx)
636 {
637 size_t len;
638 struct playlist_entry *entry_to_delete;
639
640 if (!playlist)
641 return;
642
643 len = RBUF_LEN(playlist->entries);
644 if (idx >= len)
645 return;
646
647 /* Free unwanted entry */
648 entry_to_delete = (struct playlist_entry *)(playlist->entries + idx);
649 if (entry_to_delete)
650 playlist_free_entry(entry_to_delete);
651
652 /* Shift remaining entries to fill the gap */
653 memmove(playlist->entries + idx, playlist->entries + idx + 1,
654 (len - 1 - idx) * sizeof(struct playlist_entry));
655
656 RBUF_RESIZE(playlist->entries, len - 1);
657
658 playlist->modified = true;
659 }
660
661 /**
662 * playlist_delete_by_path:
663 * @playlist : Playlist handle.
664 * @search_path : Content path.
665 *
666 * Deletes all entries with content path
667 * matching 'search_path'
668 **/
playlist_delete_by_path(playlist_t * playlist,const char * search_path)669 void playlist_delete_by_path(playlist_t *playlist,
670 const char *search_path)
671 {
672 playlist_path_id_t *path_id = NULL;
673 size_t i = 0;
674
675 if (!playlist || string_is_empty(search_path))
676 return;
677
678 path_id = playlist_path_id_init(search_path);
679 if (!path_id)
680 return;
681
682 while (i < RBUF_LEN(playlist->entries))
683 {
684 if (!playlist_path_matches_entry(path_id,
685 &playlist->entries[i], &playlist->config))
686 {
687 i++;
688 continue;
689 }
690
691 /* Paths are equal - delete entry */
692 playlist_delete_index(playlist, i);
693
694 /* Entries are shifted up by the delete
695 * operation - *do not* increment i */
696 }
697
698 playlist_path_id_free(path_id);
699 }
700
playlist_get_index_by_path(playlist_t * playlist,const char * search_path,const struct playlist_entry ** entry)701 void playlist_get_index_by_path(playlist_t *playlist,
702 const char *search_path,
703 const struct playlist_entry **entry)
704 {
705 playlist_path_id_t *path_id = NULL;
706 size_t i, len;
707
708 if (!playlist || !entry || string_is_empty(search_path))
709 return;
710
711 path_id = playlist_path_id_init(search_path);
712 if (!path_id)
713 return;
714
715 for (i = 0, len = RBUF_LEN(playlist->entries); i < len; i++)
716 {
717 if (!playlist_path_matches_entry(path_id,
718 &playlist->entries[i], &playlist->config))
719 continue;
720
721 *entry = &playlist->entries[i];
722 break;
723 }
724
725 playlist_path_id_free(path_id);
726 }
727
playlist_entry_exists(playlist_t * playlist,const char * path)728 bool playlist_entry_exists(playlist_t *playlist,
729 const char *path)
730 {
731 playlist_path_id_t *path_id = NULL;
732 size_t i, len;
733
734 if (!playlist || string_is_empty(path))
735 return false;
736
737 path_id = playlist_path_id_init(path);
738 if (!path_id)
739 return false;
740
741 for (i = 0, len = RBUF_LEN(playlist->entries); i < len; i++)
742 {
743 if (playlist_path_matches_entry(path_id,
744 &playlist->entries[i], &playlist->config))
745 {
746 playlist_path_id_free(path_id);
747 return true;
748 }
749 }
750
751 playlist_path_id_free(path_id);
752 return false;
753 }
754
playlist_update(playlist_t * playlist,size_t idx,const struct playlist_entry * update_entry)755 void playlist_update(playlist_t *playlist, size_t idx,
756 const struct playlist_entry *update_entry)
757 {
758 struct playlist_entry *entry = NULL;
759
760 if (!playlist || idx >= RBUF_LEN(playlist->entries))
761 return;
762
763 entry = &playlist->entries[idx];
764
765 if (update_entry->path && (update_entry->path != entry->path))
766 {
767 if (entry->path)
768 free(entry->path);
769 entry->path = strdup(update_entry->path);
770
771 if (entry->path_id)
772 {
773 playlist_path_id_free(entry->path_id);
774 entry->path_id = NULL;
775 }
776
777 playlist->modified = true;
778 }
779
780 if (update_entry->label && (update_entry->label != entry->label))
781 {
782 if (entry->label)
783 free(entry->label);
784 entry->label = strdup(update_entry->label);
785 playlist->modified = true;
786 }
787
788 if (update_entry->core_path && (update_entry->core_path != entry->core_path))
789 {
790 if (entry->core_path)
791 free(entry->core_path);
792 entry->core_path = NULL;
793 entry->core_path = strdup(update_entry->core_path);
794 playlist->modified = true;
795 }
796
797 if (update_entry->core_name && (update_entry->core_name != entry->core_name))
798 {
799 if (entry->core_name)
800 free(entry->core_name);
801 entry->core_name = strdup(update_entry->core_name);
802 playlist->modified = true;
803 }
804
805 if (update_entry->db_name && (update_entry->db_name != entry->db_name))
806 {
807 if (entry->db_name)
808 free(entry->db_name);
809 entry->db_name = strdup(update_entry->db_name);
810 playlist->modified = true;
811 }
812
813 if (update_entry->crc32 && (update_entry->crc32 != entry->crc32))
814 {
815 if (entry->crc32)
816 free(entry->crc32);
817 entry->crc32 = strdup(update_entry->crc32);
818 playlist->modified = true;
819 }
820 }
821
playlist_update_runtime(playlist_t * playlist,size_t idx,const struct playlist_entry * update_entry,bool register_update)822 void playlist_update_runtime(playlist_t *playlist, size_t idx,
823 const struct playlist_entry *update_entry,
824 bool register_update)
825 {
826 struct playlist_entry *entry = NULL;
827
828 if (!playlist || idx >= RBUF_LEN(playlist->entries))
829 return;
830
831 entry = &playlist->entries[idx];
832
833 if (update_entry->path && (update_entry->path != entry->path))
834 {
835 if (entry->path)
836 free(entry->path);
837 entry->path = strdup(update_entry->path);
838
839 if (entry->path_id)
840 {
841 playlist_path_id_free(entry->path_id);
842 entry->path_id = NULL;
843 }
844
845 playlist->modified = playlist->modified || register_update;
846 }
847
848 if (update_entry->core_path && (update_entry->core_path != entry->core_path))
849 {
850 if (entry->core_path)
851 free(entry->core_path);
852 entry->core_path = NULL;
853 entry->core_path = strdup(update_entry->core_path);
854 playlist->modified = playlist->modified || register_update;
855 }
856
857 if (update_entry->runtime_status != entry->runtime_status)
858 {
859 entry->runtime_status = update_entry->runtime_status;
860 playlist->modified = playlist->modified || register_update;
861 }
862
863 if (update_entry->runtime_hours != entry->runtime_hours)
864 {
865 entry->runtime_hours = update_entry->runtime_hours;
866 playlist->modified = playlist->modified || register_update;
867 }
868
869 if (update_entry->runtime_minutes != entry->runtime_minutes)
870 {
871 entry->runtime_minutes = update_entry->runtime_minutes;
872 playlist->modified = playlist->modified || register_update;
873 }
874
875 if (update_entry->runtime_seconds != entry->runtime_seconds)
876 {
877 entry->runtime_seconds = update_entry->runtime_seconds;
878 playlist->modified = playlist->modified || register_update;
879 }
880
881 if (update_entry->last_played_year != entry->last_played_year)
882 {
883 entry->last_played_year = update_entry->last_played_year;
884 playlist->modified = playlist->modified || register_update;
885 }
886
887 if (update_entry->last_played_month != entry->last_played_month)
888 {
889 entry->last_played_month = update_entry->last_played_month;
890 playlist->modified = playlist->modified || register_update;
891 }
892
893 if (update_entry->last_played_day != entry->last_played_day)
894 {
895 entry->last_played_day = update_entry->last_played_day;
896 playlist->modified = playlist->modified || register_update;
897 }
898
899 if (update_entry->last_played_hour != entry->last_played_hour)
900 {
901 entry->last_played_hour = update_entry->last_played_hour;
902 playlist->modified = playlist->modified || register_update;
903 }
904
905 if (update_entry->last_played_minute != entry->last_played_minute)
906 {
907 entry->last_played_minute = update_entry->last_played_minute;
908 playlist->modified = playlist->modified || register_update;
909 }
910
911 if (update_entry->last_played_second != entry->last_played_second)
912 {
913 entry->last_played_second = update_entry->last_played_second;
914 playlist->modified = playlist->modified || register_update;
915 }
916
917 if (update_entry->runtime_str && (update_entry->runtime_str != entry->runtime_str))
918 {
919 if (entry->runtime_str)
920 free(entry->runtime_str);
921 entry->runtime_str = NULL;
922 entry->runtime_str = strdup(update_entry->runtime_str);
923 playlist->modified = playlist->modified || register_update;
924 }
925
926 if (update_entry->last_played_str && (update_entry->last_played_str != entry->last_played_str))
927 {
928 if (entry->last_played_str)
929 free(entry->last_played_str);
930 entry->last_played_str = NULL;
931 entry->last_played_str = strdup(update_entry->last_played_str);
932 playlist->modified = playlist->modified || register_update;
933 }
934 }
935
playlist_push_runtime(playlist_t * playlist,const struct playlist_entry * entry)936 bool playlist_push_runtime(playlist_t *playlist,
937 const struct playlist_entry *entry)
938 {
939 playlist_path_id_t *path_id = NULL;
940 size_t i, len;
941 char real_core_path[PATH_MAX_LENGTH];
942
943 if (!playlist || !entry)
944 goto error;
945
946 if (string_is_empty(entry->core_path))
947 {
948 RARCH_ERR("cannot push NULL or empty core path into the playlist.\n");
949 goto error;
950 }
951
952 real_core_path[0] = '\0';
953
954 /* Get path ID */
955 path_id = playlist_path_id_init(entry->path);
956 if (!path_id)
957 goto error;
958
959 /* Get 'real' core path */
960 strlcpy(real_core_path, entry->core_path, sizeof(real_core_path));
961 if (!string_is_equal(real_core_path, FILE_PATH_DETECT) &&
962 !string_is_equal(real_core_path, FILE_PATH_BUILTIN))
963 playlist_resolve_path(PLAYLIST_SAVE, true, real_core_path,
964 sizeof(real_core_path));
965
966 if (string_is_empty(real_core_path))
967 {
968 RARCH_ERR("cannot push NULL or empty core path into the playlist.\n");
969 goto error;
970 }
971
972 len = RBUF_LEN(playlist->entries);
973 for (i = 0; i < len; i++)
974 {
975 struct playlist_entry tmp;
976 bool equal_path = (string_is_empty(path_id->real_path) &&
977 string_is_empty(playlist->entries[i].path));
978
979 equal_path = equal_path || playlist_path_matches_entry(
980 path_id, &playlist->entries[i], &playlist->config);
981
982 if (!equal_path)
983 continue;
984
985 /* Core name can have changed while still being the same core.
986 * Differentiate based on the core path only. */
987 if (!playlist_core_path_equal(real_core_path, playlist->entries[i].core_path, &playlist->config))
988 continue;
989
990 /* If top entry, we don't want to push a new entry since
991 * the top and the entry to be pushed are the same. */
992 if (i == 0)
993 goto error;
994
995 /* Seen it before, bump to top. */
996 tmp = playlist->entries[i];
997 memmove(playlist->entries + 1, playlist->entries,
998 i * sizeof(struct playlist_entry));
999 playlist->entries[0] = tmp;
1000
1001 goto success;
1002 }
1003
1004 if (playlist->config.capacity == 0)
1005 goto error;
1006
1007 if (len == playlist->config.capacity)
1008 {
1009 struct playlist_entry *last_entry = &playlist->entries[len - 1];
1010 playlist_free_entry(last_entry);
1011 len--;
1012 }
1013 else
1014 {
1015 /* Allocate memory to fit one more item and resize the buffer */
1016 if (!RBUF_TRYFIT(playlist->entries, len + 1))
1017 goto error; /* out of memory */
1018 RBUF_RESIZE(playlist->entries, len + 1);
1019 }
1020
1021 if (playlist->entries)
1022 {
1023 memmove(playlist->entries + 1, playlist->entries,
1024 len * sizeof(struct playlist_entry));
1025
1026 playlist->entries[0].path = NULL;
1027 playlist->entries[0].core_path = NULL;
1028
1029 if (!string_is_empty(path_id->real_path))
1030 playlist->entries[0].path = strdup(path_id->real_path);
1031 playlist->entries[0].path_id = path_id;
1032 path_id = NULL;
1033
1034 if (!string_is_empty(real_core_path))
1035 playlist->entries[0].core_path = strdup(real_core_path);
1036
1037 playlist->entries[0].runtime_status = entry->runtime_status;
1038 playlist->entries[0].runtime_hours = entry->runtime_hours;
1039 playlist->entries[0].runtime_minutes = entry->runtime_minutes;
1040 playlist->entries[0].runtime_seconds = entry->runtime_seconds;
1041 playlist->entries[0].last_played_year = entry->last_played_year;
1042 playlist->entries[0].last_played_month = entry->last_played_month;
1043 playlist->entries[0].last_played_day = entry->last_played_day;
1044 playlist->entries[0].last_played_hour = entry->last_played_hour;
1045 playlist->entries[0].last_played_minute = entry->last_played_minute;
1046 playlist->entries[0].last_played_second = entry->last_played_second;
1047
1048 playlist->entries[0].runtime_str = NULL;
1049 playlist->entries[0].last_played_str = NULL;
1050
1051 if (!string_is_empty(entry->runtime_str))
1052 playlist->entries[0].runtime_str = strdup(entry->runtime_str);
1053 if (!string_is_empty(entry->last_played_str))
1054 playlist->entries[0].last_played_str = strdup(entry->last_played_str);
1055 }
1056
1057 success:
1058 if (path_id)
1059 playlist_path_id_free(path_id);
1060 playlist->modified = true;
1061 return true;
1062
1063 error:
1064 if (path_id)
1065 playlist_path_id_free(path_id);
1066 return false;
1067 }
1068
1069 /**
1070 * playlist_resolve_path:
1071 * @mode : PLAYLIST_LOAD or PLAYLIST_SAVE
1072 * @is_core : Set true if path to be resolved is a core file
1073 * @path : The path to be modified
1074 *
1075 * Resolves the path of an item, such as the content path or path to the core, to a format
1076 * appropriate for saving or loading depending on the @mode parameter
1077 *
1078 * Can be platform specific. File paths for saving can be abbreviated to avoid saving absolute
1079 * paths, as the base directory (home or application dir) may change after each subsequent
1080 * install (iOS)
1081 **/
playlist_resolve_path(enum playlist_file_mode mode,bool is_core,char * path,size_t len)1082 void playlist_resolve_path(enum playlist_file_mode mode,
1083 bool is_core, char *path, size_t len)
1084 {
1085 #ifdef HAVE_COCOATOUCH
1086 char tmp[PATH_MAX_LENGTH];
1087
1088 if (mode == PLAYLIST_LOAD)
1089 {
1090 fill_pathname_expand_special(tmp, path, sizeof(tmp));
1091 strlcpy(path, tmp, len);
1092 }
1093 else
1094 {
1095 /* iOS needs to call realpath here since the call
1096 * above fails due to possibly buffer related issues.
1097 * Try to expand the path to ensure that it gets saved
1098 * correctly. The path can be abbreviated if saving to
1099 * a playlist from another playlist (ex: content history to favorites)
1100 */
1101 char tmp2[PATH_MAX_LENGTH];
1102 fill_pathname_expand_special(tmp, path, sizeof(tmp));
1103 realpath(tmp, tmp2);
1104 fill_pathname_abbreviate_special(path, tmp2, len);
1105 }
1106 #else
1107 bool resolve_symlinks = true;
1108
1109 if (mode == PLAYLIST_LOAD)
1110 return;
1111
1112 #if defined(ANDROID)
1113 /* Can't resolve symlinks when dealing with cores
1114 * installed via play feature delivery, because the
1115 * source files have non-standard file names (which
1116 * will not be recognised by regular core handling
1117 * routines) */
1118 if (is_core)
1119 resolve_symlinks = !play_feature_delivery_enabled();
1120 #endif
1121
1122 path_resolve_realpath(path, len, resolve_symlinks);
1123 #endif
1124 }
1125
1126 /**
1127 * playlist_push:
1128 * @playlist : Playlist handle.
1129 *
1130 * Push entry to top of playlist.
1131 **/
playlist_push(playlist_t * playlist,const struct playlist_entry * entry)1132 bool playlist_push(playlist_t *playlist,
1133 const struct playlist_entry *entry)
1134 {
1135 size_t i, len;
1136 char real_core_path[PATH_MAX_LENGTH];
1137 playlist_path_id_t *path_id = NULL;
1138 const char *core_name = entry->core_name;
1139 bool entry_updated = false;
1140
1141 real_core_path[0] = '\0';
1142
1143 if (!playlist || !entry)
1144 goto error;
1145
1146 if (string_is_empty(entry->core_path))
1147 {
1148 RARCH_ERR("cannot push NULL or empty core path into the playlist.\n");
1149 goto error;
1150 }
1151
1152 /* Get path ID */
1153 path_id = playlist_path_id_init(entry->path);
1154 if (!path_id)
1155 goto error;
1156
1157 /* Get 'real' core path */
1158 strlcpy(real_core_path, entry->core_path, sizeof(real_core_path));
1159 if (!string_is_equal(real_core_path, FILE_PATH_DETECT) &&
1160 !string_is_equal(real_core_path, FILE_PATH_BUILTIN))
1161 playlist_resolve_path(PLAYLIST_SAVE, true, real_core_path,
1162 sizeof(real_core_path));
1163
1164 if (string_is_empty(real_core_path))
1165 {
1166 RARCH_ERR("cannot push NULL or empty core path into the playlist.\n");
1167 goto error;
1168 }
1169
1170 if (string_is_empty(core_name))
1171 {
1172 static char base_path[255] = {0};
1173 fill_pathname_base_noext(base_path, real_core_path, sizeof(base_path));
1174 core_name = base_path;
1175
1176 if (string_is_empty(core_name))
1177 {
1178 RARCH_ERR("cannot push NULL or empty core name into the playlist.\n");
1179 goto error;
1180 }
1181 }
1182
1183 len = RBUF_LEN(playlist->entries);
1184 for (i = 0; i < len; i++)
1185 {
1186 struct playlist_entry tmp;
1187 bool equal_path = (string_is_empty(path_id->real_path) &&
1188 string_is_empty(playlist->entries[i].path));
1189
1190 equal_path = equal_path || playlist_path_matches_entry(
1191 path_id, &playlist->entries[i], &playlist->config);
1192
1193 if (!equal_path)
1194 continue;
1195
1196 /* Core name can have changed while still being the same core.
1197 * Differentiate based on the core path only. */
1198 if (!playlist_core_path_equal(real_core_path, playlist->entries[i].core_path, &playlist->config))
1199 continue;
1200
1201 if ( !string_is_empty(entry->subsystem_ident)
1202 && !string_is_empty(playlist->entries[i].subsystem_ident)
1203 && !string_is_equal(playlist->entries[i].subsystem_ident, entry->subsystem_ident))
1204 continue;
1205
1206 if ( string_is_empty(entry->subsystem_ident)
1207 && !string_is_empty(playlist->entries[i].subsystem_ident))
1208 continue;
1209
1210 if ( !string_is_empty(entry->subsystem_ident)
1211 && string_is_empty(playlist->entries[i].subsystem_ident))
1212 continue;
1213
1214 if ( !string_is_empty(entry->subsystem_name)
1215 && !string_is_empty(playlist->entries[i].subsystem_name)
1216 && !string_is_equal(playlist->entries[i].subsystem_name, entry->subsystem_name))
1217 continue;
1218
1219 if ( string_is_empty(entry->subsystem_name)
1220 && !string_is_empty(playlist->entries[i].subsystem_name))
1221 continue;
1222
1223 if ( !string_is_empty(entry->subsystem_name)
1224 && string_is_empty(playlist->entries[i].subsystem_name))
1225 continue;
1226
1227 if (entry->subsystem_roms)
1228 {
1229 unsigned j;
1230 const struct string_list *roms = playlist->entries[i].subsystem_roms;
1231 bool unequal = false;
1232
1233 if (entry->subsystem_roms->size != roms->size)
1234 continue;
1235
1236 for (j = 0; j < entry->subsystem_roms->size; j++)
1237 {
1238 char real_rom_path[PATH_MAX_LENGTH];
1239
1240 real_rom_path[0] = '\0';
1241
1242 if (!string_is_empty(entry->subsystem_roms->elems[j].data))
1243 {
1244 strlcpy(real_rom_path, entry->subsystem_roms->elems[j].data, sizeof(real_rom_path));
1245 path_resolve_realpath(real_rom_path, sizeof(real_rom_path), true);
1246 }
1247
1248 if (!playlist_path_equal(real_rom_path, roms->elems[j].data,
1249 &playlist->config))
1250 {
1251 unequal = true;
1252 break;
1253 }
1254 }
1255
1256 if (unequal)
1257 continue;
1258 }
1259
1260 /* If content was previously loaded via file browser
1261 * or command line, certain entry values will be missing.
1262 * If we are now loading the same content from a playlist,
1263 * fill in any blanks */
1264 if (!playlist->entries[i].label && !string_is_empty(entry->label))
1265 {
1266 playlist->entries[i].label = strdup(entry->label);
1267 entry_updated = true;
1268 }
1269 if (!playlist->entries[i].crc32 && !string_is_empty(entry->crc32))
1270 {
1271 playlist->entries[i].crc32 = strdup(entry->crc32);
1272 entry_updated = true;
1273 }
1274 if (!playlist->entries[i].db_name && !string_is_empty(entry->db_name))
1275 {
1276 playlist->entries[i].db_name = strdup(entry->db_name);
1277 entry_updated = true;
1278 }
1279
1280 /* If top entry, we don't want to push a new entry since
1281 * the top and the entry to be pushed are the same. */
1282 if (i == 0)
1283 {
1284 if (entry_updated)
1285 goto success;
1286
1287 goto error;
1288 }
1289
1290 /* Seen it before, bump to top. */
1291 tmp = playlist->entries[i];
1292 memmove(playlist->entries + 1, playlist->entries,
1293 i * sizeof(struct playlist_entry));
1294 playlist->entries[0] = tmp;
1295
1296 goto success;
1297 }
1298
1299 if (playlist->config.capacity == 0)
1300 goto error;
1301
1302 if (len == playlist->config.capacity)
1303 {
1304 struct playlist_entry *last_entry = &playlist->entries[len - 1];
1305 playlist_free_entry(last_entry);
1306 len--;
1307 }
1308 else
1309 {
1310 /* Allocate memory to fit one more item and resize the buffer */
1311 if (!RBUF_TRYFIT(playlist->entries, len + 1))
1312 goto error; /* out of memory */
1313 RBUF_RESIZE(playlist->entries, len + 1);
1314 }
1315
1316 if (playlist->entries)
1317 {
1318 memmove(playlist->entries + 1, playlist->entries,
1319 len * sizeof(struct playlist_entry));
1320
1321 playlist->entries[0].path = NULL;
1322 playlist->entries[0].label = NULL;
1323 playlist->entries[0].core_path = NULL;
1324 playlist->entries[0].core_name = NULL;
1325 playlist->entries[0].db_name = NULL;
1326 playlist->entries[0].crc32 = NULL;
1327 playlist->entries[0].subsystem_ident = NULL;
1328 playlist->entries[0].subsystem_name = NULL;
1329 playlist->entries[0].runtime_str = NULL;
1330 playlist->entries[0].last_played_str = NULL;
1331 playlist->entries[0].subsystem_roms = NULL;
1332 playlist->entries[0].path_id = NULL;
1333 playlist->entries[0].runtime_status = PLAYLIST_RUNTIME_UNKNOWN;
1334 playlist->entries[0].runtime_hours = 0;
1335 playlist->entries[0].runtime_minutes = 0;
1336 playlist->entries[0].runtime_seconds = 0;
1337 playlist->entries[0].last_played_year = 0;
1338 playlist->entries[0].last_played_month = 0;
1339 playlist->entries[0].last_played_day = 0;
1340 playlist->entries[0].last_played_hour = 0;
1341 playlist->entries[0].last_played_minute = 0;
1342 playlist->entries[0].last_played_second = 0;
1343
1344 if (!string_is_empty(path_id->real_path))
1345 playlist->entries[0].path = strdup(path_id->real_path);
1346 playlist->entries[0].path_id = path_id;
1347 path_id = NULL;
1348
1349 if (!string_is_empty(entry->label))
1350 playlist->entries[0].label = strdup(entry->label);
1351 if (!string_is_empty(real_core_path))
1352 playlist->entries[0].core_path = strdup(real_core_path);
1353 if (!string_is_empty(core_name))
1354 playlist->entries[0].core_name = strdup(core_name);
1355 if (!string_is_empty(entry->db_name))
1356 playlist->entries[0].db_name = strdup(entry->db_name);
1357 if (!string_is_empty(entry->crc32))
1358 playlist->entries[0].crc32 = strdup(entry->crc32);
1359 if (!string_is_empty(entry->subsystem_ident))
1360 playlist->entries[0].subsystem_ident = strdup(entry->subsystem_ident);
1361 if (!string_is_empty(entry->subsystem_name))
1362 playlist->entries[0].subsystem_name = strdup(entry->subsystem_name);
1363
1364 if (entry->subsystem_roms)
1365 {
1366 union string_list_elem_attr attributes = {0};
1367
1368 playlist->entries[0].subsystem_roms = string_list_new();
1369
1370 for (i = 0; i < entry->subsystem_roms->size; i++)
1371 string_list_append(playlist->entries[0].subsystem_roms, entry->subsystem_roms->elems[i].data, attributes);
1372 }
1373 }
1374
1375 success:
1376 if (path_id)
1377 playlist_path_id_free(path_id);
1378 playlist->modified = true;
1379 return true;
1380
1381 error:
1382 if (path_id)
1383 playlist_path_id_free(path_id);
1384 return false;
1385 }
1386
playlist_write_runtime_file(playlist_t * playlist)1387 void playlist_write_runtime_file(playlist_t *playlist)
1388 {
1389 size_t i, len;
1390 intfstream_t *file = NULL;
1391 rjsonwriter_t* writer;
1392
1393 if (!playlist || !playlist->modified)
1394 return;
1395
1396 file = intfstream_open_file(playlist->config.path,
1397 RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE);
1398
1399 if (!file)
1400 {
1401 RARCH_ERR("Failed to write to playlist file: %s\n", playlist->config.path);
1402 return;
1403 }
1404
1405 writer = rjsonwriter_open_stream(file);
1406 if (!writer)
1407 {
1408 RARCH_ERR("Failed to create JSON writer\n");
1409 goto end;
1410 }
1411
1412 rjsonwriter_add_start_object(writer);
1413 rjsonwriter_add_newline(writer);
1414 rjsonwriter_add_spaces(writer, 2);
1415 rjsonwriter_add_string(writer, "version");
1416 rjsonwriter_add_colon(writer);
1417 rjsonwriter_add_space(writer);
1418 rjsonwriter_add_string(writer, "1.0");
1419 rjsonwriter_add_comma(writer);
1420 rjsonwriter_add_newline(writer);
1421 rjsonwriter_add_spaces(writer, 2);
1422 rjsonwriter_add_string(writer, "items");
1423 rjsonwriter_add_colon(writer);
1424 rjsonwriter_add_space(writer);
1425 rjsonwriter_add_start_array(writer);
1426 rjsonwriter_add_newline(writer);
1427
1428 for (i = 0, len = RBUF_LEN(playlist->entries); i < len; i++)
1429 {
1430 rjsonwriter_add_spaces(writer, 4);
1431 rjsonwriter_add_start_object(writer);
1432
1433 rjsonwriter_add_newline(writer);
1434 rjsonwriter_add_spaces(writer, 6);
1435 rjsonwriter_add_string(writer, "path");
1436 rjsonwriter_add_colon(writer);
1437 rjsonwriter_add_space(writer);
1438 rjsonwriter_add_string(writer, playlist->entries[i].path);
1439 rjsonwriter_add_comma(writer);
1440
1441 rjsonwriter_add_newline(writer);
1442 rjsonwriter_add_spaces(writer, 6);
1443 rjsonwriter_add_string(writer, "core_path");
1444 rjsonwriter_add_colon(writer);
1445 rjsonwriter_add_space(writer);
1446 rjsonwriter_add_string(writer, playlist->entries[i].core_path);
1447 rjsonwriter_add_comma(writer);
1448 rjsonwriter_add_newline(writer);
1449
1450 rjsonwriter_add_spaces(writer, 6);
1451 rjsonwriter_add_string(writer, "runtime_hours");
1452 rjsonwriter_add_colon(writer);
1453 rjsonwriter_add_space(writer);
1454 rjsonwriter_add_unsigned(writer, playlist->entries[i].runtime_hours);
1455 rjsonwriter_add_comma(writer);
1456 rjsonwriter_add_newline(writer);
1457
1458 rjsonwriter_add_spaces(writer, 6);
1459 rjsonwriter_add_string(writer, "runtime_minutes");
1460 rjsonwriter_add_colon(writer);
1461 rjsonwriter_add_space(writer);
1462 rjsonwriter_add_unsigned(writer, playlist->entries[i].runtime_minutes);
1463 rjsonwriter_add_comma(writer);
1464 rjsonwriter_add_newline(writer);
1465
1466 rjsonwriter_add_spaces(writer, 6);
1467 rjsonwriter_add_string(writer, "runtime_seconds");
1468 rjsonwriter_add_colon(writer);
1469 rjsonwriter_add_space(writer);
1470 rjsonwriter_add_unsigned(writer, playlist->entries[i].runtime_seconds);
1471 rjsonwriter_add_comma(writer);
1472 rjsonwriter_add_newline(writer);
1473
1474 rjsonwriter_add_spaces(writer, 6);
1475 rjsonwriter_add_string(writer, "last_played_year");
1476 rjsonwriter_add_colon(writer);
1477 rjsonwriter_add_space(writer);
1478 rjsonwriter_add_unsigned(writer, playlist->entries[i].last_played_year);
1479 rjsonwriter_add_comma(writer);
1480 rjsonwriter_add_newline(writer);
1481
1482 rjsonwriter_add_spaces(writer, 6);
1483 rjsonwriter_add_string(writer, "last_played_month");
1484 rjsonwriter_add_colon(writer);
1485 rjsonwriter_add_space(writer);
1486 rjsonwriter_add_unsigned(writer, playlist->entries[i].last_played_month);
1487 rjsonwriter_add_comma(writer);
1488 rjsonwriter_add_newline(writer);
1489
1490 rjsonwriter_add_spaces(writer, 6);
1491 rjsonwriter_add_string(writer, "last_played_day");
1492 rjsonwriter_add_colon(writer);
1493 rjsonwriter_add_space(writer);
1494 rjsonwriter_add_unsigned(writer, playlist->entries[i].last_played_day);
1495 rjsonwriter_add_comma(writer);
1496 rjsonwriter_add_newline(writer);
1497
1498 rjsonwriter_add_spaces(writer, 6);
1499 rjsonwriter_add_string(writer, "last_played_hour");
1500 rjsonwriter_add_colon(writer);
1501 rjsonwriter_add_space(writer);
1502 rjsonwriter_add_unsigned(writer, playlist->entries[i].last_played_hour);
1503 rjsonwriter_add_comma(writer);
1504 rjsonwriter_add_newline(writer);
1505
1506 rjsonwriter_add_spaces(writer, 6);
1507 rjsonwriter_add_string(writer, "last_played_minute");
1508 rjsonwriter_add_colon(writer);
1509 rjsonwriter_add_space(writer);
1510 rjsonwriter_add_unsigned(writer, playlist->entries[i].last_played_minute);
1511 rjsonwriter_add_comma(writer);
1512 rjsonwriter_add_newline(writer);
1513
1514 rjsonwriter_add_spaces(writer, 6);
1515 rjsonwriter_add_string(writer, "last_played_second");
1516 rjsonwriter_add_colon(writer);
1517 rjsonwriter_add_space(writer);
1518 rjsonwriter_add_unsigned(writer, playlist->entries[i].last_played_second);
1519 rjsonwriter_add_newline(writer);
1520
1521 rjsonwriter_add_spaces(writer, 4);
1522 rjsonwriter_add_end_object(writer);
1523
1524 if (i < len - 1)
1525 rjsonwriter_add_comma(writer);
1526
1527 rjsonwriter_add_newline(writer);
1528 }
1529
1530 rjsonwriter_add_spaces(writer, 2);
1531 rjsonwriter_add_end_array(writer);
1532 rjsonwriter_add_newline(writer);
1533 rjsonwriter_add_end_object(writer);
1534 rjsonwriter_add_newline(writer);
1535 rjsonwriter_free(writer);
1536
1537 playlist->modified = false;
1538 playlist->old_format = false;
1539 playlist->compressed = false;
1540
1541 RARCH_LOG("[Playlist]: Written to playlist file: %s\n", playlist->config.path);
1542 end:
1543 intfstream_close(file);
1544 free(file);
1545 }
1546
playlist_write_file(playlist_t * playlist)1547 void playlist_write_file(playlist_t *playlist)
1548 {
1549 size_t i, len;
1550 intfstream_t *file = NULL;
1551 bool compressed = false;
1552
1553 /* Playlist will be written if any of the
1554 * following are true:
1555 * > 'modified' flag is set
1556 * > Current playlist format (old/new) does not
1557 * match requested
1558 * > Current playlist compression status does
1559 * not match requested */
1560 if (!playlist ||
1561 !(playlist->modified ||
1562 #if defined(HAVE_ZLIB)
1563 (playlist->compressed != playlist->config.compress) ||
1564 #endif
1565 (playlist->old_format != playlist->config.old_format)))
1566 return;
1567
1568 #if defined(HAVE_ZLIB)
1569 if (playlist->config.compress)
1570 file = intfstream_open_rzip_file(playlist->config.path,
1571 RETRO_VFS_FILE_ACCESS_WRITE);
1572 else
1573 #endif
1574 file = intfstream_open_file(playlist->config.path,
1575 RETRO_VFS_FILE_ACCESS_WRITE,
1576 RETRO_VFS_FILE_ACCESS_HINT_NONE);
1577
1578 if (!file)
1579 {
1580 RARCH_ERR("Failed to write to playlist file: %s\n", playlist->config.path);
1581 return;
1582 }
1583
1584 /* Get current file compression state */
1585 compressed = intfstream_is_compressed(file);
1586
1587 #ifdef RARCH_INTERNAL
1588 if (playlist->config.old_format)
1589 {
1590 for (i = 0, len = RBUF_LEN(playlist->entries); i < len; i++)
1591 intfstream_printf(file, "%s\n%s\n%s\n%s\n%s\n%s\n",
1592 playlist->entries[i].path ? playlist->entries[i].path : "",
1593 playlist->entries[i].label ? playlist->entries[i].label : "",
1594 playlist->entries[i].core_path ? playlist->entries[i].core_path : "",
1595 playlist->entries[i].core_name ? playlist->entries[i].core_name : "",
1596 playlist->entries[i].crc32 ? playlist->entries[i].crc32 : "",
1597 playlist->entries[i].db_name ? playlist->entries[i].db_name : ""
1598 );
1599
1600 /* Add metadata lines
1601 * > We add these at the end of the file to prevent
1602 * breakage if the playlist is loaded with an older
1603 * version of RetroArch */
1604 intfstream_printf(
1605 file,
1606 "default_core_path = \"%s\"\n"
1607 "default_core_name = \"%s\"\n"
1608 "label_display_mode = \"%d\"\n"
1609 "thumbnail_mode = \"%d|%d\"\n"
1610 "sort_mode = \"%d\"\n",
1611 playlist->default_core_path ? playlist->default_core_path : "",
1612 playlist->default_core_name ? playlist->default_core_name : "",
1613 playlist->label_display_mode,
1614 playlist->right_thumbnail_mode, playlist->left_thumbnail_mode,
1615 playlist->sort_mode);
1616
1617 playlist->old_format = true;
1618 }
1619 else
1620 #endif
1621 {
1622 rjsonwriter_t* writer = rjsonwriter_open_stream(file);
1623 if (!writer)
1624 {
1625 RARCH_ERR("Failed to create JSON writer\n");
1626 goto end;
1627 }
1628 if (compressed)
1629 {
1630 /* When compressing playlists, human readability
1631 * is not a factor - can skip all indentation
1632 * and new line characters */
1633 rjsonwriter_set_options(writer, RJSONWRITER_OPTION_SKIP_WHITESPACE);
1634 }
1635
1636 rjsonwriter_add_start_object(writer);
1637 rjsonwriter_add_newline(writer);
1638
1639 rjsonwriter_add_spaces(writer, 2);
1640 rjsonwriter_add_string(writer, "version");
1641 rjsonwriter_add_colon(writer);
1642 rjsonwriter_add_space(writer);
1643 rjsonwriter_add_string(writer, "1.4");
1644 rjsonwriter_add_comma(writer);
1645 rjsonwriter_add_newline(writer);
1646
1647 rjsonwriter_add_spaces(writer, 2);
1648 rjsonwriter_add_string(writer, "default_core_path");
1649 rjsonwriter_add_colon(writer);
1650 rjsonwriter_add_space(writer);
1651 rjsonwriter_add_string(writer, playlist->default_core_path);
1652 rjsonwriter_add_comma(writer);
1653 rjsonwriter_add_newline(writer);
1654
1655 rjsonwriter_add_spaces(writer, 2);
1656 rjsonwriter_add_string(writer, "default_core_name");
1657 rjsonwriter_add_colon(writer);
1658 rjsonwriter_add_space(writer);
1659 rjsonwriter_add_string(writer, playlist->default_core_name);
1660 rjsonwriter_add_comma(writer);
1661 rjsonwriter_add_newline(writer);
1662
1663 if (!string_is_empty(playlist->base_content_directory))
1664 {
1665 rjsonwriter_add_spaces(writer, 2);
1666 rjsonwriter_add_string(writer, "base_content_directory");
1667 rjsonwriter_add_colon(writer);
1668 rjsonwriter_add_space(writer);
1669 rjsonwriter_add_string(writer, playlist->base_content_directory);
1670 rjsonwriter_add_comma(writer);
1671 rjsonwriter_add_newline(writer);
1672 }
1673
1674 rjsonwriter_add_spaces(writer, 2);
1675 rjsonwriter_add_string(writer, "label_display_mode");
1676 rjsonwriter_add_colon(writer);
1677 rjsonwriter_add_space(writer);
1678 rjsonwriter_add_int(writer, (int)playlist->label_display_mode);
1679 rjsonwriter_add_comma(writer);
1680 rjsonwriter_add_newline(writer);
1681
1682 rjsonwriter_add_spaces(writer, 2);
1683 rjsonwriter_add_string(writer, "right_thumbnail_mode");
1684 rjsonwriter_add_colon(writer);
1685 rjsonwriter_add_space(writer);
1686 rjsonwriter_add_int(writer, (int)playlist->right_thumbnail_mode);
1687 rjsonwriter_add_comma(writer);
1688 rjsonwriter_add_newline(writer);
1689
1690 rjsonwriter_add_spaces(writer, 2);
1691 rjsonwriter_add_string(writer, "left_thumbnail_mode");
1692 rjsonwriter_add_colon(writer);
1693 rjsonwriter_add_space(writer);
1694 rjsonwriter_add_int(writer, (int)playlist->left_thumbnail_mode);
1695 rjsonwriter_add_comma(writer);
1696 rjsonwriter_add_newline(writer);
1697
1698 rjsonwriter_add_spaces(writer, 2);
1699 rjsonwriter_add_string(writer, "sort_mode");
1700 rjsonwriter_add_colon(writer);
1701 rjsonwriter_add_space(writer);
1702 rjsonwriter_add_int(writer, (int)playlist->sort_mode);
1703 rjsonwriter_add_comma(writer);
1704 rjsonwriter_add_newline(writer);
1705
1706 rjsonwriter_add_spaces(writer, 2);
1707 rjsonwriter_add_string(writer, "items");
1708 rjsonwriter_add_colon(writer);
1709 rjsonwriter_add_space(writer);
1710 rjsonwriter_add_start_array(writer);
1711 rjsonwriter_add_newline(writer);
1712
1713 for (i = 0, len = RBUF_LEN(playlist->entries); i < len; i++)
1714 {
1715 rjsonwriter_add_spaces(writer, 4);
1716 rjsonwriter_add_start_object(writer);
1717
1718 rjsonwriter_add_newline(writer);
1719 rjsonwriter_add_spaces(writer, 6);
1720 rjsonwriter_add_string(writer, "path");
1721 rjsonwriter_add_colon(writer);
1722 rjsonwriter_add_space(writer);
1723 rjsonwriter_add_string(writer, playlist->entries[i].path);
1724 rjsonwriter_add_comma(writer);
1725
1726 rjsonwriter_add_newline(writer);
1727 rjsonwriter_add_spaces(writer, 6);
1728 rjsonwriter_add_string(writer, "label");
1729 rjsonwriter_add_colon(writer);
1730 rjsonwriter_add_space(writer);
1731 rjsonwriter_add_string(writer, playlist->entries[i].label);
1732 rjsonwriter_add_comma(writer);
1733
1734 rjsonwriter_add_newline(writer);
1735 rjsonwriter_add_spaces(writer, 6);
1736 rjsonwriter_add_string(writer, "core_path");
1737 rjsonwriter_add_colon(writer);
1738 rjsonwriter_add_space(writer);
1739 rjsonwriter_add_string(writer, playlist->entries[i].core_path);
1740 rjsonwriter_add_comma(writer);
1741
1742 rjsonwriter_add_newline(writer);
1743 rjsonwriter_add_spaces(writer, 6);
1744 rjsonwriter_add_string(writer, "core_name");
1745 rjsonwriter_add_colon(writer);
1746 rjsonwriter_add_space(writer);
1747 rjsonwriter_add_string(writer, playlist->entries[i].core_name);
1748 rjsonwriter_add_comma(writer);
1749
1750 rjsonwriter_add_newline(writer);
1751 rjsonwriter_add_spaces(writer, 6);
1752 rjsonwriter_add_string(writer, "crc32");
1753 rjsonwriter_add_colon(writer);
1754 rjsonwriter_add_space(writer);
1755 rjsonwriter_add_string(writer, playlist->entries[i].crc32);
1756 rjsonwriter_add_comma(writer);
1757
1758 rjsonwriter_add_newline(writer);
1759 rjsonwriter_add_spaces(writer, 6);
1760 rjsonwriter_add_string(writer, "db_name");
1761 rjsonwriter_add_colon(writer);
1762 rjsonwriter_add_space(writer);
1763 rjsonwriter_add_string(writer, playlist->entries[i].db_name);
1764
1765 if (!string_is_empty(playlist->entries[i].subsystem_ident))
1766 {
1767 rjsonwriter_add_comma(writer);
1768 rjsonwriter_add_newline(writer);
1769 rjsonwriter_add_spaces(writer, 6);
1770 rjsonwriter_add_string(writer, "subsystem_ident");
1771 rjsonwriter_add_colon(writer);
1772 rjsonwriter_add_space(writer);
1773 rjsonwriter_add_string(writer, playlist->entries[i].subsystem_ident);
1774 }
1775
1776 if (!string_is_empty(playlist->entries[i].subsystem_name))
1777 {
1778 rjsonwriter_add_comma(writer);
1779 rjsonwriter_add_newline(writer);
1780 rjsonwriter_add_spaces(writer, 6);
1781 rjsonwriter_add_string(writer, "subsystem_name");
1782 rjsonwriter_add_colon(writer);
1783 rjsonwriter_add_space(writer);
1784 rjsonwriter_add_string(writer, playlist->entries[i].subsystem_name);
1785 }
1786
1787 if ( playlist->entries[i].subsystem_roms &&
1788 playlist->entries[i].subsystem_roms->size > 0)
1789 {
1790 unsigned j;
1791
1792 rjsonwriter_add_comma(writer);
1793 rjsonwriter_add_newline(writer);
1794 rjsonwriter_add_spaces(writer, 6);
1795 rjsonwriter_add_string(writer, "subsystem_roms");
1796 rjsonwriter_add_colon(writer);
1797 rjsonwriter_add_space(writer);
1798 rjsonwriter_add_start_array(writer);
1799 rjsonwriter_add_newline(writer);
1800
1801 for (j = 0; j < playlist->entries[i].subsystem_roms->size; j++)
1802 {
1803 const struct string_list *roms = playlist->entries[i].subsystem_roms;
1804 rjsonwriter_add_spaces(writer, 8);
1805 rjsonwriter_add_string(writer,
1806 !string_is_empty(roms->elems[j].data)
1807 ? roms->elems[j].data
1808 : "");
1809
1810 if (j < playlist->entries[i].subsystem_roms->size - 1)
1811 {
1812 rjsonwriter_add_comma(writer);
1813 rjsonwriter_add_newline(writer);
1814 }
1815 }
1816
1817 rjsonwriter_add_newline(writer);
1818 rjsonwriter_add_spaces(writer, 6);
1819 rjsonwriter_add_end_array(writer);
1820 }
1821
1822 rjsonwriter_add_newline(writer);
1823
1824 rjsonwriter_add_spaces(writer, 4);
1825 rjsonwriter_add_end_object(writer);
1826
1827 if (i < len - 1)
1828 rjsonwriter_add_comma(writer);
1829
1830 rjsonwriter_add_newline(writer);
1831 }
1832
1833 rjsonwriter_add_spaces(writer, 2);
1834 rjsonwriter_add_end_array(writer);
1835 rjsonwriter_add_newline(writer);
1836 rjsonwriter_add_end_object(writer);
1837 rjsonwriter_add_newline(writer);
1838
1839 if (!rjsonwriter_free(writer))
1840 {
1841 RARCH_ERR("Failed to write to playlist file: %s\n", playlist->config.path);
1842 }
1843
1844 playlist->old_format = false;
1845 }
1846
1847 playlist->modified = false;
1848 playlist->compressed = compressed;
1849
1850 RARCH_LOG("[Playlist]: Written to playlist file: %s\n", playlist->config.path);
1851 end:
1852 intfstream_close(file);
1853 free(file);
1854 }
1855
1856 /**
1857 * playlist_free:
1858 * @playlist : Playlist handle.
1859 *
1860 * Frees playlist handle.
1861 */
playlist_free(playlist_t * playlist)1862 void playlist_free(playlist_t *playlist)
1863 {
1864 size_t i, len;
1865
1866 if (!playlist)
1867 return;
1868
1869 if (playlist->default_core_path)
1870 free(playlist->default_core_path);
1871 playlist->default_core_path = NULL;
1872
1873 if (playlist->default_core_name)
1874 free(playlist->default_core_name);
1875 playlist->default_core_name = NULL;
1876
1877 if (playlist->base_content_directory)
1878 free(playlist->base_content_directory);
1879 playlist->base_content_directory = NULL;
1880
1881 if (playlist->entries)
1882 {
1883 for (i = 0, len = RBUF_LEN(playlist->entries); i < len; i++)
1884 {
1885 struct playlist_entry *entry = &playlist->entries[i];
1886
1887 if (entry)
1888 playlist_free_entry(entry);
1889 }
1890
1891 RBUF_FREE(playlist->entries);
1892 }
1893
1894 free(playlist);
1895 }
1896
1897 /**
1898 * playlist_clear:
1899 * @playlist : Playlist handle.
1900 *
1901 * Clears all playlist entries in playlist.
1902 **/
playlist_clear(playlist_t * playlist)1903 void playlist_clear(playlist_t *playlist)
1904 {
1905 size_t i, len;
1906 if (!playlist)
1907 return;
1908
1909 for (i = 0, len = RBUF_LEN(playlist->entries); i < len; i++)
1910 {
1911 struct playlist_entry *entry = &playlist->entries[i];
1912
1913 if (entry)
1914 playlist_free_entry(entry);
1915 }
1916 RBUF_CLEAR(playlist->entries);
1917 }
1918
1919 /**
1920 * playlist_size:
1921 * @playlist : Playlist handle.
1922 *
1923 * Gets size of playlist.
1924 * Returns: size of playlist.
1925 **/
playlist_size(playlist_t * playlist)1926 size_t playlist_size(playlist_t *playlist)
1927 {
1928 if (!playlist)
1929 return 0;
1930 return RBUF_LEN(playlist->entries);
1931 }
1932
1933 /**
1934 * playlist_capacity:
1935 * @playlist : Playlist handle.
1936 *
1937 * Gets maximum capacity of playlist.
1938 * Returns: maximum capacity of playlist.
1939 **/
playlist_capacity(playlist_t * playlist)1940 size_t playlist_capacity(playlist_t *playlist)
1941 {
1942 if (!playlist)
1943 return 0;
1944 return playlist->config.capacity;
1945 }
1946
JSONStartArrayHandler(void * context)1947 static bool JSONStartArrayHandler(void *context)
1948 {
1949 JSONContext *pCtx = (JSONContext *)context;
1950
1951 pCtx->array_depth++;
1952
1953 return true;
1954 }
1955
JSONEndArrayHandler(void * context)1956 static bool JSONEndArrayHandler(void *context)
1957 {
1958 JSONContext *pCtx = (JSONContext *)context;
1959
1960 retro_assert(pCtx->array_depth > 0);
1961
1962 pCtx->array_depth--;
1963
1964 if (pCtx->in_items && pCtx->array_depth == 0 && pCtx->object_depth <= 1)
1965 {
1966 pCtx->in_items = false;
1967 }
1968 else if (pCtx->in_subsystem_roms && pCtx->array_depth <= 1 && pCtx->object_depth <= 2)
1969 {
1970 pCtx->in_subsystem_roms = false;
1971 }
1972
1973 return true;
1974 }
1975
JSONStartObjectHandler(void * context)1976 static bool JSONStartObjectHandler(void *context)
1977 {
1978 JSONContext *pCtx = (JSONContext *)context;
1979
1980 pCtx->object_depth++;
1981
1982 if (pCtx->in_items && pCtx->object_depth == 2)
1983 {
1984 if ((pCtx->array_depth == 1) && !pCtx->capacity_exceeded)
1985 {
1986 size_t len = RBUF_LEN(pCtx->playlist->entries);
1987 if (len < pCtx->playlist->config.capacity)
1988 {
1989 /* Allocate memory to fit one more item but don't resize the
1990 * buffer just yet, wait until JSONEndObjectHandler for that */
1991 if (!RBUF_TRYFIT(pCtx->playlist->entries, len + 1))
1992 {
1993 pCtx->out_of_memory = true;
1994 return false;
1995 }
1996 pCtx->current_entry = &pCtx->playlist->entries[len];
1997 memset(pCtx->current_entry, 0, sizeof(*pCtx->current_entry));
1998 }
1999 else
2000 {
2001 /* Hit max item limit.
2002 * Note: We can't just abort here, since there may
2003 * be more metadata to read at the end of the file... */
2004 RARCH_WARN("JSON file contains more entries than current playlist capacity. Excess entries will be discarded.\n");
2005 pCtx->capacity_exceeded = true;
2006 pCtx->current_entry = NULL;
2007 /* In addition, since we are discarding excess entries,
2008 * the playlist must be flagged as being modified
2009 * (i.e. the playlist is not the same as when it was
2010 * last saved to disk...) */
2011 pCtx->playlist->modified = true;
2012 }
2013 }
2014 }
2015
2016 return true;
2017 }
2018
JSONEndObjectHandler(void * context)2019 static bool JSONEndObjectHandler(void *context)
2020 {
2021 JSONContext *pCtx = (JSONContext *)context;
2022
2023 if (pCtx->in_items && pCtx->object_depth == 2)
2024 {
2025 if ((pCtx->array_depth == 1) && !pCtx->capacity_exceeded)
2026 RBUF_RESIZE(pCtx->playlist->entries,
2027 RBUF_LEN(pCtx->playlist->entries) + 1);
2028 }
2029
2030 retro_assert(pCtx->object_depth > 0);
2031
2032 pCtx->object_depth--;
2033
2034 return true;
2035 }
2036
JSONStringHandler(void * context,const char * pValue,size_t length)2037 static bool JSONStringHandler(void *context, const char *pValue, size_t length)
2038 {
2039 JSONContext *pCtx = (JSONContext *)context;
2040
2041 if (pCtx->in_items && pCtx->in_subsystem_roms && pCtx->object_depth == 2 && pCtx->array_depth == 2)
2042 {
2043 if (length && !string_is_empty(pValue))
2044 {
2045 union string_list_elem_attr attr = {0};
2046
2047 if (!pCtx->current_entry->subsystem_roms)
2048 pCtx->current_entry->subsystem_roms = string_list_new();
2049
2050 string_list_append(pCtx->current_entry->subsystem_roms, pValue, attr);
2051 }
2052 }
2053 else if (pCtx->in_items && pCtx->object_depth == 2)
2054 {
2055 if (pCtx->array_depth == 1)
2056 {
2057 if (pCtx->current_string_val && length && !string_is_empty(pValue))
2058 {
2059 if (*pCtx->current_string_val)
2060 free(*pCtx->current_string_val);
2061 *pCtx->current_string_val = strdup(pValue);
2062 }
2063 }
2064 }
2065 else if (pCtx->object_depth == 1)
2066 {
2067 if (pCtx->array_depth == 0)
2068 {
2069 if (pCtx->current_string_val && length && !string_is_empty(pValue))
2070 {
2071 /* handle any top-level playlist metadata here */
2072 if (*pCtx->current_string_val)
2073 free(*pCtx->current_string_val);
2074 *pCtx->current_string_val = strdup(pValue);
2075 }
2076 }
2077 }
2078
2079 pCtx->current_string_val = NULL;
2080
2081 return true;
2082 }
2083
JSONNumberHandler(void * context,const char * pValue,size_t length)2084 static bool JSONNumberHandler(void *context, const char *pValue, size_t length)
2085 {
2086 JSONContext *pCtx = (JSONContext *)context;
2087
2088 if (pCtx->in_items && pCtx->object_depth == 2)
2089 {
2090 if (pCtx->array_depth == 1 && length && !string_is_empty(pValue))
2091 {
2092 if (pCtx->current_entry_uint_val)
2093 *pCtx->current_entry_uint_val = (unsigned)strtoul(pValue, NULL, 10);
2094 }
2095 }
2096 else if (pCtx->object_depth == 1)
2097 {
2098 if (pCtx->array_depth == 0)
2099 {
2100 if (length && !string_is_empty(pValue))
2101 {
2102 /* handle any top-level playlist metadata here */
2103 if (pCtx->current_meta_label_display_mode_val)
2104 *pCtx->current_meta_label_display_mode_val = (enum playlist_label_display_mode)strtoul(pValue, NULL, 10);
2105 else if (pCtx->current_meta_thumbnail_mode_val)
2106 *pCtx->current_meta_thumbnail_mode_val = (enum playlist_thumbnail_mode)strtoul(pValue, NULL, 10);
2107 else if (pCtx->current_meta_sort_mode_val)
2108 *pCtx->current_meta_sort_mode_val = (enum playlist_sort_mode)strtoul(pValue, NULL, 10);
2109 }
2110 }
2111 }
2112
2113 pCtx->current_entry_uint_val = NULL;
2114 pCtx->current_meta_label_display_mode_val = NULL;
2115 pCtx->current_meta_thumbnail_mode_val = NULL;
2116 pCtx->current_meta_sort_mode_val = NULL;
2117
2118 return true;
2119 }
2120
JSONObjectMemberHandler(void * context,const char * pValue,size_t length)2121 static bool JSONObjectMemberHandler(void *context, const char *pValue, size_t length)
2122 {
2123 JSONContext *pCtx = (JSONContext *)context;
2124
2125 if (pCtx->in_items && pCtx->object_depth == 2)
2126 {
2127 if (pCtx->array_depth == 1)
2128 {
2129 if (pCtx->current_string_val)
2130 {
2131 /* something went wrong */
2132 return false;
2133 }
2134
2135 if (length && !pCtx->capacity_exceeded)
2136 {
2137 pCtx->current_string_val = NULL;
2138 pCtx->current_entry_uint_val = NULL;
2139 pCtx->in_subsystem_roms = false;
2140 switch (pValue[0])
2141 {
2142 case 'c':
2143 if (string_is_equal(pValue, "core_name"))
2144 pCtx->current_string_val = &pCtx->current_entry->core_name;
2145 else if (string_is_equal(pValue, "core_path"))
2146 pCtx->current_string_val = &pCtx->current_entry->core_path;
2147 else if (string_is_equal(pValue, "crc32"))
2148 pCtx->current_string_val = &pCtx->current_entry->crc32;
2149 break;
2150 case 'd':
2151 if (string_is_equal(pValue, "db_name"))
2152 pCtx->current_string_val = &pCtx->current_entry->db_name;
2153 break;
2154 case 'l':
2155 if (string_is_equal(pValue, "label"))
2156 pCtx->current_string_val = &pCtx->current_entry->label;
2157 else if (string_is_equal(pValue, "last_played_day"))
2158 pCtx->current_entry_uint_val = &pCtx->current_entry->last_played_day;
2159 else if (string_is_equal(pValue, "last_played_hour"))
2160 pCtx->current_entry_uint_val = &pCtx->current_entry->last_played_hour;
2161 else if (string_is_equal(pValue, "last_played_minute"))
2162 pCtx->current_entry_uint_val = &pCtx->current_entry->last_played_minute;
2163 else if (string_is_equal(pValue, "last_played_month"))
2164 pCtx->current_entry_uint_val = &pCtx->current_entry->last_played_month;
2165 else if (string_is_equal(pValue, "last_played_second"))
2166 pCtx->current_entry_uint_val = &pCtx->current_entry->last_played_second;
2167 else if (string_is_equal(pValue, "last_played_year"))
2168 pCtx->current_entry_uint_val = &pCtx->current_entry->last_played_year;
2169 break;
2170 case 'p':
2171 if (string_is_equal(pValue, "path"))
2172 pCtx->current_string_val = &pCtx->current_entry->path;
2173 break;
2174 case 'r':
2175 if (string_is_equal(pValue, "runtime_hours"))
2176 pCtx->current_entry_uint_val = &pCtx->current_entry->runtime_hours;
2177 else if (string_is_equal(pValue, "runtime_minutes"))
2178 pCtx->current_entry_uint_val = &pCtx->current_entry->runtime_minutes;
2179 else if (string_is_equal(pValue, "runtime_seconds"))
2180 pCtx->current_entry_uint_val = &pCtx->current_entry->runtime_seconds;
2181 break;
2182 case 's':
2183 if (string_is_equal(pValue, "subsystem_ident"))
2184 pCtx->current_string_val = &pCtx->current_entry->subsystem_ident;
2185 else if (string_is_equal(pValue, "subsystem_name"))
2186 pCtx->current_string_val = &pCtx->current_entry->subsystem_name;
2187 else if (string_is_equal(pValue, "subsystem_roms"))
2188 pCtx->in_subsystem_roms = true;
2189 break;
2190 }
2191 }
2192 }
2193 }
2194 else if (pCtx->object_depth == 1 && pCtx->array_depth == 0 && length)
2195 {
2196 pCtx->current_string_val = NULL;
2197 pCtx->current_meta_label_display_mode_val = NULL;
2198 pCtx->current_meta_thumbnail_mode_val = NULL;
2199 pCtx->current_meta_sort_mode_val = NULL;
2200 pCtx->in_items = false;
2201 switch (pValue[0])
2202 {
2203 case 'b':
2204 if (string_is_equal(pValue, "base_content_directory"))
2205 pCtx->current_string_val = &pCtx->playlist->base_content_directory;
2206 break;
2207 case 'd':
2208 if (string_is_equal(pValue, "default_core_path"))
2209 pCtx->current_string_val = &pCtx->playlist->default_core_path;
2210 else if (string_is_equal(pValue, "default_core_name"))
2211 pCtx->current_string_val = &pCtx->playlist->default_core_name;
2212 break;
2213 case 'i':
2214 if (string_is_equal(pValue, "items"))
2215 pCtx->in_items = true;
2216 break;
2217 case 'l':
2218 if (string_is_equal(pValue, "label_display_mode"))
2219 pCtx->current_meta_label_display_mode_val = &pCtx->playlist->label_display_mode;
2220 else if (string_is_equal(pValue, "left_thumbnail_mode"))
2221 pCtx->current_meta_thumbnail_mode_val = &pCtx->playlist->left_thumbnail_mode;
2222 break;
2223 case 'r':
2224 if (string_is_equal(pValue, "right_thumbnail_mode"))
2225 pCtx->current_meta_thumbnail_mode_val = &pCtx->playlist->right_thumbnail_mode;
2226 break;
2227 case 's':
2228 if (string_is_equal(pValue, "sort_mode"))
2229 pCtx->current_meta_sort_mode_val = &pCtx->playlist->sort_mode;
2230 break;
2231 }
2232 }
2233
2234 return true;
2235 }
2236
get_old_format_metadata_value(char * metadata_line,char * value,size_t len)2237 static void get_old_format_metadata_value(
2238 char *metadata_line, char *value, size_t len)
2239 {
2240 char *end = NULL;
2241 char *start = strchr(metadata_line, '\"');
2242
2243 if (!start)
2244 return;
2245
2246 start++;
2247 end = strchr(start, '\"');
2248
2249 if (!end)
2250 return;
2251
2252 *end = '\0';
2253 strlcpy(value, start, len);
2254 }
2255
playlist_read_file(playlist_t * playlist)2256 static bool playlist_read_file(playlist_t *playlist)
2257 {
2258 unsigned i;
2259 int test_char;
2260 bool res = true;
2261
2262 #if defined(HAVE_ZLIB)
2263 /* Always use RZIP interface when reading playlists
2264 * > this will automatically handle uncompressed
2265 * data */
2266 intfstream_t *file = intfstream_open_rzip_file(
2267 playlist->config.path,
2268 RETRO_VFS_FILE_ACCESS_READ);
2269 #else
2270 intfstream_t *file = intfstream_open_file(
2271 playlist->config.path,
2272 RETRO_VFS_FILE_ACCESS_READ,
2273 RETRO_VFS_FILE_ACCESS_HINT_NONE);
2274 #endif
2275
2276 /* If playlist file does not exist,
2277 * create an empty playlist instead */
2278 if (!file)
2279 return true;
2280
2281 playlist->compressed = intfstream_is_compressed(file);
2282
2283 /* Detect format of playlist
2284 * > Read file until we find the first printable
2285 * non-whitespace ASCII character */
2286 do
2287 {
2288 test_char = intfstream_getc(file);
2289
2290 if (test_char == EOF) /* read error or end of file */
2291 goto end;
2292 }while (!isgraph(test_char) || test_char > 0x7F);
2293
2294 if (test_char == '{')
2295 {
2296 /* New playlist format detected */
2297 #if 0
2298 RARCH_LOG("[Playlist]: New playlist format detected.\n");
2299 #endif
2300 playlist->old_format = false;
2301 }
2302 else
2303 {
2304 /* old playlist format detected */
2305 #if 0
2306 RARCH_LOG("[Playlist]: Old playlist format detected.\n");
2307 #endif
2308 playlist->old_format = true;
2309 }
2310
2311 /* Reset file to start */
2312 intfstream_rewind(file);
2313
2314 if (!playlist->old_format)
2315 {
2316 rjson_t* parser;
2317 JSONContext context = {0};
2318 context.playlist = playlist;
2319
2320 parser = rjson_open_stream(file);
2321 if (!parser)
2322 {
2323 RARCH_ERR("Failed to create JSON parser\n");
2324 goto end;
2325 }
2326
2327 rjson_set_options(parser,
2328 RJSON_OPTION_ALLOW_UTF8BOM
2329 | RJSON_OPTION_ALLOW_COMMENTS
2330 | RJSON_OPTION_ALLOW_UNESCAPED_CONTROL_CHARACTERS
2331 | RJSON_OPTION_REPLACE_INVALID_ENCODING);
2332
2333 if (rjson_parse(parser, &context,
2334 JSONObjectMemberHandler,
2335 JSONStringHandler,
2336 JSONNumberHandler,
2337 JSONStartObjectHandler,
2338 JSONEndObjectHandler,
2339 JSONStartArrayHandler,
2340 JSONEndArrayHandler,
2341 NULL, NULL) /* unused boolean/null handlers */
2342 != RJSON_DONE)
2343 {
2344 if (context.out_of_memory)
2345 {
2346 RARCH_WARN("Ran out of memory while parsing JSON playlist\n");
2347 res = false;
2348 }
2349 else
2350 {
2351 RARCH_WARN("Error parsing chunk:\n---snip---\n%.*s\n---snip---\n",
2352 rjson_get_source_context_len(parser),
2353 rjson_get_source_context_buf(parser));
2354 RARCH_WARN("Error: Invalid JSON at line %d, column %d - %s.\n",
2355 (int)rjson_get_source_line(parser),
2356 (int)rjson_get_source_column(parser),
2357 (*rjson_get_error(parser) ? rjson_get_error(parser) : "format error"));
2358 }
2359 }
2360 rjson_free(parser);
2361 }
2362 else
2363 {
2364 size_t len = RBUF_LEN(playlist->entries);
2365 char line_buf[PLAYLIST_ENTRIES][PATH_MAX_LENGTH] = {{0}};
2366
2367 /* Unnecessary, but harmless */
2368 for (i = 0; i < PLAYLIST_ENTRIES; i++)
2369 line_buf[i][0] = '\0';
2370
2371 /* Read playlist entries */
2372 while (len < playlist->config.capacity)
2373 {
2374 size_t i;
2375 size_t lines_read = 0;
2376
2377 /* Attempt to read the next 'PLAYLIST_ENTRIES'
2378 * lines from the file */
2379 for (i = 0; i < PLAYLIST_ENTRIES; i++)
2380 {
2381 *line_buf[i] = '\0';
2382
2383 if (intfstream_gets(file, line_buf[i], sizeof(line_buf[i])))
2384 {
2385 /* Ensure line is NUL terminated, regardless of
2386 * Windows or Unix line endings */
2387 string_replace_all_chars(line_buf[i], '\r', '\0');
2388 string_replace_all_chars(line_buf[i], '\n', '\0');
2389
2390 lines_read++;
2391 }
2392 else
2393 break;
2394 }
2395
2396 /* If a 'full set' of lines were read, then this
2397 * is a valid playlist entry */
2398 if (lines_read >= PLAYLIST_ENTRIES)
2399 {
2400 struct playlist_entry* entry;
2401
2402 if (!RBUF_TRYFIT(playlist->entries, len + 1))
2403 {
2404 res = false; /* out of memory */
2405 goto end;
2406 }
2407 RBUF_RESIZE(playlist->entries, len + 1);
2408 entry = &playlist->entries[len++];
2409
2410 memset(entry, 0, sizeof(*entry));
2411
2412 /* path */
2413 if (!string_is_empty(line_buf[0]))
2414 entry->path = strdup(line_buf[0]);
2415
2416 /* label */
2417 if (!string_is_empty(line_buf[1]))
2418 entry->label = strdup(line_buf[1]);
2419
2420 /* core_path */
2421 if (!string_is_empty(line_buf[2]))
2422 entry->core_path = strdup(line_buf[2]);
2423
2424 /* core_name */
2425 if (!string_is_empty(line_buf[3]))
2426 entry->core_name = strdup(line_buf[3]);
2427
2428 /* crc32 */
2429 if (!string_is_empty(line_buf[4]))
2430 entry->crc32 = strdup(line_buf[4]);
2431
2432 /* db_name */
2433 if (!string_is_empty(line_buf[5]))
2434 entry->db_name = strdup(line_buf[5]);
2435 }
2436 /* If fewer than 'PLAYLIST_ENTRIES' lines were
2437 * read, then this is metadata */
2438 else
2439 {
2440 char default_core_path[PATH_MAX_LENGTH];
2441 char default_core_name[PATH_MAX_LENGTH];
2442
2443 default_core_path[0] = '\0';
2444 default_core_name[0] = '\0';
2445
2446 /* Get default_core_path */
2447 if (lines_read < 1)
2448 break;
2449
2450 if (strncmp("default_core_path",
2451 line_buf[0],
2452 STRLEN_CONST("default_core_path")) == 0)
2453 get_old_format_metadata_value(
2454 line_buf[0], default_core_path, sizeof(default_core_path));
2455
2456 /* Get default_core_name */
2457 if (lines_read < 2)
2458 break;
2459
2460 if (strncmp("default_core_name",
2461 line_buf[1],
2462 STRLEN_CONST("default_core_name")) == 0)
2463 get_old_format_metadata_value(
2464 line_buf[1], default_core_name, sizeof(default_core_name));
2465
2466 /* > Populate default core path/name, if required
2467 * (if one is empty, the other should be ignored) */
2468 if (!string_is_empty(default_core_path) &&
2469 !string_is_empty(default_core_name))
2470 {
2471 playlist->default_core_path = strdup(default_core_path);
2472 playlist->default_core_name = strdup(default_core_name);
2473 }
2474
2475 /* Get label_display_mode */
2476 if (lines_read < 3)
2477 break;
2478
2479 if (strncmp("label_display_mode",
2480 line_buf[2],
2481 STRLEN_CONST("label_display_mode")) == 0)
2482 {
2483 unsigned display_mode;
2484 char display_mode_str[4] = {0};
2485
2486 get_old_format_metadata_value(
2487 line_buf[2], display_mode_str, sizeof(display_mode_str));
2488
2489 display_mode = string_to_unsigned(display_mode_str);
2490
2491 if (display_mode <= LABEL_DISPLAY_MODE_KEEP_REGION_AND_DISC_INDEX)
2492 playlist->label_display_mode = (enum playlist_label_display_mode)display_mode;
2493 }
2494
2495 /* Get thumbnail modes */
2496 if (lines_read < 4)
2497 break;
2498
2499 if (strncmp("thumbnail_mode",
2500 line_buf[3],
2501 STRLEN_CONST("thumbnail_mode")) == 0)
2502 {
2503 char thumbnail_mode_str[8] = {0};
2504 struct string_list thumbnail_modes = {0};
2505
2506 get_old_format_metadata_value(
2507 line_buf[3], thumbnail_mode_str,
2508 sizeof(thumbnail_mode_str));
2509 string_list_initialize(&thumbnail_modes);
2510 if (string_split_noalloc(&thumbnail_modes,
2511 thumbnail_mode_str, "|"))
2512 {
2513 if (thumbnail_modes.size == 2)
2514 {
2515 unsigned thumbnail_mode;
2516
2517 /* Right thumbnail mode */
2518 thumbnail_mode = string_to_unsigned(
2519 thumbnail_modes.elems[0].data);
2520 if (thumbnail_mode <= PLAYLIST_THUMBNAIL_MODE_BOXARTS)
2521 playlist->right_thumbnail_mode = (enum playlist_thumbnail_mode)thumbnail_mode;
2522
2523 /* Left thumbnail mode */
2524 thumbnail_mode = string_to_unsigned(
2525 thumbnail_modes.elems[1].data);
2526 if (thumbnail_mode <= PLAYLIST_THUMBNAIL_MODE_BOXARTS)
2527 playlist->left_thumbnail_mode = (enum playlist_thumbnail_mode)thumbnail_mode;
2528 }
2529 }
2530 string_list_deinitialize(&thumbnail_modes);
2531 }
2532
2533 /* Get sort_mode */
2534 if (lines_read < 5)
2535 break;
2536
2537 if (strncmp("sort_mode",
2538 line_buf[4],
2539 STRLEN_CONST("sort_mode")) == 0)
2540 {
2541 unsigned sort_mode;
2542 char sort_mode_str[4] = {0};
2543
2544 get_old_format_metadata_value(
2545 line_buf[4], sort_mode_str, sizeof(sort_mode_str));
2546
2547 sort_mode = string_to_unsigned(sort_mode_str);
2548
2549 if (sort_mode <= PLAYLIST_SORT_MODE_OFF)
2550 playlist->sort_mode = (enum playlist_sort_mode)sort_mode;
2551 }
2552
2553 /* All metadata parsed -> end of file */
2554 break;
2555 }
2556 }
2557 }
2558
2559 end:
2560 intfstream_close(file);
2561 free(file);
2562 return res;
2563 }
2564
playlist_free_cached(void)2565 void playlist_free_cached(void)
2566 {
2567 if (playlist_cached && !playlist_cached->cached_external)
2568 playlist_free(playlist_cached);
2569 playlist_cached = NULL;
2570 }
2571
playlist_get_cached(void)2572 playlist_t *playlist_get_cached(void)
2573 {
2574 if (playlist_cached)
2575 return playlist_cached;
2576 return NULL;
2577 }
2578
playlist_init_cached(const playlist_config_t * config)2579 bool playlist_init_cached(const playlist_config_t *config)
2580 {
2581 playlist_t *playlist = playlist_init(config);
2582 if (!playlist)
2583 return false;
2584
2585 /* If playlist format/compression state
2586 * does not match requested settings, update
2587 * file on disk immediately */
2588 if (
2589 #if defined(HAVE_ZLIB)
2590 (playlist->compressed != playlist->config.compress) ||
2591 #endif
2592 (playlist->old_format != playlist->config.old_format))
2593 playlist_write_file(playlist);
2594
2595 playlist_cached = playlist;
2596 return true;
2597 }
2598
2599 /**
2600 * playlist_init:
2601 * @config : Playlist configuration object.
2602 *
2603 * Creates and initializes a playlist.
2604 *
2605 * Returns: handle to new playlist if successful, otherwise NULL
2606 **/
playlist_init(const playlist_config_t * config)2607 playlist_t *playlist_init(const playlist_config_t *config)
2608 {
2609 playlist_t *playlist = (playlist_t*)malloc(sizeof(*playlist));
2610 if (!playlist)
2611 goto error;
2612
2613 /* Set initial values */
2614 playlist->modified = false;
2615 playlist->old_format = false;
2616 playlist->compressed = false;
2617 playlist->cached_external = false;
2618 playlist->default_core_name = NULL;
2619 playlist->default_core_path = NULL;
2620 playlist->base_content_directory = NULL;
2621 playlist->entries = NULL;
2622 playlist->label_display_mode = LABEL_DISPLAY_MODE_DEFAULT;
2623 playlist->right_thumbnail_mode = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
2624 playlist->left_thumbnail_mode = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
2625 playlist->sort_mode = PLAYLIST_SORT_MODE_DEFAULT;
2626
2627 /* Cache configuration parameters */
2628 if (!playlist_config_copy(config, &playlist->config))
2629 goto error;
2630
2631 /* Attempt to read any existing playlist file */
2632 if (!playlist_read_file(playlist))
2633 goto error;
2634
2635 /* Try auto-fixing paths if enabled, and playlist
2636 * base content directory is different */
2637 if (config->autofix_paths && !string_is_equal(playlist->base_content_directory, config->base_content_directory))
2638 {
2639 if (!string_is_empty(playlist->base_content_directory))
2640 {
2641 size_t i, j, len;
2642 char tmp_entry_path[PATH_MAX_LENGTH];
2643
2644 for (i = 0, len = RBUF_LEN(playlist->entries); i < len; i++)
2645 {
2646 struct playlist_entry* entry = &playlist->entries[i];
2647
2648 if (!entry || string_is_empty(entry->path))
2649 continue;
2650
2651 /* Fix entry path */
2652 tmp_entry_path[0] = '\0';
2653 path_replace_base_path_and_convert_to_local_file_system(
2654 tmp_entry_path, entry->path,
2655 playlist->base_content_directory, playlist->config.base_content_directory,
2656 sizeof(tmp_entry_path));
2657
2658 free(entry->path);
2659 entry->path = strdup(tmp_entry_path);
2660
2661 /* Fix subsystem roms paths*/
2662 if (entry->subsystem_roms && (entry->subsystem_roms->size > 0))
2663 {
2664 struct string_list* subsystem_roms_new_paths = string_list_new();
2665 union string_list_elem_attr attributes = { 0 };
2666
2667 if (!subsystem_roms_new_paths)
2668 goto error;
2669
2670 for (j = 0; j < entry->subsystem_roms->size; j++)
2671 {
2672 const char* subsystem_rom_path = entry->subsystem_roms->elems[j].data;
2673
2674 if (string_is_empty(subsystem_rom_path))
2675 continue;
2676
2677 tmp_entry_path[0] = '\0';
2678 path_replace_base_path_and_convert_to_local_file_system(
2679 tmp_entry_path, subsystem_rom_path,
2680 playlist->base_content_directory, playlist->config.base_content_directory,
2681 sizeof(tmp_entry_path));
2682 string_list_append(subsystem_roms_new_paths, tmp_entry_path, attributes);
2683 }
2684
2685 string_list_free(entry->subsystem_roms);
2686 entry->subsystem_roms = subsystem_roms_new_paths;
2687 }
2688 }
2689 }
2690
2691 /* Update playlist base content directory*/
2692 if (playlist->base_content_directory)
2693 free(playlist->base_content_directory);
2694 playlist->base_content_directory = strdup(playlist->config.base_content_directory);
2695
2696 /* Save playlist */
2697 playlist->modified = true;
2698 playlist_write_file(playlist);
2699 }
2700
2701 return playlist;
2702
2703 error:
2704 playlist_free(playlist);
2705 return NULL;
2706 }
2707
playlist_qsort_func(const struct playlist_entry * a,const struct playlist_entry * b)2708 static int playlist_qsort_func(const struct playlist_entry *a,
2709 const struct playlist_entry *b)
2710 {
2711 char *a_str = NULL;
2712 char *b_str = NULL;
2713 char *a_fallback_label = NULL;
2714 char *b_fallback_label = NULL;
2715 int ret = 0;
2716
2717 if (!a || !b)
2718 goto end;
2719
2720 a_str = a->label;
2721 b_str = b->label;
2722
2723 /* It is quite possible for playlist labels
2724 * to be blank. If that is the case, have to use
2725 * filename as a fallback (this is slow, but we
2726 * have no other option...) */
2727 if (string_is_empty(a_str))
2728 {
2729 a_fallback_label = (char*)calloc(PATH_MAX_LENGTH, sizeof(char));
2730
2731 if (!a_fallback_label)
2732 goto end;
2733
2734 if (!string_is_empty(a->path))
2735 fill_short_pathname_representation(a_fallback_label, a->path, PATH_MAX_LENGTH * sizeof(char));
2736 /* If filename is also empty, use core name
2737 * instead -> this matches the behaviour of
2738 * menu_displaylist_parse_playlist() */
2739 else if (!string_is_empty(a->core_name))
2740 strlcpy(a_fallback_label, a->core_name, PATH_MAX_LENGTH * sizeof(char));
2741
2742 /* If both filename and core name are empty,
2743 * then have to compare an empty string
2744 * -> again, this is to match the behaviour of
2745 * menu_displaylist_parse_playlist() */
2746
2747 a_str = a_fallback_label;
2748 }
2749
2750 if (string_is_empty(b_str))
2751 {
2752 b_fallback_label = (char*)calloc(PATH_MAX_LENGTH, sizeof(char));
2753
2754 if (!b_fallback_label)
2755 goto end;
2756
2757 if (!string_is_empty(b->path))
2758 fill_short_pathname_representation(b_fallback_label, b->path, PATH_MAX_LENGTH * sizeof(char));
2759 else if (!string_is_empty(b->core_name))
2760 strlcpy(b_fallback_label, b->core_name, PATH_MAX_LENGTH * sizeof(char));
2761
2762 b_str = b_fallback_label;
2763 }
2764
2765 ret = strcasecmp(a_str, b_str);
2766
2767 end:
2768
2769 a_str = NULL;
2770 b_str = NULL;
2771
2772 if (a_fallback_label)
2773 {
2774 free(a_fallback_label);
2775 a_fallback_label = NULL;
2776 }
2777
2778 if (b_fallback_label)
2779 {
2780 free(b_fallback_label);
2781 b_fallback_label = NULL;
2782 }
2783
2784 return ret;
2785 }
2786
playlist_qsort(playlist_t * playlist)2787 void playlist_qsort(playlist_t *playlist)
2788 {
2789 /* Avoid inadvertent sorting if 'sort mode'
2790 * has been set explicitly to PLAYLIST_SORT_MODE_OFF */
2791 if (!playlist ||
2792 (playlist->sort_mode == PLAYLIST_SORT_MODE_OFF) ||
2793 !playlist->entries)
2794 return;
2795
2796 qsort(playlist->entries, RBUF_LEN(playlist->entries),
2797 sizeof(struct playlist_entry),
2798 (int (*)(const void *, const void *))playlist_qsort_func);
2799 }
2800
command_playlist_push_write(playlist_t * playlist,const struct playlist_entry * entry)2801 void command_playlist_push_write(
2802 playlist_t *playlist,
2803 const struct playlist_entry *entry)
2804 {
2805 if (!playlist)
2806 return;
2807
2808 if (playlist_push(playlist, entry))
2809 playlist_write_file(playlist);
2810 }
2811
command_playlist_update_write(playlist_t * plist,size_t idx,const struct playlist_entry * entry)2812 void command_playlist_update_write(
2813 playlist_t *plist,
2814 size_t idx,
2815 const struct playlist_entry *entry)
2816 {
2817 playlist_t *playlist = plist ? plist : playlist_get_cached();
2818
2819 if (!playlist)
2820 return;
2821
2822 playlist_update(
2823 playlist,
2824 idx,
2825 entry);
2826
2827 playlist_write_file(playlist);
2828 }
2829
playlist_index_is_valid(playlist_t * playlist,size_t idx,const char * path,const char * core_path)2830 bool playlist_index_is_valid(playlist_t *playlist, size_t idx,
2831 const char *path, const char *core_path)
2832 {
2833 if (!playlist)
2834 return false;
2835
2836 if (idx >= RBUF_LEN(playlist->entries))
2837 return false;
2838
2839 return string_is_equal(playlist->entries[idx].path, path) &&
2840 string_is_equal(path_basename_nocompression(playlist->entries[idx].core_path), path_basename_nocompression(core_path));
2841 }
2842
playlist_entries_are_equal(const struct playlist_entry * entry_a,const struct playlist_entry * entry_b,const playlist_config_t * config)2843 bool playlist_entries_are_equal(
2844 const struct playlist_entry *entry_a,
2845 const struct playlist_entry *entry_b,
2846 const playlist_config_t *config)
2847 {
2848 char real_path_a[PATH_MAX_LENGTH];
2849 char real_core_path_a[PATH_MAX_LENGTH];
2850
2851 real_path_a[0] = '\0';
2852 real_core_path_a[0] = '\0';
2853
2854 /* Sanity check */
2855 if (!entry_a || !entry_b || !config)
2856 return false;
2857
2858 if (string_is_empty(entry_a->path) &&
2859 string_is_empty(entry_a->core_path) &&
2860 string_is_empty(entry_b->path) &&
2861 string_is_empty(entry_b->core_path))
2862 return true;
2863
2864 /* Check content paths */
2865 if (!string_is_empty(entry_a->path))
2866 {
2867 strlcpy(real_path_a, entry_a->path, sizeof(real_path_a));
2868 path_resolve_realpath(real_path_a, sizeof(real_path_a), true);
2869 }
2870
2871 if (!playlist_path_equal(
2872 real_path_a, entry_b->path, config))
2873 return false;
2874
2875 /* Check core paths */
2876 if (!string_is_empty(entry_a->core_path))
2877 {
2878 strlcpy(real_core_path_a, entry_a->core_path, sizeof(real_core_path_a));
2879 if (!string_is_equal(real_core_path_a, FILE_PATH_DETECT) &&
2880 !string_is_equal(real_core_path_a, FILE_PATH_BUILTIN))
2881 playlist_resolve_path(PLAYLIST_SAVE, true,
2882 real_core_path_a, sizeof(real_core_path_a));
2883 }
2884
2885 return playlist_core_path_equal(real_core_path_a, entry_b->core_path, config);
2886 }
2887
2888 /* Returns true if entries at specified indices
2889 * of specified playlist have identical content
2890 * and core paths */
playlist_index_entries_are_equal(playlist_t * playlist,size_t idx_a,size_t idx_b)2891 bool playlist_index_entries_are_equal(
2892 playlist_t *playlist, size_t idx_a, size_t idx_b)
2893 {
2894 struct playlist_entry *entry_a = NULL;
2895 struct playlist_entry *entry_b = NULL;
2896 size_t len;
2897
2898 if (!playlist)
2899 return false;
2900
2901 len = RBUF_LEN(playlist->entries);
2902
2903 if ((idx_a >= len) || (idx_b >= len))
2904 return false;
2905
2906 /* Fetch entries */
2907 entry_a = &playlist->entries[idx_a];
2908 entry_b = &playlist->entries[idx_b];
2909
2910 if (!entry_a || !entry_b)
2911 return false;
2912
2913 /* Initialise path ID for entry A, if required
2914 * (entry B will be handled inside
2915 * playlist_path_matches_entry()) */
2916 if (!entry_a->path_id)
2917 entry_a->path_id = playlist_path_id_init(entry_a->path);
2918
2919 return playlist_path_matches_entry(
2920 entry_a->path_id, entry_b, &playlist->config);
2921 }
2922
playlist_get_crc32(playlist_t * playlist,size_t idx,const char ** crc32)2923 void playlist_get_crc32(playlist_t *playlist, size_t idx,
2924 const char **crc32)
2925 {
2926 if (!playlist || idx >= RBUF_LEN(playlist->entries))
2927 return;
2928
2929 if (crc32)
2930 *crc32 = playlist->entries[idx].crc32;
2931 }
2932
playlist_get_db_name(playlist_t * playlist,size_t idx,const char ** db_name)2933 void playlist_get_db_name(playlist_t *playlist, size_t idx,
2934 const char **db_name)
2935 {
2936 if (!playlist || idx >= RBUF_LEN(playlist->entries))
2937 return;
2938
2939 if (db_name)
2940 {
2941 if (!string_is_empty(playlist->entries[idx].db_name))
2942 *db_name = playlist->entries[idx].db_name;
2943 else
2944 {
2945 const char *conf_path_basename = path_basename_nocompression(playlist->config.path);
2946
2947 /* Only use file basename if this is a 'collection' playlist
2948 * (i.e. ignore history/favourites) */
2949 if (
2950 !string_is_empty(conf_path_basename)
2951 && !string_ends_with_size(conf_path_basename, "_history.lpl",
2952 strlen(conf_path_basename), STRLEN_CONST("_history.lpl"))
2953 && !string_is_equal(conf_path_basename,
2954 FILE_PATH_CONTENT_FAVORITES)
2955 )
2956 *db_name = conf_path_basename;
2957 }
2958 }
2959 }
2960
playlist_get_default_core_path(playlist_t * playlist)2961 char *playlist_get_default_core_path(playlist_t *playlist)
2962 {
2963 if (!playlist)
2964 return NULL;
2965 return playlist->default_core_path;
2966 }
2967
playlist_get_default_core_name(playlist_t * playlist)2968 char *playlist_get_default_core_name(playlist_t *playlist)
2969 {
2970 if (!playlist)
2971 return NULL;
2972 return playlist->default_core_name;
2973 }
2974
playlist_get_label_display_mode(playlist_t * playlist)2975 enum playlist_label_display_mode playlist_get_label_display_mode(playlist_t *playlist)
2976 {
2977 if (!playlist)
2978 return LABEL_DISPLAY_MODE_DEFAULT;
2979 return playlist->label_display_mode;
2980 }
2981
playlist_get_thumbnail_mode(playlist_t * playlist,enum playlist_thumbnail_id thumbnail_id)2982 enum playlist_thumbnail_mode playlist_get_thumbnail_mode(
2983 playlist_t *playlist, enum playlist_thumbnail_id thumbnail_id)
2984 {
2985 if (!playlist)
2986 return PLAYLIST_THUMBNAIL_MODE_DEFAULT;
2987
2988 if (thumbnail_id == PLAYLIST_THUMBNAIL_RIGHT)
2989 return playlist->right_thumbnail_mode;
2990 else if (thumbnail_id == PLAYLIST_THUMBNAIL_LEFT)
2991 return playlist->left_thumbnail_mode;
2992
2993 /* Fallback */
2994 return PLAYLIST_THUMBNAIL_MODE_DEFAULT;
2995 }
2996
playlist_get_sort_mode(playlist_t * playlist)2997 enum playlist_sort_mode playlist_get_sort_mode(playlist_t *playlist)
2998 {
2999 if (!playlist)
3000 return PLAYLIST_SORT_MODE_DEFAULT;
3001 return playlist->sort_mode;
3002 }
3003
playlist_set_default_core_path(playlist_t * playlist,const char * core_path)3004 void playlist_set_default_core_path(playlist_t *playlist, const char *core_path)
3005 {
3006 char real_core_path[PATH_MAX_LENGTH];
3007
3008 if (!playlist || string_is_empty(core_path))
3009 return;
3010
3011 real_core_path[0] = '\0';
3012
3013 /* Get 'real' core path */
3014 strlcpy(real_core_path, core_path, sizeof(real_core_path));
3015 if (!string_is_equal(real_core_path, FILE_PATH_DETECT) &&
3016 !string_is_equal(real_core_path, FILE_PATH_BUILTIN))
3017 playlist_resolve_path(PLAYLIST_SAVE, true,
3018 real_core_path, sizeof(real_core_path));
3019
3020 if (string_is_empty(real_core_path))
3021 return;
3022
3023 if (!string_is_equal(playlist->default_core_path, real_core_path))
3024 {
3025 if (playlist->default_core_path)
3026 free(playlist->default_core_path);
3027 playlist->default_core_path = strdup(real_core_path);
3028 playlist->modified = true;
3029 }
3030 }
3031
playlist_set_default_core_name(playlist_t * playlist,const char * core_name)3032 void playlist_set_default_core_name(
3033 playlist_t *playlist, const char *core_name)
3034 {
3035 if (!playlist || string_is_empty(core_name))
3036 return;
3037
3038 if (!string_is_equal(playlist->default_core_name, core_name))
3039 {
3040 if (playlist->default_core_name)
3041 free(playlist->default_core_name);
3042 playlist->default_core_name = strdup(core_name);
3043 playlist->modified = true;
3044 }
3045 }
3046
playlist_set_label_display_mode(playlist_t * playlist,enum playlist_label_display_mode label_display_mode)3047 void playlist_set_label_display_mode(playlist_t *playlist,
3048 enum playlist_label_display_mode label_display_mode)
3049 {
3050 if (!playlist)
3051 return;
3052
3053 if (playlist->label_display_mode != label_display_mode)
3054 {
3055 playlist->label_display_mode = label_display_mode;
3056 playlist->modified = true;
3057 }
3058 }
3059
playlist_set_thumbnail_mode(playlist_t * playlist,enum playlist_thumbnail_id thumbnail_id,enum playlist_thumbnail_mode thumbnail_mode)3060 void playlist_set_thumbnail_mode(
3061 playlist_t *playlist, enum playlist_thumbnail_id thumbnail_id,
3062 enum playlist_thumbnail_mode thumbnail_mode)
3063 {
3064 if (!playlist)
3065 return;
3066
3067 switch (thumbnail_id)
3068 {
3069 case PLAYLIST_THUMBNAIL_RIGHT:
3070 playlist->right_thumbnail_mode = thumbnail_mode;
3071 playlist->modified = true;
3072 break;
3073 case PLAYLIST_THUMBNAIL_LEFT:
3074 playlist->left_thumbnail_mode = thumbnail_mode;
3075 playlist->modified = true;
3076 break;
3077 }
3078 }
3079
playlist_set_sort_mode(playlist_t * playlist,enum playlist_sort_mode sort_mode)3080 void playlist_set_sort_mode(playlist_t *playlist,
3081 enum playlist_sort_mode sort_mode)
3082 {
3083 if (!playlist)
3084 return;
3085
3086 if (playlist->sort_mode != sort_mode)
3087 {
3088 playlist->sort_mode = sort_mode;
3089 playlist->modified = true;
3090 }
3091 }
3092
3093 /* Returns true if specified entry has a valid
3094 * core association (i.e. a non-empty string
3095 * other than DETECT) */
playlist_entry_has_core(const struct playlist_entry * entry)3096 bool playlist_entry_has_core(const struct playlist_entry *entry)
3097 {
3098 if (!entry ||
3099 string_is_empty(entry->core_path) ||
3100 string_is_empty(entry->core_name) ||
3101 string_is_equal(entry->core_path, FILE_PATH_DETECT) ||
3102 string_is_equal(entry->core_name, FILE_PATH_DETECT))
3103 return false;
3104
3105 return true;
3106 }
3107
3108 /* Fetches core info object corresponding to the
3109 * currently associated core of the specified
3110 * playlist entry.
3111 * Returns NULL if entry does not have a valid
3112 * core association */
playlist_entry_get_core_info(const struct playlist_entry * entry)3113 core_info_t *playlist_entry_get_core_info(const struct playlist_entry* entry)
3114 {
3115 core_info_t *core_info = NULL;
3116
3117 if (!playlist_entry_has_core(entry))
3118 return NULL;
3119
3120 /* Search for associated core */
3121 if (core_info_find(entry->core_path, &core_info))
3122 return core_info;
3123
3124 return NULL;
3125 }
3126
3127 /* Fetches core info object corresponding to the
3128 * currently associated default core of the
3129 * specified playlist.
3130 * Returns NULL if playlist does not have a valid
3131 * default core association */
playlist_get_default_core_info(playlist_t * playlist)3132 core_info_t *playlist_get_default_core_info(playlist_t* playlist)
3133 {
3134 core_info_t *core_info = NULL;
3135
3136 if (!playlist ||
3137 string_is_empty(playlist->default_core_path) ||
3138 string_is_empty(playlist->default_core_name) ||
3139 string_is_equal(playlist->default_core_path, FILE_PATH_DETECT) ||
3140 string_is_equal(playlist->default_core_name, FILE_PATH_DETECT))
3141 return NULL;
3142
3143 /* Search for associated core */
3144 if (core_info_find(playlist->default_core_path, &core_info))
3145 return core_info;
3146
3147 return NULL;
3148 }
3149
3150