// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package identity import ( "fmt" "sync" "github.com/gohugoio/hugo/compare" ) // NewFinder creates a new Finder. // This is a thread safe implementation with a cache. func NewFinder(cfg FinderConfig) *Finder { return &Finder{cfg: cfg, answers: make(map[ManagerIdentity]FinderResult), seenFindOnce: make(map[Identity]bool)} } var searchIDPool = sync.Pool{ New: func() interface{} { return &searchID{seen: make(map[Manager]bool)} }, } func getSearchID() *searchID { return searchIDPool.Get().(*searchID) } func putSearchID(sid *searchID) { sid.id = nil sid.isDp = false sid.isPeq = false sid.hasEqer = false sid.maxDepth = 0 sid.dp = nil sid.peq = nil sid.eqer = nil for k := range sid.seen { delete(sid.seen, k) } searchIDPool.Put(sid) } // GetSearchID returns a searchID from the pool. // Finder finds identities inside another. type Finder struct { cfg FinderConfig answers map[ManagerIdentity]FinderResult muAnswers sync.RWMutex seenFindOnce map[Identity]bool muSeenFindOnce sync.RWMutex } type FinderResult int const ( FinderNotFound FinderResult = iota FinderFoundOneOfManyRepetition FinderFoundOneOfMany FinderFound ) // Contains returns whether in contains id. func (f *Finder) Contains(id, in Identity, maxDepth int) FinderResult { if id == Anonymous || in == Anonymous { return FinderNotFound } if id == GenghisKhan && in == GenghisKhan { return FinderNotFound } if id == GenghisKhan { return FinderFound } if id == in { return FinderFound } if id == nil || in == nil { return FinderNotFound } var ( isDp bool isPeq bool dp IsProbablyDependentProvider peq compare.ProbablyEqer ) if !f.cfg.Exact { dp, isDp = id.(IsProbablyDependentProvider) peq, isPeq = id.(compare.ProbablyEqer) } eqer, hasEqer := id.(compare.Eqer) sid := getSearchID() sid.id = id sid.isDp = isDp sid.isPeq = isPeq sid.hasEqer = hasEqer sid.dp = dp sid.peq = peq sid.eqer = eqer sid.maxDepth = maxDepth defer putSearchID(sid) r := FinderNotFound if i := f.checkOne(sid, in, 0); i > r { r = i } if r == FinderFound { return r } m := GetDependencyManager(in) if m != nil { if i := f.checkManager(sid, m, 0); i > r { r = i } } return r } func (f *Finder) checkMaxDepth(sid *searchID, level int) FinderResult { if sid.maxDepth >= 0 && level > sid.maxDepth { return FinderNotFound } if level > 100 { // This should never happen, but some false positives are probably better than a panic. if !f.cfg.Exact { return FinderFound } panic("too many levels") } return -1 } func (f *Finder) checkManager(sid *searchID, m Manager, level int) FinderResult { if r := f.checkMaxDepth(sid, level); r >= 0 { return r } if m == nil { return FinderNotFound } if sid.seen[m] { return FinderNotFound } sid.seen[m] = true f.muAnswers.RLock() r, ok := f.answers[ManagerIdentity{Manager: m, Identity: sid.id}] f.muAnswers.RUnlock() if ok { return r } r = f.search(sid, m, level) if r == FinderFoundOneOfMany { // Don't cache this one. return r } f.muAnswers.Lock() f.answers[ManagerIdentity{Manager: m, Identity: sid.id}] = r f.muAnswers.Unlock() return r } func (f *Finder) checkOne(sid *searchID, v Identity, depth int) (r FinderResult) { if ff, ok := v.(FindFirstManagerIdentityProvider); ok { f.muSeenFindOnce.RLock() mi := ff.FindFirstManagerIdentity() seen := f.seenFindOnce[mi.Identity] f.muSeenFindOnce.RUnlock() if seen { return FinderFoundOneOfManyRepetition } r = f.doCheckOne(sid, mi.Identity, depth) if r == 0 { r = f.checkManager(sid, mi.Manager, depth) } if r > FinderFoundOneOfManyRepetition { f.muSeenFindOnce.Lock() // Double check. if f.seenFindOnce[mi.Identity] { f.muSeenFindOnce.Unlock() return FinderFoundOneOfManyRepetition } f.seenFindOnce[mi.Identity] = true f.muSeenFindOnce.Unlock() r = FinderFoundOneOfMany } return r } else { return f.doCheckOne(sid, v, depth) } } func (f *Finder) doCheckOne(sid *searchID, v Identity, depth int) FinderResult { id2 := Unwrap(v) if id2 == Anonymous { return FinderNotFound } id := sid.id if sid.hasEqer { if sid.eqer.Eq(id2) { return FinderFound } } else if id == id2 { return FinderFound } if f.cfg.Exact { return FinderNotFound } if id2 == nil { return FinderNotFound } if id2 == GenghisKhan { return FinderFound } if id.IdentifierBase() == id2.IdentifierBase() { return FinderFound } if sid.isDp && sid.dp.IsProbablyDependent(id2) { return FinderFound } if sid.isPeq && sid.peq.ProbablyEq(id2) { return FinderFound } if pdep, ok := id2.(IsProbablyDependencyProvider); ok && pdep.IsProbablyDependency(id) { return FinderFound } if peq, ok := id2.(compare.ProbablyEqer); ok && peq.ProbablyEq(id) { return FinderFound } return FinderNotFound } // search searches for id in ids. func (f *Finder) search(sid *searchID, m Manager, depth int) FinderResult { id := sid.id if id == Anonymous { return FinderNotFound } if !f.cfg.Exact && id == GenghisKhan { return FinderNotFound } var r FinderResult m.forEeachIdentity( func(v Identity) bool { i := f.checkOne(sid, v, depth) if i > r { r = i } if r == FinderFound { return true } m := GetDependencyManager(v) if i := f.checkManager(sid, m, depth+1); i > r { r = i } if r == FinderFound { return true } return false }, ) return r } // FinderConfig provides configuration for the Finder. // Note that we by default will use a strategy where probable matches are // good enough. The primary use case for this is to identity the change set // for a given changed identity (e.g. a template), and we don't want to // have any false negatives there, but some false positives are OK. Also, speed is important. type FinderConfig struct { // Match exact matches only. Exact bool } // ManagerIdentity wraps a pair of Identity and Manager. type ManagerIdentity struct { Identity Manager } func (p ManagerIdentity) String() string { return fmt.Sprintf("%s:%s", p.Identity.IdentifierBase(), p.Manager.IdentifierBase()) } type searchID struct { id Identity isDp bool isPeq bool hasEqer bool maxDepth int seen map[Manager]bool dp IsProbablyDependentProvider peq compare.ProbablyEqer eqer compare.Eqer }