[FFmpeg-cvslog] id3v2: add support for non-text and GEOB type tag frames

David Goldwich git at videolan.org
Wed Sep 21 21:34:03 CEST 2011


ffmpeg | branch: master | David Goldwich <david.goldwich at gmail.com> | Sun Sep 18 18:04:34 2011 +0200| [7a019dffe568e4d58cf1b8b2216f8678e45066b9] | committer: Anton Khirnov

id3v2: add support for non-text and GEOB type tag frames

This extends the ID3v2 parser to allow for reading of non-text (i.e.
other than T***) meta tag frames providing a ff_id3v2_read_all()
function. An additional data structure 'ID3v2ExtraMeta' is introduced
for these tags since AVDictionary is string oriented and unsuitable
for binary data.

A parser for tag frames of type GEOB is implemented, which is needed
to extract keyring information from encrypted OMA files. GEOB data
is parsed into 'ID3v2ExtraMetaGEOB' data structures.

The routine to decode characters from different encodings to UTF-8,
formerly part of the read_ttag() function, is moved to its own
function. Because some tag frames contain subparts of unknown length,
the function is now also able to read until a null character is found.
In addition, the function now takes care of allocating a buffer long
enough to hold the decoded characters.

Signed-off-by: David Goldwich <david.goldwich at gmail.com>
Signed-off-by: Anton Khirnov <anton at khirnov.net>

> http://git.videolan.org/gitweb.cgi/ffmpeg.git/?a=commit;h=7a019dffe568e4d58cf1b8b2216f8678e45066b9
---

 libavformat/id3v2.c |  270 ++++++++++++++++++++++++++++++++++++++++++++-------
 libavformat/id3v2.h |   38 +++++++-
 2 files changed, 270 insertions(+), 38 deletions(-)

diff --git a/libavformat/id3v2.c b/libavformat/id3v2.c
index be6c03b..68c1709 100644
--- a/libavformat/id3v2.c
+++ b/libavformat/id3v2.c
@@ -59,63 +59,129 @@ static unsigned int get_size(AVIOContext *s, int len)
     return v;
 }
 
-static void read_ttag(AVFormatContext *s, AVIOContext *pb, int taglen, const char *key)
+/**
+ * Free GEOB type extra metadata.
+ */
+static void free_geobtag(ID3v2ExtraMetaGEOB *geob)
 {
-    char *q, dst[512];
-    const char *val = NULL;
-    int len, dstlen = sizeof(dst) - 1;
-    unsigned genre;
-    unsigned int (*get)(AVIOContext*) = avio_rb16;
+    av_free(geob->mime_type);
+    av_free(geob->file_name);
+    av_free(geob->description);
+    av_free(geob->data);
+    av_free(geob);
+}
 
