demux: xspf: fix skip nodes end test
[vlc.git] / modules / demux / playlist / xspf.c
blob88d4fdf29673f07eee985d69afe01009d2f3b628
1 /*******************************************************************************
2 * xspf.c : XSPF playlist import functions
3 *******************************************************************************
4 * Copyright (C) 2006-2017 VLC authors and VideoLAN
6 * Authors: Daniel Stränger <vlc at schmaller dot de>
7 * Yoann Peronneau <yoann@videolan.org>
9 * This program is free software; you can redistribute it and/or modify it
10 * under the terms of the GNU Lesser General Public License as published by
11 * the Free Software Foundation; either version 2.1 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Lesser General Public License for more details.
19 * You should have received a copy of the GNU Lesser General Public License
20 * along with this program; if not, write to the Free Software Foundation,
21 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
22 ******************************************************************************/
23 /**
24 * \file modules/demux/playlist/xspf.c
25 * \brief XSPF playlist import functions
28 #ifdef HAVE_CONFIG_H
29 # include "config.h"
30 #endif
32 #include <vlc_common.h>
33 #include <vlc_access.h>
35 #include <vlc_xml.h>
36 #include <vlc_arrays.h>
37 #include <vlc_strings.h>
38 #include <vlc_url.h>
39 #include "playlist.h"
41 #include <limits.h>
43 #define SIMPLE_INTERFACE (input_item_t *p_input,\
44 const char *psz_name,\
45 char *psz_value,\
46 void *opaque)
47 #define COMPLEX_INTERFACE (stream_t *p_stream,\
48 input_item_node_t *p_input_node,\
49 xml_reader_t *p_xml_reader,\
50 const char *psz_element,\
51 bool b_empty_node)
53 /* prototypes */
54 static bool parse_playlist_node COMPLEX_INTERFACE;
55 static bool parse_tracklist_node COMPLEX_INTERFACE;
56 static bool parse_track_node COMPLEX_INTERFACE;
57 static bool parse_extension_node COMPLEX_INTERFACE;
58 static bool parse_extitem_node COMPLEX_INTERFACE;
59 static bool set_item_info SIMPLE_INTERFACE;
60 static bool set_option SIMPLE_INTERFACE;
61 static bool skip_element COMPLEX_INTERFACE;
63 /* datatypes */
64 typedef struct
66 const char *name;
67 union
69 bool (*smpl) SIMPLE_INTERFACE;
70 bool (*cmplx) COMPLEX_INTERFACE;
71 } pf_handler;
72 bool cmplx;
73 } xml_elem_hnd_t;
75 typedef struct
77 input_item_t **pp_tracklist;
78 int i_tracklist_entries;
79 int i_track_id;
80 char * psz_base;
81 } xspf_sys_t;
83 static int ReadDir(stream_t *, input_item_node_t *);
85 /**
86 * \brief XSPF submodule initialization function
88 int Import_xspf(vlc_object_t *p_this)
90 stream_t *p_stream = (stream_t *)p_this;
92 CHECK_FILE(p_stream);
94 if( !stream_HasExtension( p_stream, ".xspf" )
95 && !stream_IsMimeType( p_stream->p_source, "application/xspf+xml" ) )
96 return VLC_EGENERIC;
98 xspf_sys_t *sys = calloc(1, sizeof (*sys));
99 if (unlikely(sys == NULL))
100 return VLC_ENOMEM;
102 msg_Dbg(p_stream, "using XSPF playlist reader");
103 p_stream->p_sys = sys;
104 p_stream->pf_readdir = ReadDir;
105 p_stream->pf_control = access_vaDirectoryControlHelper;
107 return VLC_SUCCESS;
110 void Close_xspf(vlc_object_t *p_this)
112 stream_t *p_stream = (stream_t *)p_this;
113 xspf_sys_t *p_sys = p_stream->p_sys;
114 for (int i = 0; i < p_sys->i_tracklist_entries; i++)
115 if (p_sys->pp_tracklist[i])
116 input_item_Release(p_sys->pp_tracklist[i]);
117 free(p_sys->pp_tracklist);
118 free(p_sys->psz_base);
119 free(p_sys);
123 * \brief demuxer function for XSPF parsing
125 static int ReadDir(stream_t *p_stream, input_item_node_t *p_subitems)
127 xspf_sys_t *sys = p_stream->p_sys;
128 int i_ret = -1;
129 xml_reader_t *p_xml_reader = NULL;
130 const char *name = NULL;
132 sys->pp_tracklist = NULL;
133 sys->i_tracklist_entries = 0;
134 sys->i_track_id = -1;
135 sys->psz_base = strdup(p_stream->psz_url);
137 /* create new xml parser from stream */
138 p_xml_reader = xml_ReaderCreate(p_stream, p_stream->p_source);
139 if (!p_xml_reader)
140 goto end;
142 /* locating the root node */
143 if (xml_ReaderNextNode(p_xml_reader, &name) != XML_READER_STARTELEM)
145 msg_Err(p_stream, "can't read xml stream");
146 goto end;
149 /* checking root node name */
150 if (strcmp(name, "playlist"))
152 msg_Err(p_stream, "invalid root node name <%s>", name);
153 goto end;
156 if(xml_ReaderIsEmptyElement(p_xml_reader))
157 goto end;
159 i_ret = parse_playlist_node(p_stream, p_subitems,
160 p_xml_reader, "playlist", false ) ? 0 : -1;
162 for (int i = 0 ; i < sys->i_tracklist_entries ; i++)
164 input_item_t *p_new_input = sys->pp_tracklist[i];
165 if (p_new_input)
167 input_item_node_AppendItem(p_subitems, p_new_input);
171 end:
172 if (p_xml_reader)
173 xml_ReaderDelete(p_xml_reader);
174 return i_ret; /* Needed for correct operation of go back */
177 static const xml_elem_hnd_t *get_handler(const xml_elem_hnd_t *tab, size_t n, const char *name)
179 for (size_t i = 0; i < n; i++)
180 if (!strcmp(name, tab[i].name))
181 return &tab[i];
182 return NULL;
185 static const char *get_node_attribute(xml_reader_t *p_xml_reader, const char *psz_name)
187 const char *name, *value;
188 while ((name = xml_ReaderNextAttr(p_xml_reader, &value)) != NULL)
190 if (!strcmp(name, psz_name))
191 return value;
193 return NULL;
197 * \brief generic node parsing of a XSPF playlist
198 * \param p_stream stream instance
199 * \param input_item_node_t current input node
200 * \param p_input_item current input item
201 * \param p_xml_reader xml reader instance
202 * \param psz_root_node current node name to parse
203 * \param pl_elements xml_elem_hnd_t handlers array
204 * \param i_pl_elements xml_elem_hnd_t array count
206 static bool parse_node(stream_t *p_stream,
207 input_item_node_t *p_input_node, input_item_t *p_input_item,
208 xml_reader_t *p_xml_reader, const char *psz_root_node,
209 const xml_elem_hnd_t *pl_elements, size_t i_pl_elements)
211 bool b_ret = false;
213 char *psz_value = NULL;
214 const char *name;
215 int i_node;
216 const xml_elem_hnd_t *p_handler = NULL;
218 while ((i_node = xml_ReaderNextNode(p_xml_reader, &name)) > XML_READER_NONE)
220 const bool b_empty = xml_ReaderIsEmptyElement(p_xml_reader);
222 switch (i_node)
224 case XML_READER_STARTELEM:
225 FREENULL(psz_value);
226 if (!*name)
228 msg_Err(p_stream, "invalid XML stream");
229 goto end;
232 p_handler = get_handler(pl_elements, i_pl_elements, name);
233 if (!p_handler)
235 msg_Warn(p_stream, "skipping unexpected element <%s>", name);
236 if(!skip_element(NULL, NULL, p_xml_reader, name, b_empty))
237 return false;
239 else
241 /* complex content is parsed in a separate function */
242 if (p_handler->cmplx)
244 if (!p_handler->pf_handler.cmplx(p_stream, p_input_node,
245 p_xml_reader, p_handler->name,
246 b_empty))
247 return false;
248 /* Complex reader does read the named end element */
249 p_handler = NULL;
252 break;
254 case XML_READER_TEXT:
255 free(psz_value);
256 if(!p_handler)
258 psz_value = NULL;
260 else
262 psz_value = strdup(name);
263 if (unlikely(!psz_value))
264 goto end;
266 break;
268 case XML_READER_ENDELEM:
269 /* leave if the current parent node is terminated */
270 if (!strcmp(name, psz_root_node))
272 b_ret = true;
273 goto end;
276 if(p_handler)
278 /* there MUST have been a start tag for that element name */
279 if (strcmp(p_handler->name, name))
281 msg_Err(p_stream, "there's no open element left for <%s>", name);
282 goto end;
285 if (p_handler->pf_handler.smpl)
286 p_handler->pf_handler.smpl(p_input_item, p_handler->name,
287 psz_value, p_stream->p_sys);
289 free(psz_value);
290 psz_value = NULL;
291 p_handler = NULL;
293 break;
297 end:
298 free(psz_value);
300 return b_ret;
304 * \brief parse the root node of a XSPF playlist
305 * \param p_stream stream instance
306 * \param p_input_item current input item
307 * \param p_xml_reader xml reader instance
308 * \param psz_element name of element to parse
310 static bool parse_playlist_node COMPLEX_INTERFACE
312 xspf_sys_t *sys = p_stream->p_sys;
314 if(b_empty_node)
315 return false;
317 /* read all playlist attributes */
318 const char *psz_version = get_node_attribute(p_xml_reader, "version");
319 if(!psz_version || (strcmp(psz_version, "0") && strcmp(psz_version, "1")))
321 /* attribute version is mandatory !!! */
322 if(!psz_version)
323 msg_Warn(p_stream, "<playlist> requires \"version\" attribute");
324 else
325 msg_Warn(p_stream, "unsupported XSPF version %s", psz_version);
326 return false;
329 const char *psz_base = get_node_attribute(p_xml_reader, "xml:base");
330 if(psz_base)
332 free(sys->psz_base);
333 sys->psz_base = strdup(psz_base);
336 static const xml_elem_hnd_t pl_elements[] =
337 { {"title", {.smpl = set_item_info}, false },
338 {"creator", {.smpl = set_item_info}, false },
339 {"annotation", {.smpl = set_item_info}, false },
340 {"info", {NULL}, false },
341 {"location", {NULL}, false },
342 {"identifier", {NULL}, false },
343 {"image", {.smpl = set_item_info}, false },
344 {"date", {NULL}, false },
345 {"license", {NULL}, false },
346 {"attribution", {.cmplx = skip_element}, true },
347 {"link", {NULL}, false },
348 {"meta", {NULL}, false },
349 {"extension", {.cmplx = parse_extension_node}, true },
350 {"trackList", {.cmplx = parse_tracklist_node}, true },
353 return parse_node(p_stream, p_input_node, p_input_node->p_item,
354 p_xml_reader, psz_element,
355 pl_elements, ARRAY_SIZE(pl_elements));
359 * \brief parses the tracklist node which only may contain <track>s
361 static bool parse_tracklist_node COMPLEX_INTERFACE
363 VLC_UNUSED(psz_element);
365 if(b_empty_node)
366 return true;
368 /* parse the child elements */
369 static const xml_elem_hnd_t pl_elements[] =
370 { {"track", {.cmplx = parse_track_node}, true },
373 return parse_node(p_stream, p_input_node, p_input_node->p_item,
374 p_xml_reader, psz_element,
375 pl_elements, ARRAY_SIZE(pl_elements));
379 * \brief handles the <location> elements
381 static bool parse_location SIMPLE_INTERFACE
383 VLC_UNUSED(psz_name);
384 xspf_sys_t *p_sys = (xspf_sys_t *) opaque;
385 char* psz_uri = ProcessMRL( psz_value, p_sys->psz_base );
386 if(psz_uri)
388 input_item_SetURI(p_input, psz_uri);
389 free(psz_uri);
391 return psz_uri != NULL;
395 * \brief parse one track element
396 * \param COMPLEX_INTERFACE
398 static bool parse_track_node COMPLEX_INTERFACE
400 xspf_sys_t *p_sys = p_stream->p_sys;
402 if(b_empty_node)
403 return true;
405 input_item_t *p_new_input = input_item_New(NULL, NULL);
406 if (!p_new_input)
407 return false;
409 /* increfs p_new_input */
410 input_item_node_t *p_new_node = input_item_node_Create(p_new_input);
411 if(!p_new_node)
413 input_item_Release(p_new_input);
414 return false;
417 /* reset i_track_id */
418 p_sys->i_track_id = -1;
420 static const xml_elem_hnd_t track_elements[] =
421 { {"location", {.smpl = parse_location}, false },
422 {"identifier", {NULL}, false },
423 {"title", {.smpl = set_item_info}, false },
424 {"creator", {.smpl = set_item_info}, false },
425 {"annotation", {.smpl = set_item_info}, false },
426 {"info", {.smpl = set_item_info}, false },
427 {"image", {.smpl = set_item_info}, false },
428 {"album", {.smpl = set_item_info}, false },
429 {"trackNum", {.smpl = set_item_info}, false },
430 {"duration", {.smpl = set_item_info}, false },
431 {"link", {NULL}, false },
432 {"meta", {NULL}, false },
433 {"extension", {.cmplx = parse_extension_node}, true },
436 bool b_ret = parse_node(p_stream, p_new_node, p_new_input,
437 p_xml_reader, psz_element,
438 track_elements, ARRAY_SIZE(track_elements));
439 if(b_ret)
441 input_item_CopyOptions(p_new_input, p_input_node->p_item);
443 /* Make sure we have a URI */
444 char *psz_uri = input_item_GetURI(p_new_input);
445 if (!psz_uri)
446 input_item_SetURI(p_new_input, "vlc://nop");
447 else
448 free(psz_uri);
450 if (p_sys->i_track_id < 0 ||
451 p_sys->i_track_id == INT_MAX ||
452 (size_t)p_sys->i_track_id >= (SIZE_MAX / sizeof(p_new_input)))
454 input_item_node_AppendNode(p_input_node, p_new_node);
455 p_new_node = NULL;
457 else
459 /* Extend array as needed */
460 if (p_sys->i_track_id >= p_sys->i_tracklist_entries)
462 input_item_t **pp;
463 pp = realloc(p_sys->pp_tracklist,
464 (p_sys->i_track_id + 1) * sizeof(*pp));
465 if (pp)
467 p_sys->pp_tracklist = pp;
468 while (p_sys->i_track_id >= p_sys->i_tracklist_entries)
469 pp[p_sys->i_tracklist_entries++] = NULL;
473 if (p_sys->i_track_id < p_sys->i_tracklist_entries)
475 input_item_t **pp_insert = &p_sys->pp_tracklist[p_sys->i_track_id];
477 if (*pp_insert != NULL)
479 msg_Warn(p_stream, "track ID %d collision", p_sys->i_track_id);
480 input_item_node_AppendItem(p_input_node, p_new_input);
482 else
484 *pp_insert = p_new_input;
485 p_new_input = NULL;
488 else b_ret = false;
492 if(p_new_node)
493 input_item_node_Delete(p_new_node); /* decrefs p_new_input */
494 if(p_new_input)
495 input_item_Release(p_new_input);
497 return b_ret;
501 * \brief handles the supported <track> sub-elements
503 static bool set_item_info SIMPLE_INTERFACE
505 VLC_UNUSED(opaque);
506 /* exit if setting is impossible */
507 if (!psz_name || !psz_value || !p_input)
508 return false;
510 vlc_xml_decode(psz_value);
512 /* handle each info element in a separate "if" clause */
513 if (!strcmp(psz_name, "title"))
514 input_item_SetTitle(p_input, psz_value);
515 else if (!strcmp(psz_name, "creator"))
516 input_item_SetArtist(p_input, psz_value);
517 else if (!strcmp(psz_name, "album"))
518 input_item_SetAlbum(p_input, psz_value);
519 else if (!strcmp(psz_name, "trackNum"))
520 input_item_SetTrackNum(p_input, psz_value);
521 else if (!strcmp(psz_name, "duration"))
522 p_input->i_duration = atol(psz_value) * INT64_C(1000);
523 else if (!strcmp(psz_name, "annotation"))
524 input_item_SetDescription(p_input, psz_value);
525 else if (!strcmp(psz_name, "info"))
526 input_item_SetURL(p_input, psz_value);
527 else if (!strcmp(psz_name, "image") && *psz_value)
528 input_item_SetArtURL(p_input, psz_value);
529 return true;
533 * \brief handles the <vlc:option> elements
535 static bool set_option SIMPLE_INTERFACE
537 VLC_UNUSED(opaque);
538 /* exit if setting is impossible */
539 if (!psz_name || !psz_value || !p_input)
540 return false;
542 vlc_xml_decode(psz_value);
544 input_item_AddOption(p_input, psz_value, 0);
546 return true;
550 * \brief handles the <vlc:id> elements
552 static bool parse_vlcid SIMPLE_INTERFACE
554 VLC_UNUSED(p_input); VLC_UNUSED(psz_name);
555 xspf_sys_t *sys = (xspf_sys_t *) opaque;
556 if(psz_value)
557 sys->i_track_id = atoi(psz_value);
558 return true;
562 * \brief parse the vlc:node of a XSPF playlist
564 static bool parse_vlcnode_node COMPLEX_INTERFACE
566 input_item_t *p_input_item = p_input_node->p_item;
567 char *psz_title = NULL;
569 if(b_empty_node)
570 return true;
572 /* read all extension node attributes */
573 const char *psz_attr = get_node_attribute(p_xml_reader, "title");
574 if(psz_attr)
576 psz_title = strdup(psz_attr);
577 if (likely(psz_title != NULL))
578 vlc_xml_decode(psz_title);
581 /* attribute title is mandatory */
582 if (!psz_title)
584 msg_Warn(p_stream, "<vlc:node> requires \"title\" attribute");
585 return false;
587 input_item_t *p_new_input =
588 input_item_NewDirectory("vlc://nop", psz_title, ITEM_NET_UNKNOWN);
589 free(psz_title);
590 if (p_new_input)
592 p_input_node =
593 input_item_node_AppendItem(p_input_node, p_new_input);
594 p_input_item = p_new_input;
597 /* parse the child elements */
598 static const xml_elem_hnd_t pl_elements[] =
599 { {"vlc:node", {.cmplx = parse_vlcnode_node}, true },
600 {"vlc:item", {.cmplx = parse_extitem_node}, true },
601 {"vlc:id", {.smpl = parse_vlcid}, false },
602 {"vlc:option", {.smpl = set_option}, false },
605 bool b_ret = parse_node(p_stream, p_input_node, p_input_item,
606 p_xml_reader, psz_element,
607 pl_elements, ARRAY_SIZE(pl_elements));
609 if (p_new_input)
610 input_item_Release(p_new_input);
612 return b_ret;
616 * \brief parse the extension node of a XSPF playlist
618 static bool parse_extension_node COMPLEX_INTERFACE
620 if(b_empty_node)
621 return false;
623 const char *psz_application = get_node_attribute(p_xml_reader, "application");
624 if (!psz_application)
626 msg_Warn(p_stream, "<extension> requires \"application\" attribute");
627 return false;
630 /* Skip the extension if the application is not vlc
631 This will skip all children of the current node */
632 if (strcmp(psz_application, "http://www.videolan.org/vlc/playlist/0"))
634 msg_Dbg(p_stream, "Skipping \"%s\" extension tag", psz_application);
635 return skip_element( NULL, NULL, p_xml_reader, psz_element, b_empty_node );
638 /* parse the child elements */
639 static const xml_elem_hnd_t pl_elements[] =
640 { {"vlc:node", {.cmplx = parse_vlcnode_node}, true },
641 {"vlc:id", {.smpl = parse_vlcid}, false },
642 {"vlc:option", {.smpl = set_option}, false },
645 return parse_node(p_stream, p_input_node, p_input_node->p_item,
646 p_xml_reader, psz_element,
647 pl_elements, ARRAY_SIZE(pl_elements));
651 * \brief parse the extension item node of a XSPF playlist
654 static bool parse_extitem_node COMPLEX_INTERFACE
656 VLC_UNUSED(psz_element);
657 xspf_sys_t *sys = p_stream->p_sys;
658 input_item_t *p_new_input = NULL;
659 int i_tid = -1;
661 if(!b_empty_node)
662 return false;
664 const char *psz_tid = get_node_attribute(p_xml_reader, "tid");
665 if(psz_tid)
666 i_tid = atoi(psz_tid);
668 /* attribute href is mandatory */
669 if (!psz_tid || i_tid < 0)
671 msg_Warn(p_stream, "<vlc:item> requires valid \"tid\" attribute");
672 return false;
675 if (i_tid >= sys->i_tracklist_entries ||
676 !(p_new_input = sys->pp_tracklist[ i_tid ]) )
678 msg_Warn(p_stream, "non existing \"tid\" %d referenced", i_tid);
679 return true;
682 input_item_node_AppendItem(p_input_node, p_new_input);
683 input_item_Release(p_new_input);
684 sys->pp_tracklist[i_tid] = NULL;
686 return true;
690 * \brief skips complex element content that we can't manage
692 static bool skip_element COMPLEX_INTERFACE
694 VLC_UNUSED(p_stream); VLC_UNUSED(p_input_node);
696 if(b_empty_node)
697 return true;
699 const char *name;
700 for (unsigned lvl = 1; lvl;)
701 switch (xml_ReaderNextNode(p_xml_reader, &name))
703 case XML_READER_STARTELEM:
705 if( !xml_ReaderIsEmptyElement( p_xml_reader ) )
706 ++lvl;
707 break;
709 case XML_READER_ENDELEM:
710 if(lvl == 1 &&
711 name && psz_element && strcmp(psz_element, name))
712 return false;
713 lvl--;
714 break;
715 case XML_READER_NONE:
716 case XML_READER_ERROR:
717 return false;
720 return true;