Commit | Line | Data |
---|---|---|
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... */ | |
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 | } |