1235911Smav// SPDX-License-Identifier: 0BSD
2235911Smav
3235911Smav///////////////////////////////////////////////////////////////////////////////
4235911Smav//
5235911Smav/// \file       main.c
6235911Smav/// \brief      main()
7235911Smav//
8235911Smav//  Author:     Lasse Collin
9235911Smav//
10235911Smav///////////////////////////////////////////////////////////////////////////////
11235911Smav
12235911Smav#include "private.h"
13235911Smav#include <ctype.h>
14235911Smav
15235911Smav
16235911Smav/// Exit status to use. This can be changed with set_exit_status().
17235911Smavstatic enum exit_status_type exit_status = E_SUCCESS;
18235911Smav
19235911Smav#if defined(_WIN32) && !defined(__CYGWIN__)
20235911Smav/// exit_status has to be protected with a critical section due to
21235911Smav/// how "signal handling" is done on Windows. See signals.c for details.
22235911Smavstatic CRITICAL_SECTION exit_status_cs;
23235911Smav#endif
24235911Smav
25235911Smav/// True if --no-warn is specified. When this is true, we don't set
26235911Smav/// the exit status to E_WARNING when something worth a warning happens.
27235911Smavstatic bool no_warn = false;
28235911Smav
29235911Smav
30235911Smavextern void
31235911Smavset_exit_status(enum exit_status_type new_status)
32235911Smav{
33235911Smav	assert(new_status == E_WARNING || new_status == E_ERROR);
34235911Smav
35235911Smav#if defined(_WIN32) && !defined(__CYGWIN__)
36235911Smav	EnterCriticalSection(&exit_status_cs);
37235911Smav#endif
38235911Smav
39245891Sjh	if (exit_status != E_ERROR)
40235911Smav		exit_status = new_status;
41235911Smav
42235911Smav#if defined(_WIN32) && !defined(__CYGWIN__)
43235911Smav	LeaveCriticalSection(&exit_status_cs);
44235911Smav#endif
45235911Smav
46235911Smav	return;
47235911Smav}
48235911Smav
49235911Smav
50235911Smavextern void
51235911Smavset_exit_no_warn(void)
52235911Smav{
53235911Smav	no_warn = true;
54235911Smav	return;
55235911Smav}
56235911Smav
57235911Smav
58235911Smavstatic const char *
59235911Smavread_name(const args_info *args)
60235911Smav{
61235911Smav	// FIXME: Maybe we should have some kind of memory usage limit here
62235911Smav	// like the tool has for the actual compression and decompression.
63235911Smav	// Giving some huge text file with --files0 makes us to read the
64235911Smav	// whole file in RAM.
65235911Smav	static char *name = NULL;
66235911Smav	static size_t size = 256;
67235911Smav
68235911Smav	// Allocate the initial buffer. This is never freed, since after it
69235911Smav	// is no longer needed, the program exits very soon. It is safe to
70235911Smav	// use xmalloc() and xrealloc() in this function, because while
71235911Smav	// executing this function, no files are open for writing, and thus
72235911Smav	// there's no need to cleanup anything before exiting.
73235911Smav	if (name == NULL)
74235911Smav		name = xmalloc(size);
75235911Smav
76235911Smav	// Write position in name
77235911Smav	size_t pos = 0;
78235911Smav
79235911Smav	// Read one character at a time into name.
80235911Smav	while (!user_abort) {
81235911Smav		const int c = fgetc(args->files_file);
82235911Smav
83235911Smav		if (ferror(args->files_file)) {
84235911Smav			// Take care of EINTR since we have established
85235911Smav			// the signal handlers already.
86235911Smav			if (errno == EINTR)
87235911Smav				continue;
88235911Smav
89235911Smav			message_error(_("%s: Error reading filenames: %s"),
90235911Smav					args->files_name, strerror(errno));
91235911Smav			return NULL;
92235911Smav		}
93235911Smav
94235911Smav		if (feof(args->files_file)) {
95235911Smav			if (pos != 0)
96235911Smav				message_error(_("%s: Unexpected end of input "
97235911Smav						"when reading filenames"),
98235911Smav						args->files_name);
99235911Smav
100235911Smav			return NULL;
101235911Smav		}
102235911Smav
103235911Smav		if (c == args->files_delim) {
104235911Smav			// We allow consecutive newline (--files) or '\0'
105235911Smav			// characters (--files0), and ignore such empty
106235911Smav			// filenames.
107235911Smav			if (pos == 0)
108235911Smav				continue;
109235911Smav
110235911Smav			// A non-empty name was read. Terminate it with '\0'
111235911Smav			// and return it.
112235911Smav			name[pos] = '\0';
113235911Smav			return name;
114235911Smav		}
115235911Smav
116235911Smav		if (c == '\0') {
117235911Smav			// A null character was found when using --files,
118235911Smav			// which expects plain text input separated with
119235911Smav			// newlines.
120235911Smav			message_error(_("%s: Null character found when "
121235911Smav					"reading filenames; maybe you meant "
122235911Smav					"to use '--files0' instead "
123235911Smav					"of '--files'?"), args->files_name);
124235911Smav			return NULL;
125235911Smav		}
126235911Smav
127235911Smav		name[pos++] = c;
128235911Smav
129235911Smav		// Allocate more memory if needed. There must always be space
130235911Smav		// at least for one character to allow terminating the string
131235911Smav		// with '\0'.
132235911Smav		if (pos == size) {
133235911Smav			size *= 2;
134235911Smav			name = xrealloc(name, size);
135235911Smav		}
136235911Smav	}
137235911Smav
138235911Smav	return NULL;
139235911Smav}
140235911Smav
141235911Smav
142235911Smavint
143235911Smavmain(int argc, char **argv)
144235911Smav{
145235911Smav#if defined(_WIN32) && !defined(__CYGWIN__)
146235911Smav	InitializeCriticalSection(&exit_status_cs);
147235911Smav#endif
148235911Smav
149235911Smav	// Set up the progname variable needed for messages.
150235911Smav	tuklib_progname_init(argv);
151235911Smav
152235911Smav	// Initialize the file I/O. This makes sure that
153235911Smav	// stdin, stdout, and stderr are something valid.
154235911Smav	// This must be done before we might open any files
155235911Smav	// even indirectly like locale and gettext initializations.
156235911Smav	io_init();
157235911Smav
158235911Smav#ifdef ENABLE_SANDBOX
159235911Smav	// Enable such sandboxing that can always be enabled.
160235911Smav	// This requires that progname has been set up.
161235911Smav	// It's also good that io_init() has been called because it
162235911Smav	// might need to do things that the initial sandbox won't allow.
163235911Smav	// Otherwise this should be called as early as possible.
164235911Smav	//
165235911Smav	// NOTE: Calling this before tuklib_gettext_init() means that
166235911Smav	// translated error message won't be available if sandbox
167235911Smav	// initialization fails. However, sandbox_init() shouldn't
168235911Smav	// fail and this order simply feels better.
169235911Smav	sandbox_init();
170235911Smav#endif
171235911Smav
172235911Smav	// Set up the locale and message translations.
173235911Smav	tuklib_gettext_init(PACKAGE, LOCALEDIR);
174235911Smav
175235911Smav	// Initialize progress message handling. It's not always needed
176235911Smav	// but it's simpler to do this unconditionally.
177235911Smav	message_init();
178235911Smav
179235911Smav	// Set hardware-dependent default values. These can be overridden
180235911Smav	// on the command line, thus this must be done before args_parse().
181235911Smav	hardware_init();
182235911Smav
183235911Smav	// Parse the command line arguments and get an array of filenames.
184235911Smav	// This doesn't return if something is wrong with the command line
185235911Smav	// arguments. If there are no arguments, one filename ("-") is still
186235911Smav	// returned to indicate stdin.
187235911Smav	args_info args;
188235911Smav	args_parse(&args, argc, argv);
189235911Smav
190235911Smav	if (opt_mode != MODE_LIST && opt_robot)
191235911Smav		message_fatal(_("Compression and decompression with --robot "
192235911Smav			"are not supported yet."));
193235911Smav
194235911Smav	// Tell the message handling code how many input files there are if
195235911Smav	// we know it. This way the progress indicator can show it.
196235911Smav	if (args.files_name != NULL)
197235911Smav		message_set_files(0);
198235911Smav	else
199235911Smav		message_set_files(args.arg_count);
200235911Smav
201235911Smav	// Refuse to write compressed data to standard output if it is
202235911Smav	// a terminal.
203235911Smav	if (opt_mode == MODE_COMPRESS) {
204235911Smav		if (opt_stdout || (args.arg_count == 1
205235911Smav				&& strcmp(args.arg_names[0], "-") == 0)) {
206235911Smav			if (is_tty_stdout()) {
207235911Smav				message_try_help();
208235911Smav				tuklib_exit(E_ERROR, E_ERROR, false);
209235911Smav			}
210235911Smav		}
211235911Smav	}
212235911Smav
213235911Smav	// Set up the signal handlers. We don't need these before we
214235911Smav	// start the actual action and not in --list mode, so this is
215235911Smav	// done after parsing the command line arguments.
216235911Smav	//
217235911Smav	// It's good to keep signal handlers in normal compression and
218235911Smav	// decompression modes even when only writing to stdout, because
219235911Smav	// we might need to restore O_APPEND flag on stdout before exiting.
220235911Smav	// In --test mode, signal handlers aren't really needed, but let's
221235911Smav	// keep them there for consistency with normal decompression.
222235911Smav	if (opt_mode != MODE_LIST)
223235911Smav		signals_init();
224235911Smav
225235911Smav#ifdef ENABLE_SANDBOX
226235911Smav	// Read-only sandbox can be enabled if we won't create or delete
227235911Smav	// any files:
228235911Smav	//
229235911Smav	//   - --stdout, --test, or --list was used. Note that --test
230235911Smav	//     implies opt_stdout = true but --list doesn't.
231235911Smav	//
232235911Smav	//   - Output goes to stdout because --files or --files0 wasn't used
233235911Smav	//     and no arguments were given on the command line or the
234235911Smav	//     arguments are all "-" (indicating standard input).
235235911Smav	bool to_stdout_only = opt_stdout || opt_mode == MODE_LIST;
236235911Smav	if (!to_stdout_only && args.files_name == NULL) {
237235911Smav		// If all of the filenames provided are "-" (more than one
238235911Smav		// "-" could be specified), then we are only going to be
239235911Smav		// writing to standard output. Note that if no filename args
240235911Smav		// were provided, args.c puts a single "-" in arg_names[0].
241235911Smav		to_stdout_only = true;
242235911Smav
243235911Smav		for (unsigned i = 0; i < args.arg_count; ++i) {
244235911Smav			if (strcmp("-", args.arg_names[i]) != 0) {
245235911Smav				to_stdout_only = false;
246235911Smav				break;
247235911Smav			}
248235911Smav		}
249235911Smav	}
250235911Smav
251235911Smav	if (to_stdout_only) {
252235911Smav		sandbox_enable_read_only();
253235911Smav
254235911Smav		// Allow strict sandboxing if we are processing exactly one
255235911Smav		// file to standard output. This requires that --files or
256235911Smav		// --files0 wasn't specified (an unknown number of filenames
257235911Smav		// could be provided that way).
258235911Smav		if (args.files_name == NULL && args.arg_count == 1)
259235911Smav			sandbox_allow_strict();
260235911Smav	}
261235911Smav#endif
262235911Smav
263235911Smav	// coder_run() handles compression, decompression, and testing.
264235911Smav	// list_file() is for --list.
265235911Smav	void (*run)(const char *filename) = &coder_run;
266235911Smav#ifdef HAVE_DECODERS
267235911Smav	if (opt_mode == MODE_LIST)
268235911Smav		run = &list_file;
269235911Smav#endif
270235911Smav
271235911Smav	// Process the files given on the command line. Note that if no names
272235911Smav	// were given, args_parse() gave us a fake "-" filename.
273235911Smav	for (unsigned i = 0; i < args.arg_count && !user_abort; ++i) {
274235911Smav		if (strcmp("-", args.arg_names[i]) == 0) {
275235911Smav			// Processing from stdin to stdout. Check that we
276235911Smav			// aren't writing compressed data to a terminal or
277235911Smav			// reading it from a terminal.
278235911Smav			if (opt_mode == MODE_COMPRESS) {
279235911Smav				if (is_tty_stdout())
280235911Smav					continue;
281235911Smav			} else if (is_tty_stdin()) {
282235911Smav				continue;
283235911Smav			}
284235911Smav
285235911Smav			// It doesn't make sense to compress data from stdin
286235911Smav			// if we are supposed to read filenames from stdin
287235911Smav			// too (enabled with --files or --files0).
288235911Smav			if (args.files_name == stdin_filename) {
289235911Smav				message_error(_("Cannot read data from "
290235911Smav						"standard input when "
291235911Smav						"reading filenames "
292235911Smav						"from standard input"));
293235911Smav				continue;
294235911Smav			}
295235911Smav
296235911Smav			// Replace the "-" with a special pointer, which is
297235911Smav			// recognized by coder_run() and other things.
298235911Smav			// This way error messages get a proper filename
299235911Smav			// string and the code still knows that it is
300235911Smav			// handling the special case of stdin.
301235911Smav			args.arg_names[i] = (char *)stdin_filename;
302235911Smav		}
303235911Smav
304235911Smav		// Do the actual compression or decompression.
305235911Smav		run(args.arg_names[i]);
306235911Smav	}
307235911Smav
308235911Smav	// If --files or --files0 was used, process the filenames from the
309235911Smav	// given file or stdin. Note that here we don't consider "-" to
310235911Smav	// indicate stdin like we do with the command line arguments.
311235911Smav	if (args.files_name != NULL) {
312235911Smav		// read_name() checks for user_abort so we don't need to
313235911Smav		// check it as loop termination condition.
314235911Smav		while (true) {
315235911Smav			const char *name = read_name(&args);
316235911Smav			if (name == NULL)
317235911Smav				break;
318235911Smav
319235911Smav			// read_name() doesn't return empty names.
320235911Smav			assert(name[0] != '\0');
321235911Smav			run(name);
322235911Smav		}
323235911Smav
324235911Smav		if (args.files_name != stdin_filename)
325235911Smav			(void)fclose(args.files_file);
326235911Smav	}
327235911Smav
328235911Smav#ifdef HAVE_DECODERS
329235911Smav	// All files have now been handled. If in --list mode, display
330235911Smav	// the totals before exiting. We don't have signal handlers
331235911Smav	// enabled in --list mode, so we don't need to check user_abort.
332235911Smav	if (opt_mode == MODE_LIST) {
333235911Smav		assert(!user_abort);
334235911Smav		list_totals();
335235911Smav	}
336235911Smav#endif
337235911Smav
338235911Smav#ifndef NDEBUG
339235911Smav	coder_free();
340235911Smav	args_free();
341235911Smav#endif
342235911Smav
343235911Smav	// If we have got a signal, raise it to kill the program instead
344235911Smav	// of calling tuklib_exit().
345235911Smav	signals_exit();
346235911Smav
347235911Smav	// Make a local copy of exit_status to keep the Windows code
348235911Smav	// thread safe. At this point it is fine if we miss the user
349235911Smav	// pressing C-c and don't set the exit_status to E_ERROR on
350235911Smav	// Windows.
351235911Smav#if defined(_WIN32) && !defined(__CYGWIN__)
352235911Smav	EnterCriticalSection(&exit_status_cs);
353235911Smav#endif
354235911Smav
355235911Smav	enum exit_status_type es = exit_status;
356235911Smav
357235911Smav#if defined(_WIN32) && !defined(__CYGWIN__)
358235911Smav	LeaveCriticalSection(&exit_status_cs);
359235911Smav#endif
360235911Smav
361235911Smav	// Suppress the exit status indicating a warning if --no-warn
362235911Smav	// was specified.
363235911Smav	if (es == E_WARNING && no_warn)
364235911Smav		es = E_SUCCESS;
365235911Smav
366235911Smav	tuklib_exit((int)es, E_ERROR, message_verbosity_get() != V_SILENT);
367242720Smav}
368235911Smav