Updates every backend dependency with a newer release available, and tidies the example module alongside as any change to backend/go.mod requires.
671 lines
18 KiB
Go
671 lines
18 KiB
Go
/*
|
|
Package regexp2 is a regexp package that has an interface similar to Go's framework regexp engine but uses a
|
|
more feature full regex engine behind the scenes.
|
|
|
|
It doesn't have constant time guarantees, but it allows backtracking and is compatible with Perl5 and .NET.
|
|
You'll likely be better off with the RE2 engine from the regexp package and should only use this if you
|
|
need to write very complex patterns or require compatibility with .NET.
|
|
*/
|
|
package regexp2
|
|
|
|
import (
|
|
"container/list"
|
|
"errors"
|
|
"log"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/dlclark/regexp2/v2/syntax"
|
|
)
|
|
|
|
var (
|
|
// DefaultMatchTimeout used when running regexp matches -- "forever"
|
|
DefaultMatchTimeout = time.Duration(math.MaxInt64)
|
|
// ErrBacktrackingStackLimit is returned when a match exceeds its configured backtracking stack size.
|
|
ErrBacktrackingStackLimit = errors.New("regexp2: maximum backtracking stack size exceeded")
|
|
)
|
|
|
|
// Regexp is the representation of a compiled regular expression.
|
|
// A Regexp is safe for concurrent use by multiple goroutines.
|
|
type Regexp struct {
|
|
// A match will time out if it takes (approximately) more than
|
|
// MatchTimeout. This is a safety check in case the match
|
|
// encounters catastrophic backtracking. The default value
|
|
// (DefaultMatchTimeout) causes all time out checking to be
|
|
// suppressed.
|
|
MatchTimeout time.Duration
|
|
|
|
// read-only after Compile
|
|
pattern string // as passed to Compile
|
|
options RegexOptions // options
|
|
debug bool
|
|
|
|
caps map[int]int // capnum->index
|
|
capnames map[string]int //capture group name -> index
|
|
capslist []string //sorted list of capture group names
|
|
capsize int // size of the capture array
|
|
|
|
code *syntax.Code // compiled program
|
|
|
|
optimizations OptimizationOptions
|
|
|
|
// cache of machines for running regexp
|
|
runnerPool *sync.Pool
|
|
|
|
replaceCache *replacerDataCache
|
|
|
|
// hook points to override runner functions
|
|
findFirstChar func(r *Runner) bool
|
|
execute func(r *Runner) error
|
|
executeQuick func(r *Runner) error
|
|
stringPrefixFilter StringPrefixFilter
|
|
quickCode *syntax.Code // bool-only program with unobservable captures removed
|
|
// leftContextRunes is used when code is nil (registered engines).
|
|
// The interpreter reads the same value from code.LeftContextRunes.
|
|
leftContextRunes int
|
|
}
|
|
|
|
// Compile parses a regular expression and returns, if successful,
|
|
// a Regexp object that can be used to match against text.
|
|
func Compile(expr string, options ...CompileOption) (*Regexp, error) {
|
|
c := newCompileConfig(options)
|
|
return compile(expr, c)
|
|
}
|
|
|
|
func compile(expr string, c compileConfig) (*Regexp, error) {
|
|
// parse it
|
|
parseOptions := syntax.ParseOptions{
|
|
RegexOptions: syntax.RegexOptions(c.regexOptions),
|
|
MaintainCaptureOrder: c.maintainCaptureOrder,
|
|
CodeGen: c.codeGen,
|
|
}
|
|
tree, err := syntax.Parse(expr, parseOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if c.debug {
|
|
log.Print(tree.Dump())
|
|
}
|
|
|
|
// translate it to code
|
|
code, err := syntax.Write(tree)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if c.debug {
|
|
log.Print(code.Dump())
|
|
}
|
|
if !c.optimizations.DisableCharClassASCIIBitmap {
|
|
code.PrepareCharSetASCIIBitmaps()
|
|
}
|
|
|
|
// return it
|
|
re := &Regexp{
|
|
pattern: expr,
|
|
options: c.regexOptions,
|
|
debug: c.debug,
|
|
caps: code.Caps,
|
|
capnames: tree.Capnames,
|
|
capslist: tree.Caplist,
|
|
capsize: code.Capsize,
|
|
code: code,
|
|
quickCode: makeQuickCode(code),
|
|
MatchTimeout: DefaultMatchTimeout,
|
|
optimizations: c.optimizations,
|
|
}
|
|
re.stringPrefixFilter = newStringPrefixFilter(code)
|
|
re.initCaches()
|
|
return re, nil
|
|
}
|
|
|
|
func makeQuickCode(code *syntax.Code) *syntax.Code {
|
|
if code == nil || len(code.QuickCodes) == 0 {
|
|
return nil
|
|
}
|
|
quick := *code
|
|
quick.Codes = code.QuickCodes
|
|
quick.Dispatches = code.QuickDispatches
|
|
quick.QuickCodes = nil
|
|
quick.QuickDispatches = nil
|
|
return &quick
|
|
}
|
|
|
|
// MustCompile is like Compile but panics if the expression cannot be parsed.
|
|
// It simplifies safe initialization of global variables holding compiled regular
|
|
// expressions.
|
|
func MustCompile(str string, options ...CompileOption) *Regexp {
|
|
c := newCompileConfig(options)
|
|
|
|
// lookup if we have a pre-built state machine for this pattern and options
|
|
regexp := getEngineRegexp(str, c)
|
|
if regexp != nil {
|
|
return regexp
|
|
}
|
|
|
|
regexp, err := compile(str, c)
|
|
if err != nil {
|
|
panic(`regexp2: Compile(` + quote(str) + `): ` + err.Error())
|
|
}
|
|
return regexp
|
|
}
|
|
|
|
// Escape adds backslashes to any special characters in the input string
|
|
func Escape(input string) string {
|
|
return syntax.Escape(input)
|
|
}
|
|
|
|
// Unescape removes any backslashes from previously-escaped special characters in the input string
|
|
func Unescape(input string) (string, error) {
|
|
return syntax.Unescape(input)
|
|
}
|
|
|
|
// SetTimeoutPeriod is a debug function that sets the frequency of the timeout goroutine's sleep cycle.
|
|
// Defaults to 100ms. The only benefit of setting this lower is that the 1 background goroutine that manages
|
|
// timeouts may exit slightly sooner after all the timeouts have expired. See Github issue #63
|
|
func SetTimeoutCheckPeriod(d time.Duration) {
|
|
clockPeriod = d
|
|
}
|
|
|
|
// StopTimeoutClock should only be used in unit tests to prevent the timeout clock goroutine
|
|
// from appearing like a leaking goroutine
|
|
func StopTimeoutClock() {
|
|
stopClock()
|
|
}
|
|
|
|
// String returns the source text used to compile the regular expression.
|
|
func (re *Regexp) String() string {
|
|
return re.pattern
|
|
}
|
|
|
|
func quote(s string) string {
|
|
if strconv.CanBackquote(s) {
|
|
return "`" + s + "`"
|
|
}
|
|
return strconv.Quote(s)
|
|
}
|
|
|
|
func (re *Regexp) RightToLeft() bool {
|
|
return re.options&RightToLeft != 0
|
|
}
|
|
|
|
func (re *Regexp) Debug() bool {
|
|
return re.debug
|
|
}
|
|
|
|
// Replace searches the input string and replaces each match found with the replacement text.
|
|
// Count will limit the number of matches attempted and startAt will allow
|
|
// us to skip past possible matches at the start of the input (left or right depending on RightToLeft option).
|
|
// Set startAt and count to -1 to go through the whole string
|
|
func (re *Regexp) Replace(input, replacement string, startAt, count int) (string, error) {
|
|
data, err := re.getReplacerData(replacement)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return replace(re, data, nil, input, startAt, count)
|
|
}
|
|
|
|
func (re *Regexp) getReplacerData(replacement string) (*syntax.ReplacerData, error) {
|
|
shouldCache := re.replaceCache != nil && re.optimizations.cacheReplacerData(replacement)
|
|
if shouldCache {
|
|
if data, ok := re.replaceCache.get(replacement); ok {
|
|
return data, nil
|
|
}
|
|
}
|
|
|
|
data, err := syntax.NewReplacerData(replacement, re.caps, re.capsize, re.capnames, syntax.RegexOptions(re.options))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if shouldCache {
|
|
re.replaceCache.add(replacement, data)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// ReplaceFunc searches the input string and replaces each match found using the string from the evaluator
|
|
// Count will limit the number of matches attempted and startAt will allow
|
|
// us to skip past possible matches at the start of the input (left or right depending on RightToLeft option).
|
|
// Set startAt and count to -1 to go through the whole string.
|
|
func (re *Regexp) ReplaceFunc(input string, evaluator MatchEvaluator, startAt, count int) (string, error) {
|
|
return replace(re, nil, evaluator, input, startAt, count)
|
|
}
|
|
|
|
// FindStringMatch searches the input string for a Regexp match
|
|
func (re *Regexp) FindStringMatch(s string) (*Match, error) {
|
|
startAt, ok, err := re.findStringMatchStart(s, -1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return re.findDecodedStringMatch(s, startAt)
|
|
}
|
|
|
|
// FindRunesMatch searches the input rune slice for a Regexp match
|
|
func (re *Regexp) FindRunesMatch(r []rune) (*Match, error) {
|
|
return re.run(false, -1, -1, r, newMatchText(r))
|
|
}
|
|
|
|
// FindStringMatchStartingAt searches the input string for a Regexp match starting at the startAt index
|
|
func (re *Regexp) FindStringMatchStartingAt(s string, startAt int) (*Match, error) {
|
|
startAt, ok, err := re.findStringMatchStart(s, startAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return re.findDecodedStringMatch(s, startAt)
|
|
}
|
|
|
|
func (re *Regexp) findDecodedStringMatch(s string, startAt int) (*Match, error) {
|
|
// Returned matches retain their rune data, so this path must not consume a
|
|
// pooled buffer that can never be returned.
|
|
d := re.decodeStringInput(s, startAt, false)
|
|
runner := re.getRunner()
|
|
defer re.putRunner(runner)
|
|
text := newStringMatchTextAt(s, d.runes, d.runeOffset, d.byteOffset)
|
|
return runner.scan(d.runes, text, d.runeStart, -1, false, re.MatchTimeout)
|
|
}
|
|
|
|
// FindRunesMatchStartingAt searches the input rune slice for a Regexp match starting at the startAt index
|
|
func (re *Regexp) FindRunesMatchStartingAt(r []rune, startAt int) (*Match, error) {
|
|
return re.run(false, startAt, -1, r, newMatchText(r))
|
|
}
|
|
|
|
// FindAllStringIndex returns a slice of byte index pairs identifying all
|
|
// successive matches in s.
|
|
func (re *Regexp) FindAllStringIndex(s string, n int) ([][]int, error) {
|
|
if n == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
startAt, ok, err := re.findStringMatchStart(s, -1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
d := re.decodeStringInput(s, startAt, true)
|
|
runner := re.getRunner()
|
|
defer func() {
|
|
re.putRunner(runner)
|
|
d.release()
|
|
}()
|
|
byteOffsets := newStringByteMapper(s)
|
|
if re.quickCode != nil {
|
|
runner.code = re.quickCode
|
|
}
|
|
return re.findAllRunesIndex(runner, d.runes, d.runeStart, n, func(runeIndex, runeLength int) (int, int) {
|
|
if byteOffsets == nil {
|
|
return d.byteOffset + runeIndex, d.byteOffset + runeIndex + runeLength
|
|
}
|
|
start := runeIndex + d.runeOffset
|
|
return byteOffsets.byteIndex(start), byteOffsets.byteIndex(start + runeLength)
|
|
})
|
|
}
|
|
|
|
// FindAllRunesIndex returns a slice of rune index pairs identifying all
|
|
// successive matches in r.
|
|
func (re *Regexp) FindAllRunesIndex(r []rune, n int) ([][]int, error) {
|
|
if n == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
runner := re.getRunner()
|
|
defer re.putRunner(runner)
|
|
|
|
startAt := 0
|
|
if re.RightToLeft() {
|
|
startAt = len(r)
|
|
}
|
|
if re.quickCode != nil {
|
|
runner.code = re.quickCode
|
|
}
|
|
return re.findAllRunesIndex(runner, r, startAt, n, func(runeIndex, runeLength int) (int, int) {
|
|
return runeIndex, runeIndex + runeLength
|
|
})
|
|
}
|
|
|
|
func (re *Regexp) findAllRunesIndex(runner *Runner, input []rune, startAt, n int, makeIndex func(runeIndex, runeLength int) (int, int)) ([][]int, error) {
|
|
var out [][]int
|
|
var flat []int
|
|
if n > 0 {
|
|
out = make([][]int, 0, n)
|
|
flat = make([]int, 0, n*2)
|
|
}
|
|
|
|
prevEnd := -1
|
|
previousMatchLength := -1
|
|
for n != 0 {
|
|
m, err := runner.scan(input, nil, startAt, previousMatchLength, true, re.MatchTimeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if m == nil {
|
|
break
|
|
}
|
|
|
|
localIndex := m.runeSliceIndex()
|
|
if m.RuneLength != 0 || localIndex != prevEnd {
|
|
start, end := makeIndex(localIndex, m.RuneLength)
|
|
flat = append(flat, start, end)
|
|
out = append(out, flat[len(flat)-2:len(flat):len(flat)])
|
|
prevEnd = localIndex + m.RuneLength
|
|
if n > 0 {
|
|
n--
|
|
}
|
|
}
|
|
|
|
startAt = m.textpos
|
|
previousMatchLength = m.RuneLength
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
type stringByteMapper struct {
|
|
runeIndexes []int
|
|
deltas []int
|
|
}
|
|
|
|
func newStringByteMapper(s string) *stringByteMapper {
|
|
var mapper *stringByteMapper
|
|
runeIndex := 0
|
|
delta := 0
|
|
for strIdx, ch := range s {
|
|
runeLen := utf8.RuneLen(ch)
|
|
if ch == utf8.RuneError {
|
|
_, runeLen = utf8.DecodeRuneInString(s[strIdx:])
|
|
}
|
|
if runeLen != 1 {
|
|
if mapper == nil {
|
|
mapper = &stringByteMapper{}
|
|
}
|
|
delta += runeLen - 1
|
|
mapper.runeIndexes = append(mapper.runeIndexes, runeIndex+1)
|
|
mapper.deltas = append(mapper.deltas, delta)
|
|
}
|
|
runeIndex++
|
|
}
|
|
return mapper
|
|
}
|
|
|
|
func (m *stringByteMapper) byteIndex(runeIndex int) int {
|
|
i := sort.Search(len(m.runeIndexes), func(i int) bool {
|
|
return m.runeIndexes[i] > runeIndex
|
|
}) - 1
|
|
if i < 0 {
|
|
return runeIndex
|
|
}
|
|
return runeIndex + m.deltas[i]
|
|
}
|
|
|
|
// FindNextMatch returns the next match in the same input string as the match parameter.
|
|
// Will return nil if there is no next match or if given a nil match.
|
|
func (re *Regexp) FindNextMatch(m *Match) (*Match, error) {
|
|
if m == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return re.run(false, m.textpos, m.RuneLength, m.text.runes, m.text)
|
|
}
|
|
|
|
// MatchString return true if the string matches the regex
|
|
// error will be set if a timeout occurs
|
|
func (re *Regexp) MatchString(s string) (bool, error) {
|
|
if re.stringPrefixFilter != nil && !re.RightToLeft() {
|
|
candidateByteIndex, ok := re.stringPrefixFilter(s, 0)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
|
|
return re.matchStringAt(s, candidateByteIndex)
|
|
}
|
|
return re.matchString(s)
|
|
}
|
|
|
|
func (re *Regexp) matchString(s string) (bool, error) {
|
|
return re.matchStringAt(s, -1)
|
|
}
|
|
|
|
func (re *Regexp) matchStringAt(s string, startAt int) (bool, error) {
|
|
runner := re.getRunner()
|
|
var input []rune
|
|
var pooledInput *[]rune
|
|
runeStart := 0
|
|
if startAt <= 0 {
|
|
// Common path: decode the whole string without start/offset work.
|
|
input, pooledInput = decodeString(s, re.optimizations.MaxCachedRuneBufferLength)
|
|
if re.RightToLeft() {
|
|
runeStart = len(input)
|
|
}
|
|
} else {
|
|
d := decodeInput(s, startAt, re.decodeFrom(s, startAt), re.optimizations.MaxCachedRuneBufferLength, false)
|
|
input = d.runes
|
|
pooledInput = d.pooled
|
|
runeStart = d.runeStart
|
|
if runeStart < 0 {
|
|
runeStart = 0
|
|
}
|
|
}
|
|
defer func() {
|
|
re.putRunner(runner)
|
|
if pooledInput != nil {
|
|
*pooledInput = input
|
|
pooledRuneBuffers.put(pooledInput)
|
|
}
|
|
}()
|
|
if re.quickCode != nil {
|
|
runner.code = re.quickCode
|
|
}
|
|
|
|
m, err := runner.scan(input, nil, runeStart, -1, true, re.MatchTimeout)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return m != nil, nil
|
|
}
|
|
|
|
// MatchRunes return true if the runes matches the regex
|
|
// error will be set if a timeout occurs
|
|
func (re *Regexp) MatchRunes(r []rune) (bool, error) {
|
|
m, err := re.run(true, -1, -1, r, nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return m != nil, nil
|
|
}
|
|
|
|
// GetGroupNames Returns the set of strings used to name capturing groups in the expression.
|
|
func (re *Regexp) GetGroupNames() []string {
|
|
var result []string
|
|
|
|
if re.capslist == nil {
|
|
result = make([]string, re.capsize)
|
|
|
|
for i := 0; i < len(result); i++ {
|
|
result[i] = strconv.Itoa(i)
|
|
}
|
|
} else {
|
|
result = make([]string, len(re.capslist))
|
|
copy(result, re.capslist)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetGroupNumbers returns the integer group numbers corresponding to a group name.
|
|
func (re *Regexp) GetGroupNumbers() []int {
|
|
var result []int
|
|
|
|
if re.caps == nil {
|
|
result = make([]int, re.capsize)
|
|
|
|
for i := 0; i < len(result); i++ {
|
|
result[i] = i
|
|
}
|
|
} else {
|
|
result = make([]int, len(re.caps))
|
|
|
|
for k, v := range re.caps {
|
|
result[v] = k
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GroupNameFromNumber retrieves a group name that corresponds to a group number.
|
|
// It will return "" for an unknown group number. Unnamed groups automatically
|
|
// receive a name that is the decimal string equivalent of its number, except in
|
|
// ECMAScript mode where unnamed groups have no name.
|
|
func (re *Regexp) GroupNameFromNumber(i int) string {
|
|
if re.capslist == nil {
|
|
if i >= 0 && i < re.capsize {
|
|
return strconv.Itoa(i)
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
if re.caps != nil {
|
|
var ok bool
|
|
if i, ok = re.caps[i]; !ok {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
if i >= 0 && i < len(re.capslist) {
|
|
return re.capslist[i]
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// GroupNumberFromName returns a group number that corresponds to a group name.
|
|
// Returns -1 if the name is not a recognized group name. Numbered groups
|
|
// automatically get a group name that is the decimal string equivalent of its
|
|
// number, except in ECMAScript mode where unnamed groups have no name.
|
|
func (re *Regexp) GroupNumberFromName(name string) int {
|
|
// look up name if we have a hashtable of names
|
|
if re.capnames != nil {
|
|
if k, ok := re.capnames[name]; ok {
|
|
return k
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
// convert to an int if it looks like a number
|
|
result := 0
|
|
for i := 0; i < len(name); i++ {
|
|
ch := name[i]
|
|
|
|
if ch > '9' || ch < '0' {
|
|
return -1
|
|
}
|
|
|
|
result *= 10
|
|
result += int(ch - '0')
|
|
}
|
|
|
|
// return int if it's in range
|
|
if result >= 0 && result < re.capsize {
|
|
return result
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
// MarshalText implements [encoding.TextMarshaler]. The output
|
|
// matches that of calling the [Regexp.String] method.
|
|
func (re *Regexp) MarshalText() ([]byte, error) {
|
|
return []byte(re.String()), nil
|
|
}
|
|
|
|
// UnmarshalText implements [encoding.TextUnmarshaler] by calling
|
|
// [Compile] on the encoded value.
|
|
func (re *Regexp) UnmarshalText(text []byte) error {
|
|
newRE, err := Compile(string(text), DefaultUnmarshalOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*re = *newRE
|
|
return nil
|
|
}
|
|
|
|
func (re *Regexp) initCaches() {
|
|
re.runnerPool = &sync.Pool{
|
|
New: func() any {
|
|
return &Runner{
|
|
re: re,
|
|
code: re.code,
|
|
}
|
|
},
|
|
}
|
|
if re.optimizations.MaxCachedReplacerDataEntries > 0 {
|
|
re.replaceCache = newReplacerDataCache(re.optimizations.MaxCachedReplacerDataEntries)
|
|
}
|
|
}
|
|
|
|
type replacerDataCache struct {
|
|
mu sync.Mutex
|
|
maxSize int
|
|
ll *list.List
|
|
cache map[string]*list.Element
|
|
}
|
|
|
|
type replacerDataCacheEntry struct {
|
|
key string
|
|
data *syntax.ReplacerData
|
|
}
|
|
|
|
func newReplacerDataCache(maxSize int) *replacerDataCache {
|
|
return &replacerDataCache{
|
|
maxSize: maxSize,
|
|
ll: list.New(),
|
|
cache: make(map[string]*list.Element),
|
|
}
|
|
}
|
|
|
|
func (c *replacerDataCache) get(key string) (*syntax.ReplacerData, bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if ele, ok := c.cache[key]; ok {
|
|
c.ll.MoveToFront(ele)
|
|
return ele.Value.(*replacerDataCacheEntry).data, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (c *replacerDataCache) add(key string, data *syntax.ReplacerData) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if ele, ok := c.cache[key]; ok {
|
|
ele.Value.(*replacerDataCacheEntry).data = data
|
|
c.ll.MoveToFront(ele)
|
|
return
|
|
}
|
|
|
|
ele := c.ll.PushFront(&replacerDataCacheEntry{key: key, data: data})
|
|
c.cache[key] = ele
|
|
if c.maxSize > 0 && c.ll.Len() > c.maxSize {
|
|
oldest := c.ll.Back()
|
|
if oldest != nil {
|
|
c.ll.Remove(oldest)
|
|
delete(c.cache, oldest.Value.(*replacerDataCacheEntry).key)
|
|
}
|
|
}
|
|
}
|