Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 1 | /* Copyright 2014 Google Inc. All Rights Reserved. |
| 2 | |
| 3 | Distributed under MIT license. |
| 4 | See file LICENSE for detail or copy at https://opensource.org/licenses/MIT |
| 5 | */ |
| 6 | |
| 7 | /* Command line interface for Brotli library. */ |
| 8 | |
| 9 | #include <errno.h> |
| 10 | #include <fcntl.h> |
| 11 | #include <stdio.h> |
| 12 | #include <stdlib.h> |
| 13 | #include <string.h> |
| 14 | #include <sys/stat.h> |
| 15 | #include <sys/types.h> |
| 16 | #include <time.h> |
| 17 | |
Eugene Kliuchnikov | 37fb83e | 2017-09-19 15:57:15 +0200 | [diff] [blame] | 18 | #include "../common/constants.h" |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 19 | #include "../common/version.h" |
| 20 | #include <brotli/decode.h> |
| 21 | #include <brotli/encode.h> |
| 22 | |
| 23 | #if !defined(_WIN32) |
| 24 | #include <unistd.h> |
| 25 | #include <utime.h> |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 26 | #define MAKE_BINARY(FILENO) (FILENO) |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 27 | #else |
| 28 | #include <io.h> |
| 29 | #include <share.h> |
| 30 | #include <sys/utime.h> |
| 31 | |
| 32 | #define MAKE_BINARY(FILENO) (_setmode((FILENO), _O_BINARY), (FILENO)) |
| 33 | |
| 34 | #if !defined(__MINGW32__) |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 35 | #define STDIN_FILENO _fileno(stdin) |
| 36 | #define STDOUT_FILENO _fileno(stdout) |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 37 | #define S_IRUSR S_IREAD |
| 38 | #define S_IWUSR S_IWRITE |
| 39 | #endif |
| 40 | |
| 41 | #define fdopen _fdopen |
Eugene Kliuchnikov | 37fb83e | 2017-09-19 15:57:15 +0200 | [diff] [blame] | 42 | #define isatty _isatty |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 43 | #define unlink _unlink |
| 44 | #define utimbuf _utimbuf |
| 45 | #define utime _utime |
| 46 | |
| 47 | #define fopen ms_fopen |
| 48 | #define open ms_open |
| 49 | |
| 50 | #define chmod(F, P) (0) |
| 51 | #define chown(F, O, G) (0) |
| 52 | |
| 53 | #if defined(_MSC_VER) && (_MSC_VER >= 1400) |
| 54 | #define fseek _fseeki64 |
| 55 | #define ftell _ftelli64 |
| 56 | #endif |
| 57 | |
| 58 | static FILE* ms_fopen(const char* filename, const char* mode) { |
| 59 | FILE* result = 0; |
| 60 | fopen_s(&result, filename, mode); |
| 61 | return result; |
| 62 | } |
| 63 | |
| 64 | static int ms_open(const char* filename, int oflag, int pmode) { |
| 65 | int result = -1; |
| 66 | _sopen_s(&result, filename, oflag | O_BINARY, _SH_DENYNO, pmode); |
| 67 | return result; |
| 68 | } |
| 69 | #endif /* WIN32 */ |
| 70 | |
| 71 | typedef enum { |
| 72 | COMMAND_COMPRESS, |
| 73 | COMMAND_DECOMPRESS, |
| 74 | COMMAND_HELP, |
| 75 | COMMAND_INVALID, |
| 76 | COMMAND_TEST_INTEGRITY, |
| 77 | COMMAND_NOOP, |
| 78 | COMMAND_VERSION |
| 79 | } Command; |
| 80 | |
| 81 | #define DEFAULT_LGWIN 22 |
| 82 | #define DEFAULT_SUFFIX ".br" |
| 83 | #define MAX_OPTIONS 20 |
| 84 | |
| 85 | typedef struct { |
| 86 | /* Parameters */ |
| 87 | int quality; |
| 88 | int lgwin; |
| 89 | BROTLI_BOOL force_overwrite; |
| 90 | BROTLI_BOOL junk_source; |
| 91 | BROTLI_BOOL copy_stat; |
| 92 | BROTLI_BOOL verbose; |
| 93 | BROTLI_BOOL write_to_stdout; |
| 94 | BROTLI_BOOL test_integrity; |
| 95 | BROTLI_BOOL decompress; |
| 96 | const char* output_path; |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 97 | const char* suffix; |
| 98 | int not_input_indices[MAX_OPTIONS]; |
| 99 | size_t longest_path_len; |
| 100 | size_t input_count; |
| 101 | |
| 102 | /* Inner state */ |
| 103 | int argc; |
| 104 | char** argv; |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 105 | char* modified_path; /* Storage for path with appended / cut suffix */ |
| 106 | int iterator; |
| 107 | int ignore; |
| 108 | BROTLI_BOOL iterator_error; |
| 109 | uint8_t* buffer; |
| 110 | uint8_t* input; |
| 111 | uint8_t* output; |
| 112 | const char* current_input_path; |
| 113 | const char* current_output_path; |
| 114 | FILE* fin; |
| 115 | FILE* fout; |
| 116 | } Context; |
| 117 | |
| 118 | /* Parse up to 5 decimal digits. */ |
| 119 | static BROTLI_BOOL ParseInt(const char* s, int low, int high, int* result) { |
| 120 | int value = 0; |
| 121 | int i; |
| 122 | for (i = 0; i < 5; ++i) { |
| 123 | char c = s[i]; |
| 124 | if (c == 0) break; |
| 125 | if (s[i] < '0' || s[i] > '9') return BROTLI_FALSE; |
| 126 | value = (10 * value) + (c - '0'); |
| 127 | } |
| 128 | if (i == 0) return BROTLI_FALSE; |
| 129 | if (i > 1 && s[0] == '0') return BROTLI_FALSE; |
| 130 | if (s[i] != 0) return BROTLI_FALSE; |
| 131 | if (value < low || value > high) return BROTLI_FALSE; |
| 132 | *result = value; |
| 133 | return BROTLI_TRUE; |
| 134 | } |
| 135 | |
| 136 | /* Returns "base file name" or its tail, if it contains '/' or '\'. */ |
| 137 | static const char* FileName(const char* path) { |
| 138 | const char* separator_position = strrchr(path, '/'); |
| 139 | if (separator_position) path = separator_position + 1; |
| 140 | separator_position = strrchr(path, '\\'); |
| 141 | if (separator_position) path = separator_position + 1; |
| 142 | return path; |
| 143 | } |
| 144 | |
| 145 | /* Detect if the program name is a special alias that infers a command type. */ |
| 146 | static Command ParseAlias(const char* name) { |
| 147 | /* TODO: cast name to lower case? */ |
| 148 | const char* unbrotli = "unbrotli"; |
| 149 | size_t unbrotli_len = strlen(unbrotli); |
| 150 | name = FileName(name); |
| 151 | /* Partial comparison. On Windows there could be ".exe" suffix. */ |
Eugene Kliuchnikov | 6535435 | 2017-08-24 13:29:48 +0200 | [diff] [blame] | 152 | if (strncmp(name, unbrotli, unbrotli_len) == 0) { |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 153 | char terminator = name[unbrotli_len]; |
| 154 | if (terminator == 0 || terminator == '.') return COMMAND_DECOMPRESS; |
| 155 | } |
| 156 | return COMMAND_COMPRESS; |
| 157 | } |
| 158 | |
| 159 | static Command ParseParams(Context* params) { |
| 160 | int argc = params->argc; |
| 161 | char** argv = params->argv; |
| 162 | int i; |
| 163 | int next_option_index = 0; |
| 164 | size_t input_count = 0; |
| 165 | size_t longest_path_len = 1; |
| 166 | BROTLI_BOOL command_set = BROTLI_FALSE; |
| 167 | BROTLI_BOOL quality_set = BROTLI_FALSE; |
| 168 | BROTLI_BOOL output_set = BROTLI_FALSE; |
| 169 | BROTLI_BOOL keep_set = BROTLI_FALSE; |
| 170 | BROTLI_BOOL lgwin_set = BROTLI_FALSE; |
| 171 | BROTLI_BOOL suffix_set = BROTLI_FALSE; |
| 172 | BROTLI_BOOL after_dash_dash = BROTLI_FALSE; |
| 173 | Command command = ParseAlias(argv[0]); |
| 174 | |
| 175 | for (i = 1; i < argc; ++i) { |
| 176 | const char* arg = argv[i]; |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 177 | /* C99 5.1.2.2.1: "members argv[0] through argv[argc-1] inclusive shall |
| 178 | contain pointers to strings"; NULL and 0-length are not forbidden. */ |
Eugene Kliuchnikov | c605635 | 2017-09-20 15:02:01 +0200 | [diff] [blame] | 179 | size_t arg_len = arg ? strlen(arg) : 0; |
| 180 | |
| 181 | if (arg_len == 0) { |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 182 | params->not_input_indices[next_option_index++] = i; |
| 183 | continue; |
| 184 | } |
| 185 | |
| 186 | /* Too many options. The expected longest option list is: |
| 187 | "-q 0 -w 10 -o f -D d -S b -d -f -k -n -v --", i.e. 16 items in total. |
| 188 | This check is an additinal guard that is never triggered, but provides an |
| 189 | additional guard for future changes. */ |
| 190 | if (next_option_index > (MAX_OPTIONS - 2)) { |
| 191 | return COMMAND_INVALID; |
| 192 | } |
| 193 | |
| 194 | /* Input file entry. */ |
| 195 | if (after_dash_dash || arg[0] != '-' || arg_len == 1) { |
| 196 | input_count++; |
| 197 | if (longest_path_len < arg_len) longest_path_len = arg_len; |
| 198 | continue; |
| 199 | } |
| 200 | |
| 201 | /* Not a file entry. */ |
| 202 | params->not_input_indices[next_option_index++] = i; |
| 203 | |
| 204 | /* '--' entry stop parsing arguments. */ |
| 205 | if (arg_len == 2 && arg[1] == '-') { |
| 206 | after_dash_dash = BROTLI_TRUE; |
| 207 | continue; |
| 208 | } |
| 209 | |
| 210 | /* Simple / coalesced options. */ |
| 211 | if (arg[1] != '-') { |
| 212 | size_t j; |
| 213 | for (j = 1; j < arg_len; ++j) { |
| 214 | char c = arg[j]; |
| 215 | if (c >= '0' && c <= '9') { |
| 216 | if (quality_set) return COMMAND_INVALID; |
| 217 | quality_set = BROTLI_TRUE; |
| 218 | params->quality = c - '0'; |
| 219 | continue; |
| 220 | } else if (c == 'c') { |
| 221 | if (output_set) return COMMAND_INVALID; |
| 222 | output_set = BROTLI_TRUE; |
| 223 | params->write_to_stdout = BROTLI_TRUE; |
| 224 | continue; |
| 225 | } else if (c == 'd') { |
| 226 | if (command_set) return COMMAND_INVALID; |
| 227 | command_set = BROTLI_TRUE; |
| 228 | command = COMMAND_DECOMPRESS; |
| 229 | continue; |
| 230 | } else if (c == 'f') { |
| 231 | if (params->force_overwrite) return COMMAND_INVALID; |
| 232 | params->force_overwrite = BROTLI_TRUE; |
| 233 | continue; |
| 234 | } else if (c == 'h') { |
| 235 | /* Don't parse further. */ |
| 236 | return COMMAND_HELP; |
| 237 | } else if (c == 'j' || c == 'k') { |
| 238 | if (keep_set) return COMMAND_INVALID; |
| 239 | keep_set = BROTLI_TRUE; |
| 240 | params->junk_source = TO_BROTLI_BOOL(c == 'j'); |
| 241 | continue; |
| 242 | } else if (c == 'n') { |
| 243 | if (!params->copy_stat) return COMMAND_INVALID; |
| 244 | params->copy_stat = BROTLI_FALSE; |
| 245 | continue; |
| 246 | } else if (c == 't') { |
| 247 | if (command_set) return COMMAND_INVALID; |
| 248 | command_set = BROTLI_TRUE; |
| 249 | command = COMMAND_TEST_INTEGRITY; |
| 250 | continue; |
| 251 | } else if (c == 'v') { |
| 252 | if (params->verbose) return COMMAND_INVALID; |
| 253 | params->verbose = BROTLI_TRUE; |
| 254 | continue; |
| 255 | } else if (c == 'V') { |
| 256 | /* Don't parse further. */ |
| 257 | return COMMAND_VERSION; |
| 258 | } else if (c == 'Z') { |
| 259 | if (quality_set) return COMMAND_INVALID; |
| 260 | quality_set = BROTLI_TRUE; |
| 261 | params->quality = 11; |
| 262 | continue; |
| 263 | } |
| 264 | /* o/q/w/D/S with parameter is expected */ |
| 265 | if (c != 'o' && c != 'q' && c != 'w' && c != 'D' && c != 'S') { |
| 266 | return COMMAND_INVALID; |
| 267 | } |
| 268 | if (j + 1 != arg_len) return COMMAND_INVALID; |
| 269 | i++; |
| 270 | if (i == argc || !argv[i] || argv[i][0] == 0) return COMMAND_INVALID; |
| 271 | params->not_input_indices[next_option_index++] = i; |
| 272 | if (c == 'o') { |
| 273 | if (output_set) return COMMAND_INVALID; |
| 274 | params->output_path = argv[i]; |
| 275 | } else if (c == 'q') { |
| 276 | if (quality_set) return COMMAND_INVALID; |
| 277 | quality_set = ParseInt(argv[i], BROTLI_MIN_QUALITY, |
| 278 | BROTLI_MAX_QUALITY, ¶ms->quality); |
| 279 | if (!quality_set) return COMMAND_INVALID; |
| 280 | } else if (c == 'w') { |
| 281 | if (lgwin_set) return COMMAND_INVALID; |
| 282 | lgwin_set = ParseInt(argv[i], 0, |
| 283 | BROTLI_MAX_WINDOW_BITS, ¶ms->lgwin); |
| 284 | if (!lgwin_set) return COMMAND_INVALID; |
| 285 | if (params->lgwin != 0 && params->lgwin < BROTLI_MIN_WINDOW_BITS) { |
| 286 | return COMMAND_INVALID; |
| 287 | } |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 288 | } else if (c == 'S') { |
| 289 | if (suffix_set) return COMMAND_INVALID; |
| 290 | suffix_set = BROTLI_TRUE; |
| 291 | params->suffix = argv[i]; |
| 292 | } |
| 293 | } |
| 294 | } else { /* Double-dash. */ |
| 295 | arg = &arg[2]; |
| 296 | if (strcmp("best", arg) == 0) { |
| 297 | if (quality_set) return COMMAND_INVALID; |
| 298 | quality_set = BROTLI_TRUE; |
| 299 | params->quality = 11; |
| 300 | } else if (strcmp("decompress", arg) == 0) { |
| 301 | if (command_set) return COMMAND_INVALID; |
| 302 | command_set = BROTLI_TRUE; |
| 303 | command = COMMAND_DECOMPRESS; |
| 304 | } else if (strcmp("force", arg) == 0) { |
| 305 | if (params->force_overwrite) return COMMAND_INVALID; |
| 306 | params->force_overwrite = BROTLI_TRUE; |
| 307 | } else if (strcmp("help", arg) == 0) { |
| 308 | /* Don't parse further. */ |
| 309 | return COMMAND_HELP; |
| 310 | } else if (strcmp("keep", arg) == 0) { |
| 311 | if (keep_set) return COMMAND_INVALID; |
| 312 | keep_set = BROTLI_TRUE; |
| 313 | params->junk_source = BROTLI_FALSE; |
| 314 | } else if (strcmp("no-copy-stat", arg) == 0) { |
| 315 | if (!params->copy_stat) return COMMAND_INVALID; |
| 316 | params->copy_stat = BROTLI_FALSE; |
| 317 | } else if (strcmp("rm", arg) == 0) { |
| 318 | if (keep_set) return COMMAND_INVALID; |
| 319 | keep_set = BROTLI_TRUE; |
| 320 | params->junk_source = BROTLI_TRUE; |
| 321 | } else if (strcmp("stdout", arg) == 0) { |
| 322 | if (output_set) return COMMAND_INVALID; |
| 323 | output_set = BROTLI_TRUE; |
| 324 | params->write_to_stdout = BROTLI_TRUE; |
| 325 | } else if (strcmp("test", arg) == 0) { |
| 326 | if (command_set) return COMMAND_INVALID; |
| 327 | command_set = BROTLI_TRUE; |
| 328 | command = COMMAND_TEST_INTEGRITY; |
| 329 | } else if (strcmp("verbose", arg) == 0) { |
| 330 | if (params->verbose) return COMMAND_INVALID; |
| 331 | params->verbose = BROTLI_TRUE; |
| 332 | } else if (strcmp("version", arg) == 0) { |
| 333 | /* Don't parse further. */ |
| 334 | return COMMAND_VERSION; |
| 335 | } else { |
| 336 | /* key=value */ |
| 337 | const char* value = strrchr(arg, '='); |
| 338 | size_t key_len; |
| 339 | if (!value || value[1] == 0) return COMMAND_INVALID; |
| 340 | key_len = (size_t)(value - arg); |
| 341 | value++; |
Eugene Kliuchnikov | d63e8f7 | 2017-08-04 10:02:56 +0200 | [diff] [blame] | 342 | if (strncmp("lgwin", arg, key_len) == 0) { |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 343 | if (lgwin_set) return COMMAND_INVALID; |
| 344 | lgwin_set = ParseInt(value, 0, |
| 345 | BROTLI_MAX_WINDOW_BITS, ¶ms->lgwin); |
| 346 | if (!lgwin_set) return COMMAND_INVALID; |
| 347 | if (params->lgwin != 0 && params->lgwin < BROTLI_MIN_WINDOW_BITS) { |
| 348 | return COMMAND_INVALID; |
| 349 | } |
| 350 | } else if (strncmp("output", arg, key_len) == 0) { |
| 351 | if (output_set) return COMMAND_INVALID; |
| 352 | params->output_path = value; |
| 353 | } else if (strncmp("quality", arg, key_len) == 0) { |
| 354 | if (quality_set) return COMMAND_INVALID; |
| 355 | quality_set = ParseInt(value, BROTLI_MIN_QUALITY, |
| 356 | BROTLI_MAX_QUALITY, ¶ms->quality); |
| 357 | if (!quality_set) return COMMAND_INVALID; |
| 358 | } else if (strncmp("suffix", arg, key_len) == 0) { |
| 359 | if (suffix_set) return COMMAND_INVALID; |
| 360 | suffix_set = BROTLI_TRUE; |
| 361 | params->suffix = value; |
| 362 | } else { |
| 363 | return COMMAND_INVALID; |
| 364 | } |
| 365 | } |
| 366 | } |
| 367 | } |
| 368 | |
| 369 | params->input_count = input_count; |
| 370 | params->longest_path_len = longest_path_len; |
| 371 | params->decompress = (command == COMMAND_DECOMPRESS); |
| 372 | params->test_integrity = (command == COMMAND_TEST_INTEGRITY); |
| 373 | |
| 374 | if (input_count > 1 && output_set) return COMMAND_INVALID; |
| 375 | if (params->test_integrity) { |
| 376 | if (params->output_path) return COMMAND_INVALID; |
| 377 | if (params->write_to_stdout) return COMMAND_INVALID; |
| 378 | } |
| 379 | if (strchr(params->suffix, '/') || strchr(params->suffix, '\\')) { |
| 380 | return COMMAND_INVALID; |
| 381 | } |
| 382 | |
| 383 | return command; |
| 384 | } |
| 385 | |
| 386 | static void PrintVersion(void) { |
| 387 | int major = BROTLI_VERSION >> 24; |
| 388 | int minor = (BROTLI_VERSION >> 12) & 0xFFF; |
| 389 | int patch = BROTLI_VERSION & 0xFFF; |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 390 | fprintf(stdout, "brotli %d.%d.%d\n", major, minor, patch); |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 391 | } |
| 392 | |
| 393 | static void PrintHelp(const char* name) { |
| 394 | /* String is cut to pieces with length less than 509, to conform C90 spec. */ |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 395 | fprintf(stdout, |
| 396 | "Usage: %s [OPTION]... [FILE]...\n", |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 397 | name); |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 398 | fprintf(stdout, |
| 399 | "Options:\n" |
| 400 | " -# compression level (0-9)\n" |
| 401 | " -c, --stdout write on standard output\n" |
| 402 | " -d, --decompress decompress\n" |
| 403 | " -f, --force force output file overwrite\n" |
| 404 | " -h, --help display this help and exit\n"); |
| 405 | fprintf(stdout, |
| 406 | " -j, --rm remove source file(s)\n" |
| 407 | " -k, --keep keep source file(s) (default)\n" |
| 408 | " -n, --no-copy-stat do not copy source file(s) attributes\n" |
| 409 | " -o FILE, --output=FILE output file (only if 1 input file)\n"); |
| 410 | fprintf(stdout, |
| 411 | " -q NUM, --quality=NUM compression level (%d-%d)\n", |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 412 | BROTLI_MIN_QUALITY, BROTLI_MAX_QUALITY); |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 413 | fprintf(stdout, |
| 414 | " -t, --test test compressed file integrity\n" |
| 415 | " -v, --verbose verbose mode\n"); |
| 416 | fprintf(stdout, |
| 417 | " -w NUM, --lgwin=NUM set LZ77 window size (0, %d-%d) (default:%d)\n", |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 418 | BROTLI_MIN_WINDOW_BITS, BROTLI_MAX_WINDOW_BITS, DEFAULT_LGWIN); |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 419 | fprintf(stdout, |
| 420 | " window size = 2**NUM - 16\n" |
| 421 | " 0 lets compressor choose the optimal value\n"); |
| 422 | fprintf(stdout, |
| 423 | " -S SUF, --suffix=SUF output file suffix (default:'%s')\n", |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 424 | DEFAULT_SUFFIX); |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 425 | fprintf(stdout, |
| 426 | " -V, --version display version and exit\n" |
| 427 | " -Z, --best use best compression level (11) (default)\n" |
| 428 | "Simple options could be coalesced, i.e. '-9kf' is equivalent to '-9 -k -f'.\n" |
| 429 | "With no FILE, or when FILE is -, read standard input.\n" |
| 430 | "All arguments after '--' are treated as files.\n"); |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 431 | } |
| 432 | |
| 433 | static const char* PrintablePath(const char* path) { |
| 434 | return path ? path : "con"; |
| 435 | } |
| 436 | |
| 437 | static BROTLI_BOOL OpenInputFile(const char* input_path, FILE** f) { |
| 438 | *f = NULL; |
| 439 | if (!input_path) { |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 440 | *f = fdopen(MAKE_BINARY(STDIN_FILENO), "rb"); |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 441 | return BROTLI_TRUE; |
| 442 | } |
| 443 | *f = fopen(input_path, "rb"); |
| 444 | if (!*f) { |
| 445 | fprintf(stderr, "failed to open input file [%s]: %s\n", |
| 446 | PrintablePath(input_path), strerror(errno)); |
| 447 | return BROTLI_FALSE; |
| 448 | } |
| 449 | return BROTLI_TRUE; |
| 450 | } |
| 451 | |
| 452 | static BROTLI_BOOL OpenOutputFile(const char* output_path, FILE** f, |
| 453 | BROTLI_BOOL force) { |
| 454 | int fd; |
| 455 | *f = NULL; |
| 456 | if (!output_path) { |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 457 | *f = fdopen(MAKE_BINARY(STDOUT_FILENO), "wb"); |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 458 | return BROTLI_TRUE; |
| 459 | } |
| 460 | fd = open(output_path, O_CREAT | (force ? 0 : O_EXCL) | O_WRONLY | O_TRUNC, |
| 461 | S_IRUSR | S_IWUSR); |
| 462 | if (fd < 0) { |
| 463 | fprintf(stderr, "failed to open output file [%s]: %s\n", |
| 464 | PrintablePath(output_path), strerror(errno)); |
| 465 | return BROTLI_FALSE; |
| 466 | } |
| 467 | *f = fdopen(fd, "wb"); |
| 468 | if (!*f) { |
| 469 | fprintf(stderr, "failed to open output file [%s]: %s\n", |
| 470 | PrintablePath(output_path), strerror(errno)); |
| 471 | return BROTLI_FALSE; |
| 472 | } |
| 473 | return BROTLI_TRUE; |
| 474 | } |
| 475 | |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 476 | /* Copy file times and permissions. |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 477 | TODO: this is a "best effort" implementation; honest cross-platform |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 478 | fully featured implementation is way too hacky; add more hacks by request. */ |
| 479 | static void CopyStat(const char* input_path, const char* output_path) { |
| 480 | struct stat statbuf; |
| 481 | struct utimbuf times; |
| 482 | int res; |
| 483 | if (input_path == 0 || output_path == 0) { |
| 484 | return; |
| 485 | } |
| 486 | if (stat(input_path, &statbuf) != 0) { |
| 487 | return; |
| 488 | } |
| 489 | times.actime = statbuf.st_atime; |
| 490 | times.modtime = statbuf.st_mtime; |
| 491 | utime(output_path, ×); |
| 492 | res = chmod(output_path, statbuf.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)); |
| 493 | if (res != 0) { |
| 494 | fprintf(stderr, "setting access bits failed for [%s]: %s\n", |
| 495 | PrintablePath(output_path), strerror(errno)); |
| 496 | } |
| 497 | res = chown(output_path, (uid_t)-1, statbuf.st_gid); |
| 498 | if (res != 0) { |
| 499 | fprintf(stderr, "setting group failed for [%s]: %s\n", |
| 500 | PrintablePath(output_path), strerror(errno)); |
| 501 | } |
| 502 | res = chown(output_path, statbuf.st_uid, (gid_t)-1); |
| 503 | if (res != 0) { |
| 504 | fprintf(stderr, "setting user failed for [%s]: %s\n", |
| 505 | PrintablePath(output_path), strerror(errno)); |
| 506 | } |
| 507 | } |
| 508 | |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 509 | static BROTLI_BOOL NextFile(Context* context) { |
| 510 | const char* arg; |
| 511 | size_t arg_len; |
| 512 | |
| 513 | /* Iterator points to last used arg; increment to search for the next one. */ |
| 514 | context->iterator++; |
| 515 | |
| 516 | /* No input path; read from console. */ |
| 517 | if (context->input_count == 0) { |
| 518 | if (context->iterator > 1) return BROTLI_FALSE; |
| 519 | context->current_input_path = NULL; |
| 520 | /* Either write to the specified path, or to console. */ |
| 521 | context->current_output_path = context->output_path; |
| 522 | return BROTLI_TRUE; |
| 523 | } |
| 524 | |
| 525 | /* Skip option arguments. */ |
| 526 | while (context->iterator == context->not_input_indices[context->ignore]) { |
| 527 | context->iterator++; |
| 528 | context->ignore++; |
| 529 | } |
| 530 | |
| 531 | /* All args are scanned already. */ |
| 532 | if (context->iterator >= context->argc) return BROTLI_FALSE; |
| 533 | |
| 534 | /* Iterator now points to the input file name. */ |
| 535 | arg = context->argv[context->iterator]; |
| 536 | arg_len = strlen(arg); |
| 537 | /* Read from console. */ |
| 538 | if (arg_len == 1 && arg[0] == '-') { |
| 539 | context->current_input_path = NULL; |
| 540 | context->current_output_path = context->output_path; |
| 541 | return BROTLI_TRUE; |
| 542 | } |
| 543 | |
| 544 | context->current_input_path = arg; |
| 545 | context->current_output_path = context->output_path; |
| 546 | |
| 547 | if (context->output_path) return BROTLI_TRUE; |
| 548 | if (context->write_to_stdout) return BROTLI_TRUE; |
| 549 | |
| 550 | strcpy(context->modified_path, arg); |
| 551 | context->current_output_path = context->modified_path; |
| 552 | /* If output is not specified, input path suffix should match. */ |
| 553 | if (context->decompress) { |
| 554 | size_t suffix_len = strlen(context->suffix); |
| 555 | char* name = (char*)FileName(context->modified_path); |
| 556 | char* name_suffix; |
| 557 | size_t name_len = strlen(name); |
| 558 | if (name_len < suffix_len + 1) { |
| 559 | fprintf(stderr, "empty output file name for [%s] input file\n", |
| 560 | PrintablePath(arg)); |
| 561 | context->iterator_error = BROTLI_TRUE; |
| 562 | return BROTLI_FALSE; |
| 563 | } |
| 564 | name_suffix = name + name_len - suffix_len; |
| 565 | if (strcmp(context->suffix, name_suffix) != 0) { |
| 566 | fprintf(stderr, "input file [%s] suffix mismatch\n", |
| 567 | PrintablePath(arg)); |
| 568 | context->iterator_error = BROTLI_TRUE; |
| 569 | return BROTLI_FALSE; |
| 570 | } |
| 571 | name_suffix[0] = 0; |
| 572 | return BROTLI_TRUE; |
| 573 | } else { |
| 574 | strcpy(context->modified_path + arg_len, context->suffix); |
| 575 | return BROTLI_TRUE; |
| 576 | } |
| 577 | } |
| 578 | |
| 579 | static BROTLI_BOOL OpenFiles(Context* context) { |
| 580 | BROTLI_BOOL is_ok = OpenInputFile(context->current_input_path, &context->fin); |
| 581 | if (!context->test_integrity && is_ok) { |
| 582 | is_ok = OpenOutputFile( |
| 583 | context->current_output_path, &context->fout, context->force_overwrite); |
| 584 | } |
| 585 | return is_ok; |
| 586 | } |
| 587 | |
| 588 | static BROTLI_BOOL CloseFiles(Context* context, BROTLI_BOOL success) { |
| 589 | BROTLI_BOOL is_ok = BROTLI_TRUE; |
| 590 | if (!context->test_integrity && context->fout) { |
Eugene Kliuchnikov | c605635 | 2017-09-20 15:02:01 +0200 | [diff] [blame] | 591 | if (!success && context->current_output_path) { |
| 592 | unlink(context->current_output_path); |
| 593 | } |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 594 | if (fclose(context->fout) != 0) { |
| 595 | if (success) { |
| 596 | fprintf(stderr, "fclose failed [%s]: %s\n", |
| 597 | PrintablePath(context->current_output_path), strerror(errno)); |
| 598 | } |
| 599 | is_ok = BROTLI_FALSE; |
| 600 | } |
| 601 | |
| 602 | /* TOCTOU violation, but otherwise it is impossible to set file times. */ |
| 603 | if (success && is_ok && context->copy_stat) { |
| 604 | CopyStat(context->current_input_path, context->current_output_path); |
| 605 | } |
| 606 | } |
| 607 | |
Eugene Kliuchnikov | 5244106 | 2017-07-21 10:07:24 +0200 | [diff] [blame] | 608 | if (context->fin) { |
| 609 | if (fclose(context->fin) != 0) { |
| 610 | if (is_ok) { |
| 611 | fprintf(stderr, "fclose failed [%s]: %s\n", |
| 612 | PrintablePath(context->current_input_path), strerror(errno)); |
| 613 | } |
| 614 | is_ok = BROTLI_FALSE; |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 615 | } |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 616 | } |
Eugene Kliuchnikov | c605635 | 2017-09-20 15:02:01 +0200 | [diff] [blame] | 617 | if (success && context->junk_source && context->current_input_path) { |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 618 | unlink(context->current_input_path); |
| 619 | } |
| 620 | |
| 621 | context->fin = NULL; |
| 622 | context->fout = NULL; |
| 623 | |
| 624 | return is_ok; |
| 625 | } |
| 626 | |
| 627 | static const size_t kFileBufferSize = 1 << 16; |
| 628 | |
| 629 | static BROTLI_BOOL DecompressFile(Context* context, BrotliDecoderState* s) { |
Eugene Kliuchnikov | 05d5f3d | 2017-06-13 12:52:56 +0200 | [diff] [blame] | 630 | size_t available_in = 0; |
| 631 | const uint8_t* next_in = NULL; |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 632 | size_t available_out = kFileBufferSize; |
| 633 | uint8_t* next_out = context->output; |
| 634 | BrotliDecoderResult result = BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT; |
| 635 | for (;;) { |
| 636 | if (next_out != context->output) { |
| 637 | if (!context->test_integrity) { |
| 638 | size_t out_size = (size_t)(next_out - context->output); |
| 639 | fwrite(context->output, 1, out_size, context->fout); |
| 640 | if (ferror(context->fout)) { |
| 641 | fprintf(stderr, "failed to write output [%s]: %s\n", |
| 642 | PrintablePath(context->current_output_path), strerror(errno)); |
| 643 | return BROTLI_FALSE; |
| 644 | } |
| 645 | } |
| 646 | available_out = kFileBufferSize; |
| 647 | next_out = context->output; |
| 648 | } |
| 649 | |
| 650 | if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT) { |
| 651 | if (feof(context->fin)) { |
| 652 | fprintf(stderr, "corrupt input [%s]\n", |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 653 | PrintablePath(context->current_input_path)); |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 654 | return BROTLI_FALSE; |
| 655 | } |
| 656 | available_in = fread(context->input, 1, kFileBufferSize, context->fin); |
| 657 | next_in = context->input; |
| 658 | if (ferror(context->fin)) { |
| 659 | fprintf(stderr, "failed to read input [%s]: %s\n", |
| 660 | PrintablePath(context->current_input_path), strerror(errno)); |
| 661 | return BROTLI_FALSE; |
| 662 | } |
| 663 | } else if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { |
| 664 | /* Nothing to do - output is already written. */ |
| 665 | } else if (result == BROTLI_DECODER_RESULT_SUCCESS) { |
| 666 | if (available_in != 0 || !feof(context->fin)) { |
| 667 | fprintf(stderr, "corrupt input [%s]\n", |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 668 | PrintablePath(context->current_input_path)); |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 669 | return BROTLI_FALSE; |
| 670 | } |
| 671 | return BROTLI_TRUE; |
| 672 | } else { |
| 673 | fprintf(stderr, "corrupt input [%s]\n", |
Eugene Kliuchnikov | d7bce1e | 2017-09-07 20:27:49 +0200 | [diff] [blame] | 674 | PrintablePath(context->current_input_path)); |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 675 | return BROTLI_FALSE; |
| 676 | } |
| 677 | |
| 678 | result = BrotliDecoderDecompressStream( |
| 679 | s, &available_in, &next_in, &available_out, &next_out, 0); |
| 680 | } |
| 681 | } |
| 682 | |
| 683 | static BROTLI_BOOL DecompressFiles(Context* context) { |
| 684 | while (NextFile(context)) { |
| 685 | BROTLI_BOOL is_ok = BROTLI_TRUE; |
| 686 | BrotliDecoderState* s = BrotliDecoderCreateInstance(NULL, NULL, NULL); |
| 687 | if (!s) { |
| 688 | fprintf(stderr, "out of memory\n"); |
| 689 | return BROTLI_FALSE; |
| 690 | } |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 691 | is_ok = OpenFiles(context); |
Eugene Kliuchnikov | 37fb83e | 2017-09-19 15:57:15 +0200 | [diff] [blame] | 692 | if (is_ok && !context->current_input_path && |
| 693 | !context->force_overwrite && isatty(STDIN_FILENO)) { |
| 694 | fprintf(stderr, "Use -h help. Use -f to force input from a terminal.\n"); |
| 695 | is_ok = BROTLI_FALSE; |
| 696 | } |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 697 | if (is_ok) is_ok = DecompressFile(context, s); |
| 698 | BrotliDecoderDestroyInstance(s); |
| 699 | if (!CloseFiles(context, is_ok)) is_ok = BROTLI_FALSE; |
| 700 | if (!is_ok) return BROTLI_FALSE; |
| 701 | } |
| 702 | return BROTLI_TRUE; |
| 703 | } |
| 704 | |
| 705 | static BROTLI_BOOL CompressFile(Context* context, BrotliEncoderState* s) { |
| 706 | size_t available_in = 0; |
| 707 | const uint8_t* next_in = NULL; |
| 708 | size_t available_out = kFileBufferSize; |
| 709 | uint8_t* next_out = context->output; |
| 710 | BROTLI_BOOL is_eof = BROTLI_FALSE; |
| 711 | |
| 712 | for (;;) { |
| 713 | if (available_in == 0 && !is_eof) { |
| 714 | available_in = fread(context->input, 1, kFileBufferSize, context->fin); |
| 715 | next_in = context->input; |
| 716 | if (ferror(context->fin)) { |
| 717 | fprintf(stderr, "failed to read input [%s]: %s\n", |
| 718 | PrintablePath(context->current_input_path), strerror(errno)); |
| 719 | return BROTLI_FALSE; |
| 720 | } |
| 721 | is_eof = feof(context->fin) ? BROTLI_TRUE : BROTLI_FALSE; |
| 722 | } |
| 723 | |
| 724 | if (!BrotliEncoderCompressStream(s, |
| 725 | is_eof ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS, |
| 726 | &available_in, &next_in, &available_out, &next_out, NULL)) { |
| 727 | /* Should detect OOM? */ |
| 728 | fprintf(stderr, "failed to compress data [%s]\n", |
| 729 | PrintablePath(context->current_input_path)); |
| 730 | return BROTLI_FALSE; |
| 731 | } |
| 732 | |
| 733 | if (available_out != kFileBufferSize) { |
| 734 | size_t out_size = kFileBufferSize - available_out; |
| 735 | fwrite(context->output, 1, out_size, context->fout); |
| 736 | if (ferror(context->fout)) { |
| 737 | fprintf(stderr, "failed to write output [%s]: %s\n", |
| 738 | PrintablePath(context->current_output_path), strerror(errno)); |
| 739 | return BROTLI_FALSE; |
| 740 | } |
| 741 | available_out = kFileBufferSize; |
| 742 | next_out = context->output; |
| 743 | } |
| 744 | |
| 745 | if (BrotliEncoderIsFinished(s)) return BROTLI_TRUE; |
| 746 | } |
| 747 | } |
| 748 | |
| 749 | static BROTLI_BOOL CompressFiles(Context* context) { |
| 750 | while (NextFile(context)) { |
| 751 | BROTLI_BOOL is_ok = BROTLI_TRUE; |
| 752 | BrotliEncoderState* s = BrotliEncoderCreateInstance(NULL, NULL, NULL); |
| 753 | if (!s) { |
| 754 | fprintf(stderr, "out of memory\n"); |
| 755 | return BROTLI_FALSE; |
| 756 | } |
| 757 | BrotliEncoderSetParameter(s, |
| 758 | BROTLI_PARAM_QUALITY, (uint32_t)context->quality); |
| 759 | BrotliEncoderSetParameter(s, |
| 760 | BROTLI_PARAM_LGWIN, (uint32_t)context->lgwin); |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 761 | is_ok = OpenFiles(context); |
Eugene Kliuchnikov | 37fb83e | 2017-09-19 15:57:15 +0200 | [diff] [blame] | 762 | if (is_ok && !context->current_output_path && |
| 763 | !context->force_overwrite && isatty(STDOUT_FILENO)) { |
| 764 | fprintf(stderr, "Use -h help. Use -f to force output to a terminal.\n"); |
| 765 | is_ok = BROTLI_FALSE; |
| 766 | } |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 767 | if (is_ok) is_ok = CompressFile(context, s); |
| 768 | BrotliEncoderDestroyInstance(s); |
| 769 | if (!CloseFiles(context, is_ok)) is_ok = BROTLI_FALSE; |
| 770 | if (!is_ok) return BROTLI_FALSE; |
| 771 | } |
| 772 | return BROTLI_TRUE; |
| 773 | } |
| 774 | |
| 775 | int main(int argc, char** argv) { |
| 776 | Command command; |
| 777 | Context context; |
| 778 | BROTLI_BOOL is_ok = BROTLI_TRUE; |
| 779 | int i; |
| 780 | |
| 781 | context.quality = 11; |
| 782 | context.lgwin = DEFAULT_LGWIN; |
| 783 | context.force_overwrite = BROTLI_FALSE; |
| 784 | context.junk_source = BROTLI_FALSE; |
| 785 | context.copy_stat = BROTLI_TRUE; |
| 786 | context.test_integrity = BROTLI_FALSE; |
| 787 | context.verbose = BROTLI_FALSE; |
| 788 | context.write_to_stdout = BROTLI_FALSE; |
| 789 | context.decompress = BROTLI_FALSE; |
| 790 | context.output_path = NULL; |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 791 | context.suffix = DEFAULT_SUFFIX; |
| 792 | for (i = 0; i < MAX_OPTIONS; ++i) context.not_input_indices[i] = 0; |
| 793 | context.longest_path_len = 1; |
| 794 | context.input_count = 0; |
| 795 | |
| 796 | context.argc = argc; |
| 797 | context.argv = argv; |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 798 | context.modified_path = NULL; |
| 799 | context.iterator = 0; |
| 800 | context.ignore = 0; |
| 801 | context.iterator_error = BROTLI_FALSE; |
| 802 | context.buffer = NULL; |
| 803 | context.current_input_path = NULL; |
| 804 | context.current_output_path = NULL; |
| 805 | context.fin = NULL; |
| 806 | context.fout = NULL; |
| 807 | |
| 808 | command = ParseParams(&context); |
| 809 | |
| 810 | if (command == COMMAND_COMPRESS || command == COMMAND_DECOMPRESS || |
| 811 | command == COMMAND_TEST_INTEGRITY) { |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 812 | if (is_ok) { |
| 813 | size_t modified_path_len = |
| 814 | context.longest_path_len + strlen(context.suffix) + 1; |
| 815 | context.modified_path = (char*)malloc(modified_path_len); |
| 816 | context.buffer = (uint8_t*)malloc(kFileBufferSize * 2); |
| 817 | if (!context.modified_path || !context.buffer) { |
| 818 | fprintf(stderr, "out of memory\n"); |
| 819 | is_ok = BROTLI_FALSE; |
| 820 | } else { |
| 821 | context.input = context.buffer; |
| 822 | context.output = context.buffer + kFileBufferSize; |
| 823 | } |
| 824 | } |
| 825 | } |
| 826 | |
| 827 | if (!is_ok) command = COMMAND_NOOP; |
| 828 | |
| 829 | switch (command) { |
| 830 | case COMMAND_NOOP: |
| 831 | break; |
| 832 | |
| 833 | case COMMAND_VERSION: |
| 834 | PrintVersion(); |
| 835 | break; |
| 836 | |
| 837 | case COMMAND_COMPRESS: |
| 838 | is_ok = CompressFiles(&context); |
| 839 | break; |
| 840 | |
| 841 | case COMMAND_DECOMPRESS: |
| 842 | case COMMAND_TEST_INTEGRITY: |
| 843 | is_ok = DecompressFiles(&context); |
| 844 | break; |
| 845 | |
| 846 | case COMMAND_HELP: |
| 847 | case COMMAND_INVALID: |
| 848 | default: |
| 849 | PrintHelp(FileName(argv[0])); |
| 850 | is_ok = (command == COMMAND_HELP); |
| 851 | break; |
| 852 | } |
| 853 | |
| 854 | if (context.iterator_error) is_ok = BROTLI_FALSE; |
| 855 | |
Eugene Kliuchnikov | 03739d2 | 2017-05-29 17:55:14 +0200 | [diff] [blame] | 856 | free(context.modified_path); |
| 857 | free(context.buffer); |
| 858 | |
| 859 | if (!is_ok) exit(1); |
| 860 | return 0; |
| 861 | } |