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