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