git subrepo clone https://github.com/libretro/libretro-common.git deps/libretro-common
[pcsx_rearmed.git] / deps / libretro-common / formats / m3u / m3u_file.c
CommitLineData
3719602c
PC
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... */
51struct 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 */
62static 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
228end:
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 */
253m3u_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 */
296static 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 */
316void 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 */
346char *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 */
355size_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 */
366bool 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 */
388bool 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 */
445void 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 */
471bool 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 */
572static 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 */
585void 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) */
605bool 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}