Coverage Report

Created: 2025-12-10 06:44

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
232k
{
70
573k
    while (data.len) {
71
531k
        bstr rest = data;
72
531k
        int code = bstr_decode_utf8(data, &rest);
73
531k
        if (code < 0) {
74
            // Tolerate Latin1 => probing works (which doesn't convert charsets).
75
27.2k
            code = data.start[0];
76
27.2k
            rest.start += 1;
77
27.2k
            rest.len -= 1;
78
27.2k
        }
79
3.13M
        for (size_t n = 0; n < MP_ARRAY_SIZE(spaces); n++) {
80
2.94M
            if (spaces[n] == code) {
81
341k
                data = rest;
82
341k
                goto next;
83
341k
            }
84
2.94M
        }
85
190k
        break;
86
341k
    next: ;
87
341k
    }
88
232k
    return data;
89
232k
}
90
91
static enum cue_command read_cmd(struct bstr *data, struct bstr *out_params)
92
192k
{
93
192k
    struct bstr line = bstr_strip_linebreaks(bstr_getline(*data, data));
94
192k
    line = lstrip_whitespace(line);
95
192k
    if (line.len == 0)
96
39.0k
        return CUE_EMPTY;
97
2.15M
    for (int n = 0; cue_command_strings[n].command != -1; n++) {
98
2.02M
        struct bstr name = bstr0(cue_command_strings[n].text);
99
2.02M
        if (bstr_case_startswith(line, name)) {
100
22.4k
            struct bstr rest = bstr_cut(line, name.len);
101
22.4k
            struct bstr par = lstrip_whitespace(rest);
102
22.4k
            if (rest.len && par.len == rest.len)
103
188
                continue;
104
22.2k
            if (out_params)
105
17.8k
                *out_params = par;
106
22.2k
            return cue_command_strings[n].command;
107
22.4k
        }
108
2.02M
    }
109
130k
    return CUE_ERROR;
110
153k
}
111
112
static bool eat_char(struct bstr *data, char ch)
113
12.7k
{
114
12.7k
    if (data->len && data->start[0] == ch) {
115
3.10k
        *data = bstr_cut(*data, 1);
116
3.10k
        return true;
117
9.64k
    } else {
118
9.64k
        return false;
119
9.64k
    }
120
12.7k
}
121
122
static char *read_quoted(void *talloc_ctx, struct bstr *data)
123
7.56k
{
124
7.56k
    *data = lstrip_whitespace(*data);
125
7.56k
    if (!eat_char(data, '"'))
126
6.93k
        return NULL;
127
636
    int end = bstrchr(*data, '"');
128
636
    if (end < 0)
129
259
        return NULL;
130
377
    struct bstr res = bstr_splice(*data, 0, end);
131
377
    *data = bstr_cut(*data, end + 1);
132
377
    return bstrto0(talloc_ctx, res);
133
636
}
134
135
static struct bstr strip_quotes(struct bstr data)
136
3.15k
{
137
3.15k
    bstr s = data;
138
3.15k
    if (bstr_eatstart0(&s, "\"") && bstr_eatend0(&s, "\""))
139
914
        return s;
140
2.24k
    return data;
141
3.15k
}
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
10.3k
{
148
10.3k
    *data = lstrip_whitespace(*data);
149
10.3k
    if (data->len && data->start[0] == '-')
150
735
        return -1;
151
9.62k
    struct bstr s = *data;
152
9.62k
    int res = (int)bstrtoll(s, &s, 10);
153
9.62k
    if (data->len == s.len || (two_digit && data->len - s.len > 2))
154
7.25k
        return -1;
155
2.37k
    *data = s;
156
2.37k
    return res;
157
9.62k
}
158
159
static double read_time(struct bstr *data)
160
2.59k
{
161
2.59k
    struct bstr s = *data;
162
2.59k
    bool ok = true;
163
2.59k
    double t1 = read_int(&s, false);
164
2.59k
    ok = eat_char(&s, ':') && ok;
165
2.59k
    double t2 = read_int(&s, true);
166
2.59k
    ok = eat_char(&s, ':') && ok;
167
2.59k
    double t3 = read_int(&s, true);
168
2.59k
    ok = ok && t1 >= 0 && t2 >= 0 && t3 >= 0;
169
2.59k
    return ok ? t1 * 60.0 + t2 + t3 * SECS_PER_CUE_FRAME : 0;
170
2.59k
}
171
172
static struct bstr skip_utf8_bom(struct bstr data)
173
131k
{
174
131k
    return bstr_startswith0(data, "\xEF\xBB\xBF") ? bstr_cut(data, 3) : data;
175
131k
}
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
130k
{
182
130k
    bool valid = false;
183
130k
    data = skip_utf8_bom(data);
184
159k
    for (;;) {
185
159k
        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
159k
        if (data.len == 0)
189
81.8k
            break;
190
78.0k
        if (cmd == CUE_ERROR)
191
49.0k
            return false;
192
29.0k
        if (cmd != CUE_EMPTY)
193
4.01k
            valid = true;
194
29.0k
    }
195
81.8k
    return valid;
196
130k
}
197
198
struct cue_file *mp_parse_cue(struct bstr data)
199
597
{
200
597
    struct cue_file *f = talloc_zero(NULL, struct cue_file);
201
597
    f->tags = talloc_zero(f, struct mp_tags);
202
203
597
    data = skip_utf8_bom(data);
204
205
597
    char *filename = NULL;
206
    // Global metadata, and copied into new tracks.
207
597
    struct cue_track proto_track = {0};
208
597
    struct cue_track *cur_track = NULL;
209
210
32.3k
    while (data.len) {
211
32.2k
        struct bstr param;
212
32.2k
        int cmd = read_cmd(&data, &param);
213
32.2k
        switch (cmd) {
214
491
        case CUE_ERROR:
215
491
            talloc_free(f);
216
491
            return NULL;
217
1.93k
        case CUE_TRACK: {
218
1.93k
            if (bstr_find0(param, "AUDIO") == -1)
219
1.16k
                break;
220
773
            MP_TARRAY_GROW(f, f->tracks, f->num_tracks);
221
773
            f->num_tracks += 1;
222
773
            cur_track = &f->tracks[f->num_tracks - 1];
223
773
            *cur_track = proto_track;
224
773
            cur_track->tags = talloc_zero(f, struct mp_tags);
225
773
            break;
226
1.93k
        }
227
3.15k
        case CUE_TITLE:
228
3.15k
        case CUE_PERFORMER: {
229
3.15k
            static const char *metanames[] = {
230
3.15k
                [CUE_TITLE] = "title",
231
3.15k
                [CUE_PERFORMER] = "performer",
232
3.15k
            };
233
3.15k
            struct mp_tags *tags = cur_track ? cur_track->tags : f->tags;
234
3.15k
            mp_tags_set_bstr(tags, bstr0(metanames[cmd]), strip_quotes(param));
235
3.15k
            break;
236
3.15k
        }
237
2.59k
        case CUE_INDEX: {
238
2.59k
            int type = read_int(&param, true);
239
2.59k
            double time = read_time(&param);
240
2.59k
            if (cur_track) {
241
50
                if (type == 1) {
242
7
                    cur_track->start = time;
243
7
                    cur_track->filename = filename;
244
43
                } else if (type == 0) {
245
3
                    cur_track->pregap_start = time;
246
3
                }
247
50
            }
248
2.59k
            break;
249
3.15k
        }
250
7.56k
        case CUE_FILE:
251
            // NOTE: FILE comes before TRACK, so don't use cur_track->filename
252
7.56k
            filename = read_quoted(f, &param);
253
7.56k
            break;
254
32.2k
        }
255
32.2k
    }
256
257
106
    return f;
258
597
}
259
260
int mp_check_embedded_cue(struct cue_file *f)
261
2
{
262
2
    if (f->num_tracks == 0)
263
2
        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
}