Commit message: Fix for wrong fnmatch patttern

* dir.c (file_s_fnmatch): ensure that pattern does not contain a
  NUL character.  https://hackerone.com/reports/449617
CVE: CVE-2019-15845
CWE: NVD-CWE-noinfo
Vulnerability description: Ruby through 2.4.7, 2.5.x through 2.5.6, and 2.6.x through 2.6.4 mishandles path checking within File.fnmatch functions.

Project: ruby
File: dir.c

find_dirsep(const char *p, const char *pend, int flags, rb_encoding *enc)
{
    const int escape = !(flags & FNM_NOESCAPE);

    register char c;
    int open = 0;

    while ((c = *p++) != 0) {
	switch (c) {
	  case '[':
	    open = 1;
	    continue;
	  case ']':
	    open = 0;
	    continue;

	  case '{':
	    open = 1;
	    continue;
	  case '}':
	    open = 0;
	    continue;

	  case '/':
	    if (!open)
		return (char *)p-1;
	    continue;

	  case '\\':
	    if (escape && !(c = *p++))
		return (char *)p-1;
	    continue;
	}

	p = Next(p-1, pend, enc);
    }

    return (char *)p-1;
}

Project: ruby
File: dir.c

at_subpath(int fd, size_t baselen, const char *path)
{
#if USE_OPENDIR_AT
    if (fd != (int)AT_FDCWD && baselen > 0) {
	path += baselen;
	if (*path == '/') ++path;
    }
#endif
    return *path ? path : ".";
}

Project: ruby
File: dir.c

do_stat(int fd, size_t baselen, const char *path, struct stat *pst, int flags, rb_encoding *enc)
{
#if USE_OPENDIR_AT
    int ret = fstatat(fd, at_subpath(fd, baselen, path), pst, 0);
#else
    int ret = STAT(path, pst);
#endif
    if (ret < 0 && !to_be_ignored(errno))
	sys_warning(path, enc);

    return ret;
}

Project: ruby
File: dir.c

chdir_restore(VALUE v)
{
    struct chdir_data *args = (void *)v;
    if (args->done) {
	chdir_blocking--;
	if (chdir_blocking == 0)
	    chdir_thread = Qnil;
	dir_chdir(args->old_path);
    }
    return Qnil;
}

Project: ruby
File: dir.c

push_caller(const char *path, VALUE val, void *enc)
{
    struct push_glob_args *arg = (struct push_glob_args *)val;
    struct glob_pattern *list;
    int status;

    list = glob_make_pattern(path, path + strlen(path), arg->flags, enc);
    if (!list) {
	return -1;
    }
    status = glob_helper(arg->fd, arg->path, arg->baselen, arg->namelen, arg->dirsep,
			 arg->pathtype, &list, &list + 1, arg->flags, arg->funcs,
			 arg->arg, enc);
    glob_free_pattern(list);
    return status;
}

Project: ruby
File: dir.c

join_path_from_pattern(struct glob_pattern **beg)
{
    struct glob_pattern *p;
    char *path = NULL;
    size_t path_len = 0;

    for (p = *beg; p; p = p->next) {
	const char *str;
	switch (p->type) {
	  case RECURSIVE:
	    str = "**";
	    break;
	  case MATCH_DIR:
	    /* append last slash */
	    str = "";
	    break;
	  default:
	    str = p->str;
	    if (!str) continue;
	}
	if (!path) {
	    path_len = strlen(str);
	    path = GLOB_ALLOC_N(char, path_len + 1);
	    memcpy(path, str, path_len);
	    path[path_len] = '\0';
        }
        else {
	    size_t len = strlen(str);
	    char *tmp;
	    tmp = GLOB_REALLOC(path, path_len + len + 2);
	    if (tmp) {
		path = tmp;
		path[path_len++] = '/';
		memcpy(path + path_len, str, len);
		path_len += len;
		path[path_len] = '\0';
	    }
	}
    }
    return path;
}

Project: ruby
File: dir.c

