Blob Blame History Raw
/*
 * Copyright (c) 2010-2012 Zmanda, Inc.  All Rights Reserved.
 * Copyright (c) 2013-2016 Carbonite, Inc.  All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 *
 * Contact information: Carbonite Inc., 756 N Pastoria Ave
 * Sunnyvale, CA 94085, or: http://www.zmanda.com
 */

#include "amanda.h"
#include "testutils.h"
#include "match.h"

/*
 * Tests
 */

static gboolean
test_validate_regexp(void)
{
    gboolean ok = TRUE;
    struct {
	char *regexp;
	gboolean should_validate;
    } tests[] = {
	{ ".*", TRUE },
	{ "*", FALSE },
	{ "[abc", FALSE },
	{ "(abc", FALSE },
	{ "{1,}", FALSE },
	{ NULL, FALSE },
    }, *t;

    for (t = tests; t->regexp; t++) {
	char *validated_err = validate_regexp(t->regexp);
	if (!validated_err != !!t->should_validate) {
	    ok = FALSE;
	    if (t->should_validate) {
		g_fprintf(stderr, "should have validated regular expr %s: %s\n",
			t->regexp, validated_err);
	    } else {
		g_fprintf(stderr, "unexpectedly validated regular expr %s\n",
			t->regexp);
	    }
	}
    }

    return ok;
}

static gboolean
test_match(void)
{
    gboolean ok = TRUE;
    struct {
	char *expr, *str;
	gboolean should_match, should_match_no_newline;
    } tests[] = {
	/* literal, unanchored matching */
	{ "a", "a", TRUE, TRUE },
	{ "a", "A", FALSE, FALSE },
	{ "a", "ab", TRUE, TRUE },
	{ "a", "ba", TRUE, TRUE },
	{ "a", "bab", TRUE, TRUE },

	/* dot */
	{ ".", "", FALSE, FALSE },
	{ ".", "a", TRUE, TRUE },
	{ "..", "a", FALSE, FALSE },
	{ "..", "bc", TRUE, TRUE },

	/* brackets */
	{ "[abc]", "xbx", TRUE, TRUE },
	{ "[abc]", "xyz", FALSE, FALSE },
	{ "[^abc]", "cba", FALSE, FALSE },
	{ "[^abc]", "xyz", TRUE, TRUE },
	{ "[a-c]", "b", TRUE, TRUE },
	{ "[^a-c]", "-", TRUE, TRUE },
	{ "[1-9-]", "-", TRUE, TRUE },
	{ "[ab\\-cd]", "-", FALSE, FALSE }, /* NOTE! */

	/* anchors */
	{ "^xy", "xyz", TRUE, TRUE },
	{ "^xy", "wxyz", FALSE, FALSE },
	{ "yz$", "xyz", TRUE, TRUE },
	{ "yz$", "yza", FALSE, FALSE },
	{ "^123$", "123", TRUE, TRUE },
	{ "^123$", "0123", FALSE, FALSE },
	{ "^123$", "1234", FALSE, FALSE },

	/* capture groups */
	{ "([a-c])([x-y])", "pqaxyr", TRUE, TRUE },
	{ "([a-c])([x-y])", "paqrxy", FALSE, FALSE },
	{ "([a-c])/\\1", "a/b", FALSE, FALSE },
	{ "([a-c])/\\1", "c/c", TRUE, TRUE },

	/* * */
	{ ">[0-9]*<", "><", TRUE, TRUE },
	{ ">[0-9]*<", ">3<", TRUE, TRUE },
	{ ">[0-9]*<", ">34<", TRUE, TRUE },
	{ ">[0-9]*<", ">345<", TRUE, TRUE },
	{ ">[0-9]*<", ">x<", FALSE, FALSE },

	/* | */
	{ ":(abc|ABC);", ":abc;", TRUE, TRUE },
	{ ":(abc|ABC);", ":ABC;", TRUE, TRUE },
	{ ":(abc|ABC);", ":abcBC;", FALSE, FALSE },

	/* + */
	{ ">[0-9]+<", "><", FALSE, FALSE },
	{ ">[0-9]+<", ">3<", TRUE, TRUE },
	{ ">[0-9]+<", ">34<", TRUE, TRUE },
	{ ">[0-9]+<", ">345<", TRUE, TRUE },
	{ ">[0-9]+<", ">x<", FALSE, FALSE },

	/* { .. } */
	{ ">[0-9]{0,1}<", "><", TRUE, TRUE },
	{ ">[0-9]{0,1}<", ">9<", TRUE, TRUE },
	{ ">[0-9]{0,1}<", ">98<", FALSE, FALSE },
	{ ">[0-9]{2,3}<", "><", FALSE, FALSE },
	{ ">[0-9]{2,3}<", ">5<", FALSE, FALSE },
	{ ">[0-9]{2,3}<", ">55<", TRUE, TRUE },
	{ ">[0-9]{2,3}<", ">555<", TRUE, TRUE },
	{ ">[0-9]{2,3}<", ">5555<", FALSE, FALSE },

	/* quoting metacharacters */
	{ "\\\\", "\\", TRUE, TRUE },
	{ "\\,", ",", TRUE, TRUE },
	{ "\\[", "[", TRUE, TRUE },
	{ "\\*", "*", TRUE, TRUE },
	{ "\\?", "?", TRUE, TRUE },
	{ "\\+", "+", TRUE, TRUE },
	{ "\\.", ".", TRUE, TRUE },
	{ "\\|", "|", TRUE, TRUE },
	{ "\\^", "^", TRUE, TRUE },
	{ "\\$", "$", TRUE, TRUE },

	/* differences between match and match_no_newline */
	{ "x.y", "x\ny", FALSE, TRUE },
	{ "x[^yz]y", "x\ny", FALSE, TRUE },
	{ "^y", "x\ny", TRUE, FALSE },
	{ "x$", "x\ny", TRUE, FALSE },

	{ NULL, NULL, FALSE, FALSE },
    }, *t;

    for (t = tests; t->expr; t++) {
	gboolean matched = match(t->expr, t->str);
	if (!!matched != !!t->should_match) {
	    ok = FALSE;
	    if (t->should_match) {
		g_fprintf(stderr, "%s should have matched regular expr %s\n",
			t->str, t->expr);
	    } else {
		g_fprintf(stderr, "%s unexpectedly matched regular expr %s\n",
			t->str, t->expr);
	    }
	}

	matched = match_no_newline(t->expr, t->str);
	if (!!matched != !!t->should_match_no_newline) {
	    ok = FALSE;
	    if (t->should_match_no_newline) {
		g_fprintf(stderr, "%s should have matched (no_newline) regular expr %s\n",
			t->str, t->expr);
	    } else {
		g_fprintf(stderr, "%s unexpectedly matched (no_newline) regular expr %s\n",
			t->str, t->expr);
	    }
	}
    }

    return ok;
}

