AK: Move the wildcard-matching implementation to StringUtils

Provide wrappers in the String and StringView classes, and add some tests.
This commit is contained in:
howar6hill 2020-02-26 15:25:24 +08:00 committed by Andreas Kling
parent 2a30a020c1
commit 055344f346
Notes: sideshowbarker 2024-07-19 17:38:07 +09:00
16 changed files with 147 additions and 62 deletions

View file

@ -331,59 +331,7 @@ String String::repeated(char ch, size_t count)
bool String::matches(const StringView& mask, CaseSensitivity case_sensitivity) const
{
if (case_sensitivity == CaseSensitivity::CaseInsensitive) {
String this_lower = this->to_lowercase();
String mask_lower = String(mask).to_lowercase();
return this_lower.match_helper(mask_lower);
}
return match_helper(mask);
}
bool String::match_helper(const StringView& mask) const
{
if (is_null())
return false;
const char* string_ptr = characters();
const char* mask_ptr = mask.characters_without_null_termination();
const char* mask_end = mask_ptr + mask.length();
// Match string against mask directly unless we hit a *
while ((*string_ptr) && (mask_ptr < mask_end) && (*mask_ptr != '*')) {
if ((*mask_ptr != *string_ptr) && (*mask_ptr != '?'))
return false;
mask_ptr++;
string_ptr++;
}
const char* cp = nullptr;
const char* mp = nullptr;
while (*string_ptr) {
if ((mask_ptr < mask_end) && (*mask_ptr == '*')) {
// If we have only a * left, there is no way to not match.
if (++mask_ptr == mask_end)
return true;
mp = mask_ptr;
cp = string_ptr + 1;
} else if ((mask_ptr < mask_end) && ((*mask_ptr == *string_ptr) || (*mask_ptr == '?'))) {
mask_ptr++;
string_ptr++;
} else if ((cp != nullptr) && (mp != nullptr)) {
mask_ptr = mp;
string_ptr = cp++;
} else {
break;
}
}
// Handle any trailing mask
while ((mask_ptr < mask_end) && (*mask_ptr == '*'))
mask_ptr++;
// If we 'ate' all of the mask and the string then we match.
return (mask_ptr == mask_end) && !*string_ptr;
return StringUtils::matches(*this, mask, case_sensitivity);
}
bool String::contains(const String& needle) const

View file

@ -29,6 +29,7 @@
#include <AK/Forward.h>
#include <AK/RefPtr.h>
#include <AK/StringImpl.h>
#include <AK/StringUtils.h>
#include <AK/StringView.h>
#include <AK/Traits.h>
@ -108,13 +109,8 @@ public:
{
}
enum class CaseSensitivity {
CaseInsensitive,
CaseSensitive,
};
static String repeated(char, size_t count);
bool matches(const StringView& pattern, CaseSensitivity = CaseSensitivity::CaseInsensitive) const;
bool matches(const StringView& mask, CaseSensitivity = CaseSensitivity::CaseInsensitive) const;
// FIXME: These should be shared between String and StringView somehow!
int to_int(bool& ok) const;
@ -244,7 +240,6 @@ public:
}
private:
bool match_helper(const StringView& mask) const;
RefPtr<StringImpl> m_impl;
};

64
AK/StringUtils.cpp Normal file
View file