-    dst[0] = 0;
-    if (taglen < 1)
-        return;
+/**
+ * Decode characters to UTF-8 according to encoding type. The decoded buffer is
+ * always null terminated.
+ *
+ * @param dst Pointer where the address of the buffer with the decoded bytes is
+ * stored. Buffer must be freed by caller.
+ * @param dstlen Pointer to an int where the length of the decoded string
+ * is stored (in bytes, incl. null termination)
+ * @param maxread Pointer to maximum number of characters to read from the
+ * AVIOContext. After execution the value is decremented by the number of bytes
+ * actually read.
+ * @seeknull If true, decoding stops after the first U+0000 character found, if
+ * there is any before maxread is reached
+ * @returns 0 if no error occured, dst is uninitialized on error
+ */
+static int decode_str(AVFormatContext *s, AVIOContext *pb, int encoding,
+                      uint8_t **dst, int *dstlen, int *maxread, const int seeknull)
+{
+    int len, ret;
+    uint8_t tmp;
+    uint32_t ch = 1;
+    int left = *maxread;
+    unsigned int (*get)(AVIOContext*) = avio_rb16;
+    AVIOContext *dynbuf;
 
-    taglen--; /* account for encoding type byte */
+    if ((ret = avio_open_dyn_buf(&dynbuf)) < 0) {
+        av_log(s, AV_LOG_ERROR, "Error opening memory stream\n");
+        return ret;
+    }
 
-    switch (avio_r8(pb)) { /* encoding type */
+    switch (encoding) {
 
     case ID3v2_ENCODING_ISO8859:
-        q = dst;
-        while (taglen-- && q - dst < dstlen - 7) {
-            uint8_t tmp;
-            PUT_UTF8(avio_r8(pb), tmp, *q++ = tmp;)
+        while (left && (!seeknull || ch)) {
+            ch = avio_r8(pb);
+            PUT_UTF8(ch, tmp, avio_w8(dynbuf, tmp);)
+            left--;
         }
-        *q = 0;
         break;
 
     case ID3v2_ENCODING_UTF16BOM:
-        taglen -= 2;
+        if ((left -= 2) < 0) {
+            av_log(s, AV_LOG_ERROR, "Cannot read BOM value, input too short\n");
+            avio_close_dyn_buf(dynbuf, (uint8_t **)dst);
+            av_freep(dst);
+            return AVERROR_INVALIDDATA;
+        }
         switch (avio_rb16(pb)) {
         case 0xfffe:
             get = avio_rl16;
         case 0xfeff:
             break;
         default:
-            av_log(s, AV_LOG_ERROR, "Incorrect BOM value in tag %s.\n", key);
-            return;
+            av_log(s, AV_LOG_ERROR, "Incorrect BOM value\n");
+            avio_close_dyn_buf(dynbuf, (uint8_t **)dst);
+            av_freep(dst);
+            *maxread = left;
+            return AVERROR_INVALIDDATA;
         }
         // fall-through
 
     case ID3v2_ENCODING_UTF16BE:
-        q = dst;
-        while (taglen > 1 && q - dst < dstlen - 7) {
-            uint32_t ch;
-            uint8_t tmp;
-
-            GET_UTF16(ch, ((taglen -= 2) >= 0 ? get(pb) : 0), break;)
-            PUT_UTF8(ch, tmp, *q++ = tmp;)
+        while ((left > 1) && (!seeknull || ch)) {
+            GET_UTF16(ch, ((left -= 2) >= 0 ? get(pb) : 0), break;)
+            PUT_UTF8(ch, tmp, avio_w8(dynbuf, tmp);)
         }
-        *q = 0;
+        if (left < 0)
+            left += 2; /* did not read last char from pb */
         break;
 
     case ID3v2_ENCODING_UTF8:
-        len = FFMIN(taglen, dstlen);
-        avio_read(pb, dst, len);
-        dst[len] = 0;
+        while (left && (!seeknull || ch)) {
+            ch = avio_r8(pb);
+            avio_w8(dynbuf, ch);
+            left--;
+        }
         break;
     default:
-        av_log(s, AV_LOG_WARNING, "Unknown encoding in tag %s.\n", key);
+        av_log(s, AV_LOG_WARNING, "Unknown encoding\n");
+    }
+
+    if (ch)
+        avio_w8(dynbuf, 0);
+
+    len = avio_close_dyn_buf(dynbuf, (uint8_t **)dst);
+    if (dstlen)
+        *dstlen = len;
+
+    *maxread = left;
+
+    return 0;
+}
+
+/**
+ * Parse a text tag.
+ */
+static void read_ttag(AVFormatContext *s, AVIOContext *pb, int taglen, const char *key)
+{
+    uint8_t *dst;
+    const char *val = NULL;
+    int len, dstlen;
+    unsigned genre;
+
+    if (taglen < 1)
+        return;
+
+    taglen--; /* account for encoding type byte */
+
+    if (decode_str(s, pb, avio_r8(pb), &dst, &dstlen, &taglen, 0) < 0) {
+        av_log(s, AV_LOG_ERROR, "Error reading frame %s, skipped\n", key);
+        return;
     }
 
     if (!(strcmp(key, "TCON") && strcmp(key, "TCO"))
@@ -134,6 +200,82 @@ static void read_ttag(AVFormatContext *s, AVIOContext *pb, int taglen, const cha
 
     if (val)
         av_dict_set(&s->metadata, key, val, AV_DICT_DONT_OVERWRITE);
+
+    av_free(dst);
+}
+
+/**
+ * Parse GEOB tag into a ID3v2ExtraMetaGEOB struct.
+ */
+static void read_geobtag(AVFormatContext *s, AVIOContext *pb, int taglen, char *tag, ID3v2ExtraMeta **extra_meta)
+{
+    ID3v2ExtraMetaGEOB *geob_data = NULL;
+    ID3v2ExtraMeta *new_extra = NULL;
+    char encoding;
+    unsigned int len;
+
+    if (taglen < 1)
+        return;
+
+    geob_data = av_mallocz(sizeof(ID3v2ExtraMetaGEOB));
+    if (!geob_data) {
+        av_log(s, AV_LOG_ERROR, "Failed to alloc %zu bytes\n", sizeof(ID3v2ExtraMetaGEOB));
+        return;
+    }
+
+    new_extra = av_mallocz(sizeof(ID3v2ExtraMeta));
+    if (!new_extra) {
+        av_log(s, AV_LOG_ERROR, "Failed to alloc %zu bytes\n", sizeof(ID3v2ExtraMeta));
+        goto fail;
+    }
+
+    /* read encoding type byte */
+    encoding = avio_r8(pb);
+    taglen--;
+
+    /* read MIME type (always ISO-8859) */
+    if (decode_str(s, pb, ID3v2_ENCODING_ISO8859, &geob_data->mime_type, NULL, &taglen, 1) < 0
+        || taglen <= 0)
+        goto fail;
+
+    /* read file name */
+    if (decode_str(s, pb, encoding, &geob_data->file_name, NULL, &taglen, 1) < 0
+        || taglen <= 0)
+        goto fail;
+
+    /* read content description */
+    if (decode_str(s, pb, encoding, &geob_data->description, NULL, &taglen, 1) < 0
+        || taglen < 0)
+        goto fail;
+
+    if (taglen) {
+        /* save encapsulated binary data */
+        geob_data->data = av_malloc(taglen);
+        if (!geob_data->data) {
+            av_log(s, AV_LOG_ERROR, "Failed to alloc %d bytes\n", taglen);
+            goto fail;
+        }
+        if ((len = avio_read(pb, geob_data->data, taglen)) < taglen)
+            av_log(s, AV_LOG_WARNING, "Error reading GEOB frame, data truncated.\n");
+        geob_data->datasize = len;
+    } else {
+        geob_data->data = NULL;
+        geob_data->datasize = 0;
+    }
+
+    /* add data to the list */
+    new_extra->tag = "GEOB";
+    new_extra->data = geob_data;
+    new_extra->next = *extra_meta;
+    *extra_meta = new_extra;
+
+    return;
+
+fail:
+    av_log(s, AV_LOG_ERROR, "Error reading frame %s, skipped\n", tag);
+    free_geobtag(geob_data);
+    av_free(new_extra);
+    return;
 }
 
 static int is_number(const char *str)
@@ -182,7 +324,27 @@ finish:
         av_dict_set(m, "date", date, 0);
 }
 
-static void ff_id3v2_parse(AVFormatContext *s, int len, uint8_t version, uint8_t flags)
+/**
+ * Get the corresponding ID3v2EMFunc struct for a tag.
+ * @param isv34 Determines if v2.2 or v2.3/4 strings are used
+ * @return A pointer to the ID3v2EMFunc struct if found, NULL otherwise.
+ */
+static const ID3v2EMFunc *get_extra_meta_func(const char *tag, int isv34)
+{
+    int i = 0;
+    while (ff_id3v2_extra_meta_funcs[i].tag3) {
+        if (!memcmp(tag,
+                    (isv34 ?
+                        ff_id3v2_extra_meta_funcs[i].tag4 :
+                        ff_id3v2_extra_meta_funcs[i].tag3),
+                    (isv34 ? 4 : 3)))
+            return &ff_id3v2_extra_meta_funcs[i];
+        i++;
+    }
+    return NULL;
+}
+
+static void ff_id3v2_parse(AVFormatContext *s, int len, uint8_t version, uint8_t flags, ID3v2ExtraMeta **extra_meta)
 {
     int isv34, tlen, unsync;
     char tag[5];
@@ -190,8 +352,10 @@ static void ff_id3v2_parse(AVFormatContext *s, int len, uint8_t version, uint8_t
     int taghdrlen;
     const char *reason = NULL;
     AVIOContext pb;
+    AVIOContext *pbx;
     unsigned char *buffer = NULL;
     int buffer_size = 0;
+    void (*extra_func)(AVFormatContext*, AVIOContext*, int, char*, ID3v2ExtraMeta**) = NULL;
 
     switch (version) {
     case 2:
@@ -252,7 +416,8 @@ static void ff_id3v2_parse(AVFormatContext *s, int len, uint8_t version, uint8_t
         if (tflags & (ID3v2_FLAG_ENCRYPTION | ID3v2_FLAG_COMPRESSION)) {
             av_log(s, AV_LOG_WARNING, "Skipping encrypted/compressed ID3v2 frame %s.\n", tag);
             avio_skip(s->pb, tlen);
-        } else if (tag[0] == 'T') {
+        /* check for text tag or supported special meta tag */
+        } else if (tag[0] == 'T' || (extra_meta && (extra_func = get_extra_meta_func(tag, isv34)->read))) {
             if (unsync || tunsync) {
                 int i, j;
                 av_fast_malloc(&buffer, &buffer_size, tlen);
@@ -268,10 +433,17 @@ static void ff_id3v2_parse(AVFormatContext *s, int len, uint8_t version, uint8_t
                     }
                 }
                 ffio_init_context(&pb, buffer, j, 0, NULL, NULL, NULL, NULL);
-                read_ttag(s, &pb, j, tag);
+                tlen = j;
+                pbx = &pb; // read from sync buffer
             } else {
-                read_ttag(s, s->pb, tlen, tag);
+                pbx = s->pb; // read straight from input
             }
+            if (tag[0] == 'T')
+                /* parse text tag */
+                read_ttag(s, pbx, tlen, tag);
+            else
+                /* parse special meta tag */
+                extra_func(s, pbx, tlen, tag, extra_meta);
         }
         else if (!tag[0]) {
             if (tag[1])
@@ -295,7 +467,7 @@ seek:
     return;
 }
 
-void ff_id3v2_read(AVFormatContext *s, const char *magic)
+void ff_id3v2_read_all(AVFormatContext *s, const char *magic, ID3v2ExtraMeta **extra_meta)
 {
     int len, ret;
     uint8_t buf[ID3v2_HEADER_SIZE];
@@ -315,7 +487,7 @@ void ff_id3v2_read(AVFormatContext *s, const char *magic)
                   ((buf[7] & 0x7f) << 14) |
                   ((buf[8] & 0x7f) << 7) |
                    (buf[9] & 0x7f);
-            ff_id3v2_parse(s, len, buf[3], buf[5]);
+            ff_id3v2_parse(s, len, buf[3], buf[5], extra_meta);
         } else {
             avio_seek(s->pb, off, SEEK_SET);
         }
@@ -326,6 +498,30 @@ void ff_id3v2_read(AVFormatContext *s, const char *magic)
     merge_date(&s->metadata);
 }
 
+void ff_id3v2_read(AVFormatContext *s, const char *magic)
+{
+    ff_id3v2_read_all(s, magic, NULL);
+}
+
+void ff_id3v2_free_extra_meta(ID3v2ExtraMeta **extra_meta)
+{
+    ID3v2ExtraMeta *current = *extra_meta, *next;
+    void (*free_func)(ID3v2ExtraMeta*);
+
+    while (current) {
+        if ((free_func = get_extra_meta_func(current->tag, 1)->free))
+            free_func(current->data);
+        next = current->next;
+        av_freep(&current);
+        current = next;
+    }
+}
+
+const ID3v2EMFunc ff_id3v2_extra_meta_funcs[] = {
+    { "GEO", "GEOB", read_geobtag, free_geobtag },
+    { NULL }
+};
+
 const AVMetadataConv ff_id3v2_34_metadata_conv[] = {
     { "TALB", "album"},
     { "TCOM", "composer"},
diff --git a/libavformat/id3v2.h b/libavformat/id3v2.h
index 3e0e65a..13dec39 100644
--- a/libavformat/id3v2.h
+++ b/libavformat/id3v2.h
@@ -45,6 +45,27 @@ enum ID3v2Encoding {
     ID3v2_ENCODING_UTF8     = 3,
 };
 
+typedef struct ID3v2ExtraMeta {
+    const char *tag;
+    void *data;
+    struct ID3v2ExtraMeta *next;
+} ID3v2ExtraMeta;
+
+typedef struct ID3v2ExtraMetaGEOB {
+    uint32_t datasize;
+    uint8_t *mime_type;
+    uint8_t *file_name;
+    uint8_t *description;
+    uint8_t *data;
+} ID3v2ExtraMetaGEOB;
+
+typedef struct ID3v2EMFunc {
+    const char *tag3;
+    const char *tag4;
+    void (*read)(AVFormatContext*, AVIOContext*, int, char*, ID3v2ExtraMeta **);
+    void (*free)();
+} ID3v2EMFunc;
+
 /**
  * Detect ID3v2 Header.
  * @param buf   must be ID3v2_HEADER_SIZE byte long
@@ -61,10 +82,25 @@ int ff_id3v2_match(const uint8_t *buf, const char *magic);
 int ff_id3v2_tag_len(const uint8_t *buf);
 
 /**
- * Read an ID3v2 tag
+ * Read an ID3v2 tag (text tags only)
  */
 void ff_id3v2_read(AVFormatContext *s, const char *magic);
 
+/**
+ * Read an ID3v2 tag, including supported extra metadata (currently only GEOB)
+ * @param extra_meta If not NULL, extra metadata is parsed into a list of
+ * ID3v2ExtraMeta structs and *extra_meta points to the head of the list
+ */
+void ff_id3v2_read_all(AVFormatContext *s, const char *magic, ID3v2ExtraMeta **extra_meta);
+
+/**
+ * Free memory allocated parsing special (non-text) metadata.
+ * @param extra_meta Pointer to a pointer to the head of a ID3v2ExtraMeta list, *extra_meta is set to NULL.
+ */
+void ff_id3v2_free_extra_meta(ID3v2ExtraMeta **extra_meta);
+
+extern const ID3v2EMFunc ff_id3v2_extra_meta_funcs[];
+
 extern const AVMetadataConv ff_id3v2_34_metadata_conv[];
 extern const AVMetadataConv ff_id3v2_4_metadata_conv[];
 extern const AVMetadataConv ff_id3v2_2_metadata_conv[];



More information about the ffmpeg-cvslog mailing list