Blob Blame History Raw
/*
 * Amanda, The Advanced Maryland Automatic Network Disk Archiver
 * Copyright (c) 1991-1998 University of Maryland at College Park
 * Copyright (c) 2007-2012 Zmanda, Inc.  All Rights Reserved.
 * Copyright (c) 2013-2016 Carbonite, Inc.  All Rights Reserved.
 * All Rights Reserved.
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation, and that the name of U.M. not be used in advertising or
 * publicity pertaining to distribution of the software without specific,
 * written prior permission.  U.M. makes no representations about the
 * suitability of this software for any purpose.  It is provided "as is"
 * without express or implied warranty.
 *
 * U.M. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL U.M.
 * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * Authors: the Amanda Development Team.  Its members are listed in a
 * file named AUTHORS, in the root directory of this distribution.
 */

/*
 * See match.h for function prototypes and further explanations.
 */

#include "amanda.h"
#include "match.h"
#include <regex.h>

/*
 * DATA STRUCTURES, MACROS, STATIC DATA
 */

/*
 * Return codes used by try_match()
 */

#define MATCH_OK (1)
#define MATCH_NONE (0)
#define MATCH_ERROR (-1)

/*
 * Macro to tell whether a character is a regex metacharacter. Note that '*'
 * and '?' are NOT included: they are themselves special in globs.
 */

#define IS_REGEX_META(c) ( \
    (c) == '.' || (c) == '(' || (c) == ')' || (c) == '{' || (c) == '}' || \
    (c) == '+' || (c) == '^' || (c) == '$' || (c) == '|' \
)

/*
 * Define a specific type to hold error messages in case regex compile/matching
 * fails
 */

typedef char regex_errbuf[STR_SIZE];

/*
 * Structure used by amglob_to_regex() to expand particular glob characters. Its
 * fields are:
 * - question_mark: what the question mark ('?') should be replaced with;
 * - star: what the star ('*') should be replaced with;
 * - double_star: what two consecutive stars should be replaced with.
 *
 * Note that apart from double_star, ALL OTHER FIELDS MUST NOT BE NULL.
 */

struct subst_table {
    const char *question_mark;
    const char *star;
    const char *double_star;
};

/*
 * Susbtitution data for glob_to_regex()
 */

static struct subst_table glob_subst_stable = {
    "[^/]", /* question_mark */
    "[^/]*", /* star */
    NULL /* double_star */
};

/*
 * Substitution data for tar_to_regex()
 */

static struct subst_table tar_subst_stable = {
    "[^/]", /* question_mark */
    ".*", /* star */
    NULL /* double_star */
};

/*
 * Substitution data for match_word(): dot
 */

static struct subst_table mword_dot_subst_table = {
    "[^.]", /* question_mark */
    "[^.]*", /* star */
    ".*" /* double_star */
};

/*
 * Substitution data for match_word(): slash
 */

static struct subst_table mword_slash_subst_table = {
    "[^/]", /* question_mark */
    "[^/]*", /* star */
    ".*" /* double_star */
};

/*
 * match_word() specific data:
 * - re_double_sep: anchored regex matching two separators;
 * - re_separator: regex matching the separator;
 * - re_begin_full: regex matching the separator, anchored at the beginning;
 * - re_end_full: regex matching the separator, andchored at the end.
 */

struct mword_regexes {
    const char *re_double_sep;
    const char *re_begin_full;
    const char *re_separator;
    const char *re_end_full;
};

static struct mword_regexes mword_dot_regexes = {
    "^\\.\\.$", /* re_double_sep */
    "^\\.", /* re_begin_full */
    "\\.", /* re_separator */
    "\\.$" /* re_end_full */
};

static struct mword_regexes mword_slash_regexes = {
    "^\\/\\/$", /* re_double_sep */
    "^\\/", /* re_begin_full */
    "\\/", /* re_separator */
    "\\/$" /* re_end_full */
};

/*
 * Regular expression caches, and a static mutex to protect initialization and
 * access. This may be unnecessarily coarse, but it is unknown at this time
 * whether GHashTable accesses are thread-safe, and get_regex_from_cache() may
 * be called from within threads, so play it safe.
 */

