Coverage Report

Created: 2026-03-27 06:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mpv/demux/demux_mf.c
Line
Count
Source
1
/*
2
 * This file is part of mpv.
3
 *
4
 * mpv is free software; you can redistribute it and/or
5
 * modify it under the terms of the GNU Lesser General Public
6
 * License as published by the Free Software Foundation; either
7
 * version 2.1 of the License, or (at your option) any later version.
8
 *
9
 * mpv is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU Lesser General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Lesser General Public
15
 * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
#include <math.h>
19
#include <stdbool.h>
20
#include <stdio.h>
21
#include <stdlib.h>
22
#include <sys/types.h>
23
#include <sys/stat.h>
24
25
#include "osdep/io.h"
26
27
#include "mpv_talloc.h"
28
#include "common/msg.h"
29
#include "options/options.h"
30
#include "options/m_config.h"
31
#include "options/path.h"
32
#include "misc/ctype.h"
33
34
#include "stream/stream.h"
35
#include "demux.h"
36
#include "stheader.h"
37
#include "codec_tags.h"
38
39
5.77k
#define MF_MAX_FILE_SIZE (1024 * 1024 * 256)
40
41
typedef struct mf {
42
    struct mp_log *log;
43
    struct sh_stream *sh;
44
    int curr_frame;
45
    int nr_of_files;
46
    char **names;
47
    // optional
48
    struct stream **streams;
49
} mf_t;
50
51
52
static void mf_add(mf_t *mf, const char *fname)
53
69.0k
{
54
69.0k
    char *entry = talloc_strdup(mf, fname);
55
69.0k
    MP_TARRAY_APPEND(mf, mf->names, mf->nr_of_files, entry);
56
69.0k
}
57
58
static mf_t *open_mf_pattern(void *talloc_ctx, struct demuxer *d, char *filename)
59
468
{
60
468
    struct mp_log *log = d->log;
61
468
    int error_count = 0;
62
468
    int count = 0;
63
64
468
    mf_t *mf = talloc_zero(talloc_ctx, mf_t);
65
468
    mf->log = log;
66
67
468
    if (filename[0] == '@') {
68
300
        struct stream *s = stream_create(filename + 1,
69
300
                            d->stream_origin | STREAM_READ, d->cancel, d->global);
70
300
        if (s && !s->is_directory) {
71
27.1k
            while (1) {
72
27.1k
                char buf[512];
73
27.1k
                int len = stream_read_peek(s, buf, sizeof(buf));
74
27.1k
                if (!len)
75
176
                    break;
76
26.9k
                bstr data = (bstr){buf, len};
77
26.9k
                int pos = bstrchr(data, '\n');
78
26.9k
                data = bstr_splice(data, 0, pos < 0 ? data.len : pos + 1);
79
26.9k
                bstr fname = bstr_strip(data);
80
26.9k
                if (fname.len) {
81
18.7k
                    if (bstrchr(fname, '\0') >= 0) {
82
0
                        mp_err(log, "invalid filename\n");
83
0
                        break;
84
0
                    }
85
18.7k
                    char *entry = bstrto0(mf, fname);
86
18.7k
                    if (!mp_path_exists(entry) && !mp_is_url(fname)) {
87
10.5k
                        mp_verbose(log, "file not found: '%s'\n", entry);
88
10.5k
                    } else {
89
8.22k
                        MP_TARRAY_APPEND(mf, mf->names, mf->nr_of_files, entry);
90
8.22k
                    }
91
18.7k
                }
92
26.9k
                stream_seek_skip(s, stream_tell(s) + data.len);
93
26.9k
            }
94
176
            free_stream(s);
95
96
176
            goto exit_mf;
97
176
        }
98
124
        free_stream(s);
99
124
        mp_info(log, "%s is not indirect filelist\n", filename + 1);
100
124
    }
101
102
292
    if (strchr(filename, ',')) {
103
132
        bstr bfilename = bstr0(filename);
104
132
        mp_info(log, "filelist: %.*s\n", BSTR_P(bfilename));
105
106
184k
        while (bfilename.len) {
107
183k
            bstr bfname;
108
183k
            bstr_split_tok(bfilename, ",", &bfname, &bfilename);
109
183k
            char *fname2 = bstrdup0(mf, bfname);
110
111
183k
            if (!mp_path_exists(fname2) && !mp_is_url(bfname))
112
174k
                mp_verbose(log, "file not found: '%s'\n", fname2);
113
9.70k
            else {
114
9.70k
                mf_add(mf, fname2);
115
9.70k
            }
116
183k
            talloc_free(fname2);
117
183k
        }
118
119
132
        goto exit_mf;
120
132
    }
121
122
160
    bstr bfilename = bstr0(filename);
123
160
    if (mp_is_url(bfilename))
124
4
        goto exit_mf;
125
126
156
    size_t fname_avail = bfilename.len + 32;
127
156
    char *fname = talloc_size(mf, fname_avail);
128
129
#if HAVE_GLOB && !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
130
    if (!strchr(filename, '%')) {
131
        // append * if none present
132
        snprintf(fname, fname_avail, "%s%c", filename,
133
            strchr(filename, '*') ? 0 : '*');
134
        mp_info(log, "search expr: %s\n", fname);
135
136
        glob_t gg;
137
        if (glob(fname, 0, NULL, &gg)) {
138
            talloc_free(mf);
139
            return NULL;
140
        }
141
142
        for (int i = 0; i < gg.gl_pathc; i++) {
143
            if (mp_path_isdir(gg.gl_pathv[i]))
144
                continue;
145
            mf_add(mf, gg.gl_pathv[i]);
146
        }
147
        globfree(&gg);
148
        goto exit_mf;
149
    }
150
#endif
151
152
    // We're using arbitrary user input as printf format with 1 int argument.
153
    // Any format which uses exactly 1 int argument would be valid, but for
154
    // simplicity we reject all conversion specifiers except %% and simple
155
    // integer specifier: %[.][NUM]d where NUM is 1-3 digits (%.d is valid)
156
156
    const char *f = filename;
157
156
    int MAXDIGS = 3, nspec = 0, c;
158
156
    bool bad_spec = false;
159
160
20.9k
    while (nspec < 2 && (c = *f++)) {
161
20.8k
        if (c != '%')
162
20.4k
            continue;
163
164
386
        if (*f == '%') {
165
            // '%%', which ends up as an explicit % in the output.
166
            // Skipping forwards as it doesn't require further attention.
167
351
            f++;
168
351
            continue;
169
351
        }
170
171
        // Now c == '%' and *f != '%', thus we have entered territory of format
172
        // specifiers which we are interested in.
173
35
        nspec++;
174
175
35
        if (*f == '.')
176
0
            f++;
177
178
71
        for (int ndig = 0; mp_isdigit(*f) && ndig < MAXDIGS; ndig++, f++)
179
36
            /* no-op */;
