diff --git a/cmd/ipsw/cmd/diff.go b/cmd/ipsw/cmd/diff.go
index 0bc92d25d..874d776be 100644
--- a/cmd/ipsw/cmd/diff.go
+++ b/cmd/ipsw/cmd/diff.go
@@ -44,6 +44,7 @@ func init() {
diffCmd.Flags().StringArrayP("kdk", "k", []string{}, "Path to KDKs to diff")
diffCmd.Flags().Bool("launchd", false, "Diff launchd configs")
diffCmd.Flags().Bool("fw", false, "Diff other firmwares")
+ diffCmd.Flags().Bool("feat", false, "Diff feature flags")
diffCmd.Flags().StringSliceP("filter", "f", []string{}, "Filter MachO sections to diff (e.g. __TEXT.__text)")
diffCmd.Flags().StringP("output", "o", "", "Folder to save diff output")
diffCmd.MarkFlagDirname("output")
@@ -56,6 +57,7 @@ func init() {
viper.BindPFlag("diff.kdk", diffCmd.Flags().Lookup("kdk"))
viper.BindPFlag("diff.launchd", diffCmd.Flags().Lookup("launchd"))
viper.BindPFlag("diff.fw", diffCmd.Flags().Lookup("fw"))
+ viper.BindPFlag("diff.feat", diffCmd.Flags().Lookup("feat"))
viper.BindPFlag("diff.filter", diffCmd.Flags().Lookup("filter"))
viper.BindPFlag("diff.output", diffCmd.Flags().Lookup("output"))
}
@@ -115,6 +117,7 @@ var diffCmd = &cobra.Command{
KDKs: viper.GetStringSlice("diff.kdk"),
LaunchD: viper.GetBool("diff.launchd"),
Firmware: viper.GetBool("diff.fw"),
+ Features: viper.GetBool("diff.feat"),
Filter: viper.GetStringSlice("diff.filter"),
Output: viper.GetString("diff.output"),
})
diff --git a/internal/commands/macho/patch.go b/internal/commands/macho/patch.go
index 2d5aaba30..a768810f8 100644
--- a/internal/commands/macho/patch.go
+++ b/internal/commands/macho/patch.go
@@ -432,15 +432,15 @@ func PatchMachoMod(m *macho.File, machoPath, loadCommand string, args []string)
}
platform, err := types.GetPlatformByName(args[2])
if err != nil {
- return fmt.Errorf("failed to parse platform name %s: %v", args[2], err)
+ return fmt.Errorf("failed to parse platform name '%s': %v", args[2], err)
}
var minos types.Version
if err := minos.Set(args[3]); err != nil {
- return fmt.Errorf("failed to parse min OS versionc: %v", args[3], err)
+ return fmt.Errorf("failed to parse min OS version '%s': %v", args[3], err)
}
var sdk types.Version
if err := sdk.Set(args[4]); err != nil {
- return fmt.Errorf("failed to parse SDK version: %v", args[4], err)
+ return fmt.Errorf("failed to parse SDK version '%s': %v", args[4], err)
}
var tools []types.BuildVersionTool
if len(args) > 5 {
diff --git a/internal/diff/diff.go b/internal/diff/diff.go
index eeb3bfafb..bed059d91 100644
--- a/internal/diff/diff.go
+++ b/internal/diff/diff.go
@@ -9,6 +9,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "slices"
"strings"
"sync"
"time"
@@ -21,6 +22,7 @@ import (
"github.com/blacktop/ipsw/internal/commands/extract"
kcmd "github.com/blacktop/ipsw/internal/commands/kernel"
mcmd "github.com/blacktop/ipsw/internal/commands/macho"
+ "github.com/blacktop/ipsw/internal/search"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/aea"
"github.com/blacktop/ipsw/pkg/dyld"
@@ -40,6 +42,12 @@ type mount struct {
IsMounted bool
}
+type PlistDiff struct {
+ New []string `json:"new,omitempty"`
+ Removed []string `json:"removed,omitempty"`
+ Updated map[string]string `json:"changed,omitempty"`
+}
+
type Config struct {
Title string
IpswOld string
@@ -47,6 +55,7 @@ type Config struct {
KDKs []string
LaunchD bool
Firmware bool
+ Features bool
Filter []string
Output string
}
@@ -84,6 +93,7 @@ type Diff struct {
Machos *mcmd.MachoDiff `json:"machos,omitempty"`
Firmwares *mcmd.MachoDiff `json:"firmwares,omitempty"`
Launchd string `json:"launchd,omitempty"`
+ Features *PlistDiff `json:"features,omitempty"`
tmpDir string `json:"-"`
conf *Config
@@ -254,6 +264,13 @@ func (d *Diff) Diff() (err error) {
}
}
+ if d.conf.Features {
+ log.Info("Diffing Feature Flags")
+ if err := d.parseFeatureFlags(); err != nil {
+ return err
+ }
+ }
+
log.Info("Diffing ENTITLEMENTS")
d.Ents, err = d.parseEntitlements()
if err != nil {
@@ -555,3 +572,79 @@ func (d *Diff) parseFirmwares() (err error) {
})
return
}
+
+func (d *Diff) parseFeatureFlags() (err error) {
+ d.Features = &PlistDiff{
+ Updated: make(map[string]string),
+ }
+ conf := &mcmd.DiffConfig{
+ Markdown: true,
+ Color: false,
+ DiffTool: "git",
+ }
+
+ oldPlists := make(map[string]string)
+ if err := search.ForEachPlistInIPSW(d.Old.IPSWPath, "/System/Library/FeatureFlags", func(path string, content string) error {
+ oldPlists[path] = content
+ return nil
+ }); err != nil {
+ return err
+ }
+
+ var prevFiles []string
+ for f := range oldPlists {
+ prevFiles = append(prevFiles, f)
+ }
+ slices.Sort(prevFiles)
+
+ newPlists := make(map[string]string)
+ if err := search.ForEachPlistInIPSW(d.New.IPSWPath, "/System/Library/FeatureFlags", func(path string, content string) error {
+ newPlists[path] = content
+ return nil
+ }); err != nil {
+ return err
+ }
+
+ var nextFiles []string
+ for f := range newPlists {
+ nextFiles = append(nextFiles, f)
+ }
+ slices.Sort(nextFiles)
+
+ /* DIFF IPSW */
+ d.Features.New = utils.Difference(nextFiles, prevFiles)
+ d.Features.Removed = utils.Difference(prevFiles, nextFiles)
+ // gc
+ prevFiles = []string{}
+
+ for _, f2 := range nextFiles {
+ dat2 := newPlists[f2]
+ if dat1, ok := oldPlists[f2]; ok {
+ if strings.EqualFold(dat2, dat1) {
+ continue
+ }
+ var out string
+ if conf.Markdown {
+ out, err = utils.GitDiff(dat1+"\n", dat2+"\n", &utils.GitDiffConfig{Color: conf.Color, Tool: conf.DiffTool})
+ if err != nil {
+ return err
+ }
+ } else {
+ out, err = utils.GitDiff(dat1+"\n", dat2+"\n", &utils.GitDiffConfig{Color: conf.Color, Tool: conf.DiffTool})
+ if err != nil {
+ return err
+ }
+ }
+ if len(out) == 0 { // no diff
+ continue
+ }
+ if conf.Markdown {
+ d.Features.Updated[f2] = "```diff\n" + out + "\n```\n"
+ } else {
+ d.Features.Updated[f2] = out
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/internal/diff/md.go b/internal/diff/md.go
index 03dfeeefc..997abda02 100644
--- a/internal/diff/md.go
+++ b/internal/diff/md.go
@@ -35,22 +35,24 @@ func (d *Diff) Markdown() error {
)
// SECTION: Kernel
- out.WriteString(
- fmt.Sprintf(
- "## Kernel\n\n"+
- "### Version\n\n"+
- "| iOS | Version | Build | Date |\n"+
- "| :-- | :------ | :---- | :--- |\n"+
- "| %s *(%s)* | %s | %s | %s |\n"+
- "| %s *(%s)* | %s | %s | %s |\n\n",
- d.Old.Version, d.Old.Build,
- d.Old.Kernel.Version.KernelVersion.Darwin, d.Old.Kernel.Version.KernelVersion.XNU,
- d.Old.Kernel.Version.KernelVersion.Date.Format("Mon, 02Jan2006 15:04:05 MST"),
- d.New.Version, d.New.Build,
- d.New.Kernel.Version.KernelVersion.Darwin, d.New.Kernel.Version.KernelVersion.XNU,
- d.New.Kernel.Version.KernelVersion.Date.Format("Mon, 02Jan2006 15:04:05 MST"),
- ),
- )
+ if d.Old.Kernel.Version != nil && d.New.Kernel.Version != nil {
+ out.WriteString(
+ fmt.Sprintf(
+ "## Kernel\n\n"+
+ "### Version\n\n"+
+ "| iOS | Version | Build | Date |\n"+
+ "| :-- | :------ | :---- | :--- |\n"+
+ "| %s *(%s)* | %s | %s | %s |\n"+
+ "| %s *(%s)* | %s | %s | %s |\n\n",
+ d.Old.Version, d.Old.Build,
+ d.Old.Kernel.Version.KernelVersion.Darwin, d.Old.Kernel.Version.KernelVersion.XNU,
+ d.Old.Kernel.Version.KernelVersion.Date.Format("Mon, 02Jan2006 15:04:05 MST"),
+ d.New.Version, d.New.Build,
+ d.New.Kernel.Version.KernelVersion.Darwin, d.New.Kernel.Version.KernelVersion.XNU,
+ d.New.Kernel.Version.KernelVersion.Date.Format("Mon, 02Jan2006 15:04:05 MST"),
+ ),
+ )
+ }
// SUB-SECTION: Kexts
if d.Kexts != nil && (len(d.Kexts.New) > 0 || len(d.Kexts.Removed) > 0 || len(d.Kexts.Updated) > 0) {
@@ -351,6 +353,77 @@ func (d *Diff) Markdown() error {
}
}
+ // SUB-SECTION: Feature Flags
+ if d.Features != nil && (len(d.Features.New) > 0 || len(d.Features.Removed) > 0 || len(d.Features.Updated) > 0) {
+ out.WriteString("### Feature Flags\n\n")
+ if len(d.Features.New) > 0 {
+ out.WriteString(fmt.Sprintf("#### 🆕 NEW (%d)\n\n", len(d.Features.New)))
+ if len(d.Features.New) > 30 {
+ out.WriteString("\n" +
+ " View NEW
\n\n")
+ }
+ for _, k := range d.Features.New {
+ out.WriteString(fmt.Sprintf("- `%s`\n", k))
+ }
+ if len(d.Features.New) > 30 {
+ out.WriteString("\n \n")
+ }
+ out.WriteString("\n")
+ }
+ if len(d.Features.Removed) > 0 {
+ out.WriteString(fmt.Sprintf("#### ❌ Removed (%d)\n\n", len(d.Features.Removed)))
+ if len(d.Features.Removed) > 30 {
+ out.WriteString("\n" +
+ " View Removed
\n\n")
+ }
+ for _, k := range d.Features.Removed {
+ out.WriteString(fmt.Sprintf("- `%s`\n", k))
+ }
+ if len(d.Features.Removed) > 30 {
+ out.WriteString("\n \n")
+ }
+ out.WriteString("\n")
+ }
+ if len(d.Features.Updated) > 0 {
+ out.WriteString(fmt.Sprintf("#### ⬆️ Updated (%d)\n\n", len(d.Features.Updated)))
+ out.WriteString("\n" +
+ " View Updated
\n\n")
+ if len(d.Features.Updated) < 20 {
+ for k, v := range d.Features.Updated {
+ out.WriteString(fmt.Sprintf("#### %s\n\n", filepath.Base(k)))
+ out.WriteString(fmt.Sprintf("> `%s`\n\n", k))
+ out.WriteString(fmt.Sprintf("%s\n", v))
+ }
+ } else {
+ if err := os.MkdirAll(filepath.Join(d.conf.Output, "Features"), 0o750); err != nil {
+ return err
+ }
+ keys := make([]string, 0, len(d.Features.Updated))
+ for k := range d.Features.Updated {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ fname := filepath.Join(d.conf.Output, "Features", strings.ReplaceAll(filepath.Base(k), " ", "_")+".md")
+ if _, err := os.Stat(fname); os.IsExist(err) {
+ fname = filepath.Join(d.conf.Output, "Features", fmt.Sprintf("%s.%d.md", strings.ReplaceAll(filepath.Base(k), " ", "_"), rand.Intn(20)))
+ }
+ log.Debugf("Creating diff dylib Markdown file: %s", fname)
+ f, err := os.Create(fname)
+ if err != nil {
+ return fmt.Errorf("failed to create diff file: %w", err)
+ }
+ fmt.Fprintf(f, "## %s\n\n", filepath.Base(k))
+ fmt.Fprintf(f, "> `%s`\n\n", k)
+ fmt.Fprintf(f, d.Features.Updated[k])
+ f.Close()
+ out.WriteString(fmt.Sprintf("- [%s](%s)\n", k, filepath.Join("FEATURES", strings.ReplaceAll(filepath.Base(k), " ", "_")+".md")))
+ }
+ }
+ out.WriteString("\n \n\n")
+ }
+ }
+
out.WriteString("## EOF\n")
// Write TOC.md
diff --git a/internal/search/search.go b/internal/search/search.go
index b208d03e1..988f117c6 100644
--- a/internal/search/search.go
+++ b/internal/search/search.go
@@ -20,7 +20,8 @@ import (
"github.com/blacktop/ipsw/pkg/info"
)
-func scanDmg(ipswPath, dmgPath, dmgType string, handler func(string, *macho.File) error) error {
+// TODO: make this an array of handlers to perform multiple actions on each file
+func scanDmg(ipswPath, dmgPath, dmgType string, handler func(string, string) error) error {
// check if filesystem DMG already exists (due to previous mount command)
if _, err := os.Stat(dmgPath); os.IsNotExist(err) {
dmgs, err := utils.Unzip(ipswPath, "", func(f *zip.File) bool {
@@ -62,13 +63,40 @@ func scanDmg(ipswPath, dmgPath, dmgType string, handler func(string, *macho.File
}
var files []string
+ // Use a map to keep track of visited directories to avoid infinite loops
+ visited := make(map[string]bool)
if err := filepath.Walk(mountPoint, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Errorf("failed to walk mount %s: %v", path, err)
return nil
}
- if !info.IsDir() {
- files = append(files, path)
+ if info.Mode()&os.ModeSymlink != 0 { // follow symlinks
+ // Resolve the symlink
+ if linkPath, err := filepath.EvalSymlinks(path); err == nil {
+ // Get the info of the target file/directory
+ info, err = os.Stat(linkPath)
+ if err != nil {
+ return err
+ }
+ // If it's a directory and not visited, follow it
+ if info.IsDir() && !visited[linkPath] {
+ visited[linkPath] = true
+ return filepath.Walk(linkPath, func(subPath string, subInfo os.FileInfo, subErr error) error {
+ if subErr != nil {
+ return subErr
+ }
+ files = append(files, subPath)
+ return nil
+ })
+ }
+ }
+ } else {
+ if !info.IsDir() {
+ if !visited[path] {
+ visited[path] = true
+ files = append(files, path)
+ }
+ }
}
return nil
}); err != nil {
@@ -77,28 +105,7 @@ func scanDmg(ipswPath, dmgPath, dmgType string, handler func(string, *macho.File
for _, file := range files {
if err := func() error {
- if ok, _ := magic.IsMachO(file); ok {
- var m *macho.File
- // UNIVERSAL MACHO
- if fat, err := macho.OpenFat(file); err == nil {
- defer fat.Close()
- m = fat.Arches[len(fat.Arches)-1].File
- } else { // SINGLE MACHO
- if errors.Is(err, macho.ErrNotFat) {
- m, err = macho.Open(file)
- if err != nil {
- return nil
- }
- defer m.Close()
- } else { // NOT a macho file
- return nil
- }
- }
- if err := handler(strings.TrimPrefix(file, mountPoint), m); err != nil {
- return fmt.Errorf("failed to handle macho %s: %w", file, err)
- }
- }
- return nil
+ return handler(mountPoint, file)
}(); err != nil {
return err
}
@@ -109,6 +116,31 @@ func scanDmg(ipswPath, dmgPath, dmgType string, handler func(string, *macho.File
// ForEachMachoInIPSW walks the IPSW and calls the handler for each macho file found
func ForEachMachoInIPSW(ipswPath string, handler func(string, *macho.File) error) error {
+ scanMacho := func(mountPoint, machoPath string) error {
+ if ok, _ := magic.IsMachO(machoPath); ok {
+ var m *macho.File
+ // UNIVERSAL MACHO
+ if fat, err := macho.OpenFat(machoPath); err == nil {
+ defer fat.Close()
+ m = fat.Arches[len(fat.Arches)-1].File
+ } else { // SINGLE MACHO
+ if errors.Is(err, macho.ErrNotFat) {
+ m, err = macho.Open(machoPath)
+ if err != nil {
+ return nil
+ }
+ defer m.Close()
+ } else { // NOT a macho file
+ return nil
+ }
+ }
+ if err := handler(strings.TrimPrefix(machoPath, mountPoint), m); err != nil {
+ return fmt.Errorf("failed to handle macho %s: %w", machoPath, err)
+ }
+ }
+ return nil
+ }
+
i, err := info.Parse(ipswPath)
if err != nil {
return fmt.Errorf("failed to parse IPSW: %v", err)
@@ -116,25 +148,25 @@ func ForEachMachoInIPSW(ipswPath string, handler func(string, *macho.File) error
if fsOS, err := i.GetFileSystemOsDmg(); err == nil {
log.Info("Scanning filesystem")
- if err := scanDmg(ipswPath, fsOS, "filesystem", handler); err != nil {
+ if err := scanDmg(ipswPath, fsOS, "filesystem", scanMacho); err != nil {
return fmt.Errorf("failed to scan files in filesystem %s: %w", fsOS, err)
}
}
if systemOS, err := i.GetSystemOsDmg(); err == nil {
log.Info("Scanning SystemOS")
- if err := scanDmg(ipswPath, systemOS, "SystemOS", handler); err != nil {
+ if err := scanDmg(ipswPath, systemOS, "SystemOS", scanMacho); err != nil {
return fmt.Errorf("failed to scan files in SystemOS %s: %w", systemOS, err)
}
}
if appOS, err := i.GetAppOsDmg(); err == nil {
log.Info("Scanning AppOS")
- if err := scanDmg(ipswPath, appOS, "AppOS", handler); err != nil {
+ if err := scanDmg(ipswPath, appOS, "AppOS", scanMacho); err != nil {
return fmt.Errorf("failed to scan files in AppOS %s: %w", appOS, err)
}
}
if excOS, err := i.GetExclaveOSDmg(); err == nil {
log.Info("Scanning ExclaveOS")
- if err := scanDmg(ipswPath, excOS, "ExclaveOS", handler); err != nil {
+ if err := scanDmg(ipswPath, excOS, "ExclaveOS", scanMacho); err != nil {
return fmt.Errorf("failed to scan files in ExclaveOS %s: %w", excOS, err)
}
}
@@ -186,3 +218,72 @@ func ForEachIm4pInIPSW(ipswPath string, handler func(string, *macho.File) error)
return nil
}
+
+func ForEachPlistInIPSW(ipswPath string, directory string, handler func(string, string) error) error {
+ i, err := info.Parse(ipswPath)
+ if err != nil {
+ return fmt.Errorf("failed to parse IPSW: %v", err)
+ }
+
+ scanPlist := func(mountPoint, plistPath string) error {
+ // filter to only scan a specific directory (if provided)
+ if directory != "" && !strings.Contains(plistPath, directory) {
+ return nil
+ }
+ if strings.HasSuffix(plistPath, ".plist") {
+ // settings := make(map[string]interface{})
+ data, err := os.ReadFile(plistPath)
+ if err != nil {
+ return fmt.Errorf("failed to read plist %s: %v", plistPath, err)
+ }
+ // TODO: add support for binary plists
+ // pdata, err := plist.MarshalIndent(data, plist.XMLFormat, " ")
+ // if err != nil {
+ // return fmt.Errorf("failed to marshal plist %s: %v", plistPath, err)
+ // }
+ // if err := plist.NewDecoder(bytes.NewReader(data)).Decode(&settings); err != nil {
+ // return fmt.Errorf("failed to decode plist %s: %v", plistPath, err)
+ // }
+ // jdata, err := json.MarshalIndent(settings, "", " ")
+ // if err != nil {
+ // return fmt.Errorf("failed to marshal plist %s: %v", plistPath, err)
+ // }
+ plistPath = strings.TrimPrefix(plistPath, mountPoint)
+ plistPath, err = filepath.Rel(directory, plistPath)
+ if err != nil {
+ return fmt.Errorf("failed to get relative path for %s: %v", plistPath, err)
+ }
+ if err := handler(strings.TrimPrefix(plistPath, mountPoint), string(data)); err != nil {
+ return fmt.Errorf("failed to handle plist %s: %v", plistPath, err)
+ }
+ }
+ return nil
+ }
+
+ if fsOS, err := i.GetFileSystemOsDmg(); err == nil {
+ log.Info("Scanning filesystem")
+ if err := scanDmg(ipswPath, fsOS, "filesystem", scanPlist); err != nil {
+ return fmt.Errorf("failed to scan files in filesystem %s: %w", fsOS, err)
+ }
+ }
+ if systemOS, err := i.GetSystemOsDmg(); err == nil {
+ log.Info("Scanning SystemOS")
+ if err := scanDmg(ipswPath, systemOS, "SystemOS", scanPlist); err != nil {
+ return fmt.Errorf("failed to scan files in SystemOS %s: %w", systemOS, err)
+ }
+ }
+ if appOS, err := i.GetAppOsDmg(); err == nil {
+ log.Info("Scanning AppOS")
+ if err := scanDmg(ipswPath, appOS, "AppOS", scanPlist); err != nil {
+ return fmt.Errorf("failed to scan files in AppOS %s: %w", appOS, err)
+ }
+ }
+ if excOS, err := i.GetExclaveOSDmg(); err == nil {
+ log.Info("Scanning ExclaveOS")
+ if err := scanDmg(ipswPath, excOS, "ExclaveOS", scanPlist); err != nil {
+ return fmt.Errorf("failed to scan files in ExclaveOS %s: %w", excOS, err)
+ }
+ }
+
+ return nil
+}