Coverage Report

Created: 2026-09-28 06:42

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
249k
{
70
613k
    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
26.0k
            code = data.start[0];
76
26.0k
            rest.start += 1;
77
26.0k
            rest.len -= 1;
78
26.0k
        }
79
3.24M
        for (size_t n = 0; n < MP_ARRAY_SIZE(spaces); n++) {
80
3.05M
            if (spaces[n] == code) {
81
363k
                data = rest;
82
363k
                goto next;
83
363k
            }
84
3.05M
        }
85
192k
        break;
86
363k
    next: ;
87
363k
    }
88
249k
    return data;
89
249k
}
90
91
static enum cue_command read_cmd(struct bstr *data, struct bstr *out_params)
92
191k
{
93
191k
    struct bstr line = bstr_strip_linebreaks(bstr_getline(*data, data));
94
191k
    line = lstrip_whitespace(line);
95
191k
    if (line.len == 0)
96
53.2k
        return CUE_EMPTY;
97
1.89M
    for (int n = 0; cue_command_strings[n].command != -1; n++) {
98
1.77M
        struct bstr name = bstr0(cue_command_strings[n].text);
99
1.77M
        if (bstr_case_startswith(line, name)) {
100
24.2k
            struct bstr rest = bstr_cut(line, name.len);
101
24.2k
            struct bstr par = lstrip_whitespace(rest);
102
24.2k
            if (rest.len && par.len == rest.len)
103
154
                continue;
104
24.0k
            if (out_params)
105
19.5k
                *out_params = par;
106
24.0k
            return cue_command_strings[n].command;
107
24.2k
        }
108
1.77M
    }
109
114k
    return CUE_ERROR;
110
138k
}
111
112
static bool eat_char(struct bstr *data, char ch)
113
20.8k
{
114
20.8k
    if (data->len && data->start[0] == ch) {
115
8.89k
        *data = bstr_cut(*data, 1);
116
8.89k
        return true;
117
11.9k
    } else {
118
11.9k
        return false;
119
11.9k
    }
120
20.8k
}
121
122
static char *read_quoted(void *talloc_ctx, struct bstr *data)
123
7.71k
{
124
7.71k
    *data = lstrip_whitespace(*data);
125
7.71k
    if (!eat_char(data, '"'))
126
4.05k
        return NULL;
127
3.65k
    int end = bstrchr(*data, '"');
128
3.65k
    if (end < 0)
129
1.02k
        return NULL;
130
2.63k
    struct bstr res = bstr_splice(*data, 0, end);
131
2.63k
    *data = bstr_cut(*data, end + 1);
132
2.63k
    return bstrto0(talloc_ctx, res);
133
3.65k
}
134
135
static struct bstr strip_quotes(struct bstr data)
136
2.51k
{
137
2.51k
    bstr s = data;
138
2.51k
    if (bstr_eatstart0(&s, "\"") && bstr_eatend0(&s, "\""))
139
264
        return s;
140
2.25k
    return data;
141
2.51k
}
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.2k
{
148
26.2k
    *data = lstrip_whitespace(*data);
149
26.2k
    if (data->len && data->start[0] == '-')
150
2.04k
        return -1;
151
24.1k
    struct bstr s = *data;
152
24.1k
    int res = (int)bstrtoll(s, &s, 10);
153
24.1k
    if (data->len == s.len || (two_digit && data->len - s.len > 2))
154
17.7k
        return -1;
155
6.37k
    *data = s;
156
6.37k
    return res;
157
24.1k
}
158
159
static double read_time(struct bstr *data)
160
6.55k
{
161
6.55k
    struct bstr s = *data;
162
6.55k
    bool ok = true;
163
6.55k
    double t1 = read_int(&s, false);
164
6.55k
    ok = eat_char(&s, ':') && ok;
165
6.55k
    double t2 = read_int(&s, true);
166
6.55k
    ok = eat_char(&s, ':') && ok;
167
6.55k
    double t3 = read_int(&s, true);
168
6.55k
    ok = ok && t1 >= 0 && t2 >= 0 && t3 >= 0;
169
6.55k
    return ok ? t1 * 60.0 + t2 + t3 * SECS_PER_CUE_FRAME : 0;
170
6.55k
}
171
172
static struct bstr skip_utf8_bom(struct bstr data)
173
115k
{
174
115k
    return bstr_startswith0(data, "\xEF\xBB\xBF") ? bstr_cut(data, 3) : data;
175
115k
}
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
114k
{
182
114k
    bool valid = false;
183
114k
    data = skip_utf8_bom(data);
184
142k
    for (;;) {
185
142k
        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
142k
        if (data.len == 0)
189
62.5k
            break;
190
79.6k
        if (cmd == CUE_ERROR)
191
51.8k
            return false;
192
27.8k
        if (cmd != CUE_EMPTY)
193
4.06k
            valid = true;
194
27.8k
    }
195
62.5k
    return valid;
196
114k
}
197
198
struct cue_file *mp_parse_cue(struct bstr data)
199
567
{
200
567
    struct cue_file *f = talloc_zero(NULL, struct cue_file);
201
567
    f->tags = talloc_zero(f, struct mp_tags);
202
203
567
    data = skip_utf8_bom(data);
204
205
567
    char *filename = NULL;
206
    // Global metadata, and copied into new tracks.
207
567
    struct cue_track proto_track = {0};
208
567
    struct cue_track *cur_track = NULL;
209
210
49.4k
    while (data.len) {
211
49.3k
        struct bstr param;
212
49.3k
        int cmd = read_cmd(&data, &param);
213
49.3k
        switch (cmd) {
214
463
        case CUE_ERROR:
215
463
            talloc_free(f);
216
463
            return NULL;
217
1.97k
        case CUE_TRACK: {
218
1.97k
            if (bstr_find0(param, "AUDIO") == -1)
219
861
                break;
220
1.11k
            MP_TARRAY_GROW(f, f->tracks, f->num_tracks);
221
1.11k
            f->num_tracks += 1;
222
1.11k
            cur_track = &f->tracks[f->num_tracks - 1];
223
1.11k
            *cur_track = proto_track;
224
1.11k
            cur_track->tags = talloc_zero(f, struct mp_tags);
225
1.11k
            break;
226
1.97k
        }
227
1.88k
        case CUE_TITLE:
228
2.51k
        case CUE_PERFORMER: {
229
2.51k
            static const char *metanames[] = {
230
2.51k
                [CUE_TITLE] = "title",
231
2.51k
                [CUE_PERFORMER] = "performer",
232
2.51k
            };
233
2.51k
            struct mp_tags *tags = cur_track ? cur_track->tags : f->tags;
234
2.51k
            mp_tags_set_bstr(tags, bstr0(metanames[cmd]), strip_quotes(param));
235
2.51k
            break;
236
1.88k
        }
237
6.55k
        case CUE_INDEX: {
238
6.55k
            int type = read_int(&param, true);
239
6.55k
            double time = read_time(&param);
240
6.55k
            if (cur_track) {
241
882
                if (type == 1) {
242
37
                    cur_track->start = time;
243
37
                    cur_track->filename = filename;
244
845
                } else if (type == 0) {
245
263
                    cur_track->pregap_start = time;
246
263
                }
247
882
            }
248
6.55k
            break;
249
1.88k
        }
250
7.71k
        case CUE_FILE:
251
            // NOTE: FILE comes before TRACK, so don't use cur_track->filename
252
7.71k
            filename = read_quoted(f, &param);
253
7.71k
            break;
254
49.3k
        }
255
49.3k
    }
256
257
104
    return f;
258
567
}
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
}