push_glob(VALUE ary, VALUE str, VALUE base, int flags)
{
    struct glob_args args;
    int fd;
    rb_encoding *enc = rb_enc_get(str);

#if defined _WIN32 || defined __APPLE__
    str = rb_str_encode_ospath(str);
#endif
    if (rb_enc_to_index(enc) == ENCINDEX_US_ASCII)
	enc = rb_filesystem_encoding();
    if (rb_enc_to_index(enc) == ENCINDEX_US_ASCII)
	enc = rb_ascii8bit_encoding();
    flags |= GLOB_VERBOSE;
    args.func = push_pattern;
    args.value = ary;
    args.enc = enc;
    args.base = 0;
    fd = AT_FDCWD;
    if (!NIL_P(base)) {
	if (!RB_TYPE_P(base, T_STRING) || !rb_enc_check(str, base)) {
	    struct dir_data *dirp = DATA_PTR(base);
	    if (!dirp->dir) dir_closed();
#ifdef HAVE_DIRFD
	    if ((fd = dirfd(dirp->dir)) == -1)
		rb_sys_fail_path(dir_inspect(base));
#endif
	    base = dirp->path;
	}
	args.base = RSTRING_PTR(base);
    }
#if defined _WIN32 || defined __APPLE__
    enc = rb_utf8_encoding();
#endif

    return ruby_glob0(RSTRING_PTR(str), fd, args.base, flags, &rb_glob_funcs,
		      (VALUE)&args, enc);
}

Project: ruby
File: dir.c

do_opendir(const int basefd, size_t baselen, const char *path, int flags, rb_encoding *enc,
	   ruby_glob_errfunc *errfunc, VALUE arg, int *status)
{
    DIR *dirp;
#ifdef _WIN32
    VALUE tmp = 0;
    if (!fundamental_encoding_p(enc)) {
	tmp = rb_enc_str_new(path, strlen(path), enc);
	tmp = rb_str_encode_ospath(tmp);
	path = RSTRING_PTR(tmp);
    }
#endif
    dirp = opendir_at(basefd, at_subpath(basefd, baselen, path));
    if (!dirp) {
	int e = errno;

	*status = 0;
	if (!to_be_ignored(e)) {
	    if (errfunc) {
		*status = (*errfunc)(path, arg, enc, e);
	    }
	    else {
		sys_warning(path, enc);
	    }
	}
    }
#ifdef _WIN32
    if (tmp) rb_str_resize(tmp, 0); /* GC guard */
#endif

    return dirp;
}

Project: ruby
File: dir.c

do_lstat(int fd, size_t baselen, const char *path, struct stat *pst, int flags, rb_encoding *enc)
{
#if USE_OPENDIR_AT
    int ret = fstatat(fd, at_subpath(fd, baselen, path), pst, AT_SYMLINK_NOFOLLOW);
#else
    int ret = lstat(path, pst);
#endif
    if (ret < 0 && !to_be_ignored(errno))
	sys_warning(path, enc);

    return ret;
}

Project: ruby
File: dir.c

dirent_match_brace(const char *pattern, VALUE val, void *enc)
{
    struct dirent_brace_args *arg = (struct dirent_brace_args *)val;

    return dirent_match(pattern, enc, arg->name, arg->dp, arg->flags);
}

Project: ruby
File: dir.c

rb_push_glob(VALUE str, VALUE base, int flags) /* '\0' is delimiter */
{
    VALUE ary;
    int status;

    /* can contain null bytes as separators */
    if (!RB_TYPE_P(str, T_STRING)) {
	FilePathValue(str);
    }
    else if (!rb_str_to_cstr(str)) {
        rb_raise(rb_eArgError, "nul-separated glob pattern is deprecated");
    }
    else {
	rb_check_safe_obj(str);
	rb_enc_check(str, rb_enc_from_encoding(rb_usascii_encoding()));
    }
    ary = rb_ary_new();

    status = push_glob(ary, str, base, flags);
    if (status) GLOB_JUMP_TAG(status);

    return ary;
}

Project: ruby
File: dir.c