180
181
35
        if (*f != 'd') {
182
21
            bad_spec = true; // not int, or beyond our validation capacity
183
21
            break;
184
21
        }
185
186
        // *f is 'd'
187
14
        f++;
188
14
    }
189
190
    // nspec==0 (zero specifiers) is rejected because fname wouldn't advance.
191
156
    if (bad_spec || nspec != 1) {
192
147
        mp_err(log,
193
147
               "unsupported expr format: '%s' - exactly one format specifier of the form %%[.][NUM]d is expected\n",
194
147
               filename);
195
147
        goto exit_mf;
196
147
    }
197
198
9
    mp_info(log, "search expr: %s\n", filename);
199
200
34
    while (error_count < 5) {
201
29
        if (snprintf(fname, fname_avail, filename, count++) >= fname_avail) {
202
4
            mp_err(log, "format result too long: '%s'\n", filename);
203
4
            goto exit_mf;
204
4
        }
205
25
        if (!mp_path_exists(fname)) {
206
25
            error_count++;
207
25
            mp_verbose(log, "file not found: '%s'\n", fname);
208
25
        } else {
209
0
            mf_add(mf, fname);
210
0
        }
211
25
    }
212
213
468
exit_mf:
214
468
    mp_info(log, "number of files: %d\n", mf->nr_of_files);
215
468
    return mf;
216
9
}
217
218
static mf_t *open_mf_single(void *talloc_ctx, struct mp_log *log, char *filename)
219
59.3k
{
220
59.3k
    mf_t *mf = talloc_zero(talloc_ctx, mf_t);
221
59.3k
    mf->log = log;
222
59.3k
    mf_add(mf, filename);
223
59.3k
    return mf;
224
59.3k
}
225
226
static void demux_seek_mf(demuxer_t *demuxer, double seek_pts, int flags)
227
303
{
228
303
    mf_t *mf = demuxer->priv;
229
303
    double newpos = seek_pts * mf->sh->codec->fps;
230
303
    if (flags & SEEK_FACTOR)
231
0
        newpos = seek_pts * (mf->nr_of_files - 1);
232
303
    if (flags & SEEK_FORWARD) {
233
0
        newpos = ceil(newpos);
234
303
    } else {
235
303
        newpos = MPMIN(floor(newpos), mf->nr_of_files - 1);
236
303
    }
237
303
    mf->curr_frame = MPCLAMP((int)newpos, 0, mf->nr_of_files);
238
303
}
239
240
static bool demux_mf_read_packet(struct demuxer *demuxer,
241
                                 struct demux_packet **pkt)
