package index
import (
"encoding/gob"
"maps"
"os"
"path/filepath"
"sync"
"github.com/maypok86/otter/v2"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
folderutil "github.com/projectdiscovery/utils/folder"
)
const (
IndexFileName = "index.gob"
IndexVersion = 1
DefaultMaxSize = 50000
DefaultMaxWeight = DefaultMaxSize * 800
)
type Index struct {
cache *otter.Cache[string, *Metadata]
cacheFile string
mu sync.RWMutex
version int
}
type cacheSnapshot struct {
Version int `gob:"version"`
Data map[string]*Metadata `gob:"data"`
}
func NewIndex(cacheDir string) (*Index, error) {
if cacheDir == "" {
cacheDir = folderutil.AppCacheDirOrDefault(".nuclei-cache", config.BinaryName)
}
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return nil, err
}
cacheFile := filepath.Join(cacheDir, IndexFileName)
opts := &otter.Options[string, *Metadata]{
MaximumWeight: uint64(DefaultMaxWeight),
Weigher: func(key string, value *Metadata) uint32 {
if value == nil {
return uint32(len(key))
}
weight := len(key)
weight += len(value.ID)
weight += len(value.FilePath)
weight += 24
weight += len(value.Name)
weight += len(value.Severity)
weight += len(value.ProtocolType)
weight += len(value.TemplateVerifier)
for _, author := range value.Authors {
weight += len(author)
}
for _, tag := range value.Tags {
weight += len(tag)
}
return uint32(weight)
},
}
cache, err := otter.New(opts)
if err != nil {
return nil, err
}
c := &Index{
cache: cache,
cacheFile: cacheFile,
version: IndexVersion,
}
return c, nil
}
func NewDefaultIndex() (*Index, error) {
return NewIndex("")
}
func (i *Index) Get(path string) (*Metadata, bool) {
i.mu.RLock()
defer i.mu.RUnlock()
metadata, found := i.cache.GetIfPresent(path)
if !found {
return nil, false
}
if !metadata.IsValid() {
go i.Delete(path)
return nil, false
}
return metadata, true
}
func (i *Index) Set(path string, metadata *Metadata) (*Metadata, bool) {
i.mu.Lock()
defer i.mu.Unlock()
return i.cache.Set(path, metadata)
}
func (i *Index) SetFromTemplate(path string, tpl *templates.Template) (*Metadata, bool) {
metadata := NewMetadataFromTemplate(path, tpl)
info, err := os.Stat(path)
if err != nil {
return metadata, false
}
metadata.ModTime = info.ModTime()
if i.cache == nil {
return metadata, false
}
return i.Set(path, metadata)
}
func (i *Index) Has(path string) bool {
i.mu.RLock()
defer i.mu.RUnlock()
_, found := i.cache.GetIfPresent(path)
return found
}
func (i *Index) Delete(path string) {
i.mu.Lock()
defer i.mu.Unlock()
i.cache.Invalidate(path)
}
func (i *Index) Size() int {
i.mu.RLock()
defer i.mu.RUnlock()
return i.cache.EstimatedSize()
}
func (i *Index) Clear() {
i.mu.Lock()
defer i.mu.Unlock()
i.cache.InvalidateAll()
}
func (i *Index) Save() error {
i.mu.RLock()
defer i.mu.RUnlock()
snapshot := &cacheSnapshot{
Version: i.version,
Data: make(map[string]*Metadata),
}
maps.Insert(snapshot.Data, i.cache.All())
tmpFile := i.cacheFile + ".tmp"
file, err := os.Create(tmpFile)
if err != nil {
return err
}
encoder := gob.NewEncoder(file)
if err := encoder.Encode(snapshot); err != nil {
_ = file.Close()
_ = os.Remove(tmpFile)
return err
}
if err := file.Close(); err != nil {
_ = os.Remove(tmpFile)
return err
}
if err := os.Rename(tmpFile, i.cacheFile); err != nil {
_ = os.Remove(tmpFile)
return err
}
return nil
}
func (i *Index) Load() error {
file, err := os.Open(i.cacheFile)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer func() { _ = file.Close() }()
var snapshot cacheSnapshot
decoder := gob.NewDecoder(file)
if err := decoder.Decode(&snapshot); err != nil {
_ = file.Close()
_ = os.Remove(i.cacheFile)
return nil
}
if snapshot.Version != i.version {
_ = file.Close()
_ = os.Remove(i.cacheFile)
return nil
}
i.mu.Lock()
defer i.mu.Unlock()
for key, value := range snapshot.Data {
i.cache.Set(key, value)
}
return nil
}
func (i *Index) Filter(filter *Filter) []string {
if filter == nil || filter.IsEmpty() {
return i.All()
}
i.mu.RLock()
defer i.mu.RUnlock()
var matched []string
for path, metadata := range i.cache.All() {
if filter.Matches(metadata) {
matched = append(matched, path)
}
}
return matched
}
func (i *Index) FilterFunc(fn FilterFunc) []string {
if fn == nil {
return i.All()
}
i.mu.RLock()
defer i.mu.RUnlock()
var matched []string
for path, metadata := range i.cache.All() {
if fn(metadata) {
matched = append(matched, path)
}
}
return matched
}
func (i *Index) All() []string {
i.mu.RLock()
defer i.mu.RUnlock()
paths := make([]string, 0, i.cache.EstimatedSize())
for path := range i.cache.All() {
paths = append(paths, path)
}
return paths
}
func (i *Index) GetAll() map[string]*Metadata {
i.mu.RLock()
defer i.mu.RUnlock()
result := maps.Collect(i.cache.All())
return result
}
func (i *Index) Count(filter *Filter) int {
if filter == nil || filter.IsEmpty() {
return i.Size()
}
i.mu.RLock()
defer i.mu.RUnlock()
count := 0
for _, metadata := range i.cache.All() {
if filter.Matches(metadata) {
count++
}
}
return count
}