mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
553 lines
17 KiB
Go
553 lines
17 KiB
Go
/*
|
|
Copyright © 2018-2026 blacktop
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
*/
|
|
|
|
package ent
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// VersionComparator represents an iOS version for comparison purposes
|
|
type VersionComparator struct {
|
|
MajorMinor string // e.g., "26.0", "18.5"
|
|
Build string // e.g., "22G87", "beta6", "22F76"
|
|
Raw string // Original version string
|
|
}
|
|
|
|
// ReplacementConfig holds configuration for replacement operations
|
|
type ReplacementConfig struct {
|
|
Strategy string // auto, prompt, force
|
|
DryRun bool
|
|
}
|
|
|
|
// ReplacementPlan represents a planned replacement operation
|
|
type ReplacementPlan struct {
|
|
ToReplace []IPSWInfo
|
|
NewIPSW IPSWInfo
|
|
Config *ReplacementConfig
|
|
ReasonMsg string
|
|
ConflictType string
|
|
}
|
|
|
|
// IPSWInfo represents basic information about an IPSW
|
|
type IPSWInfo struct {
|
|
Version string
|
|
BuildID string
|
|
ID string // Database ID
|
|
Name string // Filename
|
|
Platform string // Platform (iOS, macOS, etc.)
|
|
}
|
|
|
|
// ParseVersionComparator creates a VersionComparator from a version string
|
|
func ParseVersionComparator(version, build string) VersionComparator {
|
|
majorMinor := extractMajorMinorVersion(version)
|
|
return VersionComparator{
|
|
MajorMinor: majorMinor,
|
|
Build: build,
|
|
Raw: version,
|
|
}
|
|
}
|
|
|
|
// extractMajorMinorVersion extracts major.minor from version string
|
|
// Examples: "26.0.1" -> "26.0", "18.5" -> "18.5"
|
|
func extractMajorMinorVersion(version string) string {
|
|
parts := strings.Split(version, ".")
|
|
if len(parts) >= 2 {
|
|
return parts[0] + "." + parts[1]
|
|
}
|
|
return version
|
|
}
|
|
|
|
// ShouldReplace determines if an existing version should be replaced by a new version
|
|
func ShouldReplace(existing, new VersionComparator) bool {
|
|
// Only replace if same major.minor version
|
|
if existing.MajorMinor != new.MajorMinor {
|
|
return false
|
|
}
|
|
|
|
// If versions are identical, compare builds
|
|
if existing.Raw == new.Raw {
|
|
return CompareBuildVersions(existing.Build, new.Build) < 0
|
|
}
|
|
|
|
// Compare full versions (e.g., 26.0.1 vs 26.0.2)
|
|
return CompareVersionStrings(existing.Raw, new.Raw) < 0
|
|
}
|
|
|
|
// ShouldReplaceWithPlatform determines if an existing IPSW should be replaced by a new one
|
|
// considering both platform and version
|
|
func ShouldReplaceWithPlatform(existingIPSW, newIPSW IPSWInfo) bool {
|
|
// Only replace if same platform
|
|
if existingIPSW.Platform != newIPSW.Platform {
|
|
return false
|
|
}
|
|
|
|
// Use existing version comparison logic
|
|
existingComparator := ParseVersionComparator(existingIPSW.Version, existingIPSW.BuildID)
|
|
newComparator := ParseVersionComparator(newIPSW.Version, newIPSW.BuildID)
|
|
|
|
return ShouldReplace(existingComparator, newComparator)
|
|
}
|
|
|
|
// CompareBuildVersions compares two build version strings
|
|
// Returns: -1 if a < b, 0 if a == b, 1 if a > b
|
|
func CompareBuildVersions(a, b string) int {
|
|
// Handle beta versions
|
|
if strings.Contains(a, "beta") || strings.Contains(b, "beta") {
|
|
return compareBetaBuilds(a, b)
|
|
}
|
|
|
|
// Handle numeric build IDs (e.g., 22G87 vs 22G89)
|
|
if isNumericBuild(a) && isNumericBuild(b) {
|
|
return compareNumericBuilds(a, b)
|
|
}
|
|
|
|
// Fallback to string comparison
|
|
if a < b {
|
|
return -1
|
|
} else if a > b {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// CompareVersionStrings compares two version strings (e.g., "26.0.1" vs "26.0.2")
|
|
func CompareVersionStrings(a, b string) int {
|
|
aParts := parseVersionParts(a)
|
|
bParts := parseVersionParts(b)
|
|
|
|
maxLen := max(len(bParts), len(aParts))
|
|
|
|
for i := range maxLen {
|
|
aVal := 0
|
|
bVal := 0
|
|
|
|
if i < len(aParts) {
|
|
aVal = aParts[i]
|
|
}
|
|
if i < len(bParts) {
|
|
bVal = bParts[i]
|
|
}
|
|
|
|
if aVal < bVal {
|
|
return -1
|
|
} else if aVal > bVal {
|
|
return 1
|
|
}
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// parseVersionParts parses a version string into numeric parts
|
|
func parseVersionParts(version string) []int {
|
|
parts := strings.Split(version, ".")
|
|
result := make([]int, len(parts))
|
|
|
|
for i, part := range parts {
|
|
if val, err := strconv.Atoi(part); err == nil {
|
|
result[i] = val
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// compareBetaBuilds compares beta build versions
|
|
func compareBetaBuilds(a, b string) int {
|
|
betaRegex := regexp.MustCompile(`beta(\d+)`)
|
|
|
|
aMatches := betaRegex.FindStringSubmatch(a)
|
|
bMatches := betaRegex.FindStringSubmatch(b)
|
|
|
|
// If both are beta versions
|
|
if len(aMatches) > 1 && len(bMatches) > 1 {
|
|
aNum, _ := strconv.Atoi(aMatches[1])
|
|
bNum, _ := strconv.Atoi(bMatches[1])
|
|
|
|
if aNum < bNum {
|
|
return -1
|
|
} else if aNum > bNum {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Beta versions are considered "less than" release versions
|
|
if len(aMatches) > 1 && len(bMatches) == 0 {
|
|
return -1
|
|
}
|
|
if len(aMatches) == 0 && len(bMatches) > 1 {
|
|
return 1
|
|
}
|
|
|
|
// Fallback to string comparison
|
|
return strings.Compare(a, b)
|
|
}
|
|
|
|
// isNumericBuild checks if a build ID follows numeric pattern (e.g., 22G87)
|
|
func isNumericBuild(build string) bool {
|
|
match, _ := regexp.MatchString(`^\d+[A-Z]+\d+$`, build)
|
|
return match
|
|
}
|
|
|
|
// compareNumericBuilds compares numeric build IDs
|
|
func compareNumericBuilds(a, b string) int {
|
|
// Extract prefix numbers and suffix numbers
|
|
aPrefix, aSuffix := extractBuildParts(a)
|
|
bPrefix, bSuffix := extractBuildParts(b)
|
|
|
|
// Compare prefixes first
|
|
if aPrefix != bPrefix {
|
|
if aPrefix < bPrefix {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
|
|
// Compare suffixes
|
|
if aSuffix < bSuffix {
|
|
return -1
|
|
} else if aSuffix > bSuffix {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// extractBuildParts extracts numeric parts from build ID (e.g., "22G87" -> 22, 87)
|
|
func extractBuildParts(build string) (int, int) {
|
|
regex := regexp.MustCompile(`^(\d+)[A-Z]+(\d+)$`)
|
|
matches := regex.FindStringSubmatch(build)
|
|
|
|
if len(matches) < 3 {
|
|
return 0, 0
|
|
}
|
|
|
|
prefix, _ := strconv.Atoi(matches[1])
|
|
suffix, _ := strconv.Atoi(matches[2])
|
|
|
|
return prefix, suffix
|
|
}
|
|
|
|
// CreateReplacementPlan analyzes what would be replaced and creates a plan
|
|
func CreateReplacementPlan(newIPSW IPSWInfo, existingIPSWs []IPSWInfo, config *ReplacementConfig) *ReplacementPlan {
|
|
var toReplace []IPSWInfo
|
|
var reasonMsg string
|
|
|
|
for _, existing := range existingIPSWs {
|
|
if ShouldReplaceWithPlatform(existing, newIPSW) {
|
|
toReplace = append(toReplace, existing)
|
|
}
|
|
}
|
|
|
|
if len(toReplace) > 0 {
|
|
if len(toReplace) == 1 {
|
|
reasonMsg = fmt.Sprintf("Replacing %s %s (build %s) with %s %s (build %s)",
|
|
toReplace[0].Platform, toReplace[0].Version, toReplace[0].BuildID,
|
|
newIPSW.Platform, newIPSW.Version, newIPSW.BuildID)
|
|
} else {
|
|
newComparator := ParseVersionComparator(newIPSW.Version, newIPSW.BuildID)
|
|
reasonMsg = fmt.Sprintf("Replacing %d older builds of %s %s with newer build %s",
|
|
len(toReplace), newIPSW.Platform, newComparator.MajorMinor, newIPSW.BuildID)
|
|
}
|
|
} else {
|
|
newComparator := ParseVersionComparator(newIPSW.Version, newIPSW.BuildID)
|
|
reasonMsg = fmt.Sprintf("No older builds found for %s %s - will add as new entry", newIPSW.Platform, newComparator.MajorMinor)
|
|
}
|
|
|
|
return &ReplacementPlan{
|
|
ToReplace: toReplace,
|
|
NewIPSW: newIPSW,
|
|
Config: config,
|
|
ReasonMsg: reasonMsg,
|
|
ConflictType: determineConflictType(toReplace, newIPSW),
|
|
}
|
|
}
|
|
|
|
// determineConflictType categorizes the type of replacement
|
|
func determineConflictType(toReplace []IPSWInfo, newIPSW IPSWInfo) string {
|
|
if len(toReplace) == 0 {
|
|
return "new"
|
|
}
|
|
|
|
if len(toReplace) == 1 {
|
|
existing := toReplace[0]
|
|
if existing.Version == newIPSW.Version {
|
|
return "build_update"
|
|
}
|
|
return "version_update"
|
|
}
|
|
|
|
return "multiple_replace"
|
|
}
|
|
|
|
// ReplacementStrategy interface for different replacement implementations
|
|
type ReplacementStrategy interface {
|
|
SupportsVersionBasedReplacement() bool
|
|
CreateReplacementPlan(newIPSW IPSWInfo, existingIPSWs []IPSWInfo, config *ReplacementConfig) (*ReplacementPlan, error)
|
|
ExecuteReplacement(plan *ReplacementPlan) error
|
|
GetExistingIPSWs(platform, version string) ([]IPSWInfo, error)
|
|
}
|
|
|
|
// SQLiteReplacementStrategy handles replacements for SQLite databases
|
|
type SQLiteReplacementStrategy struct {
|
|
service *DatabaseService
|
|
}
|
|
|
|
// NewSQLiteReplacementStrategy creates a new SQLite replacement strategy
|
|
func NewSQLiteReplacementStrategy(service *DatabaseService) *SQLiteReplacementStrategy {
|
|
return &SQLiteReplacementStrategy{service: service}
|
|
}
|
|
|
|
// SupportsVersionBasedReplacement returns true for SQLite strategy
|
|
func (s *SQLiteReplacementStrategy) SupportsVersionBasedReplacement() bool {
|
|
return true
|
|
}
|
|
|
|
// CreateReplacementPlan creates a replacement plan for SQLite
|
|
func (s *SQLiteReplacementStrategy) CreateReplacementPlan(newIPSW IPSWInfo, existingIPSWs []IPSWInfo, config *ReplacementConfig) (*ReplacementPlan, error) {
|
|
return CreateReplacementPlan(newIPSW, existingIPSWs, config), nil
|
|
}
|
|
|
|
// ExecuteReplacement executes the replacement plan atomically
|
|
func (s *SQLiteReplacementStrategy) ExecuteReplacement(plan *ReplacementPlan) error {
|
|
if s.service.gormDB == nil {
|
|
return fmt.Errorf("GORM database required for replacement operations")
|
|
}
|
|
|
|
// If dry run, just log what would happen
|
|
if plan.Config.DryRun {
|
|
fmt.Printf("DRY RUN: %s\n", plan.ReasonMsg)
|
|
return nil
|
|
}
|
|
|
|
// Prompt user if strategy is prompt
|
|
if plan.Config.Strategy == "prompt" && len(plan.ToReplace) > 0 {
|
|
if !promptUserForReplacement(plan) {
|
|
fmt.Printf("Replacement cancelled by user\n")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Execute atomic replacement transaction
|
|
return s.service.gormDB.Transaction(func(tx *gorm.DB) error {
|
|
// Step 1: Delete old IPSW data
|
|
for _, oldIPSW := range plan.ToReplace {
|
|
if err := s.deleteIPSWData(tx, oldIPSW.ID); err != nil {
|
|
return fmt.Errorf("failed to delete old IPSW %s: %w", oldIPSW.ID, err)
|
|
}
|
|
}
|
|
|
|
fmt.Printf("✓ %s\n", plan.ReasonMsg)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// GetExistingIPSWs retrieves existing IPSWs for version comparison
|
|
func (s *SQLiteReplacementStrategy) GetExistingIPSWs(platform, version string) ([]IPSWInfo, error) {
|
|
if s.service.gormDB == nil {
|
|
return nil, fmt.Errorf("GORM database required")
|
|
}
|
|
|
|
majorMinor := extractMajorMinorVersion(version)
|
|
|
|
// Query IPSWs with same platform and major.minor version
|
|
var ipsws []struct {
|
|
ID string
|
|
Name string
|
|
Version string
|
|
BuildID string
|
|
Platform string
|
|
}
|
|
|
|
err := s.service.gormDB.Table("ipsws").
|
|
Select("id, name, version, buildid as build_id, platform").
|
|
Where("platform = ? AND version LIKE ?", platform, majorMinor+"%").
|
|
Find(&ipsws).Error
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query existing IPSWs: %w", err)
|
|
}
|
|
|
|
result := make([]IPSWInfo, len(ipsws))
|
|
for i, ipsw := range ipsws {
|
|
result[i] = IPSWInfo{
|
|
ID: ipsw.ID,
|
|
Name: ipsw.Name,
|
|
Version: ipsw.Version,
|
|
BuildID: ipsw.BuildID,
|
|
Platform: ipsw.Platform,
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// deleteIPSWData deletes all data associated with an IPSW
|
|
func (s *SQLiteReplacementStrategy) deleteIPSWData(tx *gorm.DB, ipswID string) error {
|
|
// Delete entitlements (cascading delete handles references)
|
|
if err := tx.Exec("DELETE FROM entitlements WHERE ipsw_id = ?", ipswID).Error; err != nil {
|
|
return fmt.Errorf("failed to delete entitlements: %w", err)
|
|
}
|
|
|
|
// Delete IPSW record
|
|
if err := tx.Exec("DELETE FROM ipsws WHERE id = ?", ipswID).Error; err != nil {
|
|
return fmt.Errorf("failed to delete IPSW record: %w", err)
|
|
}
|
|
|
|
// Clean up orphaned references
|
|
return s.cleanupOrphanedReferences(tx)
|
|
}
|
|
|
|
// cleanupOrphanedReferences removes unused paths, keys, and values
|
|
func (s *SQLiteReplacementStrategy) cleanupOrphanedReferences(tx *gorm.DB) error {
|
|
// Remove paths not referenced by any entitlements
|
|
if err := tx.Exec(`
|
|
DELETE FROM paths
|
|
WHERE id NOT IN (SELECT DISTINCT path_id FROM entitlements WHERE path_id IS NOT NULL)
|
|
`).Error; err != nil {
|
|
return fmt.Errorf("failed to cleanup orphaned paths: %w", err)
|
|
}
|
|
|
|
// Remove keys not referenced by any entitlements
|
|
if err := tx.Exec(`
|
|
DELETE FROM entitlement_keys
|
|
WHERE id NOT IN (SELECT DISTINCT key_id FROM entitlements WHERE key_id IS NOT NULL)
|
|
`).Error; err != nil {
|
|
return fmt.Errorf("failed to cleanup orphaned keys: %w", err)
|
|
}
|
|
|
|
// Remove values not referenced by any entitlements
|
|
if err := tx.Exec(`
|
|
DELETE FROM entitlement_values
|
|
WHERE id NOT IN (SELECT DISTINCT value_id FROM entitlements WHERE value_id IS NOT NULL)
|
|
`).Error; err != nil {
|
|
return fmt.Errorf("failed to cleanup orphaned values: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PostgreSQLReplacementStrategy handles replacements for PostgreSQL databases
|
|
type PostgreSQLReplacementStrategy struct {
|
|
service *DatabaseService
|
|
}
|
|
|
|
// NewPostgreSQLReplacementStrategy creates a new PostgreSQL replacement strategy
|
|
func NewPostgreSQLReplacementStrategy(service *DatabaseService) *PostgreSQLReplacementStrategy {
|
|
return &PostgreSQLReplacementStrategy{service: service}
|
|
}
|
|
|
|
// SupportsVersionBasedReplacement returns true for PostgreSQL strategy
|
|
func (p *PostgreSQLReplacementStrategy) SupportsVersionBasedReplacement() bool {
|
|
return true
|
|
}
|
|
|
|
// CreateReplacementPlan creates a replacement plan for PostgreSQL
|
|
func (p *PostgreSQLReplacementStrategy) CreateReplacementPlan(newIPSW IPSWInfo, existingIPSWs []IPSWInfo, config *ReplacementConfig) (*ReplacementPlan, error) {
|
|
return CreateReplacementPlan(newIPSW, existingIPSWs, config), nil
|
|
}
|
|
|
|
// ExecuteReplacement executes the replacement plan atomically
|
|
func (p *PostgreSQLReplacementStrategy) ExecuteReplacement(plan *ReplacementPlan) error {
|
|
// Same implementation as SQLite for now since both use GORM
|
|
sqliteStrategy := &SQLiteReplacementStrategy{service: p.service}
|
|
return sqliteStrategy.ExecuteReplacement(plan)
|
|
}
|
|
|
|
// GetExistingIPSWs retrieves existing IPSWs for version comparison
|
|
func (p *PostgreSQLReplacementStrategy) GetExistingIPSWs(platform, version string) ([]IPSWInfo, error) {
|
|
// Same implementation as SQLite for now since both use GORM
|
|
sqliteStrategy := &SQLiteReplacementStrategy{service: p.service}
|
|
return sqliteStrategy.GetExistingIPSWs(platform, version)
|
|
}
|
|
|
|
// LegacyReplacementStrategy handles replacements for legacy database formats
|
|
type LegacyReplacementStrategy struct{}
|
|
|
|
// NewLegacyReplacementStrategy creates a new legacy replacement strategy
|
|
func NewLegacyReplacementStrategy() *LegacyReplacementStrategy {
|
|
return &LegacyReplacementStrategy{}
|
|
}
|
|
|
|
// SupportsVersionBasedReplacement returns false for legacy strategy (simple overwrite)
|
|
func (l *LegacyReplacementStrategy) SupportsVersionBasedReplacement() bool {
|
|
return false
|
|
}
|
|
|
|
// CreateReplacementPlan creates a simple replacement plan for legacy format
|
|
func (l *LegacyReplacementStrategy) CreateReplacementPlan(newIPSW IPSWInfo, existingIPSWs []IPSWInfo, config *ReplacementConfig) (*ReplacementPlan, error) {
|
|
return &ReplacementPlan{
|
|
ToReplace: existingIPSWs, // Replace all existing data
|
|
NewIPSW: newIPSW,
|
|
Config: config,
|
|
ReasonMsg: "Legacy format: replacing entire database with new data",
|
|
ConflictType: "legacy_overwrite",
|
|
}, nil
|
|
}
|
|
|
|
// ExecuteReplacement executes simple overwrite for legacy format
|
|
func (l *LegacyReplacementStrategy) ExecuteReplacement(plan *ReplacementPlan) error {
|
|
if plan.Config.DryRun {
|
|
fmt.Printf("DRY RUN: %s\n", plan.ReasonMsg)
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("✓ %s\n", plan.ReasonMsg)
|
|
// For legacy format, the actual overwrite is handled by the calling code
|
|
return nil
|
|
}
|
|
|
|
// GetExistingIPSWs returns empty list for legacy format
|
|
func (l *LegacyReplacementStrategy) GetExistingIPSWs(platform, version string) ([]IPSWInfo, error) {
|
|
return []IPSWInfo{}, nil
|
|
}
|
|
|
|
// promptUserForReplacement prompts the user to confirm replacement
|
|
func promptUserForReplacement(plan *ReplacementPlan) bool {
|
|
fmt.Printf("\n%s\n", plan.ReasonMsg)
|
|
|
|
if len(plan.ToReplace) > 0 {
|
|
fmt.Printf("This will delete the following existing data:\n")
|
|
for _, ipsw := range plan.ToReplace {
|
|
fmt.Printf(" - %s (iOS %s, build %s)\n", ipsw.Name, ipsw.Version, ipsw.BuildID)
|
|
}
|
|
}
|
|
|
|
fmt.Printf("\nDo you want to continue? (y/N): ")
|
|
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
if scanner.Scan() {
|
|
response := strings.ToLower(strings.TrimSpace(scanner.Text()))
|
|
return response == "y" || response == "yes"
|
|
}
|
|
|
|
return false
|
|
}
|