@ -0,0 +1,64 @@
#include <AK/String.h>
#include <AK/StringUtils.h>
#include <AK/StringView.h>
namespace AK {
namespace StringUtils {
bool matches(const StringView& str, const StringView& mask, CaseSensitivity case_sensitivity)
{
if (str.is_null() || mask.is_null())
return str.is_null() && mask.is_null();
if (case_sensitivity == CaseSensitivity::CaseInsensitive) {
const String str_lower = String(str).to_lowercase();
const String mask_lower = String(mask).to_lowercase();
return matches(str_lower, mask_lower, CaseSensitivity::CaseSensitive);
}
const char* string_ptr = str.characters_without_null_termination();
const char* string_end = string_ptr + str.length();
const char* mask_ptr = mask.characters_without_null_termination();
const char* mask_end = mask_ptr + mask.length();
// Match string against mask directly unless we hit a *
while ((string_ptr < string_end) && (mask_ptr < mask_end) && (*mask_ptr != '*')) {
if ((*mask_ptr != *string_ptr) && (*mask_ptr != '?'))
return false;
mask_ptr++;
string_ptr++;
}
const char* cp = nullptr;
const char* mp = nullptr;
while (string_ptr < string_end) {
if ((mask_ptr < mask_end) && (*mask_ptr == '*')) {
// If we have only a * left, there is no way to not match.
if (++mask_ptr == mask_end)
return true;
mp = mask_ptr;
cp = string_ptr + 1;
} else if ((mask_ptr < mask_end) && ((*mask_ptr == *string_ptr) || (*mask_ptr == '?'))) {
mask_ptr++;
string_ptr++;
} else if ((cp != nullptr) && (mp != nullptr)) {
mask_ptr = mp;
string_ptr = cp++;
} else {
break;
}
}
// Handle any trailing mask
while ((mask_ptr < mask_end) && (*mask_ptr == '*'))
mask_ptr++;
// If we 'ate' all of the mask and the string then we match.
return (mask_ptr == mask_end) && string_ptr == string_end;
}
}
}

20
AK/StringUtils.h Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <AK/Forward.h>
namespace AK {
enum class CaseSensitivity {
CaseInsensitive,
CaseSensitive,
};
namespace StringUtils {
bool matches(const StringView& str, const StringView& mask, CaseSensitivity = CaseSensitivity::CaseInsensitive);
}
}
using AK::CaseSensitivity;

View file

