1 #include <opustags.h>
2 #include "tap.h"
3 
4 #include <string.h>
5 
6 using namespace std::literals::string_literals;
7 
check_read_comments()8 void check_read_comments()
9 {
10 	std::list<std::string> comments;
11 	ot::status rc;
12 	{
13 		std::string txt = "TITLE=a b c\n\nARTIST=X\nArtist=Y\n"s;
14 		ot::file input = fmemopen((char*) txt.data(), txt.size(), "r");
15 		rc = ot::read_comments(input.get(), comments, false);
16 		if (rc != ot::st::ok)
17 			throw failure("could not read comments");
18 		auto&& expected = {"TITLE=a b c", "ARTIST=X", "Artist=Y"};
19 		if (!std::equal(comments.begin(), comments.end(), expected.begin(), expected.end()))
20 			throw failure("parsed user comments did not match expectations");
21 	}
22 	{
23 		std::string txt = "CORRUPTED=\xFF\xFF\n"s;
24 		ot::file input = fmemopen((char*) txt.data(), txt.size(), "r");
25 		rc = ot::read_comments(input.get(), comments, false);
26 		if (rc != ot::st::badly_encoded)
27 			throw failure("did not get the expected error reading corrupted data");
28 	}
29 	{
30 		std::string txt = "RAW=\xFF\xFF\n"s;
31 		ot::file input = fmemopen((char*) txt.data(), txt.size(), "r");
32 		rc = ot::read_comments(input.get(), comments, true);
33 		if (rc != ot::st::ok)
34 			throw failure("could not read comments");
35 		if (comments.front() != "RAW=\xFF\xFF")
36 			throw failure("parsed user comments did not match expectations");
37 	}
38 	{
39 		std::string txt = "MALFORMED\n"s;
40 		ot::file input = fmemopen((char*) txt.data(), txt.size(), "r");
41 		rc = ot::read_comments(input.get(), comments, false);
42 		if (rc != ot::st::error)
43 			throw failure("did not get the expected error reading malformed comments");
44 	}
45 }
46 
47 /**
48  * Wrap #ot::parse_options with a higher-level interface much more convenient for testing.
49  * In practice, the argc/argv combo are enough though for the current state of opustags.
50  */
parse_options(const std::vector<const char * > & args,ot::options & opt,FILE * comments)51 static ot::status parse_options(const std::vector<const char*>& args, ot::options& opt, FILE *comments)
52 {
53 	int argc = args.size();
54 	char* argv[argc];
55 	for (int i = 0; i < argc; ++i)
56 		argv[i] = strdup(args[i]);
57 	ot::status rc = ot::parse_options(argc, argv, opt, comments);
58 	for (int i = 0; i < argc; ++i)
59 		free(argv[i]);
60 	return rc;
61 }
62 
check_good_arguments()63 void check_good_arguments()
64 {
65 	auto parse = [](std::vector<const char*> args) {
66 		ot::options opt;
67 		std::string txt = "N=1\n"s;
68 		ot::file input = fmemopen((char*) txt.data(), txt.size(), "r");
69 		ot::status rc = parse_options(args, opt, input.get());
70 		if (rc.code != ot::st::ok)
71 			throw failure("unexpected option parsing error");
72 		return opt;
73 	};
74 
75 	ot::options opt;
76 	opt = parse({"opustags", "--help", "x", "-o", "y"});
77 	if (!opt.print_help)
78 		throw failure("did not catch --help");
79 
80 	opt = parse({"opustags", "x", "--output", "y", "-D", "-s", "X=Y Z", "-d", "a=b"});
81 	if (opt.paths_in.size() != 1 || opt.paths_in.front() != "x" || !opt.path_out ||
82 	    opt.path_out != "y" || !opt.delete_all || opt.overwrite || opt.to_delete.size() != 2 ||
83 	    opt.to_delete.front() != "X" || *std::next(opt.to_delete.begin()) != "a=b" ||
84 	    opt.to_add != std::list<std::string>{"X=Y Z"})
85 		throw failure("unexpected option parsing result for case #1");
86 
87 	opt = parse({"opustags", "-S", "x", "-S", "-a", "x=y z", "-i"});
88 	if (opt.paths_in.size() != 1 || opt.paths_in.front() != "x" || opt.path_out ||
89 	    !opt.overwrite || opt.to_delete.size() != 0 ||
90 	    opt.to_add != std::list<std::string>{"N=1", "x=y z"})
91 		throw failure("unexpected option parsing result for case #2");
92 
93 	opt = parse({"opustags", "-i", "x", "y", "z"});
94 	if (opt.paths_in.size() != 3 || opt.paths_in[0] != "x" || opt.paths_in[1] != "y" ||
95 	    opt.paths_in[2] != "z" || !opt.overwrite || !opt.in_place)
96 		throw failure("unexpected option parsing result for case #3");
97 
98 	opt = parse({"opustags", "-ie", "x"});
99 	if (opt.paths_in.size() != 1 || opt.paths_in[0] != "x" ||
100 	    !opt.edit_interactively || !opt.overwrite || !opt.in_place)
101 		throw failure("unexpected option parsing result for case #4");
102 
103 	opt = parse({"opustags", "-a", "X=\xFF", "--raw", "x"});
104 	if (!opt.raw || opt.to_add.front() != "X=\xFF")
105 		throw failure("--raw did not disable transcoding");
106 }
107 
check_bad_arguments()108 void check_bad_arguments()
109 {
110 	auto error_code_case = [](std::vector<const char*> args, const char* message, ot::st error_code, const std::string& name) {
111 		ot::options opt;
112 		std::string txt = "N=1\nINVALID"s;
113 		ot::file input = fmemopen((char*) txt.data(), txt.size(), "r");
114 		ot::status rc = parse_options(args, opt, input.get());
115 		if (rc.code != error_code)
116 			throw failure("bad error code for case " + name);
117 		if (rc.message != message)
118 			throw failure("bad error message for case " + name + ", got: " + rc.message);
119 	};
120 	auto error_case = [&error_code_case](std::vector<const char*> args, const char* message, const std::string& name) {
121 		 error_code_case(args, message, ot::st::bad_arguments, name);
122 	};
123 	error_case({"opustags"}, "No arguments specified. Use -h for help.", "no arguments");
124 	error_case({"opustags", "-a", "X"}, "Comment does not contain an equal sign: X.", "bad comment for -a");
125 	error_case({"opustags", "--set", "X"}, "Comment does not contain an equal sign: X.", "bad comment for --set");
126 	error_case({"opustags", "-a"}, "Missing value for option '-a'.", "short option with missing value");
127 	error_case({"opustags", "--add"}, "Missing value for option '--add'.", "long option with missing value");
128 	error_case({"opustags", "-x"}, "Unrecognized option '-x'.", "unrecognized short option");
129 	error_case({"opustags", "--derp"}, "Unrecognized option '--derp'.", "unrecognized long option");
130 	error_case({"opustags", "-x=y"}, "Unrecognized option '-x'.", "unrecognized short option with value");
131 	error_case({"opustags", "--derp=y"}, "Unrecognized option '--derp=y'.", "unrecognized long option with value");
132 	error_case({"opustags", "-aX=Y"}, "Exactly one input file must be specified.", "no input file");
133 	error_case({"opustags", "-i", "-o", "/dev/null", "-"}, "Cannot combine --in-place and --output.", "in-place + output");
134 	error_case({"opustags", "-S", "-"}, "Cannot use standard input as input file when --set-all is specified.",
135 	                                    "set all and read opus from stdin");
136 	error_case({"opustags", "-i", "-"}, "Cannot modify standard input in place.", "write stdin in-place");
137 	error_case({"opustags", "-o", "x", "--output", "y", "z"},
138 	           "Cannot specify --output more than once.", "double output");
139 	error_code_case({"opustags", "-S", "x"}, "Malformed tag: INVALID", ot::st::error, "attempt to read invalid argument with -S");
140 	error_case({"opustags", "-o", "", "--output", "y", "z"},
141 	           "Cannot specify --output more than once.", "double output with first filename empty");
142 	error_case({"opustags", "-e", "-i", "x", "y"},
143 	           "Exactly one input file must be specified.", "editing interactively two files at once");
144 	error_case({"opustags", "--edit", "-", "-o", "x"},
145 	           "Cannot edit interactively when standard input or standard output are already used.",
146 	           "editing interactively from stdandard intput");
147 	error_case({"opustags", "--edit", "x", "-o", "-"},
148 	           "Cannot edit interactively when standard input or standard output are already used.",
149 	           "editing interactively to stdandard output");
150 	error_case({"opustags", "--edit", "x"}, "Cannot edit interactively when no output is specified.", "editing without output");
151 	error_case({"opustags", "--edit", "x", "-i", "-a", "X=Y"}, "Cannot mix --edit with -adDsS.", "mixing -e and -a");
152 	error_case({"opustags", "--edit", "x", "-i", "-d", "X"}, "Cannot mix --edit with -adDsS.", "mixing -e and -d");
153 	error_case({"opustags", "--edit", "x", "-i", "-D"}, "Cannot mix --edit with -adDsS.", "mixing -e and -D");
154 	error_case({"opustags", "--edit", "x", "-i", "-S"}, "Cannot mix --edit with -adDsS.", "mixing -e and -S");
155 	error_case({"opustags", "-d", "\xFF", "x"},
156 	           "Could not encode argument into UTF-8: Invalid or incomplete multibyte or wide character.",
157 	           "-d with binary data");
158 	error_case({"opustags", "-a", "X=\xFF", "x"},
159 	           "Could not encode argument into UTF-8: Invalid or incomplete multibyte or wide character.",
160 	           "-a with binary data");
161 	error_case({"opustags", "-s", "X=\xFF", "x"},
162 	           "Could not encode argument into UTF-8: Invalid or incomplete multibyte or wide character.",
163 	           "-s with binary data");
164 }
165 
check_delete_comments()166 static void check_delete_comments()
167 {
168 	using C = std::list<std::string>;
169 	C original = {"TITLE=X", "Title=Y", "Title=Z", "ARTIST=A", "artIst=B"};
170 
171 	C edited = original;
172 	ot::delete_comments(edited, "derp");
173 	if (!std::equal(edited.begin(), edited.end(), original.begin(), original.end()))
174 		throw failure("should not have deleted anything");
175 
176 	ot::delete_comments(edited, "Title");
177 	C expected = {"ARTIST=A", "artIst=B"};
178 	if (!std::equal(edited.begin(), edited.end(), expected.begin(), expected.end()))
179 		throw failure("did not delete all titles correctly");
180 
181 	edited = original;
182 	ot::delete_comments(edited, "titlE=Y");
183 	ot::delete_comments(edited, "Title=z");
184 	expected = {"TITLE=X", "Title=Z", "ARTIST=A", "artIst=B"};
185 	if (!std::equal(edited.begin(), edited.end(), expected.begin(), expected.end()))
186 		throw failure("did not delete a specific title correctly");
187 }
188 
main(int argc,char ** argv)189 int main(int argc, char **argv)
190 {
191 	std::cout << "1..4\n";
192 	run(check_read_comments, "check tags parsing");
193 	run(check_good_arguments, "check options parsing");
194 	run(check_bad_arguments, "check options parsing errors");
195 	run(check_delete_comments, "delete comments");
196 	return 0;
197 }
198