kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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 }