/*
* 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 .
*/
#include
#include
#include
#include
#include
#include
#include
#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;
}