static gboolean
test_validate_glob(void)
{
    gboolean ok = TRUE;
    struct {
	char *glob;
	gboolean should_validate;
    } tests[] = {
	{ "foo.*", TRUE },
	{ "*.txt", TRUE },
	{ "x[abc]y", TRUE },
	{ "x[!abc]y", TRUE },
	{ "[abc", FALSE },
	{ "[!abc", FALSE },
	{ "??*", TRUE },
	{ "**?", TRUE }, /* legal, but weird */
	{ "foo\\", FALSE }, /* un-escaped \ is illegal */
	{ "foo\\\\", TRUE }, /* but escaped is OK */
	{ "(){}+.^$|", TRUE }, /* funny characters OK */
	{ "/usr/bin/*", TRUE }, /* filename seps are OK */
	{ NULL, FALSE },
    }, *t;

    for (t = tests; t->glob; t++) {
	char *validated_err = validate_glob(t->glob);
	if (!validated_err != !!t->should_validate) {
	    ok = FALSE;
	    if (t->should_validate) {
		g_fprintf(stderr, "should have validated glob %s: %s\n",
			t->glob, validated_err);
	    } else {
		g_fprintf(stderr, "unexpectedly validated glob %s\n",
			t->glob);
	    }
	}
    }

    return ok;
}

static gboolean
test_glob_to_regex(void)
{
    gboolean ok = TRUE;
    struct { char *glob, *regex; } tests[] = {
	{ "abc", "^abc$" },
	{ "*.txt", "^[^/]*\\.txt$" },
	{ "?.txt", "^[^/]\\.txt$" },
	{ "?*.txt", "^[^/][^/]*\\.txt$" },
	{ "foo.[tT][xX][tT]", "^foo\\.[tT][xX][tT]$" },
	{ "foo.[tT][!yY][tT]", "^foo\\.[tT][^yY][tT]$" },
	{ "foo\\\\", "^foo\\\\$" },
	{ "(){}+.^$|", "^\\(\\)\\{\\}\\+\\.\\^\\$\\|$" },
	{ "/usr/bin/*", "^/usr/bin/[^/]*$" },
	{ NULL, NULL },
    }, *t;

    for (t = tests; t->glob; t++) {
	char *regex = glob_to_regex(t->glob);
	if (!g_str_equal(regex, t->regex)) {
	    ok = FALSE;
	    g_fprintf(stderr, "glob_to_regex(\"%s\") returned \"%s\"; expected \"%s\"\n",
		    t->glob, regex, t->regex);
	}
    }

    return ok;
}

