package cachefile import ( "net/netip" "os" "strings" "sync" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common" "go.etcd.io/bbolt" ) var ( bucketSelected = []byte("selected") bucketExpand = []byte("group_expand") bucketMode = []byte("clash_mode") bucketNameList = []string{ string(bucketSelected), string(bucketExpand), string(bucketMode), } cacheIDDefault = []byte("default") ) var _ adapter.ClashCacheFile = (*CacheFile)(nil) type CacheFile struct { DB *bbolt.DB cacheID []byte saveAccess sync.RWMutex saveDomain map[netip.Addr]string saveAddress4 map[string]netip.Addr saveAddress6 map[string]netip.Addr saveMetadataTimer *time.Timer } func Open(path string, cacheID string) (*CacheFile, error) { const fileMode = 0o666 options := bbolt.Options{Timeout: time.Second} db, err := bbolt.Open(path, fileMode, &options) switch err { case bbolt.ErrInvalid, bbolt.ErrChecksum, bbolt.ErrVersionMismatch: if err = os.Remove(path); err != nil { break } db, err = bbolt.Open(path, 0o666, &options) } if err != nil { return nil, err } var cacheIDBytes []byte if cacheID != "" { cacheIDBytes = append([]byte{0}, []byte(cacheID)...) } err = db.Batch(func(tx *bbolt.Tx) error { return tx.ForEach(func(name []byte, b *bbolt.Bucket) error { if name[0] == 0 { return b.ForEachBucket(func(k []byte) error { bucketName := string(k) if !(common.Contains(bucketNameList, bucketName)) { _ = b.DeleteBucket(name) } return nil }) } else { bucketName := string(name) if !(common.Contains(bucketNameList, bucketName) || strings.HasPrefix(bucketName, fakeipBucketPrefix)) { _ = tx.DeleteBucket(name) } } return nil }) }) if err != nil { return nil, err } return &CacheFile{ DB: db, cacheID: cacheIDBytes, saveDomain: make(map[netip.Addr]string), saveAddress4: make(map[string]netip.Addr), saveAddress6: make(map[string]netip.Addr), }, nil } func (c *CacheFile) LoadMode() string { var mode string c.DB.View(func(t *bbolt.Tx) error { bucket := t.Bucket(bucketMode) if bucket == nil { return nil } var modeBytes []byte if len(c.cacheID) > 0 { modeBytes = bucket.Get(c.cacheID) } else { modeBytes = bucket.Get(cacheIDDefault) } mode = string(modeBytes) return nil }) return mode } func (c *CacheFile) StoreMode(mode string) error { return c.DB.Batch(func(t *bbolt.Tx) error { bucket, err := t.CreateBucketIfNotExists(bucketMode) if err != nil { return err } if len(c.cacheID) > 0 { return bucket.Put(c.cacheID, []byte(mode)) } else { return bucket.Put(cacheIDDefault, []byte(mode)) } }) } func (c *CacheFile) bucket(t *bbolt.Tx, key []byte) *bbolt.Bucket { if c.cacheID == nil { return t.Bucket(key) } bucket := t.Bucket(c.cacheID) if bucket == nil { return nil } return bucket.Bucket(key) } func (c *CacheFile) createBucket(t *bbolt.Tx, key []byte) (*bbolt.Bucket, error) { if c.cacheID == nil { return t.CreateBucketIfNotExists(key) } bucket, err := t.CreateBucketIfNotExists(c.cacheID) if bucket == nil { return nil, err } return bucket.CreateBucketIfNotExists(key) } func (c *CacheFile) LoadSelected(group string) string { var selected string c.DB.View(func(t *bbolt.Tx) error { bucket := c.bucket(t, bucketSelected) if bucket == nil { return nil } selectedBytes := bucket.Get([]byte(group)) if len(selectedBytes) > 0 { selected = string(selectedBytes) } return nil }) return selected } func (c *CacheFile) StoreSelected(group, selected string) error { return c.DB.Batch(func(t *bbolt.Tx) error { bucket, err := c.createBucket(t, bucketSelected) if err != nil { return err } return bucket.Put([]byte(group), []byte(selected)) }) } func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) { c.DB.View(func(t *bbolt.Tx) error { bucket := c.bucket(t, bucketExpand) if bucket == nil { return nil } expandBytes := bucket.Get([]byte(group)) if len(expandBytes) == 1 { isExpand = expandBytes[0] == 1 loaded = true } return nil }) return } func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { return c.DB.Batch(func(t *bbolt.Tx) error { bucket, err := c.createBucket(t, bucketExpand) if err != nil { return err } if isExpand { return bucket.Put([]byte(group), []byte{1}) } else { return bucket.Put([]byte(group), []byte{0}) } }) } func (c *CacheFile) Close() error { return c.DB.Close() }