opustags/opustags.c
Frédéric Mangano-Tarumi 7174a1f2f2 bump to 1.1.1
2018-10-24 18:27:57 -04:00

514 lines
16 KiB
C

#include <errno.h>
#include <getopt.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ogg/ogg.h>
#ifdef __APPLE__
#include <libkern/OSByteOrder.h>
#define htole32(x) OSSwapHostToLittleInt32(x)
#define le32toh(x) OSSwapLittleToHostInt32(x)
#endif
typedef struct {
uint32_t vendor_length;
const char *vendor_string;
uint32_t count;
uint32_t *lengths;
const char **comment;
} opus_tags;
int parse_tags(char *data, long len, opus_tags *tags){
long pos;
if(len < 8+4+4)
return -1;
if(strncmp(data, "OpusTags", 8) != 0)
return -1;
// Vendor
pos = 8;
tags->vendor_length = le32toh(*((uint32_t*) (data + pos)));
tags->vendor_string = data + pos + 4;
pos += 4 + tags->vendor_length;
if(pos + 4 > len)
return -1;
// Count
tags->count = le32toh(*((uint32_t*) (data + pos)));
if(tags->count == 0)
return 0;
tags->lengths = calloc(tags->count, sizeof(uint32_t));
if(tags->lengths == NULL)
return -1;
tags->comment = calloc(tags->count, sizeof(char*));
if(tags->comment == NULL){
free(tags->lengths);
return -1;
}
pos += 4;
// Comment
uint32_t i;
for(i=0; i<tags->count; i++){
tags->lengths[i] = le32toh(*((uint32_t*) (data + pos)));
tags->comment[i] = data + pos + 4;
pos += 4 + tags->lengths[i];
if(pos > len)
return -1;
}
if(pos < len)
fprintf(stderr, "warning: %ld unused bytes at the end of the OpusTags packet\n", len - pos);
return 0;
}
int render_tags(opus_tags *tags, ogg_packet *op){
// Note: op->packet must be manually freed.
op->b_o_s = 0;
op->e_o_s = 0;
op->granulepos = 0;
op->packetno = 1;
long len = 8 + 4 + tags->vendor_length + 4;
uint32_t i;
for(i=0; i<tags->count; i++)
len += 4 + tags->lengths[i];
op->bytes = len;
char *data = malloc(len);
if(!data)
return -1;
op->packet = (unsigned char*) data;
uint32_t n;
memcpy(data, "OpusTags", 8);
n = htole32(tags->vendor_length);
memcpy(data+8, &n, 4);
memcpy(data+12, tags->vendor_string, tags->vendor_length);
data += 12 + tags->vendor_length;
n = htole32(tags->count);
memcpy(data, &n, 4);
data += 4;
for(i=0; i<tags->count; i++){
n = htole32(tags->lengths[i]);
memcpy(data, &n, 4);
memcpy(data+4, tags->comment[i], tags->lengths[i]);
data += 4 + tags->lengths[i];
}
return 0;
}
int match_field(const char *comment, uint32_t len, const char *field){
size_t field_len;
for(field_len = 0; field[field_len] != '\0' && field[field_len] != '='; field_len++);
if(len <= field_len)
return 0;
if(comment[field_len] != '=')
return 0;
if(strncmp(comment, field, field_len) != 0)
return 0;
return 1;
}
void delete_tags(opus_tags *tags, const char *field){
uint32_t i;
for(i=0; i<tags->count; i++){
if(match_field(tags->comment[i], tags->lengths[i], field)){
tags->count--;
tags->lengths[i] = tags->lengths[tags->count];
tags->comment[i] = tags->comment[tags->count];
// No need to resize the arrays.
}
}
}
int add_tags(opus_tags *tags, const char **tags_to_add, uint32_t count){
if(count == 0)
return 0;
uint32_t *lengths = realloc(tags->lengths, (tags->count + count) * sizeof(uint32_t));
const char **comment = realloc(tags->comment, (tags->count + count) * sizeof(char*));
if(lengths == NULL || comment == NULL)
return -1;
tags->lengths = lengths;
tags->comment = comment;
uint32_t i;
for(i=0; i<count; i++){
tags->lengths[tags->count + i] = strlen(tags_to_add[i]);
tags->comment[tags->count + i] = tags_to_add[i];
}
tags->count += count;
return 0;
}
void print_tags(opus_tags *tags){
if(tags->count == 0)
puts("no tags");
int i;
for(i=0; i<tags->count; i++){
fwrite(tags->comment[i], 1, tags->lengths[i], stdout);
puts("");
}
}
void free_tags(opus_tags *tags){
if(tags->count > 0){
free(tags->lengths);
free(tags->comment);
}
}
int write_page(ogg_page *og, FILE *stream){
if(fwrite(og->header, 1, og->header_len, stream) < og->header_len)
return -1;
if(fwrite(og->body, 1, og->body_len, stream) < og->body_len)
return -1;
return 0;
}
const char *version = "opustags version 1.1.1\n";
const char *usage =
"Usage: opustags --help\n"
" opustags [OPTIONS] FILE\n"
" opustags OPTIONS FILE -o FILE\n";
const char *help =
"Options:\n"
" -h, --help print this help\n"
" -o, --output write the modified tags to a file\n"
" -i, --in-place [SUFFIX] use a temporary file then replace the original file\n"
" -y, --overwrite overwrite the output file if it already exists\n"
" -d, --delete FIELD delete all the fields of a specified type\n"
" -a, --add FIELD=VALUE add a field\n"
" -s, --set FIELD=VALUE delete then add a field\n"
" -D, --delete-all delete all the fields!\n"
" -S, --set-all read the fields from stdin\n";
struct option options[] = {
{"help", no_argument, 0, 'h'},
{"output", required_argument, 0, 'o'},
{"in-place", optional_argument, 0, 'i'},
{"overwrite", no_argument, 0, 'y'},
{"delete", required_argument, 0, 'd'},
{"add", required_argument, 0, 'a'},
{"set", required_argument, 0, 's'},
{"delete-all", no_argument, 0, 'D'},
{"set-all", no_argument, 0, 'S'},
{NULL, 0, 0, 0}
};
int main(int argc, char **argv){
if(argc == 1){
fputs(version, stdout);
fputs(usage, stdout);
return EXIT_SUCCESS;
}
char *path_in, *path_out = NULL, *inplace = NULL;
const char* to_add[argc];
const char* to_delete[argc];
int count_add = 0, count_delete = 0;
int delete_all = 0;
int set_all = 0;
int overwrite = 0;
int print_help = 0;
int c;
while((c = getopt_long(argc, argv, "ho:i::yd:a:s:DS", options, NULL)) != -1){
switch(c){
case 'h':
print_help = 1;
break;
case 'o':
path_out = optarg;
break;
case 'i':
inplace = optarg == NULL ? ".otmp" : optarg;
break;
case 'y':
overwrite = 1;
break;
case 'd':
if(strchr(optarg, '=') != NULL){
fprintf(stderr, "invalid field: '%s'\n", optarg);
return EXIT_FAILURE;
}
to_delete[count_delete++] = optarg;
break;
case 'a':
case 's':
if(strchr(optarg, '=') == NULL){
fprintf(stderr, "invalid comment: '%s'\n", optarg);
return EXIT_FAILURE;
}
to_add[count_add++] = optarg;
if(c == 's')
to_delete[count_delete++] = optarg;
break;
case 'S':
set_all = 1;
case 'D':
delete_all = 1;
break;
default:
return EXIT_FAILURE;
}
}
if(print_help){
puts(version);
puts(usage);
puts(help);
puts("See the man page for extensive documentation.");
return EXIT_SUCCESS;
}
if(optind != argc - 1){
fputs("invalid arguments\n", stderr);
return EXIT_FAILURE;
}
if(inplace && path_out){
fputs("cannot combine --in-place and --output\n", stderr);
return EXIT_FAILURE;
}
path_in = argv[optind];
if(path_out != NULL && strcmp(path_in, "-") != 0){
char canon_in[PATH_MAX+1], canon_out[PATH_MAX+1];
if(realpath(path_in, canon_in) && realpath(path_out, canon_out)){
if(strcmp(canon_in, canon_out) == 0){
fputs("error: the input and output files are the same\n", stderr);
return EXIT_FAILURE;
}
}
}
FILE *in;
if(strcmp(path_in, "-") == 0){
if(set_all){
fputs("can't open stdin for input when -S is specified\n", stderr);
return EXIT_FAILURE;
}
if(inplace){
fputs("cannot modify stdin 'in-place'\n", stderr);
return EXIT_FAILURE;
}
in = stdin;
}
else
in = fopen(path_in, "r");
if(!in){
perror("fopen");
return EXIT_FAILURE;
}
FILE *out = NULL;
if(inplace != NULL){
path_out = malloc(strlen(path_in) + strlen(inplace) + 1);
if(path_out == NULL){
fputs("failure to allocate memory\n", stderr);
fclose(in);
return EXIT_FAILURE;
}
strcpy(path_out, path_in);
strcat(path_out, inplace);
}
if(path_out != NULL){
if(strcmp(path_out, "-") == 0)
out = stdout;
else{
if(!overwrite && !inplace){
if(access(path_out, F_OK) == 0){
fprintf(stderr, "'%s' already exists (use -y to overwrite)\n", path_out);
fclose(in);
return EXIT_FAILURE;
}
}
out = fopen(path_out, "w");
if(!out){
perror("fopen");
fclose(in);
if(inplace)
free(path_out);
return EXIT_FAILURE;
}
}
}
ogg_sync_state oy;
ogg_stream_state os, enc;
ogg_page og;
ogg_packet op;
opus_tags tags;
ogg_sync_init(&oy);
char *buf;
size_t len;
char *error = NULL;
int packet_count = -1;
while(error == NULL){
// Read until we complete a page.
if(ogg_sync_pageout(&oy, &og) != 1){
if(feof(in))
break;
buf = ogg_sync_buffer(&oy, 65536);
if(buf == NULL){
error = "ogg_sync_buffer: out of memory";
break;
}
len = fread(buf, 1, 65536, in);
if(ferror(in))
error = strerror(errno);
ogg_sync_wrote(&oy, len);
if(ogg_sync_check(&oy) != 0)
error = "ogg_sync_check: internal error";
continue;
}
// We got a page.
// Short-circuit when the relevant packets have been read.
if(packet_count >= 2 && out){
if(write_page(&og, out) == -1){
error = "write_page: fwrite error";
break;
}
continue;
}
// Initialize the streams from the first page.
if(packet_count == -1){
if(ogg_stream_init(&os, ogg_page_serialno(&og)) == -1){
error = "ogg_stream_init: couldn't create a decoder";
break;
}
if(out){
if(ogg_stream_init(&enc, ogg_page_serialno(&og)) == -1){
error = "ogg_stream_init: couldn't create an encoder";
break;
}
}
packet_count = 0;
}
if(ogg_stream_pagein(&os, &og) == -1){
error = "ogg_stream_pagein: invalid page";
break;
}
// Read all the packets.
while(ogg_stream_packetout(&os, &op) == 1){
packet_count++;
if(packet_count == 1){ // Identification header
if(strncmp((char*) op.packet, "OpusHead", 8) != 0){
error = "opustags: invalid identification header";
break;
}
}
else if(packet_count == 2){ // Comment header
if(parse_tags((char*) op.packet, op.bytes, &tags) == -1){
error = "opustags: invalid comment header";
break;
}
if(delete_all)
tags.count = 0;
else{
int i;
for(i=0; i<count_delete; i++)
delete_tags(&tags, to_delete[i]);
}
char *raw_tags = NULL;
if(set_all){
raw_tags = malloc(16384);
if(raw_tags == NULL){
error = "malloc: not enough memory for buffering stdin";
free(raw_tags);
break;
}
else{
char *raw_comment[256];
size_t raw_len = fread(raw_tags, 1, 16383, stdin);
if(raw_len == 16383)
fputs("warning: truncating comment to 16 KiB\n", stderr);
raw_tags[raw_len] = '\0';
uint32_t raw_count = 0;
size_t field_len = 0;
int caught_eq = 0;
size_t i = 0;
char *cursor = raw_tags;
for(i=0; i <= raw_len && raw_count < 256; i++){
if(raw_tags[i] == '\n' || raw_tags[i] == '\0'){
if(field_len == 0)
continue;
if(caught_eq)
raw_comment[raw_count++] = cursor;
else
fputs("warning: skipping malformed tag\n", stderr);
cursor = raw_tags + i + 1;
field_len = 0;
caught_eq = 0;
raw_tags[i] = '\0';
continue;
}
if(raw_tags[i] == '=')
caught_eq = 1;
field_len++;
}
add_tags(&tags, (const char**) raw_comment, raw_count);
}
}
add_tags(&tags, to_add, count_add);
if(out){
ogg_packet packet;
render_tags(&tags, &packet);
if(ogg_stream_packetin(&enc, &packet) == -1)
error = "ogg_stream_packetin: internal error";
free(packet.packet);
}
else
print_tags(&tags);
free_tags(&tags);
if(raw_tags)
free(raw_tags);
if(error || !out)
break;
else
continue;
}
if(out){
if(ogg_stream_packetin(&enc, &op) == -1){
error = "ogg_stream_packetin: internal error";
break;
}
}
}
if(error != NULL)
break;
if(ogg_stream_check(&os) != 0)
error = "ogg_stream_check: internal error (decoder)";
// Write the page.
if(out){
ogg_stream_flush(&enc, &og);
if(write_page(&og, out) == -1)
error = "write_page: fwrite error";
else if(ogg_stream_check(&enc) != 0)
error = "ogg_stream_check: internal error (encoder)";
}
else if(packet_count >= 2) // Read-only mode
break;
}
if(packet_count >= 0){
ogg_stream_clear(&os);
if(out)
ogg_stream_clear(&enc);
}
ogg_sync_clear(&oy);
fclose(in);
if(out)
fclose(out);
if(!error && packet_count < 2)
error = "opustags: invalid file";
if(error){
fprintf(stderr, "%s\n", error);
if(path_out != NULL && out != stdout)
remove(path_out);
if(inplace)
free(path_out);
return EXIT_FAILURE;
}
else if(inplace){
if(rename(path_out, path_in) == -1){
perror("rename");
free(path_out);
return EXIT_FAILURE;
}
free(path_out);
}
return EXIT_SUCCESS;
}