/* * Copyright 2008-2013 Various Authors * Copyright 2006 Chun-Yu Shei * * Cleaned up by Timo Hirvonen * * 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 "ape.h" #include "file.h" #include "xmalloc.h" #include "utils.h" #include #include #include #include #include /* http://www.personal.uni-jena.de/~pfk/mpp/sv8/apetag.html */ #define PREAMBLE_SIZE (8) static const char preamble[PREAMBLE_SIZE] = { 'A', 'P', 'E', 'T', 'A', 'G', 'E', 'X' }; /* NOTE: not sizeof(struct ape_header)! */ #define HEADER_SIZE (32) /* returns position of APE header or -1 if not found */ static int find_ape_tag_slow(int fd) { char buf[4096]; int match = 0; int pos = 0; /* seek to start of file */ if (lseek(fd, pos, SEEK_SET) == -1) return -1; while (1) { int i, got = read(fd, buf, sizeof(buf)); if (got == -1) { if (errno == EAGAIN || errno == EINTR) continue; break; } if (got == 0) break; for (i = 0; i < got; i++) { if (buf[i] != preamble[match]) { match = 0; continue; } match++; if (match == PREAMBLE_SIZE) return pos + i + 1 - PREAMBLE_SIZE; } pos += got; } return -1; } static int ape_parse_header(const char *buf, struct ape_header *h) { if (memcmp(buf, preamble, PREAMBLE_SIZE)) return 0; h->version = read_le32(buf + 8); h->size = read_le32(buf + 12); h->count = read_le32(buf + 16); h->flags = read_le32(buf + 20); return 1; } static int read_header(int fd, struct ape_header *h) { char buf[HEADER_SIZE]; if (read_all(fd, buf, sizeof(buf)) != sizeof(buf)) return 0; return ape_parse_header(buf, h); } /* sets fd right after the header and returns 1 if found, * otherwise returns 0 */ static int find_ape_tag(int fd, struct ape_header *h, int slow) { int pos; if (lseek(fd, -HEADER_SIZE, SEEK_END) == -1) return 0; if (read_header(fd, h)) return 1; /* try to skip ID3v1 tag at the end of the file */ if (lseek(fd, -(HEADER_SIZE + 128), SEEK_END) == -1) return 0; if (read_header(fd, h)) return 1; if (!slow) return 0; pos = find_ape_tag_slow(fd); if (pos == -1) return 0; if (lseek(fd, pos, SEEK_SET) == -1) return 0; return read_header(fd, h); } /* * All keys are ASCII and length is 2..255 * * UTF-8: Artist, Album, Title, Genre * Integer: Track (N or N/M) * Date: Year (release), "Record Date" * * UTF-8 strings are NOT zero terminated. * * Also support "discnumber" (vorbis) and "disc" (non-standard) */ static int ape_parse_one(const char *buf, int size, char **keyp, char **valp) { int pos = 0; while (size - pos > 8) { uint32_t val_len, flags; char *key, *val; int64_t max_key_len, key_len; val_len = read_le32(buf + pos); pos += 4; flags = read_le32(buf + pos); pos += 4; max_key_len = size - pos - (int64_t)val_len - 1; if (max_key_len < 0) { /* corrupt */ break; } for (key_len = 0; key_len < max_key_len && buf[pos + key_len]; key_len++) ; /* nothing */ if (buf[pos + key_len]) { /* corrupt */ break; } if (!AF_IS_UTF8(flags)) { /* ignore binary data */ pos += key_len + 1 + val_len; continue; } key = xstrdup(buf + pos); pos += key_len + 1; /* should not be NUL-terminated */ val = xstrndup(buf + pos, val_len); pos += val_len; /* could be moved to comment.c but I don't think anyone else would use it */ if (!strcasecmp(key, "record date") || !strcasecmp(key, "year")) { free(key); key = xstrdup("date"); } if (!strcasecmp(key, "date")) { /* Date format * * 1999-08-11 12:34:56 * 1999-08-11 12:34 * 1999-08-11 * 1999-08 * 1999 * 1999-W34 (week 34, totally crazy) * * convert to year, pl.c supports only years anyways * * FIXME: which one is the most common tag (year or record date)? */ if (strlen(val) > 4) val[4] = 0; } *keyp = key; *valp = val; return pos; } return -1; } /* return the number of comments, or -1 */ int ape_read_tags(struct apetag *ape, int fd, int slow) { struct ape_header *h = &ape->header; int rc = -1; off_t old_pos; /* save position */ old_pos = lseek(fd, 0, SEEK_CUR); if (!find_ape_tag(fd, h, slow)) goto fail; if (AF_IS_FOOTER(h->flags)) { /* seek back right after the header */ if (lseek(fd, -((int)h->size), SEEK_CUR) == -1) goto fail; } /* ignore insane tags */ if (h->size > 1024 * 1024) goto fail; ape->buf = xnew(char, h->size); if (read_all(fd, ape->buf, h->size) != h->size) goto fail; rc = h->count; fail: lseek(fd, old_pos, SEEK_SET); return rc; } /* returned key-name must be free'd */ char *ape_get_comment(struct apetag *ape, char **val) { struct ape_header *h = &ape->header; char *key; int rc; if (ape->pos >= h->size) return NULL; rc = ape_parse_one(ape->buf + ape->pos, h->size - ape->pos, &key, val); if (rc < 0) return NULL; ape->pos += rc; return key; }