LCOV - code coverage report
Current view: top level - lib - cmdline.c (source / functions) Hit Total Coverage
Test: coverage.info Lines: 607 813 74.7 %
Date: 2015-09-30 14:09:30 Functions: 56 66 84.8 %

          Line data    Source code
       1             : /*
       2             :  *  This file is part of rmlint.
       3             :  *
       4             :  *  rmlint is free software: you can redistribute it and/or modify
       5             :  *  it under the terms of the GNU General Public License as published by
       6             :  *  the Free Software Foundation, either version 3 of the License, or
       7             :  *  (at your option) any later version.
       8             :  *
       9             :  *  rmlint is distributed in the hope that it will be useful,
      10             :  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  *  GNU General Public License for more details.
      13             :  *
      14             :  *  You should have received a copy of the GNU General Public License
      15             :  *  along with rmlint.  If not, see <http://www.gnu.org/licenses/>.
      16             :  *
      17             :  * Authors:
      18             :  *
      19             :  *  - Christopher <sahib> Pahl 2010-2015 (https://github.com/sahib)
      20             :  *  - Daniel <SeeSpotRun> T.   2014-2015 (https://github.com/SeeSpotRun)
      21             :  *
      22             :  * Hosted on http://github.com/sahib/rmlint
      23             :  *
      24             :  */
      25             : 
      26             : #include "config.h"
      27             : 
      28             : #include <stdio.h>
      29             : #include <stdlib.h>
      30             : #include <unistd.h>
      31             : #include <string.h>
      32             : #include <ctype.h>
      33             : #include <math.h>
      34             : #include <time.h>
      35             : 
      36             : #include <errno.h>
      37             : #include <search.h>
      38             : #include <sys/time.h>
      39             : #include <glib.h>
      40             : #include <glib/gstdio.h>
      41             : 
      42             : #include "cmdline.h"
      43             : #include "treemerge.h"
      44             : #include "traverse.h"
      45             : #include "preprocess.h"
      46             : #include "shredder.h"
      47             : #include "utilities.h"
      48             : #include "formats.h"
      49             : #include "replay.h"
      50             : #include "hash-utility.h"
      51             : 
      52             : #if HAVE_BTRFS_H
      53             : #include <sys/ioctl.h>
      54             : #include <linux/btrfs.h>
      55             : #endif
      56             : 
      57        1886 : static void rm_cmd_show_version(void) {
      58        1886 :     fprintf(stderr, "version %s compiled: %s at [%s] \"%s\" (rev %s)\n", RM_VERSION,
      59             :             __DATE__, __TIME__, RM_VERSION_NAME, RM_VERSION_GIT_REVISION);
      60             : 
      61             :     /* Make a list of all supported features from the macros in config.h */
      62             :     /* clang-format off */
      63             :     struct {
      64             :         bool enabled : 1;
      65             :         const char *name;
      66        1886 :     } features[] = {{.name = "mounts",         .enabled = HAVE_BLKID & HAVE_GIO_UNIX},
      67             :                     {.name = "nonstripped",    .enabled = HAVE_LIBELF},
      68             :                     {.name = "fiemap",         .enabled = HAVE_FIEMAP},
      69             :                     {.name = "sha512",         .enabled = HAVE_SHA512},
      70             :                     {.name = "bigfiles",       .enabled = HAVE_BIGFILES},
      71             :                     {.name = "intl",           .enabled = HAVE_LIBINTL},
      72             :                     {.name = "json-cache",     .enabled = HAVE_JSON_GLIB},
      73             :                     {.name = "xattr",          .enabled = HAVE_XATTR},
      74             :                     {.name = "metadata-cache", .enabled = HAVE_SQLITE3},
      75             :                     {.name = "btrfs-support",  .enabled = HAVE_BTRFS_H},
      76             :                     {.name = NULL,             .enabled = 0}};
      77             :     /* clang-format on */
      78             : 
      79        1886 :     fprintf(stderr, _("compiled with:"));
      80       20746 :     for(int i = 0; features[i].name; ++i) {
      81       18860 :         fprintf(stderr, " %c%s", (features[i].enabled) ? '+' : '-', features[i].name);
      82             :     }
      83             : 
      84        1886 :     fprintf(stderr, RESET "\n\n");
      85        1886 :     fprintf(stderr, _("rmlint was written by Christopher <sahib> Pahl and Daniel "
      86             :                       "<SeeSpotRun> Thomas."));
      87        1886 :     fprintf(stderr, "\n");
      88        1886 :     fprintf(stderr, _("The code at https://github.com/sahib/rmlint is licensed under the "
      89             :                       "terms of the GPLv3."));
      90        1886 :     fprintf(stderr, "\n");
      91        1886 :     exit(0);
      92             : }
      93             : 
      94           1 : static void rm_cmd_show_manpage(void) {
      95             :     static const char *commands[] = {"man %s docs/rmlint.1.gz 2> /dev/null",
      96             :                                      "man %s rmlint", NULL};
      97             : 
      98           1 :     bool found_manpage = false;
      99             : 
     100           2 :     for(int i = 0; commands[i] && !found_manpage; ++i) {
     101           1 :         char cmd_buf[512] = {0};
     102           1 :         if(snprintf(cmd_buf, sizeof(cmd_buf), commands[i],
     103             :                     (RM_MANPAGE_USE_PAGER) ? "" : "-P cat") == -1) {
     104           0 :             continue;
     105             :         }
     106             : 
     107           1 :         if(system(cmd_buf) == 0) {
     108           1 :             found_manpage = true;
     109             :         }
     110             :     }
     111             : 
     112           1 :     if(!found_manpage) {
     113           0 :         rm_log_warning_line(_("You seem to have no manpage for rmlint."));
     114             :     }
     115             : 
     116           1 :     exit(0);
     117             : }
     118             : 
     119           0 : static void rm_cmd_start_gui(int argc, const char **argv) {
     120           0 :     const char *commands[] = {"python3", "python", NULL};
     121           0 :     const char **command = &commands[0];
     122             : 
     123           0 :     while(*command) {
     124             :         const char *all_argv[512];
     125           0 :         const char **argp = &all_argv[0];
     126           0 :         memset(all_argv, 0, sizeof(all_argv));
     127             : 
     128           0 :         *argp++ = *command;
     129           0 :         *argp++ = "-m";
     130           0 :         *argp++ = "shredder";
     131             : 
     132           0 :         for(size_t i = 0; i < (size_t)argc && i < sizeof(all_argv) / 2; i++) {
     133           0 :             *argp++ = argv[i];
     134             :         }
     135             : 
     136           0 :         if(execvp(*command, (char *const *)all_argv) == -1) {
     137           0 :             rm_log_warning("Executed: %s ", *command);
     138           0 :             for(int j = 0; j < (argp - all_argv); j++) {
     139           0 :                 rm_log_warning("%s ", all_argv[j]);
     140             :             }
     141           0 :             rm_log_warning("\n");
     142           0 :             rm_log_error_line("%s %d", g_strerror(errno), errno == ENOENT);
     143             :         } else {
     144             :             /* This is not reached anymore when execve suceeded */
     145           0 :             break;
     146             :         }
     147             : 
     148             :         /* Try next command... */
     149           0 :         command++;
     150             :     }
     151           0 : }
     152             : 
     153       54884 : static int rm_cmd_maybe_switch_to_gui(int argc, const char **argv) {
     154      756659 :     for(int i = 0; i < argc; i++) {
     155      701775 :         if(g_strcmp0("--gui", argv[i]) == 0) {
     156           0 :             argv[i] = "shredder";
     157           0 :             rm_cmd_start_gui(argc - i - 1, &argv[i + 1]);
     158             : 
     159             :             /* We returned? Something's wrong */
     160           0 :             return EXIT_FAILURE;
     161             :         }
     162             :     }
     163             : 
     164       54884 :     return EXIT_SUCCESS;
     165             : }
     166             : 
     167       54884 : static int rm_cmd_maybe_switch_to_hasher(int argc, const char **argv) {
     168      756659 :     for(int i = 0; i < argc; i++) {
     169      701775 :         if(g_strcmp0("--hash", argv[i]) == 0) {
     170           0 :             argv[i] = argv[0];
     171           0 :             exit(rm_hasher_main(argc - i, &argv[i]));
     172             :         }
     173             :     }
     174             : 
     175       54884 :     return EXIT_SUCCESS;
     176             : }
     177             : 
     178           0 : static void rm_cmd_btrfs_clone_usage(void) {
     179           0 :     rm_log_error(_("Usage: rmlint --btrfs-clone source dest\n"));
     180           0 : }
     181             : 
     182           0 : static void rm_cmd_btrfs_clone(const char *source, const char *dest) {
     183             : #if HAVE_BTRFS_H
     184             :     struct {
     185             :         struct btrfs_ioctl_same_args args;
     186             :         struct btrfs_ioctl_same_extent_info info;
     187             :     } extent_same;
     188           0 :     memset(&extent_same, 0, sizeof(extent_same));
     189             : 
     190           0 :     int source_fd = rm_sys_open(source, O_RDONLY);
     191           0 :     if(source_fd < 0) {
     192           0 :         rm_log_error_line(_("btrfs clone: failed to open source file"));
     193           0 :         return;
     194             :     }
     195             : 
     196           0 :     extent_same.info.fd = rm_sys_open(dest, O_RDWR);
     197           0 :     if(extent_same.info.fd < 0) {
     198           0 :         rm_log_error_line(_("btrfs clone: failed to open dest file."));
     199           0 :         rm_sys_close(source_fd);
     200           0 :         return;
     201             :     }
     202             : 
     203             :     struct stat source_stat;
     204           0 :     fstat(source_fd, &source_stat);
     205             : 
     206           0 :     guint64 bytes_deduped = 0;
     207           0 :     gint64 bytes_remaining = source_stat.st_size;
     208           0 :     int ret = 0;
     209           0 :     while(bytes_deduped < (guint64)source_stat.st_size && ret == 0 &&
     210           0 :           extent_same.info.status == 0 && bytes_remaining) {
     211           0 :         extent_same.args.dest_count = 1;
     212           0 :         extent_same.args.logical_offset = bytes_deduped;
     213           0 :         extent_same.info.logical_offset = bytes_deduped;
     214             : 
     215             :         /* BTRFS_IOC_FILE_EXTENT_SAME has an internal limit at 16MB */
     216           0 :         extent_same.args.length = MIN(16 * 1024 * 1024, bytes_remaining);
     217           0 :         if(extent_same.args.length == 0) {
     218           0 :             extent_same.args.length = bytes_remaining;
     219             :         }
     220             : 
     221           0 :         ret = ioctl(source_fd, BTRFS_IOC_FILE_EXTENT_SAME, &extent_same);
     222           0 :         if(ret == 0 && extent_same.info.status == 0) {
     223           0 :             bytes_deduped += extent_same.info.bytes_deduped;
     224           0 :             bytes_remaining -= extent_same.info.bytes_deduped;
     225             :         }
     226             :     }
     227             : 
     228           0 :     rm_sys_close(source_fd);
     229           0 :     rm_sys_close(extent_same.info.fd);
     230             : 
     231           0 :     if(ret < 0) {
     232           0 :         ret = errno;
     233           0 :         rm_log_error_line(_("BTRFS_IOC_FILE_EXTENT_SAME returned error: (%d) %s"), ret,
     234             :                           strerror(ret));
     235           0 :     } else if(extent_same.info.status == -22) {
     236           0 :         rm_log_error_line(
     237             :             _("BTRFS_IOC_FILE_EXTENT_SAME returned status -22 - you probably need kernel "
     238             :               "> 4.2"));
     239           0 :     } else if(extent_same.info.status < 0) {
     240           0 :         rm_log_error_line(_("BTRFS_IOC_FILE_EXTENT_SAME returned status %d for file %s"),
     241             :                           extent_same.info.status, dest);
     242           0 :     } else if(bytes_remaining > 0) {
     243           0 :         rm_log_info_line(_("Files don't match - not cloned"));
     244             :     }
     245             : #else
     246             : 
     247             :     (void)source;
     248             :     (void)dest;
     249             :     rm_log_error_line(_("rmlint was not compiled with btrfs support."))
     250             : 
     251             : #endif
     252             : }
     253             : 
     254       54884 : static int rm_cmd_maybe_btrfs_clone(RmSession *session, int argc, const char **argv) {
     255       54884 :     if(g_strcmp0("--btrfs-clone", argv[1]) == 0) {
     256           0 :         if(argc != 4) {
     257           0 :             rm_cmd_btrfs_clone_usage();
     258           0 :             return EXIT_FAILURE;
     259           0 :         } else if(!rm_session_check_kernel_version(session, 4, 2)) {
     260           0 :             rm_log_warning_line("This needs at least linux >= 4.2.");
     261           0 :             return EXIT_FAILURE;
     262             :         } else {
     263           0 :             rm_cmd_btrfs_clone(argv[2], argv[3]);
     264           0 :             return EXIT_FAILURE;
     265             :         }
     266             :     }
     267       54884 :     return EXIT_SUCCESS;
     268             : }
     269             : 
     270             : /* clang-format off */
     271             : static const struct FormatSpec {
     272             :     const char *id;
     273             :     unsigned base;
     274             :     unsigned exponent;
     275             : } SIZE_FORMAT_TABLE[] = {
     276             :     /* This list is sorted, so bsearch() can be used */
     277             :     {.id = "b",  .base = 512,  .exponent = 1},
     278             :     {.id = "c",  .base = 1,    .exponent = 1},
     279             :     {.id = "e",  .base = 1000, .exponent = 6},
     280             :     {.id = "eb", .base = 1024, .exponent = 6},
     281             :     {.id = "g",  .base = 1000, .exponent = 3},
     282             :     {.id = "gb", .base = 1024, .exponent = 3},
     283             :     {.id = "k",  .base = 1000, .exponent = 1},
     284             :     {.id = "kb", .base = 1024, .exponent = 1},
     285             :     {.id = "m",  .base = 1000, .exponent = 2},
     286             :     {.id = "mb", .base = 1024, .exponent = 2},
     287             :     {.id = "p",  .base = 1000, .exponent = 5},
     288             :     {.id = "pb", .base = 1024, .exponent = 5},
     289             :     {.id = "t",  .base = 1000, .exponent = 4},
     290             :     {.id = "tb", .base = 1024, .exponent = 4},
     291             :     {.id = "w",  .base = 2,    .exponent = 1}
     292             : };
     293             : /* clang-format on */
     294             : 
     295             : typedef struct FormatSpec FormatSpec;
     296             : 
     297             : static const int SIZE_FORMAT_TABLE_N = sizeof(SIZE_FORMAT_TABLE) / sizeof(FormatSpec);
     298             : 
     299        7696 : static int rm_cmd_compare_spec_elem(const void *fmt_a, const void *fmt_b) {
     300        7696 :     return strcasecmp(((FormatSpec *)fmt_a)->id, ((FormatSpec *)fmt_b)->id);
     301             : }
     302             : 
     303        2392 : static RmOff rm_cmd_size_string_to_bytes(const char *size_spec, GError **error) {
     304        2392 :     if(size_spec == NULL) {
     305           0 :         g_set_error(error, RM_ERROR_QUARK, 0, _("Input size is empty"));
     306           0 :         return 0;
     307             :     }
     308             : 
     309        2392 :     char *format = NULL;
     310        2392 :     long double decimal = strtold(size_spec, &format);
     311             : 
     312        2392 :     if(decimal == 0 && format == size_spec) {
     313           0 :         g_set_error(error, RM_ERROR_QUARK, 0, _("This does not look like a number"));
     314           0 :         return 0;
     315        2392 :     } else if(decimal < 0) {
     316           2 :         g_set_error(error, RM_ERROR_QUARK, 0, _("Negativ sizes are no good idea"));
     317           2 :         return 0;
     318        2390 :     } else if(*format) {
     319        1966 :         format = g_strstrip(format);
     320             :     } else {
     321         424 :         return round(decimal);
     322             :     }
     323             : 
     324        1966 :     FormatSpec key = {.id = format};
     325        1966 :     FormatSpec *found = bsearch(&key, SIZE_FORMAT_TABLE, SIZE_FORMAT_TABLE_N,
     326             :                                 sizeof(FormatSpec), rm_cmd_compare_spec_elem);
     327             : 
     328        1966 :     if(found != NULL) {
     329             :         /* No overflow check */
     330        1966 :         return decimal * pow(found->base, found->exponent);
     331             :     } else {
     332           0 :         g_set_error(error, RM_ERROR_QUARK, 0, _("Given format specifier not found"));
     333           0 :         return 0;
     334             :     }
     335             : }
     336             : 
     337             : /* Size spec parsing implemented by qitta (http://github.com/qitta)
     338             :  * Thanks and go blame him if this breaks!
     339             :  */
     340         227 : static gboolean rm_cmd_size_range_string_to_bytes(const char *range_spec, RmOff *min,
     341             :                                                   RmOff *max, GError **error) {
     342         227 :     *min = 0;
     343         227 :     *max = G_MAXULONG;
     344             : 
     345         227 :     range_spec = (const char *)g_strstrip((char *)range_spec);
     346         227 :     gchar **split = g_strsplit(range_spec, "-", 2);
     347             : 
     348         227 :     if(*range_spec == '-') {
     349             :         /* Act like it was "0-..." */
     350           1 :         split[0] = g_strdup("0");
     351             :     }
     352             : 
     353         227 :     if(split[0] != NULL) {
     354         227 :         *min = rm_cmd_size_string_to_bytes(split[0], error);
     355             :     }
     356             : 
     357         227 :     if(split[1] != NULL && *error == NULL) {
     358         115 :         *max = rm_cmd_size_string_to_bytes(split[1], error);
     359             :     }
     360             : 
     361         227 :     g_strfreev(split);
     362             : 
     363         227 :     if(*error == NULL && *max < *min) {
     364           1 :         g_set_error(error, RM_ERROR_QUARK, 0, _("Max is smaller than min"));
     365             :     }
     366             : 
     367         227 :     return (*error == NULL);
     368             : }
     369             : 
     370         227 : static gboolean rm_cmd_parse_limit_sizes(_U const char *option_name,
     371             :                                          const gchar *range_spec,
     372             :                                          RmSession *session,
     373             :                                          GError **error) {
     374         227 :     if(!rm_cmd_size_range_string_to_bytes(range_spec, &session->cfg->minsize,
     375         227 :                                           &session->cfg->maxsize, error)) {
     376           3 :         g_prefix_error(error, _("cannot parse --size: "));
     377           3 :         return false;
     378             :     } else {
     379         224 :         session->cfg->limits_specified = true;
     380         224 :         return true;
     381             :     }
     382             : }
     383             : 
     384             : static GLogLevelFlags VERBOSITY_TO_LOG_LEVEL[] = {[0] = G_LOG_LEVEL_CRITICAL,
     385             :                                                   [1] = G_LOG_LEVEL_ERROR,
     386             :                                                   [2] = G_LOG_LEVEL_WARNING,
     387             :                                                   [3] = G_LOG_LEVEL_MESSAGE |
     388             :                                                         G_LOG_LEVEL_INFO,
     389             :                                                   [4] = G_LOG_LEVEL_DEBUG};
     390             : 
     391       52989 : static int rm_cmd_create_metadata_cache(RmSession *session) {
     392       52989 :     if(session->cfg->use_meta_cache == false) {
     393       51108 :         return false;
     394             :     }
     395             : 
     396        1881 :     if(session->replay_files.length) {
     397         570 :         rm_log_warning_line(_("--replay given; --with-metadata-cache will be ignored."));
     398         570 :         session->cfg->use_meta_cache = false;
     399         570 :         return EXIT_SUCCESS;
     400             :     }
     401             : 
     402        1311 :     GError *error = NULL;
     403        1311 :     session->meta_cache = rm_swap_table_open(FALSE, &error);
     404        1311 :     session->cfg->use_meta_cache = !!(session->meta_cache);
     405             : 
     406        1311 :     if(error != NULL) {
     407           0 :         rm_log_warning_line(_("Unable to open tmp cache: %s"), error->message);
     408           0 :         g_error_free(error);
     409           0 :         error = NULL;
     410           0 :         return EXIT_FAILURE;
     411             :     }
     412             : 
     413        1311 :     char *names[] = {"path", "dir", NULL};
     414        1311 :     int *attrs_ptrs[] = {&session->meta_cache_path_id, &session->meta_cache_dir_id, NULL};
     415             : 
     416        3933 :     for(int i = 0; attrs_ptrs[i] && names[i]; ++i) {
     417        2622 :         *attrs_ptrs[i] = rm_swap_table_create_attr(session->meta_cache, names[i], &error);
     418             : 
     419        2622 :         if(error != NULL) {
     420           0 :             rm_log_warning_line(_("Unable to create cache attr `%s`: %s"), names[i],
     421             :                                 error->message);
     422           0 :             g_error_free(error);
     423           0 :             error = NULL;
     424           0 :             return EXIT_FAILURE;
     425             :         }
     426             :     }
     427             : 
     428        1311 :     return EXIT_SUCCESS;
     429             : }
     430             : 
     431      149646 : static bool rm_cmd_add_path(RmSession *session, bool is_prefd, int index,
     432             :                             const char *path) {
     433      149646 :     RmCfg *cfg = session->cfg;
     434      149646 :     if(faccessat(AT_FDCWD, path, R_OK, AT_EACCESS) != 0) {
     435           0 :         rm_log_warning_line(_("Can't open directory or file \"%s\": %s"), path,
     436             :                             strerror(errno));
     437           0 :         return FALSE;
     438             :     } else {
     439      149646 :         cfg->is_prefd = g_realloc(cfg->is_prefd, sizeof(char) * (index + 1));
     440      149646 :         cfg->is_prefd[index] = is_prefd;
     441      149646 :         cfg->paths = g_realloc(cfg->paths, sizeof(char *) * (index + 2));
     442             : 
     443      149646 :         char *abs_path = NULL;
     444      149646 :         if(strncmp(path, "//", 2) == 0) {
     445           0 :             abs_path = g_strdup(path);
     446             :         } else {
     447      149646 :             abs_path = realpath(path, NULL);
     448             :         }
     449             : 
     450      149646 :         if(cfg->use_meta_cache) {
     451        3606 :             cfg->paths[index] = GUINT_TO_POINTER(
     452             :                 rm_swap_table_insert(session->meta_cache, session->meta_cache_dir_id,
     453             :                                      abs_path, strlen(abs_path) + 1));
     454        3606 :             g_free(abs_path);
     455             :         } else {
     456      146040 :             cfg->paths[index] = abs_path ? abs_path : g_strdup(path);
     457             :         }
     458      149646 :         cfg->paths[index + 1] = NULL;
     459      149646 :         return TRUE;
     460             :     }
     461             : }
     462             : 
     463           1 : static int rm_cmd_read_paths_from_stdin(RmSession *session, bool is_prefd, int index) {
     464           1 :     int paths_added = 0;
     465             :     char path_buf[PATH_MAX];
     466             : 
     467           5 :     while(fgets(path_buf, PATH_MAX, stdin)) {
     468           3 :         paths_added += rm_cmd_add_path(session, is_prefd, index + paths_added,
     469           3 :                                        strtok(path_buf, "\n"));
     470             :     }
     471             : 
     472           1 :     return paths_added;
     473             : }
     474             : 
     475       69771 : static bool rm_cmd_parse_output_pair(RmSession *session, const char *pair,
     476             :                                      GError **error) {
     477       69771 :     g_assert(session);
     478       69771 :     g_assert(pair);
     479             : 
     480       69771 :     char *separator = strchr(pair, ':');
     481       69771 :     char *full_path = NULL;
     482             :     char format_name[100];
     483       69771 :     memset(format_name, 0, sizeof(format_name));
     484             : 
     485       69771 :     if(separator == NULL) {
     486             :         /* default to stdout */
     487           3 :         char *extension = strchr(pair, '.');
     488           3 :         if(extension == NULL) {
     489           3 :             full_path = "stdout";
     490           3 :             strncpy(format_name, pair, strlen(pair));
     491             :         } else {
     492           0 :             extension += 1;
     493           0 :             full_path = (char *)pair;
     494           0 :             strncpy(format_name, extension, strlen(extension));
     495             :         }
     496             :     } else {
     497       69768 :         full_path = separator + 1;
     498       69768 :         strncpy(format_name, pair, MIN((long)sizeof(format_name), separator - pair));
     499             :     }
     500             : 
     501       69771 :     if(!rm_fmt_add(session->formats, format_name, full_path)) {
     502           0 :         g_set_error(error, RM_ERROR_QUARK, 0, _("Adding -o %s as output failed"), pair);
     503           0 :         return false;
     504             :     }
     505             : 
     506       69771 :     return true;
     507             : }
     508             : 
     509       52986 : static bool rm_cmd_parse_config_pair(RmSession *session, const char *pair,
     510             :                                      GError **error) {
     511       52986 :     char *domain = strchr(pair, ':');
     512       52986 :     if(domain == NULL) {
     513           0 :         g_set_error(error, RM_ERROR_QUARK, 0,
     514           0 :                     _("No format (format:key[=val]) specified in '%s'"), pair);
     515           0 :         return false;
     516             :     }
     517             : 
     518       52986 :     char *key = NULL, *value = NULL;
     519       52986 :     char **key_val = g_strsplit(&domain[1], "=", 2);
     520       52986 :     int len = g_strv_length(key_val);
     521       52986 :     bool result = true;
     522             : 
     523       52986 :     if(len < 1) {
     524           0 :         g_set_error(error, RM_ERROR_QUARK, 0, _("Missing key (format:key[=val]) in '%s'"),
     525             :                     pair);
     526           0 :         g_strfreev(key_val);
     527           0 :         return false;
     528             :     }
     529             : 
     530       52986 :     key = g_strdup(key_val[0]);
     531       52986 :     if(len == 2) {
     532           0 :         value = g_strdup(key_val[1]);
     533             :     } else {
     534       52986 :         value = g_strdup("1");
     535             :     }
     536             : 
     537       52986 :     char *formatter = g_strndup(pair, domain - pair);
     538       52986 :     if(!rm_fmt_is_valid_key(session->formats, formatter, key)) {
     539           0 :         g_set_error(error, RM_ERROR_QUARK, 0, _("Invalid key `%s' for formatter `%s'"),
     540             :                     key, formatter);
     541           0 :         g_free(key);
     542           0 :         g_free(value);
     543           0 :         result = false;
     544             :     } else {
     545       52986 :         rm_fmt_set_config_value(session->formats, formatter, key, value);
     546             :     }
     547             : 
     548       52986 :     g_free(formatter);
     549       52986 :     g_strfreev(key_val);
     550       52986 :     return result;
     551             : }
     552             : 
     553       52986 : static gboolean rm_cmd_parse_config(_U const char *option_name,
     554             :                                     const char *pair,
     555             :                                     RmSession *session,
     556             :                                     _U GError **error) {
     557       52986 :     return rm_cmd_parse_config_pair(session, pair, error);
     558             : }
     559             : 
     560         196 : static double rm_cmd_parse_clamp_factor(const char *string, GError **error) {
     561         196 :     char *error_loc = NULL;
     562         196 :     gdouble factor = g_strtod(string, &error_loc);
     563             : 
     564         196 :     if(error_loc != NULL && *error_loc != '\0' && *error_loc != '%') {
     565           0 :         g_set_error(error, RM_ERROR_QUARK, 0,
     566           0 :                     _("Unable to parse factor \"%s\": error begins at %s"), string,
     567             :                     error_loc);
     568           0 :         return 0;
     569             :     }
     570             : 
     571         196 :     if(error_loc != NULL && *error_loc == '%') {
     572          56 :         factor /= 100;
     573             :     }
     574             : 
     575         196 :     if(0 > factor || factor > 1) {
     576           0 :         g_set_error(error, RM_ERROR_QUARK, 0, _("factor value is not in range [0-1]: %f"),
     577             :                     factor);
     578           0 :         return 0;
     579             :     }
     580             : 
     581         196 :     return factor;
     582             : }
     583             : 
     584         168 : static RmOff rm_cmd_parse_clamp_offset(const char *string, GError **error) {
     585         168 :     RmOff offset = rm_cmd_size_string_to_bytes(string, error);
     586             : 
     587         168 :     if(*error != NULL) {
     588           0 :         g_prefix_error(error, _("Unable to parse offset \"%s\": "), string);
     589           0 :         return 0;
     590             :     }
     591             : 
     592         168 :     return offset;
     593             : }
     594             : 
     595         364 : static void rm_cmd_parse_clamp_option(RmSession *session, const char *string,
     596             :                                       bool start_or_end, GError **error) {
     597         560 :     if(strchr(string, '.') || g_str_has_suffix(string, "%")) {
     598         196 :         gdouble factor = rm_cmd_parse_clamp_factor(string, error);
     599         196 :         if(start_or_end) {
     600          84 :             session->cfg->use_absolute_start_offset = false;
     601          84 :             session->cfg->skip_start_factor = factor;
     602             :         } else {
     603         112 :             session->cfg->use_absolute_end_offset = false;
     604         112 :             session->cfg->skip_end_factor = factor;
     605             :         }
     606             :     } else {
     607         168 :         RmOff offset = rm_cmd_parse_clamp_offset(string, error);
     608         168 :         if(start_or_end) {
     609         140 :             session->cfg->use_absolute_start_offset = true;
     610         140 :             session->cfg->skip_start_offset = offset;
     611             :         } else {
     612          28 :             session->cfg->use_absolute_end_offset = true;
     613          28 :             session->cfg->skip_end_offset = offset;
     614             :         }
     615             :     }
     616         364 : }
     617             : 
     618             : /* parse comma-separated strong of lint types and set cfg accordingly */
     619             : typedef struct RmLintTypeOption {
     620             :     const char **names;
     621             :     gboolean **enable;
     622             : } RmLintTypeOption;
     623             : 
     624             : /* compare function for parsing lint type arguments */
     625        3052 : int rm_cmd_find_line_type_func(const void *v_input, const void *v_option) {
     626        3052 :     const char *input = v_input;
     627        3052 :     const RmLintTypeOption *option = v_option;
     628             : 
     629        6412 :     for(int i = 0; option->names[i]; ++i) {
     630        3808 :         if(strcmp(option->names[i], input) == 0) {
     631         448 :             return 0;
     632             :         }
     633             :     }
     634        2604 :     return 1;
     635             : }
     636             : 
     637             : #define OPTS (gboolean *[])
     638             : #define NAMES (const char *[])
     639             : 
     640         224 : static char rm_cmd_find_lint_types_sep(const char *lint_string) {
     641         224 :     if(*lint_string == '+' || *lint_string == '-') {
     642           0 :         lint_string++;
     643             :     }
     644             : 
     645        1344 :     while(isalpha(*lint_string)) {
     646         896 :         lint_string++;
     647             :     }
     648             : 
     649         224 :     return *lint_string;
     650             : }
     651             : 
     652         224 : static gboolean rm_cmd_parse_lint_types(_U const char *option_name,
     653             :                                         const char *lint_string,
     654             :                                         RmSession *session,
     655             :                                         _U GError **error) {
     656         224 :     RmCfg *cfg = session->cfg;
     657             : 
     658        5376 :     RmLintTypeOption option_table[] = {
     659         224 :         {.names = NAMES{"all", 0},
     660        1120 :          .enable = OPTS{&cfg->find_badids, &cfg->find_badlinks, &cfg->find_emptydirs,
     661         448 :                         &cfg->find_emptyfiles, &cfg->find_nonstripped,
     662         448 :                         &cfg->find_duplicates, &cfg->merge_directories, 0}},
     663             :         {
     664         224 :          .names = NAMES{"minimal", 0},
     665         224 :          .enable = OPTS{&cfg->find_badids, &cfg->find_badlinks, &cfg->find_duplicates, 0},
     666             :         },
     667             :         {
     668         224 :          .names = NAMES{"minimaldirs", 0},
     669             :          .enable =
     670         224 :              OPTS{&cfg->find_badids, &cfg->find_badlinks, &cfg->merge_directories, 0},
     671             :         },
     672             :         {
     673         224 :          .names = NAMES{"defaults", 0},
     674         672 :          .enable = OPTS{&cfg->find_badids, &cfg->find_badlinks, &cfg->find_emptydirs,
     675         448 :                         &cfg->find_emptyfiles, &cfg->find_duplicates, 0},
     676             :         },
     677             :         {
     678         448 :          .names = NAMES{"none", 0}, .enable = OPTS{0},
     679             :         },
     680         448 :         {.names = NAMES{"badids", "bi", 0}, .enable = OPTS{&cfg->find_badids, 0}},
     681         448 :         {.names = NAMES{"badlinks", "bl", 0}, .enable = OPTS{&cfg->find_badlinks, 0}},
     682         448 :         {.names = NAMES{"emptydirs", "ed", 0}, .enable = OPTS{&cfg->find_emptydirs, 0}},
     683         448 :         {.names = NAMES{"emptyfiles", "ef", 0}, .enable = OPTS{&cfg->find_emptyfiles, 0}},
     684         224 :         {.names = NAMES{"nonstripped", "ns", 0},
     685         224 :          .enable = OPTS{&cfg->find_nonstripped, 0}},
     686         224 :         {.names = NAMES{"duplicates", "df", "dupes", 0},
     687         224 :          .enable = OPTS{&cfg->find_duplicates, 0}},
     688         224 :         {.names = NAMES{"duplicatedirs", "dd", "dupedirs", 0},
     689         224 :          .enable = OPTS{&cfg->merge_directories, 0}}};
     690             : 
     691         224 :     RmLintTypeOption *all_opts = &option_table[0];
     692             : 
     693             :     /* initialize all options to disabled by default */
     694        1792 :     for(int i = 0; all_opts->enable[i]; i++) {
     695        1568 :         *all_opts->enable[i] = false;
     696             :     }
     697             : 
     698             :     /* split the comma-separates list of options */
     699         224 :     char lint_sep[2] = {0, 0};
     700         224 :     lint_sep[0] = rm_cmd_find_lint_types_sep(lint_string);
     701         224 :     if(lint_sep[0] == 0) {
     702           0 :         lint_sep[0] = ',';
     703             :     }
     704             : 
     705         224 :     char **lint_types = g_strsplit(lint_string, lint_sep, -1);
     706             : 
     707             :     /* iterate over the separated option strings */
     708         672 :     for(int index = 0; lint_types[index]; index++) {
     709         448 :         char *lint_type = lint_types[index];
     710         448 :         char sign = 0;
     711             : 
     712         448 :         if(*lint_type == '+') {
     713         224 :             sign = +1;
     714         224 :         } else if(*lint_type == '-') {
     715           0 :             sign = -1;
     716             :         }
     717             : 
     718         448 :         if(sign == 0) {
     719         224 :             sign = +1;
     720             :         } else {
     721             :             /* get rid of prefix. */
     722         224 :             lint_type += ABS(sign);
     723             :         }
     724             : 
     725             :         /* use lfind to find matching option from array */
     726         448 :         size_t elems = sizeof(option_table) / sizeof(RmLintTypeOption);
     727         448 :         RmLintTypeOption *option =
     728             :             lfind(lint_type, &option_table, &elems, sizeof(RmLintTypeOption),
     729             :                   rm_cmd_find_line_type_func);
     730             : 
     731             :         /* apply the found option */
     732         448 :         if(option == NULL) {
     733           0 :             rm_log_warning(_("lint type '%s' not recognised"), lint_type);
     734           0 :             continue;
     735             :         }
     736             : 
     737             :         /* enable options as appropriate */
     738         672 :         for(int i = 0; option->enable[i]; i++) {
     739         224 :             *option->enable[i] = (sign == -1) ? false : true;
     740             :         }
     741             :     }
     742             : 
     743         224 :     if(cfg->merge_directories) {
     744           0 :         cfg->ignore_hidden = false;
     745           0 :         cfg->find_hardlinked_dupes = true;
     746           0 :         cfg->cache_file_structs = true;
     747             :     }
     748             : 
     749             :     /* clean up */
     750         224 :     g_strfreev(lint_types);
     751         224 :     return true;
     752             : }
     753             : 
     754           9 : static bool rm_cmd_timestamp_is_plain(const char *stamp) {
     755           9 :     return strchr(stamp, 'T') ? false : true;
     756             : }
     757             : 
     758           6 : static gboolean rm_cmd_parse_timestamp(_U const char *option_name, const gchar *string,
     759             :                                        RmSession *session, GError **error) {
     760           6 :     time_t result = 0;
     761           6 :     bool plain = rm_cmd_timestamp_is_plain(string);
     762           6 :     session->cfg->filter_mtime = false;
     763             : 
     764           6 :     if(plain) {
     765             :         /* A simple integer is expected, just parse it as time_t */
     766           3 :         result = strtoll(string, NULL, 10);
     767             :     } else {
     768             :         /* Parse ISO8601 timestamps like 2006-02-03T16:45:09.000Z */
     769           3 :         result = rm_iso8601_parse(string);
     770             : 
     771             :         /* debug */
     772             :         {
     773             :             char time_buf[256];
     774           3 :             memset(time_buf, 0, sizeof(time_buf));
     775           3 :             rm_iso8601_format(time(NULL), time_buf, sizeof(time_buf));
     776           3 :             rm_log_debug_line("timestamp %s understood as %lu", time_buf, result);
     777             :         }
     778             :     }
     779             : 
     780           6 :     if(result <= 0) {
     781           0 :         g_set_error(error, RM_ERROR_QUARK, 0, _("Unable to parse time spec \"%s\""),
     782             :                     string);
     783           0 :         return false;
     784             :     }
     785             : 
     786             :     /* Some sort of success. */
     787           6 :     session->cfg->filter_mtime = true;
     788             : 
     789           6 :     time_t now = time(NULL);
     790           6 :     if(result > now) {
     791             :         /* Not critical, maybe there are some uses for this,
     792             :          * but print at least a small warning as indication.
     793             :          * */
     794           2 :         if(plain) {
     795           0 :             rm_log_warning_line(_("-n %lu is newer than current time (%lu)."),
     796             :                                 (long)result, (long)now);
     797             :         } else {
     798             :             char time_buf[256];
     799           2 :             memset(time_buf, 0, sizeof(time_buf));
     800           2 :             rm_iso8601_format(time(NULL), time_buf, sizeof(time_buf));
     801             : 
     802           2 :             rm_log_warning_line("-N %s is newer than current time (%s) [%lu > %lu]",
     803             :                                 string, time_buf, result, now);
     804             :         }
     805             :     }
     806             : 
     807             :     /* Remember our result */
     808           6 :     session->cfg->min_mtime = result;
     809           6 :     return true;
     810             : }
     811             : 
     812           3 : static gboolean rm_cmd_parse_timestamp_file(const char *option_name,
     813             :                                             const gchar *timestamp_path,
     814             :                                             RmSession *session, GError **error) {
     815           3 :     bool plain = true, success = false;
     816           3 :     FILE *stamp_file = fopen(timestamp_path, "r");
     817             : 
     818             :     /* Assume failure */
     819           3 :     session->cfg->filter_mtime = false;
     820             : 
     821           3 :     if(stamp_file) {
     822             :         char stamp_buf[1024];
     823           3 :         memset(stamp_buf, 0, sizeof(stamp_buf));
     824             : 
     825           3 :         if(fgets(stamp_buf, sizeof(stamp_buf), stamp_file) != NULL) {
     826           3 :             success = rm_cmd_parse_timestamp(option_name, g_strstrip(stamp_buf), session,
     827             :                                              error);
     828           3 :             plain = rm_cmd_timestamp_is_plain(stamp_buf);
     829             :         }
     830             : 
     831           3 :         fclose(stamp_file);
     832             :     } else {
     833             :         /* Cannot read... */
     834           0 :         plain = false;
     835             :     }
     836             : 
     837           3 :     if(!success) {
     838           0 :         return false;
     839             :     }
     840             : 
     841           3 :     rm_fmt_add(session->formats, "stamp", timestamp_path);
     842           3 :     if(!plain) {
     843             :         /* Enable iso8601 timestamp output */
     844           1 :         rm_fmt_set_config_value(session->formats, "stamp", g_strdup("iso8601"),
     845           1 :                                 g_strdup("true"));
     846             :     }
     847             : 
     848           3 :     return success;
     849             : }
     850             : 
     851      107891 : static void rm_cmd_set_verbosity_from_cnt(RmCfg *cfg, int verbosity_counter) {
     852      107891 :     cfg->verbosity = VERBOSITY_TO_LOG_LEVEL[CLAMP(
     853             :         verbosity_counter,
     854             :         1,
     855             :         (int)(sizeof(VERBOSITY_TO_LOG_LEVEL) / sizeof(GLogLevelFlags)) - 1)];
     856      107891 : }
     857             : 
     858        6106 : static void rm_cmd_set_paranoia_from_cnt(RmCfg *cfg, int paranoia_counter,
     859             :                                          GError **error) {
     860             :     /* Handle the paranoia option */
     861        6106 :     switch(paranoia_counter) {
     862             :     case -2:
     863        1876 :         cfg->checksum_type = RM_DIGEST_XXHASH;
     864        1876 :         break;
     865             :     case -1:
     866        3750 :         cfg->checksum_type = RM_DIGEST_BASTARD;
     867        3750 :         break;
     868             :     case 0:
     869             :         /* leave users choice of -a (default) */
     870           8 :         break;
     871             :     case 1:
     872             : #if HAVE_SHA512
     873         242 :         cfg->checksum_type = RM_DIGEST_SHA512;
     874             : #else
     875             :         cfg->checksum_type = RM_DIGEST_SHA256;
     876             : #endif
     877         242 :         break;
     878             :     case 2:
     879         226 :         cfg->checksum_type = RM_DIGEST_PARANOID;
     880         226 :         break;
     881             :     default:
     882           4 :         if(error && *error == NULL) {
     883           4 :             g_set_error(error, RM_ERROR_QUARK, 0,
     884           4 :                         _("Only up to -pp or down to -PP flags allowed"));
     885             :         }
     886           4 :         break;
     887             :     }
     888        6106 : }
     889             : 
     890           9 : static void rm_cmd_on_error(_U GOptionContext *context, _U GOptionGroup *group,
     891             :                             RmSession *session, GError **error) {
     892           9 :     if(error != NULL) {
     893           9 :         rm_log_error_line("%s.", (*error)->message);
     894           9 :         g_clear_error(error);
     895           9 :         session->cmdline_parse_error = true;
     896             :     }
     897           9 : }
     898             : 
     899       31978 : static gboolean rm_cmd_parse_algorithm(_U const char *option_name,
     900             :                                        const gchar *value,
     901             :                                        RmSession *session,
     902             :                                        GError **error) {
     903       31978 :     RmCfg *cfg = session->cfg;
     904       31978 :     cfg->checksum_type = rm_string_to_digest_type(value);
     905             : 
     906       31978 :     if(cfg->checksum_type == RM_DIGEST_UNKNOWN) {
     907           0 :         g_set_error(error, RM_ERROR_QUARK, 0, _("Unknown hash algorithm: '%s'"), value);
     908           0 :         return false;
     909       31978 :     } else if(cfg->checksum_type == RM_DIGEST_BASTARD) {
     910        1881 :         session->hash_seed1 = time(NULL) * (GPOINTER_TO_UINT(session));
     911        1881 :         session->hash_seed2 = GPOINTER_TO_UINT(&session);
     912             :     }
     913       31978 :     return true;
     914             : }
     915             : 
     916       69769 : static gboolean rm_cmd_parse_small_output(_U const char *option_name,
     917             :                                           const gchar *output_pair, RmSession *session,
     918             :                                           _U GError **error) {
     919       69769 :     session->output_cnt[0] = MAX(session->output_cnt[0], 0);
     920       69769 :     session->output_cnt[0] += rm_cmd_parse_output_pair(session, output_pair, error);
     921       69769 :     return true;
     922             : }
     923             : 
     924           2 : static gboolean rm_cmd_parse_large_output(_U const char *option_name,
     925             :                                           const gchar *output_pair, RmSession *session,
     926             :                                           _U GError **error) {
     927           2 :     session->output_cnt[1] = MAX(session->output_cnt[1], 0);
     928           2 :     session->output_cnt[1] += rm_cmd_parse_output_pair(session, output_pair, error);
     929           2 :     return true;
     930             : }
     931             : 
     932        1882 : static gboolean rm_cmd_parse_mem(const gchar *size_spec, GError **error, RmOff *target) {
     933        1882 :     RmOff size = rm_cmd_size_string_to_bytes(size_spec, error);
     934             : 
     935        1882 :     if(*error != NULL) {
     936           0 :         g_prefix_error(error, _("Invalid size description \"%s\": "), size_spec);
     937           0 :         return false;
     938             :     } else {
     939        1882 :         *target = size;
     940        1882 :         return true;
     941             :     }
     942             : }
     943             : 
     944        1882 : static gboolean rm_cmd_parse_limit_mem(_U const char *option_name, const gchar *size_spec,
     945             :                                        RmSession *session, GError **error) {
     946        1882 :     return (rm_cmd_parse_mem(size_spec, error, &session->cfg->total_mem));
     947             : }
     948             : 
     949           0 : static gboolean rm_cmd_parse_paranoid_mem(_U const char *option_name,
     950             :                                           const gchar *size_spec, RmSession *session,
     951             :                                           GError **error) {
     952           0 :     return (rm_cmd_parse_mem(size_spec, error, &session->cfg->paranoid_mem));
     953             : }
     954             : 
     955           0 : static gboolean rm_cmd_parse_read_buffer_mem(_U const char *option_name,
     956             :                                              const gchar *size_spec, RmSession *session,
     957             :                                              GError **error) {
     958           0 :     return (rm_cmd_parse_mem(size_spec, error, &session->cfg->read_buffer_mem));
     959             : }
     960             : 
     961           0 : static gboolean rm_cmd_parse_sweep_size(_U const char *option_name,
     962             :                                         const gchar *size_spec, RmSession *session,
     963             :                                         GError **error) {
     964           0 :     return (rm_cmd_parse_mem(size_spec, error, &session->cfg->sweep_size));
     965             : }
     966             : 
     967           0 : static gboolean rm_cmd_parse_sweep_count(_U const char *option_name,
     968             :                                          const gchar *size_spec, RmSession *session,
     969             :                                          GError **error) {
     970           0 :     return (rm_cmd_parse_mem(size_spec, error, &session->cfg->sweep_count));
     971             : }
     972             : 
     973         224 : static gboolean rm_cmd_parse_clamp_low(_U const char *option_name, const gchar *spec,
     974             :                                        RmSession *session, _U GError **error) {
     975         224 :     rm_cmd_parse_clamp_option(session, spec, true, error);
     976         224 :     return (error && *error == NULL);
     977             : }
     978             : 
     979         140 : static gboolean rm_cmd_parse_clamp_top(_U const char *option_name, const gchar *spec,
     980             :                                        RmSession *session, _U GError **error) {
     981         140 :     rm_cmd_parse_clamp_option(session, spec, false, error);
     982         140 :     return (error && *error == NULL);
     983             : }
     984             : 
     985          85 : static gboolean rm_cmd_parse_cache(_U const char *option_name, const gchar *cache_path,
     986             :                                    RmSession *session, GError **error) {
     987          85 :     if(!g_file_test(cache_path, G_FILE_TEST_IS_REGULAR)) {
     988           1 :         g_set_error(error, RM_ERROR_QUARK, 0, "There is no cache at `%s'", cache_path);
     989           1 :         return false;
     990             :     }
     991             : 
     992          84 :     g_queue_push_tail(&session->cache_list, g_strdup(cache_path));
     993          84 :     return true;
     994             : }
     995             : 
     996           8 : static gboolean rm_cmd_parse_progress(_U const char *option_name, _U const gchar *value,
     997             :                                       RmSession *session, _U GError **error) {
     998           8 :     rm_fmt_clear(session->formats);
     999           8 :     rm_fmt_add(session->formats, "progressbar", "stdout");
    1000           8 :     rm_fmt_add(session->formats, "summary", "stdout");
    1001             : 
    1002           8 :     session->cfg->progress_enabled = true;
    1003             : 
    1004             :     /* Set verbosity to minimal */
    1005           8 :     rm_cmd_set_verbosity_from_cnt(session->cfg, 1);
    1006           8 :     return true;
    1007             : }
    1008             : 
    1009           4 : static void rm_cmd_set_default_outputs(RmSession *session) {
    1010           4 :     rm_fmt_add(session->formats, "pretty", "stdout");
    1011           4 :     rm_fmt_add(session->formats, "summary", "stdout");
    1012             : 
    1013           4 :     if(session->replay_files.length) {
    1014           0 :         rm_fmt_add(session->formats, "sh", "rmlint.replay.sh");
    1015           0 :         rm_fmt_add(session->formats, "json", "rmlint.replay.json");
    1016             :     } else {
    1017           4 :         rm_fmt_add(session->formats, "sh", "rmlint.sh");
    1018           4 :         rm_fmt_add(session->formats, "json", "rmlint.json");
    1019             :     }
    1020           4 : }
    1021             : 
    1022           0 : static gboolean rm_cmd_parse_no_progress(_U const char *option_name,
    1023             :                                          _U const gchar *value, RmSession *session,
    1024             :                                          _U GError **error) {
    1025           0 :     rm_fmt_clear(session->formats);
    1026           0 :     rm_cmd_set_default_outputs(session);
    1027           0 :     rm_cmd_set_verbosity_from_cnt(session->cfg, session->verbosity_count);
    1028           0 :     return true;
    1029             : }
    1030             : 
    1031           0 : static gboolean rm_cmd_parse_loud(_U const char *option_name, _U const gchar *count,
    1032             :                                   RmSession *session, _U GError **error) {
    1033           0 :     rm_cmd_set_verbosity_from_cnt(session->cfg, ++session->verbosity_count);
    1034           0 :     return true;
    1035             : }
    1036             : 
    1037       52999 : static gboolean rm_cmd_parse_quiet(_U const char *option_name, _U const gchar *count,
    1038             :                                    RmSession *session, _U GError **error) {
    1039       52999 :     rm_cmd_set_verbosity_from_cnt(session->cfg, --session->verbosity_count);
    1040       52999 :     return true;
    1041             : }
    1042             : 
    1043         454 : static gboolean rm_cmd_parse_paranoid(_U const char *option_name, _U const gchar *count,
    1044             :                                       RmSession *session, _U GError **error) {
    1045         454 :     rm_cmd_set_paranoia_from_cnt(session->cfg, ++session->paranoia_count, error);
    1046         454 :     return true;
    1047             : }
    1048             : 
    1049        5652 : static gboolean rm_cmd_parse_less_paranoid(_U const char *option_name,
    1050             :                                            _U const gchar *count, RmSession *session,
    1051             :                                            _U GError **error) {
    1052        5652 :     rm_cmd_set_paranoia_from_cnt(session->cfg, --session->paranoia_count, error);
    1053        5652 :     return true;
    1054             : }
    1055             : 
    1056        1205 : static gboolean rm_cmd_parse_partial_hidden(_U const char *option_name,
    1057             :                                             _U const gchar *count, RmSession *session,
    1058             :                                             _U GError **error) {
    1059        1205 :     RmCfg *cfg = session->cfg;
    1060        1205 :     cfg->ignore_hidden = false;
    1061        1205 :     cfg->partial_hidden = true;
    1062             : 
    1063        1205 :     return true;
    1064             : }
    1065             : 
    1066          56 : static gboolean rm_cmd_parse_see_symlinks(_U const char *option_name,
    1067             :                                           _U const gchar *count, RmSession *session,
    1068             :                                           _U GError **error) {
    1069          56 :     RmCfg *cfg = session->cfg;
    1070          56 :     cfg->see_symlinks = true;
    1071          56 :     cfg->follow_symlinks = false;
    1072             : 
    1073          56 :     return true;
    1074             : }
    1075             : 
    1076          84 : static gboolean rm_cmd_parse_follow_symlinks(_U const char *option_name,
    1077             :                                              _U const gchar *count, RmSession *session,
    1078             :                                              _U GError **error) {
    1079          84 :     RmCfg *cfg = session->cfg;
    1080          84 :     cfg->see_symlinks = false;
    1081          84 :     cfg->follow_symlinks = true;
    1082             : 
    1083          84 :     return true;
    1084             : }
    1085             : 
    1086          28 : static gboolean rm_cmd_parse_no_partial_hidden(_U const char *option_name,
    1087             :                                                _U const gchar *count, RmSession *session,
    1088             :                                                _U GError **error) {
    1089          28 :     RmCfg *cfg = session->cfg;
    1090          28 :     cfg->ignore_hidden = true;
    1091          28 :     cfg->partial_hidden = false;
    1092             : 
    1093          28 :     return true;
    1094             : }
    1095             : 
    1096        1121 : static gboolean rm_cmd_parse_merge_directories(_U const char *option_name,
    1097             :                                                _U const gchar *count, RmSession *session,
    1098             :                                                _U GError **error) {
    1099        1121 :     RmCfg *cfg = session->cfg;
    1100        1121 :     cfg->merge_directories = true;
    1101             : 
    1102             :     /* Pull in some options for convinience,
    1103             :      * duplicate dir detection works better with them.
    1104             :      *
    1105             :      * They may be disabled explicitly though.
    1106             :      */
    1107        1121 :     cfg->follow_symlinks = false;
    1108        1121 :     cfg->see_symlinks = true;
    1109        1121 :     rm_cmd_parse_partial_hidden(NULL, NULL, session, error);
    1110             : 
    1111             :     /* Keep RmFiles after shredder. */
    1112        1121 :     cfg->cache_file_structs = true;
    1113             : 
    1114        1121 :     return true;
    1115             : }
    1116             : 
    1117           0 : static gboolean rm_cmd_parse_permissions(_U const char *option_name, const gchar *perms,
    1118             :                                          RmSession *session, GError **error) {
    1119           0 :     RmCfg *cfg = session->cfg;
    1120             : 
    1121           0 :     if(perms == NULL) {
    1122           0 :         cfg->permissions = R_OK | W_OK;
    1123             :     } else {
    1124           0 :         while(*perms) {
    1125           0 :             switch(*perms++) {
    1126             :             case 'r':
    1127           0 :                 cfg->permissions |= R_OK;
    1128           0 :                 break;
    1129             :             case 'w':
    1130           0 :                 cfg->permissions |= W_OK;
    1131           0 :                 break;
    1132             :             case 'x':
    1133           0 :                 cfg->permissions |= X_OK;
    1134           0 :                 break;
    1135             :             default:
    1136           0 :                 g_set_error(error, RM_ERROR_QUARK, 0,
    1137           0 :                             _("Permissions string needs to be one or many of [rwx]"));
    1138           0 :                 return false;
    1139             :             }
    1140             :         }
    1141             :     }
    1142             : 
    1143           0 :     return true;
    1144             : }
    1145             : 
    1146       51699 : static gboolean rm_cmd_check_lettervec(const char *option_name, const char *criteria,
    1147             :                                        const char *valid, GError **error) {
    1148      191263 :     for(int i = 0; criteria[i]; ++i) {
    1149      139564 :         if(strchr(valid, criteria[i]) == NULL) {
    1150           0 :             g_set_error(error, RM_ERROR_QUARK, 0, _("%s may only contain [%s], not `%c`"),
    1151           0 :                         option_name, valid, criteria[i]);
    1152           0 :             return false;
    1153             :         }
    1154             :     }
    1155             : 
    1156       51699 :     return true;
    1157             : }
    1158             : 
    1159       32216 : static gboolean rm_cmd_parse_sortby(_U const char *option_name, const gchar *criteria,
    1160             :                                     RmSession *session, GError **error) {
    1161       32216 :     RmCfg *cfg = session->cfg;
    1162       32216 :     if(!rm_cmd_check_lettervec(option_name, criteria, "moanspMOANSP", error)) {
    1163           0 :         return false;
    1164             :     }
    1165             : 
    1166             :     /* Remember the criteria string */
    1167       32216 :     strncpy(cfg->rank_criteria, criteria, sizeof(cfg->rank_criteria));
    1168             : 
    1169             :     /* ranking the files depends on caching them to the end of the program */
    1170       32216 :     cfg->cache_file_structs = true;
    1171             : 
    1172       32216 :     return true;
    1173             : }
    1174             : 
    1175       19483 : static gboolean rm_cmd_parse_rankby(_U const char *option_name, const gchar *criteria,
    1176             :                                     RmSession *session, GError **error) {
    1177       19483 :     RmCfg *cfg = session->cfg;
    1178             : 
    1179       19483 :     if(!rm_cmd_check_lettervec(option_name, criteria, "dlampDLAMP", error)) {
    1180           0 :         return false;
    1181             :     }
    1182             : 
    1183       19483 :     strncpy(cfg->sort_criteria, criteria, sizeof(cfg->sort_criteria));
    1184       19483 :     return true;
    1185             : }
    1186             : 
    1187       16108 : static gboolean rm_cmd_parse_replay(_U const char *option_name, const gchar *json_path,
    1188             :                                     RmSession *session, GError **error) {
    1189       16108 :     if(json_path == NULL) {
    1190           0 :         json_path = "rmlint.json";
    1191             :     }
    1192             : 
    1193       16108 :     if(g_access(json_path, R_OK) == -1) {
    1194           1 :         g_set_error(error, RM_ERROR_QUARK, 0, "--replay: `%s`: %s", json_path,
    1195           1 :                     g_strerror(errno));
    1196           1 :         return false;
    1197             :     }
    1198             : 
    1199       16107 :     g_queue_push_tail(&session->replay_files, g_strdup(json_path));
    1200       16107 :     session->cfg->cache_file_structs = true;
    1201       16107 :     return true;
    1202             : }
    1203             : 
    1204       54884 : static bool rm_cmd_set_cwd(RmCfg *cfg) {
    1205             :     /* Get current directory */
    1206             :     char cwd_buf[PATH_MAX + 1];
    1207       54884 :     memset(cwd_buf, 0, sizeof(cwd_buf));
    1208             : 
    1209       54884 :     if(getcwd(cwd_buf, PATH_MAX) == NULL) {
    1210           0 :         rm_log_perror("");
    1211           0 :         return false;
    1212             :     }
    1213             : 
    1214       54884 :     cfg->iwd = g_strdup_printf("%s%s", cwd_buf, G_DIR_SEPARATOR_S);
    1215       54884 :     return true;
    1216             : }
    1217             : 
    1218       54884 : static bool rm_cmd_set_cmdline(RmCfg *cfg, int argc, char **argv) {
    1219             :     /* Copy commandline rmlint was invoked with by copying argv into a
    1220             :      * NULL padded array and join that with g_strjoinv. GLib made me lazy.
    1221             :      */
    1222       54884 :     const char *argv_nul[argc + 1];
    1223       54884 :     memset(argv_nul, 0, sizeof(argv_nul));
    1224       54884 :     memcpy(argv_nul, argv, argc * sizeof(const char *));
    1225       54884 :     cfg->joined_argv = g_strjoinv(" ", (gchar **)argv_nul);
    1226             : 
    1227             :     /* This cannot fail currently */
    1228       54884 :     return true;
    1229             : }
    1230             : 
    1231       52989 : static bool rm_cmd_set_paths(RmSession *session, char **paths) {
    1232       52989 :     int path_index = 0;
    1233       52989 :     bool is_prefd = false;
    1234       52989 :     bool not_all_paths_read = false;
    1235             : 
    1236       52989 :     RmCfg *cfg = session->cfg;
    1237             : 
    1238             :     /* Check the directory to be valid */
    1239      202910 :     for(int i = 0; paths && paths[i]; ++i) {
    1240      149921 :         int read_paths = 0;
    1241      149921 :         const char *dir_path = paths[i];
    1242             : 
    1243      149921 :         if(strncmp(dir_path, "-", 1) == 0) {
    1244           1 :             read_paths = rm_cmd_read_paths_from_stdin(session, is_prefd, path_index);
    1245      149920 :         } else if(strncmp(dir_path, "//", 2) == 0 && strlen(dir_path) == 2) {
    1246         277 :             is_prefd = !is_prefd;
    1247             :         } else {
    1248      149643 :             read_paths = rm_cmd_add_path(session, is_prefd, path_index, paths[i]);
    1249             :         }
    1250             : 
    1251      149921 :         if(read_paths == 0) {
    1252         277 :             not_all_paths_read = true;
    1253             :         } else {
    1254      149644 :             path_index += read_paths;
    1255             :         }
    1256             :     }
    1257             : 
    1258       52989 :     g_strfreev(paths);
    1259             : 
    1260       52989 :     if(path_index == 0 && not_all_paths_read == false) {
    1261             :         /* Still no path set? - use `pwd` */
    1262           0 :         rm_cmd_add_path(session, is_prefd, path_index, cfg->iwd);
    1263       52989 :     } else if(path_index == 0 && not_all_paths_read) {
    1264           0 :         return false;
    1265             :     }
    1266             : 
    1267       52989 :     return true;
    1268             : }
    1269             : 
    1270       52989 : static bool rm_cmd_set_outputs(RmSession *session, GError **error) {
    1271       52989 :     if(session->output_cnt[0] >= 0 && session->output_cnt[1] >= 0) {
    1272           0 :         g_set_error(error, RM_ERROR_QUARK, 0,
    1273           0 :                     _("Specifiyng both -o and -O is not allowed"));
    1274           0 :         return false;
    1275       52999 :     } else if(session->output_cnt[0] < 0 && session->output_cnt[1] < 0 &&
    1276          10 :               !rm_fmt_len(session->formats)) {
    1277           4 :         rm_cmd_set_default_outputs(session);
    1278             :     }
    1279             : 
    1280       52989 :     return true;
    1281             : }
    1282             : 
    1283             : /* Parse the commandline and set arguments in 'settings' (glob. var accordingly) */
    1284       54884 : bool rm_cmd_parse_args(int argc, char **argv, RmSession *session) {
    1285       54884 :     RmCfg *cfg = session->cfg;
    1286       54884 :     gboolean clone = FALSE;
    1287             : 
    1288             :     /* Handle --gui before all other processing,
    1289             :      * since we need to pass other args to the python interpreter.
    1290             :      * This is not possible with GOption alone.
    1291             :      */
    1292       54884 :     if(rm_cmd_maybe_switch_to_gui(argc, (const char **)argv) == EXIT_FAILURE) {
    1293           0 :         rm_log_error_line(_("Could not start graphical user interface."));
    1294           0 :         return false;
    1295             :     }
    1296             : 
    1297       54884 :     if(rm_cmd_maybe_switch_to_hasher(argc, (const char **)argv) == EXIT_FAILURE) {
    1298           0 :         return false;
    1299             :     }
    1300             : 
    1301       54884 :     if(rm_cmd_maybe_btrfs_clone(session, argc, (const char **)argv) == EXIT_FAILURE) {
    1302           0 :         return false;
    1303             :     }
    1304             : 
    1305             :     /* List of paths we got passed (or NULL) */
    1306       54884 :     char **paths = NULL;
    1307             : 
    1308             :     /* General error variable */
    1309       54884 :     GError *error = NULL;
    1310       54884 :     GOptionContext *option_parser = NULL;
    1311             : 
    1312             : #define FUNC(name) ((GOptionArgFunc)rm_cmd_parse_##name)
    1313       54884 :     const int DISABLE = G_OPTION_FLAG_REVERSE, EMPTY = G_OPTION_FLAG_NO_ARG,
    1314       54884 :               HIDDEN = G_OPTION_FLAG_HIDDEN, OPTIONAL = G_OPTION_FLAG_OPTIONAL_ARG;
    1315             : 
    1316             :     /* Free/Used Options:
    1317             :        Used: abBcCdDeEfFgGHhiI  kKlLmMnNoOpPqQrRsStTuUvVwWxXyY
    1318             :        Free:                  jJ                              zZ
    1319             :     */
    1320             : 
    1321             :     /* clang-format off */
    1322     2853968 :     const GOptionEntry main_option_entries[] = {
    1323             :         /* Option with required arguments */
    1324      109768 :         {"max-depth"        , 'd' , 0        , G_OPTION_ARG_INT      , &cfg->depth          , _("Specify max traversal depth")          , "N"}                   ,
    1325       54884 :         {"rank-by"          , 'S' , 0        , G_OPTION_ARG_CALLBACK , FUNC(rankby)         , _("Original criteria")                    , "[dlampDLAMP]"}        ,
    1326       54884 :         {"sort-by"          , 'y' , 0        , G_OPTION_ARG_CALLBACK , FUNC(sortby)         , _("Rank lint groups by certain criteria") , "[moansMOANS]"}        ,
    1327       54884 :         {"types"            , 'T' , 0        , G_OPTION_ARG_CALLBACK , FUNC(lint_types)     , _("Specify lint types")                   , "T"}                   ,
    1328       54884 :         {"size"             , 's' , 0        , G_OPTION_ARG_CALLBACK , FUNC(limit_sizes)    , _("Specify size limits")                  , "m-M"}                 ,
    1329       54884 :         {"algorithm"        , 'a' , 0        , G_OPTION_ARG_CALLBACK , FUNC(algorithm)      , _("Choose hash algorithm")                , "A"}                   ,
    1330       54884 :         {"output"           , 'o' , 0        , G_OPTION_ARG_CALLBACK , FUNC(small_output)   , _("Add output (override default)")        , "FMT[:PATH]"}          ,
    1331       54884 :         {"add-output"       , 'O' , 0        , G_OPTION_ARG_CALLBACK , FUNC(large_output)   , _("Add output (add to defaults)")         , "FMT[:PATH]"}          ,
    1332       54884 :         {"newer-than-stamp" , 'n' , 0        , G_OPTION_ARG_CALLBACK , FUNC(timestamp_file) , _("Newer than stamp file")                , "PATH"}                ,
    1333       54884 :         {"newer-than"       , 'N' , 0        , G_OPTION_ARG_CALLBACK , FUNC(timestamp)      , _("Newer than timestamp")                 , "STAMP"}               ,
    1334       54884 :         {"replay"           , 'Y' , OPTIONAL , G_OPTION_ARG_CALLBACK , FUNC(replay)         , _("Re-output a json file")                , "path/to/rmlint.json"} ,
    1335       54884 :         {"config"           , 'c' , 0        , G_OPTION_ARG_CALLBACK , FUNC(config)         , _("Configure a formatter")                , "FMT:K[=V]"}           ,
    1336       54884 :         {"cache"            , 'C' , 0        , G_OPTION_ARG_CALLBACK , FUNC(cache)          , _("Add json cache file")                  , "PATH"}                ,
    1337             : 
    1338             :         /* Non-trvial switches */
    1339       54884 :         {"progress" , 'g' , EMPTY , G_OPTION_ARG_CALLBACK , FUNC(progress) , _("Enable progressbar")                   , NULL} ,
    1340       54884 :         {"loud"     , 'v' , EMPTY , G_OPTION_ARG_CALLBACK , FUNC(loud)     , _("Be more verbose (-vvv for much more)") , NULL} ,
    1341       54884 :         {"quiet"    , 'V' , EMPTY , G_OPTION_ARG_CALLBACK , FUNC(quiet)    , _("Be less verbose (-VVV for much less)") , NULL} ,
    1342             : 
    1343             :         /* Trivial boolean options */
    1344      109768 :         {"no-with-color"           , 'W' , DISABLE  , G_OPTION_ARG_NONE     , &cfg->with_color              , _("Be not that colorful")                               , NULL}     ,
    1345      109768 :         {"hidden"                  , 'r' , DISABLE  , G_OPTION_ARG_NONE     , &cfg->ignore_hidden           , _("Find hidden files")                                  , NULL}     ,
    1346       54884 :         {"followlinks"             , 'f' , EMPTY    , G_OPTION_ARG_CALLBACK , FUNC(follow_symlinks)         , _("Follow symlinks")                                    , NULL}     ,
    1347      109768 :         {"no-followlinks"          , 'F' , DISABLE  , G_OPTION_ARG_NONE     , &cfg->follow_symlinks         , _("Ignore symlinks")                                    , NULL}     ,
    1348      109768 :         {"crossdev"                , 'x' , 0        , G_OPTION_ARG_NONE     , &cfg->crossdev                , _("Do not cross mounpoints")                            , NULL}     ,
    1349       54884 :         {"paranoid"                , 'p' , EMPTY    , G_OPTION_ARG_CALLBACK , FUNC(paranoid)                , _("Use more paranoid hashing")                          , NULL}     ,
    1350      109768 :         {"keep-all-tagged"         , 'k' , 0        , G_OPTION_ARG_NONE     , &cfg->keep_all_tagged         , _("Keep all tagged files")                              , NULL}     ,
    1351      109768 :         {"keep-all-untagged"       , 'K' , 0        , G_OPTION_ARG_NONE     , &cfg->keep_all_untagged       , _("Keep all untagged files")                            , NULL}     ,
    1352      109768 :         {"must-match-tagged"       , 'm' , 0        , G_OPTION_ARG_NONE     , &cfg->must_match_tagged       , _("Must have twin in tagged dir")                       , NULL}     ,
    1353      109768 :         {"must-match-untagged"     , 'M' , 0        , G_OPTION_ARG_NONE     , &cfg->must_match_untagged     , _("Must have twin in untagged dir")                     , NULL}     ,
    1354      109768 :         {"match-basename"          , 'b' , 0        , G_OPTION_ARG_NONE     , &cfg->match_basename          , _("Only find twins with same basename")                 , NULL}     ,
    1355      109768 :         {"match-extension"         , 'e' , 0        , G_OPTION_ARG_NONE     , &cfg->match_with_extension    , _("Only find twins with same extension")                , NULL}     ,
    1356      109768 :         {"match-without-extension" , 'i' , 0        , G_OPTION_ARG_NONE     , &cfg->match_without_extension , _("Only find twins with same basename minus extension") , NULL}     ,
    1357       54884 :         {"merge-directories"       , 'D' , EMPTY    , G_OPTION_ARG_CALLBACK , FUNC(merge_directories)       , _("Find duplicate directories")                         , NULL}     ,
    1358       54884 :         {"perms"                   , 'z' , OPTIONAL , G_OPTION_ARG_CALLBACK , FUNC(permissions)             , _("Only use files with certain permissions")            , "[RWX]+"} ,
    1359      109768 :         {"no-hardlinked"           , 'L' , DISABLE  , G_OPTION_ARG_NONE     , &cfg->find_hardlinked_dupes   , _("Ignore hardlink twins")                              , NULL}     ,
    1360       54884 :         {"partial-hidden"          , 0   , EMPTY    , G_OPTION_ARG_CALLBACK , FUNC(partial_hidden)          , _("Find hidden files in duplicate folders only")        , NULL}     ,
    1361             : 
    1362             :         /* Callback */
    1363       54884 :         {"show-man" , 'H' , EMPTY , G_OPTION_ARG_CALLBACK , rm_cmd_show_manpage , _("Show the manpage")            , NULL} ,
    1364       54884 :         {"version"  , 0   , EMPTY , G_OPTION_ARG_CALLBACK , rm_cmd_show_version , _("Show the version & features") , NULL} ,
    1365             :         /* Dummy option for --help output only: */
    1366       54884 :         {"gui"         , 0 , 0 , G_OPTION_ARG_NONE , NULL   , _("If installed, start the optional gui with all following args")                 , NULL},
    1367       54884 :         {"hash"        , 0 , 0 , G_OPTION_ARG_NONE , NULL   , _("Work like sha1sum for all supported hash algorithms (see also --hash --help)") , NULL}                                            ,
    1368       54884 :         {"btrfs-clone" , 0 , 0 , G_OPTION_ARG_NONE , &clone , _("Clone extents from source to dest, if extents match")                          , NULL} ,
    1369             : 
    1370             :         /* Special case: accumulate leftover args (paths) in &paths */
    1371             :         {G_OPTION_REMAINING , 0 , 0 , G_OPTION_ARG_FILENAME_ARRAY , &paths , ""   , NULL}   ,
    1372             :         {NULL               , 0 , 0 , 0                           , NULL   , NULL , NULL}
    1373             :     };
    1374             : 
    1375     1207448 :     const GOptionEntry inversed_option_entries[] = {
    1376       54884 :         {"no-hidden"                  , 'R' , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->ignore_hidden           , "Ignore hidden files"                 , NULL} ,
    1377       54884 :         {"with-color"                 , 'w' , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->with_color              , "Be colorful like a unicorn"          , NULL} ,
    1378      109768 :         {"hardlinked"                 , 'l' , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->find_hardlinked_dupes   , _("Report hardlinks as duplicates")   , NULL} ,
    1379      109768 :         {"no-crossdev"                , 'X' , DISABLE | HIDDEN , G_OPTION_ARG_NONE     , &cfg->crossdev                , "Cross mountpoints"                   , NULL} ,
    1380       54884 :         {"less-paranoid"              , 'P' , EMPTY | HIDDEN   , G_OPTION_ARG_CALLBACK , FUNC(less_paranoid)           , "Use less paranoid hashing algorithm" , NULL} ,
    1381       54884 :         {"see-symlinks"               , '@' , EMPTY | HIDDEN   , G_OPTION_ARG_CALLBACK , FUNC(see_symlinks)            , "Treat symlinks a regular files"      , NULL} ,
    1382       54884 :         {"unmatched-basename"         , 'B',  HIDDEN           , G_OPTION_ARG_NONE     , &cfg->unmatched_basenames     , "Only find twins with differing names", NULL} ,       
    1383      109768 :         {"no-match-extension"         , 'E' , DISABLE | HIDDEN , G_OPTION_ARG_NONE     , &cfg->match_with_extension    , "Disable --match-extension"           , NULL} ,
    1384      109768 :         {"no-match-extension"         , 'E' , DISABLE | HIDDEN , G_OPTION_ARG_NONE     , &cfg->match_with_extension    , "Disable --match-extension"           , NULL} ,
    1385      109768 :         {"no-match-without-extension" , 'I' , DISABLE | HIDDEN , G_OPTION_ARG_NONE     , &cfg->match_without_extension , "Disable --match-without-extension"   , NULL} ,
    1386       54884 :         {"no-progress"                , 'G' , EMPTY | HIDDEN   , G_OPTION_ARG_CALLBACK , FUNC(no_progress)             , "Disable progressbar"                 , NULL} ,
    1387      109768 :         {"no-xattr-read"              , 0   , DISABLE | HIDDEN , G_OPTION_ARG_NONE     , &cfg->read_cksum_from_xattr   , "Disable --xattr-read"                , NULL} ,
    1388      109768 :         {"no-xattr-write"             , 0   , DISABLE | HIDDEN , G_OPTION_ARG_NONE     , &cfg->write_cksum_to_xattr    , "Disable --xattr-write"               , NULL} ,
    1389       54884 :         {"no-partial-hidden"          , 0   , EMPTY | HIDDEN   , G_OPTION_ARG_CALLBACK , FUNC(no_partial_hidden)       , "Invert --partial-hidden"             , NULL} ,
    1390             :         {NULL                         , 0   , 0                , 0                     , NULL                          , NULL                                  , NULL}
    1391             :     };
    1392             : 
    1393      987912 :     const GOptionEntry unusual_option_entries[] = {
    1394             :         {"clamp-low"              , 'q' , HIDDEN           , G_OPTION_ARG_CALLBACK , FUNC(clamp_low)              , "Limit lower reading barrier"                                 , "P"}    ,
    1395             :         {"clamp-top"              , 'Q' , HIDDEN           , G_OPTION_ARG_CALLBACK , FUNC(clamp_top)              , "Limit upper reading barrier"                                 , "P"}    ,
    1396             :         {"limit-mem"              , 'u' , HIDDEN           , G_OPTION_ARG_CALLBACK , FUNC(limit_mem)              , "Specify max. memory usage target"                            , "S"}    ,
    1397             :         {"paranoid-mem"           , 0   , HIDDEN           , G_OPTION_ARG_CALLBACK , FUNC(paranoid_mem)           , "Specify min. memory to use for in-progress paranoid hashing" , "S"}    ,
    1398             :         {"read-buffer"            , 0   , HIDDEN           , G_OPTION_ARG_CALLBACK , FUNC(read_buffer_mem)        , "Specify min. memory to use for read buffer during hashing"   , "S"}    ,
    1399             :         {"sweep-size"             , 'u' , HIDDEN           , G_OPTION_ARG_CALLBACK , FUNC(sweep_size)             , "Specify max. bytes per pass when scanning disks"             , "S"}    ,
    1400             :         {"sweep-files"            , 'u' , HIDDEN           , G_OPTION_ARG_CALLBACK , FUNC(sweep_count)            , "Specify max. file count per pass when scanning disks"        , "S"}    ,
    1401       54884 :         {"threads"                , 't' , HIDDEN           , G_OPTION_ARG_INT64    , &cfg->threads                , "Specify max. number of hasher threads"                       , "N"}    ,
    1402       54884 :         {"write-unfinished"       , 'U' , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->write_unfinished       , "Output unfinished checksums"                                 , NULL}   ,
    1403       54884 :         {"xattr-write"            , 0   , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->write_cksum_to_xattr   , "Cache checksum in file attributes"                           , NULL}   ,
    1404       54884 :         {"xattr-read"             , 0   , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->read_cksum_from_xattr  , "Read cached checksums from file attributes"                  , NULL}   ,
    1405       54884 :         {"xattr-clear"            , 0   , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->clear_xattr_fields     , "Clear xattrs from all seen files"                            , NULL}   ,
    1406       54884 :         {"with-metadata-cache"    , 0   , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->use_meta_cache         , "Swap certain metadata to disk to save RAM"                   , NULL}   ,
    1407      109768 :         {"without-metadata-cache" , 0   , DISABLE | HIDDEN , G_OPTION_ARG_NONE     , &cfg->use_meta_cache         , "Store all metadata in RAM"                                   , NULL}   ,
    1408       54884 :         {"with-fiemap"            , 0   , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->build_fiemap           , "Use fiemap(2) to optimize disk access patterns"              , NULL}   ,
    1409      109768 :         {"without-fiemap"         , 0   , DISABLE | HIDDEN , G_OPTION_ARG_NONE     , &cfg->build_fiemap           , "Do not use fiemap(2) in order to save memory"                , NULL}   ,
    1410       54884 :         {"shred-always-wait"      , 0   , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->shred_always_wait      , "Always waits for file increment to finish hashing"           , NULL}   ,
    1411       54884 :         {"fake-pathindex-as-disk" , 0   , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->fake_pathindex_as_disk , "Pretends each input path is a separate physical disk"        , NULL}   ,
    1412       54884 :         {"fake-holdback"          , 0   , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->cache_file_structs     , "Hold back all files to the end before outputting."           , NULL}   ,
    1413       54884 :         {"fake-fiemap"            , 0   , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->fake_fiemap            , "Create faked fiemap data for all files"                      , NULL}   ,
    1414       54884 :         {"buffered-read"          , 0   , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->use_buffered_read      , "Default to buffered reading calls (fread) during reading."   , NULL}   ,
    1415       54884 :         {"shred-never-wait"       , 0   , HIDDEN           , G_OPTION_ARG_NONE     , &cfg->shred_never_wait       , "Never waits for file increment to finish hashing"            , NULL}   ,
    1416             :         {NULL                     , 0   , HIDDEN           , 0                     , NULL                         , NULL                                                          , NULL}
    1417             :     };
    1418             : 
    1419             :     /* clang-format on */
    1420             : 
    1421             :     /* Initialize default verbosity */
    1422       54884 :     rm_cmd_set_verbosity_from_cnt(cfg, session->verbosity_count);
    1423             : 
    1424       54884 :     if(!rm_cmd_set_cwd(cfg)) {
    1425           0 :         g_set_error(&error, RM_ERROR_QUARK, 0, _("Cannot set current working directory"));
    1426           0 :         goto failure;
    1427             :     }
    1428             : 
    1429       54884 :     if(!rm_cmd_set_cmdline(cfg, argc, argv)) {
    1430           0 :         g_set_error(&error, RM_ERROR_QUARK, 0, _("Cannot join commandline"));
    1431           0 :         goto failure;
    1432             :     }
    1433             : 
    1434             :     ////////////////////
    1435             :     // OPTION PARSING //
    1436             :     ////////////////////
    1437             : 
    1438       54884 :     option_parser = g_option_context_new(
    1439       54884 :         _("[TARGET_DIR_OR_FILES …] [//] [TAGGED_TARGET_DIR_OR_FILES …] [-]"));
    1440       54884 :     g_option_context_set_translation_domain(option_parser, RM_GETTEXT_PACKAGE);
    1441             : 
    1442       54884 :     GOptionGroup *main_group =
    1443             :         g_option_group_new("rmlint", "main", "Most useful main options", session, NULL);
    1444       54884 :     GOptionGroup *inversion_group = g_option_group_new(
    1445             :         "inversed", "inverted", "Options that enable defaults", session, NULL);
    1446       54884 :     GOptionGroup *unusual_group =
    1447             :         g_option_group_new("unusual", "unusual", "Unusual options", session, NULL);
    1448             : 
    1449       54884 :     g_option_group_add_entries(main_group, main_option_entries);
    1450       54884 :     g_option_group_add_entries(main_group, inversed_option_entries);
    1451       54884 :     g_option_group_add_entries(main_group, unusual_option_entries);
    1452             : 
    1453       54884 :     g_option_context_add_group(option_parser, inversion_group);
    1454       54884 :     g_option_context_add_group(option_parser, unusual_group);
    1455       54884 :     g_option_context_set_main_group(option_parser, main_group);
    1456       54884 :     g_option_context_set_summary(option_parser,
    1457       54884 :                                  _("rmlint finds space waste and other broken things on "
    1458             :                                    "your filesystem and offers to remove it.\n"
    1459             :                                    "It is especially good at finding duplicates and "
    1460             :                                    "offers a big variety of options to handle them."));
    1461       54884 :     g_option_context_set_description(
    1462             :         option_parser,
    1463       54884 :         _("Only the most important options and options that alter the defaults are shown "
    1464             :           "above.\n"
    1465             :           "See the manpage (man 1 rmlint or rmlint --show-man) for far more detailed "
    1466             :           "usage information,\n"
    1467             :           "or http://rmlint.rtfd.org/en/latest/rmlint.1.html for the online manpage.\n"
    1468             :           "Complementary tutorials can be found at: http://rmlint.rtfd.org"));
    1469             : 
    1470       54884 :     g_option_group_set_error_hook(main_group, (GOptionErrorFunc)rm_cmd_on_error);
    1471             : 
    1472       54884 :     if(!g_option_context_parse(option_parser, &argc, &argv, &error)) {
    1473           5 :         goto failure;
    1474             :     }
    1475             : 
    1476       52989 :     if(clone) {
    1477             :         /* should not get here */
    1478           0 :         rm_cmd_btrfs_clone_usage();
    1479           0 :         session->cmdline_parse_error = TRUE;
    1480             :     }
    1481             : 
    1482             :     /* Silent fixes of invalid numberic input */
    1483       52989 :     cfg->threads = CLAMP(cfg->threads, 1, 128);
    1484       52989 :     cfg->depth = CLAMP(cfg->depth, 1, PATH_MAX / 2 + 1);
    1485             : 
    1486       52989 :     if(cfg->partial_hidden && !cfg->merge_directories) {
    1487             :         /* --partial-hidden only makes sense with --merge-directories.
    1488             :          * If the latter is not specfified, ignore it all together */
    1489          28 :         cfg->ignore_hidden = true;
    1490          28 :         cfg->partial_hidden = false;
    1491             :     }
    1492             : 
    1493       52989 :     if(cfg->progress_enabled) {
    1494           8 :         if(!rm_fmt_has_formatter(session->formats, "sh")) {
    1495           8 :             rm_fmt_add(session->formats, "sh", "rmlint.sh");
    1496             :         }
    1497             : 
    1498           8 :         if(!rm_fmt_has_formatter(session->formats, "json")) {
    1499           8 :             rm_fmt_add(session->formats, "json", "rmlint.json");
    1500             :         }
    1501             :     }
    1502             : 
    1503             :     /* Overwrite color if we do not print to a terminal directly */
    1504       52989 :     if(cfg->with_color) {
    1505       52989 :         cfg->with_stdout_color = isatty(fileno(stdout));
    1506       52989 :         cfg->with_stderr_color = isatty(fileno(stdout));
    1507       52989 :         cfg->with_color = (cfg->with_stdout_color | cfg->with_stderr_color);
    1508             :     } else {
    1509           0 :         cfg->with_stdout_color = cfg->with_stderr_color = 0;
    1510             :     }
    1511             : 
    1512       52989 :     if(rm_cmd_create_metadata_cache(session) == EXIT_FAILURE) {
    1513           0 :         error =
    1514           0 :             g_error_new(RM_ERROR_QUARK, 0, _("cannot create metadata cache (see above)"));
    1515       52989 :     } else if(cfg->keep_all_tagged && cfg->keep_all_untagged) {
    1516           0 :         error = g_error_new(
    1517             :             RM_ERROR_QUARK, 0,
    1518           0 :             _("can't specify both --keep-all-tagged and --keep-all-untagged"));
    1519       52989 :     } else if(cfg->skip_start_factor >= cfg->skip_end_factor) {
    1520           0 :         error = g_error_new(RM_ERROR_QUARK, 0,
    1521           0 :                             _("-q (--clamp-low) should be lower than -Q (--clamp-top)"));
    1522       52989 :     } else if(!rm_cmd_set_paths(session, paths)) {
    1523           0 :         error = g_error_new(RM_ERROR_QUARK, 0, _("No valid paths given."));
    1524       52989 :     } else if(!rm_cmd_set_outputs(session, &error)) {
    1525             :         /* Something wrong with the outputs */
    1526       52989 :     } else if(cfg->follow_symlinks && cfg->see_symlinks) {
    1527           0 :         rm_log_error("Program error: Cannot do both follow_symlinks and see_symlinks.");
    1528             :         ;
    1529             :         ;
    1530           0 :         g_assert_not_reached();
    1531             :     }
    1532             : failure:
    1533       52994 :     if(error != NULL) {
    1534           4 :         rm_cmd_on_error(NULL, NULL, session, &error);
    1535             :     }
    1536             : 
    1537       52994 :     g_option_context_free(option_parser);
    1538       52994 :     return !(session->cmdline_parse_error);
    1539             : }
    1540             : 
    1541       16107 : static int rm_cmd_replay_main(RmSession *session) {
    1542             :     /* User chose to replay some json files. */
    1543             :     RmParrotCage cage;
    1544       16107 :     rm_parrot_cage_open(&cage, session);
    1545             : 
    1546       32214 :     for(GList *iter = session->replay_files.head; iter; iter = iter->next) {
    1547       16107 :         rm_parrot_cage_load(&cage, iter->data);
    1548             :     }
    1549             : 
    1550       16107 :     rm_parrot_cage_close(&cage);
    1551       16107 :     rm_fmt_flush(session->formats);
    1552       16107 :     rm_fmt_set_state(session->formats, RM_PROGRESS_STATE_PRE_SHUTDOWN);
    1553       16107 :     rm_fmt_set_state(session->formats, RM_PROGRESS_STATE_SUMMARY);
    1554             : 
    1555       16107 :     return EXIT_SUCCESS;
    1556             : }
    1557             : 
    1558       52985 : int rm_cmd_main(RmSession *session) {
    1559       52985 :     int exit_state = EXIT_SUCCESS;
    1560             : 
    1561       52985 :     rm_fmt_set_state(session->formats, RM_PROGRESS_STATE_INIT);
    1562             : 
    1563       52985 :     if(session->replay_files.length) {
    1564       16107 :         return rm_cmd_replay_main(session);
    1565             :     }
    1566             : 
    1567       36878 :     rm_fmt_set_state(session->formats, RM_PROGRESS_STATE_TRAVERSE);
    1568       36878 :     session->mounts = rm_mounts_table_new(session->cfg->fake_fiemap);
    1569       36878 :     if(session->mounts == NULL) {
    1570           0 :         exit_state = EXIT_FAILURE;
    1571           0 :         goto failure;
    1572             :     }
    1573             : 
    1574       36878 :     rm_traverse_tree(session);
    1575             : 
    1576       36878 :     rm_log_debug_line("List build finished at %.3f with %d files",
    1577             :                  g_timer_elapsed(session->timer, NULL), session->total_files);
    1578             : 
    1579       36878 :     if(session->cfg->merge_directories) {
    1580        1120 :         g_assert(session->cfg->cache_file_structs);
    1581        1120 :         session->dir_merger = rm_tm_new(session);
    1582             :     }
    1583             : 
    1584       36878 :     if(session->total_files >= 1) {
    1585       36654 :         rm_fmt_set_state(session->formats, RM_PROGRESS_STATE_PREPROCESS);
    1586       36654 :         rm_preprocess(session);
    1587             : 
    1588       36654 :         if(session->cfg->find_duplicates || session->cfg->merge_directories) {
    1589       36458 :             rm_shred_run(session);
    1590             : 
    1591       36458 :             rm_log_debug_line("Dupe search finished at time %.3f",
    1592             :                          g_timer_elapsed(session->timer, NULL));
    1593             :         } else {
    1594             :             /* Clear leftovers */
    1595         196 :             rm_file_tables_clear(session);
    1596             :         }
    1597             :     }
    1598             : 
    1599       36878 :     if(session->cfg->merge_directories) {
    1600        1120 :         rm_fmt_set_state(session->formats, RM_PROGRESS_STATE_MERGE);
    1601        1120 :         rm_tm_finish(session->dir_merger);
    1602             :     }
    1603             : 
    1604       36878 :     rm_fmt_flush(session->formats);
    1605       36878 :     rm_fmt_set_state(session->formats, RM_PROGRESS_STATE_PRE_SHUTDOWN);
    1606       36878 :     rm_fmt_set_state(session->formats, RM_PROGRESS_STATE_SUMMARY);
    1607             : 
    1608       36878 :     if(session->shred_bytes_remaining != 0) {
    1609           0 :         rm_log_error_line(
    1610             :             "BUG: Number of remaining bytes is %"LLU" (not 0). Please report this.",
    1611             :             session->shred_bytes_remaining);
    1612           0 :         exit_state = EXIT_FAILURE;
    1613             :     }
    1614             : 
    1615       36878 :     if(session->shred_files_remaining != 0) {
    1616           0 :         rm_log_error_line(
    1617             :             "BUG: Number of remaining files is %"LLU" (not 0). Please report this.",
    1618             :             session->shred_files_remaining);
    1619           0 :         exit_state = EXIT_FAILURE;
    1620             :     }
    1621             : 
    1622             : failure:
    1623       36878 :     return exit_state;
    1624             : }

Generated by: LCOV version 1.11