Skip to content
Open
Show file tree
Hide file tree
Changes from all 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