|
@@ -29,6 +29,8 @@ import (
|
|
|
"slices"
|
|
"slices"
|
|
|
"strconv"
|
|
"strconv"
|
|
|
"strings"
|
|
"strings"
|
|
|
|
|
+ "sync"
|
|
|
|
|
+ "sync/atomic"
|
|
|
"time"
|
|
"time"
|
|
|
|
|
|
|
|
"miniflux.app/v2/internal/model"
|
|
"miniflux.app/v2/internal/model"
|
|
@@ -41,6 +43,34 @@ type filterRule struct {
|
|
|
|
|
|
|
|
type filterRules []filterRule
|
|
type filterRules []filterRule
|
|
|
|
|
|
|
|
|
|
+const maxCachedRegexes = 1024
|
|
|
|
|
+
|
|
|
|
|
+var (
|
|
|
|
|
+ compiledRegexesCache sync.Map
|
|
|
|
|
+ compiledRegexesCacheSize atomic.Int64
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+func cachedRegex(pattern string) *regexp.Regexp {
|
|
|
|
|
+ if v, ok := compiledRegexesCache.Load(pattern); ok {
|
|
|
|
|
+ return v.(*regexp.Regexp)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ re, err := regexp.Compile(pattern)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ slog.Warn("Failed on regexp compilation",
|
|
|
|
|
+ slog.String("regex_pattern", pattern),
|
|
|
|
|
+ slog.Any("error", err),
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ compiledRegexesCache.Store(pattern, re)
|
|
|
|
|
+ if compiledRegexesCacheSize.Add(1) >= maxCachedRegexes {
|
|
|
|
|
+ compiledRegexesCache.Clear()
|
|
|
|
|
+ compiledRegexesCacheSize.Store(0)
|
|
|
|
|
+ }
|
|
|
|
|
+ return re
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func ParseRules(userRules, feedRules string) filterRules {
|
|
func ParseRules(userRules, feedRules string) filterRules {
|
|
|
rules := make(filterRules, 0)
|
|
rules := make(filterRules, 0)
|
|
|
for line := range strings.SplitSeq(strings.TrimSpace(userRules), "\n") {
|
|
for line := range strings.SplitSeq(strings.TrimSpace(userRules), "\n") {
|
|
@@ -103,12 +133,8 @@ func matchesEntryRegexRules(regexPattern string, feed *model.Feed, entry *model.
|
|
|
return false, true // No pattern means rule is valid but doesn't match
|
|
return false, true // No pattern means rule is valid but doesn't match
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- compiledRegex, err := regexp.Compile(regexPattern)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- slog.Warn("Failed on regexp compilation",
|
|
|
|
|
- slog.String("regex_pattern", regexPattern),
|
|
|
|
|
- slog.Any("error", err),
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ compiledRegex := cachedRegex(regexPattern)
|
|
|
|
|
+ if compiledRegex == nil {
|
|
|
return false, false // Invalid regex pattern
|
|
return false, false // Invalid regex pattern
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -151,26 +177,28 @@ func matchesEntryFilterRules(rules filterRules, feed *model.Feed, entry *model.E
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func matchesRule(rule filterRule, entry *model.Entry) bool {
|
|
func matchesRule(rule filterRule, entry *model.Entry) bool {
|
|
|
- switch rule.Type {
|
|
|
|
|
- case "EntryDate":
|
|
|
|
|
|
|
+ if rule.Type == "EntryDate" {
|
|
|
return isDateMatchingPattern(rule.Value, entry.Date)
|
|
return isDateMatchingPattern(rule.Value, entry.Date)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ re := cachedRegex(rule.Value)
|
|
|
|
|
+ if re == nil {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ switch rule.Type {
|
|
|
case "EntryTitle":
|
|
case "EntryTitle":
|
|
|
- match, _ := regexp.MatchString(rule.Value, entry.Title)
|
|
|
|
|
- return match
|
|
|
|
|
|
|
+ return re.MatchString(entry.Title)
|
|
|
case "EntryURL":
|
|
case "EntryURL":
|
|
|
- match, _ := regexp.MatchString(rule.Value, entry.URL)
|
|
|
|
|
- return match
|
|
|
|
|
|
|
+ return re.MatchString(entry.URL)
|
|
|
case "EntryCommentsURL":
|
|
case "EntryCommentsURL":
|
|
|
- match, _ := regexp.MatchString(rule.Value, entry.CommentsURL)
|
|
|
|
|
- return match
|
|
|
|
|
|
|
+ return re.MatchString(entry.CommentsURL)
|
|
|
case "EntryContent":
|
|
case "EntryContent":
|
|
|
- match, _ := regexp.MatchString(rule.Value, entry.Content)
|
|
|
|
|
- return match
|
|
|
|
|
|
|
+ return re.MatchString(entry.Content)
|
|
|
case "EntryAuthor":
|
|
case "EntryAuthor":
|
|
|
- match, _ := regexp.MatchString(rule.Value, entry.Author)
|
|
|
|
|
- return match
|
|
|
|
|
|
|
+ return re.MatchString(entry.Author)
|
|
|
case "EntryTag":
|
|
case "EntryTag":
|
|
|
- return containsRegexPattern(rule.Value, entry.Tags)
|
|
|
|
|
|
|
+ return slices.ContainsFunc(entry.Tags, re.MatchString)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
return false
|
|
@@ -227,12 +255,11 @@ func isDateMatchingPattern(pattern string, entryDate time.Time) bool {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func containsRegexPattern(pattern string, items []string) bool {
|
|
func containsRegexPattern(pattern string, items []string) bool {
|
|
|
- for _, item := range items {
|
|
|
|
|
- if matched, _ := regexp.MatchString(pattern, item); matched {
|
|
|
|
|
- return true
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ re := cachedRegex(pattern)
|
|
|
|
|
+ if re == nil {
|
|
|
|
|
+ return false
|
|
|
}
|
|
}
|
|
|
- return false
|
|
|
|
|
|
|
+ return slices.ContainsFunc(items, re.MatchString)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func parseDuration(duration string) (time.Duration, error) {
|
|
func parseDuration(duration string) (time.Duration, error) {
|