#if (GLIB_MAJOR_VERSION > 2 || (GLIB_MAJOR_VERSION == 2 && GLIB_MINOR_VERSION >= 31))
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wmissing-field-initializers"
  static GStaticMutex re_cache_mutex = G_STATIC_MUTEX_INIT;
# pragma GCC diagnostic pop
#else
  static GStaticMutex re_cache_mutex = G_STATIC_MUTEX_INIT;
#endif
static GHashTable *regex_cache = NULL, *regex_cache_newline = NULL;

/*
 * REGEX FUNCTIONS
 */

/*
 * Initialize regex caches. NOTE: this function MUST be called with
 * re_cache_mutex LOCKED, see get_regex_from_cache()
 */

static void init_regex_caches(void)
{
    static gboolean initialized = FALSE;

    if (initialized)
        return;

    regex_cache = g_hash_table_new(g_str_hash, g_str_equal);
    regex_cache_newline = g_hash_table_new(g_str_hash, g_str_equal);

    initialized = TRUE;
}

/*
 * Cleanup a regular expression by escaping all non alphanumeric characters, and
 * append beginning/end anchors if need be
 */

char *clean_regex(const char *str, gboolean anchor)
{
    const char *src;
    char *result, *dst;

    result = g_malloc(2 * strlen(str) + 3);
    dst = result;

    if (anchor)
        *dst++ = '^';

    for (src = str; *src; src++) {
        if (!g_ascii_isalnum((int) *src))
            *dst++ = '\\';
        *dst++ = *src;
    }

    if (anchor)
        *dst++ = '$';

    *dst = '\0';
    return result;
}

/*
 * Compile one regular expression. Return TRUE if the regex has been compiled
 * successfully. Otherwise, return FALSE and copy the error message into the
 * supplied regex_errbuf pointer. Also, we want to know whether flags should
 * include REG_NEWLINE (See regcomp(3) for details). Since this is the more
 * frequent case, add REG_NEWLINE to the default flags, and remove it only if
 * match_newline is set to FALSE.
 */

static gboolean do_regex_compile(const char *str, regex_t *regex,
    regex_errbuf *errbuf, gboolean match_newline)
{
    int flags = REG_EXTENDED | REG_NOSUB | REG_NEWLINE;
    int result;

    if (!match_newline)
	flags &= ~REG_NEWLINE;

    result = regcomp(regex, str, flags);

    if (!result)
        return TRUE;

    regerror(result, regex, *errbuf, sizeof(*errbuf));
    return FALSE;
}

/*
 * Get an already compiled buffer from the regex cache. If the regex is not in
 * the cache, allocate a new one and compile it using do_regex_compile(). If the
 * compile fails, call regfree() on the object and return NULL to the caller. If
 * it does succeed, put the regex buffer in cache and return a pointer to it.
 */

static regex_t *get_regex_from_cache(const char *re_str, regex_errbuf *errbuf,
    gboolean match_newline)
{
    regex_t *ret;
    GHashTable *cache;

    g_static_mutex_lock(&re_cache_mutex);

    init_regex_caches();

    cache = (match_newline) ? regex_cache_newline: regex_cache;
    ret = g_hash_table_lookup(cache, re_str);

    if (ret)
        goto out;

    ret = g_new(regex_t, 1);

    if (do_regex_compile(re_str, ret, errbuf, match_newline)) {
        g_hash_table_insert(cache, g_strdup(re_str), ret);
        goto out;
    }

    regfree(ret);
    g_free(ret);
    ret = NULL;

out:
    g_static_mutex_unlock(&re_cache_mutex);
    return ret;
}

/*
 * Validate one regular expression using do_regex_compile(), and return NULL if
 * the regex is valid, or the error message otherwise.
 */

char *validate_regexp(const char *regex)
{
    regex_t regc;
    static regex_errbuf errmsg;
    gboolean valid;

    valid = do_regex_compile(regex, &regc, &errmsg, TRUE);

    regfree(&regc);
    return (valid) ? NULL : errmsg;
}

/*
 * See if a string matches a compiled regular expression. Return one of MATCH_*
 * defined above. If, for some reason, regexec() returns something other than
 * not 0 or REG_NOMATCH, return MATCH_ERROR and print the error message in the
 * supplied regex_errbuf.
 */