static gboolean
test_match_glob(void)
{
    gboolean ok = TRUE;
    struct {
	char *expr, *str;
	gboolean should_match;
    } tests[] = {
	/* literal, unanchored matching */
	{ "a", "a", TRUE },

	{ "abc", "abc", TRUE },
	{ "abc", "abcd", FALSE },
	{ "abc", "dabc", FALSE },
	{ "abc", "/usr/bin/abc", FALSE },

	{ "*.txt", "foo.txt", TRUE },
	{ "*.txt", ".txt", TRUE },
	{ "*.txt", "txt", FALSE },

	{ "?.txt", "X.txt", TRUE },
	{ "?.txt", ".txt", FALSE },
	{ "?.txt", "XY.txt", FALSE },

	{ "?*.txt", ".txt", FALSE },
	{ "?*.txt", "a.txt", TRUE },
	{ "?*.txt", "aa.txt", TRUE },
	{ "?*.txt", "aaa.txt", TRUE },

	{ "foo.[tT][xX][tT]", "foo.txt", TRUE },
	{ "foo.[tT][xX][tT]", "foo.TXt", TRUE },
	{ "foo.[tT][xX][tT]", "foo.TXT", TRUE },
	{ "foo.[tT][xX][tT]", "foo.TaT", FALSE },

	{ "foo.[tT][!yY][tT]", "foo.TXt", TRUE },
	{ "foo.[tT][!yY][tT]", "foo.TXT", TRUE },
	{ "foo.[tT][!yY][tT]", "foo.TyT", FALSE },

	{ "foo\\\\", "foo", FALSE },
	{ "foo\\\\", "foo\\", TRUE },
	{ "foo\\\\", "foo\\\\", FALSE },

	{ "(){}+.^$|", "(){}+.^$|", TRUE },

	{ "/usr/bin/*", "/usr/bin/tar", TRUE },
	{ "/usr/bin/*", "/usr/bin/local/tar", FALSE },
	{ "/usr/bin/*", "/usr/sbin/tar", FALSE },
	{ "/usr/bin/*", "/opt/usr/bin/tar", FALSE },

	{ "/usr?bin", "/usr/bin", FALSE },
	{ "/usr*bin", "/usr/bin", FALSE },

	{ NULL, NULL, FALSE },
    }, *t;

    for (t = tests; t->expr; t++) {
	gboolean matched = match_glob(t->expr, t->str);
	if (!!matched != !!t->should_match) {
	    ok = FALSE;
	    if (t->should_match) {
		g_fprintf(stderr, "%s should have matched glob %s\n",
			t->str, t->expr);
	    } else {
		g_fprintf(stderr, "%s unexpectedly matched glob %s\n",
			t->str, t->expr);
	    }
	}
    }

    return ok;
}

