Compare commits

...

14 Commits

Author SHA1 Message Date
Frédéric Mangano
5c1a7b3a99 Mention METADATA_BLOCK_PICTURE in the man page 2025-03-12 11:38:38 +09:00
Marián Konček
b70e65f0d4 Fix CI 2025-02-15 11:19:16 +09:00
Timon Giese
fc7e5e939e Fix typos and formatting in manpage 2025-01-10 22:21:57 +09:00
Marián Konček
e8b66a6207 Fix some sanitizer errors of misaligned pointers 2024-11-07 16:40:51 +09:00
Marián Konček
ba5c151b5d Add GitHub Action 2024-11-07 16:40:51 +09:00
Marián Konček
76afc0efd5 Fix string out-of-bounds access 2024-11-06 15:05:50 +09:00
Frédéric Mangano
a54bac8f55 Fix the warning on comparison of size_t and long 2024-11-01 10:30:12 +09:00
Frédéric Mangano
3293647e8f Wrap fclose to avoid compiler warnings 2024-11-01 10:20:49 +09:00
Frédéric Mangano
d9b051210b Release 1.10.1 2024-05-19 11:33:31 +09:00
perfStack
3da23b58c9 Include library header <algorithm> in cli and opus.
* fixes fmang/opustags#69
2024-05-19 11:23:29 +09:00
Frédéric Mangano
6ae008befd Release 1.10.0 2024-05-03 18:50:03 +09:00
Frédéric Mangano
0067162ffb Support NUL delimiters with -z 2024-04-30 16:24:58 +09:00
Frédéric Mangano
7ec3551f62 Refresh and install the documentation files 2024-02-15 15:00:38 +09:00
sporksnail
a63c06dc05
opustags.1: Fix typo (#64)
* opustags.1: Fix typo

Fix a minor typo in the man page

* opustags.1: remove broken macro
2023-11-26 17:06:08 +09:00
14 changed files with 178 additions and 70 deletions

30
.github/workflows/ci.yaml vendored Normal file
View File

@ -0,0 +1,30 @@
name: Continuous Integration
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
env:
LC_CTYPE: C.UTF-8
CMAKE_COLOR_DIAGNOSTICS: ON
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout git repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt install cmake g++ pkg-config libogg-dev ffmpeg libtest-harness-perl libtest-deep-perl liblist-moreutils-perl libtest-utf8-perl
- name: Build
env:
CXX: g++
CXXFLAGS: -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS -D_GLIBCXX_DEBUG -O2 -flto=auto -g -Wall -Wextra -Werror=format-security -fstack-protector-strong -fstack-clash-protection -fcf-protection -fsanitize=address,undefined
LDFLAGS: -fsanitize=address,undefined
run: |
cmake -B target -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON
cmake --build target
- name: Test
run: |
cmake --build target --target check

View File

@ -1,6 +1,19 @@
opustags changelog
==================
1.10.1 - 2024-05-19
-------------------
Fix a build error on recent systems.
1.10.0 - 2024-05-03
-------------------
- Introduce -z to delimit tags with null bytes.
This option makes it possible to leverage GNU sed or GNU grep for automated tag edition with
`opustags -z … | sed -z … | opustags -z -S …`, while also supporting multi-line tags.
1.9.0 - 2023-06-07
------------------

View File

@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.11)
project(
opustags
VERSION 1.9.0
VERSION 1.10.1
LANGUAGES CXX
)
@ -51,5 +51,6 @@ include(GNUInstallDirs)
install(TARGETS opustags DESTINATION "${CMAKE_INSTALL_BINDIR}")
configure_file(opustags.1 . @ONLY)
install(FILES "${CMAKE_BINARY_DIR}/opustags.1" DESTINATION "${CMAKE_INSTALL_MANDIR}/man1")
install(FILES CHANGELOG.md CONTRIBUTING.md LICENSE README.md DESTINATION ${CMAKE_INSTALL_DOCDIR})
add_subdirectory(t)

View File

