1 ///////////////////////////////////////////////////////////////////////////////
2 //
3 /// \file main.c
4 /// \brief main()
5 //
6 // Author: Lasse Collin
7 //
8 // This file has been put into the public domain.
9 // You can do whatever you want with this file.
10 //
11 ///////////////////////////////////////////////////////////////////////////////
12
13 #include "private.h"
14 #include <ctype.h>
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 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
142 int
143 main(int argc, char **argv)
144 {
145 #ifdef HAVE_PLEDGE
146 // OpenBSD's pledge(2) sandbox
147 //
148 // Unconditionally enable sandboxing with fairly relaxed promises.
149 // This is still way better than having no sandbox at all. :-)
150 // More strict promises will be made later in file_io.c if possible.
151 if (pledge("stdio rpath wpath cpath fattr", "")) {
152 // Don't translate the string or use message_fatal() as
153 // those haven't been initialized yet.
154 fprintf(stderr, "%s: Failed to enable the sandbox\n", argv[0]);
155 return E_ERROR;
156 }
157 #endif
158
159 #if defined(_WIN32) && !defined(__CYGWIN__)
160 InitializeCriticalSection(&exit_status_cs);
161 #endif
162
163 // Set up the progname variable.
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 io_init();
169
170 // Set up the locale and message translations.
171 tuklib_gettext_init(PACKAGE, LOCALEDIR);
172
173 // Initialize handling of error/warning/other messages.
174 message_init();
175
176 // Set hardware-dependent default values. These can be overridden
177 // on the command line, thus this must be done before args_parse().
178 hardware_init();
179
180 // Parse the command line arguments and get an array of filenames.
181 // This doesn't return if something is wrong with the command line
182 // arguments. If there are no arguments, one filename ("-") is still
183 // returned to indicate stdin.
184 args_info args;
185 args_parse(&args, argc, argv);
186
187 if (opt_mode != MODE_LIST && opt_robot)
188 message_fatal(_("Compression and decompression with --robot "
189 "are not supported yet."));
190
191 // Tell the message handling code how many input files there are if
192 // we know it. This way the progress indicator can show it.
193 if (args.files_name != NULL)
194 message_set_files(0);
195 else
196 message_set_files(args.arg_count);
197
198 // Refuse to write compressed data to standard output if it is
199 // a terminal.
200 if (opt_mode == MODE_COMPRESS) {
201 if (opt_stdout || (args.arg_count == 1
202 && strcmp(args.arg_names[0], "-") == 0)) {
203 if (is_tty_stdout()) {
204 message_try_help();
205 tuklib_exit(E_ERROR, E_ERROR, false);
206 }
207 }
208 }
209
210 // Set up the signal handlers. We don't need these before we
211 // start the actual action and not in --list mode, so this is
212 // done after parsing the command line arguments.
213 //
214 // It's good to keep signal handlers in normal compression and
215 // decompression modes even when only writing to stdout, because
216 // we might need to restore O_APPEND flag on stdout before exiting.
217 // In --test mode, signal handlers aren't really needed, but let's
218 // keep them there for consistency with normal decompression.
219 if (opt_mode != MODE_LIST)
220 signals_init();
221
222 #ifdef ENABLE_SANDBOX
223 // Set a flag that sandboxing is allowed if all these are true:
224 // - --files or --files0 wasn't used.
225 // - There is exactly one input file or we are reading from stdin.
226 // - We won't create any files: output goes to stdout or --test
227 // or --list was used. Note that --test implies opt_stdout = true
228 // but --list doesn't.
229 //
230 // This is obviously not ideal but it was easy to implement and
231 // it covers the most common use cases.
232 //
233 // TODO: Make sandboxing work for other situations too.
234 if (args.files_name == NULL && args.arg_count == 1
235 && (opt_stdout || strcmp("-", args.arg_names[0]) == 0
236 || opt_mode == MODE_LIST))
237 io_allow_sandbox();
238 #endif
239
240 // coder_run() handles compression, decompression, and testing.
241 // list_file() is for --list.
242 void (*run)(const char *filename) = &coder_run;
243 #ifdef HAVE_DECODERS
244 if (opt_mode == MODE_LIST)
245 run = &list_file;
246 #endif
247
248 // Process the files given on the command line. Note that if no names
249 // were given, args_parse() gave us a fake "-" filename.
250 for (unsigned i = 0; i < args.arg_count && !user_abort; ++i) {
251 if (strcmp("-", args.arg_names[i]) == 0) {
252 // Processing from stdin to stdout. Check that we
253 // aren't writing compressed data to a terminal or
254 // reading it from a terminal.
255 if (opt_mode == MODE_COMPRESS) {
256 if (is_tty_stdout())
257 continue;
258 } else if (is_tty_stdin()) {
259 continue;
260 }
261
262 // It doesn't make sense to compress data from stdin
263 // if we are supposed to read filenames from stdin
264 // too (enabled with --files or --files0).
265 if (args.files_name == stdin_filename) {
266 message_error(_("Cannot read data from "
267 "standard input when "
268 "reading filenames "
269 "from standard input"));
270 continue;
271 }
272
273 // Replace the "-" with a special pointer, which is
274 // recognized by coder_run() and other things.
275 // This way error messages get a proper filename
276 // string and the code still knows that it is
277 // handling the special case of stdin.
278 args.arg_names[i] = (char *)stdin_filename;
279 }
280
281 // Do the actual compression or decompression.
282 run(args.arg_names[i]);
283 }
284
285 // If --files or --files0 was used, process the filenames from the
286 // given file or stdin. Note that here we don't consider "-" to
287 // indicate stdin like we do with the command line arguments.
288 if (args.files_name != NULL) {
289 // read_name() checks for user_abort so we don't need to
290 // check it as loop termination condition.
291 while (true) {
292 const char *name = read_name(&args);
293 if (name == NULL)
294 break;
295
296 // read_name() doesn't return empty names.
297 assert(name[0] != '\0');
298 run(name);
299 }
300
301 if (args.files_name != stdin_filename)
302 (void)fclose(args.files_file);
303 }
304
305 #ifdef HAVE_DECODERS
306 // All files have now been handled. If in --list mode, display
307 // the totals before exiting. We don't have signal handlers
308 // enabled in --list mode, so we don't need to check user_abort.
309 if (opt_mode == MODE_LIST) {
310 assert(!user_abort);
311 list_totals();
312 }
313 #endif
314
315 #ifndef NDEBUG
316 coder_free();
317 args_free();
318 #endif
319
320 // If we have got a signal, raise it to kill the program instead
321 // of calling tuklib_exit().
322 signals_exit();
323
324 // Make a local copy of exit_status to keep the Windows code
325 // thread safe. At this point it is fine if we miss the user
326 // pressing C-c and don't set the exit_status to E_ERROR on
327 // Windows.
328 #if defined(_WIN32) && !defined(__CYGWIN__)
329 EnterCriticalSection(&exit_status_cs);
330 #endif
331
332 enum exit_status_type es = exit_status;
333
334 #if defined(_WIN32) && !defined(__CYGWIN__)
335 LeaveCriticalSection(&exit_status_cs);
336 #endif
337
338 // Suppress the exit status indicating a warning if --no-warn
339 // was specified.
340 if (es == E_WARNING && no_warn)
341 es = E_SUCCESS;
342
343 tuklib_exit((int)es, E_ERROR, message_verbosity_get() != V_SILENT);
344 }