mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
`ipsw download ipsw --kernel` now fetches firmware keys from theapplewiki and decrypts encrypted kernelcaches inline. Unencrypted members in the same IPSW pass through unchanged. - pkg/img4: DecryptPayload reuses Payload.GetData for decompression, removing the duplicate LZSS/LZFSE branches. - pkg/kernelcache: ParseImg4Data switches to img4.ParsePayload and exports ErrEncryptedKernelCache so callers can detect the missing-key case via errors.Is. - internal/commands/extract: new keyed remote path with all-or-nothing preflight; the encryption-status peek lets unencrypted variants succeed even when the wiki has no entry for them. closes #1193
902 lines
26 KiB
Go
902 lines
26 KiB
Go
package info
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"compress/gzip"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/apex/log"
|
|
"github.com/blacktop/ipsw/internal/utils"
|
|
"github.com/blacktop/ipsw/pkg/devicetree"
|
|
"github.com/blacktop/ipsw/pkg/plist"
|
|
"github.com/blacktop/ipsw/pkg/xcode"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
//go:embed data/procs.gz
|
|
var procsData []byte
|
|
|
|
var ErrorCryptexNotFound = errors.New("cryptex not found")
|
|
|
|
// Info in the info object
|
|
type Info struct {
|
|
Plists *plist.Plists
|
|
DeviceTrees map[string]*devicetree.DeviceTree
|
|
}
|
|
|
|
type processors struct {
|
|
Name string
|
|
Model string
|
|
Semiconductor string
|
|
DieSize string
|
|
Transistors string
|
|
CPUISA string
|
|
CPU string
|
|
CPUID string
|
|
CPUCache []string
|
|
GPU string
|
|
AIAccelerator string
|
|
Memory string
|
|
Introduced string
|
|
Devices []string
|
|
}
|
|
|
|
type ProcessorDB []processors
|
|
|
|
func GetProcessorDB() (*ProcessorDB, error) {
|
|
var ps ProcessorDB
|
|
|
|
zr, err := gzip.NewReader(bytes.NewReader(procsData))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer zr.Close()
|
|
|
|
if err := json.NewDecoder(zr).Decode(&ps); err != nil {
|
|
return nil, fmt.Errorf("failed unmarshaling procs.gz data: %w", err)
|
|
}
|
|
|
|
return &ps, nil
|
|
}
|
|
|
|
func (p *ProcessorDB) GetProcessor(cpuid string) (*processors, error) {
|
|
for _, proc := range *p {
|
|
if strings.EqualFold(proc.CPUID, cpuid) {
|
|
return &proc, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("failed to find processor for '%s'", cpuid)
|
|
}
|
|
|
|
// getProcessors reads the processors from embedded JSON
|
|
func getProcessor(cpuid string) (processors, error) {
|
|
var ps []processors
|
|
|
|
zr, err := gzip.NewReader(bytes.NewReader(procsData))
|
|
if err != nil {
|
|
return processors{}, err
|
|
}
|
|
defer zr.Close()
|
|
|
|
if err := json.NewDecoder(zr).Decode(&ps); err != nil {
|
|
return processors{}, fmt.Errorf("failed unmarshaling procs.gz data: %w", err)
|
|
}
|
|
|
|
for _, p := range ps {
|
|
if strings.EqualFold(p.CPUID, cpuid) {
|
|
return p, nil
|
|
}
|
|
}
|
|
|
|
return processors{}, fmt.Errorf("failed to find processor for %s", cpuid)
|
|
}
|
|
|
|
func (i *Info) String() string {
|
|
var iStr strings.Builder
|
|
var verextra string
|
|
if i.Plists.OTAInfo != nil {
|
|
verextra = fmt.Sprintf(" %s", i.Plists.OTAInfo.MobileAssetProperties.ProductVersionExtra)
|
|
}
|
|
if i.Plists.BuildManifest != nil {
|
|
iStr.WriteString(fmt.Sprintf(
|
|
"Version = %s\n"+
|
|
"BuildVersion = %s\n"+
|
|
"OS Type = %s\n",
|
|
i.Plists.BuildManifest.ProductVersion+verextra,
|
|
i.Plists.BuildManifest.ProductBuildVersion,
|
|
i.Plists.GetOSType(),
|
|
))
|
|
}
|
|
if i.Plists.Restore != nil {
|
|
foundFS := false
|
|
if fsDMG, err := i.GetFileSystemOsDmg(); err == nil {
|
|
foundFS = true
|
|
iStr.WriteString(fmt.Sprintf("FileSystem = %s\n", fsDMG))
|
|
}
|
|
if fsDMG, err := i.GetSystemOsDmg(); err == nil {
|
|
iStr.WriteString(fmt.Sprintf("SystemOS = %s\n", fsDMG))
|
|
}
|
|
if fsDMG, err := i.GetAppOsDmg(); err == nil {
|
|
iStr.WriteString(fmt.Sprintf("AppOS = %s\n", fsDMG))
|
|
}
|
|
if fsDMG, err := i.GetExclaveOSDmg(); err == nil {
|
|
iStr.WriteString(fmt.Sprintf("ExclaveOS = %s\n", fsDMG))
|
|
}
|
|
if ramDisk, err := i.GetRestoreRamDiskDmgs(); err == nil {
|
|
iStr.WriteString(fmt.Sprintf("RestoreRamDisk = %s\n", ramDisk))
|
|
}
|
|
if !foundFS {
|
|
if len(i.Plists.Restore.SystemRestoreImageFileSystems) > 0 {
|
|
for file, fsType := range i.Plists.Restore.SystemRestoreImageFileSystems {
|
|
iStr.WriteString(fmt.Sprintf("FileSystem = %s (Type: %s)\n", file, fsType))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if i.Plists.OTAInfo != nil {
|
|
if len(i.Plists.OTAInfo.MobileAssetProperties.RestoreVersion) > 0 {
|
|
iStr.WriteString(fmt.Sprintf("RestoreVersion = %s\n", i.Plists.OTAInfo.MobileAssetProperties.RestoreVersion))
|
|
}
|
|
if len(i.Plists.OTAInfo.MobileAssetProperties.PrerequisiteBuild) > 0 {
|
|
iStr.WriteString(fmt.Sprintf("PrereqBuild = %s\n", i.Plists.OTAInfo.MobileAssetProperties.PrerequisiteBuild))
|
|
}
|
|
if i.Plists.OTAInfo.MobileAssetProperties.SplatOnly {
|
|
iStr.WriteString("IsRSR = ✅\n")
|
|
}
|
|
}
|
|
if len(i.DeviceTrees) > 0 {
|
|
kcs := i.Plists.BuildManifest.GetKernelCaches()
|
|
bls := i.Plists.BuildManifest.GetBootLoaders()
|
|
iStr.WriteString("\nDevices\n")
|
|
iStr.WriteString("-------\n")
|
|
for _, dtree := range i.DeviceTrees {
|
|
dt, _ := dtree.Summary()
|
|
prodName := dt.ProductName
|
|
if len(prodName) == 0 {
|
|
devices, err := xcode.GetDevices()
|
|
if err == nil {
|
|
for _, device := range devices {
|
|
if device.ProductType == dt.ProductType {
|
|
prodName = device.ProductDescription
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
prodName = dt.ProductType
|
|
}
|
|
}
|
|
iStr.WriteString(fmt.Sprintf("\n%s\n", prodName))
|
|
iStr.WriteString(fmt.Sprintf(" > %s_%s_%s\n", dt.ProductType, strings.ToUpper(dt.BoardConfig), i.Plists.BuildManifest.ProductBuildVersion))
|
|
if !dt.Timestamp.IsZero() {
|
|
iStr.WriteString(fmt.Sprintf(" - TimeStamp: %s\n", dt.Timestamp.Format("02 Jan 2006 15:04:05 MST")))
|
|
}
|
|
if len(kcs[strings.ToLower(dt.BoardConfig)]) > 0 {
|
|
iStr.WriteString(fmt.Sprintf(" - KernelCache: %s\n", strings.Join(kcs[strings.ToLower(dt.BoardConfig)], ", ")))
|
|
}
|
|
if cpu := i.GetCPU(dt.BoardConfig); len(cpu) > 0 {
|
|
iStr.WriteString(fmt.Sprintf(" - %s\n", cpu))
|
|
}
|
|
if len(bls[strings.ToLower(dt.BoardConfig)]) > 0 {
|
|
iStr.WriteString(" - BootLoaders\n")
|
|
for _, bl := range bls[strings.ToLower(dt.BoardConfig)] {
|
|
iStr.WriteString(fmt.Sprintf(" * %s\n", filepath.Base(bl)))
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if i.Plists.BuildManifest != nil {
|
|
iStr.WriteString("\nDevices\n")
|
|
iStr.WriteString("-------\n")
|
|
for _, dev := range i.Plists.BuildManifest.SupportedProductTypes {
|
|
iStr.WriteString(fmt.Sprintf(" > %s_%s\n", dev, i.Plists.BuildManifest.ProductBuildVersion))
|
|
}
|
|
}
|
|
}
|
|
return iStr.String()
|
|
}
|
|
|
|
type InfoJSON struct {
|
|
Type string `json:"type,omitempty"`
|
|
Version string `json:"version,omitempty"`
|
|
Build string `json:"build,omitempty"`
|
|
OS string `json:"os,omitempty"`
|
|
Devices any `json:"devices,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
func (i *Info) ToJSON() InfoJSON {
|
|
if i.Plists.BuildIdentities == nil {
|
|
return InfoJSON{Error: "no BuildManifest.plist found"}
|
|
}
|
|
return InfoJSON{
|
|
Type: i.Plists.Type,
|
|
Version: i.Plists.BuildManifest.ProductVersion,
|
|
Build: i.Plists.BuildManifest.ProductBuildVersion,
|
|
OS: i.Plists.GetOSType(),
|
|
Devices: func() any {
|
|
if len(i.DeviceTrees) > 0 {
|
|
var devs []struct {
|
|
Name string `json:"name,omitempty"`
|
|
Product string `json:"product,omitempty"`
|
|
Board string `json:"board,omitempty"`
|
|
Timestamp string `json:"timestamp,omitempty"`
|
|
CPU string `json:"cpu,omitempty"`
|
|
}
|
|
for _, dtree := range i.DeviceTrees {
|
|
dt, _ := dtree.Summary()
|
|
devs = append(devs, struct {
|
|
Name string `json:"name,omitempty"`
|
|
Product string `json:"product,omitempty"`
|
|
Board string `json:"board,omitempty"`
|
|
Timestamp string `json:"timestamp,omitempty"`
|
|
CPU string `json:"cpu,omitempty"`
|
|
}{
|
|
Name: dt.ProductName,
|
|
Product: dt.ProductType,
|
|
Board: dt.BoardConfig,
|
|
Timestamp: dt.Timestamp.Format("02 Jan 2006 15:04:05 MST"),
|
|
CPU: i.GetCPU(dt.BoardConfig),
|
|
})
|
|
}
|
|
return devs
|
|
} else {
|
|
return i.Plists.MobileAssetProperties.SupportedDevices
|
|
}
|
|
}(),
|
|
}
|
|
}
|
|
|
|
// GetAppOsDmg returns the name of the AppOS dmg
|
|
func (i *Info) GetAppOsDmg() (string, error) {
|
|
var dmgs []string
|
|
if i.Plists != nil && i.Plists.BuildManifest != nil {
|
|
for _, bi := range i.Plists.BuildIdentities {
|
|
if appOS, ok := bi.Manifest["Cryptex1,AppOS"]; ok {
|
|
dmgs = append(dmgs, appOS.Info["Path"].(string))
|
|
}
|
|
}
|
|
dmgs = utils.Unique(dmgs)
|
|
if len(dmgs) == 0 {
|
|
return "", fmt.Errorf("no AppOS DMG found: %w", ErrorCryptexNotFound)
|
|
} else if len(dmgs) == 1 {
|
|
return dmgs[0], nil
|
|
} else {
|
|
return "", fmt.Errorf("multiple AppOS DMGs found")
|
|
}
|
|
}
|
|
return "", fmt.Errorf("no BuildManifest.plist found")
|
|
}
|
|
|
|
// GetSystemOsDmg returns the name of the SystemOS dmg (the one with the dyld_shared_cache(s))
|
|
func (i *Info) GetSystemOsDmg() (string, error) {
|
|
var dmgs []string
|
|
if i.Plists != nil && i.Plists.BuildManifest != nil {
|
|
for _, bi := range i.Plists.BuildIdentities {
|
|
if sysOS, ok := bi.Manifest["Cryptex1,SystemOS"]; ok {
|
|
return sysOS.Info["Path"].(string), nil
|
|
}
|
|
}
|
|
dmgs = utils.Unique(dmgs)
|
|
if len(dmgs) == 0 {
|
|
return "", fmt.Errorf("no SystemOS DMG found: %w", ErrorCryptexNotFound)
|
|
} else if len(dmgs) == 1 {
|
|
return dmgs[0], nil
|
|
} else {
|
|
return "", fmt.Errorf("multiple SystemOS DMGs found")
|
|
}
|
|
}
|
|
return "", fmt.Errorf("no BuildManifest.plist found")
|
|
}
|
|
|
|
// GetFileSystemOsDmg returns the name of the file system dmg
|
|
func (i *Info) GetFileSystemOsDmg() (string, error) {
|
|
var dmgs []string
|
|
if i.Plists != nil && i.Plists.BuildManifest != nil {
|
|
for _, bi := range i.Plists.BuildIdentities {
|
|
if fsOS, ok := bi.Manifest["OS"]; ok {
|
|
// log.Debugf("Found: %s", fsOS.Info["Path"].(string))
|
|
if !strings.Contains(bi.Info.Variant, "Recovery") {
|
|
dmgs = append(dmgs, fsOS.Info["Path"].(string))
|
|
}
|
|
}
|
|
}
|
|
dmgs = utils.Unique(dmgs)
|
|
if len(dmgs) == 0 {
|
|
return "", fmt.Errorf("no filesystem DMG found: %w", ErrorCryptexNotFound)
|
|
} else if len(dmgs) == 1 {
|
|
return dmgs[0], nil
|
|
} else {
|
|
return "", fmt.Errorf("multiple filesystem DMGs found")
|
|
}
|
|
} else if i.Plists != nil && i.Plists.Restore != nil {
|
|
if dmg, ok := i.Plists.Restore.SystemRestoreImages["User"]; ok {
|
|
return dmg, nil
|
|
}
|
|
return "", fmt.Errorf("no BuildManifest.plist AND no SystemRestoreImages (used in older IPSWs) found")
|
|
}
|
|
return "", fmt.Errorf("no BuildManifest.plist found")
|
|
}
|
|
|
|
// GetRestoreRamDiskDmgs returns the name of the RestoreRamDisk dmg
|
|
func (i *Info) GetRestoreRamDiskDmgs() ([]string, error) {
|
|
var dmgs []string
|
|
if i.Plists != nil && i.Plists.BuildManifest != nil {
|
|
for _, bi := range i.Plists.BuildIdentities {
|
|
if rrdisk, ok := bi.Manifest["RestoreRamDisk"]; ok {
|
|
dmgs = append(dmgs, rrdisk.Info["Path"].(string))
|
|
}
|
|
}
|
|
dmgs = utils.Unique(dmgs)
|
|
}
|
|
if len(dmgs) > 0 {
|
|
return dmgs, nil
|
|
}
|
|
return nil, fmt.Errorf("no RestoreRamDisk DMG found")
|
|
}
|
|
|
|
// GetRestoreRamDiskDmgByIdent returns the RestoreRamDisk DMG path matching a given identity hint.
|
|
// The ident string is a substring of Info.Variant (e.g. "Customer Erase", "Customer Upgrade") [case-insensitive]
|
|
func (i *Info) GetRestoreRamDiskDmgByIdent(ident string) (string, error) {
|
|
if i.Plists == nil || i.Plists.BuildManifest == nil {
|
|
return "", fmt.Errorf("no BuildManifest.plist found")
|
|
}
|
|
if len(ident) == 0 {
|
|
return "", fmt.Errorf("ident is empty")
|
|
}
|
|
identLower := strings.ToLower(ident)
|
|
var matches []string
|
|
var variants []string
|
|
for _, bi := range i.Plists.BuildIdentities {
|
|
rrdisk, ok := bi.Manifest["RestoreRamDisk"]
|
|
if !ok {
|
|
continue
|
|
}
|
|
variants = append(variants, bi.Info.Variant)
|
|
path := rrdisk.Info["Path"].(string)
|
|
if len(path) == 0 {
|
|
continue
|
|
}
|
|
if len(bi.Info.Variant) > 0 && strings.Contains(strings.ToLower(bi.Info.Variant), identLower) {
|
|
matches = append(matches, path)
|
|
continue
|
|
}
|
|
}
|
|
matches = utils.Unique(matches)
|
|
switch len(matches) {
|
|
case 0:
|
|
return "", fmt.Errorf("no RestoreRamDisk DMG found for ident '%s': found %v", ident, strings.Join(variants, ", "))
|
|
case 1:
|
|
return matches[0], nil
|
|
default:
|
|
return "", fmt.Errorf("multiple RestoreRamDisk DMGs matched ident '%s': %v", ident, strings.Join(variants, ", "))
|
|
}
|
|
}
|
|
|
|
// GetRestoreRamDiskDmg returns the best RestoreRamDisk DMG path.
|
|
// If ident is non-empty, it finds a matching identity. Otherwise, it prefers
|
|
// Erase, then Update, then falls back to the first available DMG.
|
|
func (i *Info) GetRestoreRamDiskDmg(ident string) (string, error) {
|
|
if len(ident) > 0 {
|
|
return i.GetRestoreRamDiskDmgByIdent(ident)
|
|
}
|
|
// Prefer Erase -> Update -> first
|
|
if p, err := i.GetRestoreRamDiskDmgByIdent("Erase"); err == nil {
|
|
return p, nil
|
|
}
|
|
if p, err := i.GetRestoreRamDiskDmgByIdent("Update"); err == nil {
|
|
return p, nil
|
|
}
|
|
dmgs, err := i.GetRestoreRamDiskDmgs()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get RestoreRamDisk DMGs: %w", err)
|
|
}
|
|
if len(dmgs) == 0 {
|
|
return "", fmt.Errorf("no RestoreRamDisk DMG found")
|
|
}
|
|
return dmgs[0], nil
|
|
}
|
|
|
|
func (i *Info) GetExclaveOSDmg() (string, error) {
|
|
var dmgs []string
|
|
if i.Plists != nil && i.Plists.BuildManifest != nil {
|
|
for _, bi := range i.Plists.BuildIdentities {
|
|
if appOS, ok := bi.Manifest["Ap,ExclaveOS"]; ok {
|
|
dmgs = append(dmgs, appOS.Info["Path"].(string))
|
|
}
|
|
}
|
|
dmgs = utils.Unique(dmgs)
|
|
if len(dmgs) == 0 {
|
|
return "", fmt.Errorf("no ExclaveOS DMG found: %w", ErrorCryptexNotFound)
|
|
} else if len(dmgs) == 1 {
|
|
return dmgs[0], nil
|
|
} else {
|
|
return "", fmt.Errorf("multiple ExclaveOS DMGs found")
|
|
}
|
|
}
|
|
return "", fmt.Errorf("no BuildManifest.plist found")
|
|
}
|
|
|
|
func (i *Info) IsMacOS() bool {
|
|
for _, dev := range i.Plists.BuildManifest.SupportedProductTypes {
|
|
if strings.Contains(dev, "Mac") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetOsDmg returns the name of the OS dmg
|
|
func (i *Info) GetCPU(board string) string {
|
|
if i.Plists.Restore != nil {
|
|
for _, device := range i.Plists.Restore.DeviceMap {
|
|
if strings.EqualFold(device.BoardConfig, board) {
|
|
if proc, err := getProcessor(device.Platform); err == nil {
|
|
return fmt.Sprintf("CPU: %s (%s), ID: %s", proc.Name, proc.CPUISA, device.Platform)
|
|
} else {
|
|
return fmt.Sprintf("ID: %s", device.Platform)
|
|
}
|
|
}
|
|
}
|
|
} else if i.Plists.BuildManifest != nil {
|
|
for _, ident := range i.Plists.BuildIdentities {
|
|
if strings.EqualFold(ident.Info.DeviceClass, board) {
|
|
plat := "t" + strings.TrimPrefix(ident.ApChipID, "0x")
|
|
if proc, err := getProcessor(plat); err == nil {
|
|
return fmt.Sprintf("CPU: %s (%s), ID: %s", proc.Name, proc.CPUISA, plat)
|
|
} else {
|
|
return fmt.Sprintf("AP ChipID: %s", ident.ApChipID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetFolder returns a folder name for all the devices included in an IPSW
|
|
func (i *Info) GetFolder(device ...string) (string, error) {
|
|
if i.Plists.BuildManifest == nil {
|
|
if i.Plists.Type == "OTA" && i.Plists.OTAInfo.CFBundleName == "SimulatorRuntimeAsset" {
|
|
typ, found := strings.CutPrefix(i.Plists.OTAInfo.CFBundleIdentifier, "com.apple.MobileAsset.")
|
|
if !found {
|
|
typ = "Simulator"
|
|
}
|
|
return fmt.Sprintf("%s_%s_%s",
|
|
i.Plists.OTAInfo.MobileAssetProperties.SimulatorVersion,
|
|
i.Plists.OTAInfo.MobileAssetProperties.Build,
|
|
typ,
|
|
), nil
|
|
}
|
|
return "", fmt.Errorf("no BuildManifest.plist found")
|
|
}
|
|
|
|
var dev string
|
|
if len(device) > 0 && len(device[0]) > 0 {
|
|
dev = device[0]
|
|
}
|
|
|
|
var devs []string
|
|
if len(i.DeviceTrees) > 0 {
|
|
for _, dtree := range i.DeviceTrees {
|
|
dt, err := dtree.Summary()
|
|
if err != nil {
|
|
log.Fatal(err.Error())
|
|
}
|
|
devs = append(devs, dt.ProductType)
|
|
}
|
|
|
|
devs = utils.SortDevices(utils.Unique(devs))
|
|
|
|
if len(dev) > 0 {
|
|
if slices.Contains(devs, dev) {
|
|
return fmt.Sprintf("%s__%s", i.Plists.BuildManifest.ProductBuildVersion, dev), nil
|
|
} else {
|
|
return "", fmt.Errorf("device '%s' not found in IPSW/OTA", dev)
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf("%s__%s", i.Plists.BuildManifest.ProductBuildVersion, getAbbreviatedDevListFolder(devs)), nil
|
|
}
|
|
|
|
sort.Strings(i.Plists.BuildManifest.SupportedProductTypes)
|
|
return fmt.Sprintf("%s__%s", i.Plists.BuildManifest.ProductBuildVersion, getAbbreviatedDevListFolder(i.Plists.BuildManifest.SupportedProductTypes)), nil
|
|
}
|
|
|
|
// GetFolders returns a list of the IPSW name folders
|
|
func (i *Info) GetFolders() ([]string, error) {
|
|
if i.Plists.BuildManifest == nil {
|
|
return nil, fmt.Errorf("no BuildManifest.plist found")
|
|
}
|
|
var folders []string
|
|
if len(i.DeviceTrees) > 0 {
|
|
for _, dtree := range i.DeviceTrees {
|
|
dt, _ := dtree.Summary()
|
|
folders = append(folders, fmt.Sprintf("%s_%s_%s", dt.ProductType, strings.ToUpper(dt.BoardConfig), i.Plists.BuildManifest.ProductBuildVersion))
|
|
}
|
|
return folders, nil
|
|
}
|
|
return nil, fmt.Errorf("no devices found")
|
|
}
|
|
|
|
// GetFolderForFile returns a list of the IPSW name folders for a given file
|
|
func (i *Info) GetFolderForFile(fileName string) (string, error) {
|
|
if i.Plists.BuildManifest == nil {
|
|
return "", fmt.Errorf("no BuildManifest.plist found")
|
|
}
|
|
if len(i.DeviceTrees) > 0 {
|
|
files := i.getManifestPaths()
|
|
for _, dtree := range i.DeviceTrees {
|
|
dt, _ := dtree.Summary()
|
|
for _, file := range files[strings.ToLower(dt.BoardConfig)] {
|
|
if strings.Contains(fileName, filepath.Base(file)) {
|
|
return fmt.Sprintf("%s_%s_%s", dt.ProductType, strings.ToUpper(dt.BoardConfig), i.Plists.BuildManifest.ProductBuildVersion), nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return "", fmt.Errorf("no devices found")
|
|
}
|
|
|
|
func (i *Info) getManifestPaths() map[string][]string {
|
|
|
|
files := make(map[string][]string, len(i.Plists.BuildIdentities))
|
|
|
|
for _, bID := range i.Plists.BuildIdentities {
|
|
for _, manifest := range bID.Manifest {
|
|
if len(manifest.Info["Path"].(string)) > 0 {
|
|
files[bID.Info.DeviceClass] = append(files[bID.Info.DeviceClass], manifest.Info["Path"].(string))
|
|
}
|
|
}
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
type folder struct {
|
|
Name string
|
|
KernelCaches []string
|
|
}
|
|
|
|
func (i *Info) getFolders() ([]folder, error) {
|
|
if i.Plists.BuildManifest == nil {
|
|
return nil, fmt.Errorf("no BuildManifest.plist found")
|
|
}
|
|
if len(i.DeviceTrees) > 0 {
|
|
var fs []folder
|
|
kcs := i.Plists.BuildManifest.GetKernelCaches()
|
|
for _, dtree := range i.DeviceTrees {
|
|
dt, _ := dtree.Summary()
|
|
fs = append(fs, folder{
|
|
Name: fmt.Sprintf("%s_%s_%s", dt.ProductType, strings.ToUpper(dt.BoardConfig), i.Plists.BuildManifest.ProductBuildVersion),
|
|
KernelCaches: kcs[strings.ToLower(dt.BoardConfig)],
|
|
})
|
|
}
|
|
return fs, nil
|
|
}
|
|
return nil, fmt.Errorf("no devices found")
|
|
}
|
|
|
|
// GetKernelCacheFolders returns the folders belonging to a KernelCache
|
|
func (i *Info) GetKernelCacheFolders(kc string) ([]string, error) {
|
|
var folders []string
|
|
fds, err := i.getFolders()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get folders: %v", err)
|
|
}
|
|
for _, folder := range fds {
|
|
for _, kcache := range folder.KernelCaches {
|
|
if strings.HasSuffix(kc, kcache) {
|
|
folders = append(folders, folder.Name)
|
|
}
|
|
}
|
|
}
|
|
return folders, nil
|
|
}
|
|
|
|
// GetKernelCacheFileName returns a short new kernelcache name including all the supported devices
|
|
func (i *Info) GetKernelCacheFileName(kc string) string {
|
|
devices := i.GetDevicesForKernelCache(filepath.Base(kc))
|
|
if i.kernelCacheDeviceListCollides(filepath.Base(kc), devices) {
|
|
return filepath.Base(kc)
|
|
}
|
|
devList := getAbbreviatedDevList(devices)
|
|
if len(devList) == 0 {
|
|
return filepath.Base(kc)
|
|
}
|
|
return fmt.Sprintf("%s.%s", strings.TrimSuffix(filepath.Base(kc), filepath.Ext(kc)), devList)
|
|
}
|
|
|
|
// GetDevicesForKernelCache returns a sorted array of devices that support the kernelcache
|
|
func (i *Info) GetDevicesForKernelCache(kc string) []string {
|
|
var devices []string
|
|
if i.Plists == nil || i.Plists.BuildManifest == nil {
|
|
return nil
|
|
}
|
|
|
|
for bconf, kcache := range i.Plists.BuildManifest.GetKernelCaches() {
|
|
if utils.StrSliceHas(kcache, filepath.Base(kc)) {
|
|
var matchedDeviceTree bool
|
|
for _, dtree := range i.DeviceTrees {
|
|
if dtree == nil {
|
|
continue
|
|
}
|
|
dt, err := dtree.Summary()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if strings.EqualFold(bconf, dt.BoardConfig) {
|
|
matchedDeviceTree = true
|
|
devices = append(devices, dt.ProductType)
|
|
}
|
|
}
|
|
if !matchedDeviceTree {
|
|
devices = append(devices, i.getDevicesForBuildIdentityKernelCache(filepath.Base(kc), bconf)...)
|
|
}
|
|
}
|
|
}
|
|
|
|
return utils.SortDevices(utils.Unique(devices))
|
|
}
|
|
|
|
// GetKernelCacheForDevice returns the kernelcache path(s) for a given device ProductType (e.g., "Mac16,8", "iPhone17,1")
|
|
func (i *Info) GetKernelCacheForDevice(device string) []string {
|
|
if i.Plists == nil || i.Plists.BuildManifest == nil {
|
|
return nil
|
|
}
|
|
|
|
// Find the BoardConfig for this device
|
|
var boardConfig string
|
|
for _, dtree := range i.DeviceTrees {
|
|
if dtree == nil {
|
|
continue
|
|
}
|
|
dt, err := dtree.Summary()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if dt.ProductType == device {
|
|
boardConfig = strings.ToLower(dt.BoardConfig)
|
|
break
|
|
}
|
|
}
|
|
if boardConfig != "" {
|
|
// Get kernelcaches for this BoardConfig
|
|
kcs := i.Plists.BuildManifest.GetKernelCaches()
|
|
return kcs[boardConfig]
|
|
}
|
|
|
|
return i.getKernelCachesForDeviceFromBuildManifest(device)
|
|
}
|
|
|
|
func (i *Info) getKernelCachesForDeviceFromBuildManifest(device string) []string {
|
|
if i.Plists == nil || i.Plists.BuildManifest == nil {
|
|
return nil
|
|
}
|
|
|
|
var kcs []string
|
|
singleDeviceIPSW := len(i.Plists.BuildManifest.SupportedProductTypes) == 1 &&
|
|
strings.EqualFold(i.Plists.BuildManifest.SupportedProductTypes[0], device)
|
|
for _, ident := range i.Plists.BuildManifest.BuildIdentities {
|
|
if !singleDeviceIPSW && !strings.EqualFold(ident.ApProductType, device) {
|
|
continue
|
|
}
|
|
kernel, ok := ident.Manifest["KernelCache"]
|
|
if !ok {
|
|
continue
|
|
}
|
|
path, ok := getIdentityManifestPath(kernel)
|
|
if !ok {
|
|
continue
|
|
}
|
|
kcs = append(kcs, path)
|
|
}
|
|
|
|
return utils.Unique(kcs)
|
|
}
|
|
|
|
func (i *Info) getDevicesForBuildIdentityKernelCache(kc, deviceClass string) []string {
|
|
if i.Plists == nil || i.Plists.BuildManifest == nil {
|
|
return nil
|
|
}
|
|
|
|
var devices []string
|
|
for _, ident := range i.Plists.BuildManifest.BuildIdentities {
|
|
if !strings.EqualFold(ident.Info.DeviceClass, deviceClass) {
|
|
continue
|
|
}
|
|
kernel, ok := ident.Manifest["KernelCache"]
|
|
if !ok {
|
|
continue
|
|
}
|
|
path, ok := getIdentityManifestPath(kernel)
|
|
if !ok || filepath.Base(path) != filepath.Base(kc) {
|
|
continue
|
|
}
|
|
if len(ident.ApProductType) > 0 {
|
|
devices = append(devices, ident.ApProductType)
|
|
} else if len(i.Plists.BuildManifest.SupportedProductTypes) == 1 {
|
|
devices = append(devices, i.Plists.BuildManifest.SupportedProductTypes[0])
|
|
}
|
|
}
|
|
|
|
return devices
|
|
}
|
|
|
|
func (i *Info) kernelCacheDeviceListCollides(kc string, devices []string) bool {
|
|
if i.Plists == nil || i.Plists.BuildManifest == nil || len(devices) == 0 {
|
|
return false
|
|
}
|
|
|
|
seen := make(map[string]struct{})
|
|
for _, kcaches := range i.Plists.BuildManifest.GetKernelCaches() {
|
|
for _, otherKC := range kcaches {
|
|
otherBase := filepath.Base(otherKC)
|
|
if otherBase == filepath.Base(kc) {
|
|
continue
|
|
}
|
|
if _, ok := seen[otherBase]; ok {
|
|
continue
|
|
}
|
|
seen[otherBase] = struct{}{}
|
|
if slices.Equal(devices, i.GetDevicesForKernelCache(otherBase)) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func getIdentityManifestPath(manifest plist.IdentityManifest) (string, bool) {
|
|
if manifest.Info == nil {
|
|
return "", false
|
|
}
|
|
path, ok := manifest.Info["Path"].(string)
|
|
if !ok || len(path) == 0 {
|
|
return "", false
|
|
}
|
|
return path, true
|
|
}
|
|
|
|
func getAbbreviatedDevList(devices []string) string {
|
|
var devList strings.Builder
|
|
|
|
if len(devices) == 0 {
|
|
return ""
|
|
} else if len(devices) == 1 {
|
|
return devices[0]
|
|
}
|
|
|
|
currentDev := devices[0]
|
|
devPrefix := strings.Split(currentDev, ",")[0]
|
|
devList.WriteString(currentDev)
|
|
|
|
for _, dev := range devices[1:] {
|
|
if strings.HasPrefix(dev, devPrefix) {
|
|
devList.WriteString(fmt.Sprintf("_%s", strings.Split(dev, ",")[1]))
|
|
} else {
|
|
currentDev = dev
|
|
devPrefix = strings.Split(currentDev, ",")[0]
|
|
devList.WriteString("_" + currentDev)
|
|
}
|
|
}
|
|
|
|
return devList.String()
|
|
}
|
|
|
|
func getAbbreviatedDevListFolder(devices []string) string {
|
|
var devList strings.Builder
|
|
|
|
if len(devices) == 0 {
|
|
return ""
|
|
} else if len(devices) == 1 {
|
|
return devices[0]
|
|
} else if strings.Contains(devices[0], "Mac") {
|
|
return "MacOS"
|
|
}
|
|
|
|
currentDev := devices[0]
|
|
devPrefix := strings.Split(currentDev, ",")[0]
|
|
devList.WriteString(currentDev)
|
|
|
|
for _, dev := range devices[1:] {
|
|
if strings.HasPrefix(dev, devPrefix) {
|
|
devList.WriteString(fmt.Sprintf("_%s", strings.Split(dev, ",")[1]))
|
|
} else {
|
|
currentDev = dev
|
|
devPrefix = strings.Split(currentDev, ",")[0]
|
|
devList.WriteString("_" + currentDev)
|
|
}
|
|
}
|
|
|
|
return devList.String()
|
|
}
|
|
|
|
// Parse parses plist files in a local ipsw file
|
|
func Parse(ipswPath string, keys ...string) (*Info, error) {
|
|
var err error
|
|
|
|
i := &Info{}
|
|
|
|
i.Plists, err = plist.Parse(ipswPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse plists: %v", err)
|
|
}
|
|
i.DeviceTrees, err = devicetree.Parse(ipswPath, keys...)
|
|
if err != nil {
|
|
if errors.Is(err, devicetree.ErrEncryptedDeviceTree) { // FIXME: this is a hack to avoid stopping the parsing of the metadata info
|
|
log.Debug(err.Error())
|
|
} else {
|
|
log.Errorf("failed to parse devicetree: %v", err)
|
|
}
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
// ParseZipFiles parses plist files and devicetree in a remote zip file
|
|
func ParseZipFiles(files []*zip.File, keys ...string) (*Info, error) {
|
|
var err error
|
|
|
|
i := &Info{}
|
|
|
|
i.Plists, err = plist.ParseZipFiles(files)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse plists: %v", err)
|
|
}
|
|
i.DeviceTrees, err = devicetree.ParseZipFiles(files, keys...)
|
|
if err != nil {
|
|
if errors.Is(err, devicetree.ErrEncryptedDeviceTree) { // FIXME: this is a hack to avoid stopping the parsing of the metadata info
|
|
log.Debug(err.Error())
|
|
} else {
|
|
log.Errorf("failed to parse devicetree: %v", err)
|
|
}
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
func ParseOTAFiles(files []fs.File) (*Info, error) {
|
|
var err error
|
|
|
|
i := &Info{}
|
|
|
|
i.DeviceTrees = make(map[string]*devicetree.DeviceTree)
|
|
|
|
i.Plists, err = plist.ParsePlistFiles(files)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse plists: %v", err)
|
|
}
|
|
for _, f := range files {
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get file info: %v", err)
|
|
}
|
|
if filepath.Ext(fi.Name()) == ".im4p" {
|
|
dat, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read file: %v", err)
|
|
}
|
|
dt, err := devicetree.ParseImg4Data(dat)
|
|
if err != nil {
|
|
if errors.Is(err, devicetree.ErrEncryptedDeviceTree) { // FIXME: this is a hack to avoid stopping the parsing of the metadata info
|
|
log.Debug(err.Error())
|
|
} else {
|
|
log.Errorf("failed to parse devicetree: %v", err)
|
|
}
|
|
}
|
|
i.DeviceTrees[fi.Name()] = dt
|
|
}
|
|
}
|
|
|
|
return i, nil
|
|
}
|