static int try_match(regex_t *regex, const char *str,
    regex_errbuf *errbuf)
{
    int result = regexec(regex, str, 0, 0, 0);

    switch(result) {
        case 0:
            return MATCH_OK;
        case REG_NOMATCH:
            return MATCH_NONE;
        /* Fall through: something went really wrong */
    }

    regerror(result, regex, *errbuf, sizeof(*errbuf));
    return MATCH_ERROR;
}

/*
 * Try and match a string against a regular expression, using
 * do_regex_compile() and try_match(). Exit early if the regex didn't compile
 * or there was an error during matching.
 */

int do_match(const char *regex, const char *str, gboolean match_newline)
{
    regex_t *re;
    int result;
    regex_errbuf errmsg;

    re = get_regex_from_cache(regex, &errmsg, match_newline);

    if (!re)
        error("regex \"%s\": %s", regex, errmsg);
        /*NOTREACHED*/

    result = try_match(re, str, &errmsg);

    if (result == MATCH_ERROR)
        error("regex \"%s\": %s", regex, errmsg);
        /*NOTREACHED*/

    return result;
}

/*
 * DISK/HOST EXPRESSION HANDLING
 */

/*
 * Check whether a given character should be escaped (that is, prepended with a
 * backslash), EXCEPT for one character.
 */

static gboolean should_be_escaped_except(char c, char not_this_one)
{
    if (c == not_this_one)
        return FALSE;

    switch (c) {
        case '\\':
        case '^':
        case '$':
        case '?':
        case '*':
        case '[':
        case ']':
        case '.':
        case '/':
            return TRUE;
    }

    return FALSE;
}

/*
 * Take a disk/host expression and turn it into a full-blown amglob (with
 * start and end anchors) following rules in amanda-match(7). The not_this_one
 * argument represents a character which is NOT meant to be special in this
 * case: '/' for disks and '.' for hosts.
 */

static char *full_amglob_from_expression(const char *str, char not_this_one)
{
    const char *src;
    char *result, *dst;

    result = g_malloc(2 * strlen(str) + 3);
    dst = result;

    *dst++ = '^';

    for (src = str; *src; src++) {
        if (should_be_escaped_except(*src, not_this_one))
            *dst++ = '\\';
        *dst++ = *src;
    }

    *dst++ = '$';
    *dst = '\0';
    return result;
}

/*
 * Turn a disk/host expression into a regex
 */

char *make_exact_disk_expression(const char *disk)
{
    return full_amglob_from_expression(disk, '/');
}

char *make_exact_host_expression(const char *host)
{
    return full_amglob_from_expression(host, '.');
}

/*
 * GLOB HANDLING, as per amanda-match(7)
 */

/*
 * Turn a glob into a regex.
 */

