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

369 lines
13 KiB
Go

package plist
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/fs"
"regexp"
"strings"
"github.com/apex/log"
"github.com/blacktop/go-plist"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/ota/types"
"github.com/dustin/go-humanize"
"github.com/pkg/errors"
)
// Plists IPSW/OTA plists object
type Plists struct {
Type string `json:"type,omitempty"`
*BuildManifest `json:"build_manifest,omitempty"`
*Restore `json:"restore,omitempty"`
*AssetDataInfo `json:"asset_data_info,omitempty"`
*OTAInfo `json:"ota_info,omitempty"`
*SystemVersion `json:"system_version,omitempty"`
}
// AssetDataInfo AssetData/Info.plist object found in OTAs
type AssetDataInfo struct {
ActualMinimumSystemPartition int `plist:"ActualMinimumSystemPartition,omitempty" json:"actual_minimum_system_partition,omitempty"`
Build string `plist:"Build,omitempty" json:"build,omitempty"`
DeviceClass string `plist:"DeviceClass,omitempty" json:"device_class,omitempty"`
HardwareModel string `plist:"HardwareModel,omitempty" json:"hardware_model,omitempty"`
MinimumSystemPartition int `plist:"MinimumSystemPartition,omitempty" json:"minimum_system_partition,omitempty"`
PackageVersion string `plist:"PackageVersion,omitempty" json:"package_version,omitempty"`
ProductType string `plist:"ProductType,omitempty" json:"product_type,omitempty"`
ProductVersion string `plist:"ProductVersion,omitempty" json:"product_version,omitempty"`
RSEPDigest []byte `plist:"RSEPDigest,omitempty" json:"rsep_digest,omitempty"`
RSEPTBMDigests []byte `plist:"RSEPTBMDigests,omitempty" json:"rseptbm_digests,omitempty"`
RequiredSpace int `plist:"RequiredSpace,omitempty" json:"required_space,omitempty"`
ReserveFileAware bool `plist:"ReserveFileAware,omitempty" json:"reserve_file_aware,omitempty"`
SEPDigest []byte `plist:"SEPDigest,omitempty" json:"sep_digest,omitempty"`
SEPTBMDigests []byte `plist:"SEPTBMDigests,omitempty" json:"septbm_digests,omitempty"`
SizeArchiveRoot int `plist:"SizeArchiveRoot,omitempty" json:"size_archive_root,omitempty"`
SizePatchedBinaries int `plist:"SizePatchedBinaries,omitempty" json:"size_patched_binaries,omitempty"`
SizePatchedBinariesSnapshot int `plist:"SizePatchedBinaries-Snapshot,omitempty" json:"size_patched_binaries_snapshot,omitempty"`
SystemPartitionPadding map[string]int `plist:"SystemPartitionPadding,omitempty" json:"system_partition_padding,omitempty"`
SystemUpdatePathMap map[string]string `plist:"SystemUpdatePathMap,omitempty" json:"system_update_path_map,omitempty"`
SystemVolumeSealingOverhead int `plist:"SystemVolumeSealingOverhead,omitempty" json:"system_volume_sealing_overhead,omitempty"`
TargetUpdate string `plist:"TargetUpdate,omitempty" json:"target_update,omitempty"`
}
// AssetDataInfo Stringer
func (a *AssetDataInfo) String() string {
var out strings.Builder
out.WriteString("[AssetData/Info.plist]\n")
out.WriteString("======================\n")
out.WriteString(fmt.Sprintf("Build: %s\n", a.Build))
out.WriteString(fmt.Sprintf("DeviceClass: %s\n", a.DeviceClass))
out.WriteString(fmt.Sprintf("HardwareModel: %s\n", a.HardwareModel))
out.WriteString(fmt.Sprintf("MinimumSystemPartition: %d\n", a.MinimumSystemPartition))
out.WriteString(fmt.Sprintf("PackageVersion: %s\n", a.PackageVersion))
out.WriteString(fmt.Sprintf("ProductType: %s\n", a.ProductType))
out.WriteString(fmt.Sprintf("ProductVersion: %s\n", a.ProductVersion))
out.WriteString(fmt.Sprintf("RequiredSpace: %s\n", humanize.Bytes(uint64(a.RequiredSpace))))
out.WriteString(fmt.Sprintf("ReserveFileAware: %v\n", a.ReserveFileAware))
out.WriteString(fmt.Sprintf("SizeArchiveRoot: %s\n", humanize.Bytes(uint64(a.SizeArchiveRoot))))
out.WriteString(fmt.Sprintf("SizePatchedBinaries: %s\n", humanize.Bytes(uint64(a.SizePatchedBinaries))))
out.WriteString(fmt.Sprintf("SizePatchedBinaries-Snapshot: %s\n", humanize.Bytes(uint64(a.SizePatchedBinariesSnapshot))))
if len(a.SystemUpdatePathMap) > 0 {
out.WriteString("SystemUpdatePathMap:\n")
for k, v := range a.SystemUpdatePathMap {
out.WriteString(fmt.Sprintf(" - %s: %s\n", k, v))
}
}
out.WriteString(fmt.Sprintf("SystemVolumeSealingOverhead: %d\n", a.SystemVolumeSealingOverhead))
out.WriteString(fmt.Sprintf("TargetUpdate: %s\n", a.TargetUpdate))
return out.String()
}
// OTAInfo Info.plist object found in OTAs
type OTAInfo struct {
CFBundleIdentifier string `plist:"CFBundleIdentifier,omitempty" json:"cf_bundle_identifier,omitempty"`
CFBundleInfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion,omitempty" json:"cf_bundle_info_dictionary_version,omitempty"`
CFBundleName string `plist:"CFBundleName,omitempty" json:"cf_bundle_name,omitempty"`
CFBundleShortVersionString string `plist:"CFBundleShortVersionString,omitempty" json:"cf_bundle_short_version_string,omitempty"`
CFBundleVersion string `plist:"CFBundleVersion,omitempty" json:"cf_bundle_version,omitempty"`
MobileAssetProperties types.Asset `plist:"MobileAssetProperties,omitempty" json:"mobile_asset_properties"`
}
// OTAInfo Stringer
func (o *OTAInfo) String() string {
var out string
out += "[OTA Info.plist]\n"
out += "================\n"
out += fmt.Sprintf("CFBundleIdentifier: %s\n", o.CFBundleIdentifier)
// out += fmt.Sprintf("CFBundleInfoDictionaryVersion: %s\n", o.CFBundleInfoDictionaryVersion)
// out += fmt.Sprintf("CFBundleName: %s\n", o.CFBundleName)
// out += fmt.Sprintf("CFBundleShortVersionString: %s\n", o.CFBundleShortVersionString)
// out += fmt.Sprintf("CFBundleVersion: %s\n", o.CFBundleVersion)
out += fmt.Sprintf("MobileAssetProperties:\n %s\n", o.MobileAssetProperties)
return out
}
func readZipFile(f *zip.File) ([]byte, error) {
pData := make([]byte, f.UncompressedSize64)
rc, err := f.Open()
if err != nil {
return nil, fmt.Errorf("failed to open file within zip: %s", err)
}
defer rc.Close()
io.ReadFull(rc, pData)
return pData, nil
}
// Extract extracts plists from IPSW
func Extract(ipsw string) error {
log.Info("Extracting plists from IPSW")
_, err := utils.Unzip(ipsw, "", func(f *zip.File) bool {
var validPlist = regexp.MustCompile(`.*plist$`)
return validPlist.MatchString(f.Name)
})
if err != nil {
return errors.Wrap(err, "failed to extract plists from ipsw")
}
return nil
}
// ParseOTAInfo parses the Info.plist file from an OTA
func ParseOTAInfo(data []byte) (*OTAInfo, error) {
i := &OTAInfo{}
if err := plist.NewDecoder(bytes.NewReader(data)).Decode(i); err != nil {
return nil, fmt.Errorf("failed to decode OTA Info.plist: %w", err)
}
return i, nil
}
// ParseBuildManifest parses the AssetData/Info.plist file from an OTA
func ParseAssetDataInfoPlist(data []byte) (*AssetDataInfo, error) {
i := &AssetDataInfo{}
if err := plist.NewDecoder(bytes.NewReader(data)).Decode(i); err != nil {
return nil, fmt.Errorf("failed to parse AssetData/Info.plist: %w", err)
}
return i, nil
}
// Parse parses plist files in a local IPSW/OTA file
func Parse(path string) (*Plists, error) {
zr, err := zip.OpenReader(path)
if err != nil {
return nil, fmt.Errorf("failed to open IPSW zip: %s", err)
}
defer zr.Close()
return ParseZipFiles(zr.File)
}
// ParseZipFiles parses plists in remote ipsw zip
func ParseZipFiles(files []*zip.File) (*Plists, error) {
ipsw := &Plists{Type: "IPSW"}
for _, f := range files {
if regexp.MustCompile(`.*plist$`).MatchString(f.Name) {
switch {
case strings.HasSuffix(f.Name, "Restore.plist"):
dat, err := readZipFile(f)
if err != nil {
return nil, fmt.Errorf("failed to read plist file: %s", err)
}
ipsw.Restore, err = ParseRestore(dat)
if err != nil {
return nil, err
}
case strings.HasSuffix(f.Name, "BuildManifest.plist"):
if strings.Contains(f.Name, "Restore") {
continue
}
dat, err := readZipFile(f)
if err != nil {
return nil, fmt.Errorf("failed to read plist file: %s", err)
}
ipsw.BuildManifest, err = ParseBuildManifest(dat)
if err != nil {
return nil, err
}
case strings.HasSuffix(f.Name, "AssetData/Info.plist"):
ipsw.Type = "OTA"
dat, err := readZipFile(f)
if err != nil {
return nil, fmt.Errorf("failed to read plist file: %s", err)
}
ipsw.AssetDataInfo, err = ParseAssetDataInfoPlist(dat)
if err != nil {
return nil, err
}
case strings.EqualFold(f.Name, "Info.plist"):
ipsw.Type = "OTA"
dat, err := readZipFile(f)
if err != nil {
return nil, fmt.Errorf("failed to read plist file: %s", err)
}
ipsw.OTAInfo, err = ParseOTAInfo(dat)
if err != nil {
return nil, err
}
case strings.HasSuffix(f.Name, "SystemVersion.plist"):
dat, err := readZipFile(f)
if err != nil {
return nil, fmt.Errorf("failed to read plist file: %s", err)
}
ipsw.SystemVersion, err = ParseSystemVersion(dat)
if err != nil {
return nil, err
}
}
}
}
return ipsw, nil
}
func ParsePlistFiles(files []fs.File) (*Plists, error) {
ipsw := &Plists{Type: "IPSW"}
for _, f := range files {
fi, err := f.Stat()
if err != nil {
return nil, fmt.Errorf("failed to get file info: %s", err)
}
switch {
case strings.HasSuffix(fi.Name(), "Restore.plist"):
dat, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read plist file: %s", err)
}
ipsw.Restore, err = ParseRestore(dat)
if err != nil {
return nil, err
}
case strings.HasSuffix(fi.Name(), "BuildManifest.plist"):
if strings.Contains(fi.Name(), "Restore") {
continue
}
dat, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read plist file: %s", err)
}
ipsw.BuildManifest, err = ParseBuildManifest(dat)
if err != nil {
return nil, err
}
case strings.HasSuffix(fi.Name(), "AssetData/Info.plist"):
ipsw.Type = "OTA"
dat, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read plist file: %s", err)
}
ipsw.AssetDataInfo, err = ParseAssetDataInfoPlist(dat)
if err != nil {
return nil, err
}
case strings.EqualFold(fi.Name(), "Info.plist"):
ipsw.Type = "OTA"
dat, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read plist file: %s", err)
}
ipsw.OTAInfo, err = ParseOTAInfo(dat)
if err != nil {
return nil, err
}
case strings.HasSuffix(fi.Name(), "SystemVersion.plist"):
dat, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read plist file: %s", err)
}
ipsw.SystemVersion, err = ParseSystemVersion(dat)
if err != nil {
return nil, err
}
}
}
return ipsw, nil
}
func (p *Plists) GetOSType() string {
if p != nil && p.OTAInfo != nil {
if len(p.OTAInfo.MobileAssetProperties.ReleaseType) > 0 {
return p.OTAInfo.MobileAssetProperties.ReleaseType
} else {
return p.OTAInfo.MobileAssetProperties.DocumentationID
}
} else if os, ok := p.BuildManifest.BuildIdentities[0].Info.VariantContents["OS"]; ok {
return os
}
return "?"
}
func (p *Plists) GetKernelType(name string) string {
for _, bID := range p.BuildManifest.BuildIdentities {
if strings.EqualFold(bID.Manifest["KernelCache"].Info["Path"].(string), name) {
return bID.Info.VariantContents["InstalledKernelCache"]
}
}
return p.OTAInfo.MobileAssetProperties.ReleaseType
}
func (p *Plists) GetDeviceForBoardConfig(boardConfig string) *restoreDeviceMap {
if p != nil && p.Restore != nil {
for _, dmap := range p.Restore.DeviceMap {
if strings.EqualFold(dmap.BoardConfig, boardConfig) {
return &dmap
}
}
}
return nil
}
func (i *Plists) String() string {
var iStr strings.Builder
iStr.WriteString(fmt.Sprintf(
"[Plists Info]\n"+
"===========\n"+
"Version = %s\n"+
"BuildVersion = %s\n"+
"OS Type = %s\n",
i.BuildManifest.ProductVersion,
i.BuildManifest.ProductBuildVersion,
i.GetOSType(),
))
iStr.WriteString("FileSystem = ")
for file, fsType := range i.Restore.SystemRestoreImageFileSystems {
iStr.WriteString(fmt.Sprintf("%s (Type: %s)\n", file, fsType))
}
iStr.WriteString("\nSupported Products:\n")
for _, prodType := range i.BuildManifest.SupportedProductTypes {
iStr.WriteString(fmt.Sprintf(" - %s\n", prodType))
}
iStr.WriteString("\nDeviceMap:\n")
for _, device := range i.Restore.DeviceMap {
iStr.WriteString(fmt.Sprintf(
"BDID %d)\n"+
" - BoardConfig = %s\n"+
" - CPID = %d\n"+
" - Platform = %s\n"+
" - SCEP = %d\n"+
" - SDOM = %d\n",
device.BDID,
device.BoardConfig,
device.CPID,
device.Platform,
device.SCEP,
device.SDOM,
))
}
iStr.WriteString("\nKernelCaches:\n")
kcs := i.BuildManifest.GetKernelCaches()
for key, value := range kcs {
iStr.WriteString(fmt.Sprintf(" - BoardConfig: %s => %s\n", key, value))
}
return iStr.String()
}