mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
The numextra path in wild1() (a '?' immediately after a '*') advanced the data
pointer by one byte per '?' and copied one byte per '?' in both capture-fill
sites, so on multibyte data it split a UTF-8 character across captures, e.g.
wild('*?x','éx') gave %0='\xC3' %1='\xA9' instead of %0='' %1='é'. (The
standalone '?' was already character-aware; only this post-'*' path was not.)
Make the three numextra sites character-oriented: the do-while '?'-skip advances
a whole character (wild_char_len); both capture-fill loops copy a whole character
(new wild_capture_char helper); the trailing '*' span is computed by walking back
numextra whole characters from the anchor (new wild_step_back helper) rather than
dstr - numextra bytes. datapos becomes const (read-only cursor).
wild_test.cpp extended with 8 '*?'-adjacency cases (ASCII + UTF-8); 28/28 pass,
ASCII captures unchanged. Smoke 1254/1254.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
689 lines
19 KiB
C++
689 lines
19 KiB
C++
/*! \file wild.cpp
|
|
* \brief Wildcard routines.
|
|
*
|
|
* Written by T. Alexander Popiel, 24 June 1993
|
|
* Last modified by T. Alexander Popiel, 19 August 1993
|
|
*
|
|
* Thanks go to Andrew Molitor for debugging
|
|
* Thanks also go to Rich $alz for code to benchmark against
|
|
*
|
|
* Copyright (c) 1993 by T. Alexander Popiel
|
|
* This code is hereby placed under GNU copyleft,
|
|
* see copyright.h for details.
|
|
*/
|
|
|
|
#include "copyright.h"
|
|
#include "autoconf.h"
|
|
#include "config.h"
|
|
#include "externs.h"
|
|
|
|
// The pattern is pre-lowercased by the caller (mux_strlwr, Unicode-aware) at the
|
|
// entry points. Both matchers — quick_wild_impl() (non-capturing) and wild1()
|
|
// (capturing) — fold the data side per character too, via wild_lit_eq() →
|
|
// mux_tolower(), so wildcard matching is fully Unicode case-insensitive from
|
|
// either side. wild1() captures original-case bytes: only its literal
|
|
// comparisons fold; the '*'/'?' capture spans copy from the unmodified data.
|
|
// (Historically the data side folded only ASCII via mux_tolower_ascii, so e.g.
|
|
// strmatch(CAFÉ,café) and $café matching CAFÉ both failed — see
|
|
// docs/survey-wild-matching.md, #835/#836/#837.)
|
|
//
|
|
// Argument return space and size.
|
|
//
|
|
static UTF8 **arglist;
|
|
static int numargs;
|
|
|
|
// Length in bytes of the complete UTF-8 character at dstr, or 0 if dstr is at
|
|
// end-of-string or points at a malformed / truncated character. A single '?'
|
|
// matches exactly one such character. This mirrors the validation wild1()
|
|
// already performs for its capturing '?' case, so the non-capturing matcher
|
|
// (quick_wild_impl, used by strmatch/quick_wild/wild_match) agrees with it and
|
|
// with strlen() on multibyte data — historically quick_wild_impl advanced '?'
|
|
// by a single byte, so e.g. strmatch(café,????) wrongly failed (5 bytes != 4).
|
|
//
|
|
static size_t wild_char_len(const UTF8 *dstr)
|
|
{
|
|
if ('\0' == dstr[0])
|
|
{
|
|
return 0;
|
|
}
|
|
size_t t = utf8_FirstByte[*dstr];
|
|
if (UTF8_CONTINUE <= t)
|
|
{
|
|
return 0;
|
|
}
|
|
for (size_t j = 1; j < t; j++)
|
|
{
|
|
if ( '\0' == dstr[j]
|
|
|| UTF8_CONTINUE != utf8_FirstByte[dstr[j]])
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
return t;
|
|
}
|
|
|
|
// Case-insensitively match the data character at dstr against the literal
|
|
// character at tstr. The pattern (tstr) is already lowercased by the caller
|
|
// (mux_strlwr at the entry points), so the data character is folded on the fly
|
|
// with mux_tolower() — the same Unicode mapping — and the folded bytes are
|
|
// compared against the pattern. This makes non-ASCII letters (É/é, Ñ/ñ, …)
|
|
// match case-insensitively from EITHER side, where the old byte-wise EQUAL only
|
|
// folded ASCII in the data and so e.g. strmatch(CAFÉ,café) failed.
|
|
//
|
|
// On a match, returns true and sets *p_dlen / *p_tlen to the bytes to advance
|
|
// in the data and pattern respectively; these differ when a fold changes byte
|
|
// count. At end-of-data (*dstr == '\0') it matches only the pattern's NUL (the
|
|
// ASCII path folds '\0' to '\0'), which callers treat as "strings both ended".
|
|
//
|
|
static bool wild_lit_eq(const UTF8 *tstr, const UTF8 *dstr,
|
|
size_t *p_dlen, size_t *p_tlen)
|
|
{
|
|
// ASCII fast path — the overwhelming common case. tstr is pre-lowercased.
|
|
//
|
|
unsigned char dc = static_cast<unsigned char>(*dstr);
|
|
if (dc < 0x80)
|
|
{
|
|
if (mux_tolower_ascii(dc) != static_cast<unsigned char>(*tstr))
|
|
{
|
|
return false;
|
|
}
|
|
*p_dlen = 1;
|
|
*p_tlen = 1;
|
|
return true;
|
|
}
|
|
|
|
// Multibyte data character: validate and fold it.
|
|
//
|
|
size_t dlen = wild_char_len(dstr);
|
|
if (0 == dlen)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool bXor;
|
|
const string_desc *d = mux_tolower(dstr, bXor);
|
|
|
|
if ( nullptr != d
|
|
&& bXor
|
|
&& d->n_bytes == dlen)
|
|
{
|
|
// XOR transform (e.g. Latin-1 É→é): the folded byte is the original
|
|
// byte XORed with the mask at d->p. Compare against the pattern.
|
|
//
|
|
for (size_t i = 0; i < dlen; i++)
|
|
{
|
|
if ( static_cast<unsigned char>(tstr[i])
|
|
!= (static_cast<unsigned char>(dstr[i]) ^ d->p[i]))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
*p_dlen = dlen;
|
|
*p_tlen = dlen;
|
|
return true;
|
|
}
|
|
|
|
// Table transform with the folded bytes at d->p (may change byte count), or
|
|
// no fold available (d == nullptr, or a byte-count-changing XOR we don't
|
|
// apply) — compare the folded (or original) bytes against the pattern.
|
|
//
|
|
const UTF8 *fold = (nullptr != d && !bXor) ? d->p : dstr;
|
|
size_t flen = (nullptr != d && !bXor) ? d->n_bytes : dlen;
|
|
|
|
// Compare folded data bytes to the pattern. Stops at the first mismatch,
|
|
// including the pattern's NUL terminator, so it never reads past it.
|
|
//
|
|
for (size_t i = 0; i < flen; i++)
|
|
{
|
|
if (static_cast<unsigned char>(tstr[i]) != fold[i])
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
*p_dlen = dlen;
|
|
*p_tlen = flen;
|
|
return true;
|
|
}
|
|
|
|
// Step back over n complete UTF-8 characters from p, not crossing below lo.
|
|
// Used by wild1() to locate the start of the trailing-'?' region (the last n
|
|
// whole characters before a matched literal anchor) so a '?' immediately after
|
|
// a '*' captures a whole character rather than a single byte.
|
|
//
|
|
static const UTF8 *wild_step_back(const UTF8 *p, int n, const UTF8 *lo)
|
|
{
|
|
while (n > 0 && p > lo)
|
|
{
|
|
p--;
|
|
while ( p > lo
|
|
&& UTF8_CONTINUE == utf8_FirstByte[*p])
|
|
{
|
|
p--;
|
|
}
|
|
n--;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
// Copy the one whole UTF-8 character at *pp into dst (NUL-terminated) and
|
|
// advance *pp past it. Fills a '?' capture slot in wild1().
|
|
//
|
|
static void wild_capture_char(UTF8 *dst, const UTF8 **pp)
|
|
{
|
|
const UTF8 *p = *pp;
|
|
size_t t = utf8_FirstByte[*p];
|
|
if (UTF8_CONTINUE <= t)
|
|
{
|
|
t = 1; // malformed lead byte: take one byte defensively
|
|
}
|
|
memcpy(dst, p, t);
|
|
dst[t] = '\0';
|
|
*pp += t;
|
|
}
|
|
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
// quick_wild_impl: INTERNAL: do a wildcard match, without remembering the
|
|
// wild data. The pattern (tstr) must already be lowercased by the caller.
|
|
//
|
|
// This routine will cause crashes if fed nullptrs instead of strings.
|
|
//
|
|
static bool quick_wild_impl(const UTF8 *tstr, const UTF8 *dstr)
|
|
{
|
|
if (mudstate.wild_invk_ctr >= mudconf.wild_invk_lim)
|
|
{
|
|
return false;
|
|
}
|
|
mudstate.wild_invk_ctr++;
|
|
|
|
while (*tstr != '*')
|
|
{
|
|
if ('?' == *tstr)
|
|
{
|
|
// Single character match: consume one whole UTF-8 character.
|
|
// Return false at end of data or on a malformed character.
|
|
//
|
|
size_t t = wild_char_len(dstr);
|
|
if (0 == t)
|
|
{
|
|
return false;
|
|
}
|
|
dstr += t;
|
|
tstr++;
|
|
continue;
|
|
}
|
|
|
|
if ('\\' == *tstr)
|
|
{
|
|
// Escape character. Move up, and force literal match of next
|
|
// character.
|
|
//
|
|
tstr++;
|
|
}
|
|
|
|
// Literal character. Check for a (case-insensitive, whole-character)
|
|
// match. If matching end of data, return true.
|
|
//
|
|
size_t dlen, tlen;
|
|
if (!wild_lit_eq(tstr, dstr, &dlen, &tlen))
|
|
{
|
|
return false;
|
|
}
|
|
if (!*dstr)
|
|
{
|
|
return true;
|
|
}
|
|
tstr += tlen;
|
|
dstr += dlen;
|
|
}
|
|
|
|
// Skip over '*'.
|
|
//
|
|
tstr++;
|
|
|
|
// Return true on trailing '*'.
|
|
//
|
|
if (!*tstr)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Skip over wildcards.
|
|
//
|
|
while ( *tstr == '?'
|
|
|| *tstr == '*')
|
|
{
|
|
if (*tstr == '?')
|
|
{
|
|
// Each '?' consumes one whole UTF-8 character.
|
|
//
|
|
size_t t = wild_char_len(dstr);
|
|
if (0 == t)
|
|
{
|
|
return false;
|
|
}
|
|
dstr += t;
|
|
}
|
|
tstr++;
|
|
}
|
|
|
|
// Skip over a backslash in the pattern string if it is there.
|
|
//
|
|
if (*tstr == '\\')
|
|
{
|
|
tstr++;
|
|
}
|
|
|
|
// Return true on trailing '*'.
|
|
//
|
|
if (!*tstr)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Scan for possible matches. Advance one whole UTF-8 character at a time
|
|
// so a multibyte literal anchor is only tested on character boundaries.
|
|
//
|
|
while (*dstr)
|
|
{
|
|
size_t dlen, tlen;
|
|
if ( wild_lit_eq(tstr, dstr, &dlen, &tlen)
|
|
&& quick_wild_impl(tstr + tlen, dstr + dlen))
|
|
{
|
|
return true;
|
|
}
|
|
size_t t = wild_char_len(dstr);
|
|
dstr += (0 != t) ? t : 1;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// wild1: INTERNAL: do a wildcard match, remembering the wild data.
|
|
//
|
|
// DO NOT CALL THIS FUNCTION DIRECTLY - DOING SO MAY RESULT IN SERVER CRASHES
|
|
// AND IMPROPER ARGUMENT RETURN.
|
|
//
|
|
// Side Effect: this routine modifies the 'arglist' static global variable.
|
|
//
|
|
static bool wild1(UTF8 *tstr, UTF8 *dstr, int arg)
|
|
{
|
|
if (mudstate.wild_invk_ctr >= mudconf.wild_invk_lim)
|
|
{
|
|
return false;
|
|
}
|
|
mudstate.wild_invk_ctr++;
|
|
|
|
const UTF8 *datapos;
|
|
int argpos, numextra;
|
|
|
|
while (*tstr != '*')
|
|
{
|
|
if ('?' == *tstr)
|
|
{
|
|
// Single character match: capture one whole UTF-8 character.
|
|
// Return false at end of data or on a malformed character.
|
|
//
|
|
size_t t = wild_char_len(dstr);
|
|
if (0 == t)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
memcpy(arglist[arg], dstr, t);
|
|
arglist[arg][t] = '\0';
|
|
arg++;
|
|
|
|
// Jump to the fast routine if we can.
|
|
//
|
|
if (arg >= numargs)
|
|
{
|
|
return quick_wild_impl(tstr + 1, dstr + t);
|
|
}
|
|
dstr += t;
|
|
tstr++;
|
|
continue;
|
|
}
|
|
|
|
if ('\\' == *tstr)
|
|
{
|
|
// Escape character. Move up, and force literal match of next
|
|
// character.
|
|
//
|
|
tstr++;
|
|
}
|
|
|
|
// Literal character. Check for a (case-insensitive, whole-character)
|
|
// match. If matching end of data, return true.
|
|
//
|
|
size_t dlen, tlen;
|
|
if (!wild_lit_eq(tstr, dstr, &dlen, &tlen))
|
|
{
|
|
return false;
|
|
}
|
|
if (!*dstr)
|
|
{
|
|
return true;
|
|
}
|
|
dstr += dlen;
|
|
tstr += tlen;
|
|
}
|
|
|
|
// If at end of pattern, slurp the rest, and leave.
|
|
//
|
|
if (!tstr[1])
|
|
{
|
|
mux_strncpy(arglist[arg], dstr, LBUF_SIZE-1);
|
|
return true;
|
|
}
|
|
|
|
// Remember current position for filling in the '*' return.
|
|
//
|
|
datapos = dstr;
|
|
argpos = arg;
|
|
|
|
// Scan forward until we find a non-wildcard.
|
|
//
|
|
do
|
|
{
|
|
if (argpos < arg)
|
|
{
|
|
// Fill in arguments if someone put another '*' before a fixed
|
|
// string.
|
|
//
|
|
arglist[argpos][0] = '\0';
|
|
argpos++;
|
|
|
|
// Jump to the fast routine if we can.
|
|
//
|
|
if (argpos >= numargs)
|
|
{
|
|
return quick_wild_impl(tstr, dstr);
|
|
}
|
|
|
|
// Fill in any intervening '?'s (one whole character each).
|
|
//
|
|
while (argpos < arg)
|
|
{
|
|
wild_capture_char(arglist[argpos], &datapos);
|
|
argpos++;
|
|
|
|
// Jump to the fast routine if we can.
|
|
//
|
|
if (argpos >= numargs)
|
|
{
|
|
return quick_wild_impl(tstr, dstr);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Skip over the '*' for now...
|
|
//
|
|
tstr++;
|
|
arg++;
|
|
|
|
// Skip over '?'s for now... Each '?' consumes one whole UTF-8
|
|
// character of the data.
|
|
//
|
|
numextra = 0;
|
|
while (*tstr == '?')
|
|
{
|
|
size_t t = wild_char_len(dstr);
|
|
if (0 == t)
|
|
{
|
|
return false;
|
|
}
|
|
tstr++;
|
|
dstr += t;
|
|
arg++;
|
|
numextra++;
|
|
}
|
|
} while (*tstr == '*');
|
|
|
|
// Skip over a backslash in the pattern string if it is there.
|
|
//
|
|
if (*tstr == '\\')
|
|
{
|
|
tstr++;
|
|
}
|
|
|
|
// Check for possible matches. This loop terminates either at end of data
|
|
// (resulting in failure), or at a successful match.
|
|
//
|
|
size_t anchor_dlen = 0, anchor_tlen = 0;
|
|
for (;;)
|
|
{
|
|
// Scan forward until the literal anchor matches (case-insensitively, on
|
|
// whole-character boundaries).
|
|
//
|
|
if (*tstr)
|
|
{
|
|
while (!wild_lit_eq(tstr, dstr, &anchor_dlen, &anchor_tlen))
|
|
{
|
|
if (!*dstr)
|
|
{
|
|
return false;
|
|
}
|
|
size_t t = wild_char_len(dstr);
|
|
dstr += (0 != t) ? t : 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while (*dstr)
|
|
{
|
|
dstr++;
|
|
}
|
|
}
|
|
|
|
// The anchor matches, now. Check if the rest does, using the fastest
|
|
// method, as usual.
|
|
//
|
|
if ( !*dstr
|
|
|| ((arg < numargs) ? wild1(tstr + anchor_tlen, dstr + anchor_dlen, arg)
|
|
: quick_wild_impl(tstr + anchor_tlen, dstr + anchor_dlen)))
|
|
{
|
|
// Found a match! Fill in all remaining arguments. The trailing
|
|
// '?'s matched the last numextra whole characters before the
|
|
// anchor; the '*' matched everything before those.
|
|
//
|
|
const UTF8 *qstart = wild_step_back(dstr, numextra, datapos);
|
|
mux_strncpy(arglist[argpos], datapos, qstart - datapos);
|
|
datapos = qstart;
|
|
argpos++;
|
|
|
|
// Fill in any trailing '?'s that are left (one character each).
|
|
//
|
|
while (numextra)
|
|
{
|
|
if (argpos >= numargs)
|
|
{
|
|
return true;
|
|
}
|
|
wild_capture_char(arglist[argpos], &datapos);
|
|
argpos++;
|
|
numextra--;
|
|
}
|
|
|
|
// It's done!
|
|
//
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// Anchor matched but the rest didn't — advance past this whole
|
|
// data character and keep scanning.
|
|
//
|
|
size_t t = wild_char_len(dstr);
|
|
dstr += (0 != t) ? t : 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// wild: do a wildcard match, remembering the wild data.
|
|
//
|
|
// This routine will cause crashes if fed nullptrs instead of strings.
|
|
//
|
|
// Side Effect: this routine modifies the 'arglist' and 'numargs' static
|
|
// global variables.
|
|
//
|
|
bool wild(UTF8 *tstr, UTF8 *dstr, UTF8 *args[], int nargs)
|
|
{
|
|
mudstate.wild_invk_ctr = 0;
|
|
|
|
// Pre-lowercase the pattern for case-insensitive matching (Unicode-aware).
|
|
//
|
|
size_t nLower;
|
|
UTF8 *pLower = mux_strlwr(tstr, nLower);
|
|
LBuf LoweredPattern = LBuf_Src("wild1");
|
|
if (nLower >= LBUF_SIZE) nLower = LBUF_SIZE - 1;
|
|
memcpy(LoweredPattern, pLower, nLower);
|
|
LoweredPattern[nLower] = '\0';
|
|
UTF8 *lt = LoweredPattern;
|
|
|
|
int i;
|
|
UTF8 *scan;
|
|
|
|
// Initialize the return array.
|
|
//
|
|
for (i = 0; i < nargs; i++)
|
|
{
|
|
args[i] = nullptr;
|
|
}
|
|
|
|
// Do fast match.
|
|
//
|
|
while ( *lt != '*'
|
|
&& *lt != '?')
|
|
{
|
|
if (*lt == '\\')
|
|
{
|
|
lt++;
|
|
}
|
|
size_t dlen, tlen;
|
|
if (!wild_lit_eq(lt, dstr, &dlen, &tlen))
|
|
{
|
|
return false;
|
|
}
|
|
if (!*dstr)
|
|
{
|
|
return true;
|
|
}
|
|
lt += tlen;
|
|
dstr += dlen;
|
|
}
|
|
|
|
// Allocate space for the return args.
|
|
//
|
|
i = 0;
|
|
scan = lt;
|
|
while ( *scan
|
|
&& i < nargs)
|
|
{
|
|
switch (*scan)
|
|
{
|
|
case '?':
|
|
|
|
args[i] = alloc_lbuf("wild.?");
|
|
i++;
|
|
break;
|
|
|
|
case '*':
|
|
|
|
args[i] = alloc_lbuf("wild.*");
|
|
i++;
|
|
}
|
|
scan++;
|
|
}
|
|
|
|
// Put stuff in globals for quick recursion.
|
|
//
|
|
arglist = args;
|
|
numargs = nargs;
|
|
|
|
// Do the match.
|
|
//
|
|
bool value = nargs ? wild1(lt, dstr, 0) : quick_wild_impl(lt, dstr);
|
|
|
|
// Clean out any fake match data left by wild1.
|
|
//
|
|
for (i = 0; i < nargs; i++)
|
|
{
|
|
if ( args[i] != nullptr
|
|
&& ( !*args[i]
|
|
|| !value))
|
|
{
|
|
free_lbuf(args[i]);
|
|
args[i] = nullptr;
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// wild_match: do either an order comparison or a wildcard match, remembering
|
|
// the wild data, if wildcard match is done.
|
|
//
|
|
// This routine will cause crashes if fed nullptrs instead of strings.
|
|
//
|
|
bool wild_match(UTF8 *tstr, const UTF8 *dstr)
|
|
{
|
|
PARSE_FLOAT_RESULT pfr;
|
|
UTF8 ch = *tstr;
|
|
if ( '>' == ch
|
|
|| '<' == ch)
|
|
{
|
|
tstr++;
|
|
if ( ParseFloat(&pfr, dstr, true)
|
|
&& ParseFloat(&pfr, tstr, true))
|
|
{
|
|
double dd = mux_atof(dstr);
|
|
double dt = mux_atof(tstr);
|
|
if ('<' == ch)
|
|
{
|
|
return (dd < dt);
|
|
}
|
|
else
|
|
{
|
|
return (dd > dt);
|
|
}
|
|
}
|
|
else if ('<' == ch)
|
|
{
|
|
return (strcmp(reinterpret_cast<const char *>(dstr), reinterpret_cast<const char *>(tstr)) < 0);
|
|
}
|
|
else // if ('>' == ch)
|
|
{
|
|
return (strcmp(reinterpret_cast<const char *>(dstr), reinterpret_cast<const char *>(tstr)) > 0);
|
|
}
|
|
}
|
|
|
|
// Pre-lowercase the pattern for case-insensitive matching (Unicode-aware).
|
|
//
|
|
size_t nLower;
|
|
UTF8 *pLower = mux_strlwr(tstr, nLower);
|
|
LBuf LoweredPattern = LBuf_Src("wild_match");
|
|
if (nLower >= LBUF_SIZE) nLower = LBUF_SIZE - 1;
|
|
memcpy(LoweredPattern, pLower, nLower);
|
|
LoweredPattern[nLower] = '\0';
|
|
|
|
mudstate.wild_invk_ctr = 0;
|
|
return quick_wild_impl(LoweredPattern, dstr);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// quick_wild: Public entry point. Pre-lowercases the pattern for
|
|
// Unicode-aware case-insensitive matching, then delegates to quick_wild_impl.
|
|
//
|
|
bool quick_wild(const UTF8 *tstr, const UTF8 *dstr)
|
|
{
|
|
size_t nLower;
|
|
UTF8 *pLower = mux_strlwr(tstr, nLower);
|
|
LBuf LoweredPattern = LBuf_Src("quick_wild");
|
|
if (nLower >= LBUF_SIZE) nLower = LBUF_SIZE - 1;
|
|
memcpy(LoweredPattern, pLower, nLower);
|
|
LoweredPattern[nLower] = '\0';
|
|
|
|
return quick_wild_impl(LoweredPattern, dstr);
|
|
}
|