/*
 * 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <time.h>

#if defined(WINDOWS)
#include <io.h>
#include <fcntl.h>
#endif

#if USE_FINDFILE
#include <windows.h>
#else
#include <dirent.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#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 <ctype.h>'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 <ctype.h>
#include <locale.h>
#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

/******************************************************************************/