diff --git a/format.go b/format.go index 17061e6..ae984bf 100644 --- a/format.go +++ b/format.go @@ -232,3 +232,64 @@ func IsValidUser(name string) bool { return true } + +// ToRFC1459 converts a string to the stripped down conversion within RFC +// 1459. This will do things like replace an "A" with an "a", "[]" with "{}", +// and so forth. Useful to compare two nicknames. +func ToRFC1459(input string) (out string) { + for i := 0; i < len(input); i++ { + if input[i] >= 65 && input[i] <= 94 { + out += string(rune(input[i]) + 32) + } else { + out += string(input[i]) + } + } + + return out +} + +const globChar = "*" + +// Glob will test a string pattern, potentially containing globs, against a +// string. The glob character is *. +func Glob(input, match string) bool { + // Empty pattern. + if match == "" { + return input == match + } + + // If a glob, match all. + if match == globChar { + return true + } + + parts := strings.Split(match, globChar) + + if len(parts) == 1 { + // No globs, test for equality. + return input == match + } + + leadingGlob, trailingGlob := strings.HasPrefix(match, globChar), strings.HasSuffix(match, globChar) + last := len(parts) - 1 + + // Check prefix first. + if !leadingGlob && !strings.HasPrefix(input, parts[0]) { + return false + } + + // Check middle section. + for i := 1; i < last; i++ { + if !strings.Contains(input, parts[i]) { + return false + } + + // Trim already-evaluated text from input during loop over match + // text. + idx := strings.Index(input, parts[i]) + len(parts[i]) + input = input[idx:] + } + + // Check suffix last. + return trailingGlob || strings.HasSuffix(input, parts[last]) +} diff --git a/format_test.go b/format_test.go index a8323f3..676e1ca 100644 --- a/format_test.go +++ b/format_test.go @@ -5,6 +5,7 @@ package girc import ( + "strings" "testing" ) @@ -215,3 +216,144 @@ func TestIsValidUser(t *testing.T) { } } } + +func TestToRFC1459(t *testing.T) { + cases := []struct { + in string + want string + }{ + {"", ""}, + {"a", "a"}, + {"abcd", "abcd"}, + {"AbcD", "abcd"}, + {"!@#$%^&*()_+-=", "!@#$%~&*()_+-="}, + {"Abcd[]", "abcd{}"}, + } + + for _, tt := range cases { + if got := ToRFC1459(tt.in); got != tt.want { + t.Errorf("ToRFC1459() = %q, want %q", got, tt.want) + } + } + + return +} + +func BenchmarkGlob(b *testing.B) { + for i := 0; i < b.N; i++ { + if !Glob("*quick*fox*dog", "The quick brown fox jumped over the lazy dog") { + b.Fatalf("should match") + } + } + + return +} + +func testGlobMatch(t *testing.T, subj, pattern string) { + if !Glob(subj, pattern) { + t.Fatalf("'%s' should match '%s'", pattern, subj) + } + + return +} + +func testGlobNoMatch(t *testing.T, subj, pattern string) { + if Glob(subj, pattern) { + t.Fatalf("'%s' should not match '%s'", pattern, subj) + } + + return +} + +func TestEmptyPattern(t *testing.T) { + testGlobMatch(t, "", "") + testGlobNoMatch(t, "test", "") + + return +} + +func TestEmptySubject(t *testing.T) { + cases := []string{ + "", + "*", + "**", + "***", + "****************", + strings.Repeat("*", 1000000), + } + + for _, pattern := range cases { + testGlobMatch(t, "", pattern) + } + + cases = []string{ + // No globs/non-glob characters. + "test", + "*test*", + + // Trailing characters. + "*x", + "*****************x", + strings.Repeat("*", 1000000) + "x", + + // Leading characters. + "x*", + "x*****************", + "x" + strings.Repeat("*", 1000000), + + // Mixed leading/trailing characters. + "x*x", + "x****************x", + "x" + strings.Repeat("*", 1000000) + "x", + } + + for _, pattern := range cases { + testGlobNoMatch(t, pattern, "") + } + + return +} + +func TestPatternWithoutGlobs(t *testing.T) { + testGlobMatch(t, "test", "test") + + return +} + +func TestGlob(t *testing.T) { + cases := []string{ + "*test", // Leading. + "this*", // Trailing. + "this*test", // Middle. + "*is *", // String in between two. + "*is*a*", // Lots. + "**test**", // Double glob characters. + "**is**a***test*", // Varying number. + "* *", // White space between. + "*", // Lone. + "**********", // Nothing but globs. + "*Ѿ*", // Unicode. + "*is a ϗѾ *", // Mixed ASCII/unicode. + } + + for _, pattern := range cases { + testGlobMatch(t, "this is a ϗѾ test", pattern) + } + + cases = []string{ + "test*", // Implicit substring match. + "*is", // Partial match. + "*no*", // Globs without a match between them. + " ", // Plain white space. + "* ", // Trailing white space. + " *", // Leading white space. + "*ʤ*", // Non-matching unicode. + } + + // Non-matches + for _, pattern := range cases { + testGlobNoMatch(t, "this is a test", pattern) + } + + return +}