/* * qfind - a quick! "find . -type f" like program optimized for Windows. * * Description: * 1. Only prints file names - it does *not* print directory names. * 2. Prints all a directory's files, before processing its subdirectories. * 3. Does not prefix filenames with redundant "./". * 4. Files printed in Unix "/" separated format. * 5. Does not follow links to directories. * * Background: * It is not currently possible to write efficient directory traversal code in * Perl on Windows. This is because you need to stat() each file returned by * readdir() to test if it is a directory when this information has already * been obtained but not used. The extra unnecessary stat() calls severely * degrade performance. This program was written to be run from a Perl script * to provide it with a faster method of finding all the files beneath a * directory than (for example) File::Find::find(). See "peg.pl" for details. * * To compile: * % gcc -O2 qfind.c * * Copyright (c) 2007-2011 Alex Davies. All rights reserved. This program * is free software; you can redistribute it and/or modify it under the * same terms as Perl itself. */ /******************************************************************************/ #define QFIND_VERSION "1.14" /* Are we on a MS Windows platform? */ #if defined(_WIN32) && !defined(WIN32) #define WIN32 #endif #if defined(_WIN64) && !defined(WIN64) #define WIN64 #endif #if (defined(WIN32) || defined(WIN64)) && !defined(WINDOWS) #define WINDOWS #endif #if defined(WINDOWS) && defined(UNICODE) #error A UNICODE build is not supported #endif /* There are two versions of qfind: one uses Find(First|Next)File, the other * uses opendir/readdir/stat. Both can be built on Windows (the former is more * efficient and hence faster), while only the latter will work on *nix. */ #if defined(WINDOWS) && !defined(QFINDX) #define USE_FINDFILE 1 #else #define USE_FINDFILE 0 #endif /******************************************************************************/ #include #include #include #include #include #if defined(WINDOWS) #include #include #endif #if USE_FINDFILE #include #else #include #include #include #include #endif /******************************************************************************/ #if USE_FINDFILE #define QCODE(a, b) a #else #define QCODE(a, b) b #endif #define QFIND_BUILD QCODE("Win", "POSIX") #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L #define STDC99 #endif #if !(defined(STDC99) || defined(__GNUC__) || defined(__cplusplus)) #define inline #endif #if defined(__GNUC__) #define NOINLINE __attribute__ ((noinline)) #else #define NOINLINE #endif #if defined(__cplusplus) #define CPP_CAST(type) (type) #else #define CPP_CAST(type) #endif #ifdef _DIRENT_HAVE_D_TYPE #define HAS_D_TYPE #endif #if defined(HAS_D_TYPE) && defined(WINDOWS) && !USE_FINDFILE /* Allow test compiling the HAS_D_TYPE build on Windows. */ #define d_type d_ino #define DT_UNKNOWN 0 /* NB. d_ino is always 0 on Windows. */ #define DT_FIFO 1 #define DT_CHR 2 #define DT_DIR 4 #define DT_BLK 6 #define DT_REG 8 #define DT_LNK 10 #define DT_SOCK 12 #define DT_WHT 14 #endif /******************************************************************************/ /* Encapsulate a simple stack of strings. */ struct strstack { struct { /* A single buffer containing all the strings. */ char *buf; size_t size; } str; struct { /* Store each string's offset into the buffer. */ size_t *list; size_t max; size_t next; } offset; size_t total; /* # of strings stored. */ }; /* Initial stack sizes. */ #define DIR_STACK_ELEMENTS (1 * 1024) #define DIR_STACK_BUFSIZE (8 * DIR_STACK_ELEMENTS) #define FILE_STACK_ELEMENTS (4 * 1024) #define FILE_STACK_BUFSIZE (8 * FILE_STACK_ELEMENTS) /* Use a stack of directories to allow us to print all the files * in a directory before processing its subdirectories. */ static struct strstack dir_stack; /* A stack of filenames which is sorted prior to printing. */ static struct strstack file_stack; /* A function for comparing strings. */ typedef int (*strcmp_fn_t)(const char *s1, const char *s2); /* The comparison function used to sort filenames. * We don't sort the filenames before printing by default when using * FindNextFile() because it already returns sorted results on NTFS. */ static strcmp_fn_t filename_cmp = QCODE(NULL, strcmp); #if USE_FINDFILE /* => WINDOWS */ static char slash = '/'; static char slosh = '\\'; /* The other slash. */ #else static const char slash = '/'; #endif #if USE_FINDFILE /* Print classic 8.3 filenames eg. "filename.txt". */ static int truncated_filenames = 0; /* Skip SYSTEM|HIDDEN files & directories. */ static int skip_hidden_files = 0; #else /* By default the opendir/readdir build only prints regular files. * ie. skip sockets, character/block special files & named pipes. */ static int any_file = 0; /* Skip non-readable files. */ static int skip_non_readable_files = 0; #endif /* Skip dot files & directories. */ static int skip_dot_files = 0; /* Silence messages to STDERR. */ static int silent = 0; /* Print files matching given size restraints. */ static int min_size_test = 0; static int max_size_test = 0; #if USE_FINDFILE struct high_low { DWORD high, low; }; static struct high_low min_size_hl, max_size_hl; #else typedef unsigned long filesize_t; static filesize_t min_size, max_size; #endif /* Modification time tests. */ static int skip_new_files = 0; static int skip_old_files = 0; static time_t opt_N_time; static time_t opt_O_time; static int mtime_test = 0; static int consider_ctime = 0; /* Match/ignore files belonging to a given user/group id. */ #if !USE_FINDFILE static int uid_test = 0; /* 0 = no test; 1 = match; 2 = ignore */ static int gid_test = 0; typedef unsigned int id_t; static id_t opt_uid; static id_t opt_gid; #endif #ifdef HAS_D_TYPE static int need_to_call_stat = 0; #endif /* We need to determine if a given string is among a list of other strings. * To speed lookup, after initialization the strings are sorted and then we * build a table keyed by the first character of member strings which gives the * first index in the strstack of strings with that first character. */ struct lookup_list { struct strstack ss; size_t firstchar[UCHAR_MAX + 1]; /* A primitive hash table. */ }; /* Value of firstchar[UCHAR(c)] indicating no string in the lookup_list with * a first character of 'c'. */ #define NONE ((size_t) -1) /* A list of directories to ignore eg. ".git". */ static struct lookup_list xd; /* A list of file extensions to ignore eg. "obj". */ static struct lookup_list xe; /* A list of file extensions to match. This overrides xe. */ static struct lookup_list xp; /* A list of directory paths to ignore. */ static struct lookup_list xD; /* Global variables used by cmp_ss(), the qsort comparison function. */ static struct strstack *G_ssp; static strcmp_fn_t G_strcmp; /* Do we need to call is_exclude_extension() on each filename. */ static int extension_test = 0; /* The name of the directory currently being processed. */ static char *dirname; static size_t dirname_bufsize; static size_t dirname_len; #define DIRNAME_BUFSIZE 320 #if !USE_FINDFILE /* The name of the starting directory. This is only needed if multiple DIR * command line arguments are specified. */ static char *start_dir = NULL; #define START_DIR_BUFSIZE 320 #endif /******************************************************************************/ typedef QCODE(DWORD, int) err_t; /* GetLastError() or errno. */ static void setup(void); static int process_argv(int argc, char *argv[]); static int process_option(char *arg); static void prepare(void); static void print_help(void); static void scan_dir(void); static void print_files(void) NOINLINE; static void print_file_stack(void); static void flush_output(void); static void found_file(const char *f); static void write_dirname(void); static void dirwarn(const char *msg, err_t err); static char * remove_trailing_slash(void); static char * get_error_msg(err_t err); static void clean_up(void); static size_t get_alloc_size(size_t needed); static void * Malloc(size_t size); static void * Realloc(void *ptr, size_t size); static void init_ss(struct strstack *ssp); static void alloc_ss(struct strstack *ssp, size_t elements, size_t bufsize); static void init_ll(struct lookup_list *xptr); static void init_ll_firstchar(struct lookup_list *xptr); static void push_ss(struct strstack *ssp, const char *str); static void grow_ss(struct strstack *ssp, size_t elements, size_t size); static inline char * get_str(const struct strstack *ssp, size_t i); static void clear_ss(struct strstack *ssp); static void sort_ss(struct strstack *ssp, size_t from, size_t n, strcmp_fn_t cmp); static void sort_filenames(struct strstack *ssp, size_t from, size_t n); static void remove_reps(struct lookup_list *xptr); static int stri2eq(const char *s1, const char *s2); static int cmp_ss(const void *elem1, const void *elem2); static int mystricmp(const char *s1, const char *s2); static int strnumcmp(const char *s1, const char *s2); static void parse_name_list(const char *arg, struct lookup_list *xptr); static void parse_ext_arg(char *arg, struct lookup_list *xptr); static void parse_time_arg(const char *arg, char opt); static void parse_size_arg(const char *arg, char opt); static void fix_xD(void); #if !USE_FINDFILE static void set_start_dir(void); static void chdir_to_start_dir(void); static void parse_id_arg(const char *arg, char opt); static void stat_failed(const char *f); static void lstat_failed(const char *f); static time_t get_mtime_from_stat_data(const struct stat *statp); static void chdir_up(void); static void dirdie(const char *msg, err_t err); #endif static void skip_zero_size_files(void); static int is_exclude_dir(const char *f); static int is_exclude_extension(const char *f); static int is_dirname_excluded(void); static void alloc_dirname(size_t bufsize); static void append_dirname(const char *d, size_t old_dirname_len); static void set_dirname(const char *dir); static void restore_dirname(size_t len); #if USE_FINDFILE static char * append_globchar_to_dirname(void); static time_t filetime_to_timet(const FILETIME *filetimep); static BOOL filetime_from_time(PFILETIME pFileTime, time_t Time); static time_t get_mtime_from_find_data(const WIN32_FIND_DATA *fDatap); static time_t to_utc_time(time_t t); static inline int high_low_cmp(DWORD ah, DWORD al, DWORD bh, DWORD bl); #define high_low_gt(ah, al, bh, bl) (high_low_cmp(ah, al, bh, bl) > 0) #define high_low_lt(ah, al, bh, bl) (high_low_cmp(ah, al, bh, bl) < 0) #endif static inline int to_lower(int c); static inline int is_digit(int c); #define strEQ(s1, s2) (strcmp(s1, s2) == 0) #define strNE(s1, s2) (strcmp(s1, s2) != 0) /******************************************************************************/ #define Swap(T, a, b) \ do { \ T temp = a; \ a = b; \ b = temp; \ } while (0) /******************************************************************************/ #if defined(WINDOWS) #define lstat stat /* There is no lstat() on Windows. */ #endif /* Free() - document the free()ing of alloc'd memory which we are happy to * leave to the OS to perform the clean up on program exit. */ #define Free(ptr) ((void) (ptr)) /* free((void *) (ptr)) */ /******************************************************************************/ #if UCHAR_MAX != 255 #undef QFIND_ASCII #endif #ifdef QFIND_ASCII /* Provide fast array lookup equivalents for 's tolower() and isdigit() * assuming an ASCII character set. */ /* Map ASCII characters to their lower case versions. */ static const unsigned char lc[256] = { 0, 1, 2, 3, 4, 5, 6, 7, /* 0-7 */ 8, 9, 10, 11, 12, 13, 14, 15, /* 8-15 */ 16, 17, 18, 19, 20, 21, 22, 23, /* 16-23 */ 24, 25, 26, 27, 28, 29, 30, 31, /* 24-31 */ 32, 33, 34, 35, 36, 37, 38, 39, /* 32-39 */ 40, 41, 42, 43, 44, 45, 46, 47, /* 40-47 */ 48, 49, 50, 51, 52, 53, 54, 55, /* 48-55 */ 56, 57, 58, 59, 60, 61, 62, 63, /* 56-63 */ 64, 97, 98, 99, 100, 101, 102, 103, /* 64-71 */ 104, 105, 106, 107, 108, 109, 110, 111, /* 72-79 */ 112, 113, 114, 115, 116, 117, 118, 119, /* 80-87 */ 120, 121, 122, 91, 92, 93, 94, 95, /* 88-95 */ 96, 97, 98, 99, 100, 101, 102, 103, /* 96-103 */ 104, 105, 106, 107, 108, 109, 110, 111, /* 104-111 */ 112, 113, 114, 115, 116, 117, 118, 119, /* 112-119 */ 120, 121, 122, 123, 124, 125, 126, 127, /* 120-127 */ 128, 129, 130, 131, 132, 133, 134, 135, /* 128-135 */ 136, 137, 138, 139, 140, 141, 142, 143, /* 136-143 */ 144, 145, 146, 147, 148, 149, 150, 151, /* 144-151 */ 152, 153, 154, 155, 156, 157, 158, 159, /* 152-159 */ 160, 161, 162, 163, 164, 165, 166, 167, /* 160-167 */ 168, 169, 170, 171, 172, 173, 174, 175, /* 168-175 */ 176, 177, 178, 179, 180, 181, 182, 183, /* 176-183 */ 184, 185, 186, 187, 188, 189, 190, 191, /* 184-191 */ 192, 193, 194, 195, 196, 197, 198, 199, /* 192-199 */ 200, 201, 202, 203, 204, 205, 206, 207, /* 200-207 */ 208, 209, 210, 211, 212, 213, 214, 215, /* 208-215 */ 216, 217, 218, 219, 220, 221, 222, 223, /* 216-223 */ 224, 225, 226, 227, 228, 229, 230, 231, /* 224-231 */ 232, 233, 234, 235, 236, 237, 238, 239, /* 232-239 */ 240, 241, 242, 243, 244, 245, 246, 247, /* 240-247 */ 248, 249, 250, 251, 252, 253, 254, 255, /* 248-255 */ }; /*! Optimized for is_digit(). */ #define U 0 /*! 0x01 */ /* upper */ #define L 0 /*! 0x02 */ /* lower */ #define D 1 /*! 0x04 */ /* digit */ #define S 0 /*! 0x08 */ /* space */ #define P 0 /*! 0x10 */ /* punct */ #define C 0 /*! 0x20 */ /* cntrl */ #define B 0 /*! 0x40 */ /* blank */ #define H 0 /*! 0x80 */ /* hex */ /* ASCII character info. */ static const unsigned char ctype[256] = { C, C, C, C, C, C, C, C, /* 0-7 */ C, S|C|B, S|C, S|C, S|C, S|C, C, C, /* 8-15 */ C, C, C, C, C, C, C, C, /* 16-23 */ C, C, C, C, C, C, C, C, /* 24-31 */ S|B, P, P, P, P, P, P, P, /* 32-39 */ P, P, P, P, P, P, P, P, /* 40-47 */ D|H, D|H, D|H, D|H, D|H, D|H, D|H, D|H, /* 48-55 */ D|H, D|H, P, P, P, P, P, P, /* 56-63 */ P, U|H, U|H, U|H, U|H, U|H, U|H, U, /* 64-71 */ U, U, U, U, U, U, U, U, /* 72-79 */ U, U, U, U, U, U, U, U, /* 80-87 */ U, U, U, P, P, P, P, P, /* 88-95 */ P, L|H, L|H, L|H, L|H, L|H, L|H, L, /* 96-103 */ L, L, L, L, L, L, L, L, /* 104-111 */ L, L, L, L, L, L, L, L, /* 112-119 */ L, L, L, P, P, P, P, C, /* 120-127 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 128-135 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 136-143 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 144-151 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 152-159 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 160-167 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 168-175 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 176-183 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 184-191 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 192-199 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 200-207 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 208-215 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 216-223 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 224-231 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 232-239 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 240-247 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 248-255 */ }; #else #include #include #endif static inline int to_lower(int c) { #ifdef QFIND_ASCII return lc[c]; #else return tolower(c); #endif } static inline int is_digit(int c) { #ifdef QFIND_ASCII return ctype[c] /*! & D */; #else return isdigit(c); #endif } #undef U #undef L #undef D #undef S #undef P #undef C #undef B #undef H #define UCHAR(c) ((unsigned char) (c)) #define lowercase(c) (to_lower(UCHAR(c))) #define izdigit(c) (is_digit(UCHAR(c))) /******************************************************************************/ int main(int argc, char *argv[]) { int num_dir_args; setup(); num_dir_args = process_argv(argc, argv); prepare(); if (num_dir_args > 0) { int i, first_dir_arg_idx = argc - num_dir_args; #if !USE_FINDFILE if (num_dir_args > 1) set_start_dir(); #endif for (i = first_dir_arg_idx; i < argc; ++i) { const char *dir = argv[i]; set_dirname(dir); #if !USE_FINDFILE /* Ensure relative paths work by returning to the start directory. * NB. This is unnecessary on the first iteration. */ if (i > first_dir_arg_idx) chdir_to_start_dir(); if (dir[0] && chdir(dir)) { dirwarn("can't chdir into ", errno); continue; } #endif scan_dir(); } } else { scan_dir(); } clean_up(); exit(EXIT_SUCCESS); return 0; } /******************************************************************************/ /* Initialize the stacks etc. */ static void setup(void) { #ifndef QFIND_ASCII /* Use the current locale's tolower(). */ setlocale(LC_CTYPE, ""); #endif #if defined(WINDOWS) /* Write LF newlines, not CRLFs. */ if (setmode(fileno(stdout), O_BINARY) == -1) { fprintf(stderr, "qfind: failed to binmode output: %d\n", errno); exit(EXIT_FAILURE); } #endif init_ll(&xd); init_ll(&xD); init_ll(&xe); init_ll(&xp); init_ss(&dir_stack); init_ss(&file_stack); } /******************************************************************************/ /* Process the command line arguments. */ static int process_argv(int argc, char *argv[]) { ++argv; --argc; /* Skip program name (assume it is qfind). */ while (argc > 0) { if (argv[0][0] == '-') { char *arg = argv[0]; ++argv; --argc; if (process_option(arg)) continue; } break; } return argc; /* # of remaining non option (ie. DIR) arguments. */ } /******************************************************************************/ /* Process an option argument. */ static int process_option(char *arg) { char opt; int eoarg = 0; int in_options = 1; if (*arg++ != '-') return 0; while ((opt = *arg++) != '\0') { switch (opt) { #if USE_FINDFILE case 'b': slash = '\\'; slosh = '/'; break; case 't': truncated_filenames = 1; break; case 'y': skip_hidden_files = 1; break; #else case 'a': any_file = 1; break; case 'g': case 'G': case 'u': case 'U': parse_id_arg(arg, opt); eoarg = 1; break; case 'r': skip_non_readable_files = 1; break; #endif case 'c': consider_ctime = 1; break; case 'd': case 'D': parse_name_list(arg, opt == 'd' ? &xd : &xD); eoarg = 1; break; case 'e': case 'p': parse_ext_arg(arg, opt == 'e' ? &xe : &xp); eoarg = 1; break; case 'h': print_help(); exit(EXIT_SUCCESS); break; case 'i': filename_cmp = mystricmp; break; case 'n': filename_cmp = strnumcmp; break; case 'o': skip_dot_files = 1; break; case 's': silent = 1; break; case 'x': filename_cmp = NULL; break; case 'z': skip_zero_size_files(); break; case 'J': case 'K': parse_size_arg(arg, opt); eoarg = 1; break; case 'N': case 'O': parse_time_arg(arg, opt); eoarg = 1; break; case 'S': filename_cmp = strcmp; break; case 'V': printf("qfind v%s (%s)\n", QFIND_VERSION, QFIND_BUILD); exit(EXIT_SUCCESS); break; case '-': in_options = 0; break; default: fprintf(stderr, "qfind: unknown option '%c'\n", opt); exit(EXIT_FAILURE); break; } /* end of switch */ if (eoarg) break; } return in_options; } /******************************************************************************/ /* Do any post command line argument processing setup here. */ static void prepare(void) { /* If both -N & -O, or -G & -H, are specified, assume the ranges are * intended to be between the two given values. */ if (skip_old_files && skip_new_files) if (opt_N_time > opt_O_time) Swap(time_t, opt_N_time, opt_O_time); if (min_size_test && max_size_test) { #if USE_FINDFILE if (high_low_gt(min_size_hl.high, min_size_hl.low, max_size_hl.high, max_size_hl.low)) Swap(struct high_low, max_size_hl, min_size_hl); #else if (min_size > max_size) Swap(filesize_t, max_size, min_size); #endif } if (skip_old_files || skip_new_files) mtime_test = 1; #ifdef HAS_D_TYPE if (mtime_test || min_size_test || max_size_test || skip_non_readable_files || uid_test || gid_test) need_to_call_stat = 1; #endif alloc_dirname(DIRNAME_BUFSIZE); alloc_ss(&dir_stack, DIR_STACK_ELEMENTS, DIR_STACK_BUFSIZE); if (filename_cmp) alloc_ss(&file_stack, FILE_STACK_ELEMENTS, FILE_STACK_BUFSIZE); if (xp.ss.total || xe.ss.total) { init_ll_firstchar(xp.ss.total ? &xp : &xe); /* xp overrides xe */ extension_test = 1; } init_ll_firstchar(&xd); fix_xD(); init_ll_firstchar(&xD); } /******************************************************************************/ /* Print the (regular) files in a directory, then recurse on its subdirectories. * NB. dirname is either "" or terminated with a slash. */ static void scan_dir(void) { size_t i, end, num_dirs, this_dirname_len; /* Save the state of the dir_stack. */ const size_t start = dir_stack.total; const size_t next = dir_stack.offset.next; print_files(); end = dir_stack.total; num_dirs = end - start; if (num_dirs == 0) return; sort_filenames(&dir_stack, start, num_dirs); this_dirname_len = dirname_len; /* And now process subdirectories. */ for (i = start; i < end; ++i) { const char *d = get_str(&dir_stack, i); append_dirname(d, this_dirname_len); if (is_dirname_excluded()) continue; #if !USE_FINDFILE if (chdir(d)) { dirwarn("can't chdir into ", errno); continue; } #endif scan_dir(); /* recurse */ #if !USE_FINDFILE chdir_up(); #endif } restore_dirname(this_dirname_len); /* Restore dir_stack to its state at start of function. */ dir_stack.total = start; dir_stack.offset.next = next; } /******************************************************************************/ /* Print the files in a directory according to the command line options. * NB. dirname is either "" or terminated with a slash. */ static void print_files(void) { #if USE_FINDFILE char *globchar; const char *f, *s; WIN32_FIND_DATA fData; HANDLE find_han; /* Append a "*" to dirname so it can be used as a glob pattern. */ globchar = append_globchar_to_dirname(); find_han = FindFirstFile(dirname, &fData); /* Remove temporary "*" from dirname. */ *globchar = '\0'; if (find_han == INVALID_HANDLE_VALUE) { dirwarn("", GetLastError()); return; } clear_ss(&file_stack); while (1) { if (!*fData.cAlternateFileName) f = fData.cFileName; else if (truncated_filenames) f = fData.cAlternateFileName; else { /* Any "?"s in cFileName indicate characters not in current code * page; in which case use cAlternateFileName instead. */ f = fData.cFileName; for (s = f; *s; ++s) { if (*s == '?') { f = fData.cAlternateFileName; break; } } } if (skip_dot_files && f[0] == '.') goto next_file; if (skip_hidden_files && (fData.dwFileAttributes & (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM))) goto next_file; if (fData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (f[0] == '.' && (!f[1] || (f[1] == '.' && !f[2]))) ; /* Ignore "." and ".." directories. */ else if (!is_exclude_dir(f)) push_ss(&dir_stack, f); goto next_file; } if (min_size_test && high_low_lt(fData.nFileSizeHigh, fData.nFileSizeLow, min_size_hl.high, min_size_hl.low)) goto next_file; if (max_size_test && high_low_gt(fData.nFileSizeHigh, fData.nFileSizeLow, max_size_hl.high, max_size_hl.low)) goto next_file; if (mtime_test) { time_t mtime = get_mtime_from_find_data(&fData); if ( (skip_old_files && mtime < opt_N_time) || (skip_new_files && mtime > opt_O_time)) goto next_file; } if (extension_test && is_exclude_extension(f)) goto next_file; found_file(f); next_file: if (!FindNextFile(find_han, &fData)) { const DWORD err = GetLastError(); if (err != ERROR_NO_MORE_FILES) dirwarn("FindNextFile error on ", err); break; } } if (FindClose(find_han) == 0) dirwarn("FindClose error on ", GetLastError()); #else DIR *dirp; struct dirent *dp; struct stat statbuf; const char *f; if ((dirp = opendir(".")) == NULL) { dirwarn("can't opendir ", errno); return; } clear_ss(&file_stack); while (1) { errno = 0; if ((dp = readdir(dirp)) == NULL) { if (errno) dirwarn("can't readdir ", errno); break; } f = dp->d_name; /* Ignore "." and ".." directories. */ if (f[0] == '.') if (!f[1] || (f[1] == '.' && !f[2]) || skip_dot_files) continue; #ifdef HAS_D_TYPE /* Use d_type to avoid calling stat() and lstat() when unnecessary. */ switch (dp->d_type) { case DT_DIR: goto f_is_dir; case DT_FIFO: case DT_CHR: case DT_BLK: case DT_SOCK: if (!any_file) /* We don't show non regular files. */ continue; /* No break; drop through to regular file logic. */ case DT_REG: if (!need_to_call_stat) goto non_statbuf_tests; break; case DT_LNK: case DT_UNKNOWN: default: /* Must do a stat() to see what type of file it is. */ break; case DT_WHT: /* "A whiteout is a directory entry that hides any identically named * objects in the lower layers. The semantics of a whiteout for a * given name is that a lookup on that name should return ENOENT." * So, qfind should ignore them. */ continue; } #endif /* NB. In the case of symbolic links, qfind's file tests relate to * the file *linked to*, so use stat() and not lstat(). */ if (stat(f, &statbuf)) { stat_failed(f); continue; } if (S_ISDIR(statbuf.st_mode)) { /* Must ensure we don't follow links to directories. */ #ifdef HAS_D_TYPE if (dp->d_type == DT_LNK) continue; f_is_dir: #endif if (is_exclude_dir(f)) continue; #ifdef HAS_D_TYPE if (dp->d_type != DT_DIR) { #endif /* Need to call lstat() to test if it's a link to a directory. */ if (lstat(f, &statbuf)) { lstat_failed(f); continue; } else if (!S_ISDIR(statbuf.st_mode)) continue; #ifdef HAS_D_TYPE } #endif push_ss(&dir_stack, f); continue; } if (!(S_ISREG(statbuf.st_mode) || any_file)) continue; if (min_size_test && (filesize_t) statbuf.st_size < min_size) continue; if (max_size_test && (filesize_t) statbuf.st_size > max_size) continue; if (mtime_test) { time_t mtime = get_mtime_from_stat_data(&statbuf); if ( (skip_old_files && mtime < opt_N_time) || (skip_new_files && mtime > opt_O_time)) continue; } if (skip_non_readable_files && !(statbuf.st_mode & S_IREAD)) continue; if (uid_test && ((uid_test == 1) ^ ((id_t) statbuf.st_uid == opt_uid))) continue; if (gid_test && ((gid_test == 1) ^ ((id_t) statbuf.st_gid == opt_gid))) continue; #ifdef HAS_D_TYPE non_statbuf_tests: #endif if (extension_test && is_exclude_extension(f)) continue; found_file(f); } if (closedir(dirp) == -1) dirwarn("can't closedir ", errno); #endif if (file_stack.total) print_file_stack(); } /******************************************************************************/ /* NB. We reuse the dirname buffer for writing "{dirname}{f}\n". */ static void print_file_stack(void) { size_t i, this_dirname_len = dirname_len; /* NB. filename_cmp will be non-NULL here. */ sort_ss(&file_stack, 0, file_stack.total, filename_cmp); for (i = 0; i < file_stack.total; ++i) { const char *f = get_str(&file_stack, i); append_dirname(f, this_dirname_len); write_dirname(); } restore_dirname(this_dirname_len); flush_output(); } /******************************************************************************/ /* We want output to be flushed to the caller asap; but calling fflush() * after every set of fwrite()s can be unnecessary and wasteful. * This is a compromise that ensures "peg -l +1 -dR_M 1 /" shows results * in the root directory immediately. */ static void flush_output(void) { static int n = 5; if (n > 0) { fflush(stdout); --n; } } /******************************************************************************/ /* In directories containing many thousands of files it will be faster to turn * off sorting and write the filenames as we get them. */ static void found_file(const char *f) { if (filename_cmp) { push_ss(&file_stack, f); } else { size_t this_dirname_len = dirname_len; append_dirname(f, this_dirname_len); write_dirname(); restore_dirname(this_dirname_len); } } /******************************************************************************/ static void write_dirname(void) { size_t written; /* Replace trailing slash with newline. */ dirname[dirname_len - 1] = '\n'; written = fwrite(dirname, sizeof(char), dirname_len, stdout); /* Check for an output error cf. "qfind | more". */ if (written != dirname_len) exit(EXIT_FAILURE); } /******************************************************************************/ #if USE_FINDFILE static char * append_globchar_to_dirname(void) { char *globchar; globchar = dirname + dirname_len; globchar[0] = '*'; globchar[1] = '\0'; return globchar; } #endif /******************************************************************************/ #if !USE_FINDFILE /* When the stat() fails, give a different error message for the case of a * symbolic link pointing to a nonexistent file. */ static void stat_failed(const char *f) { if (!silent) { int saved_errno = errno; int dangling_link = 0; if (errno == ENOENT) { struct stat statbuf; if (!lstat(f, &statbuf)) { dangling_link = 1; fprintf(stderr, "qfind: dangling link %s%s\n", dirname, f); } } if (!dangling_link) fprintf(stderr, "qfind: can't stat %s%s: %s\n", dirname, f, strerror(saved_errno)); } } /******************************************************************************/ static void lstat_failed(const char *f) { if (!silent) fprintf(stderr, "qfind: can't lstat %s%s: %s\n", dirname, f, strerror(errno)); } /******************************************************************************/ /* There are a number of reasons why a file's ctime is different to its mtime: * - the file has been the argument of one of various system calls that * affect the inode, but not the file contents eg. chmod(), link(), * rename(), unlink() etc. * - various file archive extraction programs such as unzip create files with * the mtime of the original archived file, but sets the ctime to the time * when it was created. */ static time_t get_mtime_from_stat_data(const struct stat *statp) { time_t mt = statp->st_mtime; if (consider_ctime) { time_t ct = statp->st_ctime; if (ct > mt) mt = ct; } return mt; } /******************************************************************************/ static void set_start_dir(void) { size_t bufsize = START_DIR_BUFSIZE; /* NB. start_dir is initialized to NULL. */ while (1) { start_dir = CPP_CAST(char *) Realloc(start_dir, bufsize); if (getcwd(start_dir, bufsize)) return; if (errno != ERANGE) { fprintf(stderr, "qfind: getcwd failed: %s\n", strerror(errno)); exit(EXIT_FAILURE); } bufsize *= 2; } } /******************************************************************************/ static void chdir_to_start_dir(void) { if (chdir(start_dir)) { fprintf(stderr, "qfind: can't chdir back to %s: %s\n", start_dir, strerror(errno)); exit(EXIT_FAILURE); } } /******************************************************************************/ static void chdir_up(void) { if (chdir("..")) dirdie("can't chdir(\"..\") from ", errno); /* XXX Have we returned to the original parent directory? * May need to perform a stat(".") and check the device and inode are the * same as those before we chdir'd into dirname. */ } /******************************************************************************/ static void dirdie(const char *msg, err_t err) { silent = 0; dirwarn(msg, err); exit(EXIT_FAILURE); } #endif /******************************************************************************/ /* NB. dirname is either "" (which corresponds to ".") or ends in a slash. * If the latter, we remove the slash prior to printing. */ static void dirwarn(const char *msg, err_t err) { char *slashp = NULL; const char *dir; if (silent) return; if (*dirname) { slashp = remove_trailing_slash(); dir = dirname; } else dir = "."; if (err) fprintf(stderr, "qfind: %s%s: %s\n", msg, dir, get_error_msg(err)); else fprintf(stderr, "qfind: %s%s\n", msg, dir); if (slashp) *slashp = slash; } /******************************************************************************/ /* Remove any trailing slash from the dirname (except for the case where it's * a root directory). */ static char * remove_trailing_slash(void) { char *end = dirname + dirname_len - 1; int root_path = (dirname_len == 1 && dirname[0] == slash); char *slashp = NULL; #if defined(WINDOWS) if (dirname_len == 3 && dirname[1] == ':' && dirname[2] == slash) root_path = 1; #endif if (!root_path && *end == slash) { slashp = end; *slashp = '\0'; } return slashp; } /******************************************************************************/ static char * get_error_msg(err_t err) { #if USE_FINDFILE static char buf[200]; if (FormatMessage( /* NB. returns 0 on error. */ FORMAT_MESSAGE_FROM_SYSTEM, NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), /* Default language. */ buf, sizeof(buf) - 1, NULL )) { /* Chomp any trailing newline from the error message. Needed! */ char *end = buf + strlen(buf) - 1; if (*end == '\n') *end = '\0'; } else { sprintf(buf, "error code %lu (FormatMessage error %lu)", (unsigned long) err, (unsigned long) GetLastError()); } return (char *) buf; #else return strerror(err); #endif } /******************************************************************************/ /* "Alright. Move On. Nothing to see here. Please disperse..." */ static void clean_up(void) { #define Free_ss(ptr) \ do { \ struct strstack *ssp = (ptr); \ Free(ssp->str.buf); \ Free(ssp->offset.list); \ } while (0) #define Free_ll(ptr) \ do { \ struct lookup_list *llp = (ptr); \ Free_ss(&llp->ss); \ } while (0) Free_ll(&xd); Free_ll(&xD); Free_ll(&xe); Free_ll(&xp); Free_ss(&dir_stack); Free_ss(&file_stack); Free(dirname); #if !USE_FINDFILE Free(start_dir); #endif } /******************************************************************************/ #if USE_FINDFILE /* Need to convert between FILETIME's and time_t's. */ /* Taken from Perl's "win32/win32.c". */ #ifdef __GNUC__ #define Const64(x) x##LL #else #define Const64(x) x##i64 #endif /* Number of 100 nanosecond units from 1/1/1601 to 1/1/1970 */ #define EPOCH_BIAS Const64(116444736000000000) static time_t filetime_to_timet(const FILETIME *filetimep) { __int64 t; t = filetimep->dwHighDateTime; t <<= 32; t |= filetimep->dwLowDateTime; t -= EPOCH_BIAS; t /= 10000000; return (time_t) t; } /******************************************************************************/ /* NB. it's possible to have a file whose LastWriteTime is older than its * CreationTime eg. a file extracted from an archive retains the 'lwt' from * the original file while its 'ct' is when it was extracted. */ static time_t get_mtime_from_find_data(const WIN32_FIND_DATA *fDatap) { time_t lwt = filetime_to_timet(&fDatap->ftLastWriteTime); if (consider_ctime) { time_t ct = filetime_to_timet(&fDatap->ftCreationTime); if (ct > lwt) lwt = ct; } return lwt; } /******************************************************************************/ static BOOL filetime_from_time(PFILETIME pFileTime, time_t Time) { struct tm *pTM = localtime(&Time); SYSTEMTIME SystemTime; FILETIME LocalTime; if (pTM == NULL) return FALSE; SystemTime.wYear = pTM->tm_year + 1900; SystemTime.wMonth = pTM->tm_mon + 1; SystemTime.wDay = pTM->tm_mday; SystemTime.wHour = pTM->tm_hour; SystemTime.wMinute = pTM->tm_min; SystemTime.wSecond = pTM->tm_sec; SystemTime.wMilliseconds = 0; return SystemTimeToFileTime(&SystemTime, &LocalTime) && LocalFileTimeToFileTime(&LocalTime, pFileTime); } /******************************************************************************/ /* See Win32::UTCFileTime for the horrors of working with UTC times and the * time_t's returned by Win32's stat(). */ static time_t to_utc_time(time_t t) { FILETIME ft; time_t utc; if (!filetime_from_time(&ft, t)) { fprintf(stderr, "qfind: failed to convert time '%ld'\n", t); exit(EXIT_FAILURE); } utc = filetime_to_timet(&ft); return utc; } #endif /******************************************************************************/ static void init_ss(struct strstack *ssp) { ssp->str.buf = NULL; ssp->str.size = 0; ssp->offset.list = NULL; ssp->offset.max = 0; ssp->offset.next = 0; ssp->total = 0; } /******************************************************************************/ static void alloc_ss(struct strstack *ssp, size_t elements, size_t bufsize) { ssp->offset.max = elements; ssp->offset.list = CPP_CAST(size_t *) Malloc( ssp->offset.max * sizeof(size_t)); ssp->str.size = bufsize; ssp->str.buf = CPP_CAST(char *) Malloc(ssp->str.size); } /******************************************************************************/ /* Place a string on the end of a strstack. */ static void push_ss(struct strstack *ssp, const char *str) { const size_t size = strlen(str) + 1; grow_ss(ssp, 1, size); memcpy(ssp->str.buf + ssp->offset.next, str, size); ssp->offset.list[ssp->total++] = ssp->offset.next; ssp->offset.next += size; } /******************************************************************************/ /* Ensure there is space in a strstack's buffers for additional strings. */ static void grow_ss(struct strstack *ssp, size_t elements, size_t size) { size_t needed; needed = elements + ssp->total; if (ssp->offset.max < needed) { ssp->offset.max = get_alloc_size(needed); ssp->offset.list = CPP_CAST(size_t *) Realloc(ssp->offset.list, ssp->offset.max * sizeof(size_t)); } needed = size + ssp->offset.next; if (ssp->str.size < needed) { ssp->str.size = get_alloc_size(needed); ssp->str.buf = CPP_CAST(char *) Realloc(ssp->str.buf, ssp->str.size); } } /******************************************************************************/ static inline char * get_str(const struct strstack *ssp, size_t i) { return ssp->str.buf + ssp->offset.list[i]; } /******************************************************************************/ static void clear_ss(struct strstack *ssp) { ssp->total = 0; ssp->offset.next = 0; } /******************************************************************************/ static void sort_ss(struct strstack *ssp, size_t from, size_t n, strcmp_fn_t cmp) { if (n > 1) { G_ssp = ssp; G_strcmp = cmp; qsort(ssp->offset.list + from, n, sizeof(ssp->offset.list[0]), cmp_ss); } } /******************************************************************************/ static void init_ll(struct lookup_list *xptr) { const size_t n = sizeof(xptr->firstchar) / sizeof(xptr->firstchar[0]); size_t i; init_ss(&xptr->ss); for (i = 0; i < n; ++i) xptr->firstchar[i] = NONE; } /******************************************************************************/ /* Organize the strings in the stack to make their lookup in the main loop * of print_files() as efficient as possible. */ static void init_ll_firstchar(struct lookup_list *xptr) { size_t i; sort_ss(&xptr->ss, 0, xptr->ss.total, strcmp); remove_reps(xptr); for (i = 0; i < xptr->ss.total; ++i) { const char *s = get_str(&xptr->ss, i); const unsigned char uc = s[0]; if (xptr->firstchar[uc] == NONE) xptr->firstchar[uc] = i; } xptr->firstchar[0] = NONE; /* Ensure "" is not a match. */ } /******************************************************************************/ /* Remove repeated names by 'collapsing' the offset.list[]. * NB. Since the strings are sorted, repetitions will be next to each other. */ static void remove_reps(struct lookup_list *xptr) { if (xptr->ss.total > 1) { size_t i, new_total = 1; const char *cur_str = get_str(&xptr->ss, 0); for (i = 1; i < xptr->ss.total; ++i) { const size_t offset = xptr->ss.offset.list[i]; const char *str = xptr->ss.str.buf + offset; if (strNE(cur_str, str)) { xptr->ss.offset.list[new_total++] = offset; cur_str = str; } } xptr->ss.total = new_total; /* Update total to reflect new contents. */ } } /******************************************************************************/ /* Is the given name in the list of excluded directories? */ static int is_exclude_dir(const char *f) { int result = 0; const char f_0 = f[0]; const size_t first_try = xd.firstchar[UCHAR(f_0)]; size_t i; if (first_try != NONE) { for (i = first_try; i < xd.ss.total; ++i) { const char *d = get_str(&xd.ss, i); if (d[0] != f_0) break; if (d[1] == f[1]) { if (!d[1] || strEQ(d + 2, f + 2)) { result = 1; break; } } else if (UCHAR(d[1]) > UCHAR(f[1])) break; } } return result; } /******************************************************************************/ /* Should the given filename be excluded because of its extension? * Treats the extension case insensitively. NB. since all the names in * xp & xe are already in lower case, we only need to lower case * the file extension to compare them. */ static int is_exclude_extension(const char *f) { const char *s, *ext = NULL; int found = 0; for (s = f; *s; ++s) if (*s == '.') ext = s + 1; if (ext) { const struct lookup_list *xptr = xp.ss.total ? &xp : &xe; const char lc_ext0 = lowercase(ext[0]); const size_t first_try = xptr->firstchar[UCHAR(lc_ext0)]; if (first_try != NONE) { const char lc_ext1 = lowercase(ext[1]); size_t i; for (i = first_try; i < xptr->ss.total; ++i) { const char *e = get_str(&xptr->ss, i); /* NB. is lowercase */ if (e[0] != lc_ext0) break; else if (e[1] == lc_ext1) { if (!lc_ext1 || stri2eq(e + 2, ext + 2)) { found = 1; break; } } else if (UCHAR(e[1]) > UCHAR(lc_ext1)) break; } } } return xp.ss.total ? !found : found; } /******************************************************************************/ /* Is dirname in the list of excluded paths? */ static int is_dirname_excluded(void) { int result = 0; const char dirname_0 = dirname[0]; const size_t first_try = xD.firstchar[UCHAR(dirname_0)]; size_t i; if (first_try != NONE) { for (i = first_try; i < xD.ss.total; ++i) { const char *d = get_str(&xD.ss, i); if (d[0] != dirname_0) break; if (d[1] == dirname[1]) { if (!d[1] || strEQ(d + 2, dirname + 2)) { result = 1; break; } } else if (UCHAR(d[1]) > UCHAR(dirname[1])) break; } } return result; } /******************************************************************************/ /* Treating file extensions as case insensitive is probably the correct * approach even on platforms that are case sensitive. * Consider: "readme.txt" vs "readme.TXT" vs "readme.Txt". */ static void parse_ext_arg(char *arg, struct lookup_list *xptr) { char *s; /* NB. 'multipart' extensions such as "tar.gz" are not supported. * We could cater for "-p=.c:.h", but frankly it does not seem worth it * when it's easier to just type "-p=c:h". */ if (strchr(arg, '.')) { fprintf(stderr, "qfind: dotted extensions not supported\n"); exit(EXIT_FAILURE); } for (s = arg; *s; ++s) *s = lowercase(*s); parse_name_list(arg, xptr); } /******************************************************************************/ /* Process -d/e/p's argument (eg. "=.git:CVS:RCS") and place the info in the * relevant lookup_list. */ static void parse_name_list(const char *arg, struct lookup_list *xptr) { size_t elements = 1, size = 1; const char *s; char *d; if (arg[0] == '=') ++arg; for (s = arg; *s; ++s) { ++size; if (*s == ':') ++elements; } /* Ensure the buffers in the xptr's strstack are large enough. */ grow_ss(&xptr->ss, elements, size); /* Copy the strings into the buffer and store their offsets. */ d = xptr->ss.str.buf + xptr->ss.offset.next; for (s = arg; 1; ++s) { if (*s == ':' || !*s) { *d++ = '\0'; xptr->ss.offset.list[xptr->ss.total++] = xptr->ss.offset.next; xptr->ss.offset.next = d - xptr->ss.str.buf; if (!*s) break; } else *d++ = *s; } } /******************************************************************************/ static void parse_time_arg(const char *arg, char opt) { static time_t time_now = 0; int time_is_since_now = 0; char *end = NULL; time_t t; if (arg[0] == '=') ++arg; /* Provide a syntax to specify times relative to now. * So "-N=#60" filters files older than 60 secs since now. */ if (arg[0] == '#') { time_is_since_now = 1; ++arg; if (!time_now) time_now = time(NULL); } errno = 0; t = strtol(arg, &end, 10); if (errno || !end || *end) { fprintf(stderr, "qfind: bad time argument '%s'\n", arg); exit(EXIT_FAILURE); } if (time_is_since_now) t = time_now - t; #if USE_FINDFILE else { /* Must convert the time_t from Win32's stat() into a UTC correct time_t * so comparisons's to the time values in a WIN32_FIND_DATA's are valid. * NB. to ensure comparisons are made at the 1 second granularity, * we compare time_t's and not FILETIMEs. */ t = to_utc_time(t); } #endif if (opt == 'N') { skip_old_files = 1; opt_N_time = t; } else { /* 'O' */ skip_new_files = 1; opt_O_time = t; } } /******************************************************************************/ /* Parse a string containing a file size. */ static void parse_size_arg(const char *arg, char opt) { #if USE_FINDFILE DWORD high, low; #else filesize_t size; #endif char *end = NULL; if (arg[0] == '=') ++arg; errno = 0; #if USE_FINDFILE { #if defined(STDC99) unsigned long long sz64 = strtoull(arg, &end, 10); high = sz64 >> 32; low = sz64 & MAXDWORD; #else unsigned long sz32 = strtoul(arg, &end, 10); high = 0; low = sz32; #endif } #else size = (filesize_t) strtoul(arg, &end, 10); #endif if (errno || !end || *end) { fprintf(stderr, "qfind: bad size argument '%s'\n", arg); exit(EXIT_FAILURE); } if (opt == 'J') { min_size_test = 1; #if USE_FINDFILE min_size_hl.high = high; min_size_hl.low = low; #else min_size = size; #endif } else { /* 'K' */ max_size_test = 1; #if USE_FINDFILE max_size_hl.high = high; max_size_hl.low = low; #else max_size = size; #endif } } /******************************************************************************/ #if !USE_FINDFILE static void parse_id_arg(const char *arg, char opt) { char *end = NULL; id_t id; if (arg[0] == '=') ++arg; errno = 0; id = (id_t) strtoul(arg, &end, 10); if (errno || !end || *end) { fprintf(stderr, "qfind: bad id argument '%s'\n", arg); exit(EXIT_FAILURE); } if (opt == 'u' || opt == 'U') { opt_uid = id; uid_test = (opt == 'u' ? 1 : 2); } else { /* 'g' or 'G' */ opt_gid = id; gid_test = (opt == 'g' ? 1 : 2); } } #endif /******************************************************************************/ /* The directory path names in xD are compared to dirname which will have a * trailing slash. This ensures the names in xD also have a trailing slash. */ static void fix_xD(void) { const size_t total = xD.ss.total; /* The total before adding more. */ char *buf; size_t i; if (!total) return; /* Use a temporary buffer to build 'fixed' path names. */ buf = CPP_CAST(char *) Malloc(xD.ss.offset.next + 2); for (i = 0; i < total; ++i) { char *orig = get_str(&xD.ss, i); char *s, *d = buf; for (s = orig; *s; ++s) { #if USE_FINDFILE if (*s == slosh) *s = slash; #endif *d++ = *s; } if (d > buf && *(d - 1) != slash) { *d++ = slash; *d = '\0'; push_ss(&xD.ss, buf); orig[0] = '\0'; /* Erase original string. */ } } free(buf); } /******************************************************************************/ /* Skip zero size files by setting a minimum size requirement of 1. */ static void skip_zero_size_files(void) { if (min_size_test) /* cf. "qfind -G=22 -z". */ return; min_size_test = 1; #if USE_FINDFILE min_size_hl.high = 0; min_size_hl.low = 1; #else min_size = 1; #endif } /******************************************************************************/ #if USE_FINDFILE /* Compare a high/low integer cf. the file size in a WIN32_FIND_DATA. * Returns 1 if a > b, -1 if a < b, and 0 if a == b. */ static inline int high_low_cmp(DWORD ah, DWORD al, DWORD bh, DWORD bl) { int result; if (ah == bh) { if (al > bl) result = 1; else if (al < bl) result = -1; else result = 0; } else if (ah > bh) result = 1; else result = -1; return result; } #endif /******************************************************************************/ static void sort_filenames(struct strstack *ssp, size_t from, size_t n) { if (filename_cmp) sort_ss(ssp, from, n, filename_cmp); } /******************************************************************************/ /* Test if two strings are the same (ignoring case differences) where the first * is known to be in lower case. */ static int stri2eq(const char *s1, const char *s2) { while (1) { const int c1 = UCHAR(*s1); const int c2 = lowercase(*s2); if (c1 != c2) return 0; if (!c1) return 1; ++s1; ++s2; } } /******************************************************************************/ /* Qsort comparison function for the strings inside a strstack. */ static int cmp_ss(const void *elem1, const void *elem2) { const size_t offset1 = *((const size_t *) elem1); const size_t offset2 = *((const size_t *) elem2); const char *s1 = G_ssp->str.buf + offset1; const char *s2 = G_ssp->str.buf + offset2; return G_strcmp(s1, s2); } /******************************************************************************/ #if !(UCHAR_MAX <= INT_MAX) #error The code assumes that UCHAR_MAX <= INT_MAX. /* ...in particular on machines where char and int are types of the same size, * the difference of two unsigned char values (including the sign bit) doesn't * fit in an int. */ #endif /* Perform a string comparison ignoring case except in a tie. * NB. This is different to the standard stricmp() and should not be used for * testing equivalence eg. mystricmp("ABC", "abc") != 0. */ static int mystricmp(const char *s1, const char *s2) { int result = 0; while (1) { const int c1 = UCHAR(*s1); const int c2 = UCHAR(*s2); if (c1 == c2) { if (!c1) break; } else { const int lc1 = to_lower(c1); const int lc2 = to_lower(c2); if (lc1 == lc2) { if (!result) result = c1 - c2; } else { result = lc1 - lc2; break; } } ++s1; ++s2; } return result; } /******************************************************************************/ /* A string comparison that considers numbers embedded within them. */ static int strnumcmp(const char *s1, const char *s2) { int case_diff = 0; while (1) { if (izdigit(*s1) && izdigit(*s2)) { int num_diff = 0, zeros = 0; while (*s1 == '0' && izdigit(s1[1])) { ++s1; --zeros; } while (*s2 == '0' && izdigit(s2[1])) { ++s2; ++zeros; } while (1) { /* Both *s1 and *s2 are digits. */ if (!num_diff) num_diff = UCHAR(*s1) - UCHAR(*s2); ++s1; ++s2; if (!izdigit(*s1)) { if (izdigit(*s2)) /* # in s1 has less digits, so is < # in s2. */ return -1; if (num_diff) return num_diff; if (zeros) return zeros; break; } else if (!izdigit(*s2)) /* # in s1 has more digits, so is > # in s2. */ return 1; } } { const int c1 = UCHAR(*s1); const int c2 = UCHAR(*s2); if (c1 == c2) { if (!c1) return case_diff; } else { const int lc1 = to_lower(c1); const int lc2 = to_lower(c2); if (lc1 == lc2) { if (!case_diff) case_diff = c1 - c2; } else return lc1 - lc2; } ++s1; ++s2; } } /* Unreachable. */ } /******************************************************************************/ static void alloc_dirname(size_t bufsize) { dirname_bufsize = bufsize; dirname = CPP_CAST(char *) Malloc(dirname_bufsize); dirname[0] = '\0'; dirname_len = 0; } /******************************************************************************/ /* Append to dirname the given string at the given position, plus add a * trailing slash. */ static void append_dirname(const char *d, size_t old_dirname_len) { const size_t d_len = strlen(d); size_t needed; dirname_len = old_dirname_len + d_len + 1; /* We also need space for the NUL char and append_globchar_to_dirname() * assumes additional space to append a "*". */ needed = dirname_len + 2; if (dirname_bufsize < needed) { dirname_bufsize = get_alloc_size(needed); dirname = CPP_CAST(char *) Realloc(dirname, dirname_bufsize); } memcpy(dirname + old_dirname_len, d, d_len); dirname[dirname_len - 1] = slash; dirname[dirname_len] = '\0'; } /******************************************************************************/ static void set_dirname(const char *dir) { /* An empty DIR argument is taken to imply scan the start directory. */ if (!dir[0]) { restore_dirname(0); return; } append_dirname(dir, 0); #if USE_FINDFILE { /* Ensure consistent slashes in dirname. */ char *s; for (s = dirname; *s; ++s) if (*s == slosh) *s = slash; } #endif /* Since append_dirname() appends a trailing slash, if the given dir path * already ends in a slash we need to chomp dirname's final slash. * NB. dirname_len >= 2. */ if (dirname[dirname_len - 2] == slash) dirname[--dirname_len] = '\0'; } /******************************************************************************/ static void restore_dirname(size_t len) { dirname[len] = '\0'; dirname_len = len; } /******************************************************************************/ /* XXX In theory, the arithmetic below could overflow. In practise, if this did * occur your filesystem would be so clogged as to make this bug in qfind the * least of your problems :-) */ static size_t get_alloc_size(size_t needed) { size_t alloc; alloc = 2 * needed; alloc &= ~7; alloc += 8; return alloc; } /******************************************************************************/ static void * Malloc(size_t size) { void *result = malloc(size); if (!result) { fprintf(stderr, "qfind: malloc(%lu) failed\n", (unsigned long) size); exit(EXIT_FAILURE); } return result; } /******************************************************************************/ static void * Realloc(void *ptr, size_t size) { void *result = realloc(ptr, size); if (!result) { fprintf(stderr, "qfind: realloc(%lu) failed\n", (unsigned long) size); exit(EXIT_FAILURE); } return result; } /******************************************************************************/ #if USE_FINDFILE #define F(s) s #define P(s) #else #define F(s) #define P(s) s #endif static void print_help(void) { fprintf(stderr, "Usage: qfind [OPTION...] [DIR...]\n" " Recursively prints out the regular files beneath a directory.\n" " If no DIR is specified, the current directory is assumed.\n" "OPTIONs:\n" P(" -a Print all non-directory files.\n") F(" -b Print backslashed path names.\n") " -c -N/-O uses most-recent(ctime, mtime).\n" " -d=DIR Ignore files beneath the directory DIR.\n" " Multiple directories can be specified:\n" " qfind -d=DIR1:DIR2:DIR3 -d=DIR4\n" " -D=PATH Ignore files beneath the pathname PATH.\n" " Multiple paths can be specified:\n" " qfind -D=PATH1:PATH2 -D=PATH3\n" " -e=EXT Ignore files whose extension is EXT.\n" " Multiple extensions can be specified:\n" " qfind -e=EXT1:EXT2:EXT3 -e=EXT4\n" P(" -g=GID Only print files belonging to group GID.\n") P(" -G=GID Ignore files belonging to group GID.\n") " -i Sort the filenames ignoring case distinctions.\n" " -n Sort the filenames wrt. embedded numbers.\n" " -o Ignore 'dot' files/directories.\n" " -p=EXT Only print files with extension EXT.\n" " This option overrides -e.\n" " Multiple extensions can be specified:\n" " qfind -p=EXT1:EXT2:EXT3 -p=EXT4\n" P(" -r Only print readable files.\n") " -s Silent. No error messages to STDERR.\n" F(" -t Truncate filenames ie. the classic 8.3 format.\n") P(" -u=UID Only print files belonging to user UID.\n") P(" -U=UID Ignore files belonging to user UID.\n") " -x Do not sort filenames.\n" F(" -y Ignore HIDDEN|SYSTEM files/directories.\n") " -z Ignore zero size files.\n" " -J=SIZE Ignore files smaller than SIZE bytes.\n" " -K=SIZE Ignore files larger than SIZE bytes.\n" " -N=TIME Ignore files older than TIME (as a time_t).\n" " -O=TIME Ignore files newer than TIME (as a time_t).\n" " -S Sort the filenames prior to printing.\n" " -V Print version information and exit.\n" " -h Show this help and exit.\n" " -- End options.\n" "\n" "Version %s (%s). Built on %s.\n", QFIND_VERSION, QFIND_BUILD, __DATE__ ); } #undef F #undef P /******************************************************************************/