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