diff --git a/configure.ac b/configure.ac index 8a8662b4d1..3a7110a9f4 100644 --- a/configure.ac +++ b/configure.ac @@ -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 \ diff --git a/include/share/grabbag.h b/include/share/grabbag.h index 3f9eb89982..63a9321fa5 100644 --- a/include/share/grabbag.h +++ b/include/share/grabbag.h @@ -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" diff --git a/include/share/grabbag/Makefile.am b/include/share/grabbag/Makefile.am index 22baa15747..c9c127f967 100644 --- a/include/share/grabbag/Makefile.am +++ b/include/share/grabbag/Makefile.am @@ -2,6 +2,7 @@ EXTRA_DIST = \ cuesheet.h \ + escapes.h \ file.h \ picture.h \ replaygain.h \ diff --git a/include/share/grabbag/escapes.h b/include/share/grabbag/escapes.h new file mode 100644 index 0000000000..01ee81ccf6 --- /dev/null +++ b/include/share/grabbag/escapes.h @@ -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 +#include + +#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 diff --git a/man/flac.md b/man/flac.md index 7762fc2849..547c3fa7db 100644 --- a/man/flac.md +++ b/man/flac.md @@ -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 diff --git a/man/metaflac.md b/man/metaflac.md index 725602f7c1..0e3a5ed42b 100644 --- a/man/metaflac.md +++ b/man/metaflac.md @@ -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 @@ -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 diff --git a/src/flac/main.c b/src/flac/main.c index f45488d180..f4e5dd21a3 100644 --- a/src/flac/main.c +++ b/src/flac/main.c @@ -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 @@ -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; @@ -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; @@ -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")) { @@ -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 */ @@ -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': @@ -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"); diff --git a/src/flac/vorbiscomment.c b/src/flac/vorbiscomment.c index 41f42b7411..043496599b 100644 --- a/src/flac/vorbiscomment.c +++ b/src/flac/vorbiscomment.c @@ -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; @@ -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); @@ -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) @@ -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; @@ -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; } diff --git a/src/flac/vorbiscomment.h b/src/flac/vorbiscomment.h index 680aefd42c..cc429bdf21 100644 --- a/src/flac/vorbiscomment.h +++ b/src/flac/vorbiscomment.h @@ -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 diff --git a/src/metaflac/operations.c b/src/metaflac/operations.c index 060c0c98e8..3488e10f2b 100644 --- a/src/metaflac/operations.c +++ b/src/metaflac/operations.c @@ -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 */ @@ -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); @@ -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) { @@ -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 @@ -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) { @@ -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; @@ -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: @@ -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; @@ -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: diff --git a/src/metaflac/operations_shorthand.h b/src/metaflac/operations_shorthand.h index 051e432712..9afdbf2b6f 100644 --- a/src/metaflac/operations_shorthand.h +++ b/src/metaflac/operations_shorthand.h @@ -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); diff --git a/src/metaflac/operations_shorthand_vorbiscomment.c b/src/metaflac/operations_shorthand_vorbiscomment.c index 4a0a8faa8e..65728dee6b 100644 --- a/src/metaflac/operations_shorthand_vorbiscomment.c +++ b/src/metaflac/operations_shorthand_vorbiscomment.c @@ -36,11 +36,11 @@ static FLAC__bool remove_vc_all(const char *filename, FLAC__StreamMetadata *bloc static FLAC__bool remove_vc_all_except(const char *filename, FLAC__StreamMetadata *block, const char *field_name, FLAC__bool *needs_write); static FLAC__bool remove_vc_field(const char *filename, FLAC__StreamMetadata *block, const char *field_name, FLAC__bool *needs_write); static FLAC__bool remove_vc_firstfield(const char *filename, FLAC__StreamMetadata *block, const char *field_name, FLAC__bool *needs_write); -static FLAC__bool set_vc_field(const char *filename, FLAC__StreamMetadata *block, const Argument_VcField *field, FLAC__bool *needs_write, FLAC__bool raw); -static FLAC__bool import_vc_from(const char *filename, FLAC__StreamMetadata *block, const Argument_String *vc_filename, FLAC__bool *needs_write, FLAC__bool raw); -static FLAC__bool export_vc_to(const char *filename, FLAC__StreamMetadata *block, const Argument_String *vc_filename, FLAC__bool raw); +static FLAC__bool set_vc_field(const char *filename, FLAC__StreamMetadata *block, const Argument_VcField *field, FLAC__bool *needs_write, FLAC__bool raw, FLAC__bool escapes); +static FLAC__bool import_vc_from(const char *filename, FLAC__StreamMetadata *block, const Argument_String *vc_filename, FLAC__bool *needs_write, FLAC__bool raw, FLAC__bool escapes); +static FLAC__bool export_vc_to(const char *filename, FLAC__StreamMetadata *block, const Argument_String *vc_filename, FLAC__bool raw, FLAC__bool escapes); -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, const FLAC__bool escapes) { FLAC__bool ok = true, found_vc_block = false; FLAC__StreamMetadata *block = 0; @@ -83,10 +83,10 @@ FLAC__bool do_shorthand_operation__vorbis_comment(const char *filename, FLAC__bo switch(operation->type) { case OP__SHOW_VC_VENDOR: - write_vc_field(prefix_with_filename? filename : 0, &block->data.vorbis_comment.vendor_string, raw, stdout); + write_vc_field(prefix_with_filename ? filename : 0, &block->data.vorbis_comment.vendor_string, raw, escapes, stdout); break; case OP__SHOW_VC_FIELD: - write_vc_fields(prefix_with_filename? filename : 0, operation->argument.vc_field_name.value, block->data.vorbis_comment.comments, block->data.vorbis_comment.num_comments, raw, stdout); + write_vc_fields(prefix_with_filename ? filename : 0, operation->argument.vc_field_name.value, block->data.vorbis_comment.comments, block->data.vorbis_comment.num_comments, raw, escapes, stdout); break; case OP__REMOVE_VC_ALL: ok = remove_vc_all(filename, block, needs_write); @@ -102,16 +102,16 @@ FLAC__bool do_shorthand_operation__vorbis_comment(const char *filename, FLAC__bo break; case OP__SET_VC_FIELD: #ifdef _WIN32 /* do not convert anything or things will break */ - ok = set_vc_field(filename, block, &operation->argument.vc_field, needs_write, true); + ok = set_vc_field(filename, block, &operation->argument.vc_field, needs_write, true, escapes); #else - ok = set_vc_field(filename, block, &operation->argument.vc_field, needs_write, raw); + ok = set_vc_field(filename, block, &operation->argument.vc_field, needs_write, raw, escapes); #endif break; case OP__IMPORT_VC_FROM: - ok = import_vc_from(filename, block, &operation->argument.filename, needs_write, raw); + ok = import_vc_from(filename, block, &operation->argument.filename, needs_write, raw, escapes); break; case OP__EXPORT_VC_TO: - ok = export_vc_to(filename, block, &operation->argument.filename, raw); + ok = export_vc_to(filename, block, &operation->argument.filename, raw, escapes); break; default: ok = false; @@ -229,7 +229,7 @@ FLAC__bool remove_vc_firstfield(const char *filename, FLAC__StreamMetadata *bloc return true; } -FLAC__bool set_vc_field(const char *filename, FLAC__StreamMetadata *block, const Argument_VcField *field, FLAC__bool *needs_write, FLAC__bool raw) +FLAC__bool set_vc_field(const char *filename, FLAC__StreamMetadata *block, const Argument_VcField *field, FLAC__bool *needs_write, FLAC__bool raw, const FLAC__bool escapes) { FLAC__StreamMetadata_VorbisComment_Entry entry; char *converted; @@ -282,6 +282,22 @@ FLAC__bool set_vc_field(const char *filename, FLAC__StreamMetadata *block, const 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); + flac_fprintf(stderr, "%s: ERROR: unescaping file '%s' contents for tag value\n", filename, field->field_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); @@ -311,6 +327,24 @@ FLAC__bool set_vc_field(const char *filename, FLAC__StreamMetadata *block, const flac_fprintf(stderr, "%s: ERROR: converting comment '%s' to UTF-8\n", filename, field->field); return false; } + + 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 { + flac_fprintf(stderr, "%s: ERROR: unescaping comment '%s'\n", filename, field->field); + return false; + } + } + } + entry.length = strlen((const char *)entry.entry); if(!FLAC__format_vorbiscomment_entry_is_legal(entry.entry, entry.length)) { if(needs_free) @@ -337,7 +371,7 @@ FLAC__bool set_vc_field(const char *filename, FLAC__StreamMetadata *block, const } } -FLAC__bool import_vc_from(const char *filename, FLAC__StreamMetadata *block, const Argument_String *vc_filename, FLAC__bool *needs_write, FLAC__bool raw) +FLAC__bool import_vc_from(const char *filename, FLAC__StreamMetadata *block, const Argument_String *vc_filename, FLAC__bool *needs_write, FLAC__bool raw, const FLAC__bool escapes) { FILE *f; char line[65536]; @@ -384,7 +418,7 @@ FLAC__bool import_vc_from(const char *filename, FLAC__StreamMetadata *block, con ret = false; } else { - ret = set_vc_field(filename, block, &field, needs_write, raw); + ret = set_vc_field(filename, block, &field, needs_write, raw, escapes); } if(0 != field.field) free(field.field); @@ -400,7 +434,7 @@ FLAC__bool import_vc_from(const char *filename, FLAC__StreamMetadata *block, con return ret; } -FLAC__bool export_vc_to(const char *filename, FLAC__StreamMetadata *block, const Argument_String *vc_filename, FLAC__bool raw) +FLAC__bool export_vc_to(const char *filename, FLAC__StreamMetadata *block, const Argument_String *vc_filename, FLAC__bool raw, const FLAC__bool escapes) { FILE *f; FLAC__bool ret; @@ -421,7 +455,7 @@ FLAC__bool export_vc_to(const char *filename, FLAC__StreamMetadata *block, const ret = true; - write_vc_fields(0, 0, block->data.vorbis_comment.comments, block->data.vorbis_comment.num_comments, raw, f); + write_vc_fields(0, 0, block->data.vorbis_comment.comments, block->data.vorbis_comment.num_comments, raw, escapes, f); if(f != stdout) fclose(f); diff --git a/src/metaflac/options.c b/src/metaflac/options.c index a2a2a363d3..c2c09cd966 100644 --- a/src/metaflac/options.c +++ b/src/metaflac/options.c @@ -44,6 +44,7 @@ struct share__option long_options_[] = { { "with-filename", 0, 0, 0 }, { "no-filename", 0, 0, 0 }, { "no-utf8-convert", 0, 0, 0 }, + { "escapes", 0, 0, 0 }, { "dont-use-padding", 0, 0, 0 }, { "no-cued-seekpoints", 0, 0, 0 }, /* shorthand operations */ @@ -136,6 +137,7 @@ void init_options(CommandLineOptions *options) options->prefix_with_filename = 2; options->utf8_convert = true; + options->escapes = false; options->use_padding = true; options->cued_seekpoints = true; options->show_long_help = false; @@ -398,6 +400,9 @@ FLAC__bool parse_option(int option_index, const char *option_argument, CommandLi else if(0 == strcmp(opt, "no-utf8-convert")) { options->utf8_convert = false; } + else if(0 == strcmp(opt, "escapes")) { + options->escapes = true; + } else if(0 == strcmp(opt, "dont-use-padding")) { options->use_padding = false; } diff --git a/src/metaflac/options.h b/src/metaflac/options.h index cbf350dc97..e44d7a6ed1 100644 --- a/src/metaflac/options.h +++ b/src/metaflac/options.h @@ -187,6 +187,7 @@ typedef struct { FLAC__bool preserve_modtime; FLAC__bool prefix_with_filename; FLAC__bool utf8_convert; + FLAC__bool escapes; /* Use \n-style escapes to allow multiline comments. */ FLAC__bool use_padding; FLAC__bool cued_seekpoints; FLAC__bool show_long_help; diff --git a/src/metaflac/usage.c b/src/metaflac/usage.c index feb9093d2a..1050e72050 100644 --- a/src/metaflac/usage.c +++ b/src/metaflac/usage.c @@ -73,6 +73,7 @@ static void usage_summary(FILE *out) flac_fprintf(out, "--no-utf8-convert Do not convert tags from UTF-8 to local charset,\n"); flac_fprintf(out, " or vice versa. This is useful for scripts, and setting\n"); flac_fprintf(out, " tags in situations where the locale is wrong.\n"); + flac_fprintf(out, "--escapes Use \\n-style escapes to allow multiline comments.\n"); flac_fprintf(out, "--dont-use-padding By default metaflac tries to use padding where possible\n"); flac_fprintf(out, " to avoid rewriting the entire file if the metadata size\n"); flac_fprintf(out, " changes. Use this option to tell metaflac to not take\n"); @@ -150,13 +151,13 @@ int long_usage(const char *message, ...) flac_fprintf(out, " blocks for that.\n"); flac_fprintf(out, "--import-tags-from=FILE Import tags from a file. Use '-' for stdin. Each line\n"); flac_fprintf(out, " should be of the form NAME=VALUE. Multi-line comments\n"); - flac_fprintf(out, " are currently not supported. Specify --remove-all-tags\n"); + flac_fprintf(out, " are supported with --escapes. Specify --remove-all-tags\n"); flac_fprintf(out, " and/or --no-utf8-convert before --import-tags-from if\n"); flac_fprintf(out, " necessary. If FILE is '-' (stdin), only one FLAC file\n"); flac_fprintf(out, " may be specified.\n"); flac_fprintf(out, "--export-tags-to=FILE Export tags to a file. Use '-' for stdout. Each line\n"); flac_fprintf(out, " will be of the form NAME=VALUE. Specify\n"); - flac_fprintf(out, " --no-utf8-convert if necessary.\n"); + flac_fprintf(out, " --escapes and/or --no-utf8-convert if necessary.\n"); flac_fprintf(out, "--import-cuesheet-from=FILE Import a cuesheet from a file. Use '-' for stdin.\n"); flac_fprintf(out, " Only one FLAC file may be specified. A seekpoint will be\n"); flac_fprintf(out, " added for each index point in the cuesheet to the\n"); diff --git a/src/metaflac/utils.c b/src/metaflac/utils.c index fb2be516c6..03a9e117b0 100644 --- a/src/metaflac/utils.c +++ b/src/metaflac/utils.c @@ -32,6 +32,7 @@ #include "share/safe_str.h" #include "share/utf8.h" #include "share/compat.h" +#include "share/grabbag.h" void die(const char *message) { @@ -233,11 +234,29 @@ FLAC__bool parse_vorbis_comment_field(const char *field_ref, char **field, char return true; } -void write_vc_field(const char *filename, const FLAC__StreamMetadata_VorbisComment_Entry *entry, FLAC__bool raw, FILE *f) +void write_vc_field(const char *filename, const FLAC__StreamMetadata_VorbisComment_Entry *entry, FLAC__bool raw, const FLAC__bool escapes, FILE *f) { - if(0 != entry->entry) { - if(filename) + if(NULL != entry->entry) { + const char *entry_str = (const char *)entry->entry; + size_t entry_length = entry->length; + char *allocated_str = NULL; + + if(filename) { flac_fprintf(f, "%s:", filename); + } + + if(escapes) { + if(grabbag__escape_string_needed(entry_str, entry_length)) { + allocated_str = grabbag__create_escaped_string(entry_str, entry_length); + if(allocated_str != NULL) { + entry_str = allocated_str; + entry_length = strlen(allocated_str); + } + else { + /* error is ignored, the original value will be used */ + } + } + } if(!raw) { /* @@ -245,21 +264,26 @@ void write_vc_field(const char *filename, const FLAC__StreamMetadata_VorbisComme * be truncated by utf_decode(). */ #ifdef _WIN32 - flac_fprintf(f, "%s", entry->entry); + flac_fprintf(f, "%s", entry_str); #else char *converted; - if(utf8_decode((const char *)entry->entry, &converted) >= 0) { + if(utf8_decode(entry_str, &converted) >= 0) { (void) local_fwrite(converted, 1, strlen(converted), f); free(converted); } else { - (void) local_fwrite(entry->entry, 1, entry->length, f); + (void)local_fwrite(entry_str, 1, entry_length, f); } #endif } else { - (void) local_fwrite(entry->entry, 1, entry->length, f); + (void)local_fwrite(entry_str, 1, entry_length, f); + } + + if(allocated_str) { + free(allocated_str); + allocated_str = NULL; } } #ifdef _WIN32 @@ -269,13 +293,13 @@ void write_vc_field(const char *filename, const FLAC__StreamMetadata_VorbisComme #endif } -void write_vc_fields(const char *filename, const char *field_name, const FLAC__StreamMetadata_VorbisComment_Entry entry[], unsigned num_entries, FLAC__bool raw, FILE *f) +void write_vc_fields(const char *filename, const char *field_name, const FLAC__StreamMetadata_VorbisComment_Entry entry[], unsigned num_entries, FLAC__bool raw, const FLAC__bool escapes, FILE *f) { unsigned i; const unsigned field_name_length = (0 != field_name)? strlen(field_name) : 0; for(i = 0; i < num_entries; i++) { if(0 == field_name || FLAC__metadata_object_vorbiscomment_entry_matches(entry[i], field_name, field_name_length)) - write_vc_field(filename, entry + i, raw, f); + write_vc_field(filename, entry + i, raw, escapes, f); } } diff --git a/src/metaflac/utils.h b/src/metaflac/utils.h index c2dfd7efaa..dbb9874840 100644 --- a/src/metaflac/utils.h +++ b/src/metaflac/utils.h @@ -41,7 +41,7 @@ void print_error_with_chain_status(FLAC__Metadata_Chain *chain, const char *form FLAC__bool parse_vorbis_comment_field(const char *field_ref, char **field, char **name, char **value, unsigned *length, const char **violation); -void write_vc_field(const char *filename, const FLAC__StreamMetadata_VorbisComment_Entry *entry, FLAC__bool raw, FILE *f); -void write_vc_fields(const char *filename, const char *field_name, const FLAC__StreamMetadata_VorbisComment_Entry entry[], unsigned num_entries, FLAC__bool raw, FILE *f); +void write_vc_field(const char *filename, const FLAC__StreamMetadata_VorbisComment_Entry *entry, FLAC__bool raw, FLAC__bool escapes, FILE *f); +void write_vc_fields(const char *filename, const char *field_name, const FLAC__StreamMetadata_VorbisComment_Entry entry[], unsigned num_entries, FLAC__bool raw, FLAC__bool escapes, FILE *f); #endif diff --git a/src/share/Makefile.am b/src/share/Makefile.am index 25cd5d9f69..52d20a9906 100644 --- a/src/share/Makefile.am +++ b/src/share/Makefile.am @@ -54,6 +54,7 @@ getopt_libgetopt_la_SOURCES = getopt/getopt.c getopt/getopt1.c grabbag_libgrabbag_la_SOURCES = \ grabbag/alloc.c \ grabbag/cuesheet.c \ + grabbag/escapes.c \ grabbag/file.c \ grabbag/picture.c \ grabbag/replaygain.c \ diff --git a/src/share/grabbag/CMakeLists.txt b/src/share/grabbag/CMakeLists.txt index 203ae3f4c2..4246a014d6 100644 --- a/src/share/grabbag/CMakeLists.txt +++ b/src/share/grabbag/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(grabbag STATIC alloc.c cuesheet.c + escapes.c file.c picture.c replaygain.c diff --git a/src/share/grabbag/escapes.c b/src/share/grabbag/escapes.c new file mode 100644 index 0000000000..130b3dbfe4 --- /dev/null +++ b/src/share/grabbag/escapes.c @@ -0,0 +1,199 @@ +/* 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 + */ + +#include +#include +#include "share/grabbag.h" + +FLAC__bool grabbag__escape_string_needed(const char *src, size_t src_size) +{ + for(size_t s = 0; s < src_size; ++s) { + if((src[s] == '\\') || + (src[s] == '\r') || + (src[s] == '\n')) { + return true; + } + } + return false; +} + +static size_t grabbag__escape_string_size(const char *src, size_t src_size) +{ + size_t dst_size = src_size; + for(size_t s = 0; s < src_size; ++s) { + if((src[s] == '\\') || + (src[s] == '\r') || + (src[s] == '\n')) { + ++dst_size; + } + } + return dst_size; +} + +static size_t grabbag__escape_string(char *dst, size_t dst_size, const char *src, size_t src_size, FLAC__bool *error) +{ + size_t d = 0; + size_t s = 0; + *error = false; + + while((d < dst_size) && (s < src_size)) { + switch(src[s]) { + case '\\': + if(d + 1 < dst_size) { + dst[d] = '\\'; + dst[d + 1] = '\\'; + } + d += 2; + break; + case '\r': + if(d + 1 < dst_size) { + dst[d] = '\\'; + dst[d + 1] = 'r'; + } + d += 2; + break; + case '\n': + if(d + 1 < dst_size) { + dst[d] = '\\'; + dst[d + 1] = 'n'; + } + d += 2; + break; + default: + dst[d++] = src[s]; + break; + } + ++s; + } + + if((d > dst_size) || (s != src_size)) { + /* dst ran out of space, or src was partially read */ + *error = true; + } + + return d; +} + +char *grabbag__create_escaped_string(const char *src, size_t src_size) +{ + const size_t dst_size = grabbag__escape_string_size(src, src_size); + char *dst = malloc(dst_size + 1); /* +1 for null terminator */ + if(dst != NULL) { + FLAC__bool error = false; + const size_t size = grabbag__escape_string(dst, dst_size, src, src_size, &error); + if(error || (size > dst_size)) { + free(dst); + dst = NULL; + } + else { + /* Add null terminator */ + dst[size] = '\0'; + } + } + return dst; +} + +FLAC__bool grabbag__unescape_string_needed(const char *src, size_t src_size) +{ + for(size_t s = 0; s < src_size; ++s) { + if(src[s] == '\\') { + return true; + } + } + return false; +} + +static size_t grabbag__unescape_string_size(const char *src, size_t src_size) +{ + size_t dst_size = 0; + size_t s = 0; + while(s < src_size) { + if(src[s] == '\\') { + s += 2; + } + else { + ++s; + } + ++dst_size; + } + return dst_size; +} + +static size_t grabbag__unescape_string(char *dst, size_t dst_size, const char *src, size_t src_size, FLAC__bool *error) +{ + size_t d = 0; + size_t s = 0; + *error = false; + + while((d < dst_size) && (s < src_size)) { + if(src[s] == '\\') { + ++s; + if(s < src_size) { + switch(src[s]) { + case '\\': + dst[d++] = '\\'; + break; + case 'r': + dst[d++] = '\r'; + break; + case 'n': + dst[d++] = '\n'; + break; + default: + /* Unsupported escape character */ + *error = true; + break; + } + ++s; + } + else { + /* Truncated escape character */ + *error = true; + } + } + else { + dst[d++] = src[s++]; + } + } + + if((d > dst_size) || (s != src_size)) { + /* dst ran out of space, or src was partially read */ + *error = true; + } + + return d; +} + +char *grabbag__create_unescaped_string(const char *src, size_t src_size) +{ + const size_t dst_size = grabbag__unescape_string_size(src, src_size); + char *dst = malloc(dst_size + 1); /* +1 for null terminator */ + if(dst != NULL) { + FLAC__bool error = false; + const size_t size = grabbag__unescape_string(dst, dst_size, src, src_size, &error); + if(error || (size > dst_size)) { + free(dst); + dst = NULL; + } + else { + /* Add null terminator */ + dst[size] = '\0'; + } + } + return dst; +} diff --git a/src/test_grabbag/CMakeLists.txt b/src/test_grabbag/CMakeLists.txt index 56abe81094..e71d79f53c 100644 --- a/src/test_grabbag/CMakeLists.txt +++ b/src/test_grabbag/CMakeLists.txt @@ -1,2 +1,4 @@ add_subdirectory(cuesheet) add_subdirectory(picture) +add_subdirectory(escapes) + diff --git a/src/test_grabbag/Makefile.am b/src/test_grabbag/Makefile.am index 9fbdc02dbb..b805e068b4 100644 --- a/src/test_grabbag/Makefile.am +++ b/src/test_grabbag/Makefile.am @@ -16,7 +16,7 @@ # restrictive of those mentioned above. See the file COPYING.Xiph in this # distribution. -SUBDIRS = cuesheet picture +SUBDIRS = cuesheet picture escapes EXTRA_DIST = \ CMakeLists.txt diff --git a/src/test_grabbag/escapes/CMakeLists.txt b/src/test_grabbag/escapes/CMakeLists.txt new file mode 100644 index 0000000000..32dacfc560 --- /dev/null +++ b/src/test_grabbag/escapes/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(test_escapes + main.c) +target_link_libraries(test_escapes FLAC grabbag) diff --git a/src/test_grabbag/escapes/Makefile.am b/src/test_grabbag/escapes/Makefile.am new file mode 100644 index 0000000000..4b49133842 --- /dev/null +++ b/src/test_grabbag/escapes/Makefile.am @@ -0,0 +1,30 @@ +# test_escapes - Simple tester for escapes routines in grabbag +# Copyright (C) 2026 Xiph.Org Foundation +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +EXTRA_DIST = \ + CMakeLists.txt + +AM_CPPFLAGS = -I$(top_builddir) -I$(srcdir)/include -I$(top_srcdir)/include +check_PROGRAMS = test_escapes +test_escapes_SOURCES = \ + main.c + +test_escapes_LDADD = \ + $(top_builddir)/src/share/grabbag/libgrabbag.la \ + $(top_builddir)/src/libFLAC/libFLAC.la + +CLEANFILES = test_escapes.exe diff --git a/src/test_grabbag/escapes/main.c b/src/test_grabbag/escapes/main.c new file mode 100644 index 0000000000..2bd84212aa --- /dev/null +++ b/src/test_grabbag/escapes/main.c @@ -0,0 +1,389 @@ +/* test_escapes - Simple tester for escapes routines in grabbag + * Copyright (C) 2026 Xiph.Org Foundation + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include "FLAC/assert.h" +#include "share/grabbag.h" + +static void print_hexdump(const char *str, size_t length) +{ + size_t i; + for(i = 0; i < length; ++i) { + unsigned char c = (unsigned char)str[i]; + if((c >= 0x20) && (c <= 0x7E)) { + printf("%c", c); + } + else { + printf("\\x%.2x", c); + } + } +} + +static FLAC__bool test_escape() +{ + static const struct + { + const char *unescaped; + const char *escaped; + } test_data[] = { + { "\\", "\\\\" }, + { "\r", "\\r" }, + { "\n", "\\n" }, + + /* one character at front */ + { "a" + "\\", + "a" + "\\\\" }, + { "a" + "\r", + "a" + "\\r" }, + { "a" + "\n", + "a" + "\\n" }, + + /* one character at end */ + { "\\" + "a", + "\\\\" + "a" }, + { "\r" + "a", + "\\r" + "a" }, + { "\n" + "a", + "\\n" + "a" }, + + /* escape in the middle */ + { "a\\a", "a\\\\a" }, + { "a\ra", "a\\ra" }, + { "a\na", "a\\na" }, + + /* characters in the middle */ + { "\\a\\", "\\\\a\\\\" }, + { "\ra\r", "\\ra\\r" }, + { "\na\n", "\\na\\n" }, + + /* double escaped */ + { "\\\\\\", "\\\\\\\\\\\\" }, + { "\\r", "\\\\r" }, + { "\\n", "\\\\n" }, + }; + const size_t test_data_size = sizeof(test_data) / sizeof(test_data[0]); + size_t i; + FLAC__bool result = true; + + for(i = 0; i < test_data_size; ++i) { + char *escaped = NULL; + char *unescaped = NULL; + FLAC__bool test_result = true; + + printf("Test escape %u:", (unsigned)i); + + if(!grabbag__escape_string_needed(test_data[i].unescaped, strlen(test_data[i].unescaped))) { + test_result = false; + printf(" ERROR: expected grabbag__escape_string_needed() to return true, but it didn't\n"); + printf("\n"); + } + + escaped = grabbag__create_escaped_string(test_data[i].unescaped, strlen(test_data[i].unescaped)); + if(escaped != NULL) { + const size_t expected_escaped_size = strlen(test_data[i].escaped); + const size_t escaped_size = strlen(escaped); + if((expected_escaped_size != escaped_size) || + (0 != memcmp(escaped, test_data[i].escaped, escaped_size))) { + printf(" ERROR: grabbag__create_escaped_string() mismatch:\n"); + printf(" Expected: "); + print_hexdump(escaped, expected_escaped_size); + printf("\n"); + printf(" Got : "); + print_hexdump(test_data[i].escaped, escaped_size); + printf("\n"); + test_result = false; + } + free(escaped); + escaped = NULL; + } + else { + printf(" ERROR: grabbag__create_escaped_string() returned NULL."); + test_result = false; + } + + if(!grabbag__unescape_string_needed(test_data[i].escaped, strlen(test_data[i].escaped))) { + test_result = false; + printf(" ERROR: expected grabbag__unescape_string_needed() to return true, but it didn't\n"); + printf("\n"); + } + + unescaped = grabbag__create_unescaped_string(test_data[i].escaped, strlen(test_data[i].escaped)); + if(unescaped != NULL) { + const size_t expected_unescaped_size = strlen(test_data[i].unescaped); + const size_t unescaped_size = strlen(unescaped); + if((expected_unescaped_size != unescaped_size) || + (0 != memcmp(unescaped, test_data[i].unescaped, unescaped_size))) { + printf(" ERROR: grabbag__create_unescaped_string() mismatch:\n"); + printf(" Expected: "); + print_hexdump(unescaped, expected_unescaped_size); + printf("\n"); + printf(" Got : "); + print_hexdump(test_data[i].unescaped, unescaped_size); + printf("\n"); + test_result = false; + } + free(unescaped); + unescaped = NULL; + } + else { + printf(" ERROR: grabbag__create_unescaped_string() returned NULL.\n"); + test_result = false; + } + + if(test_result) { + printf(" OK\n"); + } + else { + result = false; + } + } + + return result; +} + +static FLAC__bool test_escape_not_needed() +{ + static const struct + { + const char *str; + } test_data[] = { + { "" }, + { "a" }, + { "r" }, + { "n" }, + { "0" }, + { "=" }, + { "A Longer String" }, + { "\t" }, + { "\v" }, + { "\a" }, + { "\b" }, + { "\xFF" }, + }; + const size_t test_data_size = sizeof(test_data) / sizeof(test_data[0]); + size_t i; + FLAC__bool result = true; + + for(i = 0; i < test_data_size; ++i) { + const char *str = test_data[i].str; + const size_t str_size = strlen(test_data[i].str); + char *escaped = NULL; + char *unescaped = NULL; + FLAC__bool test_result = true; + + printf("Test escape not needed %u:", (unsigned)i); + + if(grabbag__escape_string_needed(str, str_size)) { + test_result = false; + printf(" ERROR: expected grabbag__escape_string_needed() to return false, but it didn't\n"); + printf("\n"); + } + + escaped = grabbag__create_escaped_string(str, str_size); + if(escaped != NULL) { + const size_t expected_escaped_size = str_size; + const size_t escaped_size = strlen(escaped); + if((expected_escaped_size != escaped_size) || + (0 != memcmp(escaped, str, escaped_size))) { + printf(" ERROR: grabbag__create_escaped_string() mismatch:\n"); + printf(" Expected: "); + print_hexdump(escaped, expected_escaped_size); + printf("\n"); + printf(" Got : "); + print_hexdump(str, str_size); + printf("\n"); + test_result = false; + } + free(escaped); + escaped = NULL; + } + else { + printf(" ERROR: grabbag__create_escaped_string() returned NULL."); + test_result = false; + } + + if(grabbag__unescape_string_needed(str, str_size)) { + test_result = false; + printf(" ERROR: expected grabbag__unescape_string_needed() to return false, but it didn't\n"); + printf("\n"); + } + + unescaped = grabbag__create_unescaped_string(str, str_size); + if(unescaped != NULL) { + const size_t expected_unescaped_size = str_size; + const size_t unescaped_size = strlen(unescaped); + if((expected_unescaped_size != unescaped_size) || + (0 != memcmp(unescaped, str, unescaped_size))) { + printf(" ERROR: grabbag__create_unescaped_string() mismatch:\n"); + printf(" Expected: "); + print_hexdump(unescaped, expected_unescaped_size); + printf("\n"); + printf(" Got : "); + print_hexdump(str, str_size); + printf("\n"); + test_result = false; + } + free(unescaped); + unescaped = NULL; + } + else { + printf(" ERROR: grabbag__create_unescaped_string() returned NULL.\n"); + test_result = false; + } + + if(test_result) { + printf(" OK\n"); + } + else { + result = false; + } + } + + return result; +} + +static FLAC__bool test_invalid_escape() +{ + static const struct + { + const char *escaped; + } test_data[] = { + /* invalid escape */ + { "\\x20" }, + { "\\x{20}" }, + { "\\X20" }, + { "\\X{20}" }, + { "\\0" }, + { "\\01" }, + { "\\012" }, + { "\\1" }, + { "\\12" }, + { "\\123" }, + { "\\2" }, + { "\\3" }, + { "\\4" }, + { "\\5" }, + { "\\6" }, + { "\\7" }, + { "\\8" }, + { "\\9" }, + { "\\u0020" }, + { "\\u{20}" }, + { "\\U00000020" }, + { "\\U{20}" }, + { "\\a" }, + { "\\b" }, + { "\\c" }, + { "\\e" }, + { "\\E" }, + { "\\f" }, + { "\\t" }, + { "\\v" }, + { "\\\"" }, + { "\\'" }, + { "\\\n" }, + { "\\\r" }, + { "\\?" }, + + /* truncated escape */ + { "\\" }, + { "a\\" }, + { "a\\n\\" }, + }; + const size_t test_data_size = sizeof(test_data) / sizeof(test_data[0]); + size_t i; + FLAC__bool result = true; + + for(i = 0; i < test_data_size; ++i) { + FLAC__bool test_result = true; + char *unescaped = NULL; + + printf("Test invalid escape %u:", (unsigned)i); + + if(!grabbag__unescape_string_needed(test_data[i].escaped, strlen(test_data[i].escaped))) { + test_result = false; + printf(" ERROR: expected grabbag__unescape_string_needed() to return true, but it didn't\n"); + printf("\n"); + } + + unescaped = grabbag__create_unescaped_string(test_data[i].escaped, strlen(test_data[i].escaped)); + if(unescaped != NULL) { + test_result = false; + printf(" ERROR: expected grabbag__create_unescaped_string() to fail, but it didn't\n"); + printf("Unescaped mismatch:\n"); + printf(" Expected: NULL\n"); + printf(" Got : "); + print_hexdump(unescaped, strlen(unescaped)); + printf("\n"); + free(unescaped); + unescaped = NULL; + } + + if(test_result) { + printf(" OK\n"); + } + else { + result = false; + } + } + + return result; +} + +int main(int argc, char *argv[]) +{ + const char *usage = "usage: test_escapes\n"; + + if(argc > 1 && 0 == strcmp(argv[1], "-h")) { + puts(usage); + return 0; + } + + if(!test_escape()) { + return 1; + } + + if(!test_escape_not_needed()) { + return 1; + } + + if(!test_invalid_escape()) { + return 1; + } + + return 0; +} diff --git a/test/Makefile.am b/test/Makefile.am index 382a6d7601..53727b807b 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -55,6 +55,7 @@ EXTRA_DIST = \ metaflac.flac.in \ metaflac.flac.ok \ picture.ok \ + escapes.ok \ $(check_SCRIPTS) \ write_iff.pl diff --git a/test/escapes.ok b/test/escapes.ok new file mode 100644 index 0000000000..456a3929fa --- /dev/null +++ b/test/escapes.ok @@ -0,0 +1,68 @@ +Test escape 0: OK +Test escape 1: OK +Test escape 2: OK +Test escape 3: OK +Test escape 4: OK +Test escape 5: OK +Test escape 6: OK +Test escape 7: OK +Test escape 8: OK +Test escape 9: OK +Test escape 10: OK +Test escape 11: OK +Test escape 12: OK +Test escape 13: OK +Test escape 14: OK +Test escape 15: OK +Test escape 16: OK +Test escape 17: OK +Test escape not needed 0: OK +Test escape not needed 1: OK +Test escape not needed 2: OK +Test escape not needed 3: OK +Test escape not needed 4: OK +Test escape not needed 5: OK +Test escape not needed 6: OK +Test escape not needed 7: OK +Test escape not needed 8: OK +Test escape not needed 9: OK +Test escape not needed 10: OK +Test escape not needed 11: OK +Test invalid escape 0: OK +Test invalid escape 1: OK +Test invalid escape 2: OK +Test invalid escape 3: OK +Test invalid escape 4: OK +Test invalid escape 5: OK +Test invalid escape 6: OK +Test invalid escape 7: OK +Test invalid escape 8: OK +Test invalid escape 9: OK +Test invalid escape 10: OK +Test invalid escape 11: OK +Test invalid escape 12: OK +Test invalid escape 13: OK +Test invalid escape 14: OK +Test invalid escape 15: OK +Test invalid escape 16: OK +Test invalid escape 17: OK +Test invalid escape 18: OK +Test invalid escape 19: OK +Test invalid escape 20: OK +Test invalid escape 21: OK +Test invalid escape 22: OK +Test invalid escape 23: OK +Test invalid escape 24: OK +Test invalid escape 25: OK +Test invalid escape 26: OK +Test invalid escape 27: OK +Test invalid escape 28: OK +Test invalid escape 29: OK +Test invalid escape 30: OK +Test invalid escape 31: OK +Test invalid escape 32: OK +Test invalid escape 33: OK +Test invalid escape 34: OK +Test invalid escape 35: OK +Test invalid escape 36: OK +Test invalid escape 37: OK diff --git a/test/flac-to-flac-metadata-test-files/Makefile.am b/test/flac-to-flac-metadata-test-files/Makefile.am index c1a787804a..d3f86bba81 100644 --- a/test/flac-to-flac-metadata-test-files/Makefile.am +++ b/test/flac-to-flac-metadata-test-files/Makefile.am @@ -26,6 +26,7 @@ EXTRA_DIST = \ case02a-expect.meta \ case02b-expect.meta \ case02c-expect.meta \ + case02d-expect.meta \ case03a-expect.meta \ case03b-expect.meta \ case03c-expect.meta \ diff --git a/test/flac-to-flac-metadata-test-files/case02d-expect.meta b/test/flac-to-flac-metadata-test-files/case02d-expect.meta new file mode 100644 index 0000000000..cfdba73539 --- /dev/null +++ b/test/flac-to-flac-metadata-test-files/case02d-expect.meta @@ -0,0 +1,80 @@ +METADATA block #0 + type: 0 (STREAMINFO) + is last: false + length: XXX + sample_rate: 44100 Hz + channels: 2 + bits-per-sample: 16 + total samples: 5880 + MD5 signature: 74ffd4737eb5488d512be4af58943362 +METADATA block #1 + type: 4 (VORBIS_COMMENT) + is last: false + length: XXX + comments: 1 + comment[0]: artist=Line1 +Line\2 Line3 +METADATA block #2 + type: 3 (SEEKTABLE) + is last: false + length: XXX + seek points: 10 + point 0: sample_number=0 + point 1: sample_number=4096 + point 2: PLACEHOLDER + point 3: PLACEHOLDER + point 4: PLACEHOLDER + point 5: PLACEHOLDER + point 6: PLACEHOLDER + point 7: PLACEHOLDER + point 8: PLACEHOLDER + point 9: PLACEHOLDER +METADATA block #3 + type: 5 (CUESHEET) + is last: false + length: XXX + media catalog number: 1234567890123 + lead-in: 88200 + is CD: true + number of tracks: 3 + track[0] + offset: 0 + number: 1 + ISRC: + type: AUDIO + pre-emphasis: false + number of index points: 2 + index[0] + offset: 0 + number: 1 + index[1] + offset: 588 + number: 2 + track[1] + offset: 2940 + number: 2 + ISRC: + type: AUDIO + pre-emphasis: false + number of index points: 1 + index[0] + offset: 0 + number: 1 + track[2] + offset: 5880 + number: 170 (LEAD-OUT) +METADATA block #4 + type: 2 (APPLICATION) + is last: false + length: XXX + application ID: 66616b65 + data contents: +METADATA block #5 + type: 126 (UNKNOWN) + is last: false + length: XXX + data contents: +METADATA block #6 + type: 1 (PADDING) + is last: true + length: XXX diff --git a/test/metaflac-test-files/Makefile.am b/test/metaflac-test-files/Makefile.am index a6a9b9bd29..85adea7a90 100644 --- a/test/metaflac-test-files/Makefile.am +++ b/test/metaflac-test-files/Makefile.am @@ -84,7 +84,8 @@ EXTRA_DIST = \ case64-expect.meta \ case65-expect.meta \ case66-expect.meta \ - case67-expect.meta + case67-expect.meta \ + case68-expect.meta clean-local: -rm -f out.* diff --git a/test/metaflac-test-files/case68-expect.meta b/test/metaflac-test-files/case68-expect.meta new file mode 100644 index 0000000000..a84da35ec0 --- /dev/null +++ b/test/metaflac-test-files/case68-expect.meta @@ -0,0 +1,92 @@ +METADATA block #0 + type: 0 (STREAMINFO) + is last: false + length: XXX + sample_rate: 44100 Hz + channels: 1 + bits-per-sample: 8 + total samples: 1 + MD5 signature: 8d39dd7eef115ea6975446ef4082951f +METADATA block #1 + type: 126 (UNKNOWN) + is last: false + length: XXX + data contents: +METADATA block #2 + type: 5 (CUESHEET) + is last: false + length: XXX + media catalog number: + lead-in: 88200 + is CD: true + number of tracks: 2 + track[0] + offset: 0 + number: 1 + ISRC: + type: AUDIO + pre-emphasis: false + number of index points: 1 + index[0] + offset: 0 + number: 1 + track[1] + offset: 1 + number: 170 (LEAD-OUT) +METADATA block #3 + type: 2 (APPLICATION) + is last: false + length: XXX + application ID: 61626364 + data contents: +calfflacMETADATA block #4 + type: 3 (SEEKTABLE) + is last: false + length: XXX + seek points: 1 + point 0: sample_number=0 +METADATA block #5 + type: 4 (VORBIS_COMMENT) + is last: false + length: XXX + comments: 2 + comment[0]: ARTIST=Line1 +Line\2 Line3 + comment[1]: TITLE=Line1 +Line\2 Line3 +METADATA block #6 + type: 126 (UNKNOWN) + is last: false + length: XXX + data contents: +METADATA block #7 + type: 5 (CUESHEET) + is last: false + length: XXX + media catalog number: + lead-in: 88200 + is CD: true + number of tracks: 2 + track[0] + offset: 0 + number: 1 + ISRC: + type: AUDIO + pre-emphasis: false + number of index points: 1 + index[0] + offset: 0 + number: 1 + track[1] + offset: 1 + number: 170 (LEAD-OUT) +METADATA block #8 + type: 2 (APPLICATION) + is last: false + length: XXX + application ID: 61626364 + data contents: +calfflacMETADATA block #9 + type: 1 (PADDING) + is last: true + length: XXX diff --git a/test/test_flac.sh b/test/test_flac.sh index 63693e53c2..ecd84c8848 100755 --- a/test/test_flac.sh +++ b/test/test_flac.sh @@ -1281,6 +1281,33 @@ flac2flac input-SCPAP.flac case02a "" flac2flac input-SCPAP.flac case02b "--tag=artist=0" # case 02c: on file with VORBIS_COMMENT block and --tag, replace existing VORBIS_COMMENT with new tags flac2flac input-SCVAUP.flac case02c "--tag=artist=0" +# case 02d: on file with VORBIS_COMMENT block and --escapes --tag, replace existing VORBIS_COMMENT with new tags +flac2flac input-SCVAUP.flac case02d "--escapes --tag=artist=Line1\\nLine\\\\2\\rLine3" +# case 02d: on file with VORBIS_COMMENT block and --escapes --tag-from-file, replace existing VORBIS_COMMENT with new tags +printf 'Line1\\nLine\\\\2\\rLine3' >vc_escapes.txt +flac2flac input-SCVAUP.flac case02d "--escapes --tag-from-file=artist=vc_escapes.txt" +rm vc_escapes.txt +# case 02d: on file with VORBIS_COMMENT block and --tag-from-file, replace existing VORBIS_COMMENT with new tags +printf 'Line1\nLine\\2\rLine3' >vc_no_escapes.txt +flac2flac input-SCVAUP.flac case02d "--tag-from-file=artist=vc_no_escapes.txt" +rm vc_no_escapes.txt + +# test with unsupported escapes, --tag +if run_flac -f -o out.flac --escapes --tag="ARTIST=\\t\\123\\x12\\u1234" input-SCVAUP.flac ; then + die "ERROR: flac --escapes --tag with unsupported escapes should have failed but didn't" +else + echo "OK, it failed as it should" +fi + +# test with unsupported escapes, --tag-from-file +printf 'ARTIST=\\t\\123\\x12\\u1234\n' >vc_escapes_unsupported.txt +if run_flac -f -o out.flac --escapes --tag-from-file=ARTIST=vc_escapes_unsupported.txt input-SCVAUP.flac ; then + die "ERROR: flac --escapes --tag-from-file with unsupported escapes should have failed but didn't" +else + echo "OK, it failed as it should" +fi +rm vc_escapes_unsupported.txt + # case 03a: on file with no CUESHEET block and --cuesheet specified, add it flac2flac input-SVAUP.flac case03a "--cuesheet=$testdatadir/input0.cue" # case 03b: on file with CUESHEET block and --cuesheet specified, overwrite existing CUESHEET diff --git a/test/test_grabbag.sh b/test/test_grabbag.sh index aa29f02bdc..3425aaee1c 100755 --- a/test/test_grabbag.sh +++ b/test/test_grabbag.sh @@ -22,10 +22,12 @@ PATH=../src/test_grabbag/cuesheet:$PATH PATH=../src/test_grabbag/picture:$PATH +PATH=../src/test_grabbag/escapes:$PATH PATH=../objs/$BUILD/bin:$PATH test_cuesheet -h 1>/dev/null 2>/dev/null || die "ERROR can't find test_cuesheet executable" test_picture -h 1>/dev/null 2>/dev/null || die "ERROR can't find test_picture executable" +test_escapes -h 1>/dev/null 2>/dev/null || die "ERROR can't find test_escapes executable" run_test_cuesheet () { @@ -47,6 +49,16 @@ run_test_picture () fi } +run_test_escapes () +{ + if [ "$FLAC__TEST_WITH_VALGRIND" = yes ] ; then + echo "valgrind --leak-check=yes --show-reachable=yes --num-callers=50 test_escapes $*" >>test_grabbag.valgrind.log + valgrind --leak-check=yes --show-reachable=yes --num-callers=50 --log-fd=4 test_escapes${EXE} $* 4>>test_grabbag.valgrind.log + else + test_escapes${EXE} $* + fi +} + ######################################################################## # # test_picture @@ -126,3 +138,27 @@ else fi echo "PASSED (results are in $log)" + + +######################################################################## +# +# test_escapes +# +######################################################################## + +log=escapes.log +escapes_dir=${top_srcdir}/test/escapes + +echo "Running test_escapes..." + +rm -f $log + +run_test_escapes $escapes_dir >> $log 2>&1 + +if [ $is_win = yes ] ; then + diff -w ${top_srcdir}/test/escapes.ok $log > escapes.diff || die "Error: .log file does not match .ok file, see escapes.diff" +else + diff ${top_srcdir}/test/escapes.ok $log > escapes.diff || die "Error: .log file does not match .ok file, see escapes.diff" +fi + +echo "PASSED (results are in $log)" diff --git a/test/test_metaflac.sh b/test/test_metaflac.sh index 4ff8fcd2a4..3024365707 100755 --- a/test/test_metaflac.sh +++ b/test/test_metaflac.sh @@ -569,4 +569,89 @@ mv $flacfile_secondary $flacfile check_flac metaflac_test_nofilter case67 "-o --append --block-number=0" "--list" + +# Escapes test + +# Set tags from cmdline with --escapes +run_metaflac --remove-all-tags $flacfile +run_metaflac --escapes --set-tag="ARTIST=Line1\\nLine\\\\2\\rLine3" --set-tag="TITLE=Line1\\nLine\\\\2\\rLine3" $flacfile +check_flac +metaflac_test case68 "--escapes --set-tag" "--list" + +# Set tags with unsupported escape sequences +if run_metaflac --escapes --set-tag="ARTIST=\\t\\123\\x12\\u1234" $flacfile ; then + die "ERROR: metaflac --escapes --set-tag with unsupported escapes should have failed but didn't" +else + echo "OK, it failed as it should" +fi +check_flac + +# Import tags from stdin with unsupported escape sequences +if printf 'ARTIST=\\t\\123\\x12\\u1234\n' | run_metaflac --escapes --import-tags-from=- $flacfile ; then + die "ERROR: metaflac --escapes --import-tags-from=- with unsupported escapes should have failed but didn't" +else + echo "OK, it failed as it should" +fi +check_flac + +# Import tags from FILE with unsupported escape sequences +printf 'ARTIST=\\t\\123\\x12\\u1234\n' >vc.txt +if run_metaflac --escapes --import-tags-from=vc.txt $flacfile ; then + die "ERROR: metaflac --escapes --import-tags-from=FILE with unsupported escapes should have failed but didn't" +else + echo "OK, it failed as it should" +fi +check_flac +rm vc.txt + +# Import tags from stdin with --escapes +run_metaflac --remove-all-tags $flacfile +printf 'ARTIST=Line1\\nLine\\\\2\\rLine3\nTITLE=Line1\\nLine\\\\2\\rLine3\n' | run_metaflac --escapes --import-tags-from=- $flacfile +check_flac +metaflac_test case68 "--escapes --import-tags-from=-" "--list" + +# Import tags from FILE with --escapes +printf 'ARTIST=Line1\\nLine\\\\2\\rLine3\nTITLE=Line1\\nLine\\\\2\\rLine3\n' >vc.txt +run_metaflac --remove-all-tags $flacfile +run_metaflac --escapes --import-tags-from=vc.txt $flacfile +check_flac +metaflac_test case68 "--escapes --import-tags-from=[FILE]" "--list" +rm vc.txt + +# Show tag with --escapes +printf 'ARTIST=Line1\\nLine\\\\2\\rLine3\n' >vc_expected.txt +run_metaflac --escapes --show-tag=ARTIST $flacfile >vc.txt +diff -w vc_expected.txt vc.txt> /dev/null 2>&1 || die "ERROR: shown tags with escapes does not match expected tags" +rm vc.txt vc_expected.txt + +# Show tag WITHOUT --escapes +printf 'ARTIST=Line1\nLine\\2\rLine3\n' >vc_expected.txt +run_metaflac --show-tag=ARTIST $flacfile >vc.txt +diff -w vc_expected.txt vc.txt> /dev/null 2>&1 || die "ERROR: shown tags without escapes does not match expected tags" +rm vc.txt vc_expected.txt + +# Export tags to stdout with --escapes +printf 'ARTIST=Line1\\nLine\\\\2\\rLine3\nTITLE=Line1\\nLine\\\\2\\rLine3\n' >vc_expected.txt +run_metaflac --escapes --export-tags-to=- $flacfile >vc.txt +diff -w vc_expected.txt vc.txt> /dev/null 2>&1 || die "ERROR: exported tags does not match expected tags" +rm vc.txt + +# Export tags to FILE with --escapes +run_metaflac --escapes --export-tags-to=vc.txt $flacfile +diff -w vc_expected.txt vc.txt > /dev/null 2>&1 || die "ERROR: exported tags does not match expected tags" +rm vc.txt vc_expected.txt + +# Export tags to stdout WITHOUT --escapes +printf 'ARTIST=Line1\nLine\\2\rLine3\nTITLE=Line1\nLine\\2\rLine3\n' >vc_expected.txt +run_metaflac --export-tags-to=- $flacfile >vc.txt +diff -w vc_expected.txt vc.txt> /dev/null 2>&1 || die "ERROR: exported tags does not match expected tags" +rm vc.txt + +# Export tags to FILE WITHOUT --escapes +run_metaflac --export-tags-to=vc.txt $flacfile +diff -w vc_expected.txt vc.txt > /dev/null 2>&1 || die "ERROR: exported tags does not match expected tags" +rm vc.txt vc_expected.txt + + +# Cleanup rm -f metaflac-test-files/out.meta metaflac-test-files/out1.meta metaflac-test-files/out.flac