static char *amglob_to_regex(const char *str, const char *begin,
    const char *end, struct subst_table *table)
{
    const char *src;
    char *result, *dst;
    char c;
    size_t worst_case;
    gboolean double_star = (table->double_star != NULL);

    /*
     * There are two particular cases when building a regex out of a glob:
     * character classes (anything inside [...] or [!...] and quotes (anything
     * preceded by a backslash). We start with none being true.
     */

    gboolean in_character_class = FALSE, in_quote = FALSE;

    /*
     * Allocate enough space for our string. At worst, the allocated space is
     * the length of the following:
     * - beginning of regex;
     * - size of original string multiplied by worst-case expansion;
     * - end of regex;
     * - final 0.
     *
     * Calculate the worst case expansion by walking our struct subst_table.
     */

    worst_case = strlen(table->question_mark);

    if (worst_case < strlen(table->star))
        worst_case = strlen(table->star);

    if (double_star && worst_case < strlen(table->double_star))
        worst_case = strlen(table->double_star);

    result = g_malloc(strlen(begin) + strlen(str) * worst_case + strlen(end) + 1);

    /*
     * Start by copying the beginning of the regex...
     */

    dst = g_stpcpy(result, begin);

    /*
     * ... Now to the meat of it.
     */

    for (src = str; *src; src++) {
        c = *src;

        /*
         * First, check that we're in a character class: each and every
         * character can be copied as is. We only need to be careful is the
         * character is a closing bracket: it will end the character class IF
         * AND ONLY IF it is not preceded by a backslash.
         */

        if (in_character_class) {
            in_character_class = ((c != ']') || (*(src - 1) == '\\'));
            goto straight_copy;
        }

        /*
         * Are we in a quote? If yes, it is really simple: copy the current
         * character, close the quote, the end.
         */

        if (in_quote) {
            in_quote = FALSE;
            goto straight_copy;
        }

        /*
         * The only thing left to handle now is the "normal" case: we are not in
         * a character class nor in a quote.
         */

        if (c == '\\') {
            /*
             * Backslash: append it, and open a new quote.
             */
            in_quote = TRUE;
            goto straight_copy;
        } else if (c == '[') {
            /*
             * Opening bracket: the beginning of a character class.
             *
             * Look ahead the next character: if it's an exclamation mark, then
             * this is a complemented character class; append a caret to make
             * the result string regex-friendly, and forward one character in
             * advance.
             */
            *dst++ = c;
            in_character_class = TRUE;
            if (*(src + 1) == '!') {
                *dst++ = '^';
                src++;
            }
        } else if (IS_REGEX_META(c)) {
            /*
             * Regex metacharacter (except for ? and *, see below): append a
             * backslash, and then the character itself.
             */
            *dst++ = '\\';
            goto straight_copy;
        } else if (c == '?')
            /*
             * Question mark: take the subsitution string out of our subst_table
             * and append it to the string.
             */
            dst = g_stpcpy(dst, table->question_mark);
        else if (c == '*') {
            /*
             * Star: append the subsitution string found in our subst_table.
             * However, look forward the next character: if it's yet another
             * star, then see if there is a substitution string for the double
             * star and append this one instead.
             *
             * FIXME: this means that two consecutive stars in a glob string
             * where there is no substition for double_star can lead to
             * exponential regex execution time: consider [^/]*[^/]*.
             */
            const char *p = table->star;
            if (double_star && *(src + 1) == '*') {
                src++;
                p = table->double_star;
            }
            dst = g_stpcpy(dst, p);
        } else {
            /*
             * Any other character: append each time.
             */
straight_copy:
            *dst++ = c;
        }
    }

    /*
     * Done, now append the end, ONLY if we are not in a quote - a lone
     * backslash at the end of a glob is illegal, just leave it as it, it will
     * make the regex compile fail.
     */

    if (!in_quote)
        dst = g_stpcpy(dst, end);
    /*
     * Finalize, return.
     */

    *dst = '\0';
    return result;
}

/*
 * File globs
 */

char *glob_to_regex(const char *glob)
{
    return amglob_to_regex(glob, "^", "$", &glob_subst_stable);
}

int match_glob(const char *glob, const char *str)
{
    char *regex;
    regex_t *re;
    int result;
    regex_errbuf errmsg;

    regex = glob_to_regex(glob);
    re = get_regex_from_cache(regex, &errmsg, TRUE);

    if (!re)
        error("glob \"%s\" -> regex \"%s\": %s", glob, regex, errmsg);
        /*NOTREACHED*/

    result = try_match(re, str, &errmsg);

    if (result == MATCH_ERROR)
        error("glob \"%s\" -> regex \"%s\": %s", glob, regex, errmsg);
        /*NOTREACHED*/

    g_free(regex);

    return result;
}

char *validate_glob(const char *glob)
{
    char *regex, *ret = NULL;
    regex_t regc;
    static regex_errbuf errmsg;

    regex = glob_to_regex(glob);

    if (!do_regex_compile(regex, &regc, &errmsg, TRUE))
        ret = errmsg;

    regfree(&regc);
    g_free(regex);
    return ret;
}

/*
 * Tar globs
 */

static char *tar_to_regex(const char *glob)
{
    return amglob_to_regex(glob, "(^|/)", "($|/)", &tar_subst_stable);
}

