summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--DOCS/man/en/options.rst16
-rw-r--r--Makefile3
-rw-r--r--demux/demux.c4
-rw-r--r--demux/demux.h3
-rw-r--r--demux/demux_playlist.c136
-rw-r--r--mpvcore/asxparser.c13
-rw-r--r--mpvcore/command.c2
-rw-r--r--mpvcore/mplayer.c45
-rw-r--r--mpvcore/options.c2
-rw-r--r--mpvcore/options.h1
-rw-r--r--mpvcore/parser-mpcmd.c3
-rw-r--r--mpvcore/playlist_parser.c69
-rw-r--r--mpvcore/playlist_parser.h9
-rw-r--r--mpvcore/timeline/tl_cue.c2
-rw-r--r--stream/stream.c18
-rw-r--r--stream/stream.h2
16 files changed, 240 insertions, 88 deletions
diff --git a/DOCS/man/en/options.rst b/DOCS/man/en/options.rst
index 37335154f4..61fa750b76 100644
--- a/DOCS/man/en/options.rst
+++ b/DOCS/man/en/options.rst
@@ -1198,6 +1198,22 @@
``--list-properties``
Print a list of the available properties.
+``--load-unsafe-playlists``
+ Normally, something like ``mpv playlist.m3u`` won't load the playlist. This
+ is because the playlist code is unsafe. (This is the same in all other
+ variations of MPlayer.)
+
+ See ``--playlist`` for details.
+
+ Note: this option will allow opening playlists using the ``playlist``
+ special demuxer. The ``--playlist`` uses different code, and supports more
+ playlist formats than the playlist demuxer. This means that for now, the
+ ``--playlist`` option should always be used if you intend to open playlists.
+ Background: the special demuxer contains newly written code, while the
+ ``--playlist`` option uses the old MPlayer code. Adding support for more
+ playlist formats to the special demuxer is work in progress, and eventually
+ the old code should disappear.
+
``--loop=<number|inf|no>``
Loops playback ``<number>`` times. ``inf`` means forever and ``no`` disables
looping. If several files are specified on command line, the entire playlist
diff --git a/Makefile b/Makefile
index e5a0c576b8..a2bb6b7300 100644
--- a/Makefile
+++ b/Makefile
@@ -171,8 +171,9 @@ SOURCES = talloc.c \
demux/demux_lavf.c \
demux/demux_mf.c \
demux/demux_mkv.c \
- demux/demux_subreader.c \
+ demux/demux_playlist.c \
demux/demux_raw.c \
+ demux/demux_subreader.c \
demux/ebml.c \
demux/mf.c \
mpvcore/asxparser.c \
diff --git a/demux/demux.c b/demux/demux.c
index 272b089871..404050a420 100644
--- a/demux/demux.c
+++ b/demux/demux.c
@@ -59,6 +59,7 @@ extern const demuxer_desc_t demuxer_desc_lavf;
extern const demuxer_desc_t demuxer_desc_mng;
extern const demuxer_desc_t demuxer_desc_libass;
extern const demuxer_desc_t demuxer_desc_subreader;
+extern const demuxer_desc_t demuxer_desc_playlist;
/* Please do not add any new demuxers here. If you want to implement a new
* demuxer, add it to libavformat, except for wrappers around external
@@ -81,6 +82,7 @@ const demuxer_desc_t *const demuxer_list[] = {
#ifdef CONFIG_MNG
&demuxer_desc_mng,
#endif
+ &demuxer_desc_playlist,
// Pretty aggressive, so should be last.
&demuxer_desc_subreader,
/* Please do not add any new demuxers here. If you want to implement a new
@@ -309,6 +311,8 @@ static void free_sh_stream(struct sh_stream *sh)
void free_demuxer(demuxer_t *demuxer)
{
+ if (!demuxer)
+ return;
if (demuxer->desc->close)
demuxer->desc->close(demuxer);
// free streams:
diff --git a/demux/demux.h b/demux/demux.h
index 06bf65809b..6976982e15 100644
--- a/demux/demux.h
+++ b/demux/demux.h
@@ -182,6 +182,9 @@ typedef struct demuxer {
// for trivial demuxers which just read the whole file for codec to use
struct bstr file_contents;
+ // If the file is a playlist file
+ struct playlist *playlist;
+
void *priv; // demuxer-specific internal data
char **info; // metadata
struct MPOpts *opts;
diff --git a/demux/demux_playlist.c b/demux/demux_playlist.c
new file mode 100644
index 0000000000..b924140497
--- /dev/null
+++ b/demux/demux_playlist.c
@@ -0,0 +1,136 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "mpvcore/mp_common.h"
+#include "mpvcore/options.h"
+#include "mpvcore/mp_msg.h"
+#include "mpvcore/playlist.h"
+#include "mpvcore/path.h"
+#include "stream/stream.h"
+#include "demux.h"
+
+#define PROBE_SIZE (8 * 1024)
+
+struct pl_parser {
+ struct stream *s;
+ char buffer[8 * 1024];
+ int utf16;
+ struct playlist *pl;
+ bool probing;
+};
+
+static char *pl_get_line0(struct pl_parser *p)
+{
+ char *res = stream_read_line(p->s, p->buffer, sizeof(p->buffer), p->utf16);
+ if (res) {
+ int len = strlen(res);
+ if (len > 0 && res[len - 1] == '\n')
+ res[len - 1] = '\0';
+ }
+ return res;
+}
+
+static bstr pl_get_line(struct pl_parser *p)
+{
+ return bstr0(pl_get_line0(p));
+}
+
+static void pl_add(struct pl_parser *p, bstr entry)
+{
+ char *s = bstrto0(NULL, entry);
+ playlist_add_file(p->pl, s);
+ talloc_free(s);
+}
+
+static bool pl_eof(struct pl_parser *p)
+{
+ return p->s->eof;
+}
+
+static int parse_m3u(struct pl_parser *p)
+{
+ bstr line = bstr_strip(pl_get_line(p));
+ if (!bstr_equals0(line, "#EXTM3U"))
+ return -1;
+ if (p->probing)
+ return 0;
+ while (!pl_eof(p)) {
+ line = bstr_lstrip(pl_get_line(p));
+ if (line.len == 0 || bstr_startswith0(line, "#"))
+ continue;
+ pl_add(p, line);
+ }
+ return 0;
+}
+
+struct pl_format {
+ const char *name;
+ int (*parse)(struct pl_parser *p);
+};
+
+static const struct pl_format formats[] = {
+ {"m3u", parse_m3u},
+};
+
+static const struct pl_format *probe_pl(struct pl_parser *p, bool force)
+{
+ int64_t start = stream_tell(p->s);
+ for (int n = 0; n < MP_ARRAY_SIZE(formats); n++) {
+ const struct pl_format *fmt = &formats[n];
+ stream_seek(p->s, start);
+ if (fmt->parse(p) >= 0)
+ return fmt;
+ }
+ return NULL;
+}
+
+static int open_file(struct demuxer *demuxer, enum demux_check check)
+{
+ bool force = check < DEMUX_CHECK_UNSAFE || check == DEMUX_CHECK_REQUEST;
+
+ struct pl_parser *p = talloc_zero(NULL, struct pl_parser);
+ p->pl = talloc_zero(p, struct playlist);
+
+ bstr probe_buf = stream_peek(demuxer->stream, PROBE_SIZE);
+ p->s = open_memory_stream(probe_buf.start, probe_buf.len);
+ p->utf16 = stream_skip_bom(p->s);
+ p->probing = true;
+ const struct pl_format *fmt = probe_pl(p, force);
+ free_stream(p->s);
+ playlist_clear(p->pl);
+ if (!fmt) {
+ talloc_free(p);
+ return -1;
+ }
+
+ p->probing = false;
+ p->s = demuxer->stream;
+ p->utf16 = stream_skip_bom(p->s);
+ bool ok = fmt->parse(p) >= 0;
+ if (ok)
+ playlist_add_base_path(p->pl, mp_dirname(demuxer->filename));
+ demuxer->playlist = talloc_steal(demuxer, p->pl);
+ demuxer->filetype = fmt->name;
+ talloc_free(p);
+ return ok ? 0 : -1;
+}
+
+const struct demuxer_desc demuxer_desc_playlist = {
+ .name = "playlist",
+ .desc = "Playlist file",
+ .open = open_file,
+};
diff --git a/mpvcore/asxparser.c b/mpvcore/asxparser.c
index 804e796f85..16646b9347 100644
--- a/mpvcore/asxparser.c
+++ b/mpvcore/asxparser.c
@@ -449,7 +449,6 @@ asx_parse_ref(ASX_Parser_t* parser, char** attribs) {
static void asx_parse_entryref(ASX_Parser_t* parser,char* buffer,char** _attribs) {
char *href;
- stream_t* stream;
if(parser->deep > 0)
return;
@@ -459,16 +458,8 @@ static void asx_parse_entryref(ASX_Parser_t* parser,char* buffer,char** _attribs
asx_warning_attrib_required(parser,"ENTRYREF" ,"HREF" );
return;
}
- stream=stream_open(href, NULL);
- if(!stream) {
- mp_msg(MSGT_PLAYTREE,MSGL_WARN,"Can't open playlist %s\n",href);
- free(href);
- return;
- }
-
- mp_msg(MSGT_PLAYTREE,MSGL_ERR,"Not recursively loading playlist %s\n",href);
-
- free_stream(stream);
+ mp_msg(MSGT_PLAYTREE,MSGL_ERR,"Recursive playlist %s\n", href);
+ playlist_add_file(parser->pl, href);
free(href);
//mp_msg(MSGT_PLAYTREE,MSGL_INFO,"Need to implement entryref\n");
}
diff --git a/mpvcore/command.c b/mpvcore/command.c
index a9a96e6ebb..db4b2a7a9c 100644
--- a/mpvcore/command.c
+++ b/mpvcore/command.c
@@ -2319,7 +2319,7 @@ void run_command(MPContext *mpctx, mp_cmd_t *cmd)
case MP_CMD_LOADLIST: {
char *filename = cmd->args[0].v.s;
bool append = cmd->args[1].v.i;
- struct playlist *pl = playlist_parse_file(filename);
+ struct playlist *pl = playlist_parse_file(filename, opts);
if (pl) {
if (!append)
playlist_clear(mpctx->playlist);
diff --git a/mpvcore/mplayer.c b/mpvcore/mplayer.c
index 2d5265d41b..43a7b5d9b0 100644
--- a/mpvcore/mplayer.c
+++ b/mpvcore/mplayer.c
@@ -4133,6 +4133,19 @@ static void stream_dump(struct MPContext *mpctx)
}
}
+// Replace the current playlist entry with playlist contents. Moves the entries
+// from the given playlist pl, so the entries don't actually need to be copied.
+static void transfer_playlist(struct MPContext *mpctx, struct playlist *pl)
+{
+ if (mpctx->demuxer->playlist->first) {
+ playlist_transfer_entries(mpctx->playlist, mpctx->demuxer->playlist);
+ if (mpctx->playlist->current)
+ playlist_remove(mpctx->playlist, mpctx->playlist->current);
+ } else {
+ MP_WARN(mpctx, "Empty playlist!\n");
+ }
+}
+
// Start playing the current playlist entry.
// Handle initialization and deinitialization.
static void play_current_file(struct MPContext *mpctx)
@@ -4213,11 +4226,7 @@ static void play_current_file(struct MPContext *mpctx)
mpctx->resolve_result = resolve_url(stream_filename, opts);
if (mpctx->resolve_result) {
if (mpctx->resolve_result->playlist) {
- // Replace entry with playlist contents
- playlist_transfer_entries(mpctx->playlist,
- mpctx->resolve_result->playlist);
- if (mpctx->playlist->current)
- playlist_remove(mpctx->playlist, mpctx->playlist->current);
+ transfer_playlist(mpctx, mpctx->resolve_result->playlist);
goto terminate_playback;
}
stream_filename = mpctx->resolve_result->url;
@@ -4258,12 +4267,29 @@ goto_reopen_demuxer: ;
mpctx->demuxer = demux_open(mpctx->stream, opts->demuxer_name, NULL, opts);
mpctx->master_demuxer = mpctx->demuxer;
-
if (!mpctx->demuxer) {
mp_tmsg(MSGT_CPLAYER, MSGL_ERR, "Failed to recognize file format.\n");
goto terminate_playback;
}
+ MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources, mpctx->demuxer);
+
+ mpctx->initialized_flags |= INITIALIZED_DEMUXER;
+
+ if (mpctx->demuxer->playlist) {
+ if (mpctx->demuxer->stream->safe_origin || opts->load_unsafe_playlists) {
+ transfer_playlist(mpctx, mpctx->demuxer->playlist);
+ } else {
+ MP_ERR(mpctx, "\nThis looks like a playlist, but playlist support "
+ "will not be used automatically.\nThe main problem with "
+ "playlist safety is that playlist entries can be arbitrary,\n"
+ "and an attacker could make mpv poke around in your local "
+ "filesystem or network.\nUse --playlist=file or the "
+ "--load-unsafe-playlists option to load them anyway.\n");
+ }
+ goto terminate_playback;
+ }
+
if (mpctx->demuxer->matroska_data.ordered_chapters)
build_ordered_chapter_timeline(mpctx);
@@ -4275,11 +4301,6 @@ goto_reopen_demuxer: ;
print_timeline(mpctx);
- if (!mpctx->num_sources) {
- MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources,
- mpctx->demuxer);
- }
-
if (mpctx->timeline) {
// With Matroska, the "master" file usually dictates track layout etc.
// On the contrary, the EDL and CUE demuxers are empty wrappers, as
@@ -4298,8 +4319,6 @@ goto_reopen_demuxer: ;
if (mpctx->timeline)
timeline_set_part(mpctx, mpctx->timeline_part, true);
- mpctx->initialized_flags |= INITIALIZED_DEMUXER;
-
add_subtitle_fonts_from_sources(mpctx);
open_subtitles_from_options(mpctx);
diff --git a/mpvcore/options.c b/mpvcore/options.c
index 733b104a46..e508262378 100644
--- a/mpvcore/options.c
+++ b/mpvcore/options.c
@@ -669,6 +669,8 @@ const m_option_t mp_opts[] = {
OPT_DOUBLE("chapter-seek-threshold", chapter_seek_threshold, 0),
+ OPT_FLAG("load-unsafe-playlists", load_unsafe_playlists, 0),
+
// a-v sync stuff:
OPT_FLAG("correct-pts", correct_pts, 0),
OPT_CHOICE("pts-association-mode", user_pts_assoc_mode, 0,
diff --git a/mpvcore/options.h b/mpvcore/options.h
index 1ff50688ce..033d3c89c6 100644
--- a/mpvcore/options.h
+++ b/mpvcore/options.h
@@ -87,6 +87,7 @@ typedef struct MPOpts {
int ordered_chapters;
int chapter_merge_threshold;
double chapter_seek_threshold;
+ int load_unsafe_playlists;
int quiet;
int load_config;
int use_filedir_conf;
diff --git a/mpvcore/parser-mpcmd.c b/mpvcore/parser-mpcmd.c
index 55615d950e..e85085b808 100644
--- a/mpvcore/parser-mpcmd.c
+++ b/mpvcore/parser-mpcmd.c
@@ -114,6 +114,7 @@ static bool split_opt(struct parse_state *p)
int m_config_parse_mp_command_line(m_config_t *config, struct playlist *files,
int argc, char **argv)
{
+ struct MPOpts *opts = config->optstruct;
int ret = M_OPT_UNKNOWN;
int mode = 0;
struct playlist_entry *local_start = NULL;
@@ -187,7 +188,7 @@ int m_config_parse_mp_command_line(m_config_t *config, struct playlist *files,
if (bstrcmp0(p.arg, "playlist") == 0) {
// append the playlist to the local args
char *param0 = bstrdup0(NULL, p.param);
- struct playlist *pl = playlist_parse_file(param0);
+ struct playlist *pl = playlist_parse_file(param0, opts);
talloc_free(param0);
if (!pl) {
mp_tmsg(MSGT_CFGPARSER, MSGL_FATAL,
diff --git a/mpvcore/playlist_parser.c b/mpvcore/playlist_parser.c
index 4573e133ae..920a004c50 100644
--- a/mpvcore/playlist_parser.c
+++ b/mpvcore/playlist_parser.c
@@ -16,6 +16,11 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
+/*
+ * Warning: this is outdated, crappy code. It is used only for --playlist.
+ * New or cleaned up code should be added to demux_playlist.c instead.
+ */
+
#include "config.h"
#include <stdlib.h>
#include <stdio.h>
@@ -406,43 +411,6 @@ static bool parse_ref_ini(play_tree_parser_t* p) {
return true;
}
-static bool parse_m3u(play_tree_parser_t* p) {
- char* line;
-
- mp_msg(MSGT_PLAYTREE,MSGL_V,"Trying extended m3u playlist...\n");
- if (!(line = play_tree_parser_get_line(p)))
- return NULL;
- strstrip(line);
- if(strcasecmp(line,"#EXTM3U"))
- return NULL;
- mp_msg(MSGT_PLAYTREE,MSGL_V,"Detected extended m3u playlist format\n");
- play_tree_parser_stop_keeping(p);
-
- while((line = play_tree_parser_get_line(p)) != NULL) {
- strstrip(line);
- if(line[0] == '\0')
- continue;
- /* EXTM3U files contain such lines:
- * #EXTINF:<seconds>, <title>
- * followed by a line with the filename
- * for now we have no place to put that
- * so we just skip that extra-info ::atmos
- */
- if(line[0] == '#') {
-#if 0 /* code functional */
- if(strncasecmp(line,"#EXTINF:",8) == 0) {
- mp_msg(MSGT_PLAYTREE,MSGL_INFO,"[M3U] Duration: %dsec Title: %s\n",
- strtol(line+8,&line,10), line+2);
- }
-#endif
- continue;
- }
- playlist_add_file(p->pl, line);
- }
-
- return true;
-}
-
static bool parse_smil(play_tree_parser_t* p) {
int entrymode=0;
char* line,source[512],*pos,*s_start,*s_end,*src_line;
@@ -697,9 +665,11 @@ err_out:
return success;
}
-struct playlist *playlist_parse_file(const char *file)
+static struct playlist *do_parse(struct stream* stream, bool forced);
+
+struct playlist *playlist_parse_file(const char *file, struct MPOpts *opts)
{
- stream_t *stream = stream_open(file, NULL);
+ stream_t *stream = stream_open(file, opts);
if(!stream) {
mp_msg(MSGT_PLAYTREE,MSGL_ERR,
"Error while opening playlist file %s: %s\n",
@@ -710,7 +680,7 @@ struct playlist *playlist_parse_file(const char *file)
mp_msg(MSGT_PLAYTREE, MSGL_V,
"Parsing playlist file %s...\n", file);
- struct playlist *ret = playlist_parse(stream);
+ struct playlist *ret = do_parse(stream, true);
free_stream(stream);
playlist_add_base_path(ret, mp_dirname(file));
@@ -723,7 +693,6 @@ typedef bool (*parser_fn)(play_tree_parser_t *);
static const parser_fn pl_parsers[] = {
parse_asx,
parse_pls,
- parse_m3u,
parse_ref_ini,
parse_smil,
parse_nsc,
@@ -740,7 +709,13 @@ static struct playlist *do_parse(struct stream* stream, bool forced)
};
bool success = false;
- if (play_tree_parser_get_line(&p) != NULL) {
+ struct demuxer *pl_demux = demux_open(stream, "playlist", NULL, stream->opts);
+ if (pl_demux && pl_demux->playlist) {
+ playlist_transfer_entries(p.pl, pl_demux->playlist);
+ success = true;
+ }
+ free_demuxer(pl_demux);
+ if (!success && play_tree_parser_get_line(&p) != NULL) {
for (int n = 0; n < sizeof(pl_parsers) / sizeof(pl_parsers[0]); n++) {
play_tree_parser_reset(&p);
if (pl_parsers[n] == parse_textplain && !forced)
@@ -765,13 +740,3 @@ static struct playlist *do_parse(struct stream* stream, bool forced)
return p.pl;
}
-
-struct playlist *playlist_parse(struct stream* stream)
-{
- return do_parse(stream, true);
-}
-
-struct playlist *playlist_probe_and_parse(struct stream* stream)
-{
- return do_parse(stream, false);
-}
diff --git a/mpvcore/playlist_parser.h b/mpvcore/playlist_parser.h
index 3ceb95c460..a541aa2cb4 100644
--- a/mpvcore/playlist_parser.h
+++ b/mpvcore/playlist_parser.h
@@ -21,14 +21,9 @@
#include <stdbool.h>
-struct stream;
+struct MPOpts;
struct playlist;
-// Parse the given stream as playlist. Append entries to pl. Return whether
-// there was an error when parsing.
-// deep = Parser depth. Some formats allow including other files,
-struct playlist *playlist_parse(struct stream* stream);
-struct playlist *playlist_probe_and_parse(struct stream* stream);
-struct playlist *playlist_parse_file(const char *file);
+struct playlist *playlist_parse_file(const char *file, struct MPOpts *opts);
#endif
diff --git a/mpvcore/timeline/tl_cue.c b/mpvcore/timeline/tl_cue.c
index f7e13ec681..634a6de5f4 100644
--- a/mpvcore/timeline/tl_cue.c
+++ b/mpvcore/timeline/tl_cue.c
@@ -356,8 +356,6 @@ void build_cue_timeline(struct MPContext *mpctx)
}
}
- add_source(mpctx, mpctx->demuxer);
-
for (size_t i = 0; i < file_count; i++) {
if (!open_source(mpctx, files[i]))
goto out;
diff --git a/stream/stream.c b/stream/stream.c
index b17b7fc19a..4650535890 100644
--- a/stream/stream.c
+++ b/stream/stream.c
@@ -742,6 +742,7 @@ static int stream_enable_cache(stream_t **stream, int64_t size, int64_t min,
cache->url = talloc_strdup(cache, orig->url);
cache->mime_type = talloc_strdup(cache, orig->mime_type);
cache->lavf_type = talloc_strdup(cache, orig->lavf_type);
+ cache->safe_origin = orig->safe_origin;
cache->opts = orig->opts;
cache->start_pos = orig->start_pos;
cache->end_pos = orig->end_pos;
@@ -862,6 +863,8 @@ unsigned char *stream_read_line(stream_t *s, unsigned char *mem, int max,
int len;
const unsigned char *end;
unsigned char *ptr = mem;
+ if (utf16 == -1)
+ utf16 = 0;
if (max < 1)
return NULL;
max--; // reserve one for 0-termination
@@ -891,6 +894,21 @@ unsigned char *stream_read_line(stream_t *s, unsigned char *mem, int max,
return mem;
}
+static const char *bom[3] = {"\xEF\xBB\xBF", "\xFF\xFE", "\xFE\xFF"};
+
+// Return utf16 argument for stream_read_line
+int stream_skip_bom(struct stream *s)
+{
+ bstr data = stream_peek(s, 4);
+ for (int n = 0; n < 3; n++) {
+ if (bstr_startswith0(data, bom[n])) {
+ stream_skip(s, strlen(bom[n]));
+ return n;
+ }
+ }
+ return -1; // default to 8 bit codepages
+}
+
// Read the rest of the stream into memory (current pos to EOF), and return it.
// talloc_ctx: used as talloc parent for the returned allocation
// max_size: must be set to >0. If the file is larger than that, it is treated
diff --git a/stream/stream.h b/stream/stream.h
index f19ab4203f..27043206c9 100644
--- a/stream/stream.h
+++ b/stream/stream.h
@@ -150,6 +150,7 @@ typedef struct stream {
char *mime_type; // when HTTP streaming is used
char *demuxer; // request demuxer to be used
char *lavf_type; // name of expected demuxer type for lavf
+ bool safe_origin; // used for playlists that can be opened safely
struct MPOpts *opts;
FILE *capture_file;
@@ -208,6 +209,7 @@ inline static uint64_t stream_read_qword(stream_t *s)
unsigned char *stream_read_line(stream_t *s, unsigned char *mem, int max,
int utf16);
+int stream_skip_bom(struct stream *s);
inline static int stream_eof(stream_t *s)
{