blob: 497ae65518b16fca85dd4c0ba6ac173034ef249b [file] [log] [blame]
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +02001/* 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 Kliuchnikov37fb83e2017-09-19 15:57:15 +020018#include "../common/constants.h"
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +020019#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 Kliuchnikovd7bce1e2017-09-07 20:27:49 +020026#define MAKE_BINARY(FILENO) (FILENO)
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +020027#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 Kliuchnikovd7bce1e2017-09-07 20:27:49 +020035#define STDIN_FILENO _fileno(stdin)
36#define STDOUT_FILENO _fileno(stdout)
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +020037#define S_IRUSR S_IREAD
38#define S_IWUSR S_IWRITE
39#endif
40
41#define fdopen _fdopen
Eugene Kliuchnikov37fb83e2017-09-19 15:57:15 +020042#define isatty _isatty
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +020043#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
58static FILE* ms_fopen(const char* filename, const char* mode) {
59 FILE* result = 0;
60 fopen_s(&result, filename, mode);
61 return result;
62}
63
64static 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
71typedef 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
85typedef 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 Kliuchnikov03739d22017-05-29 17:55:14 +020097 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200105 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. */
119static 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 '\'. */
137static 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. */
146static 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 Kliuchnikov65354352017-08-24 13:29:48 +0200152 if (strncmp(name, unbrotli, unbrotli_len) == 0) {
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200153 char terminator = name[unbrotli_len];
154 if (terminator == 0 || terminator == '.') return COMMAND_DECOMPRESS;
155 }
156 return COMMAND_COMPRESS;
157}
158
159static 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200177 /* 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 Kliuchnikovc6056352017-09-20 15:02:01 +0200179 size_t arg_len = arg ? strlen(arg) : 0;
180
181 if (arg_len == 0) {
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200182 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, &params->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, &params->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 Kliuchnikov03739d22017-05-29 17:55:14 +0200288 } 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 Kliuchnikovd63e8f72017-08-04 10:02:56 +0200342 if (strncmp("lgwin", arg, key_len) == 0) {
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200343 if (lgwin_set) return COMMAND_INVALID;
344 lgwin_set = ParseInt(value, 0,
345 BROTLI_MAX_WINDOW_BITS, &params->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, &params->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
386static void PrintVersion(void) {
387 int major = BROTLI_VERSION >> 24;
388 int minor = (BROTLI_VERSION >> 12) & 0xFFF;
389 int patch = BROTLI_VERSION & 0xFFF;
Eugene Kliuchnikovd7bce1e2017-09-07 20:27:49 +0200390 fprintf(stdout, "brotli %d.%d.%d\n", major, minor, patch);
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200391}
392
393static void PrintHelp(const char* name) {
394 /* String is cut to pieces with length less than 509, to conform C90 spec. */
Eugene Kliuchnikovd7bce1e2017-09-07 20:27:49 +0200395 fprintf(stdout,
396"Usage: %s [OPTION]... [FILE]...\n",
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200397 name);
Eugene Kliuchnikovd7bce1e2017-09-07 20:27:49 +0200398 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200412 BROTLI_MIN_QUALITY, BROTLI_MAX_QUALITY);
Eugene Kliuchnikovd7bce1e2017-09-07 20:27:49 +0200413 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200418 BROTLI_MIN_WINDOW_BITS, BROTLI_MAX_WINDOW_BITS, DEFAULT_LGWIN);
Eugene Kliuchnikovd7bce1e2017-09-07 20:27:49 +0200419 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200424 DEFAULT_SUFFIX);
Eugene Kliuchnikovd7bce1e2017-09-07 20:27:49 +0200425 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200431}
432
433static const char* PrintablePath(const char* path) {
434 return path ? path : "con";
435}
436
437static BROTLI_BOOL OpenInputFile(const char* input_path, FILE** f) {
438 *f = NULL;
439 if (!input_path) {
Eugene Kliuchnikovd7bce1e2017-09-07 20:27:49 +0200440 *f = fdopen(MAKE_BINARY(STDIN_FILENO), "rb");
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200441 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
452static BROTLI_BOOL OpenOutputFile(const char* output_path, FILE** f,
453 BROTLI_BOOL force) {
454 int fd;
455 *f = NULL;
456 if (!output_path) {
Eugene Kliuchnikovd7bce1e2017-09-07 20:27:49 +0200457 *f = fdopen(MAKE_BINARY(STDOUT_FILENO), "wb");
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200458 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200476/* Copy file times and permissions.
Eugene Kliuchnikovd7bce1e2017-09-07 20:27:49 +0200477 TODO: this is a "best effort" implementation; honest cross-platform
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200478 fully featured implementation is way too hacky; add more hacks by request. */
479static 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, &times);
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 Kliuchnikov03739d22017-05-29 17:55:14 +0200509static 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
579static 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
588static BROTLI_BOOL CloseFiles(Context* context, BROTLI_BOOL success) {
589 BROTLI_BOOL is_ok = BROTLI_TRUE;
590 if (!context->test_integrity && context->fout) {
Eugene Kliuchnikovc6056352017-09-20 15:02:01 +0200591 if (!success && context->current_output_path) {
592 unlink(context->current_output_path);
593 }
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200594 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 Kliuchnikov52441062017-07-21 10:07:24 +0200608 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200615 }
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200616 }
Eugene Kliuchnikovc6056352017-09-20 15:02:01 +0200617 if (success && context->junk_source && context->current_input_path) {
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200618 unlink(context->current_input_path);
619 }
620
621 context->fin = NULL;
622 context->fout = NULL;
623
624 return is_ok;
625}
626
627static const size_t kFileBufferSize = 1 << 16;
628
629static BROTLI_BOOL DecompressFile(Context* context, BrotliDecoderState* s) {
Eugene Kliuchnikov05d5f3d2017-06-13 12:52:56 +0200630 size_t available_in = 0;
631 const uint8_t* next_in = NULL;
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200632 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 Kliuchnikovd7bce1e2017-09-07 20:27:49 +0200653 PrintablePath(context->current_input_path));
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200654 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 Kliuchnikovd7bce1e2017-09-07 20:27:49 +0200668 PrintablePath(context->current_input_path));
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200669 return BROTLI_FALSE;
670 }
671 return BROTLI_TRUE;
672 } else {
673 fprintf(stderr, "corrupt input [%s]\n",
Eugene Kliuchnikovd7bce1e2017-09-07 20:27:49 +0200674 PrintablePath(context->current_input_path));
Eugene Kliuchnikov03739d22017-05-29 17:55:14 +0200675 return BROTLI_FALSE;
676 }
677
678 result = BrotliDecoderDecompressStream(
679 s, &available_in, &next_in, &available_out, &next_out, 0);
680 }
681}
682
683static 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200691 is_ok = OpenFiles(context);
Eugene Kliuchnikov37fb83e2017-09-19 15:57:15 +0200692 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200697 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
705static 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
749static 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200761 is_ok = OpenFiles(context);
Eugene Kliuchnikov37fb83e2017-09-19 15:57:15 +0200762 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200767 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
775int 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200791 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200798 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200812 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 Kliuchnikov03739d22017-05-29 17:55:14 +0200856 free(context.modified_path);
857 free(context.buffer);
858
859 if (!is_ok) exit(1);
860 return 0;
861}