mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
913 lines
27 KiB
Go
913 lines
27 KiB
Go
package utils
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/apex/log"
|
|
"github.com/blacktop/go-plist"
|
|
"github.com/blacktop/ipsw/internal/magic"
|
|
"github.com/blacktop/ipsw/internal/utils/lsof"
|
|
semver "github.com/hashicorp/go-version"
|
|
"golang.org/x/sys/execabs"
|
|
)
|
|
|
|
// Cp copies a file from src to dest
|
|
func Cp(src, dst string) error {
|
|
from, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer from.Close()
|
|
|
|
to, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, 0660)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer to.Close()
|
|
|
|
_, err = io.Copy(to, from)
|
|
|
|
return err
|
|
}
|
|
|
|
// MkdirAndCopy recursively copies src into dst with src's file modes while also creating the outer directory
|
|
func MkdirAndCopy(src, dst string) error {
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
|
|
return err
|
|
}
|
|
return CopyWithMode(src, dst, 0)
|
|
}
|
|
|
|
// Copy recursively copies src into dst with src's file modes.
|
|
// CREDIT: https://github.com/goreleaser/goreleaser/blob/main/internal/gio/copy.go
|
|
func Copy(src, dst string) error {
|
|
return CopyWithMode(src, dst, 0)
|
|
}
|
|
|
|
// CopyWithMode recursively copies src into dst with the given mode.
|
|
// The given mode applies only to files. Their parent dirs will have the same mode as their src counterparts.
|
|
func CopyWithMode(src, dst string, mode os.FileMode) error {
|
|
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return fmt.Errorf("failed to copy %s to %s: %w", src, dst, err)
|
|
}
|
|
// We have the following:
|
|
// - src = "a/b"
|
|
// - dst = "dist/linuxamd64/b"
|
|
// - path = "a/b/c.txt"
|
|
// So we join "a/b" with "c.txt" and use it as the destination.
|
|
dst := filepath.Join(dst, strings.Replace(path, src, "", 1))
|
|
if info.IsDir() {
|
|
return os.MkdirAll(dst, info.Mode())
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
return copySymlink(path, dst)
|
|
}
|
|
if mode != 0 {
|
|
return copyFile(path, dst, mode)
|
|
}
|
|
return copyFile(path, dst, info.Mode())
|
|
})
|
|
}
|
|
|
|
func copySymlink(src, dst string) error {
|
|
src, err := os.Readlink(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.Symlink(src, dst)
|
|
}
|
|
|
|
func copyFile(src, dst string, mode os.FileMode) error {
|
|
original, err := os.Open(src)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open '%s': %w", src, err)
|
|
}
|
|
defer original.Close()
|
|
|
|
new, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open '%s': %w", dst, err)
|
|
}
|
|
defer new.Close()
|
|
|
|
if _, err := io.Copy(new, original); err != nil {
|
|
return fmt.Errorf("failed to copy: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CodeSign codesigns a given binary
|
|
func CodeSign(filePath, signature string) error {
|
|
if runtime.GOOS == "darwin" {
|
|
filePath = filepath.Clean(filePath)
|
|
cmd := exec.Command("/usr/bin/codesign", "-s", "-", "-f", "-s", signature, "-f", filePath)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
// CodeSignWithEntitlements codesigns a given binary with given entitlements
|
|
func CodeSignWithEntitlements(filePath, entitlementsPath, signature string) error {
|
|
if runtime.GOOS != "darwin" {
|
|
return fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
return Retry(2, 0, func() error {
|
|
Indent(log.Info, 2)(fmt.Sprintf("Codesigning '%s' with entitlements", filepath.Base(filePath)))
|
|
cmd := exec.Command("/usr/bin/codesign", "--entitlements", entitlementsPath, "-s", signature, "-f", filepath.Clean(filePath))
|
|
out, err := cmd.CombinedOutput()
|
|
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
if strings.Contains(string(out), "AMFIUnserializeXML: syntax error") {
|
|
Indent(log.Error, 2)(fmt.Sprintf("%v: %s", err, strings.TrimSpace(string(out))))
|
|
Indent(log.Info, 2)(fmt.Sprintf("Converting entitlements file '%s' to XML1 format", entitlementsPath))
|
|
|
|
convertCmd := exec.Command("/usr/bin/plutil", "-convert", "xml1", entitlementsPath)
|
|
convertOut, convertErr := convertCmd.CombinedOutput()
|
|
if convertErr != nil {
|
|
return &StopRetryingError{Err: fmt.Errorf("%v: %s", convertErr, strings.TrimSpace(string(convertOut)))}
|
|
}
|
|
|
|
return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
|
|
}
|
|
|
|
return &StopRetryingError{Err: fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))}
|
|
})
|
|
}
|
|
|
|
// CodeSignAdHoc codesigns a given binary with ad-hoc signature
|
|
func CodeSignAdHoc(filePath string) error {
|
|
return CodeSign(filePath, "-")
|
|
}
|
|
|
|
func CodesignVerify(path string) (string, error) {
|
|
if runtime.GOOS == "darwin" {
|
|
cmd := exec.Command("/usr/bin/codesign", "--verify", "--deep", "--strict", "--verbose=4", path)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
return string(out), nil
|
|
}
|
|
return "", fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
func CodesignShow(path string) (string, error) {
|
|
if runtime.GOOS == "darwin" {
|
|
cmd := exec.Command("/usr/bin/codesign", "-d", "--verbose=4", path)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
return string(out), nil
|
|
}
|
|
return "", fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
// CreateSparseDiskImage creates a sparse disk image and returns it's path
|
|
func CreateSparseDiskImage(volumeName, diskPath string) (string, error) {
|
|
if runtime.GOOS == "darwin" {
|
|
|
|
cmd := exec.Command("/usr/bin/hdiutil", "create", "-size", "16g", "-fs", "HFS+", "-volname", volumeName, "-type", "SPARSE", "-plist", diskPath)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
|
|
var paths []string
|
|
if err := plist.NewDecoder(bytes.NewReader(out)).Decode(&paths); err != nil {
|
|
return "", fmt.Errorf("failed to parse hdiutil output plist: %v", err)
|
|
}
|
|
|
|
return paths[0], nil
|
|
}
|
|
|
|
return "", fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
// CreateCompressedDMG creates a compressed r/o disk image containing Install macOS.app
|
|
func CreateCompressedDMG(appPath, diskimagePath string) error {
|
|
if runtime.GOOS == "darwin" {
|
|
|
|
cmd := exec.Command("/usr/bin/hdiutil", "create", "-fs", "HFS+", "-srcfolder", appPath, diskimagePath)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
// CreateInstaller creates an macOS installer
|
|
func CreateInstaller(distPath, targetVol string) error {
|
|
if runtime.GOOS == "darwin" {
|
|
|
|
cmd := exec.Command("/usr/sbin/installer", "-pkg", distPath, "-target", targetVol)
|
|
cmd.Env = os.Environ()
|
|
cmd.Env = append(cmd.Env, "CM_BUILD=CM_BUILD")
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
|
|
if _, err := os.Stat(targetVol + "Applications"); os.IsNotExist(err) {
|
|
cmd := exec.Command("/usr/bin/ditto", targetVol+"Applications", filepath.Join(targetVol, "Applications"))
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
|
|
cmd = exec.Command("/bin/rm", "-r", targetVol+"Applications")
|
|
out, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
type IORegistryEntryChild struct {
|
|
IOBusyInterest string
|
|
IOConsoleSecurityInterest string
|
|
IOInterruptControllers []string
|
|
IOInterruptSpecifiers [][]byte
|
|
IOObjectClass string
|
|
IOObjectRetainCount int
|
|
IOPlatformSerialNumber string
|
|
IOPlatformSystemSleepPolicy []byte
|
|
IOPlatformUUID string
|
|
IOPolledInterface string
|
|
IORegistryEntryID int
|
|
IORegistryEntryName string
|
|
IOServiceBusyState int
|
|
IOServiceBusyTime int
|
|
IOServiceState int
|
|
BoardID []byte `plist:"board-id,omitempty"`
|
|
BridgeModel []byte `plist:"bridge-model,omitempty"`
|
|
ClockFrequency []byte `plist:"clock-frequency,omitempty"`
|
|
Compatible []byte `plist:"compatible,omitempty"`
|
|
Manufacturer []byte `plist:"manufacturer,omitempty"`
|
|
Model []byte `plist:"model,omitempty"`
|
|
Name []byte `plist:"name,omitempty"`
|
|
PlatformFeature []byte `plist:"platform-feature,omitempty"`
|
|
ProductName []byte `plist:"product-name,omitempty"`
|
|
SerialNumber []byte `plist:"serial-number,omitempty"`
|
|
SystemType []byte `plist:"system-type,omitempty"`
|
|
TargetType []byte `plist:"target-type,omitempty"`
|
|
Version []byte `plist:"version,omitempty"`
|
|
}
|
|
|
|
func (e IORegistryEntryChild) String() string {
|
|
return fmt.Sprintf(
|
|
"Name: %s\n"+
|
|
"Model: %s\n"+
|
|
"ProductName: %s\n"+
|
|
"Compatible: %s\n"+
|
|
"BridgeModel: %s\n"+
|
|
"BoardID: %s\n"+
|
|
"TargetType: %s\n"+
|
|
"SystemType: %d\n"+
|
|
"PlatformFeature: %s\n"+
|
|
"ClockFrequency: %#x\n"+
|
|
"Manufacturer: %s\n"+
|
|
"Version: %s\n",
|
|
string(e.Name),
|
|
string(e.Model),
|
|
string(e.ProductName),
|
|
string(e.Compatible),
|
|
string(e.BridgeModel),
|
|
string(e.BoardID),
|
|
string(e.TargetType),
|
|
e.SystemType,
|
|
string(e.PlatformFeature),
|
|
binary.BigEndian.Uint32(e.ClockFrequency),
|
|
string(e.Manufacturer),
|
|
string(e.Version),
|
|
)
|
|
}
|
|
|
|
type DeviceID struct {
|
|
IOObjectClass string
|
|
IOObjectRetainCount int
|
|
IORegistryEntryChildren []IORegistryEntryChild
|
|
IORegistryEntryID int
|
|
IORegistryEntryName string
|
|
}
|
|
|
|
// GetDeviceID returns the ioreg IOPlatformExpertDevice of the current device
|
|
func GetDeviceID() (*DeviceID, error) {
|
|
if runtime.GOOS == "darwin" {
|
|
|
|
cmd := exec.Command("/usr/sbin/ioreg", "-c", "IOPlatformExpertDevice", "-d", "2", "-a")
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
|
|
var dID DeviceID
|
|
if err := plist.NewDecoder(bytes.NewReader(out)).Decode(&dID); err != nil {
|
|
return nil, fmt.Errorf("failed to parse hdiutil output plist: %v", err)
|
|
}
|
|
|
|
return &dID, nil
|
|
}
|
|
return nil, fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
type BuildInfo struct {
|
|
ProductNames string
|
|
ProductVersion string
|
|
BuildVersion string
|
|
}
|
|
|
|
// GetBuildInfo returns the current device OS build info
|
|
func GetBuildInfo() (*BuildInfo, error) {
|
|
if runtime.GOOS == "darwin" {
|
|
var binfo BuildInfo
|
|
|
|
cmd := exec.Command("/usr/bin/sw_vers", "-productName")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
|
|
binfo.ProductNames = strings.TrimSuffix(string(out), "\n")
|
|
|
|
cmd = exec.Command("/usr/bin/sw_vers", "-productVersion")
|
|
out, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
|
|
binfo.ProductVersion = strings.TrimSuffix(string(out), "\n")
|
|
|
|
cmd = exec.Command("/usr/bin/sw_vers", "-buildVersion")
|
|
out, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
|
|
binfo.BuildVersion = strings.TrimSuffix(string(out), "\n")
|
|
|
|
return &binfo, nil
|
|
}
|
|
return nil, fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
func GetXCodePath() (string, error) {
|
|
if runtime.GOOS == "darwin" {
|
|
cmd := exec.Command("/usr/bin/xcode-select", "-p")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
return strings.TrimSpace(string(out)), nil
|
|
}
|
|
return "", fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
type xcodeVersionPlist struct {
|
|
BuildVersion string `json:"BuildVersion"`
|
|
CFBundleShortVersionString string `json:"CFBundleShortVersionString"`
|
|
CFBundleVersion string `json:"CFBundleVersion"`
|
|
ProductBuildVersion string `json:"ProductBuildVersion"`
|
|
ProjectName string `json:"ProjectName"`
|
|
SourceVersion string `json:"SourceVersion"`
|
|
}
|
|
|
|
func GetXCodeVersion(path ...string) (string, error) {
|
|
if runtime.GOOS == "darwin" {
|
|
if len(path) == 0 {
|
|
cmd := exec.Command("/usr/bin/xcodebuild", "-version")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
version, _, ok := strings.Cut(strings.TrimSpace(string(out)), "\n")
|
|
if !ok {
|
|
return "", fmt.Errorf("failed to parse xcodebuild version: %s", out)
|
|
}
|
|
return strings.TrimPrefix(version, "Xcode "), nil
|
|
} else {
|
|
f, err := os.Open(filepath.Join(path[0], "Contents", "version.plist"))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to open Xcode version.plist: %v", err)
|
|
}
|
|
defer f.Close()
|
|
var version xcodeVersionPlist
|
|
if err := plist.NewDecoder(f).Decode(&version); err != nil {
|
|
return "", fmt.Errorf("failed to decode Xcode version.plist: %v", err)
|
|
}
|
|
return version.CFBundleShortVersionString, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
func GetKernelPath() (string, error) {
|
|
if runtime.GOOS == "darwin" {
|
|
cmd := exec.Command("uname", "-a")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
uname := strings.TrimSpace(string(out))
|
|
_, triple, ok := strings.Cut(uname, "/")
|
|
if !ok {
|
|
return "", fmt.Errorf("failed to parse uname: %s", uname)
|
|
}
|
|
tripleParts := strings.Split(triple, " ")
|
|
if len(tripleParts) != 2 {
|
|
return "", fmt.Errorf("failed to parse uname: %s", uname)
|
|
}
|
|
tparts := strings.Split(tripleParts[0], "_")
|
|
if len(tparts) != 3 {
|
|
return "", fmt.Errorf("failed to parse uname: %s", uname)
|
|
}
|
|
return fmt.Sprintf("/System/Library/Kernels/kernel.release.%s", strings.ToLower(tparts[2])), nil
|
|
}
|
|
return "", fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
func GetKernelCollectionPath() (string, error) {
|
|
if runtime.GOOS == "darwin" {
|
|
defaultKcPath := "/System/Library/KernelCollections/BootKernelExtensions.kc"
|
|
if runtime.GOARCH == "amd64" {
|
|
return defaultKcPath, nil
|
|
}
|
|
cmd := exec.Command("sysctl", "-n", "kern.bootobjectspath")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return defaultKcPath, nil
|
|
}
|
|
return filepath.Join("/System/Volumes/Preboot", strings.TrimSpace(string(out)), "System/Library/Caches/com.apple.kernelcaches/kernelcache"), nil
|
|
}
|
|
return "", fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
var ErrMountResourceBusy = errors.New("hdiutil: mount failed - Resource busy")
|
|
|
|
// Mount mounts a DMG with hdiutil
|
|
func Mount(image, mountPoint string) error {
|
|
if runtime.GOOS == "darwin" {
|
|
out, err := exec.Command("/usr/bin/hdiutil", "attach", "-noverify", "-mountpoint", mountPoint, image).CombinedOutput()
|
|
if err != nil {
|
|
if strings.Contains(string(out), "hdiutil: mount failed - Resource busy") {
|
|
return ErrMountResourceBusy
|
|
}
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
} else {
|
|
// On Linux, detect filesystem type and use appropriate FUSE tool
|
|
return mountLinux(image, mountPoint)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MountedFilesystemRoot returns the root directory of a mounted filesystem.
|
|
// apfs-fuse exposes APFS volume contents below <mountPoint>/root; hdiutil and
|
|
// hfsfuse expose the volume contents directly at <mountPoint>.
|
|
func MountedFilesystemRoot(mountPoint string) string {
|
|
mountPoint = filepath.Clean(mountPoint)
|
|
|
|
rootPath := filepath.Join(mountPoint, "root")
|
|
rootInfo, err := os.Stat(rootPath)
|
|
if err != nil || !rootInfo.IsDir() {
|
|
return mountPoint
|
|
}
|
|
|
|
systemPath := filepath.Join(mountPoint, "System")
|
|
if _, err := os.Stat(systemPath); !os.IsNotExist(err) {
|
|
return mountPoint
|
|
}
|
|
|
|
return rootPath
|
|
}
|
|
|
|
// mountLinux mounts a DMG on Linux using the appropriate FUSE tool based on filesystem type
|
|
func mountLinux(image, mountPoint string) error {
|
|
// First, try to detect the filesystem type
|
|
isAPFSFilesystem, err := magic.IsAPFS(image)
|
|
if err != nil {
|
|
log.Warnf("failed to detect APFS filesystem: %v", err)
|
|
}
|
|
|
|
isHFSPlusFilesystem, err := magic.IsHFSPlus(image)
|
|
if err != nil {
|
|
log.Warnf("failed to detect HFS+ filesystem: %v", err)
|
|
}
|
|
|
|
if isAPFSFilesystem {
|
|
log.Debugf("detected APFS filesystem, using apfs-fuse")
|
|
return mountWithAPFSFuse(image, mountPoint)
|
|
} else if isHFSPlusFilesystem {
|
|
log.Debugf("detected HFS+ filesystem, using native kernel support")
|
|
return mountWithHFSPlus(image, mountPoint)
|
|
} else {
|
|
// Fallback: try APFS first, then HFS+ if that fails
|
|
log.Debugf("filesystem type unclear, trying apfs-fuse first")
|
|
if err := mountWithAPFSFuse(image, mountPoint); err != nil {
|
|
log.Debugf("apfs-fuse failed (%v), trying native HFS+ support", err)
|
|
return mountWithHFSPlus(image, mountPoint)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// findApfsFuse attempts to locate apfs-fuse binary using multiple strategies
|
|
func findApfsFuse() (string, error) {
|
|
// Check environment variable override first
|
|
if envPath := os.Getenv("IPSW_APFS_FUSE_PATH"); envPath != "" {
|
|
if _, err := os.Stat(envPath); err == nil {
|
|
return envPath, nil
|
|
}
|
|
log.Warnf("IPSW_APFS_FUSE_PATH points to non-existent file: %s", envPath)
|
|
}
|
|
|
|
// Try standard PATH lookup
|
|
if path, err := execabs.LookPath("apfs-fuse"); err == nil {
|
|
return path, nil
|
|
}
|
|
|
|
// Check common installation locations (helpful for snap/confined environments)
|
|
commonPaths := []string{
|
|
"/usr/local/bin/apfs-fuse",
|
|
"/usr/bin/apfs-fuse",
|
|
"/opt/apfs-fuse/bin/apfs-fuse",
|
|
"/snap/bin/apfs-fuse",
|
|
"/home/linuxbrew/.linuxbrew/bin/apfs-fuse",
|
|
}
|
|
|
|
for _, path := range commonPaths {
|
|
if _, err := os.Stat(path); err == nil {
|
|
return path, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("apfs-fuse not found in PATH or common locations: " +
|
|
"Install apfs-fuse or set IPSW_APFS_FUSE_PATH environment variable. " +
|
|
"(snap users: try 'sudo snap connect ipsw:system-files' if available)")
|
|
}
|
|
|
|
// mountWithAPFSFuse mounts using apfs-fuse
|
|
func mountWithAPFSFuse(image, mountPoint string) error {
|
|
apfsFusePath, err := findApfsFuse()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find apfs-fuse: %v", err)
|
|
}
|
|
out, err := exec.Command(apfsFusePath, image, mountPoint).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to mount '%s' via apfs-fuse: %v: cmd output - '%s'", image, err, out)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// mountWithHFSPlus mounts using native Linux HFS+ kernel support
|
|
func mountWithHFSPlus(image, mountPoint string) error {
|
|
// Set up a loop device for the DMG file
|
|
out, err := exec.Command("losetup", "-f", "--show", image).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set up loop device for '%s': %v: cmd output - '%s'", image, err, out)
|
|
}
|
|
|
|
loopDevice := strings.TrimSpace(string(out))
|
|
log.Debugf("created loop device: %s", loopDevice)
|
|
|
|
// Mount the loop device using native HFS+ support
|
|
err = exec.Command("mount", "-t", "hfsplus", "-o", "ro", loopDevice, mountPoint).Run()
|
|
if err != nil {
|
|
// Clean up loop device on mount failure
|
|
if cleanupErr := exec.Command("losetup", "-d", loopDevice).Run(); cleanupErr != nil {
|
|
log.Warnf("failed to clean up loop device %s: %v", loopDevice, cleanupErr)
|
|
}
|
|
return fmt.Errorf("failed to mount HFS+ filesystem '%s': %v (ensure hfsplus kernel module is loaded)", loopDevice, err)
|
|
}
|
|
|
|
log.Debugf("mounted HFS+ filesystem at %s", mountPoint)
|
|
return nil
|
|
}
|
|
|
|
func MountEncrypted(image, mountPoint, password string) error {
|
|
if runtime.GOOS == "darwin" {
|
|
cmd := exec.Command("/usr/bin/hdiutil", "attach", "-noverify", "-mountpoint", mountPoint, "-stdinpass", image)
|
|
cmd.Stdin = strings.NewReader(password)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
if strings.Contains(string(out), "hdiutil: mount failed - Resource busy") {
|
|
return ErrMountResourceBusy
|
|
}
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
func IsAlreadyMounted(image, mountPoint string) (string, bool, error) {
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
img := filepath.Clean(image)
|
|
if abs, err := filepath.Abs(img); err == nil {
|
|
img = abs
|
|
}
|
|
|
|
info, err := MountInfo()
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
for _, i := range info.Images {
|
|
ip := filepath.Clean(i.ImagePath)
|
|
if ip == img {
|
|
for _, entry := range i.SystemEntities {
|
|
if entry.MountPoint != "" {
|
|
return entry.MountPoint, true, nil
|
|
}
|
|
}
|
|
// Image matches, but no mounted filesystem entity was reported.
|
|
// Treat as not mounted so callers can attempt to (re)attach.
|
|
break
|
|
}
|
|
}
|
|
case "linux":
|
|
if _, err := os.Stat(filepath.Join(mountPoint, "root")); !os.IsNotExist(err) {
|
|
return mountPoint, true, nil
|
|
}
|
|
}
|
|
return "", false, nil
|
|
}
|
|
|
|
func MountDMG(image string, customMountPoint string) (string, bool, error) {
|
|
image = filepath.Clean(image)
|
|
if abs, err := filepath.Abs(image); err == nil {
|
|
image = abs
|
|
}
|
|
|
|
var mountPoint string
|
|
if customMountPoint != "" {
|
|
mountPoint = customMountPoint
|
|
} else {
|
|
base := filepath.Base(image)
|
|
mountPoint = fmt.Sprintf("/tmp/%s.mount", base)
|
|
}
|
|
|
|
if runtime.GOOS == "darwin" {
|
|
// check if already mounted
|
|
if prevMountPoint, mounted, err := IsAlreadyMounted(image, mountPoint); mounted && err == nil {
|
|
if prevMountPoint != "" {
|
|
mountPoint = prevMountPoint
|
|
}
|
|
return mountPoint, true, nil
|
|
}
|
|
} else {
|
|
if err := os.MkdirAll(mountPoint, 0750); err != nil {
|
|
return "", false, fmt.Errorf("failed to create temporary mount point %s: %v", mountPoint, err)
|
|
}
|
|
}
|
|
|
|
if err := Mount(image, mountPoint); err != nil {
|
|
return "", false, fmt.Errorf("failed to mount %s: %v", image, err)
|
|
}
|
|
|
|
return mountPoint, false, nil
|
|
}
|
|
|
|
// Unmount unmounts a DMG with hdiutil
|
|
func Unmount(mountPoint string, force bool) error {
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
var cmd *exec.Cmd
|
|
|
|
if force {
|
|
cmd = exec.Command("hdiutil", "detach", mountPoint, "-force")
|
|
} else {
|
|
cmd = exec.Command("hdiutil", "detach", mountPoint)
|
|
}
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
var edetail string
|
|
if strings.Contains(err.Error(), "exit status 16") {
|
|
edetail = " (Resource busy)"
|
|
}
|
|
procs, lerr := lsof.MountPoint(mountPoint)
|
|
if lerr == nil {
|
|
edetail += "\n Processes using mount point:"
|
|
for _, proc := range procs {
|
|
edetail += fmt.Sprintf("\n %s", proc)
|
|
}
|
|
edetail += "\n"
|
|
}
|
|
return fmt.Errorf("failed to unmount %s: %v%s", mountPoint, err, edetail)
|
|
}
|
|
case "linux":
|
|
// First unmount the filesystem
|
|
cmd := exec.Command("umount", mountPoint)
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("failed to unmount %s: %v", mountPoint, err)
|
|
}
|
|
|
|
// Note: Loop devices created for HFS+ mounting are automatically cleaned up
|
|
// by the kernel when the filesystem is unmounted, so no explicit cleanup needed
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type systemEntry struct {
|
|
ContentHint string `plist:"content-hint,omitempty" xml:"content-hint,omitempty"`
|
|
DevEntry string `plist:"dev-entry,omitempty" xml:"dev-entry,omitempty"`
|
|
MountPoint string `plist:"mount-point,omitempty" xml:"mount-point,omitempty"`
|
|
}
|
|
|
|
type image struct {
|
|
Autodiskmount bool `plist:"autodiskmount,omitempty" xml:"autodiskmount,omitempty"`
|
|
BlockCount int64 `plist:"blockcount,omitempty" xml:"blockcount,omitempty"`
|
|
BlockSize int64 `plist:"blocksize,omitempty" xml:"blocksize,omitempty"`
|
|
DiskImages2 bool `plist:"diskimages2,omitempty" xml:"diskimages2,omitempty"`
|
|
HdidPID int64 `plist:"hdid-pid,omitempty" xml:"hdid-pid,omitempty"`
|
|
IconPath string `plist:"icon-path,omitempty" xml:"icon-path,omitempty"`
|
|
ImageEncrypted bool `plist:"image-encrypted,omitempty" xml:"image-encrypted,omitempty"`
|
|
ImagePath string `plist:"image-path,omitempty" xml:"image-path,omitempty"`
|
|
ImageType string `plist:"image-type,omitempty" xml:"image-type,omitempty"`
|
|
OwnerUID int64 `plist:"owner-uid,omitempty" xml:"owner-uid,omitempty"`
|
|
Removable bool `plist:"removable,omitempty" xml:"removable,omitempty"`
|
|
SystemEntities []systemEntry `plist:"system-entities,omitempty" xml:"system-entities,omitempty"`
|
|
}
|
|
|
|
type HdiUtilInfo struct {
|
|
Framework string `plist:"framework,omitempty" xml:"framework,omitempty"`
|
|
Revision string `plist:"revision,omitempty" xml:"revision,omitempty"`
|
|
Vendor string `plist:"vendor,omitempty" xml:"vendor,omitempty"`
|
|
Images []image `plist:"images,omitempty" xml:"images,omitempty"`
|
|
}
|
|
|
|
func (i HdiUtilInfo) Mount(mount string) *image {
|
|
for _, img := range i.Images {
|
|
for _, entry := range img.SystemEntities {
|
|
if strings.Contains(entry.MountPoint, mount) {
|
|
return &img
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func MountInfo() (*HdiUtilInfo, error) {
|
|
if runtime.GOOS == "darwin" {
|
|
cmd := exec.Command("hdiutil", "info", "-plist")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
var info HdiUtilInfo
|
|
if err := plist.NewDecoder(bytes.NewReader(out)).Decode(&info); err != nil {
|
|
return nil, fmt.Errorf("failed to decode hdiutil info plist: %v", err)
|
|
}
|
|
return &info, nil
|
|
}
|
|
return nil, fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
func PkgUtilExpand(src, dst string) (string, error) {
|
|
if runtime.GOOS == "darwin" {
|
|
// cmd := exec.Command("pkgutil", "--expand-full", name, filepath.Join(os.TempDir(), "macosupd"))
|
|
outDir := filepath.Join(dst, "macosupd")
|
|
cmd := exec.Command("pkgutil", "--expand", src, outDir)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
return outDir, nil
|
|
}
|
|
return "", fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
func InstallXCodeSimRuntime(path string) error {
|
|
if runtime.GOOS == "darwin" {
|
|
cmd := exec.Command("xcrun", "simctl", "runtime", "add", path)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
func InstallKDK(path string) error {
|
|
if runtime.GOOS == "darwin" {
|
|
cmd := exec.Command("hdiutil", "attach", path)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
cmd = exec.Command("sudo", "installer", "-pkg", "/Volumes/Kernel Debug Kit/KernelDebugKit.pkg", "-target", "/")
|
|
out, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
cmd = exec.Command("hdiutil", "detach", "/Volumes/Kernel Debug Kit")
|
|
out, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("only supported on macOS")
|
|
}
|
|
|
|
type KMUConfig struct {
|
|
Suffix string
|
|
Arch string
|
|
KDK string
|
|
Name string
|
|
Kernel string
|
|
Exclude []string
|
|
}
|
|
|
|
func KmutilCreate(conf *KMUConfig) (err error) {
|
|
if runtime.GOOS == "darwin" {
|
|
binfo, err := GetBuildInfo()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get build info: %v", err)
|
|
}
|
|
ver, err := semver.NewVersion(binfo.ProductVersion)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse product version: %v", err)
|
|
}
|
|
args := []string{"create", "--verbose", "--new", "boot"}
|
|
if conf.Suffix != "" {
|
|
args = append(args, "--variant-suffix", conf.Suffix)
|
|
}
|
|
if conf.Arch != "" {
|
|
args = append(args, "--arch", conf.Arch)
|
|
}
|
|
if ver.GreaterThan(semver.Must(semver.NewVersion("13.0"))) {
|
|
if conf.KDK == "" {
|
|
conf.KDK = fmt.Sprintf("/Library/Developer/KDKs/KDK_%s_%s.kdk", binfo.ProductVersion, binfo.BuildVersion)
|
|
}
|
|
args = append(args, "--kdk", conf.KDK)
|
|
}
|
|
if conf.Kernel == "" {
|
|
conf.Kernel, err = GetKernelPath()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get kernel path: %v", err)
|
|
}
|
|
}
|
|
args = append(args, "--kernel", conf.Kernel)
|
|
if conf.Name != "" {
|
|
if err := os.MkdirAll(filepath.Dir(conf.Name), 0750); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %v", filepath.Dir(conf.Name), err)
|
|
}
|
|
args = append(args, "--boot-path", conf.Name)
|
|
}
|
|
if len(conf.Exclude) > 0 {
|
|
args = append(args, "--explicit-only")
|
|
args = append(args, conf.Exclude...)
|
|
}
|
|
|
|
cmd := exec.Command("kmutil", args...)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %s", err, out)
|
|
}
|
|
|
|
fmt.Println(string(out))
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("only supported on macOS")
|
|
}
|