Files
ipsw/internal/commands/mount/mount.go
blacktop e209828c31 feat(diff): add cacheable task-based reports
Refactor diff jobs into task-owned renderers with persistent cache support, root-confined walkers, and quieter Mach-O/Firmware output.
2026-06-04 12:02:46 -06:00

220 lines
6.5 KiB
Go

// Package mount provides the mount command
package mount
import (
"archive/zip"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/apex/log"
"github.com/blacktop/go-apfs/pkg/disk/dmg"
"github.com/blacktop/ipsw/internal/download"
"github.com/blacktop/ipsw/internal/magic"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/aea"
"github.com/blacktop/ipsw/pkg/img4"
"github.com/blacktop/ipsw/pkg/info"
)
var DmgTypes = []string{"app", "sys", "fs", "exc", "rdisk"}
// Config contains optional options for mounting a DMG from an IPSW
type Config struct {
PemDB string // AEA PEM DB JSON file path (for .aea decryption)
Keys any // Either string (DMG key) or download.WikiFWKeys (auto-lookup)
MountPoint string // Custom mount point
Ident string // BuildManifest identity selector (used for rdisk)
// ExtractDir is where DMGs are extracted/decrypted (default: os.TempDir()).
// Callers that also mount the same volumes via internal/search.scanDmg (which
// extracts to the cwd) set this to the cwd so both share one backing file and
// the same volume is never attached twice.
ExtractDir string
}
// Context is the mount context
type Context struct {
MountPoint string `json:"mount_point" binding:"required"`
DmgPath string `json:"dmg_path,omitempty"` // FIXME: required on linux
AlreadyMounted bool `json:"already_mounted,omitempty"`
}
// Unmount will unmount a DMG and remove the DMG source file
func (c Context) Unmount() error {
if info, err := utils.MountInfo(); err == nil { // darwin only
if image := info.Mount(c.MountPoint); image != nil {
c.DmgPath = filepath.Clean(image.ImagePath)
}
}
if err := utils.Retry(3, 2*time.Second, func() error {
return utils.Unmount(c.MountPoint, true)
}); err != nil {
return fmt.Errorf("failed to unmount %s at %s: %v", c.DmgPath, c.MountPoint, err)
}
cleanDmgPath := filepath.Clean(c.DmgPath)
if cleanDmgPath == "." || cleanDmgPath == "" {
return nil
}
return utils.Retry(3, 1*time.Second, func() error {
if err := os.Remove(cleanDmgPath); err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
return nil
})
}
// DmgInIPSW will mount a DMG from an IPSW
func DmgInIPSW(path, typ string, cfg *Config) (*Context, error) {
var err error
ipswPath := filepath.Clean(path)
var i *info.Info
if wkeys, ok := cfg.Keys.(download.WikiFWKeys); ok {
dtkey, err := wkeys.GetKeyByRegex(`.*DeviceTree.*(img3|im4p)$`)
if err != nil {
return nil, fmt.Errorf("failed to get DeviceTree key: %v", err)
}
i, err = info.Parse(ipswPath, dtkey)
if err != nil {
return nil, fmt.Errorf("failed to parse IPSW: %v", err)
}
} else {
i, err = info.Parse(ipswPath)
if err != nil {
return nil, fmt.Errorf("failed to parse IPSW: %v", err)
}
}
var dmgPath string
switch typ {
case "fs":
dmgPath, err = i.GetFileSystemOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to get filesystem DMG: %v", err)
}
case "sys":
dmgPath, err = i.GetSystemOsDmg()
if err != nil {
if errors.Is(err, info.ErrorCryptexNotFound) {
log.Warn("could not find SystemOS DMG; trying filesystem DMG (older IPSWs don't have cryptexes)")
dmgPath, err = i.GetFileSystemOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to get filesystem DMG: %v", err)
}
} else {
return nil, fmt.Errorf("failed to get SystemOS DMG: %v", err)
}
}
case "app":
dmgPath, err = i.GetAppOsDmg()
if err != nil {
return nil, fmt.Errorf("failed to get AppOS DMG: %v", err)
}
case "exc":
dmgPath, err = i.GetExclaveOSDmg()
if err != nil {
return nil, fmt.Errorf("failed to get ExclaveOS DMG: %v", err)
}
case "rdisk":
dmgPath, err = i.GetRestoreRamDiskDmg(cfg.Ident)
if err != nil {
return nil, fmt.Errorf("failed to get RestoreRamDisk DMG: %v", err)
}
default:
return nil, fmt.Errorf("invalid subcommand: %s; must be one of: '%s'", typ, strings.Join(DmgTypes, "', '"))
}
extractDir := cfg.ExtractDir
if extractDir == "" {
extractDir = os.TempDir()
}
extractedDMG := filepath.Join(extractDir, dmgPath)
if _, err := os.Stat(extractedDMG); os.IsNotExist(err) {
dmgs, err := utils.Unzip(ipswPath, extractDir, func(f *zip.File) bool {
return strings.EqualFold(filepath.Base(f.Name), dmgPath)
})
if err != nil {
return nil, fmt.Errorf("failed to extract %s from IPSW: %v", dmgPath, err)
}
if len(dmgs) == 0 {
return nil, fmt.Errorf("failed to find %s in IPSW", dmgPath)
}
}
if filepath.Ext(extractedDMG) == ".aea" {
encryptedDMG := extractedDMG
extractedDMG, err = aea.Decrypt(&aea.DecryptConfig{
Input: encryptedDMG,
Output: filepath.Dir(encryptedDMG),
PemDB: cfg.PemDB,
Proxy: "", // TODO: make proxy configurable
Insecure: false, // TODO: make insecure configurable
})
if err != nil {
return nil, fmt.Errorf("failed to parse AEA encrypted DMG: %v", err)
}
_ = os.Remove(encryptedDMG)
}
if isEncrypted, err := magic.IsEncryptedDMG(extractedDMG); err != nil {
return nil, fmt.Errorf("failed to check if DMG is encrypted: %v", err)
} else if isEncrypted {
var key string
switch v := cfg.Keys.(type) {
case string:
key = v
case download.WikiFWKeys:
key, err = v.GetKeyByFilename(extractedDMG)
if err != nil {
return nil, fmt.Errorf("failed to get key for DMG '%s': %v", extractedDMG, err)
}
}
log.Info("Decrypting DMG...")
if dmg, err := dmg.Open(extractedDMG, &dmg.Config{
Key: key,
}); err != nil {
return nil, fmt.Errorf("failed to open DMG '%s': %v", extractedDMG, err)
} else {
defer func() { _ = dmg.Close() }()
if err := os.Rename(dmg.DecryptedTemp(), extractedDMG); err != nil {
return nil, fmt.Errorf("failed to overwrite encrypted DMG with the decrypted one: %v", err)
}
}
}
if typ == "rdisk" {
// ramdisk DMGs are actually IM4P files
im4p, err := img4.OpenPayload(extractedDMG)
if err != nil {
return nil, fmt.Errorf("failed to parse ramdisk IM4P: %v", err)
}
data, err := im4p.GetData()
if err != nil {
return nil, fmt.Errorf("failed to get ramdisk IM4P data: %v", err)
}
// overwrite extractedDMG with the raw IM4P data
if err := os.WriteFile(extractedDMG, data, 0644); err != nil {
return nil, fmt.Errorf("failed to overwrite ramdisk DMG: %v", err)
}
}
mp, am, err := utils.MountDMG(extractedDMG, cfg.MountPoint)
if err != nil {
return nil, fmt.Errorf("failed to mount %s: %v", extractedDMG, err)
}
return &Context{
DmgPath: extractedDMG,
MountPoint: mp,
AlreadyMounted: am,
}, nil
}