C-Menu 0.2.9
A User Interface Toolkit
Loading...
Searching...
No Matches
lf.c
Go to the documentation of this file.
1/** @file lf.c
2 @brief list files based on matching criteria
3 @author Bill Waller
4 Copyright (c) 2026
5 MIT License
6 billxwaller@gmail.com
7 @date 2026-09-22
8 */
9#define _GNU_SOURCE
10#include "cm.h"
11#include <argp.h>
12#include <dirent.h>
13#include <errno.h>
14#include <fcntl.h>
15#include <grp.h>
16#include <linux/limits.h>
17#include <pthread.h>
18#include <pwd.h>
19#include <regex.h>
20#include <stdatomic.h>
21#include <stdbool.h>
22#include <stdint.h>
23#include <stdio.h>
24#include <stdlib.h>
25#include <string.h>
26#include <sys/mman.h>
27#include <sys/stat.h>
28#include <sys/syscall.h>
29#include <sys/sysinfo.h>
30#include <sys/types.h>
31#include <sys/wait.h>
32#include <time.h>
33#include <unistd.h>
34
35#define QUEUE_CAPACITY 16384
36#define QUEUE_MASK (QUEUE_CAPACITY - 1)
37#define MAX_PATH_LEN _POSIX_PATH_MAX
38#define MAX_DEPTH 64
39#define DIR_BUF_SIZE 262144
40#define CACHE_LINE_SIZE 64
41
42// 32 Million directories max limit. Cost: 768MB Virtual Memory, 0MB RAM initially.
43#define ARENA_MAX_NODES (32 * 1024 * 1024)
44
45typedef struct CycleNode CycleNode;
46struct CycleNode {
47 dev_t dev;
48 ino_t ino;
49 CycleNode *parent;
50};
51
52typedef struct {
53 CycleNode *nodes;
54 _Atomic size_t index;
55 size_t capacity;
56} CycleArena;
57
58static CycleArena g_cycle_arena = {NULL, 0, 0};
59
60typedef struct QueuePayload QueuePayload;
61struct QueuePayload {
62 size_t path_len;
63 uint16_t depth;
64 char path[MAX_PATH_LEN];
65 CycleNode *ctx;
66};
67
68typedef struct {
69 alignas(CACHE_LINE_SIZE) _Atomic size_t enqueue_pos;
70 alignas(CACHE_LINE_SIZE) _Atomic size_t dequeue_pos;
71 alignas(CACHE_LINE_SIZE) _Atomic atomic_int active_tasks;
72 alignas(CACHE_LINE_SIZE) _Atomic atomic_int shut_down;
73 alignas(CACHE_LINE_SIZE) _Atomic size_t sequence[QUEUE_CAPACITY];
74 QueuePayload nodes[QUEUE_CAPACITY];
75} MPMCQueue;
76
77typedef enum {
82} TerminationStatus;
83
84struct linux_dirent64 {
85 unsigned long long d_ino; /* 64-bit inode number */
86 long long d_off; /* 64-bit offset to next structure */
87 unsigned short d_reclen; /* Size of this dirent */
88 unsigned char d_type; /* File type */
89 char d_name[]; /* Filename (null-terminated) */
90};
91
92#define print_file_type(mask, lf_type, dt_type, name)
93 {
94 fprintf(stderr, "%c %08b (%3d) %08b (%2d) %s\n",
95 (mask & lf_type) ? '*' : ' ', lf_type, lf_type,
96 dt_type, dt_type, name);
97 }
98
99struct tm tm_info;
101const char *argp_program_bug_address = "billxwaller@gmail.com";
102const char doc[] = _("lf list files\nIf specified, DIRECTORY is the top-level\n directory to search. REGULAR_EXPRESSION is a properly\nformatted regular expression for which matching files\nwill be listed.");
103bool is_hidden(const char *);
104bool is_dirsys(const char *);
105static char args_doc[] = "[DIRECTORY] [REGULAR_EXPRESSION]";
106
107TerminationStatus termination_status;
108typedef struct {
109 MPMCQueue *q;
110 pthread_t *threads;
111 unsigned int nthreads;
112 atomic_size_t file_count;
113 atomic_size_t error_count;
114 pthread_mutex_t output_mutex;
115 uintmax_t user_id;
116 off_t file_size_min;
117 FILE *err_fd;
118 char error_file_spec[MAX_PATH_LEN];
119 bool error_file_open;
120 long flags;
121 time_t after;
122 time_t before;
123 int max_depth;
124 int reg_flags;
125 char *base_path;
126 char *re;
127 char *ere;
128 char *user_name;
129 char *sbuffer;
130 regex_t compiled_re;
131 regex_t compiled_ere;
132 unsigned char include_perms;
133 unsigned char include_types;
134 unsigned char suppress_types;
135 bool report_error_count;
136 bool ignore_case;
137 bool sort;
138 bool sort_reverse;
139 bool include_hidden;
140 bool hidden_only;
141 bool follow_links;
142 bool debug;
143 bool report_config;
144 bool report_info;
145 bool report_warnings;
146 bool report_errors;
147 bool report_badlinks;
148 bool report_trace;
149 bool report_all;
150 bool count;
151 bool count_silently;
152 bool only_errors;
153} LfContext;
154
155typedef struct {
156 char data[256 * MAX_PATH_LEN];
157 size_t len;
158} OutputBuffer;
159
160#define DT_LNK_DIR 14
161unsigned char const lf_mask[15] = {
162 0, 0b00000001, 0b00000010, 0, 0b00000100, 0, 0b00001000, 0, 0b00010000, 0,
163 0b00100000, 0, 0b01000000, 0, 0b10000000};
164
166char *lfargs[3];
167char *exec;
171void debug_out(LfContext *lf, int, char **);
172bool init_lf(LfContext *lf, int, char **);
173int sort_lf_output(LfContext *lf, int, char **);
174MPMCQueue *mpmc_queue_init();
175bool mpmc_enqueue(LfContext *, const QueuePayload *child_node);
176bool mpmc_dequeue(LfContext *lf, QueuePayload *output_node);
177void *worker(void *arg);
178void *finder(LfContext *lf, QueuePayload *current_node, QueuePayload *child_node);
179int scan_file(const char *file_spec, const size_t *path_len, LfContext *lf, const unsigned char,
180 const struct stat *, OutputBuffer *);
181bool build_full_path(char *, size_t, const char *, const char *, size_t *);
182void flush_output_buffer(LfContext *, OutputBuffer *);
183bool append_output_buffer(LfContext *, OutputBuffer *, const char *, size_t,
184 bool);
185int err_out(LfContext *lf, const char *format, ...);
186void cycle_arena_init(void);
187void cycle_arena_destroy(void);
188static CycleNode *cycle_arena_alloc(dev_t dev, ino_t ino, CycleNode *parent);
189bool is_link_cycle(dev_t dev, ino_t ino, CycleNode *parent);
190// ---------------------------------------------------------------
191
192static struct argp_option options[] = {
193 {_("after"), 'a', _("time"), 0, _("Last Modified after YYYY-MM-DDTHH:MM:SS"), 0},
194 {_("before"), 'b', _("time"), 0, _("Last Modified before YYYY-MM-DDTHH:MM:SS"), 0},
195 {_("max_depth"), 'd', _("number"), 0, _("Depth into directory tree"), 0},
196 {_("error_file_spec"), 'E', _("file_spec"), 0, _("Error message output file"), 0},
197 {_("ere"), 'e', _("regex"), 0, _("Exclude regular expression"), 0},
198 {_("ignore_case"), 'i', 0, 0, _("Search ignore case"), 0},
199 {_("include_perms"), 'p', _("sgrwx"), 0,
200 _("x-execute, w-write, r-read, s-setuid, g-setgid"), 0},
201 {_("re"), 'r', _("regex"), 0, _("Regular expression to search for"), 0},
202 {_("include_types"), 't', _("pcdbflsu"), 0,
203 _("p-pipe, c-character_dev, d-directory, b-block_dev, f-regular_file, l-link, s-socket, u-unknown"),
204 0},
205 {_("file_size_min"), 's', _("size"), 0,
206 _("No Suffix-bytes, K-kilobytes, M-Megabytes, or G-Gigabytes"), 0},
207 {_("user"), 'u', _("user name"), 0, _("User Name of file owner "), 0},
208 {_("debug"), 'D', "123456789", 0,
209 _("1-config, 2-info, 3-warnings, 4-errors, 5-badlinks, 6-trace, 7-all, 8-only_errors, 9-report_error_count"),
210 0},
211 {_("include_hidden"), 'H', "o", OPTION_ARG_OPTIONAL, _("Include hidden files (o=hidden only)"), 0},
212 {_("follow_links"), 'L', 0, 0, _("Follow symbolic links"), 0},
213 {_("sort_reverse"), 'R', 0, 0, _("Sort in Reverse order"), 0},
214 {_("sort"), 'S', 0, 0, _("Sort in Ascending order"), 0},
215 {_("nthreads"), 'T', "threads", 0, _("Number of nthreads"), 0},
216 {_("count"), 'c', "s", OPTION_ARG_OPTIONAL, _("Count (s only report count)"), 0},
217 {0}};
218
219/** @brief Parse a single option. */
220static error_t parse_opt(int key, char *arg, struct argp_state *state) {
221 LfContext *lf = state->input;
222 int i = 0;
223 bool a_toi_error = false;
224
225 switch (key) {
226 case 'a':
227 parse_local_timestamp(arg, &lf->after);
228 if (lf->after && lf->before && lf->before < lf->after) {
229 fprintf(stderr, _("-b time must be greater than -a time.\n"));
230 lf->after = 0;
231 }
232 break;
233 case 'b':
234 parse_local_timestamp(arg, &lf->before);
235 if (lf->after && lf->before && lf->before < lf->after) {
236 fprintf(stderr, _("-b time must be greater than -a time.\n"));
237 lf->before = 0;
238 }
239 break;
240 case 'c':
241 lf->count = true;
242 if (arg && arg[0] != '\0') {
243 if (strcmp(arg, "s") == 0)
244 lf->count_silently |= true;
245 }
246 break;
247 case 'd':
248 if (arg && arg[0] != '\0')
249 lf->max_depth = a_toi(arg, &a_toi_error);
250 break;
251 case 'D':
252 lf->debug = true;
253 if (arg && arg[0] != '\0') {
254 debug_p = strdup(arg);
255 i = 0;
256 lf->report_errors = true;
257 while (debug_p[i]) {
258 switch (debug_p[i]) {
259 case '1': // CONFIG
260 lf->report_config = true;
261 lf->only_errors = true;
262 break;
263 case '2': // INFO
264 lf->report_info = true;
265 break;
266 case '3': // WARNINGS
267 lf->report_warnings = true;
268 break;
269 case '4': // ERRORS
270 lf->report_errors = true;
271 break;
272 case '5': // BADLINKS
273 lf->report_badlinks = true;
274 lf->report_errors = true;
275 break;
276 case '6': // TRACE
277 lf->report_trace = true;
278 lf->report_badlinks = true;
279 lf->report_errors = true;
280 break;
281 case '7': // ALL
282 lf->report_config = true;
283 lf->report_info = true;
284 lf->report_warnings = true;
285 lf->report_errors = true;
286 lf->report_badlinks = true;
287 break;
288 case '8': // ONLY ERRORS
289 lf->only_errors = true;
290 lf->report_errors = true;
291 break;
292 case '9': // ERROR COUNT
293 lf->report_error_count = true;
294 break;
295 default:
296 break;
297 }
298 i++;
299 }
300 free(debug_p);
301 }
302 break;
303 case 'e':
304 lf->ere = strdup(arg);
305 lf->flags |= LF_EXC_REGEX;
306 break;
307 case 'E':
308 strncpy(lf->error_file_spec, arg, MAX_PATH_LEN - 1);
309 break;
310 case 'H':
311 lf->include_hidden = true; // Include hidden files
312 if (arg && arg[0] == 'o') {
313 lf->hidden_only = true;
314 lf->include_hidden = true;
315 }
316 lf->flags &= ~(LF_HIDE); // Turn hide flag off
317 // LF_HIDE = 0 - include hidden files,
318 // LF_HIDE = 1 - suppress hidden files
319 break;
320 case 'i':
321 lf->ignore_case = true;
322 break;
323 case 'L':
324 lf->follow_links = true; // follow symbolic links
325 break;
326 case 'p':
327 perms_p = arg;
328 while (perms_p[i]) {
329 switch (perms_p[i++]) {
330 case 'g':
331 lf->include_perms |= LF_ISGID;
332 break;
333 case 'r':
334 lf->include_perms |= LF_IRUSR;
335 break;
336 case 's':
337 lf->include_perms |= LF_ISUID;
338 break;
339 case 'w':
340 lf->include_perms |= LF_IWUSR;
341 break;
342 case 'x':
343 lf->include_perms |= LF_IXUSR;
344 break;
345 default:
346 break;
347 }
348 }
349 break;
350 case 'R':
351 lf->sort_reverse = true;
352 break;
353 case 'r':
354 lf->re = strdup(arg);
355 lf->flags |= LF_REGEX;
356 break;
357 case 'S':
358 lf->sort = true;
359 break;
360 case 's':
361 lf->file_size_min = (intmax_t)(a_to_ul(arg));
362 break;
363 case 'T':
364 if (arg && arg[0] != '\0')
365 lf->nthreads = a_toi(arg, &a_toi_error);
366 break;
367 case 't':
368 file_types_p = arg;
369 while (file_types_p[i]) {
370 switch (file_types_p[i++]) {
371 case 'b':
372 lf->include_types |= LF_BLK;
373 break;
374 case 'c':
375 lf->include_types |= LF_CHR;
376 break;
377 case 'd':
378 lf->include_types |= LF_DIR;
379 break;
380 case 'p':
381 lf->include_types |= LF_FIFO;
382 break;
383 case 'l':
384 lf->include_types |= LF_LNK;
385 break;
386 case 'f': // for regular files, 'f' is more intuitive than 'r'
387 case 'r': // regular files are the most common type, so 'r' is also
388 // accepted
389 lf->include_types |= LF_REG;
390 break;
391 case 's':
392 lf->include_types |= LF_SOCK;
393 break;
394 case 'u':
395 lf->include_types |= LF_UNKNOWN;
396 break;
397 default:
398 break;
399 }
400 }
401 break;
402 case 'u':
403 lf->user_name = strdup(arg);
404 struct passwd *pwd = getpwnam(arg);
405 if (pwd) {
406 lf->user_id = (uintmax_t)pwd->pw_uid;
407 lf->flags |= LF_USER;
408 } else {
409 fprintf(stderr, _("User '%s' not found.\n"), arg);
410 exit(EXIT_FAILURE);
411 }
412 break;
413 case ARGP_KEY_ARG:
414 if (state->arg_num == 0 || state->arg_num == 1) {
415 lfargs[state->arg_num] = arg;
416 lfargc = state->arg_num + 1;
417 } else {
418 argp_usage(state);
419 }
420 break;
421 case ARGP_KEY_END:
422 break;
423 default:
424 return ARGP_ERR_UNKNOWN;
425 }
426 return 0;
427}
428static struct argp argp = {options, parse_opt, args_doc, doc,
429 nullptr, nullptr, nullptr};
430
431int main(int argc, char **argv) {
432 LfContext *lf = (LfContext *)calloc(1, sizeof(LfContext));
433
434 lf->count = 0;
436 lf->error_count = 0;
437 lf->report_error_count = false;
438 lf->nthreads = 0;
439 lf->ignore_case = false;
440 lf->sort = false;
441 lf->sort_reverse = false;
442 lf->include_hidden = false; // By default, hidden files are suppressed.
443 lf->hidden_only = false;
444 lf->flags |= LF_HIDE; // Turn hide flag on
445 lf->follow_links = false;
446 lf->count = false;
447 lf->count_silently = false;
448 lf->report_badlinks = false;
449 lf->sbuffer = _("4G");
450 lf->error_file_spec[0] = '\0';
451 lf->error_file_open = false;
452
453 char tmp_str[MAX_PATH_LEN];
454 argp_parse(&argp, argc, argv, 0, 0, lf);
455 if (lfargc > 0) {
456 strnz__cpy(tmp_str, lfargs[0], MAXLEN - 1);
457 expand_tilde(tmp_str, MAXLEN - 1);
458 if (is_directory(tmp_str) || is_symlink_to_dir(tmp_str)) {
459 lf->base_path = strdup(tmp_str);
460 } else if (is_valid_regex(lfargs[0])) {
461 lf->re = strdup(lfargs[0]);
462 lf->flags |= LF_REGEX;
463 } else {
464 fprintf(
465 stderr,
466 _("lf: arg1: '%s' is neither a directory nor a valid regex.\n"),
467 lfargs[0]);
468 exit(EXIT_FAILURE);
469 }
470 }
471 if (lfargc > 1) {
472 if (!lf->base_path || lf->base_path[0] == '\0') {
473 strnz__cpy(tmp_str, lfargs[1], MAXLEN - 1);
474 expand_tilde(tmp_str, MAXLEN - 1);
475 if (is_directory(tmp_str) || is_symlink_to_dir(tmp_str))
476 lf->base_path = strdup(tmp_str);
477 }
478 if ((!(lf->flags & LF_REGEX)) && is_valid_regex(lfargs[1])) {
479 lf->re = strdup(lfargs[1]);
480 lf->flags |= LF_REGEX;
481 } else {
482 fprintf(stderr,
483 _("lf: '%s' is neither a directory nor a valid regular expression.\n"),
484 lfargs[1]);
485 exit(EXIT_FAILURE);
486 }
487 }
488 if (lf->base_path == nullptr || lf->base_path[0] == '\0')
489 lf->base_path = strdup(".");
490 unsigned int nprocs = get_nprocs();
491 if (nprocs == 0)
492 nprocs = 1;
493
494 if (lf->nthreads == 0)
495 lf->nthreads = (nprocs > 1) ? (nprocs - 1) : 1;
496 else {
497 if (lf->nthreads < 1)
498 lf->nthreads = 1;
499 if (lf->nthreads > nprocs)
500 lf->nthreads = nprocs;
501 }
503 if (!lf->sort) {
504 init_lf(lf, argc, argv);
505 } else
507
508 if (lf->count) {
509 size_t count = atomic_load(&lf->file_count);
510 fprintf(stderr, _("Files: %zu\n"), count);
511 }
512 atomic_load(&lf->error_count);
513 if (lf->report_error_count && lf->error_count > 0) {
514 fprintf(stderr, _("Errors: %zu\n"), lf->error_count);
516 }
517 free(lf->q);
518 free(lf->base_path);
519 free(lf->user_name);
520 free(lf->re);
521 free(lf->ere);
522 free(lf->threads);
523 free(lf);
524 exit(termination_status);
525}
526/** @brief Sort the output of the lf command using the system's sort utility.
527 @param lf A pointer to the LfContext struct containing the search settings.
528 @param argc The number of command-line arguments.
529 @param argv An array of command-line argument strings.
530 @details This function sets up a pipeline to sort the output of the lf command. It creates a pipe, forks a child process to execute the sort command, and redirects the standard input and output streams accordingly. The sort command is executed with optional flags for parallel processing and buffer size, as well as a reverse sort option if specified in the LfContext. The function waits for the child process to complete before returning.
531 */
532int sort_lf_output(LfContext *lf, int argc, char **argv) {
533 // char tmp_str[MAXLEN];
534 char *eargv[MAXARGS];
535 int eargc = 0;
536 eargv[eargc++] = strdup("sort");
537 // snprintf(tmp_str, MAXLEN - 1, "--parallel=%d", lf->nthreads);
538 // eargv[eargc++] = strdup(tmp_str);
539 // snprintf(tmp_str, MAXLEN - 1, "--buffer-size=%s", lf->sbuffer);
540 // eargv[eargc++] = strdup(tmp_str);
541 if (lf->sort_reverse)
542 eargv[eargc++] = strdup("-r");
543 eargv[eargc] = nullptr;
544
545 setenv("LC_ALL", "C", 1); // Set locale to C for consistent sorting
546 int wstatus;
547
548 int fds[2];
549 pipe(fds); // Create the pipes
550
551 pid_t pid1 = fork();
552 if (pid1 == 0) { // Child process
553 // Child's STDIN will be redirected to the read end of the pipe, so that the sort process will read the output of the finder from the pipe.
554 // Close the write end of the pipe in the child
555 close(fds[1]);
556 dup2(fds[0], STDIN_FILENO); // Clone child's read pipe to STDIN_FILENO
557 close(fds[0]); // Close the original read end of the pipe
558 execvp(eargv[0], eargv); // Execute the sort command
559 fprintf(stderr, _("Failed to execute sort: %s\n"), strerror(errno));
560 return TS_ERROR;
561 }
562 // fclose(stdout);
563 dup2(fds[1], STDOUT_FILENO); // Clone write pipe to STDOUT_FILENO
564 stdout = fdopen(STDOUT_FILENO, "w"); // Reopen STDOUT as a stream
565 setvbuf(stdout, NULL, _IOLBF, 0); // line buffering
566 init_lf(lf, argc, argv);
567 fclose(stdout);
568 close(fds[1]);
569 wait(&wstatus);
570 for (int i = 0; i < eargc; i++)
571 free(eargv[i]);
572 return 0;
573}
574// ----------------------------------------------------------------------
575// DEBUG_OUT
576// ----------------------------------------------------------------------
577/** @brief Output debug information to stderr.
578 @param lf A pointer to the LfContext struct containing the debug settings.
579 @param argc The number of command-line arguments.
580 @param argv An array of command-line argument strings.
581 @details This function outputs debug information to stderr if debugging is enabled in the LfContext. It includes a timestamp, user information, IP addresses, command-line arguments, and various configuration settings. The output is formatted for readability and includes details about file types, permissions, regex patterns, and other relevant settings.
582 */
583void debug_out(LfContext *lf, int argc, char **argv) {
584 char user_str[100];
585 char ip_str[MAXLEN];
586 int len = 0;
587 int i;
588 bool addspace_before = false;
589 if (lf->debug && (lf->report_config || lf->report_info)) {
590 fprintf(stderr, "%s,%s,%s,", get_local_timestamp(), get_user_str(user_str, 100), get_ip_addresses(ip_str, MAXLEN));
591 for (i = 0; i < argc; i++) {
592 len = len + strlen(argv[i]);
593 if (len > 72) {
594 fprintf(stderr, "\n");
595 len = strlen(argv[i]);
596 addspace_before = false;
597 }
598 if (addspace_before) {
599 fprintf(stderr, " ");
600 len++;
601 }
602 fprintf(stderr, "%s", argv[i]);
603 addspace_before = true;
604 }
605 fprintf(stderr, "\n\n");
606 fprintf(stderr, "%s\n\n", CM_VERSION);
607 fprintf(stderr, _("lf debug %s\n"),
608 lf->debug ? _("true") : _(" false"));
609 fprintf(stderr, _(" 1-config %s\n"),
610 lf->report_config ? _("true") : _("| false"));
611 fprintf(stderr, _(" 2-info %s\n"),
612 lf->report_info ? _("true") : _("| false"));
613 fprintf(stderr, _(" 3-warnings %s\n"),
614 lf->report_warnings ? _("true") : _("| false"));
615 fprintf(stderr, _(" 4-errors %s\n"),
616 lf->report_errors ? _("true") : _("| false"));
617 fprintf(stderr, _(" 5-badlinks %s\n"),
618 lf->report_errors ? _("true") : _("| false"));
619 fprintf(stderr, _(" 6-trace %s\n"),
620 lf->report_errors ? _("true") : _("| false"));
621 fprintf(stderr, _(" 8-only_errors %s\n"),
622 lf->report_errors ? _("true") : _("| false"));
623 fprintf(stderr, "\n");
624 fprintf(stderr, _("Count files: %s\n"),
625 lf->report_errors ? _("true") : _("| false"));
626 fprintf(stderr, _("Count only: %s\n"),
627 lf->report_errors ? _("true") : _("| false"));
628 fprintf(stderr, "\n");
629 fprintf(stderr, _("Search directory: %s\n\n"), lf->base_path);
630 if (lf->max_depth == 0)
631 fprintf(stderr, _("Max depth 0 (unlimited)\n\n"));
632 else
633 fprintf(stderr, _("Max depth %d\n\n"), lf->max_depth);
634 fprintf(stderr, _("Using %d threads\n\n"), lf->nthreads);
635 fprintf(stderr, _("File types preceeded by an asterisk (\"*\") will be included:\n\n"));
636 fprintf(stderr, _(" LF type DT type\n"));
637 print_file_type(lf->include_types, LF_FIFO, DT_FIFO, _("FIFO p-named pipe"));
638 print_file_type(lf->include_types, LF_CHR, DT_CHR, _("CHR c-character device"));
639 print_file_type(lf->include_types, LF_DIR, DT_DIR, _("DIR d-directory"));
640 print_file_type(lf->include_types, LF_BLK, DT_BLK, _("BLK b-block device"));
641 print_file_type(lf->include_types, LF_REG, DT_REG, _("REG f-regular file"));
642 print_file_type(lf->include_types, LF_LNK, DT_LNK, _("LINK l-symbolic link"));
643 print_file_type(lf->include_types, LF_SOCK, DT_SOCK, _("SOCK s-socket"));
644 print_file_type(lf->include_types, LF_UNKNOWN, DT_UNKNOWN, _("UNKNOWN u-unknown"));
645 fprintf(stderr, "\n");
646 fprintf(stderr, "f->include_types = %08b\n", lf->include_types);
647 fprintf(stderr, "f->suppress_types = %08b\n", lf->suppress_types);
648 fprintf(stderr, "\n");
649 if (lf->flags & LF_USER)
650 fprintf(stderr, _("User: %s (%ju)\n"), lf->user_name, lf->user_id);
651 if (lf->include_perms) {
652 if (lf->include_perms & LF_IXUSR)
653 fprintf(stderr, _(" %08b Execute\n"), LF_IXUSR);
654 if (lf->include_perms & LF_IWUSR)
655 fprintf(stderr, _(" %08b Write\n"), LF_IWUSR);
656 if (lf->include_perms & LF_IRUSR)
657 fprintf(stderr, _(" %08b Read\n"), LF_IRUSR);
658 if (lf->include_perms & LF_ISUID)
659 fprintf(stderr, _(" %08b SETUID\n"), LF_ISUID);
660 if (lf->include_perms & LF_ISGID)
661 fprintf(stderr, _(" %08b SETGID\n"), LF_ISGID);
662 }
663 fprintf(stderr, "\n");
664 if (lf->flags & LF_REGEX)
665 fprintf(stderr, _("Include regex: %s\n\n"), lf->re);
666 if (lf->flags & LF_EXC_REGEX)
667 fprintf(stderr, _("Exclude regex: %s\n\n"), lf->ere);
668
669 if (lf->after) {
670 char buf[32];
671 format_local_timestamp(lf->after, buf, sizeof(buf));
672 fprintf(stderr, _("Modified after: %s\n\n"), buf);
673 }
674
675 if (lf->before) {
676 char buf[32];
677 format_local_timestamp(lf->before, buf, sizeof(buf));
678 fprintf(stderr, _("Modified before: %s\n\n"), buf);
679 }
680
681 if (lf->file_size_min) {
682 const char *units[] = {"b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb"};
683 off_t size = lf->file_size_min;
684 i = 0;
685 while (size >= 1024 && i < 6) {
686 size /= 1024;
687 i++;
688 }
689 char buffer[32];
690 ssnprintf(buffer, 32, "%ld %s", size, units[i]);
691 fprintf(stderr, _("Minimum file size: %s\n\n"), buffer);
692 }
693 if (lf->max_depth)
694 fprintf(stderr, _("Max depth: %d\n\n"), lf->max_depth);
695 if (lf->ignore_case)
696 fprintf(stderr, _("Ignore case in regex matching.\n\n"));
697 if (lf->include_hidden)
698 fprintf(stderr, _("Include hidden files.\n\n"));
699 if (lf->follow_links)
700 fprintf(stderr, _("Follow symbolic links.\n\n"));
701 if (lf->sort)
702 fprintf(stderr, _("Sort output in ascending order.\n\n"));
703 if (lf->sort_reverse)
704 fprintf(stderr, _("Sort output in reverse order.\n\n"));
705 }
706 return;
707}
708// ----------------------------------------------------------------------
709// INIT_FIND
710// ----------------------------------------------------------------------
711/** @brief Initialize the file search process.
712 @param lf A pointer to the LfContext struct containing the search settings.
713 @param argc The number of command-line arguments.
714 @param argv An array of command-line argument strings.
715 @return true if initialization is successful, false otherwise.
716 @details This function initializes the file search process based on the
717 provided settings in the LfContext struct. It sets up file type inclusion
718 and suppression, compiles regular expressions if specified, and creates a
719 multi-producer, multi-consumer queue for managing directory traversal tasks.
720 It also initializes threads for concurrent processing of directory entries.
721 */
722bool init_lf(LfContext *lf, int argc, char **argv) {
723 /** suppress file types that aren't included */
724 if (!lf->include_types)
725 lf->include_types = 0xff;
726 if (lf->include_types)
727 lf->suppress_types = lf->include_types ^ 0xff;
728 // LF_HIDE = 0 - include hidden files,
729 // LF_HIDE = 1 - suppress hidden files
730 lf->include_hidden = !(lf->flags & LF_HIDE);
731 int reti = 0;
732 lf->reg_flags = REG_EXTENDED;
733 if (lf->ignore_case)
734 lf->reg_flags |= REG_ICASE;
735 if (lf->flags & LF_REGEX) {
736 reti = regcomp(&lf->compiled_re, lf->re, lf->reg_flags);
737 if (reti) {
738 fprintf(stderr, _("lf: '%s' Invalid pattern\n"), lf->re);
739 regfree(&lf->compiled_re);
740 return false;
741 }
742 }
743 if (lf->flags & LF_EXC_REGEX) {
744 reti = regcomp(&lf->compiled_ere, lf->ere, lf->reg_flags);
745 if (reti) {
746 fprintf(stderr, _("lf: '%s' Invalid exclude pattern\n"), lf->ere);
747 regfree(&lf->compiled_ere);
748 return false;
749 }
750 }
751 debug_out(lf, argc, argv);
752 //--------------------------------------------------------------------
753 // Create and enqueue the first QueuePayload
755 int rc = 0;
756 struct stat sb;
757 rc = pthread_mutex_init(&lf->output_mutex, NULL);
758 if (rc != 0)
759 perror(_("Mutex initialization failed"));
760 rc = stat(lf->base_path, &sb);
761 if (rc != 0)
762 return false;
763 lf->q = mpmc_queue_init();
764 QueuePayload root_node;
766 if (S_ISDIR(sb.st_mode)) {
767 root_node.path_len = strnz__cpy(root_node.path, lf->base_path, MAX_PATH_LEN - 1);
768 root_node.depth = 0;
769 CycleNode *root_ctx = cycle_arena_alloc(sb.st_dev, sb.st_ino, nullptr);
770 root_node.ctx = root_ctx;
771 if (!mpmc_enqueue(lf, &root_node)) {
772 fprintf(stderr, _("Failed to enqueue initial directory\n"));
773 return false;
774 }
775 atomic_fetch_add_explicit(&lf->q->active_tasks, 1, memory_order_relaxed);
776 //------------------------------------------------------------
777 // INITIALIZE THREADS
778 //------------------------------------------------------------
779 lf->threads = calloc(lf->nthreads, sizeof(pthread_t));
780 if (!lf->threads) {
781 fprintf(stderr, _("Out of memory allocating threads\n"));
782 return false;
783 }
784 for (unsigned int i = 0; i < lf->nthreads; i++) {
785 rc = pthread_create(
786 &lf->threads[i],
787 NULL,
788 worker,
789 lf);
790 if (rc != 0) {
791 fprintf(stderr, _("Error: Unable to create thread %d\n"), rc);
792 lf->q->shut_down = 1;
794 return false;
795 }
796 }
797 //------------------------------------------------------------
798 // END THREADS
799 //------------------------------------------------------------
800 for (unsigned int i = 0; i < lf->nthreads; i++)
801 pthread_join(lf->threads[i], NULL);
802 rc = pthread_mutex_destroy(&lf->output_mutex);
803 if (rc != 0)
804 perror(_("Mutex destroy failed"));
805 if (lf->flags & LF_REGEX)
806 regfree(&lf->compiled_re);
807 if (lf->flags & LF_EXC_REGEX)
808 regfree(&lf->compiled_ere);
809 return true;
810 } else {
811 fprintf(stderr,
812 _("Warning: Base path '%s' is not a directory. No "
813 "files will be found.\n"),
814 lf->base_path);
816 return false;
817 }
818 if (reti)
819 return false;
820 return true;
821}
822// ----------------------------------------------------------------------
823// CYCLE_ARENA_INIT
824// ----------------------------------------------------------------------
825/** @brief Initialize the cycle arena for tracking visited directories.
826 @details This function initializes a global cycle arena structure that is used to track visited directories during the file search process. It reserves virtual memory space for the arena using mmap, allowing for efficient allocation of CycleNode structures without immediate physical memory allocation. The arena is designed to handle a maximum number of nodes defined by ARENA_MAX_NODES.
827 */
829 g_cycle_arena.capacity = ARENA_MAX_NODES;
830 size_t total_bytes = g_cycle_arena.capacity * sizeof(CycleNode);
831
832 // Reserve virtual space. The OS marks it as yours but allocates NO physical RAM yet. MAP_ANONYMOUS guarantees the memory block is zero-initialized automatically.
833 g_cycle_arena.nodes = mmap(NULL, total_bytes,
834 PROT_READ | PROT_WRITE,
835 MAP_ANONYMOUS | MAP_PRIVATE,
836 -1, 0);
837
838 if (g_cycle_arena.nodes == MAP_FAILED) {
839 perror(_("mmap failed to reserve tracking arena memory"));
840 exit(EXIT_FAILURE);
841 }
842
843 atomic_init(&g_cycle_arena.index, 0);
844}
845// ----------------------------------------------------------------------
846// CYCLE_ARENA_DESTROY
847// ----------------------------------------------------------------------
848/** @brief Destroy the cycle arena and release allocated resources.
849 @details This function releases the resources allocated for the global cycle arena structure. It unmaps the virtual memory space reserved for the arena using munmap, allowing the operating system to reclaim the memory. This function should be called when the cycle arena is no longer needed to prevent memory leaks.
850 */
852 if (g_cycle_arena.nodes != NULL && g_cycle_arena.nodes != MAP_FAILED) {
853 size_t total_bytes = g_cycle_arena.capacity * sizeof(CycleNode);
854 munmap(g_cycle_arena.nodes, total_bytes);
855 }
856}
857// ----------------------------------------------------------------------
858// CYCLE_ARENA_ALLOC
859// ----------------------------------------------------------------------
860/** @brief Allocate a new CycleNode in the cycle arena.
861 @param dev The device ID of the directory being allocated.
862 @param ino The inode number of the directory being allocated.
863 @param parent A pointer to the parent CycleNode, or nullptr if this is the root.
864 @return A pointer to the newly allocated CycleNode, or nullptr if allocation fails (e.g., if the arena is full).
865 @details This function allocates a new CycleNode structure in the global cycle arena. It uses atomic operations to ensure thread-safe allocation of nodes. If the arena has reached its maximum capacity, the function returns nullptr to indicate that no more nodes can be allocated. The allocated node is initialized with the provided device ID, inode number, and parent pointer.
866 */
867static CycleNode *cycle_arena_alloc(dev_t dev, ino_t ino, CycleNode *parent) {
868 size_t idx = atomic_fetch_add_explicit(&g_cycle_arena.index, 1, memory_order_relaxed);
869 // Bounds check to ensure we don't breach our massive virtual limit
870 if (idx >= g_cycle_arena.capacity) {
871 return NULL;
872 }
873 // As soon as this thread hits an unmapped block of memory, the OS transparently provisions a 4KB hardware page of physical RAM for it on the fly.
874 CycleNode *node = &g_cycle_arena.nodes[idx];
875 node->dev = dev;
876 node->ino = ino;
877 node->parent = parent;
878 return node;
879}
880// ----------------------------------------------------------------------
881// BUILD_FULL_PATH
882// ----------------------------------------------------------------------
883/** @brief Build a full file path by concatenating a directory path and a file
884 * name.
885 @param dst A pointer to the destination buffer where the full path will be stored.
886 @param dst_size The size of the destination buffer.
887 @param dir_path The directory path to be concatenated.
888 @param name The file name to be concatenated.
889 @param out_len A pointer to a size_t variable where the length of the resulting full path will be stored (optional).
890 @return true if the full path was successfully built, false if an error occurred (e.g., null pointers or insufficient buffer size).
891 @details This function constructs a full file path by concatenating the provided directory path and file name. It ensures that there is a single '/' separator between the directory and file name, and it checks for sufficient buffer size before performing the concatenation. If successful, it null-terminates the resulting string and optionally returns its length.
892 */
893bool build_full_path(char *dst, size_t dst_size, const char *dir_path,
894 const char *name, size_t *out_len) {
895 if (dst == nullptr || dir_path == nullptr || name == nullptr || dst_size == 0)
896 return false;
897 size_t dir_len = strlen(dir_path);
898 size_t name_len = strlen(name);
899 bool need_sep = dir_len > 0 && dir_path[dir_len - 1] != '/';
900 size_t full_len = dir_len + (need_sep ? 1 : 0) + name_len;
901 if (full_len >= dst_size)
902 return false;
903 memcpy(dst, dir_path, dir_len);
904 size_t pos = dir_len;
905 if (need_sep)
906 dst[pos++] = '/';
907 memcpy(dst + pos, name, name_len);
908 dst[full_len] = '\0';
909 if (out_len)
910 *out_len = full_len;
911 return true;
912}
913// ----------------------------------------------------------------------
914// FLUSH_OUTPUT_BUFFER
915// ----------------------------------------------------------------------
916/** @brief Flush the output buffer to stdout in a thread-safe manner.
917 @param lf A pointer to the LfContext struct containing the output mutex.
918 @param output A pointer to the OutputBuffer struct containing the data to be flushed.
919 @details This function writes the contents of the output buffer to stdout using fwrite_unlocked for efficiency. It uses a mutex to ensure that only one thread can write to stdout at a time, preventing interleaved output from multiple threads. After flushing, it resets the length of the output buffer to zero.
920 */
921void flush_output_buffer(LfContext *lf, OutputBuffer *output) {
922 if (lf == nullptr || output == nullptr || output->len == 0)
923 return;
924 pthread_mutex_lock(&lf->output_mutex);
925 fwrite_unlocked(output->data, 1, output->len, stdout);
926 pthread_mutex_unlock(&lf->output_mutex);
927 output->len = 0;
928}
929// ----------------------------------------------------------------------
930// APPEND_OUTPUT_BUFFER
931// ----------------------------------------------------------------------
932/** @brief Append a path to the output buffer, flushing if necessary.
933 @param lf A pointer to the LfContext struct containing the output mutex.
934 @param output A pointer to the OutputBuffer struct where the path will be appended.
935 @param path The path string to append to the output buffer.
936 @param path_len The length of the path string.
937 @param append_slash A boolean indicating whether to append a slash after the path.
938 @return true if the path was successfully appended or printed, false if an error occurred (e.g., null pointers).
939 @details This function appends a given path to an output buffer. If the buffer is full, it flushes the buffer to stdout. If the path is too long to fit in the buffer, it prints it directly to stdout. The function uses a mutex to ensure thread-safe access to stdout when printing directly.
940 */
941bool append_output_buffer(LfContext *lf, OutputBuffer *output, const char *path,
942 size_t path_len, bool append_slash) {
943 if (lf == nullptr || output == nullptr || path == nullptr)
944 return false;
945 size_t line_len = path_len + (append_slash ? 1 : 0) + 1;
946 if (line_len > sizeof(output->data)) {
947 pthread_mutex_lock(&lf->output_mutex);
948 fwrite_unlocked(path, 1, path_len, stdout);
949 if (append_slash)
950 fputc_unlocked('/', stdout);
951 fputc_unlocked('\n', stdout);
952 pthread_mutex_unlock(&lf->output_mutex);
953 return true;
954 }
955 if (output->len + line_len > sizeof(output->data))
956 flush_output_buffer(lf, output);
957 memcpy(output->data + output->len, path, path_len);
958 output->len += path_len;
959 if (append_slash)
960 output->data[output->len++] = '/';
961 output->data[output->len++] = '\n';
962 return true;
963}
964// ----------------------------------------------------------------------
965// MPMC_QUEUE_INIT
966// ----------------------------------------------------------------------
967/** @brief Initialize the MPMCQueue.
968 @param q A pointer to the MPMCQueue struct representing the queue.
969 @details This function initializes the MPMCQueue by setting up the sequence numbers for each node in the queue and initializing the enqueue and dequeue indexes. It ensures that the queue is ready for concurrent access by multiple producer and consumer threads.
970 */
971MPMCQueue *mpmc_queue_init() {
972 MPMCQueue *q = calloc(1, sizeof(MPMCQueue));
973 if (q == nullptr)
974 return nullptr;
975 for (size_t i = 0; i < QUEUE_CAPACITY; i++)
976 atomic_init(&q->sequence[i], i);
977 atomic_init(&q->enqueue_pos, 0);
978 atomic_init(&q->dequeue_pos, 0);
979 atomic_init(&q->active_tasks, 0);
980 atomic_init(&q->shut_down, 0);
981 return q;
982}
983// ----------------------------------------------------------------------
984// MPMC_ENQUEUE
985// ----------------------------------------------------------------------
986/** @brief Enqueue a task into the MPMCQueue.
987 @param lf A pointer to the LfContext struct containing the queue.
988 @param dir A pointer to the QueuePayload struct representing the task to enqueue.
989 @return true if the task was successfully enqueued, false if the queue is full or an error occurred.
990 @details This function attempts to enqueue a task into the MPMCQueue. It uses atomic operations to ensure thread-safe access to the queue's enqueue position and sequence numbers. If the queue is full, it returns false, allowing for potential recursion or other handling by the caller. The function also includes a spin-wait mechanism to reduce contention when multiple threads are trying to enqueue tasks simultaneously.
991 */
992bool mpmc_enqueue(LfContext *lf, const QueuePayload *child_node) {
993 size_t pos = atomic_load_explicit(&lf->q->enqueue_pos,
994 memory_order_relaxed);
995 int spin_count = 0;
996 const int SPIN_LIMIT = 16;
997 while (true) {
998 size_t seq = atomic_load_explicit(&lf->q->sequence[pos & QUEUE_MASK], memory_order_acquire);
999 intptr_t diff = (intptr_t)seq - (intptr_t)pos;
1000 if (diff == 0) { // Queue is not full, ready to enqueue
1001 if (atomic_compare_exchange_weak_explicit(&lf->q->enqueue_pos, &pos, pos + 1,
1002 memory_order_relaxed, memory_order_relaxed)) {
1003 break;
1004 }
1005 } else if (diff < 0) // Queue is full
1006 return false; // trigger recursion
1007 else {
1008 pos++;
1009 // pos = atomic_load_explicit(&lf->q->enqueue_pos,
1010 // memory_order_relaxed);
1011 }
1012 if (++spin_count > SPIN_LIMIT)
1013 return false;
1014#if defined(__x86_64__)
1015 __builtin_ia32_pause();
1016#endif
1017 }
1018 memcpy(&lf->q->nodes[pos & QUEUE_MASK], child_node, sizeof(QueuePayload));
1019 atomic_store_explicit(&lf->q->sequence[pos & QUEUE_MASK], pos + 1, memory_order_release);
1020 return true;
1021}
1022// ----------------------------------------------------------------------
1023// MPMC_DEQUEUE
1024// ----------------------------------------------------------------------
1025/** @brief Dequeue a task from the MPMCQueue.
1026 @param lf A pointer to the LfContext struct containing the queue.
1027 @param task A pointer to the QueuePayload struct where the dequeued task will be stored.
1028 @return true if a task was successfully dequeued, false if the queue is empty or shut down.
1029 @details This function attempts to dequeue a task from the MPMCQueue. It uses atomic operations to ensure thread-safe access to the queue's dequeue position and sequence numbers. If the queue is empty, it yields the processor to reduce contention. If the queue has been shut down, it returns false, allowing for graceful termination of worker threads.
1030 */
1031bool mpmc_dequeue(LfContext *lf, QueuePayload *out_node) {
1032 size_t pos = atomic_load_explicit(&lf->q->dequeue_pos, memory_order_relaxed);
1033 while (true) {
1034 if (atomic_load_explicit(&lf->q->shut_down, memory_order_relaxed))
1035 return false;
1036 size_t seq = atomic_load_explicit(&lf->q->sequence[pos & QUEUE_MASK], memory_order_acquire);
1037 intptr_t diff = (intptr_t)seq - (intptr_t)(pos + 1);
1038 if (diff == 0) { // Full queue, ready to dequeue
1039 if (atomic_compare_exchange_strong_explicit(&lf->q->dequeue_pos, &pos, pos + 1,
1040 memory_order_relaxed, memory_order_relaxed)) {
1041 break;
1042 }
1043 } else if (diff < 0) { // Queue is temporarily empty
1044 if (atomic_load_explicit(&lf->q->active_tasks,
1045 memory_order_relaxed) == 0) {
1046 atomic_store_explicit(&lf->q->shut_down, 1,
1047 memory_order_release);
1048 return false;
1049 }
1050 sched_yield();
1051 pos = atomic_load_explicit(&lf->q->dequeue_pos, memory_order_relaxed);
1052 } else {
1053 pos = atomic_load_explicit(&lf->q->dequeue_pos, memory_order_relaxed);
1054 }
1055 }
1056 // memcpy(output_node, &lf->q->nodes[pos & QUEUE_MASK],
1057 // sizeof(QueuePayload));
1058 if (out_node)
1059 *out_node = lf->q->nodes[pos & QUEUE_MASK];
1060 // current_node = &lf->q->nodes[pos & QUEUE_MASK];
1061 atomic_store_explicit(&lf->q->sequence[pos & QUEUE_MASK], pos + QUEUE_CAPACITY, memory_order_release);
1062 return true;
1063}
1064// ----------------------------------------------------------------------
1065// WORKER
1066// ----------------------------------------------------------------------
1067/** @brief Worker thread function that processes tasks from the MPMCQueue.
1068 @param arg A pointer to the LfContext struct containing the queue and other context information.
1069 @return NULL upon completion.
1070 @details This function runs in a loop, dequeuing tasks from the MPMCQueue and processing them using the finder function. It decrements the active task count after processing each task. If the active task count reaches zero, it sets the shut_down flag to true, signaling other threads to terminate. The function exits when there are no more tasks to process or when a shutdown is detected.
1071 */
1072void *worker(void *arg) {
1073 LfContext *lf = (LfContext *)arg;
1074 // current_node is a pointer to the current task being processed, while child_node is a local variable used to hold the next task to be enqueued. The worker thread continuously dequeues tasks from the queue and processes them using the finder function. If the finder function returns NULL, indicating that there are no more tasks to process, the active task count is decremented. If the active task count reaches zero, the shut_down flag is set to true, signaling other threads to terminate.
1075 QueuePayload *current_node = calloc(1, sizeof(QueuePayload));
1076 QueuePayload *child_node = calloc(1, sizeof(QueuePayload));
1077 if (child_node == nullptr) {
1078 fprintf(stderr, _("Out of memory allocating child_node\n"));
1079 return NULL;
1080 }
1081 while (true) {
1082 if (mpmc_dequeue(lf, current_node) == true) {
1083 memset(child_node, 0, sizeof(QueuePayload));
1084 if (finder(lf, current_node, child_node) == NULL) {
1085 if (atomic_fetch_sub_explicit(&lf->q->active_tasks, 1, memory_order_acq_rel) == 1) {
1086 atomic_store_explicit(&lf->q->shut_down, 1, memory_order_release);
1087 }
1088 }
1089 } else
1090 break;
1091 }
1092 free(child_node);
1093 free(current_node);
1094 return NULL;
1095}
1096// ----------------------------------------------------------------------
1097// FINDER
1098// ----------------------------------------------------------------------
1099/** @brief Process a directory and its entries, applying filters and enqueuing subdirectories.
1100 @param lf A pointer to the LfContext struct containing the queue and other context information.
1101 @param current_node A pointer to the QueuePayload struct representing the current directory to process.
1102 @return NULL upon completion.
1103 @details This function opens the specified directory, reads its entries, and processes each entry according to the specified filters (e.g., file types, hidden files, max depth). It uses fstatat to get metadata for each entry and determines the effective type. If an entry is a directory and meets the criteria, it is enqueued for further processing. The function handles errors gracefully, logging them if necessary, and ensures that output is written in a thread-safe manner.
1104 */
1105void *finder(LfContext *lf, QueuePayload *current_node, QueuePayload *child_node) {
1106 OutputBuffer output = {{'\0'}, 0};
1107 long nread;
1108 char dir_buf[DIR_BUF_SIZE];
1109 struct stat sb = {};
1110 struct linux_dirent64 *entry;
1111 dev_t actual_dev = 0, effective_dev = 0;
1112 ino_t actual_ino = 0, effective_ino = 0;
1113 unsigned char actual_type;
1114 unsigned char effective_type;
1115 CycleNode *child_ctx;
1116 char full_path[MAX_PATH_LEN] = {'\0'};
1117 int rc;
1118 int dir_fd = open(current_node->path, O_RDONLY | O_DIRECTORY);
1119 if (dir_fd == -1) {
1120 if (lf->report_errors) {
1121 err_out(lf, _("OPEN_FAIL,%s,%s\n"), current_node->path, strerror(errno));
1122 }
1123 flush_output_buffer(lf, &output);
1124 return NULL;
1125 }
1126 while (1) {
1127 //--------------------------------------------------------------------
1128 // READ DIRECTORY
1129 //--------------------------------------------------------------------
1130 // Read the directory entries and process each one. We use SYS_getdents
1131 // to iterate over the entries in the directory. For each entry, we
1132 // construct the full path and use fstatat to get the metadata of the
1133 // entry. If the entry is a symbolic link, we check if the user has
1134 // chosen to follow links and get the metadata of the target it points
1135 // to. We then determine the effective type of the entry and apply the
1136 // specified filters (e.g., hidden files, max depth) to decide whether
1137 // to process it further or enqueue it for searching.
1138 nread = syscall(SYS_getdents64, dir_fd, dir_buf, DIR_BUF_SIZE);
1139 if (nread == -1) {
1140 atomic_fetch_add(&lf->error_count, 1);
1141 if (lf->report_errors) {
1142 err_out(lf, _("READDIR_FAIL,%s,%s\n"), current_node->path, strerror(errno));
1143 }
1144 break;
1145 }
1146 if (nread == 0)
1147 break;
1148 for (size_t bpos = 0; bpos < (size_t)nread;) {
1149 entry = (struct linux_dirent64 *)(dir_buf + bpos);
1150 bpos += entry->d_reclen;
1151 //--------------------------------------------------------------------
1152 // PROCESS DIRECTORY ENTRIES
1153 //--------------------------------------------------------------------
1154 // Get link's metadata We use fstatat with AT_SYMLINK_NOFOLLOW to get the metadata of the symbolic link itself, rather than the target it points to. This allows us to determine if the entry is a symbolic link and handle it according to the user's options (e.g., whether to follow links or not). If fstatat fails, we log the error (if debugging is enabled) and continue to the next entry without processing this one further.
1155 full_path[0] = '\0';
1156 actual_type = entry->d_type;
1157 effective_type = actual_type;
1158 if (actual_type == DT_DIR) {
1159 if (entry->d_name[0] == '.') {
1160 if (entry->d_name[1] == '\0')
1161 continue;
1162 if (entry->d_name[1] == '.' && entry->d_name[2] == '\0')
1163 continue;
1164 }
1165 }
1166 if (((lf->follow_links || lf->report_badlinks) &&
1167 (actual_type == DT_LNK || actual_type == DT_DIR)) ||
1168 actual_type == DT_UNKNOWN) {
1169 //--------------------------------------------------------------------
1170 // GET ADVANCED METADATA
1171 //--------------------------------------------------------------------
1172 // We use fstatat with AT_SYMLINK_NOFOLLOW to get the metadata of the symbolic link itself, rather than the target it points to. This allows us to determine if the entry is a symbolic link and handle it according to the user's options (e.g., whether to follow links or not). If fstatat fails, we log the error (if debugging is enabled) and continue to the next entry without processing this one further.
1173 memset(&sb, 0, sizeof(struct stat));
1174 rc = fstatat(dir_fd, entry->d_name, &sb, AT_SYMLINK_NOFOLLOW);
1175 if (rc == -1) {
1176 atomic_fetch_add(&lf->error_count, 1);
1177 if (lf->report_badlinks) {
1178 err_out(lf, _("LSTAT_FAIL,%s,%s\n"), full_path,
1179 strerror(errno));
1180 }
1181 } else {
1182 actual_ino = sb.st_ino;
1183 actual_dev = sb.st_dev;
1184 actual_type = (sb.st_mode & S_IFMT) >> 12;
1185 effective_dev = actual_dev;
1186 effective_ino = actual_ino;
1187 effective_type = actual_type;
1188 }
1189 if (S_ISLNK(sb.st_mode)) {
1190 // Determine the real type of the entry. If the entry is a symbolic link, we set actual_type to DT_LNK and then attempt to get the metadata of the target it points to using fstatat without AT_SYMLINK_NOFOLLOW. This allows us to determine the effective type of the entry based on the target's metadata, which is important for deciding how to process it (e.g., whether it's a directory that we should enqueue for further searching). If fstatat fails when trying to get the target's metadata, we log the error (if debugging is enabled) but continue processing the entry based on its symbolic link metadata.
1191 if (lf->follow_links) {
1192 //------------------------------------------------------------
1193 // GET LINK METADATA
1194 //------------------------------------------------------------
1195 char tmp_str[MAXLEN];
1196 rc = fstatat(dir_fd, entry->d_name, &sb, 0);
1197 if (rc == -1) {
1198 atomic_fetch_add(&lf->error_count, 1);
1199 if (lf->report_errors) {
1200 err_out(lf, _("STAT_FAIL,%s,%s\n"), entry->d_name,
1201 strerror(errno));
1202 ssnprintf(tmp_str, MAXLEN - 1, _("FSTATAT_FAIL,%s/%s\n"),
1203 entry->d_name, strerror(errno));
1204 }
1205 } else {
1206 effective_ino = sb.st_ino;
1207 effective_dev = sb.st_dev;
1208 effective_type = (sb.st_mode & S_IFMT) >> 12;
1209 }
1210 } else {
1211 effective_type = DT_REG;
1212 }
1213 }
1214 }
1215 if (effective_type == DT_DIR) {
1216 //--------------------------------------------------------------------
1217 // PROCESS DIRECTORY ENTRY - CREATE CHILD_NODE
1218 //--------------------------------------------------------------------
1219 // We build the full path for the child directory and check if it exceeds the maximum path length. If it does, we log an error and continue to the next entry. If the child directory is within the allowed depth, we create a new QueuePayload for it, copying the current history of dev/inode pairs for cycle detection. We then attempt to enqueue the child directory for further processing. If the queue is full, we handle it by running the finder function inline recursively.
1220 child_ctx = cycle_arena_alloc(effective_dev, effective_ino, current_node->ctx);
1221 child_node->ctx = child_ctx;
1222 if (!build_full_path(child_node->path,
1223 sizeof(child_node->path),
1224 current_node->path,
1225 entry->d_name,
1226 &child_node->path_len)) {
1227 atomic_fetch_add(&lf->error_count, 1);
1228 if (lf->report_errors) {
1229 err_out(lf, _("PATH_TOO_LONG,%s/%s\n"),
1230 child_node->path, entry->d_name);
1231 }
1232 continue;
1233 }
1234 bool cycle_found = false;
1235 // Determine the effective type of the entry. We use the st_mode
1236 // field from the stat struct to determine the file type by
1237 // applying the S_IFMT mask and shifting it to get a value that
1238 // corresponds to the DT_* constants. If the entry is a symbolic
1239 // link and the user has chosen not to follow links, we treat it
1240 // as a directory for the purpose of deciding whether to enqueue
1241 // it for further searching. This allows us to handle symbolic
1242 // links that point to directories in a way that respects the
1243 // user's options while still allowing for traversal of linked
1244 // directories if desired.
1245 if (lf->follow_links && actual_type == DT_LNK) {
1246 // -------------------------------------------------------
1247 // CYCLE_DETECTION
1248 // -------------------------------------------------------
1249 // Check for cycles by comparing the current directory's dev/inode with the history of dev/inode pairs from parent directories. If a match is found, it indicates a cycle and we skip processing this directory.
1250 char target_path[MAX_PATH_LEN] = {'\0'};
1251 if (lf->report_trace || lf->report_badlinks) {
1252 ssize_t len = readlinkat(dir_fd, entry->d_name, target_path, sizeof(target_path) - 1);
1253 if (len != -1)
1254 target_path[len] = '\0';
1255 }
1256 child_ctx = cycle_arena_alloc(effective_dev, effective_ino, current_node->ctx);
1257 if (child_ctx == nullptr) {
1258 atomic_fetch_add(&lf->error_count, 1);
1259 if (lf->report_errors) {
1260 err_out(lf, _("CYCLE_ARENA_ALLOC_FAIL,%s\n"), current_node->path);
1261 }
1262 continue;
1263 }
1264 child_node->ctx = child_ctx;
1265 if (lf->report_trace) {
1266 err_out(lf, _("---CYCLE_DETECTION---\n"));
1267 err_out(lf, "ctx: %12p, %8lu/%8lu, %s\n", child_ctx, child_ctx->dev, child_ctx->ino, child_node->path);
1268 err_out(lf, _("effective: %8lu/%8lu, %s\n"), effective_dev, effective_ino, target_path);
1269 }
1270 // is_link_cycle(effective_dev, effective_ino,
1271 // current_node->ctx);
1272 CycleNode *parent = current_node->ctx;
1273 while (parent != nullptr) {
1274 if (lf->report_trace) {
1275 err_out(lf, "ctx: %12p, %8lu/%8lu, %12p, %s\n", parent, parent->dev, parent->ino);
1276 }
1277 if (effective_dev == parent->dev && effective_ino == parent->ino) {
1278 cycle_found = true;
1279 break;
1280 }
1281 parent = parent->parent;
1282 }
1283 if (cycle_found) {
1284 if (lf->report_badlinks) {
1285 err_out(lf, _("CYCLER,%lu/%lu,%s,%lu/%lu,%s\n"), child_ctx->dev, child_ctx->ino, child_node->path,
1286 effective_dev, effective_ino, target_path);
1287 }
1288 atomic_fetch_add(&lf->error_count, 1);
1289 child_node->depth = current_node->depth + 1;
1290 scan_file(child_node->path, &child_node->path_len, lf, effective_type, &sb, &output);
1291 continue;
1292 }
1293 }
1294 // --------------------------------------------------------
1295 // BUILD AND ENQUEUE CHILD TASK
1296 // --------------------------------------------------------
1297 if (lf->max_depth != 0 && current_node->depth + 1 >= lf->max_depth)
1298 continue;
1299 child_node->depth = current_node->depth + 1;
1300 if (!cycle_found) {
1301 if (mpmc_enqueue(lf, child_node)) {
1302 atomic_fetch_add_explicit(&lf->q->active_tasks, 1, memory_order_relaxed);
1303 } else {
1304 QueuePayload *local_node = calloc(1, sizeof(QueuePayload));
1305 if (local_node == nullptr) {
1306 atomic_fetch_add(&lf->error_count, 1);
1307 if (lf->report_errors) {
1308 err_out(lf, _("MEM_ALLOC_FAIL,%s\n"), strerror(errno));
1309 }
1310 continue;
1311 }
1312 finder(lf, child_node, local_node);
1313 free(local_node);
1314 }
1315 }
1316 // --------------------------------------------------------
1317 // CONDITIONALLY PRINT THE DIRECTORY
1318 // --------------------------------------------------------
1319 if (entry->d_name[0] == '.') {
1320 if (!lf->include_hidden && !lf->hidden_only)
1321 continue;
1322 } else {
1323 if (lf->hidden_only)
1324 continue;
1325 }
1326 if (!lf->only_errors)
1327 scan_file(child_node->path, &child_node->path_len, lf, effective_type, &sb, &output);
1328 } else {
1329 // --------------------------------------------------------
1330 // NOT DIRECTORY - CONDITIONALLY PRINT THE ENTRY
1331 // --------------------------------------------------------
1332 if (entry->d_name[0] == '.') {
1333 if (!lf->include_hidden && !lf->hidden_only)
1334 continue;
1335 } else {
1336 if (lf->hidden_only)
1337 continue;
1338 }
1339 size_t path_len;
1340 if (full_path[0] == '\0') {
1341 if (!build_full_path(full_path,
1342 sizeof(full_path),
1343 current_node->path,
1344 entry->d_name,
1345 &path_len)) {
1346 atomic_fetch_add(&lf->error_count, 1);
1347 if (lf->report_errors) {
1348 err_out(lf, _("PATH_TOO_LONG,%s/%s\n"),
1349 current_node->path, entry->d_name);
1350 }
1351 continue;
1352 }
1353 }
1354 if (!lf->only_errors)
1355 scan_file(full_path, &path_len, lf, effective_type, &sb, &output);
1356 }
1357 }
1358 }
1359 close(dir_fd);
1360 flush_output_buffer(lf, &output);
1361 return NULL;
1362}
1363// ----------------------------------------------------------------------
1364// IS_LINK_CYCLE
1365// ----------------------------------------------------------------------
1366bool is_link_cycle(dev_t dev, ino_t ino, CycleNode *parent) {
1367 const CycleNode *current = parent;
1368 while (current != NULL) {
1369 if (current->ino == ino && current->dev == dev) {
1370 return true; // We hit a cycle!
1371 }
1372 current = current->parent; // Safe to dereference: addresses never change
1373 }
1374 return false;
1375}
1376// ----------------------------------------------------------------------
1377// SCAN_FILE
1378// ----------------------------------------------------------------------
1379/** @brief Scan a file or directory and apply filters based on the LfContext.
1380 @param file_spec The full path of the file or directory to scan.
1381 @param lf A pointer to the LfContext struct containing the search filters and options.
1382 @param effective_type The effective type of the file or directory (e.g., DT_REG, DT_DIR).
1383 @param cached_sb A pointer to a stat struct containing cached metadata for the file, or nullptr if not available.
1384 @param output A pointer to the OutputBuffer struct where matching paths will be appended.
1385 @return true if the file or directory passes all filters and is processed, false otherwise.
1386 @details This function checks various conditions based on the search filters specified in the LfContext. It evaluates whether the file or directory should be included in the output based on type, regex matching, ownership, permissions, modification time, and size. If all conditions are met, it appends the path to the output buffer. The function also handles caching of stat information to avoid redundant system calls when possible.
1387*/
1388int scan_file(const char *file_spec, const size_t *path_len, LfContext *lf,
1389 const unsigned char effective_type, const struct stat *cached_sb,
1390 OutputBuffer *output) {
1391 bool stat_cached = cached_sb != nullptr;
1392 struct stat sb = {};
1393
1394 while (1) {
1395 if (lf->suppress_types & lf_mask[effective_type])
1396 break;
1397 // Exclude non-matching files
1398 if (lf->flags & LF_REGEX) {
1399 int reti =
1400 regexec(&lf->compiled_re, file_spec, 0, NULL, 0);
1401 if (reti == REG_NOMATCH)
1402 break;
1404 }
1405 // Exclude matching files
1406 if (lf->flags & LF_EXC_REGEX) {
1407 int reti =
1408 regexec(&lf->compiled_ere, file_spec, 0, NULL, 0);
1409 if (reti == 0) {
1411 break;
1412 }
1413 }
1414 // Exclude files not owned by specified user
1415 if (lf->flags & LF_USER) {
1416 if (!stat_cached && stat(file_spec, &sb) == 0)
1417 stat_cached = true;
1418 if (!stat_cached || sb.st_uid != lf->user_id)
1419 break;
1420 }
1421 if (lf->include_perms) {
1422 if (!stat_cached && stat(file_spec, &sb) == 0)
1423 stat_cached = true;
1424 if (!stat_cached)
1425 break;
1426 if ((lf->include_perms & LF_IRUSR) && !(sb.st_mode & S_IRUSR))
1427 break;
1428 else if ((lf->include_perms & LF_IWUSR) && !(sb.st_mode & S_IWUSR))
1429 break;
1430 else if ((lf->include_perms & LF_IXUSR) && !(sb.st_mode & S_IXUSR))
1431 break;
1432 else if ((lf->include_perms & LF_ISUID) && !(sb.st_mode & S_ISUID))
1433 break;
1434 else if ((lf->include_perms & LF_ISGID) && !(sb.st_mode & S_ISGID))
1435 break;
1436 }
1437 if (lf->before) { // Last file modification
1438 if (!stat_cached && stat(file_spec, &sb) == 0)
1439 stat_cached = true;
1440 if (stat_cached && sb.st_mtime > lf->before)
1441 break;
1442 }
1443 if (lf->after) { // Last file modification
1444 if (!stat_cached && stat(file_spec, &sb) == 0)
1445 stat_cached = true;
1446 if (stat_cached && sb.st_mtime < lf->after)
1447 break;
1448 }
1449 if (lf->file_size_min) {
1450 if (!stat_cached && stat(file_spec, &sb) == 0)
1451 stat_cached = true;
1452 if (stat_cached && sb.st_size < lf->file_size_min)
1453 break;
1454 }
1455 if (lf->only_errors)
1456 break;
1457 atomic_fetch_add(&lf->file_count, 1);
1458 if (!lf->count_silently) {
1459 const char *display_path = file_spec;
1460 size_t display_len = *path_len;
1461 if (display_len > 2 && display_path[0] == '.' && display_path[1] == '/') {
1462 display_path += 2;
1463 display_len -= 2;
1464 }
1465 append_output_buffer(lf, output, display_path, display_len,
1466 effective_type == DT_DIR);
1467 }
1468 break;
1469 }
1470 return true;
1471}
1472/** @brief Thread-safe error output function that writes formatted error messages to the specified error file or stderr.
1473 @param lf A pointer to the LfContext struct containing the output mutex and error file information.
1474 @param format A printf-style format string for the error message.
1475 @param ... Additional arguments corresponding to the format string.
1476 @return The number of characters written, or a negative value if an error occurred.
1477 @details This function locks the output mutex to ensure that only one thread can write to the error output at a time. It checks if an error file has been specified; if not, it defaults to stderr. If an error file is specified but not yet opened, it attempts to open it in append mode. The function uses vfprintf to write the formatted error message and then unlocks the mutex before returning.
1478 */
1479int err_out(LfContext *lf, const char *format, ...) {
1480
1481 pthread_mutex_lock(&lf->output_mutex);
1482 va_list args;
1483 va_start(args, format);
1484 if (lf->error_file_spec[0] == '\0') {
1485 if (lf->err_fd == nullptr) {
1486 lf->err_fd = stderr;
1487 lf->error_file_open = true;
1488 setvbuf(lf->err_fd, NULL, _IOLBF, 0); // line buffering
1489 }
1490 } else {
1491 if (lf->error_file_open == false) {
1492 lf->err_fd = fopen(lf->error_file_spec, "a");
1493 if (lf->err_fd == nullptr) {
1494 lf->err_fd = stderr;
1495 fprintf(stderr, _("Failed to open error file %s: %s\n"),
1496 lf->error_file_spec, strerror(errno));
1497 return TS_ERROR;
1498 }
1499 lf->error_file_open = true;
1500 setvbuf(lf->err_fd, NULL, _IOLBF, 0); // line buffering
1501 }
1502 }
1503 int result = vfprintf(lf->err_fd, format, args);
1504 pthread_mutex_unlock(&lf->output_mutex);
1505 va_end(args);
1506 return result;
1507}
#define MAXARGS
Definition cm.h:64
@ LF_EXC_REGEX
Definition cm.h:184
@ LF_HIDE
Definition cm.h:182
@ LF_ISUID
Definition cm.h:194
@ LF_IRUSR
Definition cm.h:192
@ LF_ISGID
Definition cm.h:193
@ LF_REGEX
Definition cm.h:185
@ LF_USER
Definition cm.h:187
@ LF_IXUSR
Definition cm.h:190
@ LF_IWUSR
Definition cm.h:191
@ LF_FIFO
Definition cm.h:199
@ LF_DIR
Definition cm.h:201
@ LF_UNKNOWN
Definition cm.h:206
@ LF_SOCK
Definition cm.h:205
@ LF_BLK
Definition cm.h:202
@ LF_LNK
Definition cm.h:204
@ LF_REG
Definition cm.h:203
@ LF_CHR
Definition cm.h:200
#define CM_VERSION
Definition version.h:5
#define MAXLEN
Definition curskeys.c:15
char * exec
Definition lf.c:167
@ TS_SUCCESS
Definition lf.c:78
@ TS_ERROR
Definition lf.c:79
@ TS_MATCH_PLUS_ERROR
Definition lf.c:81
@ TS_MATCH
Definition lf.c:80
void cycle_arena_destroy(void)
Destroy the cycle arena and release allocated resources.
Definition lf.c:851
#define QUEUE_CAPACITY
Definition lf.c:35
void * finder(LfContext *lf, QueuePayload *current_node, QueuePayload *child_node)
Process a directory and its entries, applying filters and enqueuing subdirectories.
Definition lf.c:1105
void * worker(void *arg)
Worker thread function that processes tasks from the MPMCQueue.
Definition lf.c:1072
char * lfargs[3]
Definition lf.c:166
bool build_full_path(char *, size_t, const char *, const char *, size_t *)
Build a full file path by concatenating a directory path and a file name.
Definition lf.c:893
int err_out(LfContext *lf, const char *format,...)
Thread-safe error output function that writes formatted error messages to the specified error file or...
Definition lf.c:1479
#define DIR_BUF_SIZE
Definition lf.c:39
TerminationStatus termination_status
Definition lf.c:107
char * perms_p
Definition lf.c:169
char * debug_p
Definition lf.c:170
int sort_lf_output(LfContext *lf, int, char **)
Sort the output of the lf command using the system's sort utility.
Definition lf.c:532
#define QUEUE_MASK
Definition lf.c:36
int scan_file(const char *file_spec, const size_t *path_len, LfContext *lf, const unsigned char, const struct stat *, OutputBuffer *)
Scan a file or directory and apply filters based on the LfContext.
Definition lf.c:1388
char * file_types_p
Definition lf.c:168
void flush_output_buffer(LfContext *, OutputBuffer *)
Flush the output buffer to stdout in a thread-safe manner.
Definition lf.c:921
void cycle_arena_init(void)
Initialize the cycle arena for tracking visited directories.
Definition lf.c:828
bool mpmc_enqueue(LfContext *, const QueuePayload *child_node)
Enqueue a task into the MPMCQueue.
Definition lf.c:992
MPMCQueue * mpmc_queue_init()
Initialize the MPMCQueue.
Definition lf.c:971
bool append_output_buffer(LfContext *, OutputBuffer *, const char *, size_t, bool)
Append a path to the output buffer, flushing if necessary.
Definition lf.c:941
#define ARENA_MAX_NODES
Definition lf.c:43
bool init_lf(LfContext *lf, int, char **)
Initialize the file search process.
Definition lf.c:722
#define print_file_type(mask, lf_type, dt_type, name)
Definition lf.c:92
int lfargc
Definition lf.c:165
bool mpmc_dequeue(LfContext *lf, QueuePayload *output_node)
Dequeue a task from the MPMCQueue.
Definition lf.c:1031
unsigned char const lf_mask[15]
Definition lf.c:161
void debug_out(LfContext *lf, int, char **)
Output debug information to stderr.
Definition lf.c:583
#define MAX_PATH_LEN
Definition lf.c:37
struct tm tm_info
Definition lf.c:99
const char doc[]
Definition lf.c:102
bool is_dirsys(const char *)
#define CACHE_LINE_SIZE
Definition lf.c:40
bool is_link_cycle(dev_t dev, ino_t ino, CycleNode *parent)
Definition lf.c:1366
bool is_hidden(const char *)
const char * argp_program_version
Definition init.c:110
const char * argp_program_bug_address
Definition init.c:111
#define _(String)
Definition amort.c:23
int main(int argc, char **argv)
Capture the current terminal settings for later restoration.
Definition amort.c:41
size_t strnz__cpy(char *, const char *, size_t)
safer alternative to strncpy
Definition whence.c:291
bool parse_local_timestamp(const char *, time_t *)
Parses an ISO 8601 timestamp string in local time and converts it to time_t.
Definition futil.c:250
char * get_local_timestamp()
Returns the current local time as an ISO 8601 formatted string.
Definition futil.c:284
bool is_directory(const char *)
Checks if the given path is a directory.
Definition futil.c:1381
size_t ssnprintf(char *, size_t, const char *,...)
ssnprintf was designed to be a safer alternative to snprintf.
Definition futil.c:390
bool is_valid_regex(const char *)
Checks if the given regular expression pattern is valid.
Definition futil.c:1409
bool expand_tilde(char *, uint)
Replaces "~/" in string with the user's home directory.
Definition futil.c:989
char * get_ip_addresses(char *, uint)
Retrieves the IP addresses of the local machine and formats them into a string.
Definition futil.c:324
bool is_symlink_to_dir(const char *)
Checks if the given path is a symbolic link to a directory.
Definition futil.c:1394
int a_toi(char *, bool *)
a safer alternative to atoi() for converting ASCII strings to integers.
Definition futil.c:784
char * format_local_timestamp(time_t, char *, size_t)
Formats a time_t as an ISO 8601 string in local time.
Definition futil.c:273
char * get_user_str(char *, size_t)
Retrieves the current user's name and UID, and formats it into a string.
Definition futil.c:297
unsigned long a_to_ul(const char *)
Converts a string to an unsigned long long integer, with support for suffixes K, M,...
Definition futil.c:812