1// SPDX-License-Identifier: 0BSD
2
3///////////////////////////////////////////////////////////////////////////////
4//
5/// \file       main.c
6/// \brief      main()
7//
8//  Author:     Lasse Collin
9//
10///////////////////////////////////////////////////////////////////////////////
11
12#include "private.h"
13#include <ctype.h>
14
15
16/// Exit status to use. This can be changed with set_exit_status().
17static enum exit_status_type exit_status = E_SUCCESS;
18
19#if defined(_WIN32) && !defined(__CYGWIN__)
20/// exit_status has to be protected with a critical section due to
21/// how "signal handling" is done on Windows. See signals.c for details.
22static CRITICAL_SECTION exit_status_cs;
23#endif
24
25/// True if --no-warn is specified. When this is true, we don't set
26/// the exit status to E_WARNING when something worth a warning happens.
27static bool no_warn = false;
28
29
30extern void
31set_exit_status(enum exit_status_type new_status)
32{
33	assert(new_status == E_WARNING || new_status == E_ERROR);
34
35#if defined(_WIN32) && !defined(__CYGWIN__)
36	EnterCriticalSection(&exit_status_cs);
37#endif
38
39	if (exit_status != E_ERROR)
40		exit_status = new_status;
41
42#if defined(_WIN32) && !defined(__CYGWIN__)
43	LeaveCriticalSection(&exit_status_cs);
44#endif
45
46	return;
47}
48
49
50extern void
51set_exit_no_warn(void)
52{
53	no_warn = true;
54	return;
55}
56
57
58static const char *
59read_name(const args_info *args)
60{
61	// FIXME: Maybe we should have some kind of memory usage limit here
62	// like the tool has for the actual compression and decompression.
63	// Giving some huge text file with --files0 makes us to read the
64	// whole file in RAM.
65	static char *name = NULL;
66	static size_t size = 256;
67
68	// Allocate the initial buffer. This is never freed, since after it
69	// is no longer needed, the program exits very soon. It is safe to
70	// use xmalloc() and xrealloc() in this function, because while
71	// executing this function, no files are open for writing, and thus
72	// there's no need to cleanup anything before exiting.
73	if (name == NULL)
74		name = xmalloc(size);
75
76	// Write position in name
77	size_t pos = 0;
78
79	// Read one character at a time into name.
80	while (!user_abort) {
81		const int c = fgetc(args->files_file);
82
83		if (ferror(args->files_file)) {
84			// Take care of EINTR since we have established
85			// the signal handlers already.
86			if (errno == EINTR)
87				continue;
88
89			message_error(_("%s: Error reading filenames: %s"),
90					args->files_name, strerror(errno));
91			return NULL;
92		}
93
94		if (feof(args->files_file)) {
95			if (pos != 0)
96				message_error(_("%s: Unexpected end of input "
97						"when reading filenames"),
98						args->files_name);
99
100			return NULL;
101		}
102
103		if (c == args->files_delim) {
104			// We allow consecutive newline (--files) or '\0'
105			// characters (--files0), and ignore such empty
106			// filenames.
107			if (pos == 0)
108				continue;
109
110			// A non-empty name was read. Terminate it with '\0'
111			// and return it.
112			name[pos] = '\0';
113			return name;
114		}
115
116		if (c == '\0') {
117			// A null character was found when using --files,
118			// which expects plain text input separated with
119			// newlines.
120			message_error(_("%s: Null character found when "
121					"reading filenames; maybe you meant "
122					"to use '--files0' instead "
123					"of '--files'?"), args->files_name);
124			return NULL;
125		}
126
127		name[pos++] = c;
128
129		// Allocate more memory if needed. There must always be space
130		// at least for one character to allow terminating the string
131		// with '\0'.
132		if (pos == size) {
133			size *= 2;
134			name = xrealloc(name, size);
135		}
136	}
137
138	return NULL;
139}
140
141
142int
143main(int argc, char **argv)
144{
145#if defined(_WIN32) && !defined(__CYGWIN__)
146	InitializeCriticalSection(&exit_status_cs);
147#endif
148
149	// Set up the progname variable needed for messages.
150	tuklib_progname_init(argv);
151
152	// Initialize the file I/O. This makes sure that
153	// stdin, stdout, and stderr are something valid.
154	// This must be done before we might open any files
155	// even indirectly like locale and gettext initializations.
156	io_init();
157
158#ifdef ENABLE_SANDBOX
159	// Enable such sandboxing that can always be enabled.
160	// This requires that progname has been set up.
161	// It's also good that io_init() has been called because it
162	// might need to do things that the initial sandbox won't allow.
163	// Otherwise this should be called as early as possible.
164	//
165	// NOTE: Calling this before tuklib_gettext_init() means that
166	// translated error message won't be available if sandbox
167	// initialization fails. However, sandbox_init() shouldn't
168	// fail and this order simply feels better.
169	sandbox_init();
170#endif
171
172	// Set up the locale and message translations.
173	tuklib_gettext_init(PACKAGE, LOCALEDIR);
174
175	// Initialize progress message handling. It's not always needed
176	// but it's simpler to do this unconditionally.
177	message_init();
178
179	// Set hardware-dependent default values. These can be overridden
180	// on the command line, thus this must be done before args_parse().
181	hardware_init();
182
183	// Parse the command line arguments and get an array of filenames.
184	// This doesn't return if something is wrong with the command line
185	// arguments. If there are no arguments, one filename ("-") is still
186	// returned to indicate stdin.
187	args_info args;
188	args_parse(&args, argc, argv);
189
190	if (opt_mode != MODE_LIST && opt_robot)
191		message_fatal(_("Compression and decompression with --robot "
192			"are not supported yet."));
193
194	// Tell the message handling code how many input files there are if
195	// we know it. This way the progress indicator can show it.
196	if (args.files_name != NULL)
197		message_set_files(0);
198	else
199		message_set_files(args.arg_count);
200
201	// Refuse to write compressed data to standard output if it is
202	// a terminal.
203	if (opt_mode == MODE_COMPRESS) {
204		if (opt_stdout || (args.arg_count == 1
205				&& strcmp(args.arg_names[0], "-") == 0)) {
206			if (is_tty_stdout()) {
207				message_try_help();
208				tuklib_exit(E_ERROR, E_ERROR, false);
209			}
210		}
211	}
212
213	// Set up the signal handlers. We don't need these before we
214	// start the actual action and not in --list mode, so this is
215	// done after parsing the command line arguments.
216	//
217	// It's good to keep signal handlers in normal compression and
218	// decompression modes even when only writing to stdout, because
219	// we might need to restore O_APPEND flag on stdout before exiting.
220	// In --test mode, signal handlers aren't really needed, but let's
221	// keep them there for consistency with normal decompression.
222	if (opt_mode != MODE_LIST)
223		signals_init();
224
225#ifdef ENABLE_SANDBOX
226	// Read-only sandbox can be enabled if we won't create or delete
227	// any files:
228	//
229	//   - --stdout, --test, or --list was used. Note that --test
230	//     implies opt_stdout = true but --list doesn't.
231	//
232	//   - Output goes to stdout because --files or --files0 wasn't used
233	//     and no arguments were given on the command line or the
234	//     arguments are all "-" (indicating standard input).
235	bool to_stdout_only = opt_stdout || opt_mode == MODE_LIST;
236	if (!to_stdout_only && args.files_name == NULL) {
237		// If all of the filenames provided are "-" (more than one
238		// "-" could be specified), then we are only going to be
239		// writing to standard output. Note that if no filename args
240		// were provided, args.c puts a single "-" in arg_names[0].
241		to_stdout_only = true;
242
243		for (unsigned i = 0; i < args.arg_count; ++i) {
244			if (strcmp("-", args.arg_names[i]) != 0) {
245				to_stdout_only = false;
246				break;
247			}
248		}
249	}
250
251	if (to_stdout_only) {
252		sandbox_enable_read_only();
253
254		// Allow strict sandboxing if we are processing exactly one
255		// file to standard output. This requires that --files or
256		// --files0 wasn't specified (an unknown number of filenames
257		// could be provided that way).
258		if (args.files_name == NULL && args.arg_count == 1)
259			sandbox_allow_strict();
260	}
261#endif
262
263	// coder_run() handles compression, decompression, and testing.
264	// list_file() is for --list.
265	void (*run)(const char *filename) = &coder_run;
266#ifdef HAVE_DECODERS
267	if (opt_mode == MODE_LIST)
268		run = &list_file;
269#endif
270
271	// Process the files given on the command line. Note that if no names
272	// were given, args_parse() gave us a fake "-" filename.
273	for (unsigned i = 0; i < args.arg_count && !user_abort; ++i) {
274		if (strcmp("-", args.arg_names[i]) == 0) {
275			// Processing from stdin to stdout. Check that we
276			// aren't writing compressed data to a terminal or
277			// reading it from a terminal.
278			if (opt_mode == MODE_COMPRESS) {
279				if (is_tty_stdout())
280					continue;
281			} else if (is_tty_stdin()) {
282				continue;
283			}
284
285			// It doesn't make sense to compress data from stdin
286			// if we are supposed to read filenames from stdin
287			// too (enabled with --files or --files0).
288			if (args.files_name == stdin_filename) {
289				message_error(_("Cannot read data from "
290						"standard input when "
291						"reading filenames "
292						"from standard input"));
293				continue;
294			}
295
296			// Replace the "-" with a special pointer, which is
297			// recognized by coder_run() and other things.
298			// This way error messages get a proper filename
299			// string and the code still knows that it is
300			// handling the special case of stdin.
301			args.arg_names[i] = (char *)stdin_filename;
302		}
303
304		// Do the actual compression or decompression.
305		run(args.arg_names[i]);
306	}
307
308	// If --files or --files0 was used, process the filenames from the
309	// given file or stdin. Note that here we don't consider "-" to
310	// indicate stdin like we do with the command line arguments.
311	if (args.files_name != NULL) {
312		// read_name() checks for user_abort so we don't need to
313		// check it as loop termination condition.
314		while (true) {
315			const char *name = read_name(&args);
316			if (name == NULL)
317				break;
318
319			// read_name() doesn't return empty names.
320			assert(name[0] != '\0');
321			run(name);
322		}
323
324		if (args.files_name != stdin_filename)
325			(void)fclose(args.files_file);
326	}
327
328#ifdef HAVE_DECODERS
329	// All files have now been handled. If in --list mode, display
330	// the totals before exiting. We don't have signal handlers
331	// enabled in --list mode, so we don't need to check user_abort.
332	if (opt_mode == MODE_LIST) {
333		assert(!user_abort);
334		list_totals();
335	}
336#endif
337
338#ifndef NDEBUG
339	coder_free();
340	args_free();
341#endif
342
343	// If we have got a signal, raise it to kill the program instead
344	// of calling tuklib_exit().
345	signals_exit();
346
347	// Make a local copy of exit_status to keep the Windows code
348	// thread safe. At this point it is fine if we miss the user
349	// pressing C-c and don't set the exit_status to E_ERROR on
350	// Windows.
351#if defined(_WIN32) && !defined(__CYGWIN__)
352	EnterCriticalSection(&exit_status_cs);
353#endif
354
355	enum exit_status_type es = exit_status;
356
357#if defined(_WIN32) && !defined(__CYGWIN__)
358	LeaveCriticalSection(&exit_status_cs);
359#endif
360
361	// Suppress the exit status indicating a warning if --no-warn
362	// was specified.
363	if (es == E_WARNING && no_warn)
364		es = E_SUCCESS;
365
366	tuklib_exit((int)es, E_ERROR, message_verbosity_get() != V_SILENT);
367}
368