static gboolean
test_match_tar(void)
{
    gboolean ok = TRUE;
    struct {
	char *expr, *str;
	gboolean should_match;
    } tests[] = {
	/* literal, unanchored matching */
	{ "a", "a", TRUE },

	{ "abc", "abc", TRUE },
	{ "abc", "abcd", FALSE },
	{ "abc", "dabc", FALSE },
	{ "abc", "/usr/bin/abc", TRUE },

	{ "*.txt", "foo.txt", TRUE },
	{ "*.txt", ".txt", TRUE },
	{ "*.txt", "txt", FALSE },

	{ "?.txt", "X.txt", TRUE },
	{ "?.txt", ".txt", FALSE },
	{ "?.txt", "XY.txt", FALSE },

	{ "?*.txt", ".txt", FALSE },
	{ "?*.txt", "a.txt", TRUE },
	{ "?*.txt", "aa.txt", TRUE },
	{ "?*.txt", "aaa.txt", TRUE },

	{ "foo.[tT][xX][tT]", "foo.txt", TRUE },
	{ "foo.[tT][xX][tT]", "foo.TXt", TRUE },
	{ "foo.[tT][xX][tT]", "foo.TXT", TRUE },
	{ "foo.[tT][xX][tT]", "foo.TaT", FALSE },

	{ "foo.[tT][!yY][tT]", "foo.TXt", TRUE },
	{ "foo.[tT][!yY][tT]", "foo.TXT", TRUE },
	{ "foo.[tT][!yY][tT]", "foo.TyT", FALSE },

	{ "foo\\\\", "foo", FALSE },
	{ "foo\\\\", "foo\\", TRUE },
	{ "foo\\\\", "foo\\\\", FALSE },

	{ "(){}+.^$|", "(){}+.^$|", TRUE },

	{ "/usr/bin/*", "/usr/bin/tar", TRUE },
	{ "/usr/bin/*", "/usr/bin/local/tar", TRUE }, /* different from match_glob */
	{ "/usr/bin/*", "/usr/sbin/tar", FALSE },
	{ "/usr/bin/*", "/opt/usr/bin/tar", FALSE },

	{ "/usr?bin", "/usr/bin", FALSE },
	{ "/usr*bin", "/usr/bin", TRUE }, /* different from match_glob */

	/* examples from the amgtar manpage */
	{ "./temp-files", "./temp-files", TRUE },
	{ "./temp-files", "./temp-files/foo", TRUE },
	{ "./temp-files", "./temp-files/foo/bar", TRUE },
	{ "./temp-files", "./temp-files.bak", FALSE },
	{ "./temp-files", "./backup/temp-files", FALSE },

	{ "./temp-files/", "./temp-files", FALSE },
	{ "./temp-files/", "./temp-files/", TRUE },
	{ "./temp-files/", "./temp-files/foo", FALSE },
	{ "./temp-files/", "./temp-files/foo/bar", FALSE },
	{ "./temp-files/", "./temp-files.bak", FALSE },

	{ "/temp-files/", "./temp-files", FALSE },
	{ "/temp-files/", "./temp-files/", FALSE },
	{ "/temp-files/", "./temp-files/foo", FALSE },
	{ "/temp-files/", "./temp-files/foo/bar", FALSE },
	{ "/temp-files/", "./temp-files.bak", FALSE },

	{ "./temp-files/*", "./temp-files", FALSE },
	{ "./temp-files/*", "./temp-files/", TRUE },
	{ "./temp-files/*", "./temp-files/foo", TRUE },
	{ "./temp-files/*", "./temp-files/foo/bar", TRUE },

	{ "temp-files", "./my/temp-files", TRUE },
	{ "temp-files", "./my/temp-files/bar", TRUE },
	{ "temp-files", "./temp-files", TRUE },
	{ "temp-files", "./her-temp-files", FALSE },
	{ "temp-files", "./her/old-temp-files", FALSE },
	{ "temp-files", "./her/old-temp-files/bar", FALSE },

	{ "generated-*", "./my/generated-xyz", TRUE },
	{ "generated-*", "./my/generated-xyz/bar", TRUE },
	{ "generated-*", "./generated-xyz", TRUE },
	{ "generated-*", "./her-generated-xyz", FALSE },
	{ "generated-*", "./her/old-generated-xyz", FALSE },
	{ "generated-*", "./her/old-generated-xyz/bar", FALSE },

	{ "*.iso", "./my/amanda.iso", TRUE },
	{ "*.iso", "./amanda.iso", TRUE },

	{ "proxy/local/cache", "./usr/proxy/local/cache", TRUE },
	{ "proxy/local/cache", "./proxy/local/cache", TRUE },
	{ "proxy/local/cache", "./proxy/local/cache/7a", TRUE },

	{ NULL, NULL, FALSE },
    }, *t;

    for (t = tests; t->expr; t++) {
	gboolean matched = match_tar(t->expr, t->str);
	if (!!matched != !!t->should_match) {
	    ok = FALSE;
	    if (t->should_match) {
		g_fprintf(stderr, "%s should have matched tar %s\n",
			t->str, t->expr);
	    } else {
		g_fprintf(stderr, "%s unexpectedly matched tar %s\n",
			t->str, t->expr);
	    }
	}
    }

    return ok;
}

static gboolean
test_make_exact_host_expression(void)
{
    gboolean ok = TRUE;
    guint i, j;
    const char *test_strs[] = {
	"host",
	"host.org",
	"host.host.org",
	/* note that these will inter-match: */
	/*
	".host",
	".host.org",
	".host.host.org",
	"host.",
	"host.org.",
	"host.host.org.",
	*/
	"org",
	"^host",
	"host$",
	"^host$",
	"ho[s]t",
	"ho[!s]t",
	"ho\\st",
	"ho/st",
	"ho?t",
	"h*t",
	"h**t",
    };

    for (i = 0; i < G_N_ELEMENTS(test_strs); i++) {
	for (j = 0; j < G_N_ELEMENTS(test_strs); j++) {
	    char *expr = make_exact_host_expression(test_strs[i]);
	    gboolean matched = match_host(expr, test_strs[j]);
	    if (!!matched != !!(i == j)) {
		ok = FALSE;
		if (matched) {
		    g_fprintf(stderr, "expr %s for str %s unexpectedly matched %s\n",
			    expr, test_strs[i], test_strs[j]);
		} else {
		    g_fprintf(stderr, "expr %s for str %s should have matched %s\n",
			    expr, test_strs[i], test_strs[j]);
		}
	    }
	    amfree(expr);
	}
    }

    return ok;
}

