From 6a044aafcb339de709e73e4fa06ffdb48191ca9a Mon Sep 17 00:00:00 2001 From: blacktop Date: Tue, 13 May 2025 20:52:23 -0600 Subject: [PATCH] feat: add `--sim` flag to download XCode simulators via `ipsw dl ota` cmd #700 --- cmd/ipsw/cmd/download/download_ota.go | 29 +++++++++++++--- internal/download/ota.go | 50 +++++++++++++++++++++++++++ internal/download/xcode.go | 23 ++++++++++++ pkg/ota/types/asset.go | 2 ++ 4 files changed, 99 insertions(+), 5 deletions(-) diff --git a/cmd/ipsw/cmd/download/download_ota.go b/cmd/ipsw/cmd/download/download_ota.go index ad9dc197f..eec55a640 100644 --- a/cmd/ipsw/cmd/download/download_ota.go +++ b/cmd/ipsw/cmd/download/download_ota.go @@ -65,6 +65,7 @@ func init() { otaDLCmd.Flags().Bool("latest", false, "Download latest OTAs") otaDLCmd.Flags().Bool("delta", false, "Download Delta OTAs") otaDLCmd.Flags().Bool("rsr", false, "Download Rapid Security Response OTAs") + otaDLCmd.Flags().Bool("sim", false, "Download Simulator OTAs") otaDLCmd.Flags().BoolP("kernel", "k", false, "Extract kernelcache from remote OTA zip") otaDLCmd.Flags().Bool("dyld", false, "Extract dyld_shared_cache(s) from remote OTA zip") otaDLCmd.Flags().BoolP("urls", "u", false, "Dump URLs only") @@ -87,6 +88,7 @@ func init() { viper.BindPFlag("download.ota.latest", otaDLCmd.Flags().Lookup("latest")) viper.BindPFlag("download.ota.delta", otaDLCmd.Flags().Lookup("delta")) viper.BindPFlag("download.ota.rsr", otaDLCmd.Flags().Lookup("rsr")) + viper.BindPFlag("download.ota.sim", otaDLCmd.Flags().Lookup("sim")) viper.BindPFlag("download.ota.dyld", otaDLCmd.Flags().Lookup("dyld")) viper.BindPFlag("download.ota.urls", otaDLCmd.Flags().Lookup("urls")) viper.BindPFlag("download.ota.json", otaDLCmd.Flags().Lookup("json")) @@ -159,6 +161,7 @@ var otaDLCmd = &cobra.Command{ getBeta := viper.GetBool("download.ota.beta") getLatest := viper.GetBool("download.ota.latest") getRSR := viper.GetBool("download.ota.rsr") + getSim := viper.GetBool("download.ota.sim") remoteDyld := viper.GetBool("download.ota.dyld") dyldArches := viper.GetStringSlice("download.ota.dyld-arch") dyldDriverKit := viper.GetBool("download.ota.driver-kit") @@ -269,6 +272,7 @@ var otaDLCmd = &cobra.Command{ Latest: getLatest, Delta: viper.GetBool("download.ota.delta"), RSR: getRSR, + Simulator: getSim, Device: device, Model: model, Version: ver, @@ -322,7 +326,7 @@ var otaDLCmd = &cobra.Command{ for _, o := range otas { utils.Indent(log.WithFields(log.Fields{ "name": o.DocumentationID, - "version": o.OSVersion, + "version": or([]string{o.OSVersion, o.SimulatorVersion}), "build": o.Build, "device_count": len(o.SupportedDevices), "model_count": len(o.SupportedDeviceModels), @@ -357,7 +361,7 @@ var otaDLCmd = &cobra.Command{ "devices": fmt.Sprintf("%s... (count=%d)", strings.Join(o.SupportedDevices, " "), len(o.SupportedDevices)), "model": strings.Join(o.SupportedDeviceModels, " "), } - if o.IsEncrypted { + if o.IsEncrypted || len(o.ArchiveDecryptionKey) > 0 { fields["encrypted"] = true fields["key"] = o.ArchiveDecryptionKey } @@ -422,6 +426,9 @@ var otaDLCmd = &cobra.Command{ downloader := download.NewDownload(proxy, insecure, skipAll, resumeAll, restartAll, false, viper.GetBool("verbose")) for _, o := range otas { folder := filepath.Join(destPath, fmt.Sprintf("%s%s_OTAs", o.ProductSystemName, strings.TrimPrefix(o.OSVersion, "9.9."))) + if getSim { + folder = filepath.Join(destPath, fmt.Sprintf("%s_%s_Simulator_OTAs", strings.ToUpper(platform), o.SimulatorVersion)) + } os.MkdirAll(folder, 0750) var devices string if len(o.SupportedDevices) > 0 { @@ -445,21 +452,24 @@ var otaDLCmd = &cobra.Command{ isRSR = fmt.Sprintf("%s_%s_%s_RSR_", o.OSVersion, o.ProductVersionExtra, o.Build) } var isAEA string - if o.IsEncrypted { + if o.IsEncrypted || len(o.ArchiveDecryptionKey) > 0 { filesafe := o.ArchiveDecryptionKey filesafe = strings.ReplaceAll(filesafe, "/", "_") filesafe = strings.ReplaceAll(filesafe, "+", "-") isAEA = "KEY_[" + filesafe + "]_" } destName := filepath.Join(folder, fmt.Sprintf("%s_%s%s%s", devices, isRSR, isAEA, getDestName(url, removeCommas))) + if getSim { + destName = filepath.Join(folder, fmt.Sprintf("simulator_%s%s", isAEA, getDestName(url, removeCommas))) + } if _, err := os.Stat(destName); os.IsNotExist(err) { fields := log.Fields{ "device": strings.Join(o.SupportedDevices, " "), "model": strings.Join(o.SupportedDeviceModels, " "), "build": o.Build, - "type": o.DocumentationID, + "type": or([]string{o.DocumentationID, "simulator"}), } - if o.IsEncrypted { + if o.IsEncrypted || len(o.ArchiveDecryptionKey) > 0 { fields["encrypted"] = true fields["key"] = o.ArchiveDecryptionKey } @@ -482,3 +492,12 @@ var otaDLCmd = &cobra.Command{ return nil }, } + +func or(values []string) string { + for _, v := range values { + if len(v) > 0 { + return v + } + } + return "" +} diff --git a/internal/download/ota.go b/internal/download/ota.go index 20a741397..2fec53f14 100644 --- a/internal/download/ota.go +++ b/internal/download/ota.go @@ -88,6 +88,7 @@ type OtaConf struct { Latest bool Delta bool RSR bool + Simulator bool Device string Model string Version *version.Version @@ -110,6 +111,7 @@ type pallasRequest struct { BuildVersion string `json:"BuildVersion"` Build string `json:"Build,omitempty"` RequestedProductVersion string `json:"RequestedProductVersion,omitempty"` + RequestedBuild string `json:"RequestedBuild,omitempty"` Supervised bool `json:"Supervised,omitempty"` DelayRequested bool `json:"DelayRequested,omitempty"` CompatibilityVersion int `json:"CompatibilityVersion,omitempty"` @@ -305,6 +307,20 @@ func (o *Ota) getRequestAssetTypes() ([]assetType, error) { if o.Config.RSR { return []assetType{rsrUpdate}, nil } + if o.Config.Simulator { + switch o.Config.Platform { + case "ios": + return []assetType{iOsSimulatorUpdate}, nil + case "watchos": + return []assetType{watchOsSimulatorUpdate}, nil + case "tvos": + return []assetType{tvOsSimulatorUpdate}, nil + case "visionos": + return []assetType{visionOaSimulatorUpdate}, nil + default: + return nil, fmt.Errorf("unsupported simulator platform %s", o.Config.Platform) + } + } if o.Config.Platform == "ios" { return []assetType{softwareUpdate}, nil } @@ -381,6 +397,9 @@ func (o *Ota) getRequestAudienceIDs() ([]string, error) { }, nil } default: + if o.Config.Simulator { + return []string{assetAudienceDB["macos"].Generic}, nil + } if o.Config.Version != nil { segs := o.Config.Version.Segments() if len(segs) == 0 { @@ -464,6 +483,10 @@ func (o *Ota) getRequests(atype assetType, audienceID string) (reqs []pallasRequ req.Build = o.Config.Build } + if o.Config.Simulator { + req.RequestedBuild = o.Config.Build + } + if len(o.Config.Device) > 0 && len(o.Config.Model) == 0 { dev, err := o.db.LookupDevice(o.Config.Device) if err != nil { @@ -561,6 +584,21 @@ func (o *Ota) GetPallasOTAs() ([]types.Asset, error) { oassets := o.QueryPublicXML() + if o.Config.Simulator { + if o.Config.Version.Original() == "0" && o.Config.Build == "0" { + return nil, fmt.Errorf("you must supply: --build, --version or --latest WITH --sim") + } else if o.Config.Version.Original() != "0" && o.Config.Build == "0" { + dvt, err := GetDVTDownloadableIndex() + if err != nil { + return nil, fmt.Errorf("failed to get simulators index: %v", err) + } + o.Config.Build, err = dvt.LookupBuild(o.Config.Version.Original(), o.Config.Platform) + if err != nil { + return nil, fmt.Errorf("failed to lookup simulator build: %v", err) + } + } + } + pallasReqs, err := o.buildPallasRequests() if err != nil { return nil, fmt.Errorf("failed to build the pallas requests: %v", err) @@ -618,6 +656,8 @@ func (o *Ota) GetPallasOTAs() ([]types.Asset, error) { continue } + // os.WriteFile("pallas.json", b64data, 0644) + res := ota{} if err := json.Unmarshal(b64data, &res); err != nil { log.Errorf("failed to unmarshall JSON: %v", err) @@ -699,6 +739,16 @@ func (o *Ota) filterOTADevices(otas []types.Asset) []types.Asset { // FIXME: thi var filteredDevices []string var filteredOtas []types.Asset + if o.Config.Simulator { + for _, ota := range otas { + switch assetType(ota.AssetType) { + case iOsSimulatorUpdate, watchOsSimulatorUpdate, tvOsSimulatorUpdate, visionOaSimulatorUpdate: + filteredOtas = append(filteredOtas, ota) + } + } + return filteredOtas + } + if o.Config.Platform == "macos" { if o.Config.Build != "0" && !o.Config.RSR { for _, ota := range otas { diff --git a/internal/download/xcode.go b/internal/download/xcode.go index c1cfbaa3b..0ade79233 100644 --- a/internal/download/xcode.go +++ b/internal/download/xcode.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/apex/log" "github.com/blacktop/go-plist" "github.com/blacktop/ipsw/internal/utils" ) @@ -24,6 +25,14 @@ const ( xcodeReleasesAPI = "https://xcodereleases.com/data.json" ) +var platforms = map[string]string{ + "macos": "com.apple.platform.macosx", + "ios": "com.apple.platform.iphoneos", + "tvos": "com.apple.platform.appletvos", + "watchos": "com.apple.platform.watchos", + "visionos": "com.apple.platform.xros", +} + type Downloadable struct { Authentication string `plist:"authentication,omitempty"` Category string `plist:"category,omitempty"` @@ -88,6 +97,20 @@ func GetDVTDownloadableIndex() (*DVTDownloadable, error) { return &dvt, nil } +func (d *DVTDownloadable) LookupBuild(version, platform string) (string, error) { + platform, ok := platforms[platform] + if !ok { + return "", fmt.Errorf("platform not supported: %s", platform) + } + for _, dl := range d.Downloadables { + if dl.SimulatorVersion.Version == version && dl.Platform == platform { + log.WithField("name", dl.Name).Debug("Simulator") + return dl.SimulatorVersion.BuildUpdate, nil + } + } + return "", fmt.Errorf("build not found for: %s", version) +} + type Contents struct { Key string Generation int64 diff --git a/pkg/ota/types/asset.go b/pkg/ota/types/asset.go index 33ba8d616..bc7d02899 100644 --- a/pkg/ota/types/asset.go +++ b/pkg/ota/types/asset.go @@ -46,6 +46,7 @@ type Asset struct { AssetType string `json:"AssetType" plist:"AssetType,omitempty"` BridgeVersionInfo bridgeVersionInfo `json:"BridgeVersionInfo" plist:"BridgeVersionInfo,omitempty"` Build string `json:"Build" plist:"Build,omitempty"` + SimulatorVersion string `json:"SimulatorVersion" plist:"SimulatorVersion,omitempty"` DataTemplateSize int `json:"DataTemplateSize" plist:"DataTemplateSize,omitempty"` EAPFSEnabled bool `json:"EAPFSEnabled,omitempty" plist:"EAPFSEnabled,omitempty"` InstallationSize string `json:"InstallationSize" plist:"InstallationSize,omitempty"` @@ -83,6 +84,7 @@ type Asset struct { IsZipStreamable bool `json:"_IsZipStreamable" plist:"_IsZipStreamable,omitempty"` MasteredVersion string `json:"_MasteredVersion" plist:"_MasteredVersion,omitempty"` Hash []byte `json:"_Measurement" plist:"_Measurement,omitempty"` + Sha256Hash []byte `json:"_Measurement-SHA256" plist:"_Measurement-SHA256,omitempty"` HashAlgorithm string `json:"_MeasurementAlgorithm" plist:"_MeasurementAlgorithm,omitempty"` UnarchivedSize int `json:"_UnarchivedSize" plist:"_UnarchivedSize,omitempty"` AssetDefaultGarbageCollectionBehavior string `json:"__AssetDefaultGarbageCollectionBehavior" plist:"__AssetDefaultGarbageCollectionBehavior,omitempty"`