Coverage Report

Created: 2026-06-13 07:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mpv/demux/cue.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 <stdlib.h>
19
#include <stdbool.h>
20
#include <string.h>
21
#include <inttypes.h>
22
23
#include "mpv_talloc.h"
24
25
#include "misc/bstr.h"
26
#include "common/common.h"
27
#include "common/tags.h"
28
29
#include "cue.h"
30
31
0
#define SECS_PER_CUE_FRAME (1.0/75.0)
32
33
enum cue_command {
34
    CUE_ERROR = -1,     // not a valid CUE command, or an unknown extension
35
    CUE_EMPTY,          // line with whitespace only
36
    CUE_UNUSED,         // valid CUE command, but ignored by this code
37
    CUE_FILE,
38
    CUE_TRACK,
39
    CUE_INDEX,
40
    CUE_TITLE,
41
    CUE_PERFORMER,
42
};
43
44
static const struct {
45
    enum cue_command command;
46
    const char *text;
47
} cue_command_strings[] = {
48
    { CUE_FILE, "FILE" },
49
    { CUE_TRACK, "TRACK" },
50
    { CUE_INDEX, "INDEX" },
51
    { CUE_TITLE, "TITLE" },
52
    { CUE_UNUSED, "CATALOG" },
53
    { CUE_UNUSED, "CDTEXTFILE" },
54
    { CUE_UNUSED, "FLAGS" },
55
    { CUE_UNUSED, "ISRC" },
56
    { CUE_PERFORMER, "PERFORMER" },
57
    { CUE_UNUSED, "POSTGAP" },
58
    { CUE_UNUSED, "PREGAP" },
59
    { CUE_UNUSED, "REM" },
60
    { CUE_UNUSED, "SONGWRITER" },
61
    { CUE_UNUSED, "MESSAGE" },
62
    { CUE_UNUSED, "MCN" },
63
    { -1 },
64
};
65
66
static const uint8_t spaces[] = {' ', '\f', '\n', '\r', '\t', '\v', 0xA0};
67
68
static struct bstr lstrip_whitespace(struct bstr data)
69
233k
{
70
609k
    while (data.len) {
71
556k
        bstr rest = data;
72
556k
        int code = bstr_decode_utf8(data, &rest);
73
556k
        if (code < 0) {
74
            // Tolerate Latin1 => probing works (which doesn't convert charsets).
75
24.3k
            code = data.start[0];
76
24.3k
            rest.start += 1;
77
24.3k
            rest.len -= 1;
78
24.3k
        }
79
3.22M
        for (size_t n = 0; n < MP_ARRAY_SIZE(spaces); n++) {
80
3.04M
            if (spaces[n] == code) {
81
376k
                data = rest;
82
376k
                goto next;
83
376k
            }
84
3.04M
        }
85
180k
        break;
86
376k
    next: ;
87
376k
    }
88
233k
    return data;
89
233k
}
90
91
static enum cue_command read_cmd(struct bstr *data, struct bstr *out_params)
92
181k
{
93
181k
    struct bstr line = bstr_strip_linebreaks(bstr_getline(*data, data));
94
181k
    line = lstrip_whitespace(line);
95
181k
    if (line.len == 0)
96
49.8k
        return CUE_EMPTY;
97
1.83M
    for (int n = 0; cue_command_strings[n].command != -1; n++) {
98
1.72M
        struct bstr name = bstr0(cue_command_strings[n].text);
99
1.72M
        if (bstr_case_startswith(line, name)) {
100
20.2k
            struct bstr rest = bstr_cut(line, name.len);
101
20.2k
            struct bstr par = lstrip_whitespace(rest);
102
20.2k
            if (rest.len && par.len == rest.len)
103
134
                continue;
104
20.0k
            if (out_params)
105
16.0k
                *out_params = par;
106
20.0k
            return cue_command_strings[n].command;
107
20.2k
        }
108
1.72M
    }
109
111k
    return CUE_ERROR;
110
131k
}
111
112
static bool eat_char(struct bstr *data, char ch)
113
18.3k
{
114
18.3k
    if (data->len && data->start[0] == ch) {
115
7.29k
        *data = bstr_cut(*data, 1);
116
7.29k
        return true;
117
11.0k
    } else {
118
11.0k
        return false;
119
11.0k
    }
120
18.3k
}
121
122
static char *read_quoted(void *talloc_ctx, struct bstr *data)
123
5.11k
{
124
5.11k
    *data = lstrip_whitespace(*data);
125
5.11k
    if (!eat_char(data, '"'))
126
3.06k
        return NULL;
127
2.05k
    int end = bstrchr(*data, '"');
128
2.05k
    if (end < 0)
129
872
        return NULL;
130
1.18k
    struct bstr res = bstr_splice(*data, 0, end);
131
1.18k
    *data = bstr_cut(*data, end + 1);
132
1.18k
    return bstrto0(talloc_ctx, res);
133
2.05k
}
134
135
static struct bstr strip_quotes(struct bstr data)
136
1.66k
{
137
1.66k
    bstr s = data;
138
1.66k
    if (bstr_eatstart0(&s, "\"") && bstr_eatend0(&s, "\""))
139
199
        return s;
140
1.46k
    return data;
141
1.66k
}
142
143
// Read an unsigned decimal integer.
144
// Optionally check if it is 2 digit.
145
// Return -1 on failure.
146
static int read_int(struct bstr *data, bool two_digit)
147
26.5k
{
148
26.5k
    *data = lstrip_whitespace(*data);
149
26.5k
    if (data->len && data->start[0] == '-')
150
945
        return -1;
151
25.5k
    struct bstr s = *data;
152
25.5k
    int res = (int)bstrtoll(s, &s, 10);
153
25.5k
    if (data->len == s.len || (two_digit && data->len - s.len > 2))
154
18.8k
        return -1;
155
6.70k
    *data = s;
156
6.70k
    return res;
157
25.5k
}
158
159
static double read_time(struct bstr *data)
160
6.63k
{
161
6.63k
    struct bstr s = *data;
162
6.63k
    bool ok = true;
163
6.63k
    double t1 = read_int(&s, false);
164
6.63k
    ok = eat_char(&s, ':') && ok;
165
6.63k
    double t2 = read_int(&s, true);
166
6.63k
    ok = eat_char(&s, ':') && ok;
167
6.63k
    double t3 = read_int(&s, true);
168
6.63k
    ok = ok && t1 >= 0 && t2 >= 0 && t3 >= 0;
169
6.63k
    return ok ? t1 * 60.0 + t2 + t3 * SECS_PER_CUE_FRAME : 0;
170
6.63k
}
171
172
static struct bstr skip_utf8_bom(struct bstr data)
173
112k
{
174
112k
    return bstr_startswith0(data, "\xEF\xBB\xBF") ? bstr_cut(data, 3) : data;
175
112k
}
176
177
// Check if the text in data is most likely CUE data. This is used by the
178
// demuxer code to check the file type.
179
// data is the start of the probed file, possibly cut off at a random point.
180
bool mp_probe_cue(struct bstr data)
181
111k
{
182
111k
    bool valid = false;
183
111k
    data = skip_utf8_bom(data);
184
137k
    for (;;) {
185
137k
        enum cue_command cmd = read_cmd(&data, NULL);
186
        // End reached. Since the line was most likely cut off, don't use the
187
        // result of the last parsing call.
188
137k
        if (data.len == 0)
189
63.7k
            break;
190
74.1k
        if (cmd == CUE_ERROR)
191
47.8k
            return false;
192
26.2k
        if (cmd != CUE_EMPTY)
193
3.66k
            valid = true;
194
26.2k
    }
195
63.7k
    return valid;
196
111k
}
197
198
struct cue_file *mp_parse_cue(struct bstr data)
199
579
{
200
579
    struct cue_file *f = talloc_zero(NULL, struct cue_file);
201
579
    f->tags = talloc_zero(f, struct mp_tags);
202
203
579
    data = skip_utf8_bom(data);
204
205
579
    char *filename = NULL;
206
    // Global metadata, and copied into new tracks.
207
579
    struct cue_track proto_track = {0};
208
579
    struct cue_track *cur_track = NULL;
209
210
43.6k
    while (data.len) {
211
43.5k
        struct bstr param;
212
43.5k
        int cmd = read_cmd(&data, &param);
213
43.5k
        switch (cmd) {
214
482
        case CUE_ERROR:
215
482
            talloc_free(f);
216
482
            return NULL;
217
1.63k
        case CUE_TRACK: {
218
1.63k
            if (bstr_find0(param, "AUDIO") == -1)
219
988
                break;
220
651
            MP_TARRAY_GROW(f, f->tracks, f->num_tracks);
221
651
            f->num_tracks += 1;
222
651
            cur_track = &f->tracks[f->num_tracks - 1];
223
651
            *cur_track = proto_track;
224
651
            cur_track->tags = talloc_zero(f, struct mp_tags);
225
651
            break;
226
1.63k
        }
227
1.66k
        case CUE_TITLE:
228
1.66k
        case CUE_PERFORMER: {
229
1.66k
            static const char *metanames[] = {
230
1.66k
                [CUE_TITLE] = "title",
231
1.66k
                [CUE_PERFORMER] = "performer",
232
1.66k
            };
233
1.66k
            struct mp_tags *tags = cur_track ? cur_track->tags : f->tags;
234
1.66k
            mp_tags_set_bstr(tags, bstr0(metanames[cmd]), strip_quotes(param));
235
1.66k
            break;
236
1.66k
        }
237
6.63k
        case CUE_INDEX: {
238
6.63k
            int type = read_int(&param, true);
239
6.63k
            double time = read_time(&param);
240
6.63k
            if (cur_track) {
241
288
                if (type == 1) {
242
7
                    cur_track->start = time;
243
7
                    cur_track->filename = filename;
244
281
                } else if (type == 0) {
245
97
                    cur_track->pregap_start = time;
246
97
                }
247
288
            }
248
6.63k
            break;
249
1.66k
        }
250
5.11k
        case CUE_FILE:
251
            // NOTE: FILE comes before TRACK, so don't use cur_track->filename
252
5.11k
            filename = read_quoted(f, &param);
253
5.11k
            break;
254
43.5k
        }
255
43.5k
    }
256
257
97
    return f;
258
579
}
259
260
int mp_check_embedded_cue(struct cue_file *f)
261
0
{
262
0
    if (f->num_tracks == 0)
263
0
        return -1;
264
0
    char *fn0 = f->tracks[0].filename;
265
0
    for (int n = 1; n < f->num_tracks; n++) {
266
0
        char *fn = f->tracks[n].filename;
267
        // both filenames have the same address (including NULL)
268
0
        if (fn0 == fn)
269
0
            continue;
270
        // only one filename is NULL, or the strings don't match
271
0
        if (!fn0 || !fn || strcmp(fn0, fn) != 0)
272
0
            return -1;
273
0
    }
274
0
    return 0;
275
0
}