555 lines
11 KiB
C
555 lines
11 KiB
C
/*
|
|
* Copyright 2016 Various Authors
|
|
*
|
|
* This program 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.
|
|
*
|
|
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <ctype.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/mman.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
|
|
#include "cue.h"
|
|
#include "xmalloc.h"
|
|
#include "file.h"
|
|
|
|
#define ASCII_LOWER_TO_UPPER(c) ((c) & ~0x20)
|
|
|
|
struct cue_track_proto {
|
|
struct list_head node;
|
|
|
|
char *file; /* owned by cue_parser */
|
|
uint32_t nr;
|
|
int32_t pregap;
|
|
int32_t postgap;
|
|
int32_t index0;
|
|
int32_t index1;
|
|
|
|
struct cue_meta meta;
|
|
};
|
|
|
|
struct cue_parser {
|
|
const char *src;
|
|
size_t len;
|
|
bool err;
|
|
|
|
struct list_head files;
|
|
struct list_head tracks;
|
|
size_t num_tracks;
|
|
|
|
struct cue_meta meta;
|
|
};
|
|
|
|
struct cue_switch {
|
|
const char *cmd;
|
|
void (*parser)(struct cue_parser *p);
|
|
};
|
|
|
|
static struct cue_track_proto *cue_last_proto(struct cue_parser *p)
|
|
{
|
|
if (list_empty(&p->tracks))
|
|
return NULL;
|
|
return list_entry(p->tracks.prev, struct cue_track_proto, node);
|
|
}
|
|
|
|
static inline void cue_consume(struct cue_parser *p)
|
|
{
|
|
p->len--;
|
|
p->src++;
|
|
}
|
|
|
|
static void cue_set_err(struct cue_parser *p)
|
|
{
|
|
p->err = true;
|
|
}
|
|
|
|
static bool cue_str_eq(const char *a, size_t a_len, const char *b, size_t b_len)
|
|
{
|
|
if (a_len != b_len)
|
|
return false;
|
|
for (size_t i = 0; i < a_len; i++) {
|
|
if (ASCII_LOWER_TO_UPPER(a[i]) != ASCII_LOWER_TO_UPPER(b[i]))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void cue_skip_spaces(struct cue_parser *p)
|
|
{
|
|
while (p->len > 0 && (*p->src == ' ' || *p->src == '\t'))
|
|
cue_consume(p);
|
|
}
|
|
|
|
static size_t cue_extract_token(struct cue_parser *p, const char **start)
|
|
{
|
|
cue_skip_spaces(p);
|
|
|
|
bool quoted = p->len > 0 && *p->src == '"';
|
|
if (quoted)
|
|
cue_consume(p);
|
|
|
|
*start = p->src;
|
|
|
|
while (p->len > 0) {
|
|
char c = *p->src;
|
|
if (c == '\n' || c == '\r')
|
|
break;
|
|
if (quoted) {
|
|
if (c == '"')
|
|
break;
|
|
} else {
|
|
if (c == ' ' || c == '\t')
|
|
break;
|
|
}
|
|
cue_consume(p);
|
|
}
|
|
|
|
if (quoted) {
|
|
size_t len = p->src - *start;
|
|
if (p->len > 0 && *p->src == '"')
|
|
cue_consume(p);
|
|
return len;
|
|
}
|
|
|
|
return p->src - *start;
|
|
}
|
|
|
|
static void cue_skip_line(struct cue_parser *p)
|
|
{
|
|
while (p->len > 0 && *p->src != '\n' && *p->src != '\r')
|
|
cue_consume(p);
|
|
|
|
if (p->len > 0) {
|
|
char c = *p->src;
|
|
cue_consume(p);
|
|
if (p->len > 0 && c == '\r' && *p->src == '\n')
|
|
cue_consume(p);
|
|
}
|
|
}
|
|
|
|
static char *cue_strdup(const char *start, size_t len)
|
|
{
|
|
char *s = xnew(char, len + 1);
|
|
s[len] = 0;
|
|
memcpy(s, start, len);
|
|
return s;
|
|
}
|
|
|
|
static uint32_t cue_parse_int(struct cue_parser *p, const char *start, size_t len)
|
|
{
|
|
uint32_t val = 0;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (!isdigit(start[i])) {
|
|
cue_set_err(p);
|
|
return 0;
|
|
}
|
|
val = val * 10 + start[i] - '0';
|
|
}
|
|
return val;
|
|
}
|
|
|
|
static void cue_parse_str(struct cue_parser *p, char **dst)
|
|
{
|
|
const char *start;
|
|
size_t len = cue_extract_token(p, &start);
|
|
if (!*dst)
|
|
*dst = cue_strdup(start, len);
|
|
}
|
|
|
|
#define CUE_PARSE_STR(field) \
|
|
static void cue_parse_##field(struct cue_parser *p) \
|
|
{ \
|
|
struct cue_track_proto *t = cue_last_proto(p); \
|
|
if (t) \
|
|
cue_parse_str(p, &t->meta.field); \
|
|
else \
|
|
cue_parse_str(p, &p->meta.field); \
|
|
}
|
|
|
|
CUE_PARSE_STR(performer)
|
|
CUE_PARSE_STR(songwriter)
|
|
CUE_PARSE_STR(title)
|
|
CUE_PARSE_STR(genre)
|
|
CUE_PARSE_STR(date)
|
|
CUE_PARSE_STR(comment)
|
|
CUE_PARSE_STR(compilation);
|
|
CUE_PARSE_STR(discnumber);
|
|
CUE_PARSE_STR(rg_gain);
|
|
CUE_PARSE_STR(rg_peak);
|
|
|
|
static void cue_parse_file(struct cue_parser *p)
|
|
{
|
|
struct cue_track_file *f = xnew(struct cue_track_file, 1);
|
|
|
|
f->file = NULL;
|
|
cue_parse_str(p, &f->file);
|
|
|
|
list_add_tail(&f->node, &p->files);
|
|
}
|
|
|
|
static char *cue_get_last_file(struct cue_parser *p)
|
|
{
|
|
if (list_empty(&p->files))
|
|
return NULL;
|
|
|
|
struct list_head *tail = list_prev(&p->files);
|
|
return list_entry(tail, struct cue_track_file, node)->file;
|
|
}
|
|
|
|
static void cue_parse_track(struct cue_parser *p)
|
|
{
|
|
char *curr_file = cue_get_last_file(p);
|
|
|
|
if (!curr_file) {
|
|
cue_set_err(p);
|
|
return;
|
|
}
|
|
|
|
const char *nr;
|
|
size_t len = cue_extract_token(p, &nr);
|
|
|
|
uint32_t d = cue_parse_int(p, nr, len);
|
|
if (p->err)
|
|
return;
|
|
|
|
struct cue_track_proto *t = xnew(struct cue_track_proto, 1);
|
|
*t = (struct cue_track_proto) {
|
|
.nr = d,
|
|
.pregap = -1,
|
|
.postgap = -1,
|
|
.index0 = -1,
|
|
.index1 = -1,
|
|
.file = curr_file,
|
|
};
|
|
|
|
list_add_tail(&t->node, &p->tracks);
|
|
p->num_tracks++;
|
|
}
|
|
|
|
static uint32_t cue_parse_time(struct cue_parser *p, const char *start, size_t len)
|
|
{
|
|
uint32_t vals[] = { 0, 0, 0 };
|
|
uint32_t *val = vals;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (start[i] == ':') {
|
|
if (val != &vals[2]) {
|
|
val++;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (!isdigit(start[i])) {
|
|
cue_set_err(p);
|
|
return 0;
|
|
}
|
|
*val = *val * 10 + start[i] - '0';
|
|
}
|
|
return (vals[0] * 60 + vals[1]) * 75 + vals[2];
|
|
}
|
|
|
|
static void cue_parse_index(struct cue_parser *p)
|
|
{
|
|
const char *nr;
|
|
size_t nr_len = cue_extract_token(p, &nr);
|
|
|
|
uint32_t d = cue_parse_int(p, nr, nr_len);
|
|
if (p->err || d > 1)
|
|
return;
|
|
|
|
const char *offset_str;
|
|
size_t offset_len = cue_extract_token(p, &offset_str);
|
|
|
|
uint32_t offset = cue_parse_time(p, offset_str, offset_len);
|
|
if (p->err)
|
|
return;
|
|
|
|
struct cue_track_proto *last = cue_last_proto(p);
|
|
if (!last)
|
|
return;
|
|
|
|
if (d == 0)
|
|
last->index0 = offset;
|
|
else
|
|
last->index1 = offset;
|
|
}
|
|
|
|
static void cue_parse_cmd(struct cue_parser *p, struct cue_switch *s)
|
|
{
|
|
const char *start;
|
|
size_t len = cue_extract_token(p, &start);
|
|
|
|
while (s->cmd) {
|
|
if (cue_str_eq(start, len, s->cmd, strlen(s->cmd))) {
|
|
s->parser(p);
|
|
return;
|
|
}
|
|
s++;
|
|
}
|
|
}
|
|
|
|
static void cue_parse_rem(struct cue_parser *p)
|
|
{
|
|
struct cue_switch cmds[] = {
|
|
{ "DATE", cue_parse_date },
|
|
{ "GENRE", cue_parse_genre },
|
|
{ "COMMENT", cue_parse_comment },
|
|
{ "COMPILATION", cue_parse_compilation },
|
|
{ "DISCNUMBER", cue_parse_discnumber },
|
|
{ "REPLAYGAIN_ALBUM_GAIN", cue_parse_rg_gain },
|
|
{ "REPLAYGAIN_TRACK_GAIN", cue_parse_rg_gain },
|
|
{ "REPLAYGAIN_ALBUM_PEAK", cue_parse_rg_peak },
|
|
{ "REPLAYGAIN_TRACK_PEAK", cue_parse_rg_peak },
|
|
{ 0 },
|
|
};
|
|
|
|
cue_parse_cmd(p, cmds);
|
|
}
|
|
|
|
static void cue_parse_gap(struct cue_parser *p, bool post)
|
|
{
|
|
const char *gap_str;
|
|
size_t gap_len = cue_extract_token(p, &gap_str);
|
|
|
|
uint32_t gap = cue_parse_time(p, gap_str, gap_len);
|
|
if (p->err)
|
|
return;
|
|
|
|
struct cue_track_proto *last = cue_last_proto(p);
|
|
if (!last)
|
|
return;
|
|
|
|
if (post)
|
|
last->postgap = gap;
|
|
else
|
|
last->pregap = gap;
|
|
}
|
|
|
|
static void cue_parse_pregap(struct cue_parser *p)
|
|
{
|
|
cue_parse_gap(p, false);
|
|
}
|
|
|
|
static void cue_parse_postgap(struct cue_parser *p)
|
|
{
|
|
cue_parse_gap(p, true);
|
|
}
|
|
|
|
static void cue_parse_line(struct cue_parser *p)
|
|
{
|
|
struct cue_switch cmds[] = {
|
|
{ "FILE", cue_parse_file },
|
|
{ "PERFORMER", cue_parse_performer },
|
|
{ "SONGWRITER", cue_parse_songwriter },
|
|
{ "TITLE", cue_parse_title },
|
|
{ "TRACK", cue_parse_track },
|
|
{ "INDEX", cue_parse_index },
|
|
{ "REM", cue_parse_rem },
|
|
{ "PREGAP", cue_parse_pregap },
|
|
{ "POSTGAP", cue_parse_postgap },
|
|
{ 0 },
|
|
};
|
|
|
|
cue_parse_cmd(p, cmds);
|
|
cue_skip_line(p);
|
|
}
|
|
|
|
static void cue_post_process(struct cue_parser *p)
|
|
{
|
|
if (list_empty(&p->files) || p->num_tracks == 0)
|
|
goto err;
|
|
|
|
struct cue_track_proto *t;
|
|
struct cue_track_proto *prev = NULL;
|
|
list_for_each_entry(t, &p->tracks, node) {
|
|
if (prev && prev->nr >= t->nr)
|
|
goto err;
|
|
|
|
if (t->index0 == -1 && t->index1 == -1)
|
|
goto err;
|
|
|
|
/*
|
|
* NOTE: if we don't have index1, then the pregap spans the
|
|
* whole track, so we would have an empty track.
|
|
* This is pretty useless, so we do the simple thing
|
|
*/
|
|
if (t->index1 == -1)
|
|
t->index1 = t->index0;
|
|
|
|
if (t->index0 == -1)
|
|
t->index0 = t->index1;
|
|
|
|
if (prev && prev->file == t->file && prev->index1 > t->index0)
|
|
goto err;
|
|
|
|
prev = t;
|
|
}
|
|
|
|
return;
|
|
err:
|
|
cue_set_err(p);
|
|
}
|
|
|
|
static void cue_meta_move(struct cue_meta *l, struct cue_meta *r)
|
|
{
|
|
*l = *r;
|
|
*r = (struct cue_meta) { 0 };
|
|
}
|
|
|
|
static struct cue_sheet *cue_parser_to_sheet(struct cue_parser *p)
|
|
{
|
|
struct cue_sheet *s = xnew(struct cue_sheet, 1);
|
|
|
|
/* Move file list */
|
|
list_add(&s->files, &p->files);
|
|
list_del_init(&p->files);
|
|
|
|
s->tracks = xnew(struct cue_track, p->num_tracks);
|
|
s->num_tracks = p->num_tracks;
|
|
|
|
cue_meta_move(&s->meta, &p->meta);
|
|
|
|
size_t i = 0;
|
|
struct cue_track_proto *tp = NULL;
|
|
struct cue_track_proto *prev_tp = NULL;
|
|
list_for_each_entry(tp, &p->tracks, node) {
|
|
struct cue_track *t = &s->tracks[i];
|
|
t->file = tp->file;
|
|
t->offset = tp->index1 / 75.0;
|
|
t->length = -1;
|
|
t->number = tp->nr;
|
|
|
|
if (i > 0 && t->file == s->tracks[i - 1].file) {
|
|
s->tracks[i - 1].length = (tp->index0 - prev_tp->index1) / 75.0;
|
|
}
|
|
|
|
cue_meta_move(&t->meta, &tp->meta);
|
|
|
|
prev_tp = tp;
|
|
i++;
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
static void cue_meta_free(struct cue_meta *m)
|
|
{
|
|
free(m->performer);
|
|
free(m->songwriter);
|
|
free(m->title);
|
|
free(m->genre);
|
|
free(m->date);
|
|
free(m->comment);
|
|
free(m->compilation);
|
|
free(m->discnumber);
|
|
free(m->rg_gain);
|
|
free(m->rg_peak);
|
|
}
|
|
|
|
static void cue_free_files(struct list_head *files)
|
|
{
|
|
struct cue_track_file *tf, *next;
|
|
list_for_each_entry_safe(tf, next, files, node) {
|
|
free(tf->file);
|
|
free(tf);
|
|
}
|
|
}
|
|
|
|
static void cue_parser_free(struct cue_parser *p)
|
|
{
|
|
struct cue_track_proto *t, *next;
|
|
list_for_each_entry_safe(t, next, &p->tracks, node) {
|
|
cue_meta_free(&t->meta);
|
|
free(t);
|
|
}
|
|
|
|
cue_free_files(&p->files);
|
|
|
|
cue_meta_free(&p->meta);
|
|
}
|
|
|
|
struct cue_sheet *cue_parse(const char *src, size_t len)
|
|
{
|
|
struct cue_sheet *res = NULL;
|
|
|
|
struct cue_parser p = {
|
|
.src = src,
|
|
.len = len,
|
|
};
|
|
list_init(&p.tracks);
|
|
list_init(&p.files);
|
|
|
|
while (p.len > 0 && !p.err)
|
|
cue_parse_line(&p);
|
|
|
|
if (p.err)
|
|
goto out;
|
|
|
|
cue_post_process(&p);
|
|
|
|
if (p.err)
|
|
goto out;
|
|
|
|
res = cue_parser_to_sheet(&p);
|
|
|
|
out:
|
|
cue_parser_free(&p);
|
|
return res;
|
|
}
|
|
|
|
struct cue_sheet *cue_from_file(const char *file)
|
|
{
|
|
ssize_t size;
|
|
char *buf = mmap_file(file, &size);
|
|
if (size == -1)
|
|
return NULL;
|
|
struct cue_sheet *rv;
|
|
|
|
// Check for UTF-8 BOM, and skip ahead if found
|
|
if (size >= 3 && memcmp(buf, "\xEF\xBB\xBF", 3) == 0) {
|
|
rv = cue_parse(buf + 3, size - 3);
|
|
} else {
|
|
rv = cue_parse(buf, size);
|
|
}
|
|
|
|
munmap(buf, size);
|
|
return rv;
|
|
}
|
|
|
|
void cue_free(struct cue_sheet *s)
|
|
{
|
|
size_t i;
|
|
for (i = 0; i < s->num_tracks; i++)
|
|
cue_meta_free(&s->tracks[i].meta);
|
|
free(s->tracks);
|
|
|
|
cue_free_files(&s->files);
|
|
|
|
cue_meta_free(&s->meta);
|
|
free(s);
|
|
}
|
|
|
|
struct cue_track *cue_get_track(struct cue_sheet *s, size_t n)
|
|
{
|
|
size_t i;
|
|
for (i = 0; i < s->num_tracks; i++) {
|
|
struct cue_track *t = &s->tracks[i];
|
|
if (t->number == n)
|
|
return t;
|
|
}
|
|
return NULL;
|
|
}
|