compress.c (15863B)
1 #include <kit/compress.h> 2 #include <kit/core.h> 3 #include <stddef.h> 4 #include <stdint.h> 5 #include <string.h> 6 7 #include "driver.h" 8 #include "env.h" 9 10 /* `kit compress` — compress or decompress a stream with one of two standard, 11 * interoperable containers: gzip (`.gz`, default) or the LZ4 frame format 12 * (`.lz4`). Reads a FILE operand or stdin, writes to `-o OUT` or stdout. On 13 * decompress the format is auto-detected from the input's magic bytes unless 14 * `-z` forces it. Drives the public kit/compress.h API. 15 * 16 * The same core backs the standard-named aliases, each pinning a container and 17 * a default direction so it drops in for the matching command: 18 * gzip -> compress, gzip gunzip -> decompress, gzip 19 * lz4 -> compress, LZ4 frame lz4c -> compress, LZ4 frame (legacy name) 20 * Under an alias -z is rejected (the container is fixed), the common 21 * gzip/lz4 flags (-c/-k/-f, levels, ...) are accepted, and -d still flips the 22 * direction. kit always streams to stdout/`-o` and never rewrites the input in 23 * place, so the in-place file flags are deliberately not emulated. */ 24 25 #define COMPRESS_TOOL "compress" 26 27 /* Invocation personality. The generic `compress` leaves format and direction 28 * to flags; the aliases pin a container and a starting direction. */ 29 typedef struct CompressPersona { 30 const char* name; /* tool name for diagnostics: compress/gzip/... */ 31 KitCompressFormat format; /* default (and, when locked, the only) container */ 32 int locked_format; /* alias pins the container: -z is rejected, no 33 * decompress auto-detection */ 34 int default_decompress; /* alias starts in decompress mode (gunzip) */ 35 int compat; /* accept gzip/lz4-style compatibility flags */ 36 } CompressPersona; 37 38 static const CompressPersona CZ_GENERIC = {"compress", KIT_COMPRESS_GZIP, 0, 0, 39 0}; 40 static const CompressPersona CZ_GZIP = {"gzip", KIT_COMPRESS_GZIP, 1, 0, 1}; 41 static const CompressPersona CZ_GUNZIP = {"gunzip", KIT_COMPRESS_GZIP, 1, 1, 1}; 42 static const CompressPersona CZ_LZ4 = {"lz4", KIT_COMPRESS_LZ4_FRAME, 1, 0, 1}; 43 static const CompressPersona CZ_LZ4C = {"lz4c", KIT_COMPRESS_LZ4_FRAME, 1, 0, 44 1}; 45 46 typedef struct CompressOpts { 47 int decompress; /* -d */ 48 int have_format; /* whether -z/--format (or a pinned container) was given */ 49 KitCompressFormat format; 50 int seen_input; /* a FILE or `-` operand was seen */ 51 const char* in; /* input path, or NULL = stdin */ 52 const char* out; /* output path, or NULL = stdout */ 53 } CompressOpts; 54 55 static int compress_parse_format(const char* s, KitCompressFormat* out) { 56 if (driver_streq(s, "gzip") || driver_streq(s, "gz")) { 57 *out = KIT_COMPRESS_GZIP; 58 return 0; 59 } 60 if (driver_streq(s, "lz4")) { 61 *out = KIT_COMPRESS_LZ4_FRAME; 62 return 0; 63 } 64 return 1; 65 } 66 67 static void compress_unknown_format(const char* tool, const char* value) { 68 const char* const formats[] = {"gzip", "gz", "lz4"}; 69 DriverSuggestion suggestions[3]; 70 size_t n = driver_suggest_values(value, formats, 71 sizeof formats / sizeof formats[0], 72 suggestions, 3); 73 if (n) 74 driver_errf(tool, "unknown format: %s; did you mean '%s'?", value, 75 suggestions[0].value); 76 else 77 driver_errf(tool, "unknown format: %s (use gzip or lz4)", value); 78 } 79 80 /* gzip/lz4-style flags accepted (as no-ops) under an alias so the tool works 81 * as a drop-in. kit always streams stdin/-o to stdout and never mutates the 82 * input in place, so -c/--stdout, -k/--keep and -f/--force have nothing to do; 83 * the codecs expose no level, so -1..-9 / --fast[=N] / --best are ignored too. 84 * Direction (-d) and the long decompress spellings are handled by the caller. 85 * Returns 1 if `a` is one of these accepted no-ops. */ 86 static int compress_compat_noop(const char* a) { 87 if (driver_streq(a, "-c") || driver_streq(a, "--stdout") || 88 driver_streq(a, "--to-stdout")) 89 return 1; 90 if (driver_streq(a, "-k") || driver_streq(a, "--keep")) return 1; 91 if (driver_streq(a, "-f") || driver_streq(a, "--force")) return 1; 92 if (driver_streq(a, "-n") || driver_streq(a, "--no-name")) return 1; 93 if (driver_streq(a, "-q") || driver_streq(a, "--quiet")) return 1; 94 if (driver_streq(a, "--best") || driver_streq(a, "--fast")) return 1; 95 if (driver_strneq(a, "--fast=", 7)) return 1; 96 /* -N / -NN compression level (gzip 1-9, lz4 up to 12): a dash then digits. */ 97 if (a[0] == '-' && a[1] >= '1' && a[1] <= '9') { 98 size_t k = 2; 99 while (a[k] >= '0' && a[k] <= '9') ++k; 100 if (a[k] == '\0') return 1; 101 } 102 return 0; 103 } 104 105 static void compress_help(const CompressPersona* persona) { 106 if (!persona->locked_format) { 107 driver_printf( 108 "%.*s", 109 KIT_SLICE_ARG(KIT_SLICE_LIT( 110 "kit compress — compress or decompress gzip and LZ4 frames\n" 111 "\n" 112 "USAGE\n" 113 " kit compress [-z FMT] [-o OUT] [FILE]\n" 114 " kit compress -d [-z FMT] [-o OUT] [FILE]\n" 115 "\n" 116 "DESCRIPTION\n" 117 " Reads FILE, or stdin when FILE is absent or -, and writes to\n" 118 " OUT (stdout by default). gzip and LZ4-frame output interoperates\n" 119 " with standard gzip/lz4 tools. Input files are never rewritten.\n" 120 "\n" 121 "OPTIONS\n" 122 " -d decompress (default: compress)\n" 123 " -z, --format FMT gzip (compression default) | lz4. On\n" 124 " decompression, magic auto-detects the format\n" 125 " unless this option fixes it.\n" 126 " -o OUT write OUT instead of stdout\n" 127 " -h, --help show this help and exit\n" 128 "\n" 129 "ALIASES\n" 130 " gzip, gunzip, lz4, and lz4c fix the container. Their own help\n" 131 " describes default direction and accepted compatibility flags.\n" 132 "\n" 133 "PATHS\n" 134 " Use -- before a leading-dash file name.\n" 135 "\n" 136 "EXAMPLES\n" 137 " kit compress -z gzip -o data.gz data.txt\n" 138 " kit compress -d -o data.copy data.gz\n" 139 " kit compress -z lz4 data.txt > data.lz4\n" 140 " kit compress -d data.lz4 > data.copy\n" 141 "\n" 142 "EXIT CODES\n" 143 " 0 success 1 I/O or codec error 2 bad usage\n"))); 144 return; 145 } 146 147 if (persona == &CZ_GZIP) { 148 driver_printf( 149 "gzip — write a gzip stream\n" 150 "\n" 151 "USAGE\n" 152 " gzip [OPTIONS] [FILE]\n" 153 "\n" 154 "DESCRIPTION\n" 155 " Compresses FILE, or stdin when omitted or -, as gzip. Output goes\n" 156 " to stdout unless -o is given; FILE is never renamed or rewritten.\n" 157 "\n" 158 "OPTIONS\n" 159 " -d, --decompress, --uncompress decompress gzip instead\n" 160 " -o OUT write OUT instead of stdout\n" 161 " -c/--stdout, -k, -f, -n, -q, -1..-9, --fast, --best\n" 162 " accepted compatibility no-ops\n" 163 " -h, --help show this help and exit\n" 164 " -z/--format is rejected because gzip fixes the container.\n" 165 "\n" 166 "PATHS\n" 167 " Use -- before a leading-dash file name.\n" 168 "\n" 169 "EXAMPLES\n" 170 " gzip -o data.gz data.txt\n" 171 " gzip -d -o data.copy data.gz\n" 172 " printf 'hello\\n' | gzip > hello.gz\n" 173 "\n" 174 "EXIT CODES\n" 175 " 0 success 1 I/O or codec error 2 bad usage\n"); 176 } else if (persona == &CZ_GUNZIP) { 177 driver_printf( 178 "gunzip — decompress a gzip stream\n" 179 "\n" 180 "USAGE\n" 181 " gunzip [OPTIONS] [FILE]\n" 182 "\n" 183 "DESCRIPTION\n" 184 " Decompresses gzip from FILE, or stdin when omitted or -. Output\n" 185 " goes to stdout unless -o is given; FILE is never renamed or removed.\n" 186 " Empty, corrupt, or non-gzip input is an operational error.\n" 187 "\n" 188 "OPTIONS\n" 189 " -d, --decompress, --uncompress accepted; remains decompression\n" 190 " -o OUT write OUT instead of stdout\n" 191 " -c/--stdout, -k, -f, -n, -q accepted compatibility no-ops\n" 192 " -h, --help show this help and exit\n" 193 " -z/--format is rejected because gunzip fixes gzip input.\n" 194 "\n" 195 "PATHS\n" 196 " Use -- before a leading-dash file name.\n" 197 "\n" 198 "EXAMPLES\n" 199 " gunzip -o data.txt data.gz\n" 200 " cat data.gz | gunzip > data.txt\n" 201 "\n" 202 "EXIT CODES\n" 203 " 0 success 1 I/O or codec error 2 bad usage\n"); 204 } else { 205 driver_printf( 206 "%s — write an LZ4-frame stream%s\n" 207 "\n" 208 "USAGE\n" 209 " %s [OPTIONS] [FILE]\n" 210 "\n" 211 "DESCRIPTION\n" 212 " Compresses FILE, or stdin when omitted or -, as an LZ4 frame.\n" 213 " Output goes to stdout unless -o is given; FILE is never rewritten.\n" 214 "%s" 215 "\n" 216 "OPTIONS\n" 217 " -d, --decompress, --uncompress decompress LZ4-frame input\n" 218 " -o OUT write OUT instead of stdout\n" 219 " -c/--stdout, -k, -f, -n, -q, -1..-9, --fast, --best\n" 220 " accepted compatibility no-ops\n" 221 " -h, --help show this help and exit\n" 222 " -z/--format is rejected because the container is fixed.\n" 223 "\n" 224 "PATHS\n" 225 " Use -- before a leading-dash file name.\n" 226 "\n" 227 "EXAMPLES\n" 228 " %s -o data.lz4 data.txt\n" 229 " %s -d -o data.copy data.lz4\n" 230 "\n" 231 "EXIT CODES\n" 232 " 0 success 1 I/O or codec error 2 bad usage\n", 233 persona->name, persona == &CZ_LZ4C ? " (lz4 alias)" : "", 234 persona->name, 235 persona == &CZ_LZ4C 236 ? " lz4c is the legacy compression alias for lz4.\n" 237 : "", 238 persona->name, persona->name); 239 } 240 } 241 242 void driver_help_compress(void) { compress_help(&CZ_GENERIC); } 243 void driver_help_gzip(void) { compress_help(&CZ_GZIP); } 244 void driver_help_gunzip(void) { compress_help(&CZ_GUNZIP); } 245 void driver_help_lz4(void) { compress_help(&CZ_LZ4); } 246 void driver_help_lz4c(void) { compress_help(&CZ_LZ4C); } 247 248 static int compress_main(int argc, char** argv, 249 const CompressPersona* persona) { 250 DriverEnv env; 251 KitContext ctx; 252 CompressOpts o; 253 KitWriter* w = NULL; 254 const uint8_t* data = NULL; 255 size_t len = 0; 256 DriverLoad ld = {0}; 257 uint8_t* sbuf = NULL; 258 size_t sbuf_len = 0; 259 int loaded = 0, owned_writer = 0, i, rc = 2, options = 1; 260 KitCompressFormat fmt; 261 262 if (driver_argv_wants_help(argc, argv, 1)) { 263 compress_help(persona); 264 return 0; 265 } 266 267 memset(&o, 0, sizeof o); 268 o.format = persona->format; 269 /* A pinned container both fixes the format and disables decompress 270 * auto-detection (gunzip only reads gzip, lz4 only reads LZ4). */ 271 o.have_format = persona->locked_format; 272 o.decompress = persona->default_decompress; 273 driver_env_init(&env); 274 ctx = driver_env_to_context(&env); 275 276 for (i = 1; i < argc; ++i) { 277 const char* a = argv[i]; 278 if (options && driver_streq(a, "--")) { 279 options = 0; 280 continue; 281 } 282 if (options && (driver_streq(a, "-d") || 283 (persona->compat && (driver_streq(a, "--decompress") || 284 driver_streq(a, "--uncompress"))))) { 285 o.decompress = 1; 286 continue; 287 } 288 if (options && 289 (driver_streq(a, "-z") || driver_streq(a, "--format"))) { 290 if (persona->locked_format) { 291 driver_errf(persona->name, "-z is not accepted; %s always uses %s", 292 persona->name, 293 persona->format == KIT_COMPRESS_GZIP ? "gzip" : "lz4"); 294 goto done; 295 } 296 if (i + 1 >= argc) { 297 driver_errf(persona->name, "%s requires gzip or lz4", a); 298 goto done; 299 } 300 ++i; 301 if (compress_parse_format(argv[i], &o.format) != 0) { 302 compress_unknown_format(persona->name, argv[i]); 303 goto done; 304 } 305 o.have_format = 1; 306 continue; 307 } 308 if (options && driver_streq(a, "-o")) { 309 if (i + 1 >= argc) { 310 driver_errf(persona->name, "-o requires a file path"); 311 goto done; 312 } 313 o.out = argv[++i]; 314 continue; 315 } 316 if (driver_streq(a, "-")) { /* explicit stdin */ 317 if (o.seen_input) { 318 driver_errf(persona->name, "only one input may be given"); 319 goto done; 320 } 321 o.seen_input = 1; 322 continue; 323 } 324 if (options && persona->compat && compress_compat_noop(a)) continue; 325 if (options && a[0] == '-' && a[1] != '\0') { 326 const char* const valid[] = { 327 "-d", "--decompress", "--uncompress", "-z", "--format", 328 "-o", "-h", "--help", "--version"}; 329 DriverSuggestion suggestions[3]; 330 size_t n = driver_suggest_values(a, valid, 331 sizeof valid / sizeof valid[0], 332 suggestions, 3); 333 if (n) 334 driver_errf(persona->name, 335 "unknown option: %s; did you mean '%s'?", a, 336 suggestions[0].value); 337 else 338 driver_errf(persona->name, "unknown option: %s", a); 339 goto done; 340 } 341 if (o.seen_input) { 342 driver_errf(persona->name, "only one input may be given"); 343 goto done; 344 } 345 o.seen_input = 1; 346 o.in = a; 347 } 348 349 /* Load the whole input. */ 350 if (o.in) { 351 KitSlice input; 352 if (driver_load_bytes(&env.file_io, persona->name, o.in, &ld, &input) != 353 0) { 354 rc = 1; 355 goto done; 356 } 357 loaded = 1; 358 data = input.data; 359 len = input.len; 360 } else { 361 if (!driver_read_stdin(&env, &sbuf, &sbuf_len)) { 362 driver_errf(persona->name, "failed to read stdin"); 363 rc = 1; 364 goto done; 365 } 366 data = sbuf; 367 len = sbuf_len; 368 } 369 370 /* Resolve the format. Compress uses the requested/default codec; decompress 371 * auto-detects from magic bytes unless -z forced one. */ 372 fmt = o.format; 373 if (o.decompress && !o.have_format) { 374 if (kit_compress_detect(data, len, &fmt) != KIT_OK) { 375 driver_errf(persona->name, "cannot detect input format; use -z gzip|lz4"); 376 rc = 1; 377 goto done; 378 } 379 } 380 381 /* Open the output. */ 382 if (o.out) { 383 if (ctx.file_io->open_writer(ctx.file_io->user, o.out, &w) != KIT_OK) { 384 driver_errf(persona->name, "failed to open output: %s", o.out); 385 rc = 1; 386 goto done; 387 } 388 owned_writer = 1; 389 } else { 390 w = driver_stdout_writer(&env); 391 owned_writer = 1; 392 } 393 394 /* Transform. The public API reports the specific reason through ctx->diag 395 * (the driver's stderr sink), so we only set the exit status here. */ 396 if (o.decompress) { 397 rc = kit_decompress(&ctx, fmt, data, len, w) == KIT_OK ? 0 : 1; 398 } else { 399 rc = kit_compress(&ctx, fmt, data, len, w) == KIT_OK ? 0 : 1; 400 } 401 402 done: 403 if (owned_writer && w) kit_writer_close(w); 404 if (sbuf) driver_free(&env, sbuf, sbuf_len); 405 if (loaded) driver_release_bytes(&env.file_io, &ld); 406 driver_env_fini(&env); 407 return rc; 408 } 409 410 int driver_compress(int argc, char** argv) { 411 return compress_main(argc, argv, &CZ_GENERIC); 412 } 413 414 int driver_gzip(int argc, char** argv) { 415 return compress_main(argc, argv, &CZ_GZIP); 416 } 417 418 int driver_gunzip(int argc, char** argv) { 419 return compress_main(argc, argv, &CZ_GUNZIP); 420 } 421 422 int driver_lz4(int argc, char** argv) { 423 return compress_main(argc, argv, &CZ_LZ4); 424 } 425 426 int driver_lz4c(int argc, char** argv) { 427 return compress_main(argc, argv, &CZ_LZ4C); 428 }