@ -25,6 +25,9 @@ You should check that your changes don't break the test suite by running
Following these practices is important to keep the history clean, and to allow
for better code reviews.
You can submit pull requests on GitHub at <https://github.com/fmang/opustags>,
or email me your patches at <fmang+opustags@mg0.fr>.
## History of opustags
opustags is originally a small project made to fill a need to edit tags in Opus
@ -49,6 +52,8 @@ modules, and reviewed for safety.
1.3.0 was focused on correctness, and detects edge cases as early as possible,
instead of hoping something will eventually fail if something is weird.
Subsequent releases have been adding new features.
## Candidate features
The code contains a few `\todo` markers where something could be improved in the
@ -59,13 +64,7 @@ More generally, here are a few features that could be added in the future:
- Discouraging non-ASCII field names.
- Logicial stream listing and selection for multiplexed files.
- Escaping control characters with --escape.
- Dump binary packets with --binary.
- Edition of the vendor string.
- Edition of the arbitrary binary block past the comments.
- Support for OpusTags packets spanning multiple pages (> 64 kB).
- Interactive edition of comments inside the EDITOR (--edit).
- Support for cover arts.
- Load tags from a file with --set-all=tags.txt.
- Colored output.
Don't hesitate to contact me before you do anything, I'll give you directions.

View File

@ -1,4 +1,4 @@
Copyright (c) 2013-2018, Frédéric Mangano-Tarumi
Copyright (c) 2013-2024, Frédéric Mangano
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,

View File

@ -17,6 +17,8 @@ ever be performed.
opustags is tag-agnostic: you can write arbitrary key-value tags, and none of them will be treated
specially. After all, common tags like TITLE or ARTIST are nothing more than conventions.
The projects homepage is located at <https://github.com/fmang/opustags>.
Requirements
------------
@ -67,5 +69,6 @@ Documentation
--vendor print the vendor string
--set-vendor VALUE set the vendor string
--raw disable encoding conversion
-z delimit tags with NUL
See the man page, `opustags.1`, for extensive documentation.

View File

