Repository: oragono/oragono (go)

Issue:
consolidate all globbing code
We currently have:

1. ircmatch, for klines
2. An ad-hoc implementation for compiling all ban masks into a single regex (see #983)
3. `utils.CompileGlob`, which is used for websocket origin matching and guest format nicknames

Let's consolidate all of this somehow. We should investigate whether submatch support (currently added unconditionally by `utils.CompileGlob`) imposes any runtime performance cost when calling plain `Match` (as opposed to `FindStringSubmatch`).

Pull request:
fix #1003

Patch:
diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go
index 6db2384d..95314509 100644
--- a/irc/client_lookup_set.go
+++ b/irc/client_lookup_set.go
@@ -7,14 +7,12 @@ package irc
 import (
 	"regexp"
 	"strings"
+	"sync"
 	"time"
 
-	"github.com/goshuirc/irc-go/ircmatch"
-
 	"github.com/oragono/oragono/irc/caps"
 	"github.com/oragono/oragono/irc/modes"
-
-	"sync"
+	"github.com/oragono/oragono/irc/utils"
 )
 
 // ClientManager keeps track of clients by nick, enforcing uniqueness of casefolded nicks
@@ -301,12 +299,16 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
 	if err != nil {
 		return set
 	}
-	matcher := ircmatch.MakeMatch(userhost)
+	matcher, err := utils.CompileGlob(userhost, false)
+	if err != nil {
+		// not much we can do here
+		return
+	}
 
 	clients.RLock()
 	defer clients.RUnlock()
 	for _, client := range clients.byNick {
-		if matcher.Match(client.NickMaskCasefolded()) {
+		if matcher.MatchString(client.NickMaskCasefolded()) {
 			set.Add(client)
 		}
 	}
@@ -330,8 +332,9 @@ type MaskInfo struct {
 // UserMaskSet holds a set of client masks and lets you match  hostnames to them.
 type UserMaskSet struct {
 	sync.RWMutex
-	masks  map[string]MaskInfo
-	regexp *regexp.Regexp
+	serialCacheUpdateMutex sync.Mutex
+	masks                  map[string]MaskInfo
+	regexp                 *regexp.Regexp
 }
 
 func NewUserMaskSet() *UserMaskSet {
@@ -345,6 +348,9 @@ func (set *UserMaskSet) Add(mask, creatorNickmask, creatorAccount string) (maskA
 		return
 	}
 
+	set.serialCacheUpdateMutex.Lock()
+	defer set.serialCacheUpdateMutex.Unlock()
+
 	set.Lock()
 	if set.masks == nil {
 		set.masks = make(map[string]MaskInfo)
@@ -373,6 +379,9 @@ func (set *UserMaskSet) Remove(mask string) (maskRemoved string, err error) {
 		return
 	}
 
+	set.serialCacheUpdateMutex.Lock()
+	defer set.serialCacheUpdateMutex.Unlock()
+
 	set.Lock()
 	_, removed := set.masks[mask]
 	if removed {
@@ -423,38 +432,15 @@ func (set *UserMaskSet) Length() int {
 	return len(set.masks)
 }
 
-// setRegexp generates a regular expression from the set of user mask
-// strings. Masks are split at the two types of wildcards, `*` and
-// `?`. All the pieces are meta-escaped. `*` is replaced with `.*`,
-// the regexp equivalent. Likewise, `?` is replaced with `.`. The
-// parts are re-joined and finally all masks are joined into a big
-// or-expression.
 func (set *UserMaskSet) setRegexp() {
-	var re *regexp.Regexp
-
 	set.RLock()
 	maskExprs := make([]string, len(set.masks))
-	index := 0
 	for mask := range set.masks {
-		manyParts := strings.Split(mask, "*")
-		manyExprs := make([]string, len(manyParts))
-		for mindex, manyPart := range manyParts {
-			oneParts := strings.Split(manyPart, "?")
-			oneExprs := make([]string, len(oneParts))
-			for oindex, onePart := range oneParts {
-				oneExprs[oindex] = regexp.QuoteMeta(onePart)
-			}
-			manyExprs[mindex] = strings.Join(oneExprs, ".")
-		}
-		maskExprs[index] = strings.Join(manyExprs, ".*")
-		index++
+		maskExprs = append(maskExprs, mask)
 	}
 	set.RUnlock()
 
-	if index > 0 {
-		expr := "^(" + strings.Join(maskExprs, "|") + ")$"
-		re, _ = regexp.Compile(expr)
-	}
+	re, _ := utils.CompileMasks(maskExprs)
 
 	set.Lock()
 	set.regexp = re
diff --git a/irc/config.go b/irc/config.go
index 982afc3a..68d85c7c 100644
--- a/irc/config.go
+++ b/irc/config.go
@@ -853,7 +853,7 @@ func LoadConfig(filename string) (config *Config, err error) {
 	}
 
 	for _, glob := range config.Server.WebSockets.AllowedOrigins {
-		globre, err := utils.CompileGlob(glob)
+		globre, err := utils.CompileGlob(glob, false)
 		if err != nil {
 			return nil, fmt.Errorf("invalid websocket allowed-origin expression: %s", glob)
 		}
@@ -1219,7 +1219,7 @@ func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard,
 		return
 	}
 
-	standard, err = utils.CompileGlob(guestFormat)
+	standard, err = utils.CompileGlob(guestFormat, true)
 	if err != nil {
 		return
 	}
@@ -1235,6 +1235,6 @@ func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard,
 	if err != nil {
 		return
 	}
-	folded, err = utils.CompileGlob(fmt.Sprintf("%s*%s", initialFolded, finalFolded))
+	folded, err = utils.CompileGlob(fmt.Sprintf("%s*%s", initialFolded, finalFolded), false)
 	return
 }
diff --git a/irc/handlers.go b/irc/handlers.go
index 64d16cde..350a9f89 100644
--- a/irc/handlers.go
+++ b/irc/handlers.go
@@ -21,7 +21,6 @@ import (
 	"time"
 
 	"github.com/goshuirc/irc-go/ircfmt"
-	"github.com/goshuirc/irc-go/ircmatch"
 	"github.com/goshuirc/irc-go/ircmsg"
 	"github.com/oragono/oragono/irc/caps"
 	"github.com/oragono/oragono/irc/custime"
@@ -1280,10 +1279,14 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
 		return false
 	}
 
-	matcher := ircmatch.MakeMatch(mask)
+	matcher, err := utils.CompileGlob(mask, false)
+	if err != nil {
+		rb.Add(nil, server.name, ERR_UNKNOWNERROR, details.nick, msg.Command, client.t("Erroneous nickname"))
+		return false
+	}
 
 	for _, clientMask := range client.AllNickmasks() {
-		if !klineMyself && matcher.Match(clientMask) {
+		if !klineMyself && matcher.MatchString(clientMask) {
 			rb.Add(nil, server.name, ERR_UNKNOWNERROR, details.nick, msg.Command, client.t("This ban matches you. To KLINE yourself, you must use the command:  /KLINE MYSELF <arguments>"))
 			return false
 		}
@@ -1327,7 +1330,7 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
 
 		for _, mcl := range server.clients.AllClients() {
 			for _, clientMask := range mcl.AllNickmasks() {
-				if matcher.Match(clientMask) {
+				if matcher.MatchString(clientMask) {
 					clientsToKill = append(clientsToKill, mcl)
 					killedClientNicks = append(killedClientNicks, mcl.nick)
 				}
diff --git a/irc/kline.go b/irc/kline.go
index 4099f482..ca669a5e 100644
--- a/irc/kline.go
+++ b/irc/kline.go
@@ -6,12 +6,14 @@ package irc
 import (
 	"encoding/json"
 	"fmt"
+	"regexp"
 	"strings"
 	"sync"
 	"time"
 
-	"github.com/goshuirc/irc-go/ircmatch"
 	"github.com/tidwall/buntdb"
+
+	"github.com/oragono/oragono/irc/utils"
 )
 
 const (
@@ -23,7 +25,7 @@ type KLineInfo struct {
 	// Mask that is blocked.
 	Mask string
 	// Matcher, to facilitate fast matching.
-	Matcher ircmatch.Matcher
+	Matcher *regexp.Regexp
 	// Info contains information on the ban.
 	Info IPBanInfo
 }
@@ -80,9 +82,14 @@ func (km *KLineManager) AddMask(mask string, duration time.Duration, reason, ope
 }
 
 func (km *KLineManager) addMaskInternal(mask string, info IPBanInfo) {
+	re, err := utils.CompileGlob(mask, false)
+	// this is validated externally and shouldn't fail regardless
+	if err != nil {
+		return
+	}
 	kln := KLineInfo{
 		Mask:    mask,
-		Matcher: ircmatch.MakeMatch(mask),
+		Matcher: re,
 		Info:    info,
 	}
 
@@ -189,7 +196,7 @@ func (km *KLineManager) CheckMasks(masks ...string) (isBanned bool, info IPBanIn
 
 	for _, entryInfo := range km.entries {
 		for _, mask := range masks {
-			if entryInfo.Matcher.Match(mask) {
+			if entryInfo.Matcher.MatchString(mask) {
 				return true, entryInfo.Info
 			}
 		}
diff --git a/irc/utils/glob.go b/irc/utils/glob.go
index 58fb7f34..04ee6116 100644
--- a/irc/utils/glob.go
+++ b/irc/utils/glob.go
@@ -11,21 +11,56 @@ import (
 
 // yet another glob implementation in Go
 
-func CompileGlob(glob string) (result *regexp.Regexp, err error) {
-	var buf bytes.Buffer
-	buf.WriteByte('^')
+func addRegexp(buf *bytes.Buffer, glob string, submatch bool) (err error) {
 	for _, r := range glob {
 		switch r {
 		case '*':
-			buf.WriteString("(.*)")
+			if submatch {
+				buf.WriteString("(.*)")
+			} else {
+				buf.WriteString(".*")
+			}
 		case '?':
-			buf.WriteString("(.)")
+			if submatch {
+				buf.WriteString("(.)")
+			} else {
+				buf.WriteString(".")
+			}
 		case 0xFFFD:
-			return nil, &syntax.Error{Code: syntax.ErrInvalidUTF8, Expr: glob}
+			return &syntax.Error{Code: syntax.ErrInvalidUTF8, Expr: glob}
 		default:
 			buf.WriteString(regexp.QuoteMeta(string(r)))
 		}
 	}
+	return
+}
+
+func CompileGlob(glob string, submatch bool) (result *regexp.Regexp, err error) {
+	var buf bytes.Buffer
+	buf.WriteByte('^')
+	err = addRegexp(&buf, glob, submatch)
+	if err != nil {
+		return
+	}
 	buf.WriteByte('$')
 	return regexp.Compile(buf.String())
 }
+
+// Compile a list of globs into a single or-expression that matches any one of them.
+// This is used for channel ban/invite/exception lists. It's applicable to k-lines
+// but we're not using it there yet.
+func CompileMasks(masks []string) (result *regexp.Regexp, err error) {
+	var buf bytes.Buffer
+	buf.WriteString("^(")
+	for i, mask := range masks {
+		err = addRegexp(&buf, mask, false)
+		if err != nil {
+			return
+		}
+		if i != len(masks)-1 {
+			buf.WriteByte('|')
+		}
+	}
+	buf.WriteString(")$")
+	return regexp.Compile(buf.String())
+}
diff --git a/vendor/github.com/goshuirc/e-nfa/.travis.yml b/vendor/github.com/goshuirc/e-nfa/.travis.yml
deleted file mode 100644
index 9e4d19ec..00000000
--- a/vendor/github.com/goshuirc/e-nfa/.travis.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-language: go
-
-go:
-    - 1.4
-    - tip
-
-before_install:
-    - go get golang.org/x/tools/cmd/cover
-    - go get golang.org/x/tools/cmd/vet
-    - go get golang.org/x/tools/cmd/goimports
-    - go get github.com/golang/lint/golint
-    - go get github.com/mattn/goveralls
-
-script:
-    - go vet ./...
-#    - $HOME/gopath/bin/goveralls -coverprofile=coverage.cov -service=travis-ci
-#    - bash <(curl -s https://codecov.io/bash)
-    - go test -bench=. -benchmem ./...
-    #- sh ./install_all_cmd.sh
\ No newline at end of file
diff --git a/vendor/github.com/goshuirc/e-nfa/README.md b/vendor/github.com/goshuirc/e-nfa/README.md
deleted file mode 100644
index cf13e416..00000000
--- a/vendor/github.com/goshuirc/e-nfa/README.md
+++ /dev/null
@@ -1,122 +0,0 @@
-ε-NFA: Epsilon-Nondeterministic finite automaton
-==============
-
-[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/kkdai/e-nfa/master/LICENSE)  [![GoDoc](https://godoc.org/github.com/kkdai/e-nfa?status.svg)](https://godoc.org/github.com/kkdai/e-nfa)  [![Build Status](https://travis-ci.org/kkdai/e-nfa.svg?branch=master)](https://travis-ci.org/kkdai/e-nfa)
-
-
-
-![image](https://upload.wikimedia.org/wikipedia/commons/thumb/0/0e/NFAexample.svg/250px-NFAexample.svg.png)
-
-
-
-What is Epsilon-Nondeterministic finite automaton
-=============
-
-`ε-NFA`: Epsilon-Nondeterministic finite automaton (so call:Nondeterministic finite automaton with ε-moves)
-
-In the automata theory, a nondeterministic finite automaton with ε-moves (NFA-ε)(also known as NFA-λ) is an extension of nondeterministic finite automaton(NFA), which allows a transformation to a new state without consuming any input symbols. The transitions without consuming an input symbol are called ε-transitions or λ-transitions. In the state diagrams, they are usually labeled with the Greek letter ε or λ.
-
-(sited from [here](https://en.wikipedia.org/wiki/Nondeterministic_finite_automaton))
-
-
-Looking for DFA implement?
-=============
-
-I also write a DFA implenent in Go here. [https://github.com/kkdai/dfa](https://github.com/kkdai/dfa)
-
-Looking for NFA implement?
-=============
-
-I also write a NFA implenent in Go here. [https://github.com/kkdai/nfa](https://github.com/kkdai/nfa)
-
-
-Installation and Usage
-=============
-
-
-Install
----------------
-
-    go get github.com/kkdai/e-nfa
-
-
-
-Usage
----------------
-
-Following is sample code to implement a epsilon-NFA automata diagram as follow:
-
-![image](image/eNFA.png)
-
-
-
-```go
-
-package main
-
-import (
-    "github.com/kkdai/enfa"
-    "fmt"
-)
-
-func main() {
-
-	nfa := NewENFA(0, false)
-	nfa.AddState(1, false)
-	nfa.AddState(2, false)
-	nfa.AddState(3, true)
-	nfa.AddState(4, false)
-	nfa.AddState(5, false)
-
-	nfa.AddTransition(0, "1", 1)
-	nfa.AddTransition(0, "0", 4)
-
-	nfa.AddTransition(1, "1", 2)
-	nfa.AddTransition(1, "", 3) //epsilon
-	nfa.AddTransition(2, "1", 3)
-	nfa.AddTransition(4, "0", 5)
-	nfa.AddTransition(4, "", 1, 2) //E -> epsilon B C
-	nfa.AddTransition(5, "0", 3)
-
-	nfa.PrintTransitionTable()
-
-	if !nfa.VerifyInputs([]string{"1"}) {
-		fmt.Printf("Verify inputs is failed")
-	}
-
-	nfa.Reset()
-
-	if !nfa.VerifyInputs([]string{"1", "1", "1"}) {
-		fmt.Printf("Verify inputs is failed")
-	}
-
-	nfa.Reset()
-
-	if !nfa.VerifyInputs([]string{"0", "1"}) {
-		fmt.Printf"Verify inputs is failed")
-	}
-
-	nfa.Reset()
-	if !nfa.VerifyInputs([]string{"0", "0", "0"}) {
-		fmt.Printf("Verify inputs is failed")
-	}
-}
-
-```
-
-Inspired By
-=============
-
-- [ε-NFA: Wiki](https://en.wikipedia.org/wiki/Nondeterministic_finite_automaton_with_%CE%B5-moves)
-- [Coursera: Automata](https://class.coursera.org/automata-004/)
-
-Project52
----------------
-
-It is one of my [project 52](https://github.com/kkdai/project52).
-
-
-License
----------------
-
-This package is licensed under MIT license. See LICENSE for details.
diff --git a/vendor/github.com/goshuirc/e-nfa/enfa.go b/vendor/github.com/goshuirc/e-nfa/enfa.go
deleted file mode 100644
index e1d92be4..00000000
--- a/vendor/github.com/goshuirc/e-nfa/enfa.go
+++ /dev/null
@@ -1,185 +0,0 @@
-package enfa
-
-import "fmt"
-
-type transitionInput struct {
-	srcState int
-	input    string
-}
-
-type destState map[int]bool
-
-type ENFA struct {
-	initState    int
-	currentState map[int]bool
-	totalStates  []int
-	finalStates  []int
-	transition   map[transitionInput]destState
-	inputMap     map[string]bool
-}
-
-//New a new NFA
-func NewENFA(initState int, isFinal bool) *ENFA {
-
-	retNFA := &ENFA{
-		transition: make(map[transitionInput]destState),
-		inputMap:   make(map[string]bool),
-		initState:  initState}
-
-	retNFA.currentState = make(map[int]bool)
-	retNFA.currentState[initState] = true
-	retNFA.AddState(initState, isFinal)
-	return retNFA
-}
-
-//Add new state in this NFA
-func (d *ENFA) AddState(state int, isFinal bool) {
-	if state == -1 {
-		fmt.Println("Cannot add state as -1, it is dead state")
-		return
-	}
-
-	d.totalStates = append(d.totalStates, state)
-	if isFinal {
-		d.finalStates = append(d.finalStates, state)
-	}
-}
-
-//Add new transition function into NFA
-func (d *ENFA) AddTransition(srcState int, input string, dstStateList ...int) {
-	find := false
-
-	//find input if exist in NFA input List
-	if _, ok := d.inputMap[input]; !ok {
-		//not exist, new input in this NFA
-		d.inputMap[input] = true
-	}
-
-	for _, v := range d.totalStates {
-		if v == srcState {
-			find = true
-		}
-	}
-
-	if !find {
-		fmt.Println("No such state:", srcState, " in current NFA")
-		return
-	}
-
-	dstMap := make(map[int]bool)
-	for _, destState := range dstStateList {
-		dstMap[destState] = true
-	}
-
-	targetTrans := transitionInput{srcState: srcState, input: input}
-	d.transition[targetTrans] = dstMap
-}
-
-func (d *ENFA) CheckPathExist(src int, input string, dst int) bool {
-	retMap, _ := d.transition[transitionInput{srcState: src, input: input}]
-	if _, ok := retMap[dst]; ok {
-		return true
-	}
-	return false
-}
-
-func (d *ENFA) Input(testInput string) []int {
-	updateCurrentState := make(map[int]bool)
-	for current, _ := range d.currentState {
-		for _, realTestInput := range []string{testInput, "*", "?"} {
-			intputTrans := transitionInput{srcState: current, input: realTestInput}
-			valMap, ok := d.transition[intputTrans]
-			if ok {
-				for dst, _ := range valMap {
-					updateCurrentState[dst] = true
-
-					//Update epsilon input way... if exist
-					epsilonTrans := transitionInput{srcState: dst}
-					if eMap, ok := d.transition[epsilonTrans]; ok {
-						for eDst, _ := range eMap {
-							updateCurrentState[eDst] = true
-						}
-					}
-				}
-			} else {
-				//dead state, remove in current state
-				//do nothing.
-			}
-		}
-	}
-
-	//update curret state
-	d.currentState = updateCurrentState
-
-	//return result
-	var ret []int
-	for state, _ := range updateCurrentState {
-		ret = append(ret, state)
-	}
-	return ret
-}
-
-//To verify current state if it is final state
-func (d *ENFA) Verify() bool {
-	for _, val := range d.finalStates {
-		for cState, _ := range d.currentState {
-			if val == cState {
-				return true
-			}
-		}
-	}
-	return false
-}
-
-//Reset NFA state to initilize state, but all state and transition function will remain
-func (d *ENFA) Reset() {
-	initState := make(map[int]bool)
-	initState[d.initState] = true
-	d.currentState = initState
-}
-
-//Verify if list of input could be accept by NFA or not
-func (d *ENFA) VerifyInputs(inputs []string) bool {
-	for _, v := range inputs {
-		d.Input(v)
-	}
-	return d.Verify()
-}
-
-//To print detail transition table contain of current NFA
-func (d *ENFA) PrintTransitionTable() {
-	fmt.Println("===================================================")
-	//list all inputs
-	var inputList []string
-	for key, _ := range d.inputMap {
-		if key == "" {
-			fmt.Printf("\tε|")
-		} else {
-			fmt.Printf("\t%s|", key)
-		}
-		inputList = append(inputList, key)
-	}
-
-	fmt.Printf("\n")
-	fmt.Println("---------------------------------------------------")
-
-	for _, state := range d.totalStates {
-		fmt.Printf("%d |", state)
-		for _, key := range inputList {
-			checkInput := transitionInput{srcState: state, input: key}
-			if dstState, ok := d.transition[checkInput]; ok {
-				fmt.Printf("\t")
-				for val, _ := range dstState {
-					fmt.Printf("%d,", val)
-				}
-				fmt.Printf("|")
-			} else {
-				fmt.Printf("\tNA|")
-			}
-		}
-		fmt.Printf("\n")
-	}
-
-	fmt.Println("---------------------------------------------------")
-	fmt.Println("===================================================")
-}
diff --git a/vendor/github.com/goshuirc/irc-go/ircmatch/doc.go b/vendor/github.com/goshuirc/irc-go/ircmatch/doc.go
deleted file mode 100644
index 0561b4d0..00000000
--- a/vendor/github.com/goshuirc/irc-go/ircmatch/doc.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// written by Daniel Oaks <daniel@danieloaks.net>
-// released under the ISC license
-
-/*
-Package ircmatch handles matching IRC strings with the traditional glob-like syntax.
-*/
-package ircmatch
diff --git a/vendor/github.com/goshuirc/irc-go/ircmatch/ircmatch.go b/vendor/github.com/goshuirc/irc-go/ircmatch/ircmatch.go
deleted file mode 100644
index 03e7cf54..00000000
--- a/vendor/github.com/goshuirc/irc-go/ircmatch/ircmatch.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package ircmatch
-
-import enfa "github.com/goshuirc/e-nfa"
-
-// Matcher represents an object that can match IRC strings.
-type Matcher struct {
-	internalENFA *enfa.ENFA
-}
-
-// MakeMatch creates a Matcher.
-func MakeMatch(globTemplate string) Matcher {
-	var newmatch Matcher
-
-	// assemble internal enfa
-	newmatch.internalENFA = enfa.NewENFA(0, false)
-
-	var currentState int
-	var lastWasStar bool
-	for _, char := range globTemplate {
-		if char == '*' {
-			if lastWasStar {
-				continue
-			}
-			newmatch.internalENFA.AddTransition(currentState, "*", currentState)
-			lastWasStar = true
-			continue
-		} else if char == '?' {
-			newmatch.internalENFA.AddState(currentState+1, false)
-			newmatch.internalENFA.AddTransition(currentState, "?", currentState+1)
-			currentState++
-		} else {
-			newmatch.internalENFA.AddState(currentState+1, false)
-			newmatch.internalENFA.AddTransition(currentState, string(char), currentState+1)
-			currentState++
-		}
-
-		lastWasStar = false
-	}
-
-	// create end state
-	newmatch.internalENFA.AddState(currentState+1, true)
-	newmatch.internalENFA.AddTransition(currentState, "", currentState+1)
-
-	return newmatch
-}
-
-// Match returns true if the given string matches this glob.
-func (menfa *Matcher) Match(search string) bool {
-	var searchChars []string
-	for _, char := range search {
-		searchChars = append(searchChars, string(char))
-	}
-
-	isMatch := menfa.internalENFA.VerifyInputs(searchChars)
-	menfa.internalENFA.Reset()
-	return isMatch
-}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index d829c119..5a9312b3 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -17,11 +17,9 @@ github.com/go-sql-driver/mysql
 github.com/gorilla/websocket
 # github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940
 ## explicit
-github.com/goshuirc/e-nfa
 # github.com/goshuirc/irc-go v0.0.0-20200311142257-57fd157327ac
 ## explicit
 github.com/goshuirc/irc-go/ircfmt
-github.com/goshuirc/irc-go/ircmatch
 github.com/goshuirc/irc-go/ircmsg
 # github.com/onsi/ginkgo v1.12.0
 ## explicit


Test patch:
diff --git a/irc/utils/glob_test.go b/irc/utils/glob_test.go
index 2c9dee35..5cd83af8 100644
--- a/irc/utils/glob_test.go
+++ b/irc/utils/glob_test.go
@@ -9,7 +9,7 @@ import (
 )
 
 func globMustCompile(glob string) *regexp.Regexp {
-	re, err := CompileGlob(glob)
+	re, err := CompileGlob(glob, false)
 	if err != nil {
 		panic(err)
 	}
@@ -46,3 +46,123 @@ func TestGlob(t *testing.T) {
 	assertMatches("S*e", "Skåne", true, t)
 	assertMatches("Sk?ne", "Skåne", true, t)
 }
+
+func BenchmarkGlob(b *testing.B) {
+	g := globMustCompile("https://*google.com")
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		g.MatchString("https://www.google.com")
+	}
+}
+
+func BenchmarkGlobCompilation(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		CompileGlob("https://*google.com", false)
+	}
+}
+
+// these are actual bans from my production network :-/
+var bans = []string{
+	"*!*@tor-network.onion",
+	"`!*@*",
+	"qanon!*@*",
+	"*!bibi@tor-network.onion",
+	"shivarm!*@*",
+	"8====d!*@*",
+	"shiviram!*@*",
+	"poop*!*@*",
+	"shivoram!*@*",
+	"shivvy!*@*",
+	"shavirim!*@*",
+	"shivarm_!*@*",
+	"_!*@*",
+}
+
+func TestMasks(t *testing.T) {
+	matcher, err := CompileMasks(bans)
+	if err != nil {
+		panic(err)
+	}
+
+	if !matcher.MatchString("evan!user@tor-network.onion") {
+		t.Errorf("match expected")
+	}
+	if !matcher.MatchString("`!evan@b9un4fv3he44q.example.com") {
+		t.Errorf("match expected")
+	}
+	if matcher.MatchString("horse!horse@t5dwi8vacg47y.example.com") {
+		t.Errorf("match not expected")
+	}
+	if matcher.MatchString("horse_!horse@t5dwi8vacg47y.example.com") {
+		t.Errorf("match not expected")
+	}
+	if matcher.MatchString("shivaram!shivaram@yrqgsrjy2p7my.example.com") {
+		t.Errorf("match not expected")
+	}
+}
+
+func BenchmarkMasksCompile(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		CompileMasks(bans)
+	}
+}
+
+func BenchmarkMasksMatch(b *testing.B) {
+	matcher, _ := CompileMasks(bans)
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		matcher.MatchString("evan!user@tor-network.onion")
+		matcher.MatchString("horse_!horse@t5dwi8vacg47y.example.com")
+		matcher.MatchString("shivaram!shivaram@yrqgsrjy2p7my.example.com")
+	}
+}
+
+// compare performance to compilation of the | clauses as separate regexes
+// first for compilation, then for matching
+
+func compileAll(masks []string) (result []*regexp.Regexp, err error) {
+	a := make([]*regexp.Regexp, 0, len(masks))
+	for _, mask := range masks {
+		m, err := CompileGlob(mask, false)
+		if err != nil {
+			return nil, err
+		}
+		a = append(a, m)
+	}
+	return a, nil
+}
+
+func matchesAny(masks []*regexp.Regexp, str string) bool {
+	for _, r := range masks {
+		if r.MatchString(str) {
+			return true
+		}
+	}
+	return false
+}
+
+func BenchmarkLinearCompile(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		compileAll(bans)
+	}
+}
+
+func BenchmarkLinearMatch(b *testing.B) {
+	a, err := compileAll(bans)
+	if err != nil {
+		panic(err)
+	}
+	if matchesAny(a, "horse_!horse@t5dwi8vacg47y.example.com") {
+		panic("incorrect match")
+	}
+	if !matchesAny(a, "evan!user@tor-network.onion") {
+		panic("incorrect match")
+	}
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		matchesAny(a, "horse_!horse@t5dwi8vacg47y.example.com")
+		matchesAny(a, "evan!user@tor-network.onion")
+		matchesAny(a, "shivaram!shivaram@yrqgsrjy2p7my.example.com")
+	}
+}