Files
ipsw/internal/commands/watch/cache.go
blacktop 8e6f2c554b chore: modernize to 1.26
Refactor string concatenation to use strings.Builder for improved performance in multiple files
2026-02-12 10:02:01 -07:00

261 lines
5.7 KiB
Go

package watch
import (
"bytes"
"encoding/json"
"fmt"
"maps"
"os"
"path/filepath"
"slices"
"time"
lru "github.com/hashicorp/golang-lru/v2"
)
type Tags []string
type Commits []string
type Function map[string]string
type FcsKey struct {
Fingerprint string `json:"fingerprint,omitempty"`
IosBuild string `json:"ios_build,omitempty"`
MacOsBuild string `json:"macos_build,omitempty"`
VisionOsBuild string `json:"visionos_build,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
type FcsKeys struct {
Beta FcsKey `json:"beta"`
RC FcsKey `json:"rc"`
Release FcsKey `json:"release"`
}
type CacheItem struct {
Tags Tags `json:"tags,omitempty"`
Commits Commits `json:"commits,omitempty"`
Functions Function `json:"functions,omitempty"`
URL string `json:"url,omitempty"`
FcsKeys FcsKeys `json:"fcs_keys"`
}
type Cache map[string]CacheItem
type WatchCache interface {
Add(key string, value any) error
Get(key string) *CacheItem
Has(key string, value any) bool
}
type MemoryCache struct {
cache *lru.Cache[string, any]
}
type FileCache struct {
path string
}
func NewMemoryCache(size int) (*MemoryCache, error) {
lcache, err := lru.New[string, any](size)
if err != nil {
return nil, err
}
return &MemoryCache{
cache: lcache,
}, nil
}
func (c *MemoryCache) Add(key string, value any) error {
// Get existing item or create new one
var item CacheItem
if existingVal, found := c.cache.Get(key); found {
item = existingVal.(CacheItem)
}
// Update the appropriate field based on value type
switch v := value.(type) {
case Tags:
// Merge tags with existing ones
for _, tag := range v {
if !slices.Contains(item.Tags, tag) {
item.Tags = append(item.Tags, tag)
}
}
case Commits:
// Merge commits with existing ones
for _, commit := range v {
if !slices.Contains(item.Commits, commit) {
item.Commits = append(item.Commits, commit)
}
}
case Function:
if item.Functions == nil {
item.Functions = make(Function)
}
maps.Copy(item.Functions, v)
default:
return fmt.Errorf("unexpected cache item type %T", v)
}
c.cache.Add(key, item)
return nil
}
func (c *MemoryCache) Get(key string) *CacheItem {
val, found := c.cache.Get(key)
if !found {
return nil
}
item := val.(CacheItem)
return &item
}
func (c *MemoryCache) Has(key string, value any) bool {
if item, ok := c.cache.Get(key); ok {
switch v := value.(type) {
case Tags:
for _, tag := range v {
if slices.Contains(item.(CacheItem).Tags, tag) {
return true
}
}
case Commits:
for _, commit := range v {
if slices.Contains(item.(CacheItem).Commits, commit) {
return true
}
}
case Function:
for name, commit := range v {
if commitVal, ok := item.(CacheItem).Functions[name]; ok && commitVal == commit {
return true
}
}
}
}
return false
}
func NewFileCache(path string) (*FileCache, error) {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return nil, err
}
// Check if file exists, create it if it doesn't
if _, err := os.Stat(path); os.IsNotExist(err) {
cache := Cache{}
var out bytes.Buffer
if err := json.NewEncoder(&out).Encode(cache); err != nil {
return nil, fmt.Errorf("failed to encode initial cache data: %w", err)
}
if err := os.WriteFile(path, out.Bytes(), 0644); err != nil {
return nil, err
}
}
return &FileCache{
path: path,
}, nil
}
func (c *FileCache) Add(key string, value any) error {
// read existing JSON
data, err := os.ReadFile(c.path)
if err != nil {
return fmt.Errorf("failed to read cache file %s: %w", c.path, err)
}
var cache Cache
if err := json.Unmarshal(data, &cache); err != nil {
return fmt.Errorf("failed to unmarshal cache file %s: %w", c.path, err)
}
if item, found := cache[key]; found {
switch v := value.(type) {
case Tags:
// Merge tags with existing ones
for _, tag := range v {
if !slices.Contains(item.Tags, tag) {
item.Tags = append(item.Tags, tag)
}
}
case Commits:
// Update commits
for _, commit := range v {
if !slices.Contains(item.Commits, commit) {
item.Commits = append(item.Commits, commit)
}
}
case Function:
maps.Copy(item.Functions, v)
default:
return fmt.Errorf("unexpected cache item type %T", v)
}
cache[key] = item
} else {
switch v := value.(type) {
case Tags:
cache[key] = CacheItem{Tags: v}
case Commits:
cache[key] = CacheItem{Commits: v}
case Function:
cache[key] = CacheItem{Functions: v}
default:
return fmt.Errorf("unexpected cache item type %T", v)
}
}
var out bytes.Buffer
if err := json.NewEncoder(&out).Encode(cache); err != nil {
return fmt.Errorf("failed to encode cache to JSON: %w", err)
}
if err := os.WriteFile(c.path, out.Bytes(), 0644); err != nil {
return fmt.Errorf("failed to write cache JSON: %w", err)
}
return nil
}
func (c *FileCache) Get(key string) *CacheItem {
data, err := os.ReadFile(c.path)
if err != nil {
return nil
}
var cache Cache
if err := json.Unmarshal(data, &cache); err != nil {
return nil
}
if item, ok := cache[key]; ok {
return &item
}
return nil
}
func (c *FileCache) Has(key string, value any) bool {
data, err := os.ReadFile(c.path)
if err != nil {
return false
}
var cache Cache
if err := json.Unmarshal(data, &cache); err != nil {
return false
}
if item, ok := cache[key]; ok {
switch v := value.(type) {
case Tags:
for _, tag := range v {
if slices.Contains(item.Tags, tag) {
return true
}
}
case Commits:
for _, commit := range v {
if slices.Contains(item.Commits, commit) {
return true
}
}
case Function:
for name, commit := range v {
if commitVal, ok := item.Functions[name]; ok && commitVal == commit {
return true
}
}
}
}
return false
}