Files
ipsw/pkg/info/info.go
blacktop 4f23763438 feat(extract): auto-decrypt remote kernelcaches via wiki keys
`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
2026-04-27 15:57:23 -06:00

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
}