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 } 345