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