open_toontown_panda3d/dtool/src/dtoolutil/globPattern.cxx

494 lines
14 KiB
C++

/**
* PANDA 3D SOFTWARE
* Copyright (c) Carnegie Mellon University. All rights reserved.
*
* All use of this software is subject to the terms of the revised BSD
* license. You should have received a copy of this license along
* with this source code in a file named "LICENSE."
*
* @file globPattern.cxx
* @author drose
* @date 2000-05-30
*/
#include "globPattern.h"
#include "string_utils.h"
#include <ctype.h>
using std::string;
/**
* Returns true if the pattern includes any special globbing characters, or
* false if it is just a literal string.
*/
bool GlobPattern::
has_glob_characters() const {
string::const_iterator pi;
pi = _pattern.begin();
while (pi != _pattern.end()) {
switch (*pi) {
case '*':
case '?':
case '[':
return true;
case '\\':
++pi;
if (pi == _pattern.end()) {
return false;
}
}
++pi;
}
return false;
}
/**
* Returns the initial part of the pattern before the first glob character.
* Since many glob patterns begin with a sequence of static characters and end
* with one or more glob characters, this can be used to optimized searches
* through sorted indices.
*/
string GlobPattern::
get_const_prefix() const {
string prefix;
size_t p = 0; // current point
size_t q = 0; // starting point
while (p < _pattern.size()) {
switch (_pattern[p]) {
case '*':
case '?':
case '[':
return prefix + _pattern.substr(q, p - q);
case '\\':
// Skip over the backslash.
prefix += _pattern.substr(q, p - q);
++p;
q = p;
}
++p;
}
return prefix += _pattern.substr(q, p - q);
}
/**
* Treats the GlobPattern as a filename pattern, and returns a list of any
* actual files that match the pattern. This is the behavior of the standard
* Posix glob() function. Any part of the filename may contain glob
* characters, including intermediate directory names.
*
* If cwd is specified, it is the directory that relative filenames are taken
* to be relative to; otherwise, the actual current working directory is
* assumed.
*
* The return value is the number of files matched, which are added to the
* results vector.
*/
int GlobPattern::
match_files(vector_string &results, const Filename &cwd) const {
string prefix, pattern, suffix;
string source = _pattern;
if (!source.empty() && source[0] == '/') {
// If the first character is a slash, that becomes the prefix.
prefix = "/";
source = source.substr(1);
}
size_t slash = source.find('/');
if (slash == string::npos) {
pattern = source;
} else {
pattern = source.substr(0, slash);
suffix = source.substr(slash);
}
GlobPattern glob(pattern);
glob.set_case_sensitive(_case_sensitive);
return glob.r_match_files(prefix, suffix, results, cwd);
}
/**
* The recursive implementation of match_files().
*/
int GlobPattern::
r_match_files(const Filename &prefix, const string &suffix,
vector_string &results, const Filename &cwd) {
string next_pattern, next_suffix;
size_t slash = suffix.find('/');
if (slash == string::npos) {
next_pattern = suffix;
} else if (slash + 1 == suffix.size()) {
// If the slash is at the end, we need to keep it, since it indicates that
// we only want to match directories.
next_pattern = suffix.substr(0, slash);
next_suffix = "/";
} else {
next_pattern = suffix.substr(0, slash);
next_suffix = suffix.substr(slash + 1);
}
if (_pattern == "**" && next_pattern == "**") {
// Collapse consecutive globstar patterns.
return r_match_files(prefix, next_suffix, results, cwd);
}
Filename parent_dir;
if (prefix.is_local() && !cwd.empty()) {
parent_dir = Filename(cwd, prefix);
} else {
parent_dir = prefix;
}
GlobPattern next_glob(next_pattern);
next_glob.set_case_sensitive(_case_sensitive);
if (!has_glob_characters()) {
// If there are no special characters in the pattern, it's a literal
// match.
Filename fn(parent_dir, _pattern);
if (suffix.empty()) {
// Time to stop.
if (fn.exists()) {
results.push_back(Filename(prefix, _pattern));
return 1;
}
return 0;
} else if (fn.is_directory()) {
// If the pattern ends with a slash, match a directory only.
if (suffix == "/") {
results.push_back(Filename(prefix, _pattern + "/"));
return 1;
} else {
return next_glob.r_match_files(Filename(prefix, _pattern),
next_suffix, results, cwd);
}
}
}
// If there *are* special glob characters, we must attempt to match the
// pattern against the files in this directory.
vector_string dir_files;
if (!parent_dir.scan_directory(dir_files)) {
// Not a directory, or unable to read directory; stop here.
return 0;
}
// Now go through each file in the directory looking for one that matches
// the pattern.
int num_matched = 0;
// A globstar pattern matches zero or more directories.
if (_pattern == "**") {
// Try to match this directory (as if the globstar wasn't there)
if (suffix.empty()) {
// This is a directory. Add it.
results.push_back(Filename(prefix));
num_matched++;
} else if (suffix == "/") {
// Keep the trailing slash, but be sure not to duplicate it.
results.push_back(Filename(prefix, ""));
num_matched++;
} else {
num_matched += next_glob.r_match_files(prefix, next_suffix, results, cwd);
}
next_suffix = suffix;
next_glob = *this;
}
for (const string &local_file : dir_files) {
if (_pattern[0] == '.' || (local_file.empty() || local_file[0] != '.')) {
if (matches(local_file)) {
// We have a match; continue.
if (Filename(parent_dir, local_file).is_directory()) {
if (suffix.empty() && _pattern != "**") {
results.push_back(Filename(prefix, local_file));
num_matched++;
} else if (suffix == "/" && _pattern != "**") {
results.push_back(Filename(prefix, local_file + "/"));
num_matched++;
} else {
num_matched += next_glob.r_match_files(Filename(prefix, local_file),
next_suffix, results, cwd);
}
} else if (suffix.empty()) {
results.push_back(Filename(prefix, local_file));
num_matched++;
}
}
}
}
return num_matched;
}
/**
* Treats the GlobPattern as a filename pattern, and returns true if the given
* filename matches the pattern. Unlike matches(), this will not match slash
* characters for single asterisk characters, and it will ignore path
* components that only contain a dot.
*/
bool GlobPattern::
matches_file(Filename candidate) const {
if (_pattern.empty()) {
// Special case.
return candidate.empty();
}
// Either both must be absolute, or both must be relative.
if ((_pattern[0] != '/') != candidate.is_local()) {
return false;
}
return r_matches_file(_pattern, candidate);
}
/**
* The recursive implementation of matches_file().
*/
bool GlobPattern::
r_matches_file(const string &pattern, const Filename &candidate) const {
// Split off the next bit of pattern.
std::string next_pattern;
GlobPattern glob;
glob.set_case_sensitive(get_case_sensitive());
glob.set_nomatch_chars(get_nomatch_chars());
bool pattern_end;
size_t slash = pattern.find('/');
if (slash == string::npos) {
glob.set_pattern(pattern);
pattern_end = true;
} else {
glob.set_pattern(pattern.substr(0, slash));
next_pattern = pattern.substr(slash + 1);
pattern_end = false;
if (slash == 0 || (slash == 1 && pattern[0] == '.')) {
// Ignore // and /./ in patterns
return r_matches_file(next_pattern, candidate);
}
}
// Also split off the next component in the candidate filename.
std::string part;
Filename next_candidate;
bool candidate_end;
size_t fn_slash = ((const std::string &)candidate).find('/');
if (fn_slash == string::npos) {
part = candidate;
candidate_end = true;
} else {
part = candidate.substr(0, fn_slash);
next_candidate = candidate.substr(fn_slash + 1);
candidate_end = false;
// Ignore // and /./ in filenames.
if (fn_slash == 0 || part == ".") {
return r_matches_file(pattern, next_candidate);
}
}
// Now check if the current part matches the current pattern.
bool part_matches;
if (glob.get_pattern() == "**") {
// This matches any number of parts.
if (pattern_end) {
// We might as well stop checking here; it matches whatever might come.
return true;
}
// We branch out to three options: either we match nothing, we match this
// part only, or we match this part and maybe more.
return r_matches_file(next_pattern, candidate)
|| (!candidate_end && r_matches_file(next_pattern, next_candidate))
|| (!candidate_end && r_matches_file(pattern, next_candidate));
}
else if (glob.get_pattern() == "*" && _nomatch_chars.empty()) {
// Matches any part (faster version of below)
part_matches = true;
}
else if ((glob.get_pattern() == "." && part.empty())
|| (glob.get_pattern().empty() && part == ".")) {
// So that /path/. matches /path/, and vice versa.
part_matches = true;
}
else if (glob.has_glob_characters()) {
part_matches = glob.matches(part);
}
else if (get_case_sensitive()) {
part_matches = (part == glob.get_pattern());
}
else {
part_matches = (cmp_nocase(part, glob.get_pattern()) == 0);
}
if (!part_matches) {
// It doesn't match, so we end our search here.
return false;
}
if (candidate_end && pattern_end) {
// We've reached the end of both candidate and pattern, so it matches.
return true;
}
if (pattern_end != candidate_end) {
// One of them has ended, but the other hasn't, so it's not a match.
return false;
}
// It matches; move on to the next part.
return r_matches_file(next_pattern, next_candidate);
}
/**
* The recursive implementation of matches(). This returns true if the
* pattern substring [pi, pend) matches the candidate substring [ci, cend),
* false otherwise.
*/
bool GlobPattern::
matches_substr(string::const_iterator pi, string::const_iterator pend,
string::const_iterator ci, string::const_iterator cend) const {
// If we run out of pattern or candidate string, it's a match only if they
// both ran out at the same time.
if (pi == pend || ci == cend) {
// A special exception: we allow ci to reach the end before pi, only if pi
// is one character before the end and that last character is '*'.
if ((ci == cend) && (std::distance(pi, pend) == 1) && (*pi) == '*') {
return true;
}
return (pi == pend && ci == cend);
}
switch (*pi) {
case '*':
// A '*' in the pattern string means to match any sequence of zero or more
// characters in the candidate string. This means we have to recurse
// twice: either consume one character of the candidate string and
// continue to try matching the *, or stop trying to match the * here.
if (_nomatch_chars.find(*ci) == string::npos) {
return
matches_substr(pi, pend, ci + 1, cend) ||
matches_substr(pi + 1, pend, ci, cend);
} else {
// On the other hand, if this is one of the nomatch chars, we can only
// stop here.
return matches_substr(pi + 1, pend, ci, cend);
}
case '?':
// A '?' in the pattern string means to match exactly one character in the
// candidate string. That's easy.
return matches_substr(pi + 1, pend, ci + 1, cend);
case '[':
// An open square bracket begins a set.
++pi;
if ((*pi) == '!') {
++pi;
if (matches_set(pi, pend, *ci)) {
return false;
}
} else {
if (!matches_set(pi, pend, *ci)) {
return false;
}
}
if (pi == pend) {
// Oops, there wasn't a closing square bracket.
return false;
}
return matches_substr(pi + 1, pend, ci + 1, cend);
case '\\':
// A backslash escapes the next special character.
++pi;
if (pi == pend) {
return false;
}
// fall through.
default:
// Anything else means to match exactly that.
if (_case_sensitive) {
if ((*pi) != (*ci)) {
return false;
}
} else {
if (tolower(*pi) != tolower(*ci)) {
return false;
}
}
return matches_substr(pi + 1, pend, ci + 1, cend);
}
}
/**
* Called when an unescaped open square bracked is scanned, this is called
* with pi positioned after the opening square bracket, scans the set
* sequence, leaving pi positioned on the closing square bracket, and returns
* true if the indicated character matches the set of characters indicated,
* false otherwise.
*/
bool GlobPattern::
matches_set(string::const_iterator &pi, string::const_iterator pend,
char ch) const {
bool matched = false;
while (pi != pend && (*pi) != ']') {
if ((*pi) == '\\') {
// Backslash escapes the next character.
++pi;
if (pi == pend) {
return false;
}
}
if (ch == (*pi)) {
matched = true;
}
// Maybe it's an a-z style range?
char start = (*pi);
++pi;
if (pi != pend && (*pi) == '-') {
++pi;
if (pi != pend && (*pi) != ']') {
// Yes, we have a range: start-end.
if ((*pi) == '\\') {
// Backslash escapes.
++pi;
if (pi == pend) {
return false;
}
}
char end = (*pi);
++pi;
if ((ch >= start && ch <= end) ||
(!_case_sensitive &&
((tolower(ch) >= start && tolower(ch) <= end) ||
(toupper(ch) >= start && toupper(ch) <= end)))) {
matched = true;
}
} else {
// This was a - at the end of the string.
if (ch == '-') {
matched = true;
}
}
}
}
return matched;
}