mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
687 lines
20 KiB
Go
687 lines
20 KiB
Go
package ent
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
|
|
"github.com/blacktop/go-plist"
|
|
"github.com/blacktop/ipsw/internal/db"
|
|
"github.com/blacktop/ipsw/internal/model"
|
|
"github.com/blacktop/ipsw/pkg/info"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// DatabaseService handles entitlement database operations
|
|
type DatabaseService struct {
|
|
db db.Database
|
|
gormDB *gorm.DB
|
|
}
|
|
|
|
// NewDatabaseService creates a new database service
|
|
func NewDatabaseService(database db.Database) *DatabaseService {
|
|
ds := &DatabaseService{db: database}
|
|
|
|
// Try to get GORM database if available
|
|
switch dbImpl := database.(type) {
|
|
case *db.Sqlite:
|
|
ds.gormDB = dbImpl.GetDB()
|
|
case *db.Postgres:
|
|
ds.gormDB = dbImpl.GetDB()
|
|
}
|
|
|
|
return ds
|
|
}
|
|
|
|
// StoreEntitlements stores entitlements from a database into the SQLite database
|
|
func (ds *DatabaseService) StoreEntitlements(ipswPath string, entDB map[string]string) error {
|
|
// Get or create IPSW record
|
|
var ipswRecord *model.Ipsw
|
|
|
|
if ipswPath != "" {
|
|
// Parse IPSW info from file
|
|
ipswInfo, err := info.Parse(ipswPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse IPSW info: %v", err)
|
|
}
|
|
|
|
// Detect platform from IPSW
|
|
platform := DetectPlatformFromIPSW(ipswPath, ipswInfo)
|
|
|
|
ipswRecord = &model.Ipsw{
|
|
ID: generateIPSWIDWithPlatform(platform, ipswInfo.Plists.BuildManifest.ProductVersion, ipswInfo.Plists.BuildManifest.ProductBuildVersion),
|
|
Name: filepath.Base(ipswPath),
|
|
Version: ipswInfo.Plists.BuildManifest.ProductVersion,
|
|
BuildID: ipswInfo.Plists.BuildManifest.ProductBuildVersion,
|
|
Platform: platform,
|
|
}
|
|
|
|
// Add devices
|
|
for _, deviceName := range ipswInfo.Plists.BuildManifest.SupportedProductTypes {
|
|
device := &model.Device{Name: deviceName}
|
|
ipswRecord.Devices = append(ipswRecord.Devices, device)
|
|
}
|
|
|
|
// Create or update IPSW record (proper upsert)
|
|
if ds.gormDB != nil {
|
|
// Check if IPSW record already exists
|
|
var existingIPSW model.Ipsw
|
|
result := ds.gormDB.Where("id = ?", ipswRecord.ID).First(&existingIPSW)
|
|
|
|
if result.Error != nil && result.Error != gorm.ErrRecordNotFound {
|
|
return fmt.Errorf("failed to check existing IPSW record: %v", result.Error)
|
|
}
|
|
|
|
if result.Error == gorm.ErrRecordNotFound {
|
|
// Record doesn't exist, create it
|
|
if err := ds.gormDB.Create(ipswRecord).Error; err != nil {
|
|
return fmt.Errorf("failed to create IPSW record: %v", err)
|
|
}
|
|
} else {
|
|
// Record exists, update it
|
|
if err := ds.gormDB.Model(&existingIPSW).Updates(ipswRecord).Error; err != nil {
|
|
return fmt.Errorf("failed to update IPSW record: %v", err)
|
|
}
|
|
// Use the existing record for further processing
|
|
ipswRecord = &existingIPSW
|
|
}
|
|
} else {
|
|
// Fallback for other database types
|
|
if err := ds.db.Create(ipswRecord); err != nil {
|
|
return fmt.Errorf("failed to create IPSW record: %v", err)
|
|
}
|
|
}
|
|
} else {
|
|
// Create a minimal IPSW record for standalone usage
|
|
ipswRecord = &model.Ipsw{
|
|
ID: generateIPSWIDWithPlatform(model.PlatformIOS, "unknown", "unknown"),
|
|
Version: "unknown",
|
|
BuildID: "unknown",
|
|
Platform: model.PlatformIOS,
|
|
}
|
|
}
|
|
|
|
// Store entitlements using bulk operations for better performance
|
|
if err := ds.storeEntitlementsBulk(ipswRecord, entDB); err != nil {
|
|
return fmt.Errorf("failed to bulk store entitlements: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// storeEntitlementsBulk stores entitlements using bulk operations for much better performance
|
|
func (ds *DatabaseService) storeEntitlementsBulk(ipswRecord *model.Ipsw, entDB map[string]string) error {
|
|
if ds.gormDB == nil {
|
|
return fmt.Errorf("GORM database required for bulk entitlement storage")
|
|
}
|
|
|
|
// Collect all unique keys, values, and paths first
|
|
var allKeys []model.EntitlementKey
|
|
var allValues []model.EntitlementValue
|
|
var allPaths []model.Path
|
|
var entitlements []model.Entitlement
|
|
|
|
// Maps to avoid duplicates during collection
|
|
keySet := make(map[string]bool)
|
|
valueSet := make(map[string]bool) // using hash as key
|
|
pathSet := make(map[string]bool)
|
|
|
|
// First pass: collect all unique keys, values, and paths
|
|
for filePath, entPlist := range entDB {
|
|
if len(entPlist) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Add path if not seen before
|
|
if !pathSet[filePath] {
|
|
allPaths = append(allPaths, model.Path{Path: filePath})
|
|
pathSet[filePath] = true
|
|
}
|
|
|
|
// Parse entitlement plist
|
|
ents := make(map[string]any)
|
|
if err := plist.NewDecoder(bytes.NewReader([]byte(entPlist))).Decode(&ents); err != nil {
|
|
continue // Skip invalid plists
|
|
}
|
|
|
|
for key, value := range ents {
|
|
// Add key if not seen before
|
|
if !keySet[key] {
|
|
allKeys = append(allKeys, model.EntitlementKey{Key: key})
|
|
keySet[key] = true
|
|
}
|
|
|
|
// Determine value string and type
|
|
var valueStr, valueType string
|
|
switch v := value.(type) {
|
|
case bool:
|
|
valueType = "bool"
|
|
if v {
|
|
valueStr = "true"
|
|
} else {
|
|
valueStr = "false"
|
|
}
|
|
case string:
|
|
valueType = "string"
|
|
valueStr = v
|
|
case int, int64, uint64:
|
|
valueType = "number"
|
|
if num, ok := v.(int64); ok {
|
|
valueStr = fmt.Sprintf("%d", num)
|
|
} else if num, ok := v.(int); ok {
|
|
valueStr = fmt.Sprintf("%d", num)
|
|
} else if num, ok := v.(uint64); ok {
|
|
valueStr = fmt.Sprintf("%d", num)
|
|
}
|
|
case []any:
|
|
valueType = "array"
|
|
if jsonData, err := json.Marshal(v); err == nil {
|
|
valueStr = string(jsonData)
|
|
}
|
|
case map[string]any:
|
|
valueType = "dict"
|
|
if jsonData, err := json.Marshal(v); err == nil {
|
|
valueStr = string(jsonData)
|
|
}
|
|
default:
|
|
valueType = "unknown"
|
|
valueStr = fmt.Sprintf("%v", v)
|
|
}
|
|
|
|
// Add value if not seen before
|
|
valueHash := createValueHash(valueType, valueStr)
|
|
if !valueSet[valueHash] {
|
|
allValues = append(allValues, model.EntitlementValue{
|
|
Value: valueStr,
|
|
ValueType: valueType,
|
|
ValueHash: valueHash,
|
|
})
|
|
valueSet[valueHash] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bulk insert unique keys, values, and paths with ON CONFLICT DO NOTHING
|
|
if len(allKeys) > 0 {
|
|
if err := ds.gormDB.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(allKeys, 1000).Error; err != nil {
|
|
return fmt.Errorf("failed to bulk insert unique keys: %v", err)
|
|
}
|
|
}
|
|
|
|
if len(allValues) > 0 {
|
|
if err := ds.gormDB.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(allValues, 1000).Error; err != nil {
|
|
return fmt.Errorf("failed to bulk insert unique values: %v", err)
|
|
}
|
|
}
|
|
|
|
if len(allPaths) > 0 {
|
|
if err := ds.gormDB.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(allPaths, 1000).Error; err != nil {
|
|
return fmt.Errorf("failed to bulk insert unique paths: %v", err)
|
|
}
|
|
}
|
|
|
|
// Query back only the keys, values, and paths we need to get their IDs
|
|
keyMap := make(map[string]uint)
|
|
valueMap := make(map[string]uint) // hash -> ID
|
|
pathMap := make(map[string]uint)
|
|
|
|
// Get key IDs for only the keys we need
|
|
if len(keySet) > 0 {
|
|
keyNames := make([]string, 0, len(keySet))
|
|
for key := range keySet {
|
|
keyNames = append(keyNames, key)
|
|
}
|
|
|
|
var dbKeys []model.EntitlementKey
|
|
if err := ds.gormDB.Where("key IN ?", keyNames).Find(&dbKeys).Error; err != nil {
|
|
return fmt.Errorf("failed to query unique keys: %v", err)
|
|
}
|
|
for _, key := range dbKeys {
|
|
keyMap[key.Key] = key.ID
|
|
}
|
|
}
|
|
|
|
// Get value IDs for only the values we need
|
|
if len(valueSet) > 0 {
|
|
valueHashes := make([]string, 0, len(valueSet))
|
|
for hash := range valueSet {
|
|
valueHashes = append(valueHashes, hash)
|
|
}
|
|
|
|
var dbValues []model.EntitlementValue
|
|
if err := ds.gormDB.Where("value_hash IN ?", valueHashes).Find(&dbValues).Error; err != nil {
|
|
return fmt.Errorf("failed to query unique values: %v", err)
|
|
}
|
|
for _, value := range dbValues {
|
|
valueMap[value.ValueHash] = value.ID
|
|
}
|
|
}
|
|
|
|
// Get path IDs for only the paths we need
|
|
if len(pathSet) > 0 {
|
|
pathNames := make([]string, 0, len(pathSet))
|
|
for path := range pathSet {
|
|
pathNames = append(pathNames, path)
|
|
}
|
|
|
|
var dbPaths []model.Path
|
|
if err := ds.gormDB.Where("path IN ?", pathNames).Find(&dbPaths).Error; err != nil {
|
|
return fmt.Errorf("failed to query unique paths: %v", err)
|
|
}
|
|
for _, path := range dbPaths {
|
|
pathMap[path.Path] = path.ID
|
|
}
|
|
}
|
|
|
|
// Second pass: create web entries with resolved IDs
|
|
for filePath, entPlist := range entDB {
|
|
if len(entPlist) == 0 {
|
|
continue
|
|
}
|
|
|
|
pathID, exists := pathMap[filePath]
|
|
if !exists {
|
|
continue // Skip if path not found
|
|
}
|
|
|
|
// Parse entitlement plist
|
|
ents := make(map[string]any)
|
|
if err := plist.NewDecoder(bytes.NewReader([]byte(entPlist))).Decode(&ents); err != nil {
|
|
continue // Skip invalid plists
|
|
}
|
|
|
|
for key, value := range ents {
|
|
keyID, keyExists := keyMap[key]
|
|
if !keyExists {
|
|
continue
|
|
}
|
|
|
|
// Determine value string and type (same logic as before)
|
|
var valueStr, valueType string
|
|
switch v := value.(type) {
|
|
case bool:
|
|
valueType = "bool"
|
|
if v {
|
|
valueStr = "true"
|
|
} else {
|
|
valueStr = "false"
|
|
}
|
|
case string:
|
|
valueType = "string"
|
|
valueStr = v
|
|
case int, int64, uint64:
|
|
valueType = "number"
|
|
if num, ok := v.(int64); ok {
|
|
valueStr = fmt.Sprintf("%d", num)
|
|
} else if num, ok := v.(int); ok {
|
|
valueStr = fmt.Sprintf("%d", num)
|
|
} else if num, ok := v.(uint64); ok {
|
|
valueStr = fmt.Sprintf("%d", num)
|
|
}
|
|
case []any:
|
|
valueType = "array"
|
|
if jsonData, err := json.Marshal(v); err == nil {
|
|
valueStr = string(jsonData)
|
|
}
|
|
case map[string]any:
|
|
valueType = "dict"
|
|
if jsonData, err := json.Marshal(v); err == nil {
|
|
valueStr = string(jsonData)
|
|
}
|
|
default:
|
|
valueType = "unknown"
|
|
valueStr = fmt.Sprintf("%v", v)
|
|
}
|
|
|
|
valueHash := createValueHash(valueType, valueStr)
|
|
valueID, valueExists := valueMap[valueHash]
|
|
if !valueExists {
|
|
continue
|
|
}
|
|
|
|
// Create entitlement entry
|
|
entitlement := model.Entitlement{
|
|
IpswID: ipswRecord.ID,
|
|
PathID: pathID,
|
|
KeyID: keyID,
|
|
ValueID: valueID,
|
|
}
|
|
entitlements = append(entitlements, entitlement)
|
|
}
|
|
}
|
|
|
|
// Bulk insert entitlement entries with ON CONFLICT DO NOTHING
|
|
if len(entitlements) > 0 {
|
|
result := ds.gormDB.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(entitlements, 1000)
|
|
if result.Error != nil {
|
|
return fmt.Errorf("failed to bulk insert entitlement entries: %v", result.Error)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// storeEntitlement stores entitlement data directly to the normalized structure
|
|
func (ds *DatabaseService) storeEntitlement(ipswRecord *model.Ipsw, filePath, entPlist string) error {
|
|
if ds.gormDB == nil {
|
|
return fmt.Errorf("GORM database required for normalized entitlement storage")
|
|
}
|
|
|
|
// Parse entitlement plist if not empty
|
|
if len(entPlist) > 0 {
|
|
ents := make(map[string]any)
|
|
if err := plist.NewDecoder(bytes.NewReader([]byte(entPlist))).Decode(&ents); err == nil {
|
|
|
|
// Process each entitlement key-value pair directly
|
|
for key, value := range ents {
|
|
// Get or create unique key ID
|
|
keyID, err := ds.getOrCreateUniqueKey(key)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get unique key ID for '%s': %v", key, err)
|
|
}
|
|
|
|
// Determine the value string and type based on value type
|
|
var valueStr, valueType string
|
|
switch v := value.(type) {
|
|
case bool:
|
|
valueType = "bool"
|
|
if v {
|
|
valueStr = "true"
|
|
} else {
|
|
valueStr = "false"
|
|
}
|
|
case string:
|
|
valueType = "string"
|
|
valueStr = v
|
|
case int, int64, uint64:
|
|
valueType = "number"
|
|
if num, ok := v.(int64); ok {
|
|
valueStr = fmt.Sprintf("%d", num)
|
|
} else if num, ok := v.(int); ok {
|
|
valueStr = fmt.Sprintf("%d", num)
|
|
} else if num, ok := v.(uint64); ok {
|
|
valueStr = fmt.Sprintf("%d", num)
|
|
}
|
|
case []any:
|
|
valueType = "array"
|
|
if jsonData, err := json.Marshal(v); err == nil {
|
|
valueStr = string(jsonData)
|
|
}
|
|
case map[string]any:
|
|
valueType = "dict"
|
|
if jsonData, err := json.Marshal(v); err == nil {
|
|
valueStr = string(jsonData)
|
|
}
|
|
default:
|
|
valueType = "unknown"
|
|
valueStr = fmt.Sprintf("%v", v)
|
|
}
|
|
|
|
// Get or create unique value ID
|
|
valueID, err := ds.getOrCreateUniqueValue(valueType, valueStr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get unique value ID for '%s' (type: %s): %v", valueStr, valueType, err)
|
|
}
|
|
|
|
// Get or create unique path ID
|
|
pathID, err := ds.getOrCreateUniquePath(filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get unique path ID for '%s': %v", filePath, err)
|
|
}
|
|
|
|
// Create or update entitlement entry (idempotent)
|
|
entitlement := &model.Entitlement{
|
|
IpswID: ipswRecord.ID,
|
|
PathID: pathID,
|
|
KeyID: keyID,
|
|
ValueID: valueID,
|
|
}
|
|
|
|
// Check if this exact combination already exists
|
|
var existingEntry model.Entitlement
|
|
result := ds.gormDB.Where("ipsw_id = ? AND path_id = ? AND key_id = ? AND value_id = ?",
|
|
entitlement.IpswID, entitlement.PathID, entitlement.KeyID, entitlement.ValueID).First(&existingEntry)
|
|
|
|
if result.Error != nil && result.Error != gorm.ErrRecordNotFound {
|
|
return fmt.Errorf("failed to check existing entitlement entry: %v", result.Error)
|
|
}
|
|
|
|
if result.Error == gorm.ErrRecordNotFound {
|
|
// Entry doesn't exist, create it
|
|
if err := ds.gormDB.Create(entitlement).Error; err != nil {
|
|
return fmt.Errorf("failed to create entitlement entry: %v", err)
|
|
}
|
|
}
|
|
// If entry exists, skip it (idempotent behavior)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateIPSWID creates a unique ID for an IPSW based on version and build (legacy)
|
|
func generateIPSWID(version, build string) string {
|
|
return fmt.Sprintf("%s_%s", version, build)
|
|
}
|
|
|
|
// generateIPSWIDWithPlatform creates a unique ID for an IPSW based on platform, version and build
|
|
func generateIPSWIDWithPlatform(platform model.Platform, version, build string) string {
|
|
return fmt.Sprintf("%s_%s_%s", string(platform), version, build)
|
|
}
|
|
|
|
// createValueHash creates a hash for a value to ensure uniqueness
|
|
func createValueHash(valueType, value string) string {
|
|
hashInput := fmt.Sprintf("%s:%s", valueType, value)
|
|
hash := sha256.Sum256([]byte(hashInput))
|
|
// Return first 16 characters of hex-encoded hash for storage optimization
|
|
// This provides 99.999% collision safety while fitting in CHAR(16)
|
|
return hex.EncodeToString(hash[:])[:16]
|
|
}
|
|
|
|
// getOrCreateUniqueKey ensures a unique key exists and returns its ID
|
|
func (ds *DatabaseService) getOrCreateUniqueKey(key string) (uint, error) {
|
|
var uniqueKey model.EntitlementKey
|
|
|
|
// Try to find existing key
|
|
if err := ds.gormDB.Where("key = ?", key).First(&uniqueKey).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
// Create new unique key
|
|
uniqueKey = model.EntitlementKey{Key: key}
|
|
if err := ds.gormDB.Create(&uniqueKey).Error; err != nil {
|
|
return 0, fmt.Errorf("failed to create unique key: %v", err)
|
|
}
|
|
} else {
|
|
return 0, fmt.Errorf("failed to query unique key: %v", err)
|
|
}
|
|
}
|
|
|
|
return uniqueKey.ID, nil
|
|
}
|
|
|
|
// getOrCreateUniqueValue ensures a unique value exists and returns its ID
|
|
func (ds *DatabaseService) getOrCreateUniqueValue(valueType, value string) (uint, error) {
|
|
valueHash := createValueHash(valueType, value)
|
|
var uniqueValue model.EntitlementValue
|
|
|
|
// Try to find existing value
|
|
if err := ds.gormDB.Where("value_hash = ?", valueHash).First(&uniqueValue).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
// Create new unique value
|
|
uniqueValue = model.EntitlementValue{
|
|
Value: value,
|
|
ValueType: valueType,
|
|
ValueHash: valueHash,
|
|
}
|
|
if err := ds.gormDB.Create(&uniqueValue).Error; err != nil {
|
|
return 0, fmt.Errorf("failed to create unique value: %v", err)
|
|
}
|
|
} else {
|
|
return 0, fmt.Errorf("failed to query unique value: %v", err)
|
|
}
|
|
}
|
|
|
|
return uniqueValue.ID, nil
|
|
}
|
|
|
|
// getOrCreateUniquePath ensures a unique path exists and returns its ID
|
|
func (ds *DatabaseService) getOrCreateUniquePath(path string) (uint, error) {
|
|
var uniquePath model.Path
|
|
|
|
// Try to find existing path
|
|
if err := ds.gormDB.Where("path = ?", path).First(&uniquePath).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
// Create new unique path
|
|
uniquePath = model.Path{Path: path}
|
|
if err := ds.gormDB.Create(&uniquePath).Error; err != nil {
|
|
return 0, fmt.Errorf("failed to create unique path: %v", err)
|
|
}
|
|
} else {
|
|
return 0, fmt.Errorf("failed to query unique path: %v", err)
|
|
}
|
|
}
|
|
|
|
return uniquePath.ID, nil
|
|
}
|
|
|
|
// GetIOSVersions returns all available iOS versions in the database
|
|
func (ds *DatabaseService) GetIOSVersions() ([]string, error) {
|
|
if ds.gormDB == nil {
|
|
return nil, fmt.Errorf("GORM database required for version queries")
|
|
}
|
|
|
|
var versions []string
|
|
if err := ds.gormDB.Model(&model.Ipsw{}).
|
|
Distinct("version").
|
|
Order("version DESC").
|
|
Pluck("version", &versions).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to get iOS versions: %v", err)
|
|
}
|
|
|
|
return versions, nil
|
|
}
|
|
|
|
// SearchEntitlements performs optimized queries for the web UI
|
|
func (ds *DatabaseService) SearchEntitlements(version, keyPattern, filePattern string, limit int) ([]*model.Entitlement, error) {
|
|
if ds.gormDB == nil {
|
|
return nil, fmt.Errorf("GORM database required for web search")
|
|
}
|
|
|
|
query := ds.gormDB.Model(&model.Entitlement{}).
|
|
Preload("Path").
|
|
Preload("Key").
|
|
Preload("Value").
|
|
Preload("Ipsw")
|
|
|
|
// Filter by iOS version using JOIN with ipsws table
|
|
if version != "" {
|
|
query = query.Joins("JOIN ipsws ON ipsws.id = entitlements.ipsw_id").
|
|
Where("ipsws.version = ?", version)
|
|
}
|
|
|
|
// Filter by key pattern using JOIN with entitlement_keys table
|
|
if keyPattern != "" {
|
|
query = query.Joins("JOIN entitlement_keys ON entitlement_keys.id = entitlements.key_id").
|
|
Where("entitlement_keys.key LIKE ?", "%"+keyPattern+"%")
|
|
}
|
|
|
|
// Filter by file pattern using JOIN with paths table
|
|
if filePattern != "" {
|
|
query = query.Joins("JOIN paths ON paths.id = entitlements.path_id").
|
|
Where("paths.path LIKE ?", "%"+filePattern+"%")
|
|
}
|
|
|
|
// Order by path_id for consistent results
|
|
query = query.Order("path_id, key_id")
|
|
|
|
// Apply limit
|
|
if limit > 0 {
|
|
query = query.Limit(limit)
|
|
}
|
|
|
|
var results []*model.Entitlement
|
|
if err := query.Find(&results).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to search web entitlements: %v", err)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// GetStatistics returns database statistics using the new normalized structure
|
|
func (ds *DatabaseService) GetStatistics() (map[string]any, error) {
|
|
if ds.gormDB == nil {
|
|
return nil, fmt.Errorf("statistics not supported with this database type")
|
|
}
|
|
|
|
stats := make(map[string]any)
|
|
|
|
var ipswCount int64
|
|
if err := ds.gormDB.Model(&model.Ipsw{}).Count(&ipswCount).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to count IPSWs: %v", err)
|
|
}
|
|
stats["ipsw_count"] = ipswCount
|
|
|
|
var mappingCount int64
|
|
if err := ds.gormDB.Model(&model.Entitlement{}).Count(&mappingCount).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to count entitlement mappings: %v", err)
|
|
}
|
|
stats["entitlement_mapping_count"] = mappingCount
|
|
|
|
var uniqueKeyCount int64
|
|
if err := ds.gormDB.Model(&model.EntitlementKey{}).Count(&uniqueKeyCount).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to count unique keys: %v", err)
|
|
}
|
|
stats["unique_key_count"] = uniqueKeyCount
|
|
|
|
var uniqueValueCount int64
|
|
if err := ds.gormDB.Model(&model.EntitlementValue{}).Count(&uniqueValueCount).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to count unique values: %v", err)
|
|
}
|
|
stats["unique_value_count"] = uniqueValueCount
|
|
|
|
var uniquePathCount int64
|
|
if err := ds.gormDB.Model(&model.Path{}).Count(&uniquePathCount).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to count unique paths: %v", err)
|
|
}
|
|
stats["unique_path_count"] = uniquePathCount
|
|
|
|
// Get top 10 most common entitlement keys
|
|
var topKeys []struct {
|
|
Key string
|
|
Count int64
|
|
}
|
|
if err := ds.gormDB.Table("entitlements e").
|
|
Joins("JOIN entitlement_keys ek ON ek.id = e.key_id").
|
|
Select("ek.key, COUNT(*) as count").
|
|
Group("ek.key").
|
|
Order("COUNT(*) DESC"). // Use COUNT(*) instead of alias in ORDER BY
|
|
Limit(10).
|
|
Scan(&topKeys).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to get top keys: %v", err)
|
|
}
|
|
stats["top_keys"] = topKeys
|
|
|
|
// Get top 10 least common entitlement keys (excluding single occurrences)
|
|
var leastKeys []struct {
|
|
Key string
|
|
Count int64
|
|
}
|
|
if err := ds.gormDB.Table("entitlements e").
|
|
Joins("JOIN entitlement_keys ek ON ek.id = e.key_id").
|
|
Select("ek.key, COUNT(*) as count").
|
|
Group("ek.key").
|
|
Having("COUNT(*) > 1"). // Use COUNT(*) instead of alias in HAVING clause
|
|
Order("COUNT(*) ASC"). // Use COUNT(*) instead of alias in ORDER BY
|
|
Limit(10).
|
|
Scan(&leastKeys).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to get least common keys: %v", err)
|
|
}
|
|
stats["least_keys"] = leastKeys
|
|
|
|
return stats, nil
|
|
}
|