afh: Unify name of init functions.
[paraslash.git] / wma_afh.c
index 65795fd..ebb2f9c 100644 (file)
--- a/wma_afh.c
+++ b/wma_afh.c
@@ -1,20 +1,14 @@
 /*
- * Copyright (C) 2009-2014 Andre Noll <maan@systemlinux.org>
+ * Copyright (C) 2009 Andre Noll <maan@tuebingen.mpg.de>
  *
  * Licensed under the GPL v2. For licencing details see COPYING.
  */
 
 /** \file wma_afh.c The audio format handler for WMA files. */
 
-#include <stdlib.h>
-#include <inttypes.h>
 #include <sys/types.h>
-#include <sys/stat.h>
-#include <fcntl.h>
-#include <stdio.h>
-#include <unistd.h>
-#include <string.h>
 #include <regex.h>
+#include <iconv.h>
 
 #include "para.h"
 #include "error.h"
 #include "portable_io.h"
 #include "string.h"
 #include "wma.h"
+#include "fd.h"
 
-#define FOR_EACH_FRAME(_f, _buf, _size, _ba) for (_f = (_buf); \
-       _f + (_ba) + WMA_FRAME_SKIP < (_buf) + (_size); \
-       _f += (_ba) + WMA_FRAME_SKIP)
+#define FOR_EACH_FRAME(_f, _buf, _size, _ps) for (_f = (_buf); \
+       _f + (_ps) < (_buf) + (_size); \
+       _f += (_ps))
 
 /*
  * Must be called on a frame boundary, e.g. start + header_len.
  * \return Frame count, superframe count via *num_superframes.
  */
