1 /* Copyright (C) 2010-2020 The RetroArch team
3 * ---------------------------------------------------------------------------------------
4 * The following license statement only applies to this file (m3u_file.c).
5 * ---------------------------------------------------------------------------------------
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:
13 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
23 #include <retro_miscellaneous.h>
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>
31 #include <formats/m3u_file.h>
33 /* We parse the following types of entry label:
34 * - '#LABEL:<label>' non-standard, but used by
36 * - '#EXTINF:<runtime>,<label>' standard extended
38 * - '<content path>|<label>' non-standard, but
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 '|'
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
54 m3u_file_entry_t *entries;
57 /* File Initialisation / De-Initialisation */
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)
64 const char *file_ext = NULL;
66 uint8_t *file_buf = NULL;
67 struct string_list *lines = NULL;
70 char entry_path[PATH_MAX_LENGTH];
71 char entry_label[PATH_MAX_LENGTH];
74 entry_label[0] = '\0';
79 /* Check whether file exists
80 * > If path is empty, then an error
82 if (string_is_empty(m3u_file->path))
85 /* > File must have the correct extension */
86 file_ext = path_get_extension(m3u_file->path);
88 if (string_is_empty(file_ext) ||
89 !string_is_equal_noncase(file_ext, M3U_FILE_EXT))
92 /* > If file does not exist, no action
94 if (!path_is_valid(m3u_file->path))
100 /* Read file from disk */
101 if (filestream_read_file(m3u_file->path, (void**)&file_buf, &file_len) >= 0)
103 /* Split file into lines */
105 lines = string_split((const char*)file_buf, "\n");
107 /* File buffer no longer required */
114 /* File IO error... */
118 /* If file was empty, no action is required */
125 /* Parse lines of file */
126 for (i = 0; i < lines->size; i++)
128 const char *line = lines->elems[i].data;
130 if (string_is_empty(line))
133 /* Determine line 'type' */
136 if (string_starts_with_size(line, M3U_FILE_NONSTD_LABEL,
137 STRLEN_CONST(M3U_FILE_NONSTD_LABEL)))
139 /* Label is the string to the right
141 const char *label = line + STRLEN_CONST(M3U_FILE_NONSTD_LABEL);
143 if (!string_is_empty(label))
146 entry_label, line + STRLEN_CONST(M3U_FILE_NONSTD_LABEL),
147 sizeof(entry_label));
148 string_trim_whitespace(entry_label);
152 else if (string_starts_with_size(line, M3U_FILE_EXTSTD_LABEL,
153 STRLEN_CONST(M3U_FILE_EXTSTD_LABEL)))
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);
161 if (!string_is_empty(label_ptr))
164 if (!string_is_empty(label_ptr))
166 strlcpy(entry_label, label_ptr, sizeof(entry_label));
167 string_trim_whitespace(entry_label);
171 /* > Ignore other comments/directives */
172 else if (line[0] == M3U_FILE_COMMENT)
174 /* > An actual 'content' line */
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);
183 size_t len = (size_t)(1 + token_ptr - line);
185 /* Get entry_path segment */
188 memset(entry_path, 0, sizeof(entry_path));
191 ((len < PATH_MAX_LENGTH ?
192 len : PATH_MAX_LENGTH) * sizeof(char)));
193 string_trim_whitespace(entry_path);
196 /* Get entry_label segment */
198 if (*token_ptr != '\0')
200 strlcpy(entry_label, token_ptr, sizeof(entry_label));
201 string_trim_whitespace(entry_label);
206 /* Just a normal file name/path */
207 strlcpy(entry_path, line, sizeof(entry_path));
208 string_trim_whitespace(entry_path);
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))
220 /* Reset entry_path/entry_label */
221 entry_path[0] = '\0';
222 entry_label[0] = '\0';
232 string_list_free(lines);
245 /* Creates and initialises an M3U file
246 * - If 'path' refers to an existing file,
248 * - If path does not exist, an empty M3U file
250 * - Returned m3u_file_t object must be free'd using
252 * - Returns NULL in the event of an error */
253 m3u_file_t *m3u_file_init(const char *path)
255 m3u_file_t *m3u_file = NULL;
256 char m3u_path[PATH_MAX_LENGTH];
261 if (string_is_empty(path))
264 /* Get 'real' file path */
265 strlcpy(m3u_path, path, sizeof(m3u_path));
266 path_resolve_realpath(m3u_path, sizeof(m3u_path), false);
268 if (string_is_empty(m3u_path))
271 /* Create m3u_file_t object */
272 m3u_file = (m3u_file_t*)malloc(sizeof(*m3u_file));
277 /* Initialise members */
278 m3u_file->path = NULL;
279 m3u_file->entries = NULL;
282 m3u_file->path = strdup(m3u_path);
284 /* Read existing file contents from
285 * disk, if required */
286 if (!m3u_file_load(m3u_file))
288 m3u_file_free(m3u_file);
295 /* Frees specified M3U file entry */
296 static void m3u_file_free_entry(m3u_file_entry_t *entry)
304 if (entry->full_path)
305 free(entry->full_path);
311 entry->full_path = NULL;
315 /* Frees specified M3U file */
316 void m3u_file_free(m3u_file_t *m3u_file)
324 free(m3u_file->path);
326 m3u_file->path = NULL;
329 if (m3u_file->entries)
331 for (i = 0; i < RBUF_LEN(m3u_file->entries); i++)
333 m3u_file_entry_t *entry = &m3u_file->entries[i];
334 m3u_file_free_entry(entry);
337 RBUF_FREE(m3u_file->entries);
345 /* Returns M3U file path */
346 char *m3u_file_get_path(m3u_file_t *m3u_file)
351 return m3u_file->path;
354 /* Returns number of entries in M3U file */
355 size_t m3u_file_get_size(m3u_file_t *m3u_file)
360 return RBUF_LEN(m3u_file->entries);
363 /* Fetches specified M3U file entry
364 * - Returns false if 'idx' is invalid, or internal
366 bool m3u_file_get_entry(
367 m3u_file_t *m3u_file, size_t idx, m3u_file_entry_t **entry)
371 (idx >= RBUF_LEN(m3u_file->entries)))
374 *entry = &m3u_file->entries[idx];
384 /* Adds specified entry to the M3U file
385 * - Returns false if path is invalid, or
386 * memory could not be allocated for the
388 bool m3u_file_add_entry(
389 m3u_file_t *m3u_file, const char *path, const char *label)
391 m3u_file_entry_t *entry = NULL;
393 char full_path[PATH_MAX_LENGTH];
397 if (!m3u_file || string_is_empty(path))
400 /* Get current number of file entries */
401 num_entries = RBUF_LEN(m3u_file->entries);
403 /* Attempt to allocate memory for new entry */
404 if (!RBUF_TRYFIT(m3u_file->entries, num_entries + 1))
407 /* Allocation successful - increment array size */
408 RBUF_RESIZE(m3u_file->entries, num_entries + 1);
410 /* Fetch entry at end of list, and zero-initialise
412 entry = &m3u_file->entries[num_entries];
413 memset(entry, 0, sizeof(*entry));
415 /* Copy path and label */
416 entry->path = strdup(path);
418 if (!string_is_empty(label))
419 entry->label = strdup(label);
421 /* Populate 'full_path' field */
422 if (path_is_absolute(path))
424 strlcpy(full_path, path, sizeof(full_path));
425 path_resolve_realpath(full_path, sizeof(full_path), false);
428 fill_pathname_resolve_relative(
429 full_path, m3u_file->path, path,
432 /* Handle unforeseen errors... */
433 if (string_is_empty(full_path))
435 m3u_file_free_entry(entry);
439 entry->full_path = strdup(full_path);
444 /* Removes all entries in M3U file */
445 void m3u_file_clear(m3u_file_t *m3u_file)
452 if (m3u_file->entries)
454 for (i = 0; i < RBUF_LEN(m3u_file->entries); i++)
456 m3u_file_entry_t *entry = &m3u_file->entries[i];
457 m3u_file_free_entry(entry);
460 RBUF_FREE(m3u_file->entries);
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 */
472 m3u_file_t *m3u_file, enum m3u_file_label_type label_type)
476 char base_dir[PATH_MAX_LENGTH];
480 if (!m3u_file || !m3u_file->entries)
483 /* This should never happen */
484 if (string_is_empty(m3u_file->path))
487 /* Get M3U file base directory */
488 if (find_last_slash(m3u_file->path))
490 strlcpy(base_dir, m3u_file->path, sizeof(base_dir));
491 path_basedir(base_dir);
494 /* Open file for writing */
495 file = filestream_open(
497 RETRO_VFS_FILE_ACCESS_WRITE,
498 RETRO_VFS_FILE_ACCESS_HINT_NONE);
503 /* Loop over entries */
504 for (i = 0; i < RBUF_LEN(m3u_file->entries); i++)
506 m3u_file_entry_t *entry = &m3u_file->entries[i];
507 char entry_path[PATH_MAX_LENGTH];
509 entry_path[0] = '\0';
511 if (!entry || string_is_empty(entry->full_path))
514 /* When writing M3U files, entry paths are
516 if (string_is_empty(base_dir))
518 entry_path, entry->full_path,
522 entry_path, entry->full_path, base_dir,
525 if (string_is_empty(entry_path))
528 /* Check if we need to write a label */
529 if (!string_is_empty(entry->label))
533 case M3U_FILE_LABEL_NONSTD:
536 M3U_FILE_NONSTD_LABEL, entry->label,
539 case M3U_FILE_LABEL_EXTSTD:
541 file, "%s%c%s\n%s\n",
542 M3U_FILE_EXTSTD_LABEL, M3U_FILE_EXTSTD_LABEL_TOKEN, entry->label,
545 case M3U_FILE_LABEL_RETRO:
548 entry_path, M3U_FILE_RETRO_LABEL_TOKEN, entry->label);
550 case M3U_FILE_LABEL_NONE:
553 file, "%s\n", entry_path);
557 /* No label - just write entry path */
560 file, "%s\n", entry_path);
564 filestream_close(file);
571 /* Internal qsort function */
572 static int m3u_file_qsort_func(
573 const m3u_file_entry_t *a, const m3u_file_entry_t *b)
578 if (string_is_empty(a->full_path) || string_is_empty(b->full_path))
581 return strcasecmp(a->full_path, b->full_path);
584 /* Sorts M3U file entries in alphabetical order */
585 void m3u_file_qsort(m3u_file_t *m3u_file)
592 num_entries = RBUF_LEN(m3u_file->entries);
598 m3u_file->entries, num_entries,
599 sizeof(m3u_file_entry_t),
600 (int (*)(const void *, const void *))m3u_file_qsort_func);
603 /* Returns true if specified path corresponds
604 * to an M3U file (simple convenience function) */
605 bool m3u_file_is_m3u(const char *path)
607 const char *file_ext = NULL;
610 if (string_is_empty(path))
613 /* Check file extension */
614 file_ext = path_get_extension(path);
616 if (string_is_empty(file_ext))
619 if (!string_is_equal_noncase(file_ext, M3U_FILE_EXT))
622 /* Ensure file exists */
623 if (!path_is_valid(path))
626 /* Ensure we have non-zero file size */
627 file_size = path_get_size(path);