static gboolean
test_match_host(void)
{
    gboolean ok = TRUE;
    struct {
	char *expr, *str;
	gboolean should_match;
    } tests[] = {
	/* from the amanda(8) manpage */
	{ "hosta", "hosta", TRUE },
	{ "hosta", "foo.hosta.org", TRUE },
	{ "hosta", "hoSTA.dOMAIna.ORG", TRUE },
	{ "hosta", "hostb", FALSE },
	{ "hOsta", "hosta", TRUE },
	{ "hOsta", "foo.hosta.org", TRUE },
	{ "hOsta", "hoSTA.dOMAIna.ORG", TRUE },
	{ "hOsta", "hostb", FALSE },

	{ "host", "host", TRUE },
	{ "host", "hosta", FALSE },

	{ "host?", "hosta", TRUE },
	{ "host?", "hostb", TRUE },
	{ "host?", "host", FALSE },
	{ "host?", "hostabc", FALSE },

	{ "ho*na", "hona", TRUE },
	{ "ho*na", "hoina", TRUE },
	{ "ho*na", "hoina.org", TRUE },
	{ "ho*na", "ns.hoina.org", TRUE },
	{ "ho*na", "ho.aina.org", FALSE },

	{ "ho**na", "hona", TRUE },
	{ "ho**na", "hoina", TRUE },
	{ "ho**na", "hoina.org", TRUE },
	{ "ho**na", "ns.hoina.org", TRUE },
	{ "ho**na", "ho.aina.org", TRUE },

	{ "^hosta", "hosta", TRUE },
	{ "^hosta", "hosta.org", TRUE },
	{ "^hosta", "hostabc", FALSE },
	{ "^hosta", "www.hosta", FALSE },
	{ "^hosta", "www.hosta.org", FALSE },

	{ "/opt", "opt", FALSE },

	{ ".hosta.", "hosta", TRUE },
	{ ".hosta.", "foo.hosta", TRUE },
	{ ".hosta.", "hosta.org", TRUE },
	{ ".hosta.", "foo.hosta.org", TRUE },
	{ "/hosta", "hosta", FALSE },
	{ "/hosta", "foo.hosta", FALSE },
	{ "/hosta", "hosta.org", FALSE },
	{ "/hosta", "foo.hosta.org", FALSE },

	{ ".opt.", "opt", TRUE },
	{ ".opt.", "www.opt", TRUE },
	{ ".opt.", "www.opt.com", TRUE },
	{ ".opt.", "opt.com", TRUE },

	/* other examples */
	{ "^hosta$", "hosta", TRUE },
	{ "^hosta$", "foo.hosta", FALSE },
	{ "^hosta$", "hosta.org", FALSE },
	{ "^hosta$", "foo.hosta.org", FALSE },

	{ "^lu.vis.ta$", "lu.vis.ta", TRUE },
	{ "^lu.vis.ta$", "lu-vis.ta", FALSE },
	{ "^lu.vis.ta$", "luvista", FALSE },
	{ "^lu.vis.ta$", "foo.lu.vis.ta", FALSE },
	{ "^lu.vis.ta$", "lu.vis.ta.org", FALSE },
	{ "^lu.vis.ta$", "foo.lu.vis.ta.org", FALSE },

	{ "mo[st]a", "mota", TRUE },
	{ "mo[st]a", "mosa", TRUE },
	{ "mo[st]a", "mosta", FALSE },
	{ "mo[!st]a", "mota", FALSE },
	{ "mo[!st]a", "moma", TRUE },
	{ "mo[!st]a", "momma", FALSE },

	{ "host[acd]", "hosta", TRUE },
	{ "host[acd]", "hostb", FALSE },
	{ "host[acd]", "hostc", TRUE },
	{ "host[!acd]", "hosta", FALSE },
	{ "host[!acd]", "hostb", TRUE },
	{ "host[!acd]", "hostc", FALSE },

	{ "toast", "www.toast.com", TRUE },
	{ ".toast", "www.toast.com", TRUE },
	{ "toast.", "www.toast.com", TRUE },
	{ ".toast.", "www.toast.com", TRUE },

	{ NULL, NULL, FALSE },
    }, *t;

    for (t = tests; t->expr; t++) {
	gboolean matched = match_host(t->expr, t->str);
	if (!!matched != !!t->should_match) {
	    ok = FALSE;
	    if (t->should_match) {
		g_fprintf(stderr, "%s should have matched host expr %s\n",
			t->str, t->expr);
	    } else {
		g_fprintf(stderr, "%s unexpectedly matched host expr %s\n",
			t->str, t->expr);
	    }
	}
    }

    return ok;
}

