1 /*
2    mkvmerge -- utility for splicing together matroska files
3    from component media subtypes
4 
5    Distributed under the GPL v2
6    see the file COPYING for details
7    or visit https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
8 
9    definitions used in all programs, helper functions
10 
11    Written by Moritz Bunkus <moritz@bunkus.org>.
12 */
13 
14 #include "common/common_pch.h"
15 
16 #include <algorithm>
17 #include <cstring>
18 #ifdef SYS_WINDOWS
19 # include <windows.h>
20 #endif
21 
22 #include "common/command_line.h"
23 #if defined(SYS_APPLE)
24 # include "common/fs_sys_helpers.h"
25 #endif
26 #include "common/hacks.h"
27 #include "common/json.h"
28 #include "common/mm_io_x.h"
29 #include "common/mm_file_io.h"
30 #include "common/mm_proxy_io.h"
31 #include "common/mm_text_io.h"
32 #include "common/mm_write_buffer_io.h"
33 #include "common/strings/editing.h"
34 #include "common/strings/utf8.h"
35 #include "common/translation.h"
36 #include "common/version.h"
37 
38 namespace mtx::cli {
39 
40 bool g_gui_mode          = false;
41 bool g_abort_on_warnings = false;
42 
43 static void
read_args_from_json_file(std::vector<std::string> & args,std::string const & filename)44 read_args_from_json_file(std::vector<std::string> &args,
45                          std::string const &filename) {
46   std::string buffer;
47 
48   try {
49     auto io = std::make_shared<mm_text_io_c>(std::make_shared<mm_file_io_c>(filename));
50     io->read(buffer, io->get_size());
51 
52   } catch (mtx::mm_io::exception &ex) {
53     mxerror(fmt::format(Y("The file '{0}' could not be opened for reading: {1}.\n"), filename, ex));
54   }
55 
56   try {
57     auto doc       = mtx::json::parse(buffer);
58     auto skip_next = false;
59 
60     if (!doc.is_array())
61       throw std::domain_error{Y("JSON option files must contain a JSON array consisting solely of JSON strings")};
62 
63     for (auto const &val : doc) {
64       if (!val.is_string())
65         throw std::domain_error{Y("JSON option files must contain a JSON array consisting solely of JSON strings")};
66 
67       if (skip_next) {
68         skip_next = false;
69         continue;
70       }
71 
72       auto string = val.get<std::string>();
73       if (string == "--command-line-charset")
74         skip_next = true;
75 
76       else
77         args.push_back(string);
78     }
79 
80   } catch (std::exception const &ex) {
81     mxerror(fmt::format("The JSON option file '{0}' contains an error: {1}.\n", filename, ex.what()));
82   }
83 }
84 
85 /** \brief Expand the command line parameters
86 
87    Takes each command line paramter, converts it to UTF-8, and reads more
88    commands from command files if the argument starts with '@'. Puts all
89    arguments into a new array.
90    On Windows it uses the \c GetCommandLineW() function. That way it can
91    also handle multi-byte input like Japanese file names.
92 
93    \param argc The number of arguments. This is the same argument that
94      \c main normally receives.
95    \param argv The arguments themselves. This is the same argument that
96      \c main normally receives.
97    \return An array of strings converted to UTF-8 containing all the
98      command line arguments and any arguments read from option files.
99 */
100 #if !defined(SYS_WINDOWS)
101 std::vector<std::string>
args_in_utf8(int argc,char ** argv)102 args_in_utf8(int argc,
103              char **argv) {
104   int i;
105   std::vector<std::string> args;
106 
107   charset_converter_cptr cc_command_line = g_cc_stdio;
108 
109   for (i = 1; i < argc; i++)
110     if (argv[i][0] == '@')
111       read_args_from_json_file(args, &argv[i][1]);
112     else {
113       if (!strcmp(argv[i], "--command-line-charset")) {
114         if ((i + 1) == argc)
115           mxerror(Y("'--command-line-charset' is missing its argument.\n"));
116         cc_command_line = charset_converter_c::init(!argv[i + 1] ? "" : argv[i + 1]);
117         i++;
118       } else
119         args.push_back(cc_command_line->utf8(argv[i]));
120     }
121 
122 #if defined(SYS_APPLE)
123   // Always use NFD on macOS, no matter which normalization form the
124   // command-line arguments used.
125   for (auto &arg : args)
126     arg = mtx::sys::normalize_unicode_string(arg, mtx::sys::unicode_normalization_form_e::d);
127 #endif  // SYS_APPLE
128 
129   return args;
130 }
131 
132 #else  // !defined(SYS_WINDOWS)
133 
134 std::vector<std::string>
args_in_utf8(int,char **)135 args_in_utf8(int,
136              char **) {
137   std::vector<std::string> args;
138   std::string utf8;
139 
140   int num_args     = 0;
141   LPWSTR *arg_list = CommandLineToArgvW(GetCommandLineW(), &num_args);
142 
143   if (!arg_list)
144     return args;
145 
146   int i;
147   for (i = 1; i < num_args; i++) {
148     auto arg = to_utf8(std::wstring{arg_list[i]});
149 
150     if (arg[0] == '@')
151       read_args_from_json_file(args, arg.substr(1));
152 
153     else
154       args.push_back(arg);
155   }
156 
157   LocalFree(arg_list);
158 
159   return args;
160 }
161 #endif // !defined(SYS_WINDOWS)
162 
163 std::string g_usage_text;
164 
165 /** Handle command line arguments common to all programs
166 
167    Iterates over the list of command line arguments and handles the ones
168    that are common to all programs. These include --output-charset,
169    --redirect-output, --help, --version and --verbose along with their
170    short counterparts.
171 
172    \param args A vector of strings containing the command line arguments.
173      The ones that have been handled are removed from the vector.
174    \param redirect_output_short The name of the short option that is
175      recognized for --redirect-output. If left empty then no short
176      version is accepted.
177    \returns \c true if the locale has changed and the function should be
178      called again and \c false otherwise.
179 */
180 bool
handle_common_args(std::vector<std::string> & args,const std::string & redirect_output_short)181 handle_common_args(std::vector<std::string> &args,
182                    const std::string &redirect_output_short) {
183   size_t i = 0;
184 
185   while (args.size() > i) {
186     if (args[i] == "--debug") {
187       if ((i + 1) == args.size())
188         mxerror("Missing argument for '--debug'.\n");
189 
190       debugging_c::request(args[i + 1]);
191       args.erase(args.begin() + i, args.begin() + i + 2);
192 
193     } else if (args[i] == "--engage") {
194       if ((i + 1) == args.size())
195         mxerror(Y("'--engage' lacks its argument.\n"));
196 
197       mtx::hacks::engage(args[i + 1]);
198       args.erase(args.begin() + i, args.begin() + i + 2);
199 
200     } else if (args[i] == "--gui-mode") {
201       g_gui_mode = true;
202       args.erase(args.begin() + i, args.begin() + i + 1);
203 
204     } else if (args[i] == "--flush-on-close") {
205       mm_file_io_c::enable_flushing_on_close(true);
206       args.erase(args.begin() + i, args.begin() + i + 1);
207 
208     } else if (args[i] == "--abort-on-warnings") {
209       g_abort_on_warnings = true;
210       args.erase(args.begin() + i, args.begin() + i + 1);
211 
212     } else
213       ++i;
214   }
215 
216   // First see if there's an output charset given.
217   i = 0;
218   while (args.size() > i) {
219     if (args[i] == "--output-charset") {
220       if ((i + 1) == args.size())
221         mxerror(Y("Missing argument for '--output-charset'.\n"));
222       set_cc_stdio(args[i + 1]);
223       args.erase(args.begin() + i, args.begin() + i + 2);
224     } else
225       ++i;
226   }
227 
228   // Now let's see if the user wants the output redirected.
229   i = 0;
230   while (args.size() > i) {
231     if ((args[i] == "--redirect-output") || (args[i] == "-r") ||
232         ((redirect_output_short != "") &&
233          (args[i] == redirect_output_short))) {
234       if ((i + 1) == args.size())
235         mxerror(fmt::format(Y("'{0}' is missing the file name.\n"), args[i]));
236       try {
237         if (!stdio_redirected()) {
238           mm_io_cptr file = mm_write_buffer_io_c::open(args[i + 1], 128 * 1024);
239           file->write_bom(g_stdio_charset);
240           redirect_stdio(file);
241         }
242         args.erase(args.begin() + i, args.begin() + i + 2);
243       } catch(mtx::mm_io::exception &) {
244         mxerror(fmt::format(Y("Could not open the file '{0}' for directing the output.\n"), args[i + 1]));
245       }
246     } else
247       ++i;
248   }
249 
250   // Check for the translations to use (if any).
251   i = 0;
252   while (args.size() > i) {
253     if (args[i] == "--ui-language") {
254       if ((i + 1) == args.size())
255         mxerror(Y("Missing argument for '--ui-language'.\n"));
256 
257       if (args[i + 1] == "list") {
258         mxinfo(Y("Available translations:\n"));
259         auto translation = translation_c::ms_available_translations.begin(), end = translation_c::ms_available_translations.end();
260         while (translation != end) {
261           mxinfo(fmt::format("  {0} ({1})\n", translation->get_locale(), translation->m_english_name));
262           ++translation;
263         }
264         mxexit();
265       }
266 
267       if (-1 == translation_c::look_up_translation(args[i + 1]))
268         mxerror(fmt::format(Y("There is no translation available for '{0}'.\n"), args[i + 1]));
269 
270       init_locales(args[i + 1]);
271 
272       args.erase(args.begin() + i, args.begin() + i + 2);
273 
274       return true;
275     } else
276       ++i;
277   }
278 
279   // Last find the --help, --version, --check-for-updates arguments.
280   i = 0;
281   while (args.size() > i) {
282     if ((args[i] == "-V") || (args[i] == "--version")) {
283       mxinfo(fmt::format("{0}\n", get_version_info(get_program_name(), vif_full)));
284       mxexit();
285 
286     } else if ((args[i] == "-v") || (args[i] == "--verbose")) {
287       ++verbose;
288       args.erase(args.begin() + i, args.begin() + i + 1);
289 
290     } else if ((args[i] == "-q") || (args[i] == "--quiet")) {
291       verbose         = 0;
292       g_suppress_info = true;
293       args.erase(args.begin() + i, args.begin() + i + 1);
294 
295     } else if ((args[i] == "-h") || (args[i] == "-?") || (args[i] == "--help"))
296       display_usage();
297 
298     else
299       ++i;
300   }
301 
302   return false;
303 }
304 
305 void
display_usage(int exit_code)306 display_usage(int exit_code) {
307   if (!g_usage_text.empty()) {
308     mxinfo(g_usage_text);
309     if (g_usage_text.at(g_usage_text.size() - 1) != '\n')
310       mxinfo("\n");
311   }
312   mxexit(exit_code);
313 }
314 
315 }
316