242
20.3k
{
243
20.3k
    mf_t *mf = demuxer->priv;
244
20.3k
    if (mf->curr_frame >= mf->nr_of_files)
245
2.14k
        return false;
246
20.3k
    bool ok = false;
247
248
18.1k
    struct stream *entry_stream = NULL;
249
18.1k
    if (mf->streams)
250
1.97k
        entry_stream = mf->streams[mf->curr_frame];
251
18.1k
    struct stream *stream = entry_stream;
252
18.1k
    if (!stream) {
253
16.2k
        char *filename = mf->names[mf->curr_frame];
254
16.2k
        if (filename) {
255
16.2k
            stream = stream_create(filename, demuxer->stream_origin | STREAM_READ,
256
16.2k
                                   demuxer->cancel, demuxer->global);
257
16.2k
        }
258
16.2k
    }
259
260
18.1k
    if (stream) {
261
5.77k
        stream_seek(stream, 0);
262
5.77k
        bstr data = stream_read_complete(stream, NULL, MF_MAX_FILE_SIZE);
263
5.77k
        if (data.len) {
264
3.54k
            demux_packet_t *dp = new_demux_packet(demuxer->packet_pool, data.len);
265
3.54k
            if (dp) {
266
3.54k
                memcpy(dp->buffer, data.start, data.len);
267
3.54k
                dp->pts = mf->curr_frame / mf->sh->codec->fps;
268
3.54k
                dp->keyframe = true;
269
3.54k
                dp->stream = mf->sh->index;
270
3.54k
                *pkt = dp;
271
3.54k
                ok = true;
272
3.54k
            }
273
3.54k
        }
274
5.77k
        talloc_free(data.start);
275
5.77k
    }
276
277
18.1k
    if (stream && stream != entry_stream)
278
3.80k
        free_stream(stream);
279
280
18.1k
    mf->curr_frame++;
281
282
18.1k
    if (!ok)
283
18.1k
        MP_ERR(demuxer, "error reading image file\n");
284
285
18.1k
    return true;
286
20.3k
}
287
288
// map file extension/type to a codec name
289
290
static const struct {
291
    const char *type;
292
    const char *codec;
293
} type2format[] = {
294
    { "bmp",            "bmp" },
295
    { "dpx",            "dpx" },
296
    { "j2c",            "jpeg2000" },
297
    { "j2k",            "jpeg2000" },
298
    { "jp2",            "jpeg2000" },
299
    { "jpc",            "jpeg2000" },
300
    { "jpeg",           "mjpeg" },
301
    { "jpg",            "mjpeg" },
302
    { "jps",            "mjpeg" },
303
    { "jls",            "ljpeg" },
304
    { "thm",            "mjpeg" },
305
    { "db",             "mjpeg" },
306
    { "pcd",            "photocd" },
307
    { "pfm",            "pfm" },
308
    { "phm",            "phm" },
309
    { "hdr",            "hdr" },
310
    { "pcx",            "pcx" },
311
    { "png",            "png" },
312
    { "pns",            "png" },
313
    { "ptx",            "ptx" },
314
    { "tga",            "targa" },
315
    { "tif",            "tiff" },
316
    { "tiff",           "tiff" },
317
    { "sgi",            "sgi" },
318
    { "sun",            "sunrast" },
319
    { "ras",            "sunrast" },
320
    { "rs",             "sunrast" },
321
    { "ra",             "sunrast" },
322
    { "im1",            "sunrast" },
323
    { "im8",            "sunrast" },
324
    { "im24",           "sunrast" },
325
    { "im32",           "sunrast" },
326
    { "sunras",         "sunrast" },
327
    { "xbm",            "xbm" },
328
    { "pam",            "pam" },
329
    { "pbm",            "pbm" },
330
    { "pgm",            "pgm" },
331
    { "pgmyuv",         "pgmyuv" },
332
    { "ppm",            "ppm" },
333
    { "pnm",            "ppm" },
334
    { "gif",            "gif" }, // usually handled by demux_lavf
335
    { "pix",            "brender_pix" },
336
    { "exr",            "exr" },
337
    { "pic",            "pictor" },
338
    { "qoi",            "qoi" },
339
    { "xface",          "xface" },
340
    { "xwd",            "xwd" },
341
    { "svg",            "svg" },
342
    { "webp",           "webp" },
343
    { "jxl",            "jpegxl" },
344
    {0}
345
};
346
347
static const char *probe_format(mf_t *mf, char *type, enum demux_check check)
348
59.5k
{
349
59.5k
    if (check > DEMUX_CHECK_REQUEST)
350
31.2k
        return NULL;
351
28.3k
    char *org_type = type;
352
28.3k
    if (!type || !type[0]) {
353
28.3k
        char *p = strrchr(mf->names[0], '.');
354
28.3k
        if (p)
355
16.6k
            type = p + 1;
356
28.3k
    }
357
1.38M
    for (int i = 0; type2format[i].type; i++) {
358
1.35M
        if (type && strcasecmp(type, type2format[i].type) == 0)
359
2.19k
            return type2format[i].codec;
360
1.35M
    }
361
26.1k
    if (check == DEMUX_CHECK_REQUEST) {
362
93
        if (!org_type) {
363
93
            MP_ERR(mf, "file type was not set! (try --mf-type=ext)\n");
364
93
        } else {
365
0
            MP_ERR(mf, "--mf-type set to an unknown codec!\n");
366
0
        }
367
93
    }
368
26.1k
    return NULL;
369
28.3k
}
370
371
static int demux_open_mf(demuxer_t *demuxer, enum demux_check check)
372
59.7k
{
373
59.7k
    mf_t *mf;
374
375
59.7k
    if (strncmp(demuxer->stream->url, "mf://", 5) == 0 &&
376
468
        demuxer->stream->info && strcmp(demuxer->stream->info->name, "mf") == 0)
377
468
    {
378
468
        mf = open_mf_pattern(demuxer, demuxer, demuxer->stream->url + 5);
379
59.3k
    } else {
380
59.3k
        mf = open_mf_single(demuxer, demuxer->log, demuxer->stream->url);
381
59.3k
        int bog = 0;
382
59.3k
        MP_TARRAY_APPEND(mf, mf->streams, bog, demuxer->stream);
383
59.3k
    }
384
385
59.7k
    if (!mf || mf->nr_of_files < 1)
386
199
        goto error;
387
388
59.5k
    const char *codec = mp_map_mimetype_to_video_codec(demuxer->stream->mime_type);
389
59.5k
    if (!codec || (demuxer->opts->mf_type && demuxer->opts->mf_type[0]))
390
59.5k
        codec = probe_format(mf, demuxer->opts->mf_type, check);
391
59.5k
    if (!codec)
392
57.3k
        goto error;
393
394
2.19k
    mf->curr_frame = 0;
395
396
    // create a new video stream header
397
2.19k
    struct sh_stream *sh = demux_alloc_sh_stream(STREAM_VIDEO);
398
2.19k
    if (mf->nr_of_files == 1) {
399
2.02k
        MP_VERBOSE(demuxer, "Assuming this is an image format.\n");
400
2.02k
        sh->image = true;
401
2.02k
    }
402
403
2.19k
    struct mp_codec_params *c = sh->codec;
404
2.19k
    c->codec = codec;
405
2.19k
    c->disp_w = 0;
406
2.19k
    c->disp_h = 0;
407
2.19k
    c->fps = demuxer->opts->mf_fps;
408
2.19k
    c->reliable_fps = true;
409
410
2.19k
    demux_add_sh_stream(demuxer, sh);
411
412
2.19k
    mf->sh = sh;
413
2.19k
    demuxer->priv = (void *)mf;
414
2.19k
    demuxer->seekable = true;
415
2.19k
    demuxer->duration = mf->nr_of_files / mf->sh->codec->fps;
416
417
2.19k
    return 0;
418
419
57.5k
error:
420
57.5k
    return -1;
421
59.5k
}
422
423
static void demux_close_mf(demuxer_t *demuxer)
424
59.7k
{
425
59.7k
}
426
427
const demuxer_desc_t demuxer_desc_mf = {
428
    .name = "mf",
429
    .desc = "image files (mf)",
430
    .read_packet = demux_mf_read_packet,
431
    .open = demux_open_mf,
432
    .close = demux_close_mf,
433
    .seek = demux_seek_mf,
434
};