From 055344f3461a28578f8b49e9f2431bb9f22936b4 Mon Sep 17 00:00:00 2001 From: howar6hill Date: Wed, 26 Feb 2020 15:25:24 +0800 Subject: [PATCH] AK: Move the wildcard-matching implementation to StringUtils Provide wrappers in the String and StringView classes, and add some tests. --- AK/String.cpp | 54 +--------------- AK/String.h | 9 +-- AK/StringUtils.cpp | 64 +++++++++++++++++++ AK/StringUtils.h | 20 ++++++ AK/StringView.cpp | 5 ++ AK/StringView.h | 2 + AK/TestSuite.h | 2 +- AK/Tests/Makefile | 1 + AK/Tests/TestStringUtils.cpp | 44 +++++++++++++ DevTools/FormCompiler/Makefile | 1 + DevTools/IPCCompiler/Makefile | 1 + Kernel/Makefile | 1 + Libraries/LibC/Makefile | 1 + .../Generate_CSS_PropertyID_cpp/Makefile | 1 + .../Generate_CSS_PropertyID_h/Makefile | 1 + Shell/main.cpp | 2 +- 16 files changed, 147 insertions(+), 62 deletions(-) create mode 100644 AK/StringUtils.cpp create mode 100644 AK/StringUtils.h create mode 100644 AK/Tests/TestStringUtils.cpp diff --git a/AK/String.cpp b/AK/String.cpp index 131ba9e70f2..c0bfd9bfea9 100644 --- a/AK/String.cpp +++ b/AK/String.cpp @@ -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 diff --git a/AK/String.h b/AK/String.h index c1c381f00f3..9716eabf73f 100644 --- a/AK/String.h +++ b/AK/String.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -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 m_impl; }; diff --git a/AK/StringUtils.cpp b/AK/StringUtils.cpp new file mode 100644 index 00000000000..fe37f49e0bc --- /dev/null +++ b/AK/StringUtils.cpp @@ -0,0 +1,64 @@ +#include +#include +#include + +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; + } + +} + +} diff --git a/AK/StringUtils.h b/AK/StringUtils.h new file mode 100644 index 00000000000..a21fc23de2d --- /dev/null +++ b/AK/StringUtils.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace AK { + +enum class CaseSensitivity { + CaseInsensitive, + CaseSensitive, +}; + +namespace StringUtils { + + bool matches(const StringView& str, const StringView& mask, CaseSensitivity = CaseSensitivity::CaseInsensitive); + +} + +} + +using AK::CaseSensitivity; diff --git a/AK/StringView.cpp b/AK/StringView.cpp index 0dcd8e5b544..c94c8dea33f 100644 --- a/AK/StringView.cpp +++ b/AK/StringView.cpp @@ -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); diff --git a/AK/StringView.h b/AK/StringView.h index 8ccbefcad5c..7835498cad7 100644 --- a/AK/StringView.h +++ b/AK/StringView.h @@ -28,6 +28,7 @@ #include #include +#include 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 split_view(char, bool keep_empty = false) const; diff --git a/AK/TestSuite.h b/AK/TestSuite.h index 67317df3c8d..ec68f780de8 100644 --- a/AK/TestSuite.h +++ b/AK/TestSuite.h @@ -182,7 +182,7 @@ NonnullRefPtrVector TestSuite::find_cases(const String& search, bool f { NonnullRefPtrVector 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; } diff --git a/AK/Tests/Makefile b/AK/Tests/Makefile index abede6c7ba0..783d92c0dd9 100644 --- a/AK/Tests/Makefile +++ b/AK/Tests/Makefile @@ -3,6 +3,7 @@ SHARED_TEST_SOURCES = \ ../StringImpl.cpp \ ../StringBuilder.cpp \ ../StringView.cpp \ + ../StringUtils.cpp \ ../LogStream.cpp \ ../JsonValue.cpp \ ../JsonParser.cpp \ diff --git a/AK/Tests/TestStringUtils.cpp b/AK/Tests/TestStringUtils.cpp new file mode 100644 index 00000000000..5162f48f749 --- /dev/null +++ b/AK/Tests/TestStringUtils.cpp @@ -0,0 +1,44 @@ +#include +#include + +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) diff --git a/DevTools/FormCompiler/Makefile b/DevTools/FormCompiler/Makefile index 18efd405897..317ad961192 100644 --- a/DevTools/FormCompiler/Makefile +++ b/DevTools/FormCompiler/Makefile @@ -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 \ diff --git a/DevTools/IPCCompiler/Makefile b/DevTools/IPCCompiler/Makefile index 00f10d43e3f..416c93b0fd8 100644 --- a/DevTools/IPCCompiler/Makefile +++ b/DevTools/IPCCompiler/Makefile @@ -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 \ diff --git a/Kernel/Makefile b/Kernel/Makefile index c884ad11e8f..8e9b7201eef 100644 --- a/Kernel/Makefile +++ b/Kernel/Makefile @@ -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 \ diff --git a/Libraries/LibC/Makefile b/Libraries/LibC/Makefile index 71acff8ace0..3f9f00cef41 100644 --- a/Libraries/LibC/Makefile +++ b/Libraries/LibC/Makefile @@ -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 \ diff --git a/Libraries/LibHTML/CodeGenerators/Generate_CSS_PropertyID_cpp/Makefile b/Libraries/LibHTML/CodeGenerators/Generate_CSS_PropertyID_cpp/Makefile index b95dc4ed630..121b113dfb2 100644 --- a/Libraries/LibHTML/CodeGenerators/Generate_CSS_PropertyID_cpp/Makefile +++ b/Libraries/LibHTML/CodeGenerators/Generate_CSS_PropertyID_cpp/Makefile @@ -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 \ diff --git a/Libraries/LibHTML/CodeGenerators/Generate_CSS_PropertyID_h/Makefile b/Libraries/LibHTML/CodeGenerators/Generate_CSS_PropertyID_h/Makefile index edb40a55f13..b94e2119296 100644 --- a/Libraries/LibHTML/CodeGenerators/Generate_CSS_PropertyID_h/Makefile +++ b/Libraries/LibHTML/CodeGenerators/Generate_CSS_PropertyID_h/Makefile @@ -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 \ diff --git a/Shell/main.cpp b/Shell/main.cpp index d63f02ccb61..992f2740cbb 100644 --- a/Shell/main.cpp +++ b/Shell/main.cpp @@ -630,7 +630,7 @@ static Vector 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);