/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (c) 2023 Todd C. Miller <Todd.Miller@sudo.ws>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, 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.
 */

#include <config.h>

#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>
#include <regex.h>

#include <sudo_compat.h>
#include <sudo_debug.h>
#include <sudo_util.h>
#include <sudo_gettext.h>

static char errbuf[1024];

/*
 * Parse a number between 0 and INT_MAX, handling escaped digits.
 * Returns the number on success or -1 on error.
 * Sets endp to the first non-matching character.
 */
static int
parse_num(const char *str, char **endp)
{
    debug_decl(check_pattern, SUDO_DEBUG_UTIL);
    const int lastval = INT_MAX / 10;
    const int remainder = INT_MAX % 10;
    int result = 0;
    char ch;

    while ((ch = *str++) != '\0') {
	if (ch == '\\' && isdigit((unsigned char)str[0]))
	    ch = *str++;
	else if (!isdigit((unsigned char)ch))
	    break;
	ch -= '0';
	if (result > lastval || (result == lastval && ch > remainder)) {
	    result = -1;
	    break;
	}
	result *= 10;
	result += ch;
    }
    *endp = (char *)(str - 1);

    debug_return_int(result);
}

/*
 * Check pattern for invalid repetition sequences.
 * This is implementation-specific behavior, not all regcomp(3) forbid them.
 * Glibc allows it but uses excessive memory for repeated '+' ops.
 */
static int
check_pattern(const char *pattern)
{
    debug_decl(check_pattern, SUDO_DEBUG_UTIL);
    const char *cp = pattern;
    int b1, b2 = 0;
    char ch, *ep, prev = '\0';

    while ((ch = *cp++) != '\0') {
	switch (ch) {
	case '\\':
	    if (*cp != '\0') {
		/* Skip escaped character. */
		cp++;
		prev = '\0';
		continue;
	    }
	    break;
	case '?':
	case '*':
	case '+':
	    if (prev == '?' || prev == '*' || prev == '+' || prev == '{' ) {
		/* Invalid repetition operator. */
		debug_return_int(REG_BADRPT);
	    }
	    break;
	case '{':
	    /*
	     * Try to match bound: {[0-9\\]*\?,[0-9\\]*}
	     * GNU libc supports escaped digits and commas.
	     */
	    b1 = parse_num(cp, &ep);
	    switch (ep[0]) {
	    case '\\':
		if (ep[1] != ',')
		    break;
		ep++;
		FALLTHROUGH;
	    case ',':
		cp = ep + 1;
		b2 = parse_num(cp, &ep);
		break;
	    }
	    cp = ep;
	    if (*cp == '}') {
		if (b1 < 0 || b1 > 255 || b2 < 0 || b2 > 255) {
		    /* Invalid bound value. */
		    debug_return_int(REG_BADBR);
		}
		if (prev == '?' || prev == '*' || prev == '+' || prev == '{' ) {
		    /* Invalid repetition operator. */
		    debug_return_int(REG_BADRPT);
		}
		/* Skip past '}', prev will be set to '{' below */
		cp++;
		break;
	    }
	    prev = '\0';
	    continue;
	}
	prev = ch;
    }

    debug_return_int(0);
}

/*
 * Wrapper around regcomp() that handles a regex starting with (?i).
 * Avoid using regex_t in the function args so we don't need to
 * include regex.h everywhere.
 */
bool
sudo_regex_compile_v1(void *v, const char *pattern, const char **errstr)
{
    int errcode, cflags = REG_EXTENDED|REG_NOSUB;
    regex_t *preg;
    char *copy = NULL;
    const char *cp;
    regex_t rebuf;
    debug_decl(regex_compile, SUDO_DEBUG_UTIL);

    /* Some callers just want to check the validity of the pattern. */
    preg = v ? v : &rebuf;

    /* Limit the length of regular expressions to avoid fuzzer issues. */
    if (strlen(pattern) > 1024) {
	*errstr = N_("regular expression too large");
	debug_return_bool(false);
    }

    /* Check for (?i) to enable case-insensitive matching. */
    cp = pattern[0] == '^' ? pattern + 1 : pattern;
    if (strncmp(cp, "(?i)", 4) == 0) {
	cflags |= REG_ICASE;
	copy = strdup(pattern + 4);
	if (copy == NULL) {
	    *errstr = N_("unable to allocate memory");
	    debug_return_bool(false);
	}
	if (pattern[0] == '^')
	    copy[0] = '^';
	pattern = copy;
    }

    errcode = check_pattern(pattern);
    if (errcode == 0)
	errcode = regcomp(preg, pattern, cflags);
    if (errcode == 0) {
	if (preg == &rebuf)
	    regfree(&rebuf);
    } else {
        regerror(errcode, preg, errbuf, sizeof(errbuf));
        *errstr = errbuf;
    }
    free(copy);

    debug_return_bool(errcode == 0);
}
