From 7f91c09a1ffe8fd345a2cb3bc3dcf3353a162e27 Mon Sep 17 00:00:00 2001 From: blacktop Date: Sun, 7 Jul 2024 12:16:10 -0600 Subject: [PATCH] feat: add `--feat` flag to `ipsw diff` to diff Feature Flags --- cmd/ipsw/cmd/diff.go | 3 + internal/commands/macho/patch.go | 6 +- internal/diff/diff.go | 93 ++++++++++++++++++ internal/diff/md.go | 105 ++++++++++++++++---- internal/search/search.go | 159 +++++++++++++++++++++++++------ 5 files changed, 318 insertions(+), 48 deletions(-) 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 +}