/* * libid3tag - ID3 tag manipulation library * Copyright (C) 2000-2003 Underbit Technologies, Inc. * * 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * $Id: tag.c,v 1.1 2003/08/14 03:57:13 shank Exp $ */ # ifdef HAVE_CONFIG_H # include "config.h" # endif # include "global.h" # include # include # ifdef HAVE_ASSERT_H # include # endif # include "id3tag.h" # include "tag.h" # include "frame.h" # include "compat.h" # include "parse.h" # include "render.h" # include "latin1.h" # include "ucs4.h" # include "genre.h" # include "crc.h" # include "field.h" # include "util.h" /* * NAME: tag->new() * DESCRIPTION: allocate and return a new, empty tag */ struct id3_tag *id3_tag_new(void) { struct id3_tag *tag; tag = malloc(sizeof(*tag)); if (tag) { tag->refcount = 0; tag->version = ID3_TAG_VERSION; tag->flags = 0; tag->extendedflags = 0; tag->restrictions = 0; tag->options = /* ID3_TAG_OPTION_UNSYNCHRONISATION | */ ID3_TAG_OPTION_COMPRESSION | ID3_TAG_OPTION_CRC; tag->nframes = 0; tag->frames = 0; tag->paddedsize = 0; } return tag; } /* * NAME: tag->delete() * DESCRIPTION: destroy a tag and deallocate all associated memory */ void id3_tag_delete(struct id3_tag *tag) { assert(tag); if (tag->refcount == 0) { id3_tag_clearframes(tag); if (tag->frames) free(tag->frames); free(tag); } } /* * NAME: tag->addref() * DESCRIPTION: add an external reference to a tag */ void id3_tag_addref(struct id3_tag *tag) { assert(tag); ++tag->refcount; } /* * NAME: tag->delref() * DESCRIPTION: remove an external reference to a tag */ void id3_tag_delref(struct id3_tag *tag) { assert(tag && tag->refcount > 0); --tag->refcount; } /* * NAME: tag->version() * DESCRIPTION: return the tag's original ID3 version number */ unsigned int id3_tag_version(struct id3_tag const *tag) { assert(tag); return tag->version; } /* * NAME: tag->options() * DESCRIPTION: get or set tag options */ int id3_tag_options(struct id3_tag *tag, int mask, int values) { assert(tag); if (mask) tag->options = (tag->options & ~mask) | (values & mask); return tag->options; } /* * NAME: tag->setlength() * DESCRIPTION: set the minimum rendered tag size */ void id3_tag_setlength(struct id3_tag *tag, id3_length_t length) { assert(tag); tag->paddedsize = length; } /* * NAME: tag->clearframes() * DESCRIPTION: detach and delete all frames associated with a tag */ void id3_tag_clearframes(struct id3_tag *tag) { unsigned int i; assert(tag); for (i = 0; i < tag->nframes; ++i) { id3_frame_delref(tag->frames[i]); id3_frame_delete(tag->frames[i]); } tag->nframes = 0; } /* * NAME: tag->attachframe() * DESCRIPTION: attach a frame to a tag */ int id3_tag_attachframe(struct id3_tag *tag, struct id3_frame *frame) { struct id3_frame **frames; assert(tag && frame); frames = realloc(tag->frames, (tag->nframes + 1) * sizeof(*frames)); if (frames == 0) return -1; tag->frames = frames; tag->frames[tag->nframes++] = frame; id3_frame_addref(frame); return 0; } /* * NAME: tag->detachframe() * DESCRIPTION: detach (but don't delete) a frame from a tag */ int id3_tag_detachframe(struct id3_tag *tag, struct id3_frame *frame) { unsigned int i; assert(tag && frame); for (i = 0; i < tag->nframes; ++i) { if (tag->frames[i] == frame) break; } if (i == tag->nframes) return -1; --tag->nframes; while (i++ < tag->nframes) tag->frames[i - 1] = tag->frames[i]; id3_frame_delref(frame); return 0; } /* * NAME: tag->findframe() * DESCRIPTION: find in a tag the nth (0-based) frame with the given frame ID */ struct id3_frame *id3_tag_findframe(struct id3_tag const *tag, char const *id, unsigned int index) { unsigned int len, i; assert(tag); if (id == 0 || *id == 0) return (index < tag->nframes) ? tag->frames[index] : 0; len = strlen(id); if (len == 4) { struct id3_compat const *compat; compat = id3_compat_lookup(id, len); if (compat && compat->equiv && !compat->translate) { id = compat->equiv; len = strlen(id); } } for (i = 0; i < tag->nframes; ++i) { if (strncmp(tag->frames[i]->id, id, len) == 0 && index-- == 0) return tag->frames[i]; } return 0; } enum tagtype { TAGTYPE_NONE = 0, TAGTYPE_ID3V1, TAGTYPE_ID3V2, TAGTYPE_ID3V2_FOOTER }; static enum tagtype tagtype(id3_byte_t const *data, id3_length_t length) { if (length >= 3 && data[0] == 'T' && data[1] == 'A' && data[2] == 'G') return TAGTYPE_ID3V1; if (length >= 10 && ((data[0] == 'I' && data[1] == 'D' && data[2] == '3') || (data[0] == '3' && data[1] == 'D' && data[2] == 'I')) && data[3] < 0xff && data[4] < 0xff && data[6] < 0x80 && data[7] < 0x80 && data[8] < 0x80 && data[9] < 0x80) return data[0] == 'I' ? TAGTYPE_ID3V2 : TAGTYPE_ID3V2_FOOTER; return TAGTYPE_NONE; } static void parse_header(id3_byte_t const **ptr, unsigned int *version, int *flags, id3_length_t *size) { *ptr += 3; *version = id3_parse_uint(ptr, 2); *flags = id3_parse_uint(ptr, 1); *size = id3_parse_syncsafe(ptr, 4); } /* * NAME: tag->query() * DESCRIPTION: if a tag begins at the given location, return its size */ signed long id3_tag_query(id3_byte_t const *data, id3_length_t length) { unsigned int version; int flags; id3_length_t size; assert(data); switch (tagtype(data, length)) { case TAGTYPE_ID3V1: return 128; case TAGTYPE_ID3V2: parse_header(&data, &version, &flags, &size); if (flags & ID3_TAG_FLAG_FOOTERPRESENT) size += 10; return 10 + size; case TAGTYPE_ID3V2_FOOTER: parse_header(&data, &version, &flags, &size); return -size - 10; case TAGTYPE_NONE: break; } return 0; } static void trim(char *str) { char *ptr; ptr = str + strlen(str); while (ptr > str && ptr[-1] == ' ') --ptr; *ptr = 0; } static int v1_attachstr(struct id3_tag *tag, char const *id, char *text, unsigned long number) { struct id3_frame *frame; id3_ucs4_t ucs4[31]; if (text) { trim(text); if (*text == 0) return 0; } frame = id3_frame_new(id); if (frame == 0) return -1; if (id3_field_settextencoding(&frame->fields[0], ID3_FIELD_TEXTENCODING_ISO_8859_1) == -1) goto fail; if (text) id3_latin1_decode(text, ucs4); else id3_ucs4_putnumber(ucs4, number); if (strcmp(id, ID3_FRAME_COMMENT) == 0) { if (id3_field_setlanguage(&frame->fields[1], "XXX") == -1 || id3_field_setstring(&frame->fields[2], id3_ucs4_empty) == -1 || id3_field_setfullstring(&frame->fields[3], ucs4) == -1) goto fail; } else { id3_ucs4_t *ptr = ucs4; if (id3_field_setstrings(&frame->fields[1], 1, &ptr) == -1) goto fail; } if (id3_tag_attachframe(tag, frame) == -1) goto fail; return 0; fail: id3_frame_delete(frame); return -1; } static struct id3_tag *v1_parse(id3_byte_t const *data) { struct id3_tag *tag; tag = id3_tag_new(); if (tag) { char title[31], artist[31], album[31], year[5], comment[31]; unsigned int genre, track; tag->version = 0x0100; tag->options |= ID3_TAG_OPTION_ID3V1; tag->options &= ~ID3_TAG_OPTION_COMPRESSION; tag->restrictions = ID3_TAG_RESTRICTION_TEXTENCODING_LATIN1_UTF8 | ID3_TAG_RESTRICTION_TEXTSIZE_30_CHARS; title[30] = artist[30] = album[30] = year[4] = comment[30] = 0; memcpy(title, &data[3], 30); memcpy(artist, &data[33], 30); memcpy(album, &data[63], 30); memcpy(year, &data[93], 4); memcpy(comment, &data[97], 30); genre = data[127]; track = 0; if (comment[28] == 0 && comment[29] != 0) { track = comment[29]; tag->version = 0x0101; } /* populate tag frames */ if (v1_attachstr(tag, ID3_FRAME_TITLE, title, 0) == -1 || v1_attachstr(tag, ID3_FRAME_ARTIST, artist, 0) == -1 || v1_attachstr(tag, ID3_FRAME_ALBUM, album, 0) == -1 || v1_attachstr(tag, ID3_FRAME_YEAR, year, 0) == -1 || (track && v1_attachstr(tag, ID3_FRAME_TRACK, 0, track) == -1) || (genre < 0xff && v1_attachstr(tag, ID3_FRAME_GENRE, 0, genre) == -1) || v1_attachstr(tag, ID3_FRAME_COMMENT, comment, 0) == -1) { id3_tag_delete(tag); tag = 0; } } return tag; } static struct id3_tag *v2_parse(id3_byte_t const *ptr) { struct id3_tag *tag; id3_byte_t *mem = 0; tag = id3_tag_new(); if (tag) { id3_byte_t const *end; id3_length_t size; parse_header(&ptr, &tag->version, &tag->flags, &size); tag->paddedsize = 10 + size; if ((tag->flags & ID3_TAG_FLAG_UNSYNCHRONISATION) && ID3_TAG_VERSION_MAJOR(tag->version) < 4) { mem = malloc(size); if (mem == 0) goto fail; memcpy(mem, ptr, size); size = id3_util_deunsynchronise(mem, size); ptr = mem; } end = ptr + size; if (tag->flags & ID3_TAG_FLAG_EXTENDEDHEADER) { switch (ID3_TAG_VERSION_MAJOR(tag->version)) { case 2: goto fail; case 3: { id3_byte_t const *ehptr, *ehend; id3_length_t ehsize; enum { EH_FLAG_CRC = 0x8000 /* CRC data present */ }; if (end - ptr < 4) goto fail; ehsize = id3_parse_uint(&ptr, 4); if (ehsize > end - ptr) goto fail; ehptr = ptr; ehend = ptr + ehsize; ptr = ehend; if (ehend - ehptr >= 6) { int ehflags; id3_length_t padsize; ehflags = id3_parse_uint(&ehptr, 2); padsize = id3_parse_uint(&ehptr, 4); if (padsize > end - ptr) goto fail; end -= padsize; if (ehflags & EH_FLAG_CRC) { unsigned long crc; if (ehend - ehptr < 4) goto fail; crc = id3_parse_uint(&ehptr, 4); if (crc != id3_crc_calculate(ptr, end - ptr)) goto fail; tag->extendedflags |= ID3_TAG_EXTENDEDFLAG_CRCDATAPRESENT; } } } break; case 4: { id3_byte_t const *ehptr, *ehend; id3_length_t ehsize; unsigned int bytes; if (end - ptr < 4) goto fail; ehptr = ptr; ehsize = id3_parse_syncsafe(&ptr, 4); if (ehsize < 6 || ehsize > end - ehptr) goto fail; ehend = ehptr + ehsize; bytes = id3_parse_uint(&ptr, 1); if (bytes < 1 || bytes > ehend - ptr) goto fail; ehptr = ptr + bytes; /* verify extended header size */ { id3_byte_t const *flagsptr = ptr, *dataptr = ehptr; unsigned int datalen; int ehflags; while (bytes--) { for (ehflags = id3_parse_uint(&flagsptr, 1); ehflags; ehflags = (ehflags << 1) & 0xff) { if (ehflags & 0x80) { if (dataptr == ehend) goto fail; datalen = id3_parse_uint(&dataptr, 1); if (datalen > 0x7f || datalen > ehend - dataptr) goto fail; dataptr += datalen; } } } } tag->extendedflags = id3_parse_uint(&ptr, 1); ptr = ehend; if (tag->extendedflags & ID3_TAG_EXTENDEDFLAG_TAGISANUPDATE) { bytes = id3_parse_uint(&ehptr, 1); ehptr += bytes; } if (tag->extendedflags & ID3_TAG_EXTENDEDFLAG_CRCDATAPRESENT) { unsigned long crc; bytes = id3_parse_uint(&ehptr, 1); if (bytes < 5) goto fail; crc = id3_parse_syncsafe(&ehptr, 5); ehptr += bytes - 5; if (crc != id3_crc_calculate(ptr, end - ptr)) goto fail; } if (tag->extendedflags & ID3_TAG_EXTENDEDFLAG_TAGRESTRICTIONS) { bytes = id3_parse_uint(&ehptr, 1); if (bytes < 1) goto fail; tag->restrictions = id3_parse_uint(&ehptr, 1); ehptr += bytes - 1; } } break; } } /* frames */ while (ptr < end) { struct id3_frame *frame; if (*ptr == 0) break; /* padding */ frame = id3_frame_parse(&ptr, end - ptr, tag->version); if (frame == 0 || id3_tag_attachframe(tag, frame) == -1) goto fail; } if (ID3_TAG_VERSION_MAJOR(tag->version) < 4 && id3_compat_fixup(tag) == -1) goto fail; } if (0) { fail: id3_tag_delete(tag); tag = 0; } if (mem) free(mem); return tag; } /* * NAME: tag->parse() * DESCRIPTION: parse a complete ID3 tag */ struct id3_tag *id3_tag_parse(id3_byte_t const *data, id3_length_t length) { id3_byte_t const *ptr; unsigned int version; int flags; id3_length_t size; assert(data); switch (tagtype(data, length)) { case TAGTYPE_ID3V1: return (length < 128) ? 0 : v1_parse(data); case TAGTYPE_ID3V2: break; case TAGTYPE_ID3V2_FOOTER: case TAGTYPE_NONE: return 0; } /* ID3v2.x */ ptr = data; parse_header(&ptr, &version, &flags, &size); switch (ID3_TAG_VERSION_MAJOR(version)) { case 4: if (flags & ID3_TAG_FLAG_FOOTERPRESENT) size += 10; case 2: case 3: return (length < 10 + size) ? 0 : v2_parse(data); } return 0; } static void v1_renderstr(struct id3_tag const *tag, char const *frameid, id3_byte_t **buffer, id3_length_t length) { struct id3_frame *frame; id3_ucs4_t const *string; frame = id3_tag_findframe(tag, frameid, 0); if (frame == 0) string = id3_ucs4_empty; else { if (strcmp(frameid, ID3_FRAME_COMMENT) == 0) string = id3_field_getfullstring(&frame->fields[3]); else string = id3_field_getstrings(&frame->fields[1], 0); } id3_render_paddedstring(buffer, string, length); } /* * NAME: v1->render() * DESCRIPTION: render an ID3v1 (or ID3v1.1) tag */ static id3_length_t v1_render(struct id3_tag const *tag, id3_byte_t *buffer) { id3_byte_t data[128], *ptr; struct id3_frame *frame; unsigned int i; int genre = -1; ptr = data; id3_render_immediate(&ptr, "TAG", 3); v1_renderstr(tag, ID3_FRAME_TITLE, &ptr, 30); v1_renderstr(tag, ID3_FRAME_ARTIST, &ptr, 30); v1_renderstr(tag, ID3_FRAME_ALBUM, &ptr, 30); v1_renderstr(tag, ID3_FRAME_YEAR, &ptr, 4); v1_renderstr(tag, ID3_FRAME_COMMENT, &ptr, 30); /* ID3v1.1 track number */ frame = id3_tag_findframe(tag, ID3_FRAME_TRACK, 0); if (frame) { unsigned int track; track = id3_ucs4_getnumber(id3_field_getstrings(&frame->fields[1], 0)); if (track > 0 && track <= 0xff) { ptr[-2] = 0; ptr[-1] = track; } } /* ID3v1 genre number */ frame = id3_tag_findframe(tag, ID3_FRAME_GENRE, 0); if (frame) { unsigned int nstrings; nstrings = id3_field_getnstrings(&frame->fields[1]); for (i = 0; i < nstrings; ++i) { genre = id3_genre_number(id3_field_getstrings(&frame->fields[1], i)); if (genre != -1) break; } if (i == nstrings && nstrings > 0) genre = ID3_GENRE_OTHER; } id3_render_int(&ptr, genre, 1); /* make sure the tag is not empty */ if (genre == -1) { for (i = 3; i < 127; ++i) { if (data[i] != ' ') break; } if (i == 127) return 0; } if (buffer) memcpy(buffer, data, 128); return 128; } /* * NAME: tag->render() * DESCRIPTION: render a complete ID3 tag */ id3_length_t id3_tag_render(struct id3_tag const *tag, id3_byte_t *buffer) { id3_length_t size = 0; id3_byte_t **ptr, *header_ptr = 0, *tagsize_ptr = 0, *crc_ptr = 0, *frames_ptr = 0; int flags, extendedflags; unsigned int i; assert(tag); if (tag->options & ID3_TAG_OPTION_ID3V1) return v1_render(tag, buffer); /* a tag must contain at least one (renderable) frame */ for (i = 0; i < tag->nframes; ++i) { if (id3_frame_render(tag->frames[i], 0, 0) > 0) break; } if (i == tag->nframes) return 0; ptr = buffer ? &buffer : 0; /* get flags */ flags = tag->flags & ID3_TAG_FLAG_KNOWNFLAGS; extendedflags = tag->extendedflags & ID3_TAG_EXTENDEDFLAG_KNOWNFLAGS; extendedflags &= ~ID3_TAG_EXTENDEDFLAG_CRCDATAPRESENT; if (tag->options & ID3_TAG_OPTION_CRC) extendedflags |= ID3_TAG_EXTENDEDFLAG_CRCDATAPRESENT; extendedflags &= ~ID3_TAG_EXTENDEDFLAG_TAGRESTRICTIONS; if (tag->restrictions) extendedflags |= ID3_TAG_EXTENDEDFLAG_TAGRESTRICTIONS; flags &= ~ID3_TAG_FLAG_UNSYNCHRONISATION; if (tag->options & ID3_TAG_OPTION_UNSYNCHRONISATION) flags |= ID3_TAG_FLAG_UNSYNCHRONISATION; flags &= ~ID3_TAG_FLAG_EXTENDEDHEADER; if (extendedflags) flags |= ID3_TAG_FLAG_EXTENDEDHEADER; flags &= ~ID3_TAG_FLAG_FOOTERPRESENT; if (tag->options & ID3_TAG_OPTION_APPENDEDTAG) flags |= ID3_TAG_FLAG_FOOTERPRESENT; /* header */ if (ptr) header_ptr = *ptr; size += id3_render_immediate(ptr, "ID3", 3); size += id3_render_int(ptr, ID3_TAG_VERSION, 2); size += id3_render_int(ptr, flags, 1); if (ptr) tagsize_ptr = *ptr; size += id3_render_syncsafe(ptr, 0, 4); /* extended header */ if (flags & ID3_TAG_FLAG_EXTENDEDHEADER) { id3_length_t ehsize = 0; id3_byte_t *ehsize_ptr = 0; if (ptr) ehsize_ptr = *ptr; ehsize += id3_render_syncsafe(ptr, 0, 4); ehsize += id3_render_int(ptr, 1, 1); ehsize += id3_render_int(ptr, extendedflags, 1); if (extendedflags & ID3_TAG_EXTENDEDFLAG_TAGISANUPDATE) ehsize += id3_render_int(ptr, 0, 1); if (extendedflags & ID3_TAG_EXTENDEDFLAG_CRCDATAPRESENT) { ehsize += id3_render_int(ptr, 5, 1); if (ptr) crc_ptr = *ptr; ehsize += id3_render_syncsafe(ptr, 0, 5); } if (extendedflags & ID3_TAG_EXTENDEDFLAG_TAGRESTRICTIONS) { ehsize += id3_render_int(ptr, 1, 1); ehsize += id3_render_int(ptr, tag->restrictions, 1); } if (ehsize_ptr) id3_render_syncsafe(&ehsize_ptr, ehsize, 4); size += ehsize; } /* frames */ if (ptr) frames_ptr = *ptr; for (i = 0; i < tag->nframes; ++i) size += id3_frame_render(tag->frames[i], ptr, tag->options); /* padding */ if (!(flags & ID3_TAG_FLAG_FOOTERPRESENT)) { if (size < tag->paddedsize) size += id3_render_padding(ptr, 0, tag->paddedsize - size); else if (tag->options & ID3_TAG_OPTION_UNSYNCHRONISATION) { if (ptr == 0) size += 1; else { if ((*ptr)[-1] == 0xff) size += id3_render_padding(ptr, 0, 1); } } } /* patch tag size and CRC */ if (tagsize_ptr) id3_render_syncsafe(&tagsize_ptr, size - 10, 4); if (crc_ptr) { id3_render_syncsafe(&crc_ptr, id3_crc_calculate(frames_ptr, *ptr - frames_ptr), 5); } /* footer */ if (flags & ID3_TAG_FLAG_FOOTERPRESENT) { size += id3_render_immediate(ptr, "3DI", 3); size += id3_render_binary(ptr, header_ptr + 3, 7); } return size; }