Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ AC_CONFIG_FILES([ \
src/share/Makefile \
src/test_grabbag/Makefile \
src/test_grabbag/cuesheet/Makefile \
src/test_grabbag/escapes/Makefile \
src/test_grabbag/picture/Makefile \
src/test_libs_common/Makefile \
src/test_libFLAC/Makefile \
Expand Down
1 change: 1 addition & 0 deletions include/share/grabbag.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

/* These can't be included by themselves, only from within grabbag.h */
#include "grabbag/cuesheet.h"
#include "grabbag/escapes.h"
#include "grabbag/file.h"
#include "grabbag/picture.h"
#include "grabbag/replaygain.h"
Expand Down
1 change: 1 addition & 0 deletions include/share/grabbag/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

EXTRA_DIST = \
cuesheet.h \
escapes.h \
file.h \
picture.h \
replaygain.h \
Expand Down
43 changes: 43 additions & 0 deletions include/share/grabbag/escapes.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* grabbag - Convenience lib for various routines common to several tools
* Copyright (C) 2026 Xiph.Org Foundation
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

/* This .h cannot be included by itself; #include "share/grabbag.h" instead. */

#ifndef GRABBAG__ESCAPES_H
#define GRABBAG__ESCAPES_H

#include <stdio.h>
#include <stddef.h>

#include "FLAC/ordinals.h"

#ifdef __cplusplus
extern "C" {
#endif

FLAC__bool grabbag__escape_string_needed(const char *src, size_t src_size);
char *grabbag__create_escaped_string(const char *src, size_t src_size);

FLAC__bool grabbag__unescape_string_needed(const char *src, size_t src_size);
char *grabbag__create_unescaped_string(const char *src, size_t src_size);

#ifdef __cplusplus
}
#endif

#endif
5 changes: 5 additions & 0 deletions man/flac.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,11 @@ Encoding will default to -5, -A "tukey(5e-1)" and one CPU thread.
is useful for scripts, and for setting tags in situations where the
locale is wrong. This option must appear *before* any tag options!

**\--escapes**
: Use \\n-style escapes to allow multiline comments. Supported escapes
are c-style "\\n", "\\r" and "\\\\". A backslash followed by anything
else is an error. This option must appear *before* any tag options!

**-T** "*FIELD=VALUE*"**, \--tag**="*FIELD=VALUE*"
: Add a FLAC tag. The comment must adhere to the Vorbis comment spec;
i.e. the FIELD must contain only legal characters, terminated by an
Expand Down
9 changes: 7 additions & 2 deletions man/metaflac.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ modification time is set to the current time):
is useful for scripts, and setting tags in situations where the
locale is wrong.

**\--escapes**
: Use \\n-style escapes to allow multiline comments. Supported escapes
are c-style "\\n", "\\r" and "\\\\". A backslash followed by anything
else is an error. This option must appear *before* any tag options!

**\--dont-use-padding**
: By default metaflac tries to use padding where possible to avoid
rewriting the entire file if the metadata size changes. Use this
Expand Down Expand Up @@ -146,14 +151,14 @@ modification time is set to the current time):

**\--import-tags-from=file**
: Import tags from a file. Use '-' for stdin. Each line should be of
the form NAME=VALUE. Multi-line comments are currently not supported.
the form NAME=VALUE. Multi-line comments are supported with \--escapes.
Specify \--remove-all-tags and/or \--no-utf8-convert before
\--import-tags-from if necessary. If FILE is '-' (stdin), only one
FLAC file may be specified.

**\--export-tags-to=file**
: Export tags to a file. Use '-' for stdout. Each line will be of the
form NAME=VALUE. Specify \--no-utf8-convert if necessary.
form NAME=VALUE. Specify \--escapes and/or \--no-utf8-convert if necessary.