int match_tar(const char *glob, const char *str)
{
    char *regex;
    regex_t *re;
    int result;
    regex_errbuf errmsg;

    regex = tar_to_regex(glob);
    re = get_regex_from_cache(regex, &errmsg, TRUE);

    if (!re)
        error("glob \"%s\" -> regex \"%s\": %s", glob, regex, errmsg);
        /*NOTREACHED*/

    result = try_match(re, str, &errmsg);

    if (result == MATCH_ERROR)
        error("glob \"%s\" -> regex \"%s\": %s", glob, regex, errmsg);
        /*NOTREACHED*/

    g_free(regex);

    return result;
}

/*
 * DISK/HOST MATCHING
 *
 * The functions below wrap input strings with separators and attempt to match
 * the result. The core of the operation is the match_word() function.
 */

/*
 * Check whether a glob passed as an argument to match_word() only looks for the
 * separator
 */

static gboolean glob_is_separator_only(const char *glob, char sep) {
    size_t len = strlen(glob);
    const char len2_1[] = { '^', sep , 0 }, len2_2[] = { sep, '$', 0 },
        len3[] = { '^', sep, '$', 0 };

    switch (len) {
        case 1:
            return (*glob == sep);
        case 2:
            return !(!g_str_equal(glob, len2_1) && !g_str_equal(glob, len2_2));
        case 3:
            return g_str_equal(glob, len3);
        default:
            return FALSE;
    }
}

/*
 * Given a word and a separator as an argument, wrap the word with separators -
 * if need be. For instance, if '/' is the separator, the rules are:
 *
 * - "" -> "/"
 * - "/" -> "//"
 * - "//" -> left alone
 * - "xxx" -> "/xxx/"
 * - "/xxx" -> "/xxx/"
 * - "xxx/" -> "/xxx/"
 * - "/xxx/" -> left alone
 *
 * (note that xxx here may contain the separator as well)
 *
 * Note that the returned string is dynamically allocated: it is up to the
 * caller to free it. Note also that the first argument MUST NOT BE NULL.
 */

static char *wrap_word(const char *word, const char separator, const char *glob)
{
    size_t len = strlen(word);
    size_t len_glob = strlen(glob);
    char *result, *p;

    /*
     * We allocate for the worst case, which is two bytes more than the input
     * (have to prepend and append a separator).
     */
    result = g_malloc(len + 3);
    p = result;

    /*
     * Zero-length: separator only
     */

    if (len == 0) {
        *p++ = separator;
        goto out;
    }

    /*
     * Length is one: if the only character is the separator only, the result
     * string is two separators
     */

    if (len == 1 && word[0] == separator) {
        *p++ = separator;
        *p++ = separator;
        goto out;
    }

    /*
     * Otherwise: prepend the separator if needed, append the separator if
     * needed.
     */

    if (word[0] != separator && glob[0] != '^')
        *p++ = separator;

    p = g_stpcpy(p, word);

    if (word[len - 1] != separator && glob[len_glob-1] != '$')
        *p++ = separator;

out:
    *p++ = '\0';
    return result;
}

static int match_word(const char *glob, const char *word, const char separator)
{
    char *wrapped_word = wrap_word(word, separator, glob);
    struct mword_regexes *regexes = &mword_slash_regexes;
    struct subst_table *table = &mword_slash_subst_table;
    gboolean not_slash = (separator != '/');
    int ret;

    /*
     * We only expect two separators: '/' or '.'. If it's not '/', it has to be
     * the other one...
     */
    if (not_slash) {
        regexes = &mword_dot_regexes;
        table = &mword_dot_subst_table;
    }

    if(glob_is_separator_only(glob, separator)) {
        ret = do_match(regexes->re_double_sep, wrapped_word, TRUE);
        goto out;
    } else {
        /*
         * Unlike what happens for tar and disk expressions, we need to
         * calculate the beginning and end of our regex before calling
         * amglob_to_regex().
         */

        const char *begin, *end;
        char *glob_copy = g_strdup(glob);
        char *p, *g = glob_copy;
        char *regex;

        /*
         * Calculate the beginning of the regex:
         * - by default, it is an unanchored separator;
         * - if the glob begins with a caret, make that an anchored separator,
         *   and increment g appropriately;
         * - if it begins with a separator, make it the empty string.
         */

        p = glob_copy;
        begin = regexes->re_separator;

        if (*p == '^') {
            begin = "^";
            p++, g++;
            if (*p == separator) {
		begin = regexes->re_begin_full;
                g++;
	    }
        } else if (*p == separator)
            begin = "";

        /*
         * Calculate the end of the regex:
         * - an unanchored separator by default;
         * - if the last character is a backslash or the separator itself, it
         *   should be the empty string;
         * - if it is a dollar sign, overwrite it with 0 and look at the
         *   character before it: if it is the separator, only anchor at the
         *   end, otherwise, add a separator before the anchor.
         */

        p = &(glob_copy[strlen(glob_copy) - 1]);
        end = regexes->re_separator;
        if (*p == '\\' || *p == separator) {
            end = "";
        } else if (*p == '$') {
            char prev = *(p - 1);
            *p = '\0';
	    if (prev == separator) {
		*(p-1) = '\0';
		if (p-2 >= glob_copy) {
		    prev = *(p - 2);
		    if (prev == '\\') {
			*(p-2) = '\0';
		    }
		}
		end = regexes->re_end_full;
	    } else {
		end = "$";
	    }
        }

        regex = amglob_to_regex(g, begin, end, table);
        ret = do_match(regex, wrapped_word, TRUE);

        g_free(glob_copy);
        g_free(regex);
    }

out:
    g_free(wrapped_word);
    return ret;
}

