mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 03:03:32 +01:00
chore: housekeeping and refactoring (bump to 4.7.1)
- Remove unused parameters, dead files, and inaccurate doc.go files - Extract shared helpers, eliminate duplication - Rename cheatpath.Cheatpath to cheatpath.Path - Optimize filesystem walks (WalkDir, skip .git) - Move sheet name validation to sheet.Validate - Move integration tests to test/integration/ - Consolidate internal/mock into mocks/ - Move fuzz.sh to test/ - Inline loadSheets helper into command callers - Extract config.New into its own file - Fix stale references in HACKING.md and CLAUDE.md - Restore plan9 build target - Remove redundant and low-value tests - Clean up project documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
@@ -16,45 +17,26 @@ func TestColorize(t *testing.T) {
|
||||
}
|
||||
|
||||
// mock a sheet
|
||||
original := "echo 'foo'"
|
||||
s := Sheet{
|
||||
Text: "echo 'foo'",
|
||||
Text: original,
|
||||
}
|
||||
|
||||
// colorize the sheet text
|
||||
s.Colorize(conf)
|
||||
|
||||
// initialize expectations
|
||||
want := "[38;2;181;137;0mecho[0m[38;2;147;161;161m"
|
||||
want += " [0m[38;2;42;161;152m'foo'[0m"
|
||||
// assert that the text was modified (colorization applied)
|
||||
if s.Text == original {
|
||||
t.Error("Colorize did not modify sheet text")
|
||||
}
|
||||
|
||||
// assert
|
||||
if s.Text != want {
|
||||
t.Errorf("failed to colorize sheet: want: %s, got: %s", want, s.Text)
|
||||
// assert that ANSI escape codes are present
|
||||
if !strings.Contains(s.Text, "\x1b[") && !strings.Contains(s.Text, "[0m") {
|
||||
t.Errorf("colorized text does not contain ANSI escape codes: %q", s.Text)
|
||||
}
|
||||
|
||||
// assert that the original content is still present within the colorized output
|
||||
if !strings.Contains(s.Text, "echo") || !strings.Contains(s.Text, "foo") {
|
||||
t.Errorf("colorized text lost original content: %q", s.Text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestColorizeError tests the error handling in Colorize
|
||||
func TestColorizeError(_ *testing.T) {
|
||||
// Create a sheet with content
|
||||
sheet := Sheet{
|
||||
Text: "some text",
|
||||
Syntax: "invalidlexer12345", // Use an invalid lexer that might cause issues
|
||||
}
|
||||
|
||||
// Create a config with invalid formatter/style
|
||||
conf := config.Config{
|
||||
Formatter: "invalidformatter",
|
||||
Style: "invalidstyle",
|
||||
}
|
||||
|
||||
// Store original text
|
||||
originalText := sheet.Text
|
||||
|
||||
// Colorize should not panic even with invalid settings
|
||||
sheet.Colorize(conf)
|
||||
|
||||
// The text might be unchanged if there was an error, or it might be colorized
|
||||
// We're mainly testing that it doesn't panic
|
||||
_ = sheet.Text
|
||||
_ = originalText
|
||||
}
|
||||
|
||||
@@ -10,15 +10,12 @@ import (
|
||||
// TestCopyErrors tests error cases for the Copy method
|
||||
func TestCopyErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func() (*Sheet, string, func())
|
||||
wantErr bool
|
||||
errMsg string
|
||||
name string
|
||||
setup func() (*Sheet, string, func())
|
||||
}{
|
||||
{
|
||||
name: "source file does not exist",
|
||||
setup: func() (*Sheet, string, func()) {
|
||||
// Create a sheet with non-existent path
|
||||
sheet := &Sheet{
|
||||
Title: "test",
|
||||
Path: "/non/existent/file.txt",
|
||||
@@ -30,13 +27,10 @@ func TestCopyErrors(t *testing.T) {
|
||||
}
|
||||
return sheet, dest, cleanup
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "failed to open cheatsheet",
|
||||
},
|
||||
{
|
||||
name: "destination directory creation fails",
|
||||
setup: func() (*Sheet, string, func()) {
|
||||
// Create a source file
|
||||
src, err := os.CreateTemp("", "copy-test-src-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
@@ -50,13 +44,11 @@ func TestCopyErrors(t *testing.T) {
|
||||
CheatPath: "test",
|
||||
}
|
||||
|
||||
// Create a file where we want a directory
|
||||
blockerFile := filepath.Join(os.TempDir(), "copy-blocker-file")
|
||||
if err := os.WriteFile(blockerFile, []byte("blocker"), 0644); err != nil {
|
||||
t.Fatalf("failed to create blocker file: %v", err)
|
||||
}
|
||||
|
||||
// Try to create dest under the blocker file (will fail)
|
||||
dest := filepath.Join(blockerFile, "subdir", "dest.txt")
|
||||
|
||||
cleanup := func() {
|
||||
@@ -65,13 +57,10 @@ func TestCopyErrors(t *testing.T) {
|
||||
}
|
||||
return sheet, dest, cleanup
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "failed to create directory",
|
||||
},
|
||||
{
|
||||
name: "destination file creation fails",
|
||||
setup: func() (*Sheet, string, func()) {
|
||||
// Create a source file
|
||||
src, err := os.CreateTemp("", "copy-test-src-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
@@ -85,7 +74,6 @@ func TestCopyErrors(t *testing.T) {
|
||||
CheatPath: "test",
|
||||
}
|
||||
|
||||
// Create a directory where we want the file
|
||||
destDir := filepath.Join(os.TempDir(), "copy-test-dir")
|
||||
if err := os.Mkdir(destDir, 0755); err != nil && !os.IsExist(err) {
|
||||
t.Fatalf("failed to create dest dir: %v", err)
|
||||
@@ -97,8 +85,6 @@ func TestCopyErrors(t *testing.T) {
|
||||
}
|
||||
return sheet, destDir, cleanup
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "failed to create outfile",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -108,43 +94,27 @@ func TestCopyErrors(t *testing.T) {
|
||||
defer cleanup()
|
||||
|
||||
err := sheet.Copy(dest)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Copy() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil && tt.errMsg != "" {
|
||||
if !contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("Copy() error = %v, want error containing %q", err, tt.errMsg)
|
||||
}
|
||||
if err == nil {
|
||||
t.Error("Copy() expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCopyIOError tests the io.Copy error case
|
||||
func TestCopyIOError(t *testing.T) {
|
||||
// This is difficult to test without mocking io.Copy
|
||||
// The error case would occur if the source file is modified
|
||||
// or removed after opening but before copying
|
||||
t.Skip("Skipping io.Copy error test - requires file system race condition")
|
||||
}
|
||||
|
||||
// TestCopyCleanupOnError verifies that partially written files are cleaned up on error
|
||||
func TestCopyCleanupOnError(t *testing.T) {
|
||||
// TestCopyUnreadableSource verifies that Copy returns an error when the source
|
||||
// file cannot be opened (e.g., permission denied).
|
||||
func TestCopyUnreadableSource(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("chmod does not restrict reads on Windows")
|
||||
}
|
||||
|
||||
// Create a source file that we'll make unreadable after opening
|
||||
src, err := os.CreateTemp("", "copy-test-cleanup-*")
|
||||
src, err := os.CreateTemp("", "copy-test-unreadable-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(src.Name())
|
||||
|
||||
// Write some content
|
||||
content := "test content for cleanup"
|
||||
if _, err := src.WriteString(content); err != nil {
|
||||
if _, err := src.WriteString("test content"); err != nil {
|
||||
t.Fatalf("failed to write content: %v", err)
|
||||
}
|
||||
src.Close()
|
||||
@@ -155,38 +125,21 @@ func TestCopyCleanupOnError(t *testing.T) {
|
||||
CheatPath: "test",
|
||||
}
|
||||
|
||||
// Destination path
|
||||
dest := filepath.Join(os.TempDir(), "copy-cleanup-test.txt")
|
||||
defer os.Remove(dest) // Clean up if test fails
|
||||
dest := filepath.Join(os.TempDir(), "copy-unreadable-test.txt")
|
||||
defer os.Remove(dest)
|
||||
|
||||
// Make the source file unreadable (simulating a read error during copy)
|
||||
// This is platform-specific, but should work on Unix-like systems
|
||||
if err := os.Chmod(src.Name(), 0000); err != nil {
|
||||
t.Skip("Cannot change file permissions on this platform")
|
||||
}
|
||||
defer os.Chmod(src.Name(), 0644) // Restore permissions for cleanup
|
||||
defer os.Chmod(src.Name(), 0644)
|
||||
|
||||
// Attempt to copy - this should fail during io.Copy
|
||||
err = sheet.Copy(dest)
|
||||
if err == nil {
|
||||
t.Error("Expected Copy to fail with permission error")
|
||||
t.Error("expected Copy to fail with permission error")
|
||||
}
|
||||
|
||||
// Verify the destination file was cleaned up
|
||||
// Destination should not exist since the error occurs before it is created
|
||||
if _, err := os.Stat(dest); !os.IsNotExist(err) {
|
||||
t.Error("Destination file should have been removed after copy failure")
|
||||
t.Error("destination file should not exist after open failure")
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
|
||||
}
|
||||
|
||||
func containsHelper(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
// Package sheet provides functionality for parsing and managing individual cheat sheets.
|
||||
//
|
||||
// A sheet represents a single cheatsheet file containing helpful commands, notes,
|
||||
// or documentation. Sheets can include optional YAML frontmatter for metadata
|
||||
// such as tags and syntax highlighting preferences.
|
||||
//
|
||||
// # Sheet Format
|
||||
//
|
||||
// Sheets are plain text files that may begin with YAML frontmatter:
|
||||
//
|
||||
// ---
|
||||
// syntax: bash
|
||||
// tags: [networking, linux, ssh]
|
||||
// ---
|
||||
// # Connect to remote server
|
||||
// ssh user@hostname
|
||||
//
|
||||
// # Copy files over SSH
|
||||
// scp local_file user@hostname:/remote/path
|
||||
//
|
||||
// The frontmatter is optional. If omitted, the sheet will use default values.
|
||||
//
|
||||
// # Core Types
|
||||
//
|
||||
// The Sheet type contains:
|
||||
// - Title: The sheet's name (derived from filename)
|
||||
// - Path: Full filesystem path to the sheet
|
||||
// - Text: The content of the sheet (without frontmatter)
|
||||
// - Tags: Categories assigned to the sheet
|
||||
// - Syntax: Language hint for syntax highlighting
|
||||
// - ReadOnly: Whether the sheet can be modified
|
||||
//
|
||||
// Key Functions
|
||||
//
|
||||
// - New: Creates a new Sheet from a file path
|
||||
// - Parse: Extracts frontmatter and content from sheet text
|
||||
// - Search: Searches sheet content using regular expressions
|
||||
// - Colorize: Applies syntax highlighting to sheet content
|
||||
//
|
||||
// # Syntax Highlighting
|
||||
//
|
||||
// The package integrates with the Chroma library to provide syntax highlighting.
|
||||
// Supported languages include bash, python, go, javascript, and many others.
|
||||
// The syntax can be specified in the frontmatter or auto-detected.
|
||||
//
|
||||
// Example Usage
|
||||
//
|
||||
// // Load a sheet from disk
|
||||
// s, err := sheet.New("/path/to/sheet", []string{"personal"}, false)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// // Search for content
|
||||
// matches, err := s.Search("ssh", false)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// // Apply syntax highlighting
|
||||
// colorized, err := s.Colorize(config)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
package sheet
|
||||
@@ -27,22 +27,3 @@ func TestParseWindowsLineEndings(t *testing.T) {
|
||||
t.Errorf("failed to parse syntax: want: %s, got: %s", want, fm.Syntax)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseInvalidYAML tests parsing with invalid YAML in frontmatter
|
||||
func TestParseInvalidYAML(t *testing.T) {
|
||||
// stub our cheatsheet content with invalid YAML
|
||||
markdown := `---
|
||||
syntax: go
|
||||
tags: [ test
|
||||
unclosed bracket
|
||||
---
|
||||
To foo the bar: baz`
|
||||
|
||||
// parse the frontmatter
|
||||
_, _, err := parse(markdown)
|
||||
|
||||
// assert that an error was returned for invalid YAML
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid YAML, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ To foo the bar: baz`
|
||||
t.Errorf("failed to parse tags: want: %s, got: %s", want, fm.Tags[0])
|
||||
}
|
||||
if len(fm.Tags) != 1 {
|
||||
t.Errorf("failed to parse tags: want: len 0, got: len %d", len(fm.Tags))
|
||||
t.Errorf("failed to parse tags: want: len 1, got: len %d", len(fm.Tags))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,69 +122,3 @@ func FuzzSearchRegex(f *testing.F) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cheat/cheat/internal/mock"
|
||||
"github.com/cheat/cheat/mocks"
|
||||
)
|
||||
|
||||
// TestSheetSuccess asserts that sheets initialize properly
|
||||
@@ -14,7 +14,7 @@ func TestSheetSuccess(t *testing.T) {
|
||||
sheet, err := New(
|
||||
"foo",
|
||||
"community",
|
||||
mock.Path("sheet/foo"),
|
||||
mocks.Path("sheet/foo"),
|
||||
[]string{"alpha", "bravo"},
|
||||
false,
|
||||
)
|
||||
@@ -27,10 +27,10 @@ func TestSheetSuccess(t *testing.T) {
|
||||
t.Errorf("failed to init title: want: foo, got: %s", sheet.Title)
|
||||
}
|
||||
|
||||
if sheet.Path != mock.Path("sheet/foo") {
|
||||
if sheet.Path != mocks.Path("sheet/foo") {
|
||||
t.Errorf(
|
||||
"failed to init path: want: %s, got: %s",
|
||||
mock.Path("sheet/foo"),
|
||||
mocks.Path("sheet/foo"),
|
||||
sheet.Path,
|
||||
)
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func TestSheetFailure(t *testing.T) {
|
||||
_, err := New(
|
||||
"foo",
|
||||
"community",
|
||||
mock.Path("/does-not-exist"),
|
||||
mocks.Path("/does-not-exist"),
|
||||
[]string{"alpha", "bravo"},
|
||||
false,
|
||||
)
|
||||
@@ -80,7 +80,7 @@ func TestSheetFrontMatterFailure(t *testing.T) {
|
||||
_, err := New(
|
||||
"foo",
|
||||
"community",
|
||||
mock.Path("sheet/bad-fm"),
|
||||
mocks.Path("sheet/bad-fm"),
|
||||
[]string{"alpha", "bravo"},
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
package sheet
|
||||
|
||||
import "slices"
|
||||
|
||||
// Tagged returns true if a sheet was tagged with `needle`
|
||||
func (s *Sheet) Tagged(needle string) bool {
|
||||
|
||||
// if any of the tags match `needle`, return `true`
|
||||
for _, tag := range s.Tags {
|
||||
if tag == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, return `false`
|
||||
return false
|
||||
return slices.Contains(s.Tags, needle)
|
||||
}
|
||||
|
||||
40
internal/sheet/validate.go
Normal file
40
internal/sheet/validate.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Validate ensures that a cheatsheet name does not contain
|
||||
// directory traversal sequences or other potentially dangerous patterns.
|
||||
func Validate(name string) error {
|
||||
// Reject empty names
|
||||
if name == "" {
|
||||
return fmt.Errorf("cheatsheet name cannot be empty")
|
||||
}
|
||||
|
||||
// Reject names containing directory traversal
|
||||
if strings.Contains(name, "..") {
|
||||
return fmt.Errorf("cheatsheet name cannot contain '..'")
|
||||
}
|
||||
|
||||
// Reject absolute paths
|
||||
if filepath.IsAbs(name) {
|
||||
return fmt.Errorf("cheatsheet name cannot be an absolute path")
|
||||
}
|
||||
|
||||
// Reject names that start with ~ (home directory expansion)
|
||||
if strings.HasPrefix(name, "~") {
|
||||
return fmt.Errorf("cheatsheet name cannot start with '~'")
|
||||
}
|
||||
|
||||
// Reject hidden files (files that start with a dot)
|
||||
// We don't display hidden files, so we shouldn't create them
|
||||
filename := filepath.Base(name)
|
||||
if strings.HasPrefix(filename, ".") {
|
||||
return fmt.Errorf("cheatsheet name cannot start with '.' (hidden files are not supported)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
169
internal/sheet/validate_fuzz_test.go
Normal file
169
internal/sheet/validate_fuzz_test.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// FuzzValidate tests the Validate function with fuzzing
|
||||
// to ensure it properly prevents path traversal and other security issues
|
||||
func FuzzValidate(f *testing.F) {
|
||||
// Add seed corpus with various valid and malicious inputs
|
||||
// Valid names
|
||||
f.Add("docker")
|
||||
f.Add("docker/compose")
|
||||
f.Add("lang/go/slice")
|
||||
f.Add("my-cheat_sheet")
|
||||
f.Add("file.txt")
|
||||
f.Add("a")
|
||||
f.Add("123")
|
||||
|
||||
// Path traversal attempts
|
||||
f.Add("..")
|
||||
f.Add("../etc/passwd")
|
||||
f.Add("foo/../bar")
|
||||
f.Add("foo/../../etc/passwd")
|
||||
f.Add("..\\windows\\system32")
|
||||
f.Add("foo\\..\\..\\windows")
|
||||
|
||||
// Encoded traversal attempts
|
||||
f.Add("%2e%2e")
|
||||
f.Add("%2e%2e%2f")
|
||||
f.Add("..%2f")
|
||||
f.Add("%2e.")
|
||||
f.Add(".%2e")
|
||||
f.Add("\x2e\x2e")
|
||||
f.Add("\\x2e\\x2e")
|
||||
|
||||
// Unicode and special characters
|
||||
f.Add("€test")
|
||||
f.Add("test€")
|
||||
f.Add("中文")
|
||||
f.Add("🎉emoji")
|
||||
f.Add("\x00null")
|
||||
f.Add("test\x00null")
|
||||
f.Add("\nnewline")
|
||||
f.Add("test\ttab")
|
||||
|
||||
// Absolute paths
|
||||
f.Add("/etc/passwd")
|
||||
f.Add("C:\\Windows\\System32")
|
||||
f.Add("\\\\server\\share")
|
||||
f.Add("//server/share")
|
||||
|
||||
// Home directory
|
||||
f.Add("~")
|
||||
f.Add("~/config")
|
||||
f.Add("~user/file")
|
||||
|
||||
// Hidden files
|
||||
f.Add(".hidden")
|
||||
f.Add("dir/.hidden")
|
||||
f.Add(".git/config")
|
||||
|
||||
// Edge cases
|
||||
f.Add("")
|
||||
f.Add(" ")
|
||||
f.Add(" ")
|
||||
f.Add("\t")
|
||||
f.Add(".")
|
||||
f.Add("./")
|
||||
f.Add("./file")
|
||||
f.Add(".../")
|
||||
f.Add("...")
|
||||
f.Add("....")
|
||||
|
||||
// Very long names
|
||||
f.Add(strings.Repeat("a", 255))
|
||||
f.Add(strings.Repeat("a/", 100) + "file")
|
||||
f.Add(strings.Repeat("../", 50) + "etc/passwd")
|
||||
|
||||
f.Fuzz(func(t *testing.T, input string) {
|
||||
// The function should never panic
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Validate panicked with input %q: %v", input, r)
|
||||
}
|
||||
}()
|
||||
|
||||
err := Validate(input)
|
||||
|
||||
// Security invariants that must always hold
|
||||
if err == nil {
|
||||
// If validation passed, verify security properties
|
||||
|
||||
// Should not contain ".." for path traversal
|
||||
if strings.Contains(input, "..") {
|
||||
t.Errorf("validation passed but input contains '..': %q", input)
|
||||
}
|
||||
|
||||
// Should not be empty
|
||||
if input == "" {
|
||||
t.Error("validation passed for empty input")
|
||||
}
|
||||
|
||||
// Should not start with ~ (home directory)
|
||||
if strings.HasPrefix(input, "~") {
|
||||
t.Errorf("validation passed but input starts with '~': %q", input)
|
||||
}
|
||||
|
||||
// Base filename should not start with .
|
||||
parts := strings.Split(input, "/")
|
||||
if len(parts) > 0 {
|
||||
lastPart := parts[len(parts)-1]
|
||||
if strings.HasPrefix(lastPart, ".") && lastPart != "." {
|
||||
t.Errorf("validation passed but filename starts with '.': %q", input)
|
||||
}
|
||||
}
|
||||
|
||||
// Additional check: result should be valid UTF-8
|
||||
if !utf8.ValidString(input) {
|
||||
// While the function doesn't explicitly check this,
|
||||
// we want to ensure it handles invalid UTF-8 gracefully
|
||||
t.Logf("validation passed for invalid UTF-8: %q", input)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzValidatePathTraversal specifically targets path traversal bypasses
|
||||
func FuzzValidatePathTraversal(f *testing.F) {
|
||||
// Seed corpus focusing on path traversal variations
|
||||
f.Add("..", "/", "")
|
||||
f.Add("", "..", "/")
|
||||
f.Add("a", "b", "c")
|
||||
|
||||
f.Fuzz(func(t *testing.T, prefix string, middle string, suffix string) {
|
||||
// Construct various path traversal attempts
|
||||
inputs := []string{
|
||||
prefix + ".." + suffix,
|
||||
prefix + "/.." + suffix,
|
||||
prefix + "\\.." + suffix,
|
||||
prefix + middle + ".." + suffix,
|
||||
prefix + "../" + middle + suffix,
|
||||
prefix + "..%2f" + suffix,
|
||||
prefix + "%2e%2e" + suffix,
|
||||
prefix + "%2e%2e%2f" + suffix,
|
||||
}
|
||||
|
||||
for _, input := range inputs {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Validate panicked with constructed input %q: %v", input, r)
|
||||
}
|
||||
}()
|
||||
|
||||
err := Validate(input)
|
||||
|
||||
// If the input contains literal "..", it must be rejected
|
||||
if strings.Contains(input, "..") && err == nil {
|
||||
t.Errorf("validation incorrectly passed for input containing '..': %q", input)
|
||||
}
|
||||
}()
|
||||
}
|
||||
})
|
||||
}
|
||||
113
internal/sheet/validate_test.go
Normal file
113
internal/sheet/validate_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
// Valid names
|
||||
{
|
||||
name: "simple name",
|
||||
input: "docker",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "name with slash",
|
||||
input: "docker/compose",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "name with multiple slashes",
|
||||
input: "lang/go/slice",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "name with dash and underscore",
|
||||
input: "my-cheat_sheet",
|
||||
wantErr: false,
|
||||
},
|
||||
// Invalid names
|
||||
{
|
||||
name: "empty name",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
errMsg: "empty",
|
||||
},
|
||||
{
|
||||
name: "parent directory traversal",
|
||||
input: "../etc/passwd",
|
||||
wantErr: true,
|
||||
errMsg: "'..'",
|
||||
},
|
||||
{
|
||||
name: "complex traversal",
|
||||
input: "foo/../../etc/passwd",
|
||||
wantErr: true,
|
||||
errMsg: "'..'",
|
||||
},
|
||||
{
|
||||
name: "absolute path unix",
|
||||
input: "/etc/passwd",
|
||||
wantErr: runtime.GOOS != "windows", // /etc/passwd is not absolute on Windows
|
||||
errMsg: "absolute",
|
||||
},
|
||||
{
|
||||
name: "absolute path windows",
|
||||
input: `C:\evil`,
|
||||
wantErr: runtime.GOOS == "windows", // C:\evil is not absolute on Unix
|
||||
errMsg: "absolute",
|
||||
},
|
||||
{
|
||||
name: "home directory",
|
||||
input: "~/secrets",
|
||||
wantErr: true,
|
||||
errMsg: "'~'",
|
||||
},
|
||||
{
|
||||
name: "just dots",
|
||||
input: "..",
|
||||
wantErr: true,
|
||||
errMsg: "'..'",
|
||||
},
|
||||
{
|
||||
name: "hidden file not allowed",
|
||||
input: ".hidden",
|
||||
wantErr: true,
|
||||
errMsg: "cannot start with '.'",
|
||||
},
|
||||
{
|
||||
name: "current dir is ok",
|
||||
input: "./current",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nested hidden file not allowed",
|
||||
input: "config/.gitignore",
|
||||
wantErr: true,
|
||||
errMsg: "cannot start with '.'",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := Validate(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil && tt.errMsg != "" {
|
||||
if !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("Validate(%q) error = %v, want error containing %q", tt.input, err, tt.errMsg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user