**\--import-cuesheet-from=file**
: Import a cuesheet from a file. Use '-' for stdin. Only one FLAC file
Expand Down
11 changes: 9 additions & 2 deletions src/flac/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ static struct share__option long_options_[] = {
{ "input-size" , share__required_argument, 0, 0 },
{ "error-on-compression-fail" , share__no_argument, 0, 0 },
{ "limit-min-bitrate" , share__no_argument, 0, 0 },
{ "escapes" , share__no_argument, 0, 0 },

/*
* analysis options
Expand Down Expand Up @@ -264,6 +265,7 @@ static struct {
FLAC__bool replay_gain;
FLAC__bool ignore_chunk_sizes;
FLAC__bool utf8_convert; /* true by default, to convert tag strings from locale to utf-8, false if --no-utf8-convert used */
FLAC__bool escapes; /* Use \n-style escapes to allow multiline comments. */
const char *cmdline_forced_outfilename;
const char *output_prefix;
analysis_options aopts;
Expand Down Expand Up @@ -630,6 +632,7 @@ FLAC__bool init_options(void)
option_values.replay_gain = false;
option_values.ignore_chunk_sizes = false;
option_values.utf8_convert = true;
option_values.escapes = false;
option_values.cmdline_forced_outfilename = 0;
option_values.output_prefix = 0;
option_values.aopts.do_residual_text = false;
Expand Down Expand Up @@ -809,7 +812,7 @@ int parse_option(int short_option, const char *long_option, const char *option_a
}
else if(0 == strcmp(long_option, "tag-from-file")) {
FLAC__ASSERT(0 != option_argument);
if(!flac__vorbiscomment_add(option_values.vorbis_comment, option_argument, /*value_from_file=*/true, /*raw=*/!option_values.utf8_convert, &violation))
if(!flac__vorbiscomment_add(option_values.vorbis_comment, option_argument, /*value_from_file=*/true, /*raw=*/!option_values.utf8_convert, option_values.escapes, &violation))
return usage_error("ERROR: (--tag-from-file) %s\n", violation);
}
else if(0 == strcmp(long_option, "no-cued-seekpoints")) {
Expand Down Expand Up @@ -899,6 +902,9 @@ int parse_option(int short_option, const char *long_option, const char *option_a
else if(0 == strcmp(long_option, "limit-min-bitrate")) {
option_values.limit_min_bitrate = true;
}
else if(0 == strcmp(long_option, "escapes")) {
option_values.escapes = true;
}
/*
* negatives
*/
Expand Down Expand Up @@ -1031,7 +1037,7 @@ int parse_option(int short_option, const char *long_option, const char *option_a
break;
case 'T':
FLAC__ASSERT(0 != option_argument);
if(!flac__vorbiscomment_add(option_values.vorbis_comment, option_argument, /*value_from_file=*/false, /*raw=*/!option_values.utf8_convert, &violation))
if(!flac__vorbiscomment_add(option_values.vorbis_comment, option_argument, /*value_from_file=*/false, /*raw=*/!option_values.utf8_convert, option_values.escapes, &violation))
return usage_error("ERROR: (-T/--tag) %s\n", violation);
break;
case '0':
Expand Down Expand Up @@ -1346,6 +1352,7 @@ void show_help(void)
printf(" --skip={#|mm:ss.ss} Skip the given initial samples for each input\n");
printf(" --until={#|[+|-]mm:ss.ss} Stop at the given sample for each input file\n");
printf(" --no-utf8-convert Do not convert tags from local charset to UTF-8\n");
printf(" --escapes Use \\n-style escapes to allow multiline comments.\n");
printf(" -s, --silent Do not write runtime encode/decode statistics\n");
printf(" --totally-silent Do not print anything, including errors\n");
printf(" -w, --warnings-as-errors Treat all warnings as errors\n");
Expand Down
40 changes: 37 additions & 3 deletions src/flac/vorbiscomment.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ static FLAC__bool parse_vorbis_comment_field(const char *field_ref, char **field
}

/* slight modification: no 'filename' arg, and errors are passed back in 'violation' instead of printed to stderr */
static FLAC__bool set_vc_field(FLAC__StreamMetadata *block, const Argument_VcField *field, FLAC__bool *needs_write, FLAC__bool raw, const char **violation)
static FLAC__bool set_vc_field(FLAC__StreamMetadata *block, const Argument_VcField *field, FLAC__bool *needs_write, FLAC__bool raw, const FLAC__bool escapes, const char **violation)
{
FLAC__StreamMetadata_VorbisComment_Entry entry;
char *converted = NULL;
Expand Down Expand Up @@ -154,6 +154,22 @@ static FLAC__bool set_vc_field(FLAC__StreamMetadata *block, const Argument_VcFie
return false;
}

if(escapes) {
const size_t converted_size = strlen(converted);
if(grabbag__unescape_string_needed(converted, converted_size)) {
char *unescaped = grabbag__create_unescaped_string(converted, converted_size);
if(unescaped != NULL) {
free(converted);
converted = unescaped;
}
else {
free(converted);
*violation = "error unescaping tag value";
return false;
}
}
}

/* create and entry and append it */
if(!FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(&entry, field->field_name, converted)) {
free(converted);
Expand Down Expand Up @@ -187,6 +203,24 @@ static FLAC__bool set_vc_field(FLAC__StreamMetadata *block, const Argument_VcFie
return false;
}
#endif

if(escapes) {
const char *entry_str = (const char *)entry.entry;
const size_t entry_size = strlen(entry_str);
if(grabbag__unescape_string_needed(entry_str, entry_size)) {
char *unescaped = grabbag__create_unescaped_string(entry_str, entry_size);
if(unescaped != NULL) {
entry.entry = (FLAC__byte *)unescaped;
converted = unescaped;
needs_free = true;
}
else {
*violation = "error unescaping tag value";
return false;
}
}
}

entry.length = strlen((const char *)entry.entry);
if(!FLAC__format_vorbiscomment_entry_is_legal(entry.entry, entry.length)) {
if(needs_free)
Expand Down Expand Up @@ -227,7 +261,7 @@ static void free_field(Argument_VcField *obj)
free(obj->field_value);
}

FLAC__bool flac__vorbiscomment_add(FLAC__StreamMetadata *block, const char *comment, FLAC__bool value_from_file, FLAC__bool raw, const char **violation)
FLAC__bool flac__vorbiscomment_add(FLAC__StreamMetadata *block, const char *comment, FLAC__bool value_from_file, FLAC__bool raw, const FLAC__bool escapes, const char **violation)
{
Argument_VcField parsed;
FLAC__bool dummy;
Expand All @@ -244,7 +278,7 @@ FLAC__bool flac__vorbiscomment_add(FLAC__StreamMetadata *block, const char *comm
return false;
}

if(parsed.field_value_length > 0 && !set_vc_field(block, &parsed, &dummy, raw, violation)) {
if(parsed.field_value_length > 0 && !set_vc_field(block, &parsed, &dummy, raw, escapes, violation)) {
free_field(&parsed);
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion src/flac/vorbiscomment.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@

#include "FLAC/metadata.h"

FLAC__bool flac__vorbiscomment_add(FLAC__StreamMetadata *block, const char *comment, FLAC__bool value_from_file, FLAC__bool raw, const char **violation);
FLAC__bool flac__vorbiscomment_add(FLAC__StreamMetadata *block, const char *comment, FLAC__bool value_from_file, FLAC__bool raw, FLAC__bool escapes, const char **violation);

#endif
22 changes: 11 additions & 11 deletions src/metaflac/operations.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ static FLAC__bool do_major_operation__remove(FLAC__Metadata_Chain *chain, const
static FLAC__bool do_major_operation__remove_all(FLAC__Metadata_Chain *chain, const CommandLineOptions *options);
static FLAC__bool do_shorthand_operations(const CommandLineOptions *options);
static FLAC__bool do_shorthand_operations_on_file(const char *filename, const CommandLineOptions *options);
static FLAC__bool do_shorthand_operation(const char *filename, FLAC__bool prefix_with_filename, FLAC__Metadata_Chain *chain, const Operation *operation, FLAC__bool *needs_write, FLAC__bool utf8_convert);
static FLAC__bool do_shorthand_operation(const char *filename, FLAC__bool prefix_with_filename, FLAC__Metadata_Chain *chain, const Operation *operation, FLAC__bool *needs_write, FLAC__bool utf8_convert, FLAC__bool escapes);
static FLAC__bool do_shorthand_operation__add_replay_gain(char **filenames, unsigned num_files, FLAC__bool preserve_modtime, FLAC__bool scan);
static FLAC__bool do_shorthand_operation__add_padding(const char *filename, FLAC__Metadata_Chain *chain, unsigned length, FLAC__bool *needs_write);

static FLAC__bool passes_filter(const CommandLineOptions *options, const FLAC__StreamMetadata *block, unsigned block_number);
static void write_metadata(const char *filename, FLAC__StreamMetadata *block, unsigned block_number, FLAC__bool raw, FLAC__bool hexdump_application);
static void write_metadata(const char *filename, FLAC__StreamMetadata *block, unsigned block_number, FLAC__bool raw, const FLAC__bool escapes, FLAC__bool hexdump_application);
static void write_metadata_binary(FLAC__StreamMetadata *block, FLAC__byte *block_raw, FLAC__bool headerless);

/* from operations_shorthand_seektable.c */
Expand All @@ -58,7 +58,7 @@ extern FLAC__bool do_shorthand_operation__add_seekpoints(const char *filename, F
extern FLAC__bool do_shorthand_operation__streaminfo(const char *filename, FLAC__bool prefix_with_filename, FLAC__Metadata_Chain *chain, const Operation *operation, FLAC__bool *needs_write);

/* from operations_shorthand_vorbiscomment.c */
extern FLAC__bool do_shorthand_operation__vorbis_comment(const char *filename, FLAC__bool prefix_with_filename, FLAC__Metadata_Chain *chain, const Operation *operation, FLAC__bool *needs_write, FLAC__bool raw);
extern FLAC__bool do_shorthand_operation__vorbis_comment(const char *filename, FLAC__bool prefix_with_filename, FLAC__Metadata_Chain *chain, const Operation *operation, FLAC__bool *needs_write, FLAC__bool raw, FLAC__bool escapes);

/* from operations_shorthand_cuesheet.c */
extern FLAC__bool do_shorthand_operation__cuesheet(const char *filename, FLAC__Metadata_Chain *chain, const Operation *operation, FLAC__bool *needs_write);
Expand Down Expand Up @@ -206,7 +206,7 @@ FLAC__bool do_major_operation__list(const char *filename, FLAC__Metadata_Chain *
flac_fprintf(stderr, "%s: ERROR: couldn't get block from chain\n", filename);
else if(passes_filter(options, FLAC__metadata_iterator_get_block(iterator), block_number)) {
if(!options->data_format_is_binary && !options->data_format_is_binary_headerless)
write_metadata(filename, block, block_number, !options->utf8_convert, options->application_data_format_is_hexdump);
write_metadata(filename, block, block_number, !options->utf8_convert, options->escapes, options->application_data_format_is_hexdump);
else {
FLAC__byte * block_raw = FLAC__metadata_object_get_raw(block);
if(block_raw == 0) {
Expand Down Expand Up @@ -447,7 +447,7 @@ FLAC__bool do_shorthand_operations_on_file(const char *filename, const CommandLi
* --add-seekpoint and --import-cuesheet-from are used.
*/
if(options->ops.operations[i].type != OP__ADD_SEEKPOINT)
ok &= do_shorthand_operation(filename, options->prefix_with_filename, chain, &options->ops.operations[i], &needs_write, options->utf8_convert);
ok &= do_shorthand_operation(filename, options->prefix_with_filename, chain, &options->ops.operations[i], &needs_write, options->utf8_convert, options->escapes);

/* The following seems counterintuitive but the meaning
* of 'use_padding' is 'try to keep the overall metadata
Expand All @@ -466,7 +466,7 @@ FLAC__bool do_shorthand_operations_on_file(const char *filename, const CommandLi
*/
for(i = 0; i < options->ops.num_operations && ok; i++) {
if(options->ops.operations[i].type == OP__ADD_SEEKPOINT)
ok &= do_shorthand_operation(filename, options->prefix_with_filename, chain, &options->ops.operations[i], &needs_write, options->utf8_convert);
ok &= do_shorthand_operation(filename, options->prefix_with_filename, chain, &options->ops.operations[i], &needs_write, options->utf8_convert, options->escapes);
}

if(ok && needs_write) {
Expand All @@ -490,7 +490,7 @@ FLAC__bool do_shorthand_operations_on_file(const char *filename, const CommandLi
return ok;
}

FLAC__bool do_shorthand_operation(const char *filename, FLAC__bool prefix_with_filename, FLAC__Metadata_Chain *chain, const Operation *operation, FLAC__bool *needs_write, FLAC__bool utf8_convert)
FLAC__bool do_shorthand_operation(const char *filename, FLAC__bool prefix_with_filename, FLAC__Metadata_Chain *chain, const Operation *operation, FLAC__bool *needs_write, FLAC__bool utf8_convert, const FLAC__bool escapes)
{
FLAC__bool ok = true;

Expand Down Expand Up @@ -524,7 +524,7 @@ FLAC__bool do_shorthand_operation(const char *filename, FLAC__bool prefix_with_f
case OP__SET_VC_FIELD:
case OP__IMPORT_VC_FROM:
case OP__EXPORT_VC_TO:
ok = do_shorthand_operation__vorbis_comment(filename, prefix_with_filename, chain, operation, needs_write, !utf8_convert);
ok = do_shorthand_operation__vorbis_comment(filename, prefix_with_filename, chain, operation, needs_write, !utf8_convert, escapes);
break;
case OP__IMPORT_CUESHEET_FROM:
case OP__EXPORT_CUESHEET_TO:
Expand Down Expand Up @@ -718,7 +718,7 @@ FLAC__bool passes_filter(const CommandLineOptions *options, const FLAC__StreamMe
return matches_number && matches_type;
}

void write_metadata(const char *filename, FLAC__StreamMetadata *block, unsigned block_number, FLAC__bool raw, FLAC__bool hexdump_application)
void write_metadata(const char *filename, FLAC__StreamMetadata *block, unsigned block_number, FLAC__bool raw, const FLAC__bool escapes, FLAC__bool hexdump_application)
{
unsigned i, j;

Expand Down Expand Up @@ -790,11 +790,11 @@ void write_metadata(const char *filename, FLAC__StreamMetadata *block, unsigned
break;
case FLAC__METADATA_TYPE_VORBIS_COMMENT:
PPR; flac_printf(" vendor string: ");
write_vc_field(0, &block->data.vorbis_comment.vendor_string, raw, stdout);
write_vc_field(0, &block->data.vorbis_comment.vendor_string, raw, escapes, stdout);
PPR; flac_printf(" comments: %u\n", block->data.vorbis_comment.num_comments);
for(i = 0; i < block->data.vorbis_comment.num_comments; i++) {
PPR; flac_printf(" comment[%u]: ", i);
write_vc_field(0, &block->data.vorbis_comment.comments[i], raw, stdout);
write_vc_field(0, &block->data.vorbis_comment.comments[i], raw, escapes, stdout);
}
break;
case FLAC__METADATA_TYPE_CUESHEET:
Expand Down
2 changes: 1 addition & 1 deletion src/metaflac/operations_shorthand.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ FLAC__bool do_shorthand_operation__picture(const char *filename, FLAC__Metadata_
FLAC__bool do_shorthand_operation__cuesheet(const char *filename, FLAC__Metadata_Chain *chain, const Operation *operation, FLAC__bool *needs_write);
FLAC__bool do_shorthand_operation__add_seekpoints(const char *filename, FLAC__Metadata_Chain *chain, const char *specification, FLAC__bool *needs_write);
FLAC__bool do_shorthand_operation__streaminfo(const char *filename, FLAC__bool prefix_with_filename, FLAC__Metadata_Chain *chain, const Operation *operation, FLAC__bool *needs_write);
FLAC__bool do_shorthand_operation__vorbis_comment(const char *filename, FLAC__bool prefix_with_filename, FLAC__Metadata_Chain *chain, const Operation *operation, FLAC__bool *needs_write, FLAC__bool raw);
FLAC__bool do_shorthand_operation__vorbis_comment(const char *filename, FLAC__bool prefix_with_filename, FLAC__Metadata_Chain *chain, const Operation *operation, FLAC__bool *needs_write, FLAC__bool raw, FLAC__bool escapes);
Loading