@ -1,4 +1,4 @@
.TH opustags 1 "March 2023" "@PROJECT_NAME@ @PROJECT_VERSION@"
.TH opustags 1 "March 2025" "@PROJECT_NAME@ @PROJECT_VERSION@"
.SH NAME
opustags \- Ogg Opus tag editor
.SH SYNOPSIS
@ -11,7 +11,7 @@ opustags \- Ogg Opus tag editor
.B opustags
.I OPTIONS
.B -i
.R \fIFILE\fP...
\fIFILE\fP...
.br
.B opustags
.I OPTIONS
@ -43,13 +43,13 @@ to set new tags without being bothered by the old ones.
If you want to replace all the tags, you can use the \fB--set-all\fP option which will cause
\fBopustags\fP to read tags from standard input.
The format is the same as the one used for output: newline-separated \fIFIELD=Value\fP assignment.
All the previously existing tags as deleted.
All the previously existing tags are deleted.
.PP
The Opus format specifications requires that tags are encoded in UTF-8, so that's the only encoding
opustags supports. If your system encoding is different, the tags are automatically converted to and
from your system locale. When you edit an Opus file whose tags contains characters unsupported by
your system encoding, the original UTF-8 values will be preserved for the tags you don't explictly
modify.
The Opus format specification requires that tags are encoded in UTF-8, so thats the only encoding
\fBopustags\fP supports. If your system encoding is different, the tags are automatically converted
to and from your system locale. When you edit an Opus file whose tags contain characters unsupported
by your system encoding, the original UTF-8 values will be preserved for the tags you dont
explicitly modify.
.SH OPTIONS
.TP
.B \-h, \-\-help
@ -67,7 +67,7 @@ setting \fB--output\fP to the same path as the input file and enabling \fB--over
This option conflicts with \fB--output\fP.
.TP
.B \-y, \-\-overwrite
By default, \fBopustags\fP refuses to overwrite an already-existent file.
By default, \fBopustags\fP refuses to overwrite an already-existing file.
Use \fB-y\fP to allow overwriting.
Note that this option is not needed when the output is a special file like \fI/dev/null\fP.
.TP
@ -79,10 +79,10 @@ In both cases, the field names are case-insensitive, and expected to be ASCII.
.B \-a, \-\-add \fIFIELD=VALUE\fP
Add a tag. Note that multiple tags with the same field name are perfectly acceptable, so you can add
multiple fields with the same name, and previously existing tags will also be preserved.
When the \fB--delete\fP is used with the same \fIFIELD\fP, only the older tags are deleted.
When \fB--delete\fP is used with the same \fIFIELD\fP, only the older tags are deleted.
.TP
.B \-s, \-\-set \fIFIELD=VALUE\fP
This option is provided for convenience. It delete all the fields of the same
This option is provided for convenience. It deletes all the fields of the same
type that may already exist, then adds it with the wanted value.
This is strictly equivalent to \fB--delete\fP \fIFIELD\fP \fB--add\fP
\fIFIELD=VALUE\fP. You can combine it with \fB--add\fP to add tags of the same
@ -93,33 +93,35 @@ added with \fB--add\fP.
Delete all the previously existing tags.
.TP
.B \-S, \-\-set-all
Sets the tags from scratch.
Set the tags from scratch.
All the original tags are deleted and new ones are read from standard input.
Each line must specify a \fIFIELD=VALUE\fP pair and be separated with line feeds.
Empty lines and lines starting with \fI#\fP are ignored.
Multiline tags must have their continuation lines prefixed by a single tab (in other words, every
Multi-line tags must have their continuation lines prefixed by a single tab (in other words, every
\fI\\n\fP must be replaced by \fI\\n\\t\fP).
.TP
.B \-e, \-\-edit
Edit tags interactively by spawning the program specified by the EDITOR
environment variable. The allowed format is the same as \fB--set-all\fP.
environment variable. The allowed format is the same as with \fB--set-all\fP.
If TERM and VISUAL are set, VISUAL takes precedence over EDITOR.
.TP
.B \-\-output-cover \fIFILE\fP
Save the cover art of the input Opus file to the specified location.
Extract the cover art from the \fBMETADATA_BLOCK_PICTURE\fP tag into the specified location.
If the input file does not contain any cover art, this option has no effect.
To allow overwriting the target location, specify \fB--overwrite\fP.
In the case of multiple pictures embedded in the Opus tags, only the first one is saved.
Note that the since the image format is not fixed, you should consider an extension-less file name
and rely on the magic number to deduce the type. opustags does not add or check the target files
extension.
Note that since the image format is not fixed, you should consider an extension-less file name and
rely on the magic number to deduce the type.
\fBopustags\fP does not add or check the target files extension.
You can specify \fB-\fP for standard output, in which case the regular output will be suppressed.
.TP
.B \-\-set-cover \fIFILE\fP
Replace or set the cover art to the specified picture.
Set the cover art by embedding the specified picture into the \fBMETADATA_BLOCK_PICTURE\fP tag,
replacing any existing values.
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.
the front cover really matters.
\fBopustags\fP can currently only handle one front cover and nothing else.
.TP
.B \-\-vendor
Print the vendor string from the OpusTags packet and do nothing else. Standard tags operations are
@ -135,6 +137,15 @@ corrupted or possibly even contain intentional binary data. In that case, --raw
kind of binary data without ensuring the validity of the tags encoding. This option may also be
useful when your system encoding is different from UTF-8 and you wish to preserve the full UTF-8
character set even though your system cannot display it.
.TP
.B \-z
When editing tags programmatically with line-based tools like grep or sed, tags containing newlines
are likely to corrupt the result because these tools wont interpret multi-line tags as a whole. To
make automatic processing easier, \fB-z\fP delimits tags by a null byte (ASCII NUL) instead of line
feeds. That same \fB-z\fP flag is also supported by GNU grep or GNU sed and, combined with
\fBopustags -z\fP, would make them process the input tag-by-tag instead of line-by-line, thus
supporting multi-line tags as well.
This option also disables the tab prefix for continuation lines after a line feed.
.SH EXAMPLES
.PP
List all the tags in file foo.opus:
@ -145,10 +156,6 @@ Copy in.opus to out.opus, with the TITLE tag added:
.PP
opustags in.opus --output out.opus --add "TITLE=Hello world!"
.PP
Replace all the tags in dest.opus with the ones from src.opus:
.PP
opustags src.opus | opustags --in-place dest.opus --set-all
.PP
Remove the previously existing ARTIST tags and add the two X and Y ARTIST tags, then display the new
tags without writing them to the Opus file:
.PP
@ -157,10 +164,18 @@ tags without writing them to the Opus file:
Edit tags interactively in Vim:
.PP
EDITOR=vim opustags --in-place --edit file.opus
.PP
Replace all the tags in dest.opus with the ones from src.opus:
.PP
opustags src.opus | opustags --in-place dest.opus --set-all
.PP
Use GNU grep to remove all the CHAPTER* tags, with -z to support multi-line tags:
.PP
opustags -z file.opus | grep -z -v ^CHAPTER | opustags -z --in-place file.opus --set-all
.SH CAVEATS
.PP
\fBopustags\fP currently has the following limitations:
.IP \[bu]
.IP \[bu] 2n
Multiplexed streams are not supported.
.IP \[bu]
Control characters inside tags are printed raw rather than being escaped.

View File

@ -56,7 +56,7 @@ std::u8string ot::encode_base64(ot::byte_string_view src)
ot::byte_string ot::decode_base64(std::u8string_view src)
{
// Remove the padding and rely on the string length instead.
while (src.back() == u8'=')
while (!src.empty() && src.back() == u8'=')
src.remove_suffix(1);
size_t olen = src.size() / 4 * 3; // Whole blocks;

View File

@ -15,6 +15,7 @@
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <algorithm>
static const char help_message[] =
PROJECT_NAME " version " PROJECT_VERSION
@ -41,6 +42,7 @@ Options:
--vendor print the vendor string
--set-vendor VALUE set the vendor string
--raw disable encoding conversion
-z delimit tags with NUL
See the man page for extensive documentation.
)raw";
@ -79,7 +81,7 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input)
throw status {st::bad_arguments, "No arguments specified. Use -h for help."};
int c;
optind = 0;
while ((c = getopt_long(argc, argv, ":ho:iyd:a:s:DSe", getopt_options, NULL)) != -1) {
while ((c = getopt_long(argc, argv, ":ho:iyd:a:s:DSez", getopt_options, NULL)) != -1) {
switch (c) {
case 'h':
opt.print_help = true;
@ -139,6 +141,9 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input)
case 'r':
opt.raw = true;
break;
case 'z':
opt.tag_delimiter = '\0';
break;
case ':':
throw status {st::bad_arguments, "Missing value for option '"s + argv[optind - 1] + "'."};
default:
@ -226,17 +231,17 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input)
if (set_all) {
// Read comments from stdin and prepend them to opt.to_add.
std::list<std::u8string> comments = read_comments(comments_input, opt.raw);
std::list<std::u8string> comments = read_comments(comments_input, opt);
opt.to_add.splice(opt.to_add.begin(), std::move(comments));
}
return opt;
}
/** Format a UTF-8 string by adding tabulations (\t) after line feeds (\n) to mark continuation for
* multiline values. */
static std::u8string format_value(const std::u8string& source)
* multiline values. With -z, this behavior applies for embedded NUL characters instead of LF. */
static std::u8string format_value(const std::u8string& source, const ot::options& opt)
{
auto newline_count = std::count(source.begin(), source.end(), u8'\n');
auto newline_count = std::count(source.begin(), source.end(), opt.tag_delimiter);
// General case: the value fits on a single line. Use std::strings copy constructor for the
// most efficient copy we could hope for.
@ -247,7 +252,7 @@ static std::u8string format_value(const std::u8string& source)
formatted.reserve(source.size() + newline_count);
for (auto c : source) {
formatted.push_back(c);
if (c == '\n')
if (c == opt.tag_delimiter)
formatted.push_back(u8'\t');
}
return formatted;
@ -257,9 +262,9 @@ static std::u8string format_value(const std::u8string& source)
* Convert the comment from UTF-8 to the system encoding if relevant, and print it with a trailing
* line feed.
*/
static void puts_utf8(std::u8string_view str, FILE* output, bool raw)
static void puts_utf8(std::u8string_view str, FILE* output, const ot::options& opt)
{
if (raw) {
if (opt.raw) {
fwrite(str.data(), 1, str.size(), output);
} else {
try {
@ -270,7 +275,7 @@ static void puts_utf8(std::u8string_view str, FILE* output, bool raw)
throw;
}
}
putc('\n', output);
putc(opt.tag_delimiter, output);
}
/**
@ -279,7 +284,7 @@ static void puts_utf8(std::u8string_view str, FILE* output, bool raw)
* To disambiguate between a newline embedded in a comment and a newline representing the start of
* the next tag, continuation lines always have a single TAB (^I) character added to the beginning.
*/
void ot::print_comments(const std::list<std::u8string>& comments, FILE* output, bool raw)
void ot::print_comments(const std::list<std::u8string>& comments, FILE* output, const ot::options& opt)
{
bool has_control = false;
for (const std::u8string& source_comment : comments) {
@ -291,14 +296,14 @@ void ot::print_comments(const std::list<std::u8string>& comments, FILE* output,
}
}
}
std::u8string utf8_comment = format_value(source_comment);
puts_utf8(utf8_comment, output, raw);
std::u8string utf8_comment = format_value(source_comment, opt);
puts_utf8(utf8_comment, output, opt);
}
if (has_control)
fputs("warning: Some tags contain control characters.\n", stderr);
}
std::list<std::u8string> ot::read_comments(FILE* input, bool raw)
std::list<std::u8string> ot::read_comments(FILE* input, const ot::options& opt)
{
std::list<std::u8string> comments;
comments.clear();
@ -306,12 +311,12 @@ std::list<std::u8string> ot::read_comments(FILE* input, bool raw)
size_t buflen = 0;
ssize_t nread;
std::u8string* previous_comment = nullptr;
while ((nread = getline(&source_line, &buflen, input)) != -1) {
if (nread > 0 && source_line[nread - 1] == '\n')
while ((nread = getdelim(&source_line, &buflen, opt.tag_delimiter, input)) != -1) {
if (nread > 0 && source_line[nread - 1] == opt.tag_delimiter)
--nread; // Chomp.
std::u8string line;
if (raw) {
if (opt.raw) {
line = std::u8string(reinterpret_cast<char8_t*>(source_line), nread);
} else {
try {
@ -335,7 +340,7 @@ std::list<std::u8string> ot::read_comments(FILE* input, bool raw)
free(source_line);
throw rc;
} else {
line[0] = '\n';
line[0] = opt.tag_delimiter;
previous_comment->append(line);
}
} else if (line.find(u8'=') == decltype(line)::npos) {
@ -391,7 +396,7 @@ static void edit_tags(ot::opus_tags& tags, const ot::options& opt)
}
/** Spawn VISUAL or EDITOR to edit the given tags. */
static void edit_tags_interactively(ot::opus_tags& tags, const std::optional<std::string>& base_path, bool raw)
static void edit_tags_interactively(ot::opus_tags& tags, const std::optional<std::string>& base_path, const ot::options& opt)
{
const char* editor = nullptr;
if (getenv("TERM") != nullptr)
@ -410,7 +415,7 @@ static void edit_tags_interactively(ot::opus_tags& tags, const std::optional<std
if (fd == -1 || (tags_file = fdopen(fd, "w")) == nullptr)
throw ot::status {ot::st::standard_error,
"Could not open '" + tags_path + "': " + strerror(errno)};
ot::print_comments(tags.comments, tags_file.get(), raw);
ot::print_comments(tags.comments, tags_file.get(), opt);
tags_file.reset();
// Spawn the editor, and watch the modification timestamps.
@ -441,7 +446,7 @@ static void edit_tags_interactively(ot::opus_tags& tags, const std::optional<std
if (tags_file == nullptr)
throw ot::status {ot::st::standard_error, "Error opening " + tags_path + ": " + strerror(errno)};
try {
tags.comments = ot::read_comments(tags_file.get(), raw);
tags.comments = ot::read_comments(tags_file.get(), opt);
} catch (const ot::status& rc) {
fprintf(stderr, "warning: Leaving %s on the disk.\n", tags_path.c_str());
throw;
@ -524,7 +529,7 @@ static void process(ot::ogg_reader& reader, ot::ogg_writer* writer, const ot::op
if (writer) {
if (opt.edit_interactively) {
fflush(writer->file); // flush before calling the subprocess
edit_tags_interactively(tags, writer->path, opt.raw);
edit_tags_interactively(tags, writer->path, opt);
}
auto packet = ot::render_tags(tags);
writer->write_header_packet(serialno, pageno, packet);
@ -532,9 +537,9 @@ static void process(ot::ogg_reader& reader, ot::ogg_writer* writer, const ot::op
} else {
if (opt.cover_out != "-") {
if (opt.print_vendor)
puts_utf8(tags.vendor, stdout, opt.raw);
puts_utf8(tags.vendor, stdout, opt);
else
ot::print_comments(tags.comments, stdout, opt.raw);
ot::print_comments(tags.comments, stdout, opt);
}
break;
}

View File

@ -24,6 +24,7 @@
#include <opustags.h>
#include <string.h>
#include <algorithm>
ot::opus_tags ot::parse_tags(const ogg_packet& packet)
{
@ -54,7 +55,9 @@ ot::opus_tags ot::parse_tags(const ogg_packet& packet)
// Comment count
if (pos + 4 > size)
throw status {st::cut_comment_count, "Comment count did not fit the comment header"};
uint32_t count = le32toh(*((uint32_t*) (data + pos)));
uint32_t count;
memcpy(&count, data + pos, sizeof(count));
count = le32toh(count);
pos += 4;
// Comments' data
@ -62,7 +65,9 @@ ot::opus_tags ot::parse_tags(const ogg_packet& packet)
if (pos + 4 > size)
throw status {st::cut_comment_length,
"Comment length did not fit the comment header"};
uint32_t comment_length = le32toh(*((uint32_t*) (data + pos)));
uint32_t comment_length;
memcpy(&comment_length, data + pos, sizeof(comment_length));
comment_length = le32toh(comment_length);
if (pos + 4 + comment_length > size)
throw status {st::cut_comment_data,
"Comment string did not fit the comment header"};
@ -133,12 +138,16 @@ ot::picture::picture(ot::byte_string block)
size_t desc_offset = mime_offset + 4 + mime_size;
if (storage.size() < desc_offset + 4)
throw status { st::invalid_size, "missing description in picture block" };
uint32_t desc_size = be32toh(*reinterpret_cast<const uint32_t*>(&storage[desc_offset]));
uint32_t desc_size;
memcpy(&desc_size, &storage[desc_offset], sizeof(desc_size));
desc_size = be32toh(desc_size);
size_t pic_offset = desc_offset + 4 + desc_size + 16;
if (storage.size() < pic_offset + 4)
throw status { st::invalid_size, "missing picture data in picture block" };
uint32_t pic_size = be32toh(*reinterpret_cast<const uint32_t*>(&storage[pic_offset]));
uint32_t pic_size;
memcpy(&pic_size, &storage[pic_offset], sizeof(pic_size));
pic_size = be32toh(pic_size);
if (storage.size() != pic_offset + 4 + pic_size)
throw status { st::invalid_size, "invalid picture block size" };
@ -156,7 +165,8 @@ ot::byte_string ot::picture::serialize() const
*reinterpret_cast<uint32_t*>(&bytes[0]) = htobe32(3); // Picture type: front cover.
*reinterpret_cast<uint32_t*>(&bytes[mime_offset]) = htobe32(mime_type.size());
std::copy(mime_type.begin(), mime_type.end(), std::next(bytes.begin(), mime_offset + 4));
*reinterpret_cast<uint32_t*>(&bytes[pic_offset]) = htobe32(picture_data.size());
uint32_t picture_data_size = htobe32(picture_data.size());
memcpy(&bytes[pic_offset], &picture_data_size, sizeof(picture_data_size));
std::copy(picture_data.begin(), picture_data.end(), std::next(bytes.begin(), pic_offset + 4));
return bytes;
}

View File

@ -119,13 +119,16 @@ using byte_string_view = std::basic_string_view<uint8_t>;
* \{
*/
/** fclose wrapper for std::unique_ptrs deleter. */
void close_file(FILE*);
/**
* Smart auto-closing FILE* handle.
*
* It implictly converts from an already opened FILE*.
*/
struct file : std::unique_ptr<FILE, decltype(&fclose)> {
file(FILE* f = nullptr) : std::unique_ptr<FILE, decltype(&fclose)>(f, &fclose) {}
struct file : std::unique_ptr<FILE, decltype(&close_file)> {
file(FILE* f = nullptr) : std::unique_ptr<FILE, decltype(&close_file)>(f, &close_file) {}
};
/**
@ -534,6 +537,13 @@ struct options {
* extract and set as-is, encoding conversion would get in the way.
*/
bool raw = false;
/**
* In text mode (default), tags are separated by a line feed. However, when combining
* opustags with grep or other line-based tools, this proves to be a bad separator because
* tag values may contain newlines. Changing the delimiter to '\0' with -z eases the
* processing of multi-line tags with other tools that support null-terminated lines.
*/
char tag_delimiter = '\n';
};
/**
@ -551,13 +561,13 @@ options parse_options(int argc, char** argv, FILE* comments);
*
* The output generated is meant to be parseable by #ot::read_comments.
*/
void print_comments(const std::list<std::u8string>& comments, FILE* output, bool raw);
void print_comments(const std::list<std::u8string>& comments, FILE* output, const options& opt);
/**
* Parse the comments outputted by #ot::print_comments. Unless raw is true, the comments are
* converted from the system encoding to UTF-8, and returned as UTF-8.
*/
std::list<std::u8string> read_comments(FILE* input, bool raw);
std::list<std::u8string> read_comments(FILE* input, const options& opt);
/**
* Remove all comments matching the specified selector, which may either be a field name or a

View File

@ -29,6 +29,11 @@ ot::byte_string_view operator""_bsv(const char* data, size_t size)
return ot::byte_string_view(reinterpret_cast<const uint8_t*>(data), size);
}
void ot::close_file(FILE* file)
{
fclose(file);
}
void ot::partial_file::open(const char* destination)
{
final_name = destination;
@ -122,7 +127,7 @@ ot::byte_string ot::slurp_binary_file(const char* filename)
byte_string content;
long file_size = get_file_size(f.get());
if (file_size == -1) {
if (file_size < 0) {
// Read the input stream block by block and resize the output byte string as needed.
uint8_t buffer[4096];
while (!feof(f.get())) {
@ -135,7 +140,7 @@ ot::byte_string ot::slurp_binary_file(const char* filename)
} else {
// Lucky! We know the file size, so lets slurp it at once.
content.resize(file_size);
if (fread(content.data(), 1, file_size, f.get()) < file_size)
if (fread(content.data(), 1, file_size, f.get()) < size_t(file_size))
throw status { st::standard_error,
"Could not read '"s + filename + "': " + strerror(errno) + "." };
}

View File

@ -5,8 +5,10 @@
static ot::status read_comments(FILE* input, std::list<std::u8string>& comments, bool raw)
{
ot::options opt;
opt.raw = raw;
try {
comments = ot::read_comments(input, raw);
comments = ot::read_comments(input, opt);
} catch (const ot::status& rc) {
return rc;
}

View File

@ -4,7 +4,7 @@ use strict;
use warnings;
use utf8;
use Test::More tests => 62;
use Test::More tests => 66;
use Test::Deep qw(cmp_deeply re);
use Digest::MD5;
@ -327,3 +327,18 @@ is_deeply(opustags(qw(--vendor gobble.opus)), ["Lavf58.12.100\n", '', 0], 'print
is_deeply(opustags(qw(--set-vendor opustags gobble.opus -o out.opus)), ['', '', 0], 'set the vendor string');
is_deeply(opustags(qw(--vendor out.opus)), ["opustags\n", '', 0], 'the vendor string was updated');
unlink('out.opus');
####################################################################################################
# Multi-line tags
is_deeply(opustags(qw(--set-all gobble.opus -o out.opus), { in => "MULTILINE=one\n\ttwo\nSIMPLE=three\n" }), ['', '', 0], 'parses continuation lines');
is_deeply(opustags(qw(out.opus -z)), ["MULTILINE=one\ntwo\0SIMPLE=three\0", '', 0], 'delimits output with NUL on -z');
unlink('out.opus');
is_deeply(opustags(qw(--set-all gobble.opus -o out.opus -z), { in => "MULTILINE=one\ntwo\0SIMPLE=three\0" }), ['', '', 0], 'delimits input with NUL on -z');
is_deeply(opustags(qw(out.opus)), [<<'END', '', 0], 'indents continuation lines');
MULTILINE=one
two
SIMPLE=three
END
unlink('out.opus');