static gboolean
test_make_exact_disk_expression(void)
{
    gboolean ok = TRUE;
    guint i, j;
    const char *test_strs[] = {
	"/disk",
	"/disk/disk",
	"d[i]sk",
	"d**k",
	"d*k",
	"d?sk",
	"d.sk",
	"d[!pqr]sk",
	"^disk",
	"disk$",
	"^disk$",
	/* these intermatch due to some special-casing */
	/*
	"//windows/share",
	"\\\\windows\\share",
	*/
    };

    for (i = 0; i < G_N_ELEMENTS(test_strs); i++) {
	for (j = 0; j < G_N_ELEMENTS(test_strs); j++) {
	    char *expr = make_exact_disk_expression(test_strs[i]);
	    gboolean matched = match_disk(expr, test_strs[j]);
	    if (!!matched != !!(i == j)) {
		ok = FALSE;
		if (matched) {
		    g_fprintf(stderr, "expr %s for str %s unexpectedly matched %s\n",
			    expr, test_strs[i], test_strs[j]);
		} else {
		    g_fprintf(stderr, "expr %s for str %s should have matched %s\n",
			    expr, test_strs[i], test_strs[j]);
		}
	    }
	    amfree(expr);
	}
    }

    return ok;
}

static gboolean
test_match_disk(void)
{
    gboolean ok = TRUE;
    struct {
	char *expr, *str;
	gboolean should_match;
    } tests[] = {
	/* from the amanda(8) manpage */
	{ "sda*", "/dev/sda1", TRUE },
	{ "sda*", "/dev/sda2", TRUE },
	{ "sda*", "/dev/sdb2", FALSE },

	{ "opt", "opt", TRUE },
	{ "opt", "/opt", TRUE },
	{ "opt", "/opt/foo", TRUE },
	{ "opt", "opt/foo", TRUE },

	{ "/opt", "opt", TRUE },
	{ "/opt", "opt/", TRUE },
	{ "/opt", "/opt", TRUE },
	{ "/opt", "/opt/", TRUE },
	{ "/opt", "/local/opt/", TRUE },
	{ "/opt", "/opt/local/", TRUE },

	{ "opt/", "opt", TRUE },
	{ "opt/", "opt/", TRUE },
	{ "opt/", "/opt", TRUE },
	{ "opt/", "/opt/", TRUE },
	{ "opt/", "/local/opt/", TRUE },
	{ "opt/", "/opt/local/", TRUE },

	{ "/", "/", TRUE },
	{ "/", "/opt/local/", FALSE },

	{ "/usr$", "/", FALSE },
	{ "/usr$", "/usr", TRUE },
	{ "/usr$", "/usr/local", FALSE },

	{ "share", "\\\\windows1\\share", TRUE },
	{ "share", "\\\\windows2\\share", TRUE },
	{ "share", "//windows1/share", TRUE },
	{ "share", "//windows2/share", TRUE },

	{ "share*", "\\\\windows\\share1", TRUE },
	{ "share*", "\\\\windows\\share2", TRUE },
	{ "share*", "//windows/share3", TRUE },
	{ "share*", "//windows/share4", TRUE },

	{ "//windows/share", "//windows/share", TRUE },
	{ "//windows/share", "\\\\windows\\share", TRUE },
	{ "\\\\windows\\share", "//windows/share", FALSE },
	{ "\\\\windows\\share", "\\\\windows\\share", FALSE },
	{ "\\\\\\\\windows\\\\share", "//windows/share", FALSE },
	{ "\\\\\\\\windows\\\\share", "\\\\windows\\share", TRUE },

	/* other expressions */
	{ "^local", "/local", FALSE },
	{ "^/local", "/local", TRUE },
	{ "^local", "/local/vore", FALSE },
	{ "^/local", "/local/vore", TRUE },
	{ "^local", "/usr/local", FALSE },

	{ "local/bin", "/local/bin", TRUE },
	{ "local/bin", "/opt/local/bin", TRUE },
	{ "local/bin", "/local/bin/git", TRUE },

	{ "//windows/share", "//windows/share/files", TRUE },
	{ "//windows/share", "\\\\windows\\share\\files", TRUE },
	{ "\\\\windows\\share", "//windows/share/files", FALSE },
	{ "\\\\windows\\share", "\\\\windows\\share\\files", FALSE },

	/* longer expressions */
	{ "local/var", "/local/var", TRUE },
	{ "local/var", "/opt/local/var", TRUE },
	{ "local/var", "/local/var/lib", TRUE },
	{ "local/var", "/local/usr/var", FALSE },

	/* trailing slashes */
	{ "/usr/bin", "/usr/bin", TRUE },
	{ "/usr/bin", "/usr/bin/", TRUE },
	{ "/usr/bin/", "/usr/bin", TRUE },
	{ "/usr/bin/", "/usr/bin/", TRUE },
	{ "/usr/bin", "/usr/bin//", TRUE },
	{ "/usr/bin//", "/usr/bin", FALSE },
	{ "/usr/bin//", "/usr/bin//", TRUE },

	/* quoting '/' is weird: it doesn't work on the leading slash.  Note that
	 * the documentation does not specify how to quote metacharacters in a host
	 * or disk expression. */
	{ "/usr\\/bin", "/usr/bin", TRUE },
	{ "^/usr\\/bin$", "/usr/bin", TRUE },
	{ "\\/usr\\/bin", "/usr/bin", FALSE },
	{ "^\\/usr\\/bin$", "/usr/bin", TRUE },
	{ "/usr\\/bin\\/", "/usr/bin/", TRUE },
	{ "^/usr\\/bin\\/$", "/usr/bin/", TRUE },
	{ "\\/usr\\/bin\\/", "/usr/bin/", FALSE },
	{ "^\\/usr\\/bin\\/$", "/usr/bin/", TRUE },

	{ "oracle", "oracle", TRUE },
	{ "oracle", "/oracle", TRUE },
	{ "oracle", "oracle/", TRUE },
	{ "oracle", "/oracle/", TRUE },
	{ "/oracle", "oracle", TRUE },
	{ "/oracle", "/oracle", TRUE },
	{ "/oracle", "oracle/", TRUE },
	{ "/oracle", "/oracle/", TRUE },
	{ "^oracle", "oracle", TRUE },
	{ "^oracle", "/oracle", FALSE },
	{ "^oracle", "oracle/", TRUE },
	{ "^oracle", "/oracle/", FALSE },
	{ "^/oracle", "oracle", FALSE },
	{ "^/oracle", "/oracle", TRUE },
	{ "^/oracle", "oracle/", FALSE },
	{ "^/oracle", "/oracle/", TRUE },

	{ "oracle", "oracle", TRUE },
	{ "oracle", "/oracle", TRUE },
	{ "oracle", "oracle/", TRUE },
	{ "oracle", "/oracle/", TRUE },
	{ "oracle$", "oracle", TRUE },
	{ "oracle$", "/oracle", TRUE },
	{ "oracle$", "oracle/", FALSE },
	{ "oracle$", "/oracle/", FALSE },
	{ "oracle/$", "oracle", FALSE },
	{ "oracle/$", "/oracle", FALSE },
	{ "oracle/$", "oracle/", TRUE },
	{ "oracle/$", "/oracle/", TRUE },

	{ NULL, NULL, FALSE },
    }, *t;

    for (t = tests; t->expr; t++) {
	gboolean matched = match_disk(t->expr, t->str);
	if (!!matched != !!t->should_match) {
	    ok = FALSE;
	    if (t->should_match) {
		g_fprintf(stderr, "%s should have matched disk expr %s\n",
			t->str, t->expr);
	    } else {
		g_fprintf(stderr, "%s unexpectedly matched disk expr %s\n",
			t->str, t->expr);
	    }
	}
    }

    return ok;
}