-static int count_frames(const char *buf, int buf_size, int block_align,
+static int count_frames(const char *buf, int buf_size, uint32_t packet_size,
        int *num_superframes)
 {
        int fc = 0, sfc = 0; /* frame count, superframe count */
        const uint8_t *p;
 
 
-       FOR_EACH_FRAME(p, (uint8_t *)buf, buf_size, block_align) {
+       FOR_EACH_FRAME(p, (uint8_t *)buf, buf_size, packet_size) {
                fc += p[WMA_FRAME_SKIP] & 0x0f;
                sfc++;
        }
        PARA_INFO_LOG("%d frames, %d superframes\n", fc, sfc);
-       if (num_superframes)
-               *num_superframes = sfc;
+       *num_superframes = sfc;
        return fc;
 }
 
@@ -73,7 +67,7 @@ static int put_utf8(uint32_t val, char *result)
                *out++ = in;
                return 1;
        }
-       bytes = (wma_log2(in) + 4) / 5;
+       bytes = DIV_ROUND_UP(wma_log2(in), 5);
        shift = (bytes - 1) * 6;
        *out++ = (256 - (256 >> bytes)) | (in >> shift);
        while (shift >= 6) {
@@ -107,7 +101,7 @@ static char *get_str16(const char *in, int len)
        return out;
 }
 
-static const char comment_header[] = {
+static const char content_description_header[] = {
        0x33, 0x26, 0xb2, 0x75, 0x8E, 0x66, 0xCF, 0x11,
        0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c
 };
@@ -134,10 +128,10 @@ static void read_asf_tags(const char *buf, int buf_size, struct taginfo *ti)
        const char *p, *end = buf + buf_size, *q;
        uint16_t len1, len2, len3, len4;
 
-       p = search_pattern(comment_header, sizeof(comment_header),
-               buf, buf_size);
+       p = search_pattern(content_description_header,
+               sizeof(content_description_header), buf, buf_size);
        if (!p || p + 34 >= end) {
-               PARA_NOTICE_LOG("comment header not found\n");
+               PARA_NOTICE_LOG("content description header not found\n");
                goto next;
        }
        p += 24;
@@ -197,7 +191,7 @@ static void set_chunk_tv(int frames_per_chunk, int frequency,
 }
 
 /* Must be called on a frame boundary. */
-static int wma_make_chunk_table(char *buf, size_t buf_size, int block_align,
+static int wma_make_chunk_table(char *buf, size_t buf_size, uint32_t packet_size,
                struct afh_info *afhi)
 {
        const uint8_t *f, *start = (uint8_t *)buf;
@@ -209,7 +203,7 @@ static int wma_make_chunk_table(char *buf, size_t buf_size, int block_align,
        afhi->chunk_table[0] = 0;
        afhi->chunk_table[1] = afhi->header_len;
 
-       num_frames = count_frames(buf, buf_size, block_align,
+       num_frames = count_frames(buf, buf_size, packet_size,
                &num_superframes);
        ret = -E_NO_WMA;
        if (num_frames == 0 || num_superframes == 0)
@@ -219,7 +213,7 @@ static int wma_make_chunk_table(char *buf, size_t buf_size, int block_align,
        frames_per_chunk = num_frames / num_superframes / 2;
        PARA_INFO_LOG("%d frames per chunk\n", frames_per_chunk);
        j = 1;
-       FOR_EACH_FRAME(f, start, buf_size, block_align) {
+       FOR_EACH_FRAME(f, start, buf_size, packet_size) {
                count += f[WMA_FRAME_SKIP] & 0x0f;
                while (count > j * frames_per_chunk) {
                        j++;
@@ -229,10 +223,12 @@ static int wma_make_chunk_table(char *buf, size_t buf_size, int block_align,
                                        afhi->chunk_table,
                                        ct_size * sizeof(uint32_t));
                        }
-                       afhi->chunk_table[j] = f - start + afhi->header_len + block_align + WMA_FRAME_SKIP;
+                       afhi->chunk_table[j] = f - start + afhi->header_len
+                               + packet_size;
                }
        }
        afhi->chunks_total = j;
+       set_max_chunk_size(afhi);
        set_chunk_tv(frames_per_chunk, afhi->frequency, &afhi->chunk_tv);
        return 1;
 fail:
@@ -267,12 +263,386 @@ static int wma_get_file_info(char *map, size_t numbytes, __a_unused int fd,
                ahi.use_variable_block_len? "vbl" : ""
        );
        wma_make_chunk_table(map + ahi.header_len, numbytes - ahi.header_len,
-               ahi.block_align, afhi);
+               ahi.packet_size, afhi);
        read_asf_tags(map, ahi.header_len, &afhi->tags);
        return 0;
 }
 
-static const char* wma_suffixes[] = {"wma", NULL};
+struct asf_object {
+       char *ptr;
+       uint64_t size;
+};
+
+struct tag_object_nums {
+       int content_descr_obj_num;
+       int extended_content_descr_obj_num;
+};
+
+struct afs_top_level_header_object {
+       uint64_t size;
+       uint32_t num_objects;
+       uint8_t reserved1, reserved2;
+       struct asf_object *objects;
+};
+
+#define CHECK_HEADER(_p, _h) (memcmp((_p), (_h), sizeof((_h))) == 0)
+
+static int read_asf_objects(const char *src, size_t size, uint32_t num_objects,
+               struct asf_object *objs, struct tag_object_nums *ton)
+{
+       int i;
+       const char *p;
+
+       for (i = 0, p = src; i < num_objects; p += objs[i++].size) {
+               if (p + 24 > src + size)
+                       return -E_NO_WMA;
+               objs[i].ptr = (char *)p;
+               objs[i].size = read_u64(p + 16);
+               if (p + objs[i].size > src + size)
+                       return -E_NO_WMA;
+
+               if (CHECK_HEADER(p, content_description_header))
+                       ton->content_descr_obj_num = i;
+               else if (CHECK_HEADER(p, extended_content_header))
+                       ton->extended_content_descr_obj_num = i;
+       }
+       return 1;
+}
+
+static const char top_level_header_object_guid[] = {
+       0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11,
+       0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c
+};
+
+static int convert_utf8_to_utf16(char *src, char **dst)
+{
+       iconv_t cd;
+       size_t sz, inbytes, outbytes, inbytesleft, outbytesleft;
+       char *inbuf, *outbuf;
+       int ret;
+
+       if (!src || !*src) {
+               *dst = para_calloc(2);
+               return 0;
+       }
+       /*
+        * Without specifying LE (little endian), iconv includes a byte order
+        * mark (e.g. 0xFFFE) at the beginning.
+        */
+       cd = iconv_open("UTF-16LE", "UTF-8");
+       if (cd == (iconv_t)-1) {
+               *dst = NULL;
+               return -ERRNO_TO_PARA_ERROR(errno);
+       }
+       inbuf = src;
+       /* even though src is in UTF-8, strlen() should DTRT */
+       inbytes = inbytesleft = strlen(src);
+       outbytes = outbytesleft = 4 * inbytes + 2; /* hope that's enough */
+       *dst = outbuf = para_malloc(outbytes);
+       sz = iconv(cd, ICONV_CAST &inbuf, &inbytesleft, &outbuf, &outbytesleft);
+       if (sz == (size_t)-1) {
+               ret = -ERRNO_TO_PARA_ERROR(errno);
+               free(*dst);
+               *dst = NULL;
+               goto out;
+       }
+       assert(outbytes >= outbytesleft);
+       assert(outbytes - outbytesleft < INT_MAX - 2);
+       ret = outbytes - outbytesleft;
+       outbuf = para_realloc(*dst, ret + 2);
+       outbuf[ret] = outbuf[ret + 1] = '\0';
+       ret += 2;
+       *dst = outbuf;
+       PARA_INFO_LOG("converted %s to %d UTF-16 bytes\n", src, ret);
+out:
+       if (iconv_close(cd) < 0)
+               PARA_WARNING_LOG("iconv_close: %s\n", strerror(errno));
+       return ret;
+}
+
+/* The content description object contains artist, title, comment. */
+static int make_cdo(struct taginfo *tags, const struct asf_object *cdo,
+               struct asf_object *result)
+{
+       const char *cr, *rating; /* orig data */
+       uint16_t orig_cr_bytes, orig_rating_bytes;
+       /* pointers to new UTF-16 tags */
+       char *artist = NULL, *title = NULL, *comment = NULL;
+       /* number of bytes in UTF-16 for the new tags */
+       int artist_bytes, title_bytes, comment_bytes, ret;
+       char *p, null[2] = "\0\0";
+
+       result->ptr = NULL;
+       result->size = 0;
+       ret = convert_utf8_to_utf16(tags->artist, &artist);
+       if (ret < 0)
+               return ret;
+       artist_bytes = ret;
+       ret = convert_utf8_to_utf16(tags->title, &title);
+       if (ret < 0)
+               goto out;
+       title_bytes = ret;
+       ret = convert_utf8_to_utf16(tags->comment, &comment);
+       if (ret < 0)
+               goto out;
+       comment_bytes = ret;
+
+       if (cdo) {
+               uint16_t orig_title_bytes, orig_artist_bytes, orig_comment_bytes;
+               /*
+                * Sizes of the five fields (stored as 16-bit numbers) are
+                * located after the header (16 bytes) and the cdo size (8
+                * bytes).
+                */
+               orig_title_bytes = read_u16(cdo->ptr + 24);
+               orig_artist_bytes = read_u16(cdo->ptr + 26);
+               orig_cr_bytes = read_u16(cdo->ptr + 28);
+               orig_comment_bytes = read_u16(cdo->ptr + 30);
+               orig_rating_bytes = read_u16(cdo->ptr + 32);
+               cr = cdo->ptr + 34 + orig_title_bytes + orig_artist_bytes;
+               rating = cr + orig_cr_bytes + orig_comment_bytes;
+       } else {
+               orig_cr_bytes = 2;
+               orig_rating_bytes = 2;
+               cr = null;
+               rating = null;
+       }
+
+       /* compute size of result cdo */
+       result->size = 16 + 8 + 5 * 2 + title_bytes + artist_bytes
+               + orig_cr_bytes + comment_bytes + orig_rating_bytes;
+       PARA_DEBUG_LOG("cdo is %zu bytes\n", (size_t)result->size);
+       p = result->ptr = para_malloc(result->size);
+       memcpy(p, content_description_header, 16);
+       p += 16;
+       write_u64(p, result->size);
+       p += 8;
+       write_u16(p, title_bytes);
+       p += 2;
+       write_u16(p, artist_bytes);
+       p += 2;
+       write_u16(p, orig_cr_bytes);
+       p += 2;
+       write_u16(p, comment_bytes);
+       p += 2;
+       write_u16(p, orig_rating_bytes);
+       p += 2;
+       memcpy(p, title, title_bytes);
+       p += title_bytes;
+       memcpy(p, artist, artist_bytes);
+       p += artist_bytes;
+       memcpy(p, cr, orig_cr_bytes);
+       p += orig_cr_bytes;
+       memcpy(p, comment, comment_bytes);
+       p += comment_bytes;
+       memcpy(p, rating, orig_rating_bytes);
+       p += orig_rating_bytes;
+       assert(p - result->ptr == result->size);
+       ret = 1;
+out:
+       free(artist);
+       free(title);
+       free(comment);
+       return ret;
+}
+
+/* The extended content description object contains album and year. */
+static int make_ecdo(struct taginfo *tags, struct asf_object *result)
+{
+       int ret;
+       char *p, *album = NULL, *year = NULL, null[2] = "\0\0";
+       int album_bytes, year_bytes;
+
+       result->ptr = NULL;
+       result->size = 0;
+       ret = convert_utf8_to_utf16(tags->album, &album);
+       if (ret < 0)
+               return ret;
+       album_bytes = ret;
+       ret = convert_utf8_to_utf16(tags->year, &year);
+       if (ret < 0)
+               goto out;
+       year_bytes = ret;
+       result->size = 16 + 8 + 2; /* GUID, size, count */
+       /* name_length + name + null + data type + val length + val */
+       result->size += 2 + sizeof(album_tag_header) + 2 + 2 + 2 + album_bytes;
+       result->size += 2 + sizeof(year_tag_header) + 2 + 2 + 2 + year_bytes;
+
+       p = result->ptr = para_malloc(result->size);
+       memcpy(p, extended_content_header, 16);
+       p += 16;
+       write_u64(p, result->size);
+       p += 8;
+       write_u16(p, 2); /* count */
+       p += 2;
+
+       /* album */
+       write_u16(p, sizeof(album_tag_header) + 2);
+       p += 2;
+       memcpy(p, album_tag_header, sizeof(album_tag_header));
+       p += sizeof(album_tag_header);
+       memcpy(p, null, 2);
+       p += 2;
+       write_u16(p, 0); /* data type (UTF-16) */
+       p += 2;
+       write_u16(p, album_bytes);
+       p += 2;
+       memcpy(p, album, album_bytes);
+       p += album_bytes;
+
+       /* year */
+       write_u16(p, sizeof(year_tag_header));
+       p += 2;
+       memcpy(p, year_tag_header, sizeof(year_tag_header));
+       p += sizeof(year_tag_header);
+       memcpy(p, null, 2);
+       p += 2;
+       write_u16(p, 0); /* data type (UTF-16) */
+       p += 2;
+       write_u16(p, year_bytes);
+       p += 2;
+       memcpy(p, year, year_bytes);
+       p += year_bytes;
+       assert(p - result->ptr == result->size);
+       ret = 1;
+out:
+       free(album);
+       free(year);
+       return ret;
+}
+
+static int write_output_file(int fd, const char *map, size_t mapsize,
+               struct afs_top_level_header_object *top, struct tag_object_nums *ton,
+               struct asf_object *cdo, struct asf_object *ecdo)
+{
+       int i, ret;
+       uint64_t sz; /* of the new header object */
+       uint32_t num_objects;
+       char tmp[8];
+
+       sz = 16 + 8 + 4 + 1 + 1; /* top-level header object */
+       for (i = 0; i < top->num_objects; i++) {
+               if (i == ton->content_descr_obj_num)
+                       continue;
+               if (i == ton->extended_content_descr_obj_num)
+                       continue;
+               sz += top->objects[i].size;
+       }
+       sz += cdo->size;
+       sz += ecdo->size;
+       num_objects = top->num_objects;
+       if (ton->content_descr_obj_num < 0)
+               num_objects++;
+       if (ton->extended_content_descr_obj_num < 0)
+               num_objects++;
+       ret = xwrite(fd, top_level_header_object_guid, 16);
+       if (ret < 0)
+               goto out;
+       write_u64(tmp, sz);
+       ret = xwrite(fd, tmp, 8);
+       if (ret < 0)
+               goto out;
+       write_u32(tmp, num_objects);
+       ret = xwrite(fd, tmp, 4);
+       if (ret < 0)
+               goto out;
+       write_u8(tmp, top->reserved1);
+       ret = xwrite(fd, tmp, 1);
+       if (ret < 0)
+               goto out;
+       write_u8(tmp, top->reserved2);
+       ret = xwrite(fd, tmp, 1);
+       if (ret < 0)
+               goto out;
+       /*
+        * Write cto and ecto as objects 0 and 1 if they did not exist in the
+        * original file.
+        */
+       if (ton->content_descr_obj_num < 0) {
+               ret = xwrite(fd, cdo->ptr, cdo->size);
+               if (ret < 0)
+                       goto out;
+       }
+       if (ton->extended_content_descr_obj_num < 0) {
+               ret = xwrite(fd, ecdo->ptr, ecdo->size);
+               if (ret < 0)
+                       goto out;
+       }
+
+       for (i = 0; i < top->num_objects; i++) {
+               char *buf = top->objects[i].ptr;
+               sz = top->objects[i].size;
+               if (i == ton->content_descr_obj_num) {
+                       buf = cdo->ptr;
+                       sz = cdo->size;
+               } else if (i == ton->extended_content_descr_obj_num) {
+                       buf = ecdo->ptr;
+                       sz = ecdo->size;
+               }
+               ret = xwrite(fd, buf, sz);
+               if (ret < 0)
+                       goto out;
+       }
+       ret = xwrite(fd, map + top->size, mapsize - top->size);
+out:
+       return ret;
+}
+
+static int wma_rewrite_tags(const char *map, size_t mapsize,
+               struct taginfo *tags, int fd,
+               __a_unused const char *filename)
+{
+       struct afs_top_level_header_object top;
+       struct tag_object_nums ton = {-1, -1};
+       const char *p = map;
+       /* (extended) content description object */
+       struct asf_object cdo = {.ptr = NULL}, ecdo = {.ptr = NULL};
+       int ret;
+
+       /* guid + size + num_objects + 2 * reserved */
+       if (mapsize < 16 + 8 + 4 + 1 + 1)
+               return -E_NO_WMA;
+       if (memcmp(map, top_level_header_object_guid, 16))
+               return -E_NO_WMA;
+       p += 16;
+       top.size = read_u64(p);
+       PARA_INFO_LOG("header_size: %lu\n", (long unsigned)top.size);
+       if (top.size >= mapsize)
+               return -E_NO_WMA;
+       p += 8;
+       top.num_objects = read_u32(p);
+       PARA_NOTICE_LOG("%u header objects\n", top.num_objects);
+       if (top.num_objects > top.size / 24)
+               return -E_NO_WMA;
+       p += 4;
+       top.reserved1 = read_u8(p);
+       p++;
+       top.reserved2 = read_u8(p);
+       if (top.reserved2 != 2)
+               return -E_NO_WMA;
+       p++; /* objects start at p */
+       top.objects = para_malloc(top.num_objects * sizeof(struct asf_object));
+       ret = read_asf_objects(p, top.size - (p - map), top.num_objects,
+               top.objects, &ton);
+       if (ret < 0)
+               goto out;
+       ret = make_cdo(tags, ton.content_descr_obj_num >= 0?
+               top.objects + ton.content_descr_obj_num : NULL, &cdo);
+       if (ret < 0)
+               goto out;
+       ret = make_ecdo(tags, &ecdo);
+       if (ret < 0)
+               goto out;
+       ret = write_output_file(fd, map, mapsize, &top, &ton, &cdo,
+               &ecdo);
+out:
+       free(cdo.ptr);
+       free(ecdo.ptr);
+       free(top.objects);
+       return ret;
+}
+
+static const char * const wma_suffixes[] = {"wma", NULL};
 
 /**
  * The init function of the wma audio format handler.
@@ -283,4 +653,5 @@ void wma_afh_init(struct audio_format_handler *afh)
 {
        afh->get_file_info = wma_get_file_info;
        afh->suffixes = wma_suffixes;
+       afh->rewrite_tags = wma_rewrite_tags;
 }