root/usr/src/common/smbsrv/smb_match.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved.
 * Copyright 2014 Nexenta Systems, Inc. All rights reserved.
 */

#if !defined(_KERNEL) && !defined(_FAKE_KERNEL)
#include <stdlib.h>
#include <string.h>
#else
#include <sys/types.h>
#include <sys/systm.h>
#include <sys/sunddi.h>
#endif
#include <smbsrv/string.h>
#include <smbsrv/smb.h>

/*
 * Maximum recursion depth for the wildcard match functions.
 * These functions may recurse when processing a '*'.
 */
#define SMB_MATCH_DEPTH_MAX     32

struct match_priv {
        int depth;
        boolean_t ci;
};

static int smb_match_private(const char *, const char *, struct match_priv *);

static const char smb_wildcards[] = "*?<>\"";

/*
 * Return B_TRUE if pattern contains wildcards
 */
boolean_t
smb_contains_wildcards(const char *pattern)
{

        return (strpbrk(pattern, smb_wildcards) != NULL);
}

/*
 * NT-compatible file name match function.  [MS-FSA 3.1.4.4]
 * Returns TRUE if there is a match.
 */
boolean_t
smb_match(const char *p, const char *s, boolean_t ci)
{
        struct match_priv priv;
        int rc;

        /*
         * Optimize common patterns that match everything:
         * ("*", "<\"*")  That second one is the converted
         * form of "*.*" after smb_convert_wildcards() does
         * its work on it for an old LM client. Note that a
         * plain "*.*" never gets this far.
         */
        if (p[0] == '*' && p[1] == '\0')
                return (B_TRUE);
        if (p[0] == '<' && p[1] == '\"' && p[2] == '*' && p[3] == '\0')
                return (B_TRUE);

        /*
         * Match string ".." as if "."  This is Windows behavior
         * (not mentioned in MS-FSA) that was determined using
         * the Samba masktest program.
         */
        if (s[0] == '.' && s[1] == '.' && s[2] == '\0')
                s++;

        /*
         * Optimize simple patterns (no wildcards)
         */
        if (NULL == strpbrk(p, smb_wildcards)) {
                if (ci)
                        rc = smb_strcasecmp(p, s, 0);
                else
                        rc = strcmp(p, s);
                return (rc == 0);
        }

        /*
         * Do real wildcard match.
         */
        priv.depth = 0;
        priv.ci = ci;
        rc = smb_match_private(p, s, &priv);
        return (rc == 1);
}

/*
 * Internal file name match function.  [MS-FSA 3.1.4.4]
 * This does the full expression evaluation.
 *
 * '*' matches zero of more of any characters.
 * '?' matches exactly one of any character.
 * '<' matches any string up through the last dot or EOS.
 * '>' matches any one char not a dot, dot at EOS, or EOS.
 * '"' matches a dot, or EOS.
 *
 * Returns:
 *  1   match
 *  0   no-match
 * -1   no-match, error (illseq, too many wildcards in pattern, ...)
 *
 * Note that both the pattern and the string are in multi-byte form.
 *
 * The implementation of this is quite tricky.  First note that it
 * can call itself recursively, though it limits the recursion depth.
 * Each switch case in the while loop can basically do one of three
 * things: (a) return "Yes, match", (b) return "not a match", or
 * continue processing the match pattern.  The cases for wildcards
 * that may match a variable number of characters ('*' and '<') do
 * recursive calls, looking for a match of the remaining pattern,
 * starting at the current and later positions in the string.
 */
static int
smb_match_private(const char *pat, const char *str, struct match_priv *priv)
{
        const char      *limit;
        char            pc;             /* current pattern char */
        int             rc;
        uint32_t        wcpat, wcstr;   /* current wchar in pat, str */
        int             nbpat, nbstr;   /* multi-byte length of it */

        if (priv->depth >= SMB_MATCH_DEPTH_MAX)
                return (-1);

        /*
         * Advance over one multi-byte char, used in cases like
         * '?' or '>' where "match one character" needs to be
         * interpreted as "match one multi-byte sequence".
         *
         * This macro needs to consume the semicolon following
         * each place it appears, so this is carefully written
         * as an if/else with a missing semicolon at the end.
         */
#define ADVANCE(str) \
        if ((nbstr = smb_mbtowc(NULL, str, MTS_MB_CHAR_MAX)) < 1) \
                return (-1); \
        else \
                str += nbstr    /* no ; */

        /*
         * We move pat forward in each switch case so that the
         * default case can move it by a whole multi-byte seq.
         */
        while ((pc = *pat) != '\0') {
                switch (pc) {

                case '?':       /* exactly one of any character */
                        pat++;
                        if (*str != '\0') {
                                ADVANCE(str);
                                continue;
                        }
                        /* EOS: no-match */
                        return (0);

                case '*':       /* zero or more of any characters */
                        pat++;
                        /* Optimize '*' at end of pattern. */
                        if (*pat == '\0')
                                return (1); /* match */
                        while (*str != '\0') {
                                priv->depth++;
                                rc = smb_match_private(pat, str, priv);
                                priv->depth--;
                                if (rc != 0)
                                        return (rc); /* match */
                                ADVANCE(str);
                        }
                        continue;

                case '<':       /* any string up through the last dot or EOS */
                        pat++;
                        if ((limit = strrchr(str, '.')) != NULL)
                                limit++;
                        while (*str != '\0' && str != limit) {
                                priv->depth++;
                                rc = smb_match_private(pat, str, priv);
                                priv->depth--;
                                if (rc != 0)
                                        return (rc); /* match */
                                ADVANCE(str);
                        }
                        continue;

                case '>':       /* anything not a dot, dot at EOS, or EOS */
                        pat++;
                        if (*str == '.') {
                                if (str[1] == '\0') {
                                        /* dot at EOS */
                                        str++;  /* ADVANCE over '.' */
                                        continue;
                                }
                                /* dot NOT at EOS: no-match */
                                return (0);
                        }
                        if (*str != '\0') {
                                /* something not a dot */
                                ADVANCE(str);
                                continue;
                        }
                        continue;

                case '\"':      /* dot, or EOS */
                        pat++;
                        if (*str == '.') {
                                str++;  /* ADVANCE over '.' */
                                continue;
                        }
                        if (*str == '\0') {
                                continue;
                        }
                        /* something else: no-match */
                        return (0);

                default:        /* not a wildcard */
                        nbpat = smb_mbtowc(&wcpat, pat, MTS_MB_CHAR_MAX);
                        nbstr = smb_mbtowc(&wcstr, str, MTS_MB_CHAR_MAX);
                        /* make sure we advance */
                        if (nbpat < 1 || nbstr < 1)
                                return (-1);
                        if (wcpat == wcstr) {
                                pat += nbpat;
                                str += nbstr;
                                continue;
                        }
                        if (priv->ci) {
                                wcpat = smb_tolower(wcpat);
                                wcstr = smb_tolower(wcstr);
                                if (wcpat == wcstr) {
                                        pat += nbpat;
                                        str += nbstr;
                                        continue;
                                }
                        }
                        return (0); /* no-match */
                }
        }
        return (*str == '\0');
}