static gboolean
test_match_datestamp(void)
{
    gboolean ok = TRUE;
    struct {
	char *expr, *str;
	gboolean should_match;
    } tests[] = {
	/* from the amanda(8) manpage */
	{ "20001212-14", "20001212", TRUE },
	{ "20001212-14", "20001212010203", TRUE },
	{ "20001212-14", "20001213", TRUE },
	{ "20001212-14", "20001213010203", TRUE },
	{ "20001212-14", "20001214", TRUE },
	{ "20001212-14", "20001215", FALSE },

	{ "20001212-4", "20001212", TRUE },
	{ "20001212-4", "20001212010203", TRUE },
	{ "20001212-4", "20001213", TRUE },
	{ "20001212-4", "20001213010203", TRUE },
	{ "20001212-4", "20001214", TRUE },
	{ "20001212-4", "20001215", FALSE },

	{ "20001212-214", "20001212", TRUE },
	{ "20001212-214", "20001212010203", TRUE },
	{ "20001212-214", "20001213", TRUE },
	{ "20001212-214", "20001213010203", TRUE },
	{ "20001212-214", "20001214", TRUE },
	{ "20001212-214", "20001215", FALSE },

	{ "20001212-24", "20001211", FALSE },
	{ "20001212-24", "20001214010203", TRUE },
	{ "20001212-24", "20001221010203", TRUE },
	{ "20001212-24", "20001224", TRUE },
	{ "20001212-24", "20001225", FALSE },

	{ "2000121", "20001209", FALSE },
	{ "2000121", "20001210", TRUE },
	{ "2000121", "20001210012345", TRUE },
	{ "2000121", "20001219", TRUE },
	{ "2000121", "20001219012345", TRUE },
	{ "2000121", "20001220", FALSE },

	{ "2", "19991231", FALSE },
	{ "2", "20000101", TRUE },
	{ "2", "20100419", TRUE },

	{ "^2", "19991231", FALSE },
	{ "^2", "20000101", TRUE },
	{ "^2", "20100419", TRUE },

	{ "2000-2010", "19991231235959", FALSE },
	{ "2000-2010", "20001010", TRUE },
	{ "2000-2010", "20101231", TRUE },
	{ "2000-2010", "20111010", FALSE },

	{ "200010$", "200010", TRUE }, /* but it's not a real datestamp */
	{ "200010$", "20001001", FALSE },
	{ "200010$", "20001001061500", FALSE },

	{ "20000615$", "20000615", TRUE },
	{ "20000615$", "20000615000000", FALSE },
	{ "20000615$", "20000615010306", FALSE },

	{ NULL, NULL, FALSE },
    }, *t;

    for (t = tests; t->expr; t++) {
	gboolean matched = match_datestamp(t->expr, t->str);
	if (!!matched != !!t->should_match) {
	    ok = FALSE;
	    if (t->should_match) {
		g_fprintf(stderr, "%s should have matched datestamp expr %s\n",
			t->str, t->expr);
	    } else {
		g_fprintf(stderr, "%s unexpectedly matched datestamp expr %s\n",
			t->str, t->expr);
	    }
	}
    }

    return ok;
}