/*
 * Match a host expression
 */

int match_host(const char *glob, const char *host)
{
    char *lglob, *lhost;
    int ret;

    if (*glob == '=') {
	return strcmp(glob+1, host) == 0;
    }
    lglob = g_ascii_strdown(glob, -1);
    lhost = g_ascii_strdown(host, -1);

    ret = match_word(lglob, lhost, '.');

    g_free(lglob);
    g_free(lhost);
    return ret;
}

/*
 * Match a disk expression. Not as straightforward, since Windows paths must be
 * accounted for.
 */

/*
 * Convert a disk and glob from Windows expressed paths (backslashes) into Unix
 * paths (slashes).
 *
 * Note: the resulting string is dynamically allocated, it is up to the caller
 * to free it.
 *
 * Note 2: UNC in convert_unc_to_unix stands for Uniform Naming Convention.
 */

static char *convert_unc_to_unix(const char *unc)
{
    char *result = g_strdup(unc);
    return g_strdelimit(result, "\\", '/');
}

static char *convert_winglob_to_unix(const char *glob)
{
    const char *src;
    char *result, *dst;
    result = g_malloc(strlen(glob) + 1);
    dst = result;

    for (src = glob; *src; src++) {
        if (*src == '\\' && *(src + 1) == '\\') {
            *dst++ = '/';
            src++;
            continue;
        }
        *dst++ = *src;
    }
    *dst = '\0';
    return result;
}

/*
 * Match a disk expression
 */

int match_disk(const char *glob, const char *disk)
{
    char *glob2 = NULL, *disk2 = NULL;
    const char *g = glob, *d = disk;
    int result;

    /*
     * Check whether our disk potentially refers to a Windows share (the first
     * two characters are '\' and there is no / in the word at all): if yes,
     * build Unix paths instead and pass those as arguments to match_word()
     */

    gboolean windows_share = !(strncmp(disk, "\\\\", 2) || strchr(disk, '/'));

    if (*glob == '=') {
	return strcmp(glob+1, disk) == 0;
    }

    if (windows_share) {
        glob2 = convert_winglob_to_unix(glob);
        disk2 = convert_unc_to_unix(disk);
        g = (const char *) glob2;
        d = (const char *) disk2;
    }

    result = match_word(g, d, '/');

    /*
     * We can g_free(NULL), so this is "safe"
     */
    g_free(glob2);
    g_free(disk2);

    return result;
}

/*
 * TIMESTAMPS/LEVEL MATCHING
 */

static int
alldigits(
    const char *str)
{
    while (*str) {
	if (!isdigit((int)*(str++)))
	    return 0;
    }
    return 1;
}