@ -143,6 +143,11 @@ bool StringView::ends_with(const StringView& str) const
return !memcmp(characters_without_null_termination() + length() - str.length(), str.characters_without_null_termination(), str.length());
}
bool StringView::matches(const StringView& mask, CaseSensitivity case_sensitivity) const
{
return StringUtils::matches(*this, mask, case_sensitivity);
}
StringView StringView::substring_view(size_t start, size_t length) const
{
ASSERT(start + length <= m_length);

View file

@ -28,6 +28,7 @@
#include <AK/Forward.h>
#include <AK/StdLibExtras.h>
#include <AK/StringUtils.h>
namespace AK {
@ -65,6 +66,7 @@ public:
bool ends_with(const StringView&) const;
bool starts_with(char) const;
bool ends_with(char) const;
bool matches(const StringView& mask, CaseSensitivity = CaseSensitivity::CaseInsensitive) const;
StringView substring_view(size_t start, size_t length) const;
Vector<StringView> split_view(char, bool keep_empty = false) const;

View file

@ -182,7 +182,7 @@ NonnullRefPtrVector<TestCase> TestSuite::find_cases(const String& search, bool f
{
NonnullRefPtrVector<TestCase> matches;
for (const auto& t : m_cases) {
if (!search.is_empty() && !t.name().matches(search, String::CaseSensitivity::CaseInsensitive)) {
if (!search.is_empty() && !t.name().matches(search, CaseSensitivity::CaseInsensitive)) {
continue;
}

View file

@ -3,6 +3,7 @@ SHARED_TEST_SOURCES = \
../StringImpl.cpp \
../StringBuilder.cpp \
../StringView.cpp \
../StringUtils.cpp \
../LogStream.cpp \
../JsonValue.cpp \
../JsonParser.cpp \

View file

@ -0,0 +1,44 @@
#include <AK/StringUtils.h>
#include <AK/TestSuite.h>
TEST_CASE(matches_null)
{
EXPECT(AK::StringUtils::matches(StringView(), StringView()));
EXPECT(!AK::StringUtils::matches(StringView(), ""));
EXPECT(!AK::StringUtils::matches(StringView(), "*"));
EXPECT(!AK::StringUtils::matches(StringView(), "?"));
EXPECT(!AK::StringUtils::matches(StringView(), "a"));
EXPECT(!AK::StringUtils::matches("", StringView()));
EXPECT(!AK::StringUtils::matches("a", StringView()));
}
TEST_CASE(matches_empty)
{
EXPECT(AK::StringUtils::matches("", ""));
EXPECT(AK::StringUtils::matches("", "*"));
EXPECT(!AK::StringUtils::matches("", "?"));
EXPECT(!AK::StringUtils::matches("", "a"));
EXPECT(!AK::StringUtils::matches("a", ""));
}
TEST_CASE(matches_case_sensitive)
{
EXPECT(AK::StringUtils::matches("a", "a", CaseSensitivity::CaseSensitive));
EXPECT(!AK::StringUtils::matches("a", "A", CaseSensitivity::CaseSensitive));
EXPECT(!AK::StringUtils::matches("A", "a", CaseSensitivity::CaseSensitive));
}
TEST_CASE(matches_case_insensitive)
{
EXPECT(!AK::StringUtils::matches("aa", "a"));
EXPECT(AK::StringUtils::matches("aa", "*"));
EXPECT(!AK::StringUtils::matches("cb", "?a"));
EXPECT(AK::StringUtils::matches("adceb", "a*b"));
EXPECT(!AK::StringUtils::matches("acdcb", "a*c?b"));
}
TEST_MAIN(StringUtils)

View file

@ -8,6 +8,7 @@ OBJS = \
../../AK/StringImpl.o \
../../AK/StringBuilder.o \
../../AK/StringView.o \
../../AK/StringUtils.o \
../../AK/JsonValue.o \
../../AK/JsonParser.o \
../../AK/LogStream.o \

View file

@ -8,6 +8,7 @@ OBJS = \
../../AK/StringImpl.o \
../../AK/StringBuilder.o \
../../AK/StringView.o \
../../AK/StringUtils.o \
../../AK/JsonValue.o \
../../AK/JsonParser.o \
../../AK/LogStream.o \

View file

@ -7,6 +7,7 @@ OBJS = \
../AK/StringBuilder.o \
../AK/StringImpl.o \
../AK/StringView.o \
../AK/StringUtils.o \
../Libraries/LibELF/ELFImage.o \
../Libraries/LibELF/ELFLoader.o \
../Libraries/LibBareMetal/Output/Console.o \

View file

@ -3,6 +3,7 @@ AK_OBJS = \
../../AK/String.o \
../../AK/StringView.o \
../../AK/StringBuilder.o \
../../AK/StringUtils.o \
../../AK/FileSystemPath.o \
../../AK/URL.o \
../../AK/JsonValue.o \

View file

@ -8,6 +8,7 @@ OBJS = \
../../../../AK/StringImpl.o \
../../../../AK/StringBuilder.o \
../../../../AK/StringView.o \
../../../../AK/StringUtils.o \
../../../../AK/JsonValue.o \
../../../../AK/JsonParser.o \
../../../../AK/LogStream.o \

View file

@ -4,6 +4,7 @@ PROGRAM = Generate_CSS_PropertyID_h
OBJS = \
Generate_CSS_PropertyID_h.o \
../../../../AK/StringUtils.o \
../../../../AK/String.o \
../../../../AK/StringImpl.o \
../../../../AK/StringBuilder.o \

View file

@ -630,7 +630,7 @@ static Vector<String> expand_globs(const StringView& path, const StringView& bas
if (name[0] == '.' && part[0] != '.')
continue;
if (name.matches(part, String::CaseSensitivity::CaseSensitive)) {
if (name.matches(part, CaseSensitivity::CaseSensitive)) {
StringBuilder nested_base;
nested_base.append(new_base);