static gboolean
test_match_level(void)
{
    gboolean ok = TRUE;
    struct {
	char *expr, *str;
	gboolean should_match;
    } tests[] = {
	/* exact matches, optionally ignoring "^" */
	{ "3$", "2", FALSE },
	{ "3$", "3", TRUE },
	{ "3$", "4", FALSE },
	{ "3$", "32", FALSE },

	{ "^3$", "2", FALSE },
	{ "^3$", "3", TRUE },
	{ "^3$", "4", FALSE },
	{ "^3$", "32", FALSE },

	/* prefix matches */
	{ "3", "2", FALSE },
	{ "3", "3", TRUE },
	{ "3", "4", FALSE },
	{ "3", "32", TRUE },

	/* ranges */
	{ "2-5", "1", FALSE },
	{ "2-5", "13", FALSE },
	{ "2-5", "23", FALSE },
	{ "2-5", "2", TRUE },
	{ "2-5", "4", TRUE },
	{ "2-5", "5", TRUE },
	{ "2-5", "53", FALSE },
	{ "2-5", "63", FALSE },
	{ "2-5", "6", FALSE },

	{ "9-15", "8", FALSE },
	{ "9-15", "19", FALSE },
	{ "9-15", "91", FALSE },
	{ "9-15", "9", TRUE },
	{ "9-15", "14", TRUE },
	{ "9-15", "15", TRUE },
	{ "9-15", "152", FALSE },
	{ "9-15", "16", FALSE },

	{ "19-21", "18", FALSE },
	{ "19-21", "19", TRUE },
	{ "19-21", "21", TRUE },
	{ "19-21", "22", FALSE },

	/* single range is the same as an exact match */
	{ "99-99", "98", FALSE },
	{ "99-99", "99", TRUE },
	{ "99-99", "100", FALSE },

	/* reversed range never matches */
	{ "21-19", "18", FALSE },
	{ "21-19", "19", FALSE },
	{ "21-19", "21", FALSE },
	{ "21-19", "22", FALSE },

	{ NULL, NULL, FALSE },
    }, *t;

    for (t = tests; t->expr; t++) {
	gboolean matched = match_level(t->expr, t->str);
	if (!!matched != !!t->should_match) {
	    ok = FALSE;
	    if (t->should_match) {
		g_fprintf(stderr, "%s should have matched level expr %s\n",
			t->str, t->expr);
	    } else {
		g_fprintf(stderr, "%s unexpectedly matched level expr %s\n",
			t->str, t->expr);
	    }
	}
    }

    return ok;
}


/*
 * Main driver
 */

int
main(int argc, char **argv)
{
    static TestUtilsTest tests[] = {
	TU_TEST(test_validate_regexp, 90),
	TU_TEST(test_match, 90),
	TU_TEST(test_validate_glob, 90),
	TU_TEST(test_glob_to_regex, 90),
	TU_TEST(test_match_glob, 90),
	TU_TEST(test_match_tar, 90),
	TU_TEST(test_make_exact_host_expression, 90),
	TU_TEST(test_match_host, 90),
	TU_TEST(test_make_exact_disk_expression, 90),
	TU_TEST(test_match_disk, 90),
	TU_TEST(test_match_datestamp, 90),
	TU_TEST(test_match_level, 90),
	TU_END()
    };

    glib_init();

    return testutils_run_tests(argc, argv, tests);
}