int
match_datestamp(
    const char *	dateexp,
    const char *	datestamp)
{
    char *dash;
    size_t len, len_suffix;
    size_t len_prefix;
    char firstdate[100], lastdate[100];
    char mydateexp[100];
    int match_exact;

    if(strlen(dateexp) >= 100 || strlen(dateexp) < 1) {
	goto illegal;
    }

    /*
     * datestamp matching does not involve globs or regexes, so
     * handle wildcarding as a special case: if dateexp is "*",
     * then all datestamps match.
     */
    if (strcmp(dateexp, "*") == 0) {
	return TRUE;
    }

    if (*dateexp == '=') {
	return strcmp(dateexp+1, datestamp) == 0;
    }

    /* strip and ignore an initial "^" */
    if(dateexp[0] == '^') {
	strncpy(mydateexp, dateexp+1, sizeof(mydateexp)-1);
	mydateexp[sizeof(mydateexp)-1] = '\0';
    }
    else {
	strncpy(mydateexp, dateexp, sizeof(mydateexp)-1);
	mydateexp[sizeof(mydateexp)-1] = '\0';
    }

    if(strlen(dateexp) < 1) {
	goto illegal;
    }

    if(mydateexp[strlen(mydateexp)-1] == '$') {
	match_exact = 1;
	mydateexp[strlen(mydateexp)-1] = '\0';	/* strip the trailing $ */
    }
    else
	match_exact = 0;

    /* a single dash represents a date range */
    if((dash = strchr(mydateexp,'-'))) {
	if(match_exact == 1 || strchr(dash+1, '-')) {
	    goto illegal;
	}

	/* format: XXXYYYY-ZZZZ, indicating dates XXXYYYY to XXXZZZZ */

	len = (size_t)(dash - mydateexp);   /* length of XXXYYYY */
	len_suffix = strlen(dash) - 1;	/* length of ZZZZ */
	if (len_suffix > len) goto illegal;
	if (len < len_suffix) {
	    goto illegal;
	}
	len_prefix = len - len_suffix; /* length of XXX */

	dash++;

	strncpy(firstdate, mydateexp, len);
	firstdate[len] = '\0';
	strncpy(lastdate, mydateexp, len_prefix);
	strncpy(&(lastdate[len_prefix]), dash, len_suffix);
	lastdate[len] = '\0';
	if (!alldigits(firstdate) || !alldigits(lastdate))
	    goto illegal;
	if (strncmp(firstdate, lastdate, strlen(firstdate)) > 0)
	    goto illegal;
	return ((strncmp(datestamp, firstdate, strlen(firstdate)) >= 0) &&
		(strncmp(datestamp, lastdate , strlen(lastdate))  <= 0));
    }
    else {
	if (!alldigits(mydateexp))
	    goto illegal;
	if(match_exact == 1) {
	    return (g_str_equal(datestamp, mydateexp));
	}
	else {
	    return (g_str_has_prefix(datestamp, mydateexp));
	}
    }
illegal:
	error("Illegal datestamp expression %s", dateexp);
	/*NOTREACHED*/
}


int
match_level(
    const char *	levelexp,
    const char *	level)
{
    char *dash;
    long int low, hi, level_i;
    char mylevelexp[100];
    int match_exact;

    if(strlen(levelexp) >= 100 || strlen(levelexp) < 1) {
	error("Illegal level expression %s", levelexp);
	/*NOTREACHED*/
    }

    if (*levelexp == '=') {
	return strcmp(levelexp+1, level) == 0;
    }

    if(levelexp[0] == '^') {
	strncpy(mylevelexp, levelexp+1, strlen(levelexp)-1);
	mylevelexp[strlen(levelexp)-1] = '\0';
	if (strlen(levelexp) == 0) {
	    error("Illegal level expression %s", levelexp);
	    /*NOTREACHED*/
	}
    }
    else {
	strncpy(mylevelexp, levelexp, strlen(levelexp));
	mylevelexp[strlen(levelexp)] = '\0';
    }

    if(mylevelexp[strlen(mylevelexp)-1] == '$') {
	match_exact = 1;
	mylevelexp[strlen(mylevelexp)-1] = '\0';
    }
    else
	match_exact = 0;

    if((dash = strchr(mylevelexp,'-'))) {
	if(match_exact == 1) {
            goto illegal;
	}

        *dash = '\0';
        if (!alldigits(mylevelexp) || !alldigits(dash+1)) goto illegal;

        errno = 0;
        low = strtol(mylevelexp, (char **) NULL, 10);
        if (errno) goto illegal;
        hi = strtol(dash+1, (char **) NULL, 10);
        if (errno) goto illegal;
        level_i = strtol(level, (char **) NULL, 10);
        if (errno) goto illegal;

	return ((level_i >= low) && (level_i <= hi));
    }
    else {
	if (!alldigits(mylevelexp)) goto illegal;
	if(match_exact == 1) {
	    return (g_str_equal(level, mylevelexp));
	}
	else {
	    return (g_str_has_prefix(level, mylevelexp));
	}
    }
illegal:
    error("Illegal level expression %s", levelexp);
    /*NOTREACHED*/
}

