diff --git a/opustags.1 b/opustags.1 index ab031f6..fd8425e 100644 --- a/opustags.1 +++ b/opustags.1 @@ -118,6 +118,7 @@ You can specify \fB-\fP for standard output, in which case the regular output wi .TP .B \-\-set-cover \fIFILE\fP Replace or set the cover art to the specified picture. +Specify \fB-\fP to read the picture from standard input. In theory, an Opus file may contain multiple pictures with different roles, though in practice only the front cover really matters. opustags can currently only handle one front cover and nothing else. .TP diff --git a/src/cli.cc b/src/cli.cc index 4575348..bc9a3d5 100644 --- a/src/cli.cc +++ b/src/cli.cc @@ -136,17 +136,20 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) return opt; // All non-option arguments are input files. - bool stdin_as_input = false; + size_t stdin_uses = 0; for (int i = optind; i < argc; i++) { - stdin_as_input = stdin_as_input || strcmp(argv[i], "-") == 0; + if (strcmp(argv[i], "-") == 0) + ++stdin_uses; opt.paths_in.emplace_back(argv[i]); } + bool stdin_as_input = stdin_uses > 0; - if (set_cover) { - byte_string picture_data = ot::slurp_binary_file(set_cover->c_str()); - opt.to_delete.push_back("METADATA_BLOCK_PICTURE"); - opt.to_add.push_back(ot::make_cover(picture_data)); - } + if (set_cover == "-") + ++stdin_uses; + if (set_all) + ++stdin_uses; + if (stdin_uses > 1) + throw status { st::bad_arguments, "Cannot use standard input more than once." }; // Convert arguments to UTF-8. if (!opt.raw) { @@ -169,10 +172,7 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) if ((!opt.in_place || opt.edit_interactively) && opt.paths_in.size() != 1) throw status {st::bad_arguments, "Exactly one input file must be specified."}; - if (set_all && stdin_as_input) - throw status {st::bad_arguments, "Cannot use standard input as input file when --set-all is specified."}; - - if (opt.edit_interactively && (stdin_as_input || opt.path_out == "-")) + if (opt.edit_interactively && (stdin_as_input || opt.path_out == "-" || opt.cover_out == "-")) throw status {st::bad_arguments, "Cannot edit interactively when standard input or standard output are already used."}; if (opt.edit_interactively && !opt.path_out.has_value() && !opt.in_place) @@ -187,6 +187,12 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) if (opt.cover_out && opt.paths_in.size() > 1) throw status {st::bad_arguments, "Cannot use --output-cover with multiple input files."}; + if (set_cover) { + byte_string picture_data = ot::slurp_binary_file(set_cover->c_str()); + opt.to_delete.push_back("METADATA_BLOCK_PICTURE"); + opt.to_add.push_back(ot::make_cover(picture_data)); + } + if (set_all) { // Read comments from stdin and prepend them to opt.to_add. std::list comments = read_comments(comments_input, opt.raw); diff --git a/src/system.cc b/src/system.cc index a892067..6d26703 100644 --- a/src/system.cc +++ b/src/system.cc @@ -98,26 +98,47 @@ void ot::partial_file::abort() remove(temporary_name.c_str()); } -/** \todo Support non-seekable files like streams. */ +/** + * Determine the file size, in bytes, of the given file. Return -1 on for streams. + */ +static long get_file_size(FILE* f) +{ + if (fseek(f, 0L, SEEK_END) != 0) { + clearerr(f); // Recover. + return -1; + } + long file_size = ftell(f); + rewind(f); + return file_size; +} + ot::byte_string ot::slurp_binary_file(const char* filename) { - std::ifstream file(filename, std::ios::binary | std::ios::ate); - if (file.fail()) + file f = strcmp(filename, "-") == 0 ? freopen(nullptr, "rb", stdin) + : fopen(filename, "rb"); + if (f == nullptr) throw status { st::standard_error, "Could not open '"s + filename + "': " + strerror(errno) + "." }; - auto file_size = file.tellg(); - if (file_size == decltype(file)::pos_type(-1)) - throw status { st::standard_error, - "Could not determine the size of '"s + filename + "': " + strerror(errno) + "." }; - byte_string content; - content.resize(file_size); - file.seekg(0); - file.read(reinterpret_cast(content.data()), file_size); - if (file.fail()) - throw status { st::standard_error, - "Could not read '"s + filename + "': " + strerror(errno) + "." }; + long file_size = get_file_size(f.get()); + if (file_size == -1) { + // Read the input stream block by block and resize the output byte string as needed. + uint8_t buffer[4096]; + while (!feof(f.get())) { + size_t read_len = fread(buffer, 1, sizeof(buffer), f.get()); + content.append(buffer, read_len); + if (ferror(f.get())) + throw status { st::standard_error, + "Could not read '"s + filename + "': " + strerror(errno) + "." }; + } + } else { + // Lucky! We know the file size, so let’s slurp it at once. + content.resize(file_size); + if (fread(content.data(), 1, file_size, f.get()) < file_size) + throw status { st::standard_error, + "Could not read '"s + filename + "': " + strerror(errno) + "." }; + } return content; } diff --git a/t/cli.cc b/t/cli.cc index 51e0493..bb847ad 100644 --- a/t/cli.cc +++ b/t/cli.cc @@ -159,8 +159,7 @@ void check_bad_arguments() error_case({"opustags", "--derp=y"}, "Unrecognized option '--derp=y'.", "unrecognized long option with value"); error_case({"opustags", "-aX=Y"}, "Exactly one input file must be specified.", "no input file"); error_case({"opustags", "-i", "-o", "/dev/null", "-"}, "Cannot combine --in-place and --output.", "in-place + output"); - error_case({"opustags", "-S", "-"}, "Cannot use standard input as input file when --set-all is specified.", - "set all and read opus from stdin"); + error_case({"opustags", "-S", "-"}, "Cannot use standard input more than once.", "set all and read opus from stdin"); error_case({"opustags", "-i", "-"}, "Cannot modify standard input in place.", "write stdin in-place"); error_case({"opustags", "-o", "x", "--output", "y", "z"}, "Cannot specify --output more than once.", "double output"); diff --git a/t/opustags.t b/t/opustags.t index a9545c3..47957ab 100755 --- a/t/opustags.t +++ b/t/opustags.t @@ -4,7 +4,7 @@ use strict; use warnings; use utf8; -use Test::More tests => 58; +use Test::More tests => 59; use Digest::MD5; use File::Basename; @@ -331,10 +331,14 @@ unlink('out.opus'); #################################################################################################### # Cover arts -is_deeply(opustags(qw(-D --set-cover pixel.png gobble.opus -o out.opus), ), ['', '', 0], 'set the cover'); -is_deeply(opustags(qw(--output-cover out.png out.opus), ), [<<'END_OUT', '', 0], 'extract the cover'); +is_deeply(opustags(qw(-D --set-cover pixel.png gobble.opus -o out.opus)), ['', '', 0], 'set the cover'); +is_deeply(opustags(qw(--output-cover out.png out.opus)), [<<'END_OUT', '', 0], 'extract the cover'); METADATA_BLOCK_PICTURE=AAAAAwAAAAlpbWFnZS9wbmcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWJUE5HDQoaCgAAAA1JSERSAAAAAQAAAAEIAgAAAJB3U94AAAAMSURBVAjXY/j//z8ABf4C/tzMWecAAAAASUVORK5CYII= END_OUT is(md5('out.png'), md5('pixel.png'), 'the extracted cover is identical to the one set'); unlink('out.opus'); unlink('out.png'); + +is_deeply(opustags(qw(-D --set-cover - gobble.opus), { in => "GIF8 x" }), [<<'END_OUT', '', 0], 'read the cover from stdin'); +METADATA_BLOCK_PICTURE=AAAAAwAAAAlpbWFnZS9naWYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZHSUY4IHg= +END_OUT