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