static char *
make_template(
    const gboolean add_begin_and_end,
    const char *al_template,
    const char *barcode,
    const char *meta,
    const char *storage)
{
    char *template = g_malloc(1024);
    char *t = template;
    const char *at = al_template;

    if (al_template == NULL)
	at = "";
    if (add_begin_and_end)
	*t++ = '^';
    while (*at != '\0') {
	if (*at == '$') {
	    at++;
	    if (*at == 'c') {
		strcpy(t, get_config_name());
		t += strlen(get_config_name());
		at++;
	    } else if (*at == 'o') {
		strcpy(t, getconf_str(CNF_ORG));
		t += strlen(getconf_str(CNF_ORG));
		at++;
	    } else if (*at == 'b') {
		if (barcode) {
		    strcpy(t, barcode);
		    t += strlen(barcode);
		}
		at++;
	    } else if (*at == 'm') {
		if (meta) {
		    strcpy(t, meta);
		    t += strlen(meta);
		}
		at++;
	    } else if (*at == 'r') {
		if (storage) {
		    strcpy(t, storage);
		    t += strlen(storage);
		}
		at++;
	    } else if (*at == 's') {
		*t++ = '[';
		*t++ = '0';
		*t++ = '-';
		*t++ = '9';
		*t++ = ']';
		*t++ = '*';
		at++;
	    } else if (*at >= '0' && *at <= '9') {
		int num = *at - '0';
		at++;
		while (*at >= '0' && *at <= '9') {
		    num *= 10;
		    num += *at - '0';
		}
		if (*at == 's') {
		    int i;
		    for (i=0; i< num; i++) {
			*t++ = '[';
			*t++ = '0';
			*t++ = '-';
			*t++ = '9';
			*t++ = ']';
		    }
		}
		at++;
	    } else if (*at == '$') {
		/* two $, copy one */
		*t++ = *at++;
	    } else if (*at == '\0') {
		/* $ at end, copy it */
		*t++ = '$';
	    } else {
		/* Copy the $ and continue withthe next character */
		*t++ = *at;
	    }
	} else if (*at == '%') {
	    *t++ = '[';
	    *t++ = '0';
	    *t++ = '-';
	    *t++ = '9';
	    *t++ = ']';
	    at++;
	} else if (*at == '!') {
	    *t++ = '[';
	    *t++ = 'A';
	    *t++ = '-';
	    *t++ = 'Z';
	    *t++ = ']';
	    at++;
	} else if (*at == '\\') {
	    at++;
	    *t++ = *at++;
	} else {
	    *t++ = *at++;
	}
    }
    if (add_begin_and_end)
	*t++ = '$';
    *t = '\0';

    return template;

}

int
match_labelstr_template(
    const char *template,
    const char *label,
    const char *barcode,
    const char *meta,
    const char *storage)
{
    char *ztemplate = make_template(FALSE, template, barcode, meta, storage);
    int   result;

    result = match(ztemplate, label);
    g_free(ztemplate);
    return result;
}

int
match_labelstr(
    const labelstr_s *labelstr,
    const autolabel_t *autolabel,
    const char *label,
    const char *barcode,
    const char *meta,
    const char *storage)
{
    char *template;
    int   result;

    if (labelstr->match_autolabel) {
	template = make_template(TRUE, autolabel->template, barcode, meta, storage);
    } else {
	template = make_template(FALSE, labelstr->template, barcode, meta, storage);
    }
    result = match(template, label);
    g_free(template);
    return result;
}