glob_helper(
    int fd,
    const char *path,
    size_t baselen,
    size_t namelen,
    int dirsep, /* '/' should be placed before appending child entry's name to 'path'. */
    rb_pathtype_t pathtype, /* type of 'path' */
    struct glob_pattern **beg,
    struct glob_pattern **end,
    int flags,
    const ruby_glob_funcs_t *funcs,
    VALUE arg,
    rb_encoding *enc)
{
    struct stat st;
    int status = 0;
    struct glob_pattern **cur, **new_beg, **new_end;
    int plain = 0, brace = 0, magical = 0, recursive = 0, match_all = 0, match_dir = 0;
    int escape = !(flags & FNM_NOESCAPE);
    size_t pathlen = baselen + namelen;

    for (cur = beg; cur < end; ++cur) {
	struct glob_pattern *p = *cur;
	if (p->type == RECURSIVE) {
	    recursive = 1;
	    p = p->next;
	}
	switch (p->type) {
	  case PLAIN:
	    plain = 1;
	    break;
	  case ALPHA:
#if USE_NAME_ON_FS == USE_NAME_ON_FS_REAL_BASENAME
	    plain = 1;
#else
	    magical = 1;
#endif
	    break;
	  case BRACE:
	    if (!recursive) {
		brace = 1;
	    }
	    break;
	  case MAGICAL:
	    magical = 2;
	    break;
	  case MATCH_ALL:
	    match_all = 1;
	    break;
	  case MATCH_DIR:
	    match_dir = 1;
	    break;
	  case RECURSIVE:
	    rb_bug("continuous RECURSIVEs");
	}
    }

    if (brace) {
	struct push_glob_args args;
	char* brace_path = join_path_from_pattern(beg);
	if (!brace_path) return -1;
	args.fd = fd;
	args.path = path;
	args.baselen = baselen;
	args.namelen = namelen;
	args.dirsep = dirsep;
	args.pathtype = pathtype;
	args.flags = flags;
	args.funcs = funcs;
	args.arg = arg;
	status = ruby_brace_expand(brace_path, flags, push_caller, (VALUE)&args, enc, Qfalse);
	GLOB_FREE(brace_path);
	return status;
    }

    if (*path) {
	if (match_all && pathtype == path_unknown) {
	    if (do_lstat(fd, baselen, path, &st, flags, enc) == 0) {
		pathtype = IFTODT(st.st_mode);
	    }
	    else {
		pathtype = path_noent;
	    }
	}
	if (match_dir && (pathtype == path_unknown || pathtype == path_symlink)) {
	    if (do_stat(fd, baselen, path, &st, flags, enc) == 0) {
		pathtype = IFTODT(st.st_mode);
	    }
	    else {
		pathtype = path_noent;
	    }
	}
	if (match_all && pathtype > path_noent) {
	    const char *subpath = path + baselen + (baselen && path[baselen] == '/');
	    status = glob_call_func(funcs->match, subpath, arg, enc);
	    if (status) return status;
	}
	if (match_dir && pathtype == path_directory) {
	    int seplen = (baselen && path[baselen] == '/');
	    const char *subpath = path + baselen + seplen;
	    char *tmp = join_path(subpath, namelen - seplen, dirsep, "", 0);
	    if (!tmp) return -1;
	    status = glob_call_func(funcs->match, tmp, arg, enc);
	    GLOB_FREE(tmp);
	    if (status) return status;
	}
    }

    if (pathtype == path_noent) return 0;

    if (magical || recursive) {
	struct dirent *dp;
	DIR *dirp;
# if USE_NAME_ON_FS == USE_NAME_ON_FS_BY_FNMATCH
	char *plainname = 0;
# endif
	IF_NORMALIZE_UTF8PATH(int norm_p);
# if USE_NAME_ON_FS == USE_NAME_ON_FS_BY_FNMATCH
	if (cur + 1 == end && (*cur)->type <= ALPHA) {
	    plainname = join_path(path, pathlen, dirsep, (*cur)->str, strlen((*cur)->str));
	    if (!plainname) return -1;
	    dirp = do_opendir(fd, basename, plainname, flags, enc, funcs->error, arg, &status);
	    GLOB_FREE(plainname);
	}
	else
# else
	    ;
# endif
	dirp = do_opendir(fd, baselen, path, flags, enc, funcs->error, arg, &status);
	if (dirp == NULL) {
# if FNM_SYSCASE || NORMALIZE_UTF8PATH
	    if ((magical < 2) && !recursive && (errno == EACCES)) {
		/* no read permission, fallback */
		goto literally;
	    }
# endif
	    return status;
	}
	IF_NORMALIZE_UTF8PATH(norm_p = need_normalization(dirp, *path ? path : "."));

# if NORMALIZE_UTF8PATH
	if (!(norm_p || magical || recursive)) {
	    closedir(dirp);
	    goto literally;
	}
# endif
# ifdef HAVE_GETATTRLIST
	if (is_case_sensitive(dirp, path) == 0)
	    flags |= FNM_CASEFOLD;
# endif
	while ((dp = READDIR(dirp, enc)) != NULL) {
	    char *buf;
	    rb_pathtype_t new_pathtype = path_unknown;
	    const char *name;
	    size_t namlen;
	    int dotfile = 0;
	    IF_NORMALIZE_UTF8PATH(VALUE utf8str = Qnil);

	    name = dp->d_name;
	    namlen = NAMLEN(dp);
	    if (recursive && name[0] == '.') {
		++dotfile;
		if (namlen == 1) {
		    /* unless DOTMATCH, skip current directories not to recurse infinitely */
		    if (!(flags & FNM_DOTMATCH)) continue;
		    ++dotfile;
		    new_pathtype = path_directory; /* force to skip stat/lstat */
		}
		else if (namlen == 2 && name[1] == '.') {
		    /* always skip parent directories not to recurse infinitely */
		    continue;
		}
	    }

# if NORMALIZE_UTF8PATH
	    if (norm_p && has_nonascii(name, namlen)) {
		if (!NIL_P(utf8str = rb_str_normalize_ospath(name, namlen))) {
		    RSTRING_GETMEM(utf8str, name, namlen);
		}
	    }
# endif
	    buf = join_path(path, pathlen, dirsep, name, namlen);
	    IF_NORMALIZE_UTF8PATH(if (!NIL_P(utf8str)) rb_str_resize(utf8str, 0));
	    if (!buf) {
		status = -1;
		break;
	    }
	    name = buf + pathlen + (dirsep != 0);
#ifdef DT_UNKNOWN
	    if (dp->d_type != DT_UNKNOWN) {
		/* Got it. We need no more lstat. */
		new_pathtype = dp->d_type;
	    }
#endif
	    if (recursive && dotfile < ((flags & FNM_DOTMATCH) ? 2 : 1) &&
		new_pathtype == path_unknown) {
		/* RECURSIVE never match dot files unless FNM_DOTMATCH is set */
		if (do_lstat(fd, baselen, buf, &st, flags, enc) == 0)
		    new_pathtype = IFTODT(st.st_mode);
		else
		    new_pathtype = path_noent;
	    }

	    new_beg = new_end = GLOB_ALLOC_N(struct glob_pattern *, (end - beg) * 2);
	    if (!new_beg) {
		GLOB_FREE(buf);
		status = -1;
		break;
	    }

	    for (cur = beg; cur < end; ++cur) {
		struct glob_pattern *p = *cur;
		struct dirent_brace_args args;
		if (p->type == RECURSIVE) {
		    if (new_pathtype == path_directory || /* not symlink but real directory */
			new_pathtype == path_exist) {
			if (dotfile < ((flags & FNM_DOTMATCH) ? 2 : 1))
			    *new_end++ = p; /* append recursive pattern */
		    }
		    p = p->next; /* 0 times recursion */
		}
		switch (p->type) {
		  case BRACE:
		    args.name = name;
		    args.dp = dp;
		    args.flags = flags;
		    if (ruby_brace_expand(p->str, flags, dirent_match_brace,
					  (VALUE)&args, enc, Qfalse) > 0)
			*new_end++ = p->next;
		    break;
		  case ALPHA:
# if USE_NAME_ON_FS == USE_NAME_ON_FS_BY_FNMATCH
		    if (plainname) {
			*new_end++ = p->next;
			break;
		    }
# endif
		  case PLAIN:
		  case MAGICAL:
		    if (dirent_match(p->str, enc, name, dp, flags))
			*new_end++ = p->next;
		  default:
		    break;
		}
	    }

	    status = glob_helper(fd, buf, baselen, name - buf - baselen + namlen, 1,
				 new_pathtype, new_beg, new_end,
				 flags, funcs, arg, enc);
	    GLOB_FREE(buf);
	    GLOB_FREE(new_beg);
	    if (status) break;
	}

	closedir(dirp);
    }
    else if (plain) {
	struct glob_pattern **copy_beg, **copy_end, **cur2;

# if FNM_SYSCASE || NORMALIZE_UTF8PATH
      literally:
# endif
	copy_beg = copy_end = GLOB_ALLOC_N(struct glob_pattern *, end - beg);
	if (!copy_beg) return -1;
	for (cur = beg; cur < end; ++cur)
	    *copy_end++ = (*cur)->type <= ALPHA ? *cur : 0;

	for (cur = copy_beg; cur < copy_end; ++cur) {
	    if (*cur) {
		rb_pathtype_t new_pathtype = path_unknown;
		char *buf;
		char *name;
		size_t len = strlen((*cur)->str) + 1;
		name = GLOB_ALLOC_N(char, len);
		if (!name) {
		    status = -1;
		    break;
		}
		memcpy(name, (*cur)->str, len);
		if (escape)
		    len = remove_backslashes(name, name+len-1, enc) - name;

		new_beg = new_end = GLOB_ALLOC_N(struct glob_pattern *, end - beg);
		if (!new_beg) {
		    GLOB_FREE(name);
		    status = -1;
		    break;
		}
		*new_end++ = (*cur)->next;
		for (cur2 = cur + 1; cur2 < copy_end; ++cur2) {
		    if (*cur2 && fnmatch((*cur2)->str, enc, name, flags) == 0) {
			*new_end++ = (*cur2)->next;
			*cur2 = 0;
		    }
		}

		buf = join_path(path, pathlen, dirsep, name, len);
		GLOB_FREE(name);
		if (!buf) {
		    GLOB_FREE(new_beg);
		    status = -1;
		    break;
		}
#if USE_NAME_ON_FS == USE_NAME_ON_FS_REAL_BASENAME
		if ((*cur)->type == ALPHA) {
		    buf = replace_real_basename(buf, pathlen + (dirsep != 0), enc,
						IF_NORMALIZE_UTF8PATH(1)+0,
						flags, &new_pathtype);
		    if (!buf) break;
		}
#endif
		status = glob_helper(fd, buf, baselen,
				     namelen + strlen(buf + pathlen), 1,
				     new_pathtype, new_beg, new_end,
				     flags, funcs, arg, enc);
		GLOB_FREE(buf);
		GLOB_FREE(new_beg);
		if (status) break;
	    }
	}

	GLOB_FREE(copy_beg);
    }

    return status;
}

