Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

ahocorasick.gno

8.66 Kb · 276 lines
  1// Package ahocorasick implements Aho-Corasick multi-pattern search as a pure,
  2// reusable package.
  3//
  4// Searching for k patterns by running a single-pattern scan k times costs
  5// O(k*n): every extra word in the dictionary is another full pass over the
  6// text. Aho-Corasick builds one automaton from the whole set, a trie of the
  7// patterns plus a failure link on every node pointing at the longest proper
  8// suffix that is also a prefix of some pattern, and then reads the text once.
  9// Building the trie and the failure links costs O(total pattern length) and
 10// scanning costs O(n + matches), whatever k is, and the text cursor never moves
 11// backwards.
 12//
 13// One deliberate departure from the paper: each node stores the full list of
 14// patterns matching there, merged from its failure target at build time, rather
 15// than a link to be walked at every offset. That makes a reported match O(1)
 16// instead of a walk up the failure chain, and it costs build memory in exactly
 17// one shape, a set whose patterns nest ("a", "aa", "aaa", ...), where the
 18// merged lists are quadratic in k rather than linear. MaxPatterns is what
 19// bounds that, and nothing else does.
 20//
 21// That last property is why it is worth having on chain: the worst case equals
 22// the best case, so a dictionary and a text chosen by an attacker cost the same
 23// as a friendly pair. A naive k-pass scan is an input away from quadratic.
 24//
 25// It is [p/moul/x/daily/kmp](/p/moul/x/daily/kmp/v0) generalised: KMP's failure
 26// table is this automaton for a one-pattern set.
 27//
 28// Operates on BYTES, not runes. gno strings are UTF-8 and a match offset is a
 29// byte offset, which is the right unit for slicing. UTF-8 is self-synchronizing,
 30// so a pattern that is itself valid UTF-8 can only match on a rune boundary of
 31// valid UTF-8 text; a pattern that is a bare continuation byte can and will
 32// match inside a rune, and slicing on that offset splits it.
 33//
 34// A live demo of this package is at
 35// [r/moul/x/daily/ahocorasickdemo](/r/moul/x/daily/ahocorasickdemo/v0).
 36package ahocorasick
 37
 38// Bounds on what New will build, so an on-chain caller cannot be handed an
 39// automaton whose size it did not choose. At most MaxPatterns*MaxPatternLen+1
 40// nodes exist, and the build is linear in the total pattern length.
 41const (
 42	MaxPatterns   = 256
 43	MaxPatternLen = 256
 44)
 45
 46// Match is one occurrence of one pattern. Start and End are byte offsets into
 47// the text, half-open, so text[Start:End] is the matched run. Index is the
 48// pattern's slot in the slice given to New, which is how a caller maps a match
 49// back to whatever it stored alongside the pattern.
 50type Match struct {
 51	Start   int
 52	End     int
 53	Index   int
 54	Pattern string
 55}
 56
 57// edge is one labelled transition. A node keeps its edges sorted by label so a
 58// lookup is a binary search over the alphabet actually used, not a 256-entry
 59// row per node: a dictionary of words touches a few dozen distinct bytes and
 60// paying 1 KiB a node for the rest is how this structure gets expensive.
 61type edge struct {
 62	label byte
 63	next  int
 64}
 65
 66type node struct {
 67	edges []edge
 68	fail  int
 69	out   []int // pattern slots matching here, longest first
 70}
 71
 72// Matcher is a built automaton. It is immutable once New returns, so one
 73// Matcher can serve any number of scans.
 74type Matcher struct {
 75	patterns []string
 76	nodes    []node
 77}
 78
 79// New builds the automaton for patterns. Empty patterns are dropped: the empty
 80// string matches at every offset, which is a different question from the one
 81// this package answers.
 82//
 83// Returns nil when there is nothing to build (no pattern survives the drop) or
 84// when the set exceeds the bounds above. A nil Matcher answers every method,
 85// so a caller may hand the result straight to a scan without a check.
 86func New(patterns []string) *Matcher {
 87	accepted := make([]string, 0, len(patterns))
 88	for _, p := range patterns {
 89		if p == "" {
 90			continue
 91		}
 92		if len(p) > MaxPatternLen {
 93			return nil
 94		}
 95		accepted = append(accepted, p)
 96	}
 97	if len(accepted) == 0 || len(accepted) > MaxPatterns {
 98		return nil
 99	}
100
101	m := &Matcher{patterns: accepted, nodes: []node{{fail: 0}}}
102	for i, p := range accepted {
103		cur := 0
104		for j := 0; j < len(p); j++ {
105			next, ok := m.child(cur, p[j])
106			if !ok {
107				m.nodes = append(m.nodes, node{fail: 0})
108				next = len(m.nodes) - 1
109				m.addEdge(cur, p[j], next)
110			}
111			cur = next
112		}
113		// Same depth, so same length: ordering by slot keeps a duplicated
114		// pattern reporting in the caller's order.
115		m.nodes[cur].out = append(m.nodes[cur].out, i)
116	}
117	m.link()
118	return m
119}
120
121// link computes every failure link, then merges each node's outputs with its
122// failure target's. The merge is what makes a scan O(1) per reported match
123// instead of a walk up the failure chain at every offset, and the breadth-first
124// order is what makes it correct: a node's failure target is always shallower,
125// so it has already been merged.
126//
127// It is also where the memory goes: a nested set makes the merged lists
128// quadratic in the number of patterns. See the package doc.
129func (m *Matcher) link() {
130	queue := make([]int, 0, len(m.nodes))
131	for _, e := range m.nodes[0].edges {
132		m.nodes[e.next].fail = 0
133		queue = append(queue, e.next)
134	}
135	for len(queue) > 0 {
136		u := queue[0]
137		queue = queue[1:]
138		for _, e := range m.nodes[u].edges {
139			v := e.next
140			f := m.nodes[u].fail
141			for {
142				if w, ok := m.child(f, e.label); ok {
143					m.nodes[v].fail = w
144					break
145				}
146				if f == 0 {
147					m.nodes[v].fail = 0
148					break
149				}
150				f = m.nodes[f].fail
151			}
152			inherited := m.nodes[m.nodes[v].fail].out
153			if len(inherited) > 0 {
154				merged := make([]int, 0, len(m.nodes[v].out)+len(inherited))
155				merged = append(merged, m.nodes[v].out...)
156				merged = append(merged, inherited...)
157				m.nodes[v].out = merged
158			}
159			queue = append(queue, v)
160		}
161	}
162}
163
164// child returns the node reached from n by label, if there is one.
165func (m *Matcher) child(n int, label byte) (int, bool) {
166	edges := m.nodes[n].edges
167	lo, hi := 0, len(edges)
168	for lo < hi {
169		mid := (lo + hi) / 2
170		if edges[mid].label < label {
171			lo = mid + 1
172		} else {
173			hi = mid
174		}
175	}
176	if lo < len(edges) && edges[lo].label == label {
177		return edges[lo].next, true
178	}
179	return 0, false
180}
181
182// addEdge inserts a transition, keeping the edge list sorted by label.
183func (m *Matcher) addEdge(n int, label byte, next int) {
184	edges := m.nodes[n].edges
185	pos := len(edges)
186	for i, e := range edges {
187		if e.label > label {
188			pos = i
189			break
190		}
191	}
192	edges = append(edges, edge{})
193	copy(edges[pos+1:], edges[pos:])
194	edges[pos] = edge{label: label, next: next}
195	m.nodes[n].edges = edges
196}
197
198// step advances the automaton one byte, following failure links until the byte
199// is consumed or the root refuses it. This is the whole scan: the text index
200// only ever moves forward.
201func (m *Matcher) step(cur int, c byte) int {
202	for {
203		if w, ok := m.child(cur, c); ok {
204			return w
205		}
206		if cur == 0 {
207			return 0
208		}
209		cur = m.nodes[cur].fail
210	}
211}
212
213// FindAll returns every occurrence of every pattern, including overlapping and
214// nested ones, ordered by end offset. At one end offset the longest match comes
215// first, because the scan reports the node it reached before the shorter
216// suffixes hanging off its failure chain, so a caller taking the first match at
217// each offset gets the greedy one.
218func (m *Matcher) FindAll(text string) []Match {
219	if m == nil {
220		return nil
221	}
222	var out []Match
223	cur := 0
224	for i := 0; i < len(text); i++ {
225		cur = m.step(cur, text[i])
226		for _, slot := range m.nodes[cur].out {
227			p := m.patterns[slot]
228			out = append(out, Match{Start: i + 1 - len(p), End: i + 1, Index: slot, Pattern: p})
229		}
230	}
231	return out
232}
233
234// FindFirst returns the first match FindAll would return, and stops the scan
235// there. The one to reach for when the question is "does this text contain
236// anything from the list, and where".
237func (m *Matcher) FindFirst(text string) (Match, bool) {
238	if m == nil {
239		return Match{}, false
240	}
241	cur := 0
242	for i := 0; i < len(text); i++ {
243		cur = m.step(cur, text[i])
244		if out := m.nodes[cur].out; len(out) > 0 {
245			p := m.patterns[out[0]]
246			return Match{Start: i + 1 - len(p), End: i + 1, Index: out[0], Pattern: p}, true
247		}
248	}
249	return Match{}, false
250}
251
252// Contains reports whether any pattern occurs in text.
253func (m *Matcher) Contains(text string) bool {
254	_, ok := m.FindFirst(text)
255	return ok
256}
257
258// Patterns returns the accepted patterns, in the caller's order, as a copy.
259func (m *Matcher) Patterns() []string {
260	if m == nil {
261		return nil
262	}
263	out := make([]string, len(m.patterns))
264	copy(out, m.patterns)
265	return out
266}
267
268// Nodes returns the size of the automaton: one node per distinct pattern
269// prefix, plus the root. The number a caller sizing a dictionary wants, since
270// it is what the build actually costs.
271func (m *Matcher) Nodes() int {
272	if m == nil {
273		return 0
274	}
275	return len(m.nodes)
276}