gstreamer/gst-libs/gst/tag/gstid3tag.c
Sebastian Dröge 7095b7c47a id3tag: Correctly validate the year from v1 tags before passing to GstDateTime
By using strtoul(), invalid values will get mapped to MAXULONG and we
would have to check errno. They won't get mapped to 0.

To solve this, use the signed g_ascii_strtoll(). This will map errors to
0 or G_MAXINT64 or G_MININT64, and the valid range for GstDateTime is >
0 and <= 9999 so we can directly check for this here.

Fixes https://gitlab.freedesktop.org/gstreamer/gstreamer/issues/384
2019-04-26 09:46:58 +03:00

389 lines
14 KiB
C

/* GStreamer
* Copyright (C) 2003 Benjamin Otte <in7y118@public.uni-hamburg.de>
*
* gstid3tag.c: plugin for reading / modifying id3 tags
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
/**
* SECTION:gsttagid3
* @title: ID3 tag utils
* @short_description: tag mappings and support functions for plugins
* dealing with ID3v1 and ID3v2 tags
* @see_also: #GstTagList
*
* Contains various utility functions for plugins to parse or create
* ID3 tags and map ID3v2 identifiers to and from GStreamer identifiers.
*
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gsttageditingprivate.h"
#include <stdlib.h>
#include <string.h>
#include "id3v2.h"
#ifndef GST_DISABLE_GST_DEBUG
#define GST_CAT_DEFAULT id3v2_ensure_debug_category()
#endif
static const gchar genres[] =
"Blues\000Classic Rock\000Country\000Dance\000Disco\000Funk\000Grunge\000"
"Hip-Hop\000Jazz\000Metal\000New Age\000Oldies\000Other\000Pop\000R&B\000"
"Rap\000Reggae\000Rock\000Techno\000Industrial\000Alternative\000Ska\000"
"Death Metal\000Pranks\000Soundtrack\000Euro-Techno\000Ambient\000Trip-Hop"
"\000Vocal\000Jazz+Funk\000Fusion\000Trance\000Classical\000Instrumental\000"
"Acid\000House\000Game\000Sound Clip\000Gospel\000Noise\000Alternative Rock"
"\000Bass\000Soul\000Punk\000Space\000Meditative\000Instrumental Pop\000"
"Instrumental Rock\000Ethnic\000Gothic\000Darkwave\000Techno-Industrial\000"
"Electronic\000Pop-Folk\000Eurodance\000Dream\000Southern Rock\000Comedy"
"\000Cult\000Gangsta\000Top 40\000Christian Rap\000Pop/Funk\000Jungle\000"
"Native American\000Cabaret\000New Wave\000Psychedelic\000Rave\000Showtunes"
"\000Trailer\000Lo-Fi\000Tribal\000Acid Punk\000Acid Jazz\000Polka\000"
"Retro\000Musical\000Rock & Roll\000Hard Rock\000Folk\000Folk/Rock\000"
"National Folk\000Swing\000Bebob\000Latin\000Revival\000Celtic\000Bluegrass"
"\000Avantgarde\000Gothic Rock\000Progressive Rock\000Psychedelic Rock\000"
"Symphonic Rock\000Slow Rock\000Big Band\000Chorus\000Easy Listening\000"
"Acoustic\000Humour\000Speech\000Chanson\000Opera\000Chamber Music\000"
"Sonata\000Symphony\000Booty Bass\000Primus\000Porn Groove\000Satire\000"
"Slow Jam\000Club\000Tango\000Samba\000Folklore\000Ballad\000Power Ballad\000"
"Rhythmic Soul\000Freestyle\000Duet\000Punk Rock\000Drum Solo\000A Capella"
"\000Euro-House\000Dance Hall\000Goa\000Drum & Bass\000Club-House\000"
"Hardcore\000Terror\000Indie\000BritPop\000Negerpunk\000Polsk Punk\000"
"Beat\000Christian Gangsta Rap\000Heavy Metal\000Black Metal\000"
"Crossover\000Contemporary Christian\000Christian Rock\000Merengue\000"
"Salsa\000Thrash Metal\000Anime\000Jpop\000Synthpop";
static const guint16 genres_idx[] = {
0, 6, 19, 27, 33, 39, 44, 51, 59, 64, 70, 78, 85, 91, 95, 99, 103, 110, 115,
122, 133, 145, 149, 161, 168, 179, 191, 199, 208, 214, 224, 231, 238, 248,
261, 266, 272, 277, 288, 295, 301, 318, 323, 328, 333, 339, 350, 367, 385,
392, 399, 408, 426, 437, 446, 456, 462, 476, 483, 488, 496, 503, 517, 526,
533, 549, 557, 566, 578, 583, 593, 601, 607, 614, 624, 634, 640, 646, 654,
666, 676, 681, 691, 705, 224, 711, 717, 723, 731, 738, 748, 759, 771, 788,
805, 820, 830, 839, 846, 861, 870, 877, 884, 892, 898, 912, 919, 928, 939,
946, 958, 965, 974, 979, 985, 991, 1000, 1007, 1020, 1034, 1044, 1049,
1059, 1069, 1079, 1090, 1101, 1105, 1117, 1128, 1137, 1144, 1150, 1158,
1168, 1179, 1184, 1206, 1218, 1230, 1240, 1263, 1278, 1287, 1293, 1306,
1312, 1317
};
static const GstTagEntryMatch tag_matches[] = {
{GST_TAG_TITLE, "TIT2"},
{GST_TAG_ALBUM, "TALB"},
{GST_TAG_TRACK_NUMBER, "TRCK"},
{GST_TAG_ARTIST, "TPE1"},
{GST_TAG_ALBUM_ARTIST, "TPE2"},
{GST_TAG_COMPOSER, "TCOM"},
{GST_TAG_CONDUCTOR, "TPE3"},
{GST_TAG_COPYRIGHT, "TCOP"},
{GST_TAG_COPYRIGHT_URI, "WCOP"},
{GST_TAG_ENCODED_BY, "TENC"},
{GST_TAG_GENRE, "TCON"},
{GST_TAG_DATE_TIME, "TDRC"},
{GST_TAG_COMMENT, "COMM"},
{GST_TAG_ALBUM_VOLUME_NUMBER, "TPOS"},
{GST_TAG_DURATION, "TLEN"},
{GST_TAG_ISRC, "TSRC"},
{GST_TAG_IMAGE, "APIC"},
{GST_TAG_ENCODER, "TSSE"},
{GST_TAG_BEATS_PER_MINUTE, "TBPM"},
{GST_TAG_ARTIST_SORTNAME, "TSOP"},
{GST_TAG_ALBUM_SORTNAME, "TSOA"},
{GST_TAG_TITLE_SORTNAME, "TSOT"},
{GST_TAG_PUBLISHER, "TPUB"},
{GST_TAG_INTERPRETED_BY, "TPE4"},
{GST_TAG_MUSICAL_KEY, "TKEY"},
{GST_TAG_PRIVATE_DATA, "PRIV"},
{NULL, NULL}
};
/**
* gst_tag_from_id3_tag:
* @id3_tag: ID3v2 tag to convert to GStreamer tag
*
* Looks up the GStreamer tag for a ID3v2 tag.
*
* Returns: The corresponding GStreamer tag or NULL if none exists.
*/
const gchar *
gst_tag_from_id3_tag (const gchar * id3_tag)
{
int i = 0;
g_return_val_if_fail (id3_tag != NULL, NULL);
while (tag_matches[i].gstreamer_tag != NULL) {
if (strncmp (id3_tag, tag_matches[i].original_tag, 5) == 0) {
return tag_matches[i].gstreamer_tag;
}
i++;
}
GST_FIXME ("Cannot map ID3v2 tag '%c%c%c%c' to GStreamer tag",
id3_tag[0], id3_tag[1], id3_tag[2], id3_tag[3]);
return NULL;
}
static const GstTagEntryMatch user_tag_matches[] = {
/* musicbrainz identifiers being used in the real world (foobar2000) */
{GST_TAG_MUSICBRAINZ_ARTISTID, "TXXX|musicbrainz_artistid"},
{GST_TAG_MUSICBRAINZ_ALBUMID, "TXXX|musicbrainz_albumid"},
{GST_TAG_MUSICBRAINZ_ALBUMARTISTID, "TXXX|musicbrainz_albumartistid"},
{GST_TAG_MUSICBRAINZ_TRMID, "TXXX|musicbrainz_trmid"},
{GST_TAG_CDDA_MUSICBRAINZ_DISCID, "TXXX|musicbrainz_discid"},
/* musicbrainz identifiers according to spec no one pays
* attention to (http://musicbrainz.org/docs/specs/metadata_tags.html) */
{GST_TAG_MUSICBRAINZ_ARTISTID, "TXXX|MusicBrainz Artist Id"},
{GST_TAG_MUSICBRAINZ_ALBUMID, "TXXX|MusicBrainz Album Id"},
{GST_TAG_MUSICBRAINZ_ALBUMARTISTID, "TXXX|MusicBrainz Album Artist Id"},
{GST_TAG_MUSICBRAINZ_TRMID, "TXXX|MusicBrainz TRM Id"},
/* according to: http://wiki.musicbrainz.org/MusicBrainzTag (yes, no space
* before 'ID' and not 'Id' either this time, yay for consistency) */
{GST_TAG_CDDA_MUSICBRAINZ_DISCID, "TXXX|MusicBrainz DiscID"},
/* foobar2000 uses these identifiers to store gain/peak information in
* ID3v2 tags <= v2.3.0. In v2.4.0 there's the RVA2 frame for that */
{GST_TAG_TRACK_GAIN, "TXXX|replaygain_track_gain"},
{GST_TAG_TRACK_PEAK, "TXXX|replaygain_track_peak"},
{GST_TAG_ALBUM_GAIN, "TXXX|replaygain_album_gain"},
{GST_TAG_ALBUM_PEAK, "TXXX|replaygain_album_peak"},
/* the following two are more or less made up, there seems to be little
* evidence that any popular application is actually putting this info
* into TXXX frames; the first one comes from a musicbrainz wiki 'proposed
* tags' page, the second one is analogue to the vorbis/ape/flac tag. */
{GST_TAG_CDDA_CDDB_DISCID, "TXXX|discid"},
{GST_TAG_CDDA_CDDB_DISCID, "TXXX|CDDB DiscID"}
};
/**
* gst_tag_from_id3_user_tag:
* @type: the type of ID3v2 user tag (e.g. "TXXX" or "UDIF")
* @id3_user_tag: ID3v2 user tag to convert to GStreamer tag
*
* Looks up the GStreamer tag for an ID3v2 user tag (e.g. description in
* TXXX frame or owner in UFID frame).
*
* Returns: The corresponding GStreamer tag or NULL if none exists.
*/
const gchar *
gst_tag_from_id3_user_tag (const gchar * type, const gchar * id3_user_tag)
{
int i = 0;
g_return_val_if_fail (type != NULL && strlen (type) == 4, NULL);
g_return_val_if_fail (id3_user_tag != NULL, NULL);
for (i = 0; i < G_N_ELEMENTS (user_tag_matches); ++i) {
if (strncmp (type, user_tag_matches[i].original_tag, 4) == 0 &&
g_ascii_strcasecmp (id3_user_tag,
user_tag_matches[i].original_tag + 5) == 0) {
GST_LOG ("Mapped ID3v2 user tag '%s' to GStreamer tag '%s'",
user_tag_matches[i].original_tag, user_tag_matches[i].gstreamer_tag);
return user_tag_matches[i].gstreamer_tag;
}
}
GST_FIXME ("Cannot map ID3v2 user tag '%s' of type '%s' to GStreamer tag",
id3_user_tag, type);
return NULL;
}
/**
* gst_tag_to_id3_tag:
* @gst_tag: GStreamer tag to convert to vorbiscomment tag
*
* Looks up the ID3v2 tag for a GStreamer tag.
*
* Returns: The corresponding ID3v2 tag or NULL if none exists.
*/
const gchar *
gst_tag_to_id3_tag (const gchar * gst_tag)
{
int i = 0;
g_return_val_if_fail (gst_tag != NULL, NULL);
while (tag_matches[i].gstreamer_tag != NULL) {
if (strcmp (gst_tag, tag_matches[i].gstreamer_tag) == 0) {
return tag_matches[i].original_tag;
}
i++;
}
return NULL;
}
static void
gst_tag_extract_id3v1_string (GstTagList * list, const gchar * tag,
const gchar * start, const guint size)
{
const gchar *env_vars[] = { "GST_ID3V1_TAG_ENCODING",
"GST_ID3_TAG_ENCODING", "GST_TAG_ENCODING", NULL
};
gchar *utf8;
utf8 = gst_tag_freeform_string_to_utf8 (start, size, env_vars);
if (utf8 && *utf8 != '\0') {
gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, tag, utf8, NULL);
}
g_free (utf8);
}
/**
* gst_tag_list_new_from_id3v1:
* @data: (array fixed-size=128): 128 bytes of data containing the ID3v1 tag
*
* Parses the data containing an ID3v1 tag and returns a #GstTagList from the
* parsed data.
*
* Returns: A new tag list or NULL if the data was not an ID3v1 tag.
*/
GstTagList *
gst_tag_list_new_from_id3v1 (const guint8 * data)
{
gint64 year;
gchar *ystr;
GstTagList *list;
g_return_val_if_fail (data != NULL, NULL);
if (data[0] != 'T' || data[1] != 'A' || data[2] != 'G')
return NULL;
list = gst_tag_list_new_empty ();
gst_tag_extract_id3v1_string (list, GST_TAG_TITLE, (gchar *) & data[3], 30);
gst_tag_extract_id3v1_string (list, GST_TAG_ARTIST, (gchar *) & data[33], 30);
gst_tag_extract_id3v1_string (list, GST_TAG_ALBUM, (gchar *) & data[63], 30);
ystr = g_strndup ((gchar *) & data[93], 4);
year = g_ascii_strtoll (ystr, NULL, 10);
g_free (ystr);
if (year > 0 && year <= 9999) {
GstDateTime *dt = gst_date_time_new_y (year);
gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, GST_TAG_DATE_TIME, dt, NULL);
gst_date_time_unref (dt);
}
if (data[125] == 0 && data[126] != 0) {
gst_tag_extract_id3v1_string (list, GST_TAG_COMMENT, (gchar *) & data[97],
28);
gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, GST_TAG_TRACK_NUMBER,
(guint) data[126], NULL);
} else {
gst_tag_extract_id3v1_string (list, GST_TAG_COMMENT, (gchar *) & data[97],
30);
}
if (data[127] < gst_tag_id3_genre_count () && !gst_tag_list_is_empty (list)) {
gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, GST_TAG_GENRE,
gst_tag_id3_genre_get (data[127]), NULL);
}
return list;
}
/**
* gst_tag_id3_genre_count:
*
* Gets the number of ID3v1 genres that can be identified. Winamp genres are
* included.
*
* Returns: the number of ID3v1 genres that can be identified
*/
guint
gst_tag_id3_genre_count (void)
{
return G_N_ELEMENTS (genres_idx);
}
/**
* gst_tag_id3_genre_get:
* @id: ID of genre to query
*
* Gets the ID3v1 genre name for a given ID.
*
* Returns: the genre or NULL if no genre is associated with that ID.
*/
const gchar *
gst_tag_id3_genre_get (const guint id)
{
guint idx;
if (id >= G_N_ELEMENTS (genres_idx))
return NULL;
idx = genres_idx[id];
g_assert (idx < sizeof (genres));
return &genres[idx];
}
/**
* gst_tag_list_add_id3_image:
* @tag_list: a tag list
* @image_data: (array length=image_data_len): the (encoded) image
* @image_data_len: the length of the encoded image data at @image_data
* @id3_picture_type: picture type as per the ID3 (v2.4.0) specification for
* the APIC frame (0 = unknown/other)
*
* Adds an image from an ID3 APIC frame (or similar, such as used in FLAC)
* to the given tag list. Also see gst_tag_image_data_to_image_sample() for
* more information on image tags in GStreamer.
*
* Returns: %TRUE if the image was processed, otherwise %FALSE
*/
gboolean
gst_tag_list_add_id3_image (GstTagList * tag_list, const guint8 * image_data,
guint image_data_len, guint id3_picture_type)
{
GstTagImageType tag_image_type;
const gchar *tag_name;
GstSample *image;
g_return_val_if_fail (GST_IS_TAG_LIST (tag_list), FALSE);
g_return_val_if_fail (image_data != NULL, FALSE);
g_return_val_if_fail (image_data_len > 0, FALSE);
if (id3_picture_type == 0x01 || id3_picture_type == 0x02) {
/* file icon for preview. Don't add image-type to caps, since there
* is only supposed to be one of these, and the type is already indicated
* via the special tag */
tag_name = GST_TAG_PREVIEW_IMAGE;
tag_image_type = GST_TAG_IMAGE_TYPE_NONE;
} else {
tag_name = GST_TAG_IMAGE;
/* Remap the ID3v2 APIC type our ImageType enum */
if (id3_picture_type >= 0x3 && id3_picture_type <= 0x14)
tag_image_type = (GstTagImageType) (id3_picture_type - 2);
else
tag_image_type = GST_TAG_IMAGE_TYPE_UNDEFINED;
}
image = gst_tag_image_data_to_image_sample (image_data, image_data_len,
tag_image_type);
if (image == NULL)
return FALSE;
gst_tag_list_add (tag_list, GST_TAG_MERGE_APPEND, tag_name, image, NULL);
gst_sample_unref (image);
return TRUE;
}