Project: ruby
File: dir.c

file_s_fnmatch(int argc, VALUE *argv, VALUE obj)
{
    VALUE pattern, path;
    VALUE rflags;
    int flags;

    if (rb_scan_args(argc, argv, "21", &pattern, &path, &rflags) == 3)
	flags = NUM2INT(rflags);
    else
	flags = 0;

    StringValueCStr(pattern);
    FilePathStringValue(path);

    if (flags & FNM_EXTGLOB) {
	struct brace_args args;

	args.value = path;
	args.flags = flags;
	if (ruby_brace_expand(RSTRING_PTR(pattern), flags, fnmatch_brace,
			      (VALUE)&args, rb_enc_get(pattern), pattern) > 0)
	    return Qtrue;
    }
    else {
	rb_encoding *enc = rb_enc_compatible(pattern, path);
	if (!enc) return Qfalse;
	if (fnmatch(RSTRING_PTR(pattern), enc, RSTRING_PTR(path), flags) == 0)
	    return Qtrue;
    }
    RB_GC_GUARD(pattern);

    return Qfalse;
}

Project: ruby
File: dir.c

dir_s_chdir(int argc, VALUE *argv, VALUE obj)
{
    VALUE path = Qnil;

    if (rb_check_arity(argc, 0, 1) == 1) {
        path = rb_str_encode_ospath(rb_get_path(argv[0]));
    }
    else {
	const char *dist = getenv("HOME");
	if (!dist) {
	    dist = getenv("LOGDIR");
	    if (!dist) rb_raise(rb_eArgError, "HOME/LOGDIR not set");
	}
	path = rb_str_new2(dist);
    }

    if (chdir_blocking > 0) {
	if (!rb_block_given_p() || rb_thread_current() != chdir_thread)
	    rb_warn("conflicting chdir during another chdir block");
    }

    if (rb_block_given_p()) {
	struct chdir_data args;

	args.old_path = rb_str_encode_ospath(rb_dir_getwd());
	args.new_path = path;
	args.done = FALSE;
	return rb_ensure(chdir_yield, (VALUE)&args, chdir_restore, (VALUE)&args);
    }
    else {
	char *p = RSTRING_PTR(path);
	int r = (int)(VALUE)rb_thread_call_without_gvl(nogvl_chdir, p,
							RUBY_UBF_IO, 0);
	if (r < 0)
	    rb_sys_fail_path(path);
    }

    return INT2FIX(0);
}

Project: ruby
File: dir.c

dir_open_dir(int argc, VALUE *argv)
{
    VALUE dir = rb_funcallv_kw(rb_cDir, rb_intern("open"), argc, argv, RB_PASS_CALLED_KEYWORDS);

    rb_check_typeddata(dir, &dir_data_type);
    return dir;
}

Project: ruby
File: dir.c

dir_each_entry(VALUE dir, VALUE (*each)(VALUE, VALUE), VALUE arg, int children_only)
{
    struct dir_data *dirp;
    struct dirent *dp;
    IF_NORMALIZE_UTF8PATH(int norm_p);

    GetDIR(dir, dirp);
    rewinddir(dirp->dir);
    IF_NORMALIZE_UTF8PATH(norm_p = need_normalization(dirp->dir, RSTRING_PTR(dirp->path)));
    while ((dp = READDIR(dirp->dir, dirp->enc)) != NULL) {
	const char *name = dp->d_name;
	size_t namlen = NAMLEN(dp);
	VALUE path;

	if (children_only && name[0] == '.') {
	    if (namlen == 1) continue; /* current directory */
	    if (namlen == 2 && name[1] == '.') continue; /* parent directory */
	}
#if NORMALIZE_UTF8PATH
	if (norm_p && has_nonascii(name, namlen) &&
	    !NIL_P(path = rb_str_normalize_ospath(name, namlen))) {
	    path = rb_external_str_with_enc(path, dirp->enc);
	}
	else
#endif
	path = rb_external_str_new_with_enc(name, namlen, dirp->enc);
	(*each)(arg, path);
    }
    return dir;
}

Project: ruby
File: dir.c

push_glob0_caller(const char *path, VALUE val, void *enc)
{
    struct push_glob0_args *arg = (struct push_glob0_args *)val;
    return ruby_glob0(path, arg->fd, arg->base, arg->flags, arg->funcs, arg->arg, enc);
}

Project: ruby
File: dir.c

chdir_yield(VALUE v)
{
    struct chdir_data *args = (void *)v;
    dir_chdir(args->new_path);
    args->done = TRUE;
    chdir_blocking++;
    if (chdir_thread == Qnil)
	chdir_thread = rb_thread_current();
    return rb_yield(args->new_path);
}

Project: ruby
File: dir.c

ruby_glob0(const char *path, int fd, const char *base, int flags,
	   const ruby_glob_funcs_t *funcs, VALUE arg,
	   rb_encoding *enc)
{
    struct glob_pattern *list;
    const char *root, *start;
    char *buf;
    size_t n, baselen = 0;
    int status, dirsep = FALSE;

    start = root = path;

    if (*root == '{') {
        struct push_glob0_args args;
        args.fd = fd;
        args.base = base;
        args.flags = flags;
        args.funcs = funcs;
        args.arg = arg;
        return ruby_brace_expand(path, flags, push_glob0_caller, (VALUE)&args, enc, Qfalse);
    }

    flags |= FNM_SYSCASE;
#if defined DOSISH
    root = rb_enc_path_skip_prefix(root, root + strlen(root), enc);
#endif

    if (*root == '/') root++;

    n = root - start;
    if (!n && base) {
	n = strlen(base);
	baselen = n;
	start = base;
	dirsep = TRUE;
    }
    buf = GLOB_ALLOC_N(char, n + 1);
    if (!buf) return -1;
    MEMCPY(buf, start, char, n);
    buf[n] = '\0';

    list = glob_make_pattern(root, root + strlen(root), flags, enc);
    if (!list) {
	GLOB_FREE(buf);
	return -1;
    }
    status = glob_helper(fd, buf, baselen, n-baselen, dirsep,
			 path_unknown, &list, &list + 1,
			 flags, funcs, arg, enc);
    glob_free_pattern(list);
    GLOB_FREE(buf);

    return status;
}

Project: ruby
File: dir.c

has_magic(const char *p, const char *pend, int flags, rb_encoding *enc)
{
    const int escape = !(flags & FNM_NOESCAPE);
    int hasalpha = 0;
    int hasmagical = 0;

    register char c;

    while (p < pend && (c = *p++) != 0) {
	switch (c) {
	  case '{':
	    return BRACE;

	  case '*':
	  case '?':
	  case '[':
	    hasmagical = 1;
	    break;

	  case '\\':
	    if (escape && p++ >= pend)
		continue;
	    break;

#ifdef _WIN32
	  case '.':
	    break;

	  case '~':
	    hasalpha = 1;
	    break;
#endif
	  default:
	    if (IS_WIN32 || ISALPHA(c)) {
		hasalpha = 1;
	    }
	    break;
	}

	p = Next(p-1, pend, enc);
    }

    return hasmagical ? MAGICAL : hasalpha ? ALPHA : PLAIN;
}