Mercurial > audlegacy
diff src/libaudtag/ape/ape.c @ 4887:0ddbd0025174 default tip
added libaudtag. (not used yet.)
author | Yoshiki Yazawa <yaz@honeyplanet.jp> |
---|---|
date | Wed, 05 May 2010 18:26:06 +0900 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libaudtag/ape/ape.c Wed May 05 18:26:06 2010 +0900 @@ -0,0 +1,433 @@ +/* + * Copyright 2010 John Lindgren + * + * This file is part of Audacious. + * + * Audacious 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, version 3 of the License. + * + * Audacious 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 + * Audacious. If not, see <http://www.gnu.org/licenses/>. + * + * The Audacious team does not consider modular code linking to Audacious or + * using our public API to be a derived work. + */ + +/* TODO: + * - ReplayGain info + * - Support updating files that have their tag at the beginning? + */ + +#include <glib.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <audlegacy/vfs.h> + +#include "ape.h" + +#define DEBUG(...) fprintf (stderr, "APE: " __VA_ARGS__) + +typedef struct +{ + gchar magic[8]; + guint32 version; /* LE */ + guint32 length; /* LE */ + guint32 items; /* LE */ + guint32 flags; /* LE */ + guint64 reserved; +} +APEHeader; + +typedef struct +{ + gchar * key, * value; +} +ValuePair; + +#define APE_FLAG_HAS_HEADER (1 << 31) +#define APE_FLAG_HAS_NO_FOOTER (1 << 30) +#define APE_FLAG_IS_HEADER (1 << 29) + +static gboolean ape_read_header (VFSFile * handle, APEHeader * header) +{ + if (vfs_fread (header, 1, sizeof (APEHeader), handle) != sizeof (APEHeader)) + return FALSE; + + if (strncmp (header->magic, "APETAGEX", 8)) + return FALSE; + + header->version = GUINT32_FROM_LE (header->version); + header->length = GUINT32_FROM_LE (header->length); + header->items = GUINT32_FROM_LE (header->items); + header->flags = GUINT32_FROM_LE (header->flags); + + if (header->length < sizeof (APEHeader)) + return FALSE; + + return TRUE; +} + +static gboolean ape_find_header (VFSFile * handle, APEHeader * header, gint * + start, gint * length, gint * data_start, gint * data_length) +{ + APEHeader secondary; + + if (vfs_fseek (handle, 0, SEEK_SET)) + return FALSE; + + if (ape_read_header (handle, header)) + { + DEBUG ("Found header at 0, length = %d, version = %d.\n", (gint) + header->length, (gint) header->version); + * start = 0; + * length = header->length; + * data_start = sizeof (APEHeader); + * data_length = header->length - sizeof (APEHeader); + + if (! (header->flags & APE_FLAG_HAS_HEADER) || ! (header->flags & + APE_FLAG_IS_HEADER)) + { + DEBUG ("Invalid header flags (%u).\n", (guint) header->flags); + return FALSE; + } + + if (! (header->flags & APE_FLAG_HAS_NO_FOOTER)) + { + if (vfs_fseek (handle, header->length, SEEK_CUR)) + return FALSE; + + if (! ape_read_header (handle, & secondary)) + { + DEBUG ("Expected footer, but found none.\n"); + return FALSE; + } + + * length += sizeof (APEHeader); + } + + return TRUE; + } + + if (vfs_fseek (handle, -sizeof (APEHeader), SEEK_END)) + return FALSE; + + if (ape_read_header (handle, header)) + { + DEBUG ("Found footer at %d, length = %d, version = %d.\n", (gint) + vfs_ftell (handle) - (gint) sizeof (APEHeader), (gint) header->length, + (gint) header->version); + * start = vfs_ftell (handle) - header->length; + * length = header->length; + * data_start = vfs_ftell (handle) - header->length; + * data_length = header->length - sizeof (APEHeader); + + if ((header->flags & APE_FLAG_HAS_NO_FOOTER) || (header->flags & + APE_FLAG_IS_HEADER)) + { + DEBUG ("Invalid footer flags (%u).\n", (guint) header->flags); + return FALSE; + } + + if (header->flags & APE_FLAG_HAS_HEADER) + { + if (vfs_fseek (handle, -(gint) header->length - sizeof (APEHeader), + SEEK_CUR)) + return FALSE; + + if (! ape_read_header (handle, & secondary)) + { + DEBUG ("Expected header, but found none.\n"); + return FALSE; + } + + * start -= sizeof (APEHeader); + * length += sizeof (APEHeader); + } + + return TRUE; + } + + DEBUG ("No header found.\n"); + return FALSE; +} + +static gboolean ape_is_our_file (VFSFile * handle) +{ + APEHeader header; + gint start, length, data_start, data_length; + + return ape_find_header (handle, & header, & start, & length, & data_start, + & data_length); +} + +static ValuePair * ape_read_item (void * * data, gint length) +{ + guint32 * header = * data; + gchar * value; + ValuePair * pair; + + if (length < 8) + { + DEBUG ("Expected item, but only %d bytes remain in tag.\n", length); + return NULL; + } + + value = memchr ((gchar *) (* data) + 8, 0, length - 8); + + if (value == NULL) + { + DEBUG ("Unterminated item key (max length = %d).\n", length - 8); + return NULL; + } + + value ++; + + if (header[0] > (gchar *) (* data) + length - value) + { + DEBUG ("Item value of length %d, but only %d bytes remain in tag.\n", + (gint) header[0], (gint) ((gchar *) (* data) + length - value)); + return NULL; + } + + pair = g_malloc (sizeof (ValuePair)); + pair->key = g_strdup ((gchar *) (* data) + 8); + pair->value = g_strndup (value, header[0]); + + * data = value + header[0]; + + return pair; +} + +static GList * ape_read_tag (VFSFile * handle) +{ + GList * list = NULL; + APEHeader header; + gint start, length, data_start, data_length; + void * data, * item; + + if (! ape_find_header (handle, & header, & start, & length, & data_start, + & data_length)) + return NULL; + + if (vfs_fseek (handle, data_start, SEEK_SET)) + return NULL; + + data = g_malloc (data_length); + + if (vfs_fread (data, 1, data_length, handle) != data_length) + { + g_free (data); + return NULL; + } + + DEBUG ("Reading %d items:\n", header.items); + item = data; + + while (header.items --) + { + ValuePair * pair = ape_read_item (& item, (gchar *) data + data_length - + (gchar *) item); + + if (pair == NULL) + break; + + DEBUG ("Read: %s = %s.\n", pair->key, pair->value); + list = g_list_prepend (list, pair); + } + + g_free (data); + return g_list_reverse (list); +} + +static void free_tag_list (GList * list) +{ + while (list != NULL) + { + g_free (((ValuePair *) list->data)->key); + g_free (((ValuePair *) list->data)->value); + g_free (list->data); + list = g_list_delete_link (list, list); + } +} + +static Tuple * ape_fill_tuple (Tuple * tuple, VFSFile * handle) +{ + GList * list = ape_read_tag (handle), * node; + + for (node = list; node != NULL; node = node->next) + { + gchar * key = ((ValuePair *) node->data)->key; + gchar * value = ((ValuePair *) node->data)->value; + + if (! strcmp (key, "Artist")) + tuple_associate_string (tuple, FIELD_ARTIST, NULL, value); + else if (! strcmp (key, "Title")) + tuple_associate_string (tuple, FIELD_TITLE, NULL, value); + else if (! strcmp (key, "Album")) + tuple_associate_string (tuple, FIELD_ALBUM, NULL, value); + else if (! strcmp (key, "Comment")) + tuple_associate_string (tuple, FIELD_COMMENT, NULL, value); + else if (! strcmp (key, "Genre")) + tuple_associate_string (tuple, FIELD_GENRE, NULL, value); + else if (! strcmp (key, "Track")) + tuple_associate_int (tuple, FIELD_TRACK_NUMBER, NULL, atoi (value)); + else if (! strcmp (key, "Date")) + tuple_associate_int (tuple, FIELD_YEAR, NULL, atoi (value)); + } + + free_tag_list (list); + return tuple; +} + +static gboolean ape_write_item (VFSFile * handle, const gchar * key, + const gchar * value, int * written_length) +{ + gint key_len = strlen (key) + 1; + gint value_len = strlen (value); + guint32 header[2]; + + DEBUG ("Write: %s = %s.\n", key, value); + + header[0] = GUINT32_TO_LE (value_len); + header[1] = 0; + + if (vfs_fwrite (header, 1, 8, handle) != 8) + return FALSE; + + if (vfs_fwrite (key, 1, key_len, handle) != key_len) + return FALSE; + + if (vfs_fwrite (value, 1, value_len, handle) != value_len) + return FALSE; + + * written_length += 8 + key_len + value_len; + return TRUE; +} + +static gboolean write_string_item (Tuple * tuple, int field, VFSFile * handle, + const gchar * key, int * written_length, int * written_items) +{ + const gchar * value = tuple_get_string (tuple, field, NULL); + + if (value == NULL) + return TRUE; + + if (! ape_write_item (handle, key, value, written_length)) + return FALSE; + + (* written_items) ++; + return TRUE; +} + +static gboolean write_integer_item (Tuple * tuple, int field, VFSFile * handle, + const gchar * key, int * written_length, int * written_items) +{ + gint value = tuple_get_int (tuple, field, NULL); + gchar scratch[32]; + + if (! value) + return TRUE; + + snprintf (scratch, sizeof scratch, "%d", value); + + if (! ape_write_item (handle, key, scratch, written_length)) + return FALSE; + + (* written_items) ++; + return TRUE; +} + +static gboolean write_header (gint data_length, gint items, gboolean is_header, + VFSFile * handle) +{ + APEHeader header; + + memcpy (header.magic, "APETAGEX", 8); + header.version = GUINT32_TO_LE (2000); + header.length = GUINT32_TO_LE (data_length + sizeof (APEHeader)); + header.items = GUINT32_TO_LE (items); + header.flags = is_header ? GUINT32_TO_LE (APE_FLAG_HAS_HEADER | + APE_FLAG_IS_HEADER) : GUINT32_TO_LE (APE_FLAG_HAS_HEADER); + header.reserved = 0; + + return vfs_fwrite (& header, 1, sizeof (APEHeader), handle) == sizeof + (APEHeader); +} + +static gboolean ape_write_tag (Tuple * tuple, VFSFile * handle) +{ + GList * list = ape_read_tag (handle), * node; + APEHeader header; + gint start, length, data_start, data_length, items; + + if (! ape_find_header (handle, & header, & start, & length, & data_start, + & data_length)) + goto ERROR; + + if (start + length != vfs_fsize (handle)) + { + DEBUG ("Writing tags is only supported at end of file.\n"); + goto ERROR; + } + + if (vfs_truncate (handle, start) || vfs_fseek (handle, start, SEEK_SET) || + ! write_header (0, 0, TRUE, handle)) + goto ERROR; + + length = 0; + items = 0; + + if (! write_string_item (tuple, FIELD_ARTIST, handle, "Artist", & length, + & items) || ! write_string_item (tuple, FIELD_TITLE, handle, "Title", + & length, & items) || ! write_string_item (tuple, FIELD_ALBUM, handle, + "Album", & length, & items) || ! write_string_item (tuple, FIELD_COMMENT, + handle, "Comment", & length, & items) || ! write_string_item (tuple, + FIELD_GENRE, handle, "Genre", & length, & items) || ! write_integer_item + (tuple, FIELD_TRACK_NUMBER, handle, "Track", & length, & items) || + ! write_integer_item (tuple, FIELD_YEAR, handle, "Date", & length, & items)) + goto ERROR; + + for (node = list; node != NULL; node = node->next) + { + gchar * key = ((ValuePair *) node->data)->key; + gchar * value = ((ValuePair *) node->data)->value; + + if (! strcmp (key, "Artist") || ! strcmp (key, "Title") || ! strcmp + (key, "Album") || ! strcmp (key, "Comment") || ! strcmp (key, "Genre") + || ! strcmp (key, "Track") || ! strcmp (key, "Date")) + continue; + + if (! ape_write_item (handle, key, value, & length)) + goto ERROR; + + items ++; + } + + DEBUG ("Wrote %d items, %d bytes.\n", items, length); + + if (write_header (length, items, FALSE, handle) || vfs_fseek (handle, start, + SEEK_SET) || ! write_header (length, items, TRUE, handle)) + goto ERROR; + + free_tag_list (list); + return TRUE; + +ERROR: + free_tag_list (list); + return FALSE; +} + +const tag_module_t ape = +{ + .name = "APE", + .can_handle_file = ape_is_our_file, + .populate_tuple_from_file = ape_fill_tuple, + .write_tuple_to_file = ape_write_tag, +};