static int loglevel;
INIT_STDERR_LOGGING(loglevel)
+static inline bool tag_needs_update(bool given, const char *tag,
+ const char *arg)
+{
+ return given && (!tag || strcmp(tag, arg) != 0);
+}
+
+static int rewrite_tags(const char *name, int input_fd, void *map,
+ size_t map_size, int audio_format_id, struct afh_info *afhi)
+{
+ struct taginfo *tags = &afhi->tags;
+ bool modified = false;
+ char *tmp_name;
+ int output_fd = -1, ret;
+ struct stat sb;
+
+ if (tag_needs_update(conf.year_given, tags->year, conf.year_arg)) {
+ free(tags->year);
+ tags->year = para_strdup(conf.year_arg);
+ modified = true;
+ }
+ if (tag_needs_update(conf.title_given, tags->title, conf.title_arg)) {
+ free(tags->title);
+ tags->title = para_strdup(conf.title_arg);
+ modified = true;
+ }
+ if (tag_needs_update(conf.artist_given, tags->artist,
+ conf.artist_arg)) {
+ free(tags->artist);
+ tags->artist = para_strdup(conf.artist_arg);
+ modified = true;
+ }
+ if (tag_needs_update(conf.album_given, tags->album, conf.album_arg)) {
+ free(tags->album);
+ tags->album = para_strdup(conf.album_arg);
+ modified = true;
+ }
+ if (tag_needs_update(conf.comment_given, tags->comment,
+ conf.comment_arg)) {
+ free(tags->comment);
+ tags->comment = para_strdup(conf.comment_arg);
+ modified = true;
+ }
+ if (!modified) {
+ PARA_WARNING_LOG("no modifications necessary\n");
+ return 0;
+ }
+ /*
+ * mkstmp() creates the temporary file with permissions 0600, but we
+ * like it to have the same permissions as the original file, so we
+ * have to get this information.
+ */
+ if (fstat(input_fd, &sb) < 0) {
+ ret = -ERRNO_TO_PARA_ERROR(errno);
+ PARA_ERROR_LOG("failed to fstat fd %d (%s)\n", input_fd, name);
+ return ret;
+ }
+ tmp_name = make_message("%s.XXXXXX", name);
+ ret = mkstemp(tmp_name);
+ if (ret < 0) {
+ ret = -ERRNO_TO_PARA_ERROR(errno);
+ PARA_ERROR_LOG("could not create temporary file\n");
+ goto out;
+ }
+ output_fd = ret;
+ if (fchmod(output_fd, sb.st_mode) < 0) {
+ ret = -ERRNO_TO_PARA_ERROR(errno);
+ PARA_ERROR_LOG("failed to fchmod fd %d (%s)\n", output_fd,
+ tmp_name);
+ goto out;
+ }
+ ret = afh_rewrite_tags(audio_format_id, map, map_size, tags, output_fd,
+ tmp_name);
+ if (ret < 0)
+ goto out;
+ if (conf.backup_given) {
+ char *backup_name = make_message("%s~", name);
+ ret = xrename(name, backup_name);
+ free(backup_name);
+ if (ret < 0)
+ goto out;
+ }
+ ret = xrename(tmp_name, name);
+out:
+ if (ret < 0 && output_fd >= 0)
+ unlink(tmp_name); /* ignore errors */
+ free(tmp_name);
+ if (output_fd >= 0)
+ close(output_fd);
+ return ret;
+}
+
static void print_info(int audio_format_num, struct afh_info *afhi)
{
char *msg;
fd, &afhi);
if (ret >= 0) {
audio_format_num = ret;
- printf("File %d: %s\n", i + 1, conf.inputs[i]);
- print_info(audio_format_num, &afhi);
- if (conf.chunk_table_given)
- print_chunk_table(&afhi);
- printf("\n");
+ if (conf.modify_given) {
+ ret = rewrite_tags(conf.inputs[i], fd, audio_file_data,
+ audio_file_size, audio_format_num, &afhi);
+ } else {
+ printf("File %d: %s\n", i + 1, conf.inputs[i]);
+ print_info(audio_format_num, &afhi);
+ if (conf.chunk_table_given)
+ print_chunk_table(&afhi);
+ printf("\n");
+ }
clear_afhi(&afhi);
}
close(fd);
#include <sys/types.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); \
return 0;
}
+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)
+{
+ /*
+ * Without specifying LE (little endian), iconv includes a byte order
+ * mark (e.g. 0xFFFE) at the beginning.
+ */
+ iconv_t cd = iconv_open("UTF-16LE", "UTF-8");
+ size_t sz, inbytes, outbytes, inbytesleft, outbytesleft;
+ char *inbuf, *outbuf;
+ int ret;
+
+ if (!src || !*src) {
+ *dst = para_calloc(2);
+ ret = 0;
+ goto out;
+ }
+ if (cd == (iconv_t) -1)
+ 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);
+ 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 (ret < 0)
+ free(*dst);
+ 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_title_bytes, orig_artist_bytes, orig_cr_bytes,
+ orig_comment_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) {
+ /*
+ * 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_title_bytes = 2;
+ orig_artist_bytes = 2;
+ orig_cr_bytes = 2;
+ orig_comment_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* wma_suffixes[] = {"wma", NULL};
/**
{
afh->get_file_info = wma_get_file_info;
afh->suffixes = wma_suffixes;
+ afh->rewrite_tags = wma_rewrite_tags;
}