git subrepo clone https://github.com/libretro/libretro-common.git deps/libretro-common
[pcsx_rearmed.git] / deps / libretro-common / formats / m3u / m3u_file.c
1 /* Copyright  (C) 2010-2020 The RetroArch team
2  *
3  * ---------------------------------------------------------------------------------------
4  * The following license statement only applies to this file (m3u_file.c).
5  * ---------------------------------------------------------------------------------------
6  *
7  * Permission is hereby granted, free of charge,
8  * to any person obtaining a copy of this software and associated documentation files (the "Software"),
9  * to deal in the Software without restriction, including without limitation the rights to
10  * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
11  * and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
16  * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18  * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19  * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21  */
22
23 #include <retro_miscellaneous.h>
24
25 #include <string/stdstring.h>
26 #include <lists/string_list.h>
27 #include <file/file_path.h>
28 #include <streams/file_stream.h>
29 #include <array/rbuf.h>
30
31 #include <formats/m3u_file.h>
32
33 /* We parse the following types of entry label:
34  * - '#LABEL:<label>' non-standard, but used by
35  *   some cores
36  * - '#EXTINF:<runtime>,<label>' standard extended
37  *   M3U directive
38  * - '<content path>|<label>' non-standard, but
39  *   used by some cores
40  * All other comments/directives are ignored */
41 #define M3U_FILE_COMMENT            '#'
42 #define M3U_FILE_NONSTD_LABEL       "#LABEL:"
43 #define M3U_FILE_EXTSTD_LABEL       "#EXTINF:"
44 #define M3U_FILE_EXTSTD_LABEL_TOKEN ','
45 #define M3U_FILE_RETRO_LABEL_TOKEN  '|'
46
47 /* Holds all internal M3U file data
48  * > Note the awkward name: 'content_m3u_file'
49  *   If we used just 'm3u_file' here, it would
50  *   lead to conflicts elsewhere... */
51 struct content_m3u_file
52 {
53    char *path;
54    m3u_file_entry_t *entries;
55 };
56
57 /* File Initialisation / De-Initialisation */
58
59 /* Reads M3U file contents from disk
60  * - Does nothing if file does not exist 
61  * - Returns false in the event of an error */
62 static bool m3u_file_load(m3u_file_t *m3u_file)
63 {
64    const char *file_ext      = NULL;
65    int64_t file_len          = 0;
66    uint8_t *file_buf         = NULL;
67    struct string_list *lines = NULL;
68    bool success              = false;
69    size_t i;
70    char entry_path[PATH_MAX_LENGTH];
71    char entry_label[PATH_MAX_LENGTH];
72
73    entry_path[0]  = '\0';
74    entry_label[0] = '\0';
75
76    if (!m3u_file)
77       goto end;
78
79    /* Check whether file exists
80     * > If path is empty, then an error
81     *   has occurred... */
82    if (string_is_empty(m3u_file->path))
83       goto end;
84
85    /* > File must have the correct extension */
86    file_ext = path_get_extension(m3u_file->path);
87
88    if (string_is_empty(file_ext) ||
89        !string_is_equal_noncase(file_ext, M3U_FILE_EXT))
90       goto end;
91
92    /* > If file does not exist, no action
93     *   is required */
94    if (!path_is_valid(m3u_file->path))
95    {
96       success = true;
97       goto end;
98    }
99
100    /* Read file from disk */
101    if (filestream_read_file(m3u_file->path, (void**)&file_buf, &file_len) >= 0)
102    {
103       /* Split file into lines */
104       if (file_len > 0)
105          lines = string_split((const char*)file_buf, "\n");
106
107       /* File buffer no longer required */
108       if (file_buf)
109       {
110          free(file_buf);
111          file_buf = NULL;
112       }
113    }
114    /* File IO error... */
115    else
116       goto end;
117
118    /* If file was empty, no action is required */
119    if (!lines)
120    {
121       success = true;
122       goto end;
123    }
124
125    /* Parse lines of file */
126    for (i = 0; i < lines->size; i++)
127    {
128       const char *line = lines->elems[i].data;
129
130       if (string_is_empty(line))
131          continue;
132
133       /* Determine line 'type' */
134
135       /* > '#LABEL:' */
136       if (string_starts_with_size(line, M3U_FILE_NONSTD_LABEL,
137             STRLEN_CONST(M3U_FILE_NONSTD_LABEL)))
138       {
139          /* Label is the string to the right
140           * of '#LABEL:' */
141          const char *label = line + STRLEN_CONST(M3U_FILE_NONSTD_LABEL);
142
143          if (!string_is_empty(label))
144          {
145             strlcpy(
146                   entry_label, line + STRLEN_CONST(M3U_FILE_NONSTD_LABEL),
147                   sizeof(entry_label));
148             string_trim_whitespace(entry_label);
149          }
150       }
151       /* > '#EXTINF:' */
152       else if (string_starts_with_size(line, M3U_FILE_EXTSTD_LABEL,
153             STRLEN_CONST(M3U_FILE_EXTSTD_LABEL)))
154       {
155          /* Label is the string to the right
156           * of the first comma */
157          const char* label_ptr = strchr(
158                line + STRLEN_CONST(M3U_FILE_EXTSTD_LABEL),
159                M3U_FILE_EXTSTD_LABEL_TOKEN);
160
161          if (!string_is_empty(label_ptr))
162          {
163             label_ptr++;
164             if (!string_is_empty(label_ptr))
165             {
166                strlcpy(entry_label, label_ptr, sizeof(entry_label));
167                string_trim_whitespace(entry_label);
168             }
169          }
170       }
171       /* > Ignore other comments/directives */
172       else if (line[0] == M3U_FILE_COMMENT)
173          continue;
174       /* > An actual 'content' line */
175       else
176       {
177          /* This is normally a file name/path, but may
178           * have the format <content path>|<label> */
179          const char *token_ptr = strchr(line, M3U_FILE_RETRO_LABEL_TOKEN);
180
181          if (token_ptr)
182          {
183             size_t len = (size_t)(1 + token_ptr - line);
184
185             /* Get entry_path segment */
186             if (len > 0)
187             {
188                memset(entry_path, 0, sizeof(entry_path));
189                strlcpy(
190                      entry_path, line,
191                      ((len < PATH_MAX_LENGTH ?
192                            len : PATH_MAX_LENGTH) * sizeof(char)));
193                string_trim_whitespace(entry_path);
194             }
195
196             /* Get entry_label segment */
197             token_ptr++;
198             if (*token_ptr != '\0')
199             {
200                strlcpy(entry_label, token_ptr, sizeof(entry_label));
201                string_trim_whitespace(entry_label);
202             }
203          }
204          else
205          {
206             /* Just a normal file name/path */
207             strlcpy(entry_path, line, sizeof(entry_path));
208             string_trim_whitespace(entry_path);
209          }
210
211          /* Add entry to file
212           * > Note: The only way that m3u_file_add_entry()
213           *   can fail here is if we run out of memory.
214           *   This is a critical error, and m3u_file must
215           *   be considered invalid in this case */
216          if (!string_is_empty(entry_path) &&
217              !m3u_file_add_entry(m3u_file, entry_path, entry_label))
218             goto end;
219
220          /* Reset entry_path/entry_label */
221          entry_path[0]  = '\0';
222          entry_label[0] = '\0';
223       }
224    }
225
226    success = true;
227
228 end:
229    /* Clean up */
230    if (lines)
231    {
232       string_list_free(lines);
233       lines = NULL;
234    }
235
236    if (file_buf)
237    {
238       free(file_buf);
239       file_buf = NULL;
240    }
241
242    return success;
243 }
244
245 /* Creates and initialises an M3U file
246  * - If 'path' refers to an existing file,
247  *   contents is parsed
248  * - If path does not exist, an empty M3U file
249  *   is created
250  * - Returned m3u_file_t object must be free'd using
251  *   m3u_file_free()
252  * - Returns NULL in the event of an error */
253 m3u_file_t *m3u_file_init(const char *path)
254 {
255    m3u_file_t *m3u_file = NULL;
256    char m3u_path[PATH_MAX_LENGTH];
257
258    m3u_path[0] = '\0';
259
260    /* Sanity check */
261    if (string_is_empty(path))
262       return NULL;
263
264    /* Get 'real' file path */
265    strlcpy(m3u_path, path, sizeof(m3u_path));
266    path_resolve_realpath(m3u_path, sizeof(m3u_path), false);
267
268    if (string_is_empty(m3u_path))
269       return NULL;
270
271    /* Create m3u_file_t object */
272    m3u_file = (m3u_file_t*)malloc(sizeof(*m3u_file));
273
274    if (!m3u_file)
275       return NULL;
276
277    /* Initialise members */
278    m3u_file->path    = NULL;
279    m3u_file->entries = NULL;
280
281    /* Copy file path */
282    m3u_file->path    = strdup(m3u_path);
283
284    /* Read existing file contents from
285     * disk, if required */
286    if (!m3u_file_load(m3u_file))
287    {
288       m3u_file_free(m3u_file);
289       return NULL;
290    }
291
292    return m3u_file;
293 }
294
295 /* Frees specified M3U file entry */
296 static void m3u_file_free_entry(m3u_file_entry_t *entry)
297 {
298    if (!entry)
299       return;
300
301    if (entry->path)
302       free(entry->path);
303
304    if (entry->full_path)
305       free(entry->full_path);
306
307    if (entry->label)
308       free(entry->label);
309
310    entry->path      = NULL;
311    entry->full_path = NULL;
312    entry->label     = NULL;
313 }
314
315 /* Frees specified M3U file */
316 void m3u_file_free(m3u_file_t *m3u_file)
317 {
318    size_t i;
319
320    if (!m3u_file)
321       return;
322
323    if (m3u_file->path)
324       free(m3u_file->path);
325
326    m3u_file->path = NULL;
327
328    /* Free entries */
329    if (m3u_file->entries)
330    {
331       for (i = 0; i < RBUF_LEN(m3u_file->entries); i++)
332       {
333          m3u_file_entry_t *entry = &m3u_file->entries[i];
334          m3u_file_free_entry(entry);
335       }
336
337       RBUF_FREE(m3u_file->entries);
338    }
339
340    free(m3u_file);
341 }
342
343 /* Getters */
344
345 /* Returns M3U file path */
346 char *m3u_file_get_path(m3u_file_t *m3u_file)
347 {
348    if (!m3u_file)
349       return NULL;
350
351    return m3u_file->path;
352 }
353
354 /* Returns number of entries in M3U file */
355 size_t m3u_file_get_size(m3u_file_t *m3u_file)
356 {
357    if (!m3u_file)
358       return 0;
359
360    return RBUF_LEN(m3u_file->entries);
361 }
362
363 /* Fetches specified M3U file entry
364  * - Returns false if 'idx' is invalid, or internal
365  *   entry is NULL */
366 bool m3u_file_get_entry(
367       m3u_file_t *m3u_file, size_t idx, m3u_file_entry_t **entry)
368 {
369    if (!m3u_file ||
370        !entry ||
371        (idx >= RBUF_LEN(m3u_file->entries)))
372       return false;
373
374    *entry = &m3u_file->entries[idx];
375
376    if (!*entry)
377       return false;
378
379    return true;
380 }
381
382 /* Setters */
383
384 /* Adds specified entry to the M3U file
385  * - Returns false if path is invalid, or
386  *   memory could not be allocated for the
387  *   entry */
388 bool m3u_file_add_entry(
389       m3u_file_t *m3u_file, const char *path, const char *label)
390 {
391    m3u_file_entry_t *entry = NULL;
392    size_t num_entries;
393    char full_path[PATH_MAX_LENGTH];
394
395    full_path[0] = '\0';
396
397    if (!m3u_file || string_is_empty(path))
398       return false;
399
400    /* Get current number of file entries */
401    num_entries = RBUF_LEN(m3u_file->entries);
402
403    /* Attempt to allocate memory for new entry */
404    if (!RBUF_TRYFIT(m3u_file->entries, num_entries + 1))
405       return false;
406
407    /* Allocation successful - increment array size */
408    RBUF_RESIZE(m3u_file->entries, num_entries + 1);
409
410    /* Fetch entry at end of list, and zero-initialise
411     * members */
412    entry = &m3u_file->entries[num_entries];
413    memset(entry, 0, sizeof(*entry));
414
415    /* Copy path and label */
416    entry->path = strdup(path);
417
418    if (!string_is_empty(label))
419       entry->label = strdup(label);
420
421    /* Populate 'full_path' field */
422    if (path_is_absolute(path))
423    {
424       strlcpy(full_path, path, sizeof(full_path));
425       path_resolve_realpath(full_path, sizeof(full_path), false);
426    }
427    else
428       fill_pathname_resolve_relative(
429             full_path, m3u_file->path, path,
430             sizeof(full_path));
431
432    /* Handle unforeseen errors... */
433    if (string_is_empty(full_path))
434    {
435       m3u_file_free_entry(entry);
436       return false;
437    }
438
439    entry->full_path = strdup(full_path);
440
441    return true;
442 }
443
444 /* Removes all entries in M3U file */
445 void m3u_file_clear(m3u_file_t *m3u_file)
446 {
447    size_t i;
448
449    if (!m3u_file)
450       return;
451
452    if (m3u_file->entries)
453    {
454       for (i = 0; i < RBUF_LEN(m3u_file->entries); i++)
455       {
456          m3u_file_entry_t *entry = &m3u_file->entries[i];
457          m3u_file_free_entry(entry);
458       }
459
460       RBUF_FREE(m3u_file->entries);
461    }
462 }
463
464 /* Saving */
465
466 /* Saves M3U file to disk
467  * - Setting 'label_type' to M3U_FILE_LABEL_NONE
468  *   just outputs entry paths - this the most
469  *   common format supported by most cores
470  * - Returns false in the event of an error */
471 bool m3u_file_save(
472       m3u_file_t *m3u_file, enum m3u_file_label_type label_type)
473 {
474    RFILE *file = NULL;
475    size_t i;
476    char base_dir[PATH_MAX_LENGTH];
477
478    base_dir[0] = '\0';
479
480    if (!m3u_file || !m3u_file->entries)
481       return false;
482
483    /* This should never happen */
484    if (string_is_empty(m3u_file->path))
485       return false;
486
487    /* Get M3U file base directory */
488    if (find_last_slash(m3u_file->path))
489    {
490       strlcpy(base_dir, m3u_file->path, sizeof(base_dir));
491       path_basedir(base_dir);
492    }
493
494    /* Open file for writing */
495    file = filestream_open(
496          m3u_file->path,
497          RETRO_VFS_FILE_ACCESS_WRITE,
498          RETRO_VFS_FILE_ACCESS_HINT_NONE);
499
500    if (!file)
501       return false;
502
503    /* Loop over entries */
504    for (i = 0; i < RBUF_LEN(m3u_file->entries); i++)
505    {
506       m3u_file_entry_t *entry = &m3u_file->entries[i];
507       char entry_path[PATH_MAX_LENGTH];
508
509       entry_path[0] = '\0';
510
511       if (!entry || string_is_empty(entry->full_path))
512          continue;
513
514       /* When writing M3U files, entry paths are
515        * always relative */
516       if (string_is_empty(base_dir))
517          strlcpy(
518                entry_path, entry->full_path,
519                sizeof(entry_path));
520       else
521          path_relative_to(
522                entry_path, entry->full_path, base_dir,
523                sizeof(entry_path));
524
525       if (string_is_empty(entry_path))
526          continue;
527
528       /* Check if we need to write a label */
529       if (!string_is_empty(entry->label))
530       {
531          switch (label_type)
532          {
533             case M3U_FILE_LABEL_NONSTD:
534                filestream_printf(
535                      file, "%s%s\n%s\n",
536                      M3U_FILE_NONSTD_LABEL, entry->label,
537                      entry_path);
538                break;
539             case M3U_FILE_LABEL_EXTSTD:
540                filestream_printf(
541                      file, "%s%c%s\n%s\n",
542                      M3U_FILE_EXTSTD_LABEL, M3U_FILE_EXTSTD_LABEL_TOKEN, entry->label,
543                      entry_path);
544                break;
545             case M3U_FILE_LABEL_RETRO:
546                filestream_printf(
547                      file, "%s%c%s\n",
548                      entry_path, M3U_FILE_RETRO_LABEL_TOKEN, entry->label);
549                break;
550             case M3U_FILE_LABEL_NONE:
551             default:
552                filestream_printf(
553                      file, "%s\n", entry_path);
554                break;
555          }
556       }
557       /* No label - just write entry path */
558       else
559          filestream_printf(
560                file, "%s\n", entry_path);
561    }
562
563    /* Close file */
564    filestream_close(file);
565
566    return true;
567 }
568
569 /* Utilities */
570
571 /* Internal qsort function */
572 static int m3u_file_qsort_func(
573       const m3u_file_entry_t *a, const m3u_file_entry_t *b)
574 {
575    if (!a || !b)
576       return 0;
577
578    if (string_is_empty(a->full_path) || string_is_empty(b->full_path))
579       return 0;
580
581    return strcasecmp(a->full_path, b->full_path);
582 }
583
584 /* Sorts M3U file entries in alphabetical order */
585 void m3u_file_qsort(m3u_file_t *m3u_file)
586 {
587    size_t num_entries;
588
589    if (!m3u_file)
590       return;
591
592    num_entries = RBUF_LEN(m3u_file->entries);
593
594    if (num_entries < 2)
595       return;
596
597    qsort(
598          m3u_file->entries, num_entries,
599          sizeof(m3u_file_entry_t),
600          (int (*)(const void *, const void *))m3u_file_qsort_func);
601 }
602
603 /* Returns true if specified path corresponds
604  * to an M3U file (simple convenience function) */
605 bool m3u_file_is_m3u(const char *path)
606 {
607    const char *file_ext = NULL;
608    int32_t file_size;
609
610    if (string_is_empty(path))
611       return false;
612
613    /* Check file extension */
614    file_ext = path_get_extension(path);
615
616    if (string_is_empty(file_ext))
617       return false;
618
619    if (!string_is_equal_noncase(file_ext, M3U_FILE_EXT))
620       return false;
621
622    /* Ensure file exists */
623    if (!path_is_valid(path))
624       return false;
625
626    /* Ensure we have non-zero file size */
627    file_size = path_get_size(path);
628
629    if (file_size <= 0)
630       return false;
631
632    return true;
633 }