mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 11:13:33 +01:00
Bug fixes: - Fix inverted pager detection logic (returned error instead of path) - Fix repo.Clone ignoring destination directory parameter - Fix sheet loading using append on pre-sized slices - Clean up partial files on copy failure - Trim whitespace from editor config Security: - Add path traversal protection for cheatsheet names Performance: - Move regex compilation outside search loop - Replace string concatenation with strings.Join in search Build: - Remove go:generate; embed config and usage as string literals - Parallelize release builds - Add fuzz testing infrastructure Testing: - Improve test coverage from 38.9% to 50.2% - Add fuzz tests for search, filter, tags, and validation Documentation: - Fix inaccurate code examples in HACKING.md - Add missing --conf and --all options to man page - Add ADRs for path traversal, env parsing, and search parallelization - Update CONTRIBUTING.md to reflect project policy Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
191 lines
5.0 KiB
Go
191 lines
5.0 KiB
Go
package sheet
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// FuzzSearchRegex tests the regex compilation and search functionality
|
|
// to ensure it handles malformed patterns gracefully and doesn't suffer
|
|
// from catastrophic backtracking
|
|
func FuzzSearchRegex(f *testing.F) {
|
|
// Add seed corpus with various regex patterns
|
|
// Valid patterns
|
|
f.Add("test", "This is a test string")
|
|
f.Add("(?i)test", "This is a TEST string")
|
|
f.Add("foo|bar", "foo and bar")
|
|
f.Add("^start", "start of line\nnext line")
|
|
f.Add("end$", "at the end\nnext line")
|
|
f.Add("\\d+", "123 numbers 456")
|
|
f.Add("[a-z]+", "lowercase UPPERCASE")
|
|
|
|
// Edge cases and potentially problematic patterns
|
|
f.Add("", "empty pattern")
|
|
f.Add(".", "any character")
|
|
f.Add(".*", "match everything")
|
|
f.Add(".+", "match something")
|
|
f.Add("\\", "backslash")
|
|
f.Add("(", "unclosed paren")
|
|
f.Add(")", "unmatched paren")
|
|
f.Add("[", "unclosed bracket")
|
|
f.Add("]", "unmatched bracket")
|
|
f.Add("[^]", "negated empty class")
|
|
f.Add("(?", "incomplete group")
|
|
|
|
// Patterns that might cause performance issues
|
|
f.Add("(a+)+", "aaaaaaaaaaaaaaaaaaaaaaaab")
|
|
f.Add("(a*)*", "aaaaaaaaaaaaaaaaaaaaaaaab")
|
|
f.Add("(a|a)*", "aaaaaaaaaaaaaaaaaaaaaaaab")
|
|
f.Add("(.*)*", "any text here")
|
|
f.Add("(\\d+)+", "123456789012345678901234567890x")
|
|
|
|
// Unicode patterns
|
|
f.Add("☺", "Unicode ☺ smiley")
|
|
f.Add("[一-龯]", "Chinese 中文 characters")
|
|
f.Add("\\p{L}+", "Unicode letters")
|
|
|
|
// Very long patterns
|
|
f.Add(strings.Repeat("a", 1000), "long pattern")
|
|
f.Add(strings.Repeat("(a|b)", 100), "complex pattern")
|
|
|
|
f.Fuzz(func(t *testing.T, pattern string, text string) {
|
|
// Test 1: Regex compilation should not panic
|
|
var reg *regexp.Regexp
|
|
var compileErr error
|
|
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("regexp.Compile panicked with pattern %q: %v", pattern, r)
|
|
}
|
|
}()
|
|
|
|
reg, compileErr = regexp.Compile(pattern)
|
|
}()
|
|
|
|
// If compilation failed, that's OK - we're testing error handling
|
|
if compileErr != nil {
|
|
// This is expected for invalid patterns
|
|
return
|
|
}
|
|
|
|
// Test 2: Create a sheet and test Search method
|
|
sheet := Sheet{
|
|
Title: "test",
|
|
Text: text,
|
|
}
|
|
|
|
// Search should not panic
|
|
var result string
|
|
done := make(chan bool, 1)
|
|
|
|
go func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("Search panicked with pattern %q on text %q: %v", pattern, text, r)
|
|
}
|
|
done <- true
|
|
}()
|
|
|
|
result = sheet.Search(reg)
|
|
}()
|
|
|
|
// Timeout after 100ms to catch catastrophic backtracking
|
|
select {
|
|
case <-done:
|
|
// Search completed successfully
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Errorf("Search timed out (possible catastrophic backtracking) with pattern %q on text %q", pattern, text)
|
|
}
|
|
|
|
// Test 3: Verify search result invariants
|
|
if result != "" {
|
|
// The Search function splits by "\n\n", so we need to compare using the same logic
|
|
resultLines := strings.Split(result, "\n\n")
|
|
textLines := strings.Split(text, "\n\n")
|
|
|
|
// Every result line should exist in the original text lines
|
|
for _, rLine := range resultLines {
|
|
found := false
|
|
for _, tLine := range textLines {
|
|
if rLine == tLine {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found && rLine != "" {
|
|
t.Errorf("Search result contains line not in original text: %q", rLine)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzSearchCatastrophicBacktracking specifically tests for regex patterns
|
|
// that could cause performance issues
|
|
func FuzzSearchCatastrophicBacktracking(f *testing.F) {
|
|
// Seed with patterns known to potentially cause issues
|
|
f.Add("a", 10, 5)
|
|
f.Add("x", 20, 3)
|
|
|
|
f.Fuzz(func(t *testing.T, char string, repeats int, groups int) {
|
|
// Limit the size to avoid memory issues in the test
|
|
if repeats > 30 || repeats < 0 || groups > 10 || groups < 0 || len(char) > 5 {
|
|
t.Skip("Skipping invalid or overly large test case")
|
|
}
|
|
|
|
// Construct patterns that might cause backtracking
|
|
patterns := []string{
|
|
strings.Repeat(char, repeats),
|
|
"(" + char + "+)+",
|
|
"(" + char + "*)*",
|
|
"(" + char + "|" + char + ")*",
|
|
}
|
|
|
|
// Add nested groups
|
|
if groups > 0 && groups < 10 {
|
|
nested := char
|
|
for i := 0; i < groups; i++ {
|
|
nested = "(" + nested + ")+"
|
|
}
|
|
patterns = append(patterns, nested)
|
|
}
|
|
|
|
// Test text that might trigger backtracking
|
|
testText := strings.Repeat(char, repeats) + "x"
|
|
|
|
for _, pattern := range patterns {
|
|
// Try to compile the pattern
|
|
reg, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
// Invalid pattern, skip
|
|
continue
|
|
}
|
|
|
|
// Test with timeout
|
|
done := make(chan bool, 1)
|
|
|
|
go func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("Search panicked with backtracking pattern %q: %v", pattern, r)
|
|
}
|
|
done <- true
|
|
}()
|
|
|
|
sheet := Sheet{Text: testText}
|
|
_ = sheet.Search(reg)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// Completed successfully
|
|
case <-time.After(50 * time.Millisecond):
|
|
t.Logf("Warning: potential backtracking issue with pattern %q (completed slowly)", pattern)
|
|
}
|
|
}
|
|
})
|
|
}
|