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 : }
|