@@ -16,6 +16,9 @@ import (
1616const (
1717 maxRun = 10
1818 ipv4TokenWidth = 7 // D Period D Period D Period D
19+ // maxIPv4OctetDigits is the longest digit run that can be an octet, so a
20+ // run longer than this can never close a dotted quad.
21+ maxIPv4OctetDigits = 3
1922)
2023
2124// maxSpecialTokenLen and the special-token/debug-string tables are generated
@@ -73,6 +76,10 @@ type Tokenizer struct {
7376 maxEvalBytes int
7477 tsBuf []Token // Reusable token buffer
7578 idxBuf []int // Reusable index buffer
79+ // skipHybridCollapse disables hybrid-token collapsing so tests can
80+ // reconstruct the pre-IPv4-token tokenizer. Zero value collapses, so a
81+ // zero-value Tokenizer still behaves like a production one.
82+ skipHybridCollapse bool
7683}
7784
7885// NewTokenizer returns a new Tokenizer detection heuristic.
@@ -150,9 +157,15 @@ func (t *Tokenizer) emitToken(input []byte, token Token, start, end int) {
150157 r = maxRun - 1
151158 }
152159 t .tsBuf = append (t .tsBuf , token + Token (r ))
153- } else {
154- t .tsBuf = append (t .tsBuf , token )
160+ // A dotted quad can only ever be closed by its final octet, so this is
161+ // the one emission that can complete the pattern. Checking here keeps
162+ // the cost off every other token and avoids a second pass entirely.
163+ if token == D1 && runLen <= maxIPv4OctetDigits && ! t .skipHybridCollapse {
164+ t .collapseIPv4Tail ()
165+ }
166+ return
155167 }
168+ t .tsBuf = append (t .tsBuf , token )
156169}
157170
158171// tokenizeIntoBuffers scans input a single time and emits tokens into the
@@ -163,13 +176,12 @@ func (t *Tokenizer) tokenizeIntoBuffers(input []byte) ([]Token, []int) {
163176 return nil , nil
164177 }
165178 t .emitRuns (input )
166- t .collapseHybridTokens ()
167179 return t .tsBuf , t .idxBuf
168180}
169181
170- // emitRuns run-length-encodes input into tsBuf/idxBuf without hybrid collapse.
171- // Tests use this to reconstruct the pre-IPv4-token tokenizer for the IIS
172- // false-aggregate case.
182+ // emitRuns run-length-encodes input into tsBuf/idxBuf, collapsing hybrid tokens
183+ // as they complete unless skipHybridCollapse is set. Tests set that flag to
184+ // reconstruct the pre-IPv4-token tokenizer for the IIS false-aggregate case.
173185func (t * Tokenizer ) emitRuns (input []byte ) {
174186 inputLen := len (input )
175187 if inputLen == 0 {
@@ -213,49 +225,40 @@ func isIPv4OctetToken(tok Token) bool {
213225 return tok >= D1 && tok <= D3
214226}
215227
216- // ipv4At reports whether tokens[i:] starts with an IPv4 dotted quad.
217- // Each octet is a 1-3 digit run and each separator is a single '.'.
218- // A collapsed ".." / "..." Period token is rejected via the start indices.
219- func ipv4At (tokens []Token , indices []int , i int ) bool {
220- if i + ipv4TokenWidth > len (tokens ) {
221- return false
222- }
223- if ! isIPv4OctetToken (tokens [i ]) || tokens [i + 1 ] != Period ||
224- ! isIPv4OctetToken (tokens [i + 2 ]) || tokens [i + 3 ] != Period ||
225- ! isIPv4OctetToken (tokens [i + 4 ]) || tokens [i + 5 ] != Period ||
226- ! isIPv4OctetToken (tokens [i + 6 ]) {
227- return false
228- }
229- return indices [i + 2 ] == indices [i + 1 ]+ 1 &&
230- indices [i + 4 ] == indices [i + 3 ]+ 1 &&
231- indices [i + 6 ] == indices [i + 5 ]+ 1
232- }
233-
234- // collapseHybridTokens rewrites multi-token patterns into a single token.
235- // IPv4 dotted quads are the first of these: as separate digit/period tokens
236- // they look like timestamp fragments to the detector, and addresses with
237- // different octet widths would otherwise be different sampler patterns.
238- func (t * Tokenizer ) collapseHybridTokens () {
228+ // collapseIPv4Tail rewrites a dotted quad ending at the last emitted token into
229+ // a single IPv4 token. As separate digit/period tokens a quad looks like a
230+ // timestamp fragment to the detector, and addresses with different octet widths
231+ // would otherwise be different sampler patterns.
232+ //
233+ // Called from emitToken immediately after a 1-3 digit run is appended, which is
234+ // the only emission that can close the pattern, so the tokenizer never makes a
235+ // second pass over the token list. Each octet must be a 1-3 digit run and each
236+ // separator a single '.'; a collapsed ".." / "..." Period token is rejected via
237+ // the start indices.
238+ func (t * Tokenizer ) collapseIPv4Tail () {
239239 n := len (t .tsBuf )
240240 if n < ipv4TokenWidth {
241241 return
242242 }
243- w := 0
244- for r := 0 ; r < n ; {
245- if ipv4At (t .tsBuf , t .idxBuf , r ) {
246- t .tsBuf [w ] = IPv4
247- t .idxBuf [w ] = t .idxBuf [r ]
248- w ++
249- r += ipv4TokenWidth
250- continue
251- }
252- t .tsBuf [w ] = t .tsBuf [r ]
253- t .idxBuf [w ] = t .idxBuf [r ]
254- w ++
255- r ++
243+ ts := t .tsBuf [n - ipv4TokenWidth :]
244+
245+ // Separators first: they reject nearly every call in a single compare, and
246+ // the trailing octet is already known from the caller.
247+ if ts [5 ] != Period || ts [3 ] != Period || ts [1 ] != Period {
248+ return
249+ }
250+ if ! isIPv4OctetToken (ts [0 ]) || ! isIPv4OctetToken (ts [2 ]) || ! isIPv4OctetToken (ts [4 ]) {
251+ return
256252 }
257- t .tsBuf = t .tsBuf [:w ]
258- t .idxBuf = t .idxBuf [:w ]
253+ idx := t .idxBuf [n - ipv4TokenWidth :]
254+ if idx [2 ] != idx [1 ]+ 1 || idx [4 ] != idx [3 ]+ 1 || idx [6 ] != idx [5 ]+ 1 {
255+ return
256+ }
257+
258+ // Keep idx[0] (the address start) and drop the six tokens it absorbed.
259+ ts [0 ] = IPv4
260+ t .tsBuf = t .tsBuf [:n - ipv4TokenWidth + 1 ]
261+ t .idxBuf = t .idxBuf [:n - ipv4TokenWidth + 1 ]
259262}
260263
261264// tokensToString converts a list of tokens to a debug string.
0 commit comments