mirror of
https://github.com/blacktop/ipsw.git
synced 2026-05-08 12:22:26 +00:00
feat(diff): add sandbox profile diffing behind sandbox build tag
This commit is contained in:
@@ -50,6 +50,7 @@ func init() {
|
||||
diffCmd.Flags().Bool("low-memory", false, "Use disk caching to reduce RAM usage")
|
||||
diffCmd.Flags().StringSlice("allow-list", []string{}, "Filter MachO sections to diff (e.g. __TEXT.__text)")
|
||||
diffCmd.Flags().StringSlice("block-list", []string{}, "Remove MachO sections to diff (e.g. __TEXT.__info_plist)")
|
||||
registerDiffSandboxFlags(diffCmd)
|
||||
diffCmd.Flags().StringP("signatures", "s", "", "Path to symbolicator signatures folder")
|
||||
diffCmd.MarkFlagDirname("signatures")
|
||||
diffCmd.Flags().StringP("output", "o", "", "Folder to save diff output")
|
||||
@@ -140,6 +141,9 @@ var diffCmd = &cobra.Command{
|
||||
if len(viper.GetStringSlice("diff.kdk")) > 0 && len(viper.GetStringSlice("diff.kdk")) != 2 {
|
||||
return fmt.Errorf("you must specify two KDKs to diff; example: --kdk <KDK1> --kdk <KDK2>")
|
||||
}
|
||||
if err := diffSandboxPreflight(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d := diff.New(&diff.Config{
|
||||
Title: viper.GetString("diff.title"),
|
||||
@@ -150,6 +154,7 @@ var diffCmd = &cobra.Command{
|
||||
Firmware: viper.GetBool("diff.fw"),
|
||||
Features: viper.GetBool("diff.feat"),
|
||||
Files: viper.GetBool("diff.files"),
|
||||
Sandbox: diffSandboxEnabled(),
|
||||
CStrings: viper.GetBool("diff.strs"),
|
||||
FuncStarts: viper.GetBool("diff.starts"),
|
||||
Entitlements: viper.GetBool("diff.ent"),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build sandbox
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func registerDiffSandboxFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().Bool("sandbox", false, "Diff compiled sandbox profiles")
|
||||
viper.BindPFlag("diff.sandbox", cmd.Flags().Lookup("sandbox"))
|
||||
}
|
||||
|
||||
func diffSandboxEnabled() bool {
|
||||
return viper.GetBool("diff.sandbox")
|
||||
}
|
||||
|
||||
func diffSandboxPreflight() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//go:build !sandbox
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/blacktop/ipsw/internal/diff"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func registerDiffSandboxFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().Bool("sandbox", false, "Diff compiled sandbox profiles")
|
||||
cmd.Flags().Lookup("sandbox").Hidden = true
|
||||
viper.BindPFlag("diff.sandbox", cmd.Flags().Lookup("sandbox"))
|
||||
}
|
||||
|
||||
func diffSandboxEnabled() bool {
|
||||
return viper.GetBool("diff.sandbox")
|
||||
}
|
||||
|
||||
func diffSandboxPreflight() error {
|
||||
if !viper.GetBool("diff.sandbox") {
|
||||
return nil
|
||||
}
|
||||
return diff.ErrSandboxDiffUnavailable
|
||||
}
|
||||
@@ -77,6 +77,7 @@ type Config struct {
|
||||
Firmware bool
|
||||
Features bool
|
||||
Files bool
|
||||
Sandbox bool
|
||||
CStrings bool
|
||||
FuncStarts bool
|
||||
Entitlements bool
|
||||
@@ -137,6 +138,7 @@ type Diff struct {
|
||||
Firmwares *mcmd.MachoDiff `json:"firmwares,omitempty"`
|
||||
IBoot *IBootDiff `json:"iboot,omitempty"`
|
||||
Launchd string `json:"launchd,omitempty"`
|
||||
Sandbox string `json:"sandbox,omitempty"`
|
||||
Features *PlistDiff `json:"features,omitempty"`
|
||||
Files *FileDiff `json:"files,omitempty"`
|
||||
tmpDir string `json:"-"`
|
||||
@@ -428,6 +430,14 @@ func (d *Diff) Diff() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if d.conf.Sandbox && !directoryMode {
|
||||
log.Info("Diffing Sandbox Profiles")
|
||||
d.Sandbox, err = d.parseSandboxProfiles()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("failed to diff sandbox profiles")
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Diffing DYLD_SHARED_CACHES")
|
||||
if !directoryMode {
|
||||
if err := d.mountSystemOsDMGs(); err != nil {
|
||||
|
||||
@@ -70,6 +70,9 @@ func unsupportedFlagsForDirectoryMode(conf *Config) []string {
|
||||
if conf.LowMemory {
|
||||
unsupported = append(unsupported, "--low-memory")
|
||||
}
|
||||
if conf.Sandbox {
|
||||
unsupported = append(unsupported, "--sandbox")
|
||||
}
|
||||
return unsupported
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,9 @@ const diffMarkdownTemplate = `
|
||||
{{- end }}
|
||||
{{- if .Ents }}
|
||||
- [🔑 Entitlements](#entitlements)
|
||||
{{- end }}
|
||||
{{- if .Sandbox }}
|
||||
- [Sandbox Profiles](#sandbox-profiles)
|
||||
{{- end }}
|
||||
- [DSC](#dsc)
|
||||
- [WebKit](#webkit)
|
||||
@@ -161,6 +164,11 @@ const diffMarkdownTemplate = `
|
||||
</details>
|
||||
{{ end -}}
|
||||
|
||||
{{ if .Sandbox }}
|
||||
## Sandbox Profiles
|
||||
{{ .Sandbox | noescape }}
|
||||
{{ end -}}
|
||||
|
||||
{{- if .Firmwares }}
|
||||
## Firmwares
|
||||
{{ if .Firmwares.New }}
|
||||
@@ -312,6 +320,7 @@ type diffHTMLPageData struct {
|
||||
KDKs template.HTML
|
||||
Machos *htmlMachoDiff
|
||||
Ents template.HTML
|
||||
Sandbox template.HTML
|
||||
Firmwares *htmlMachoDiff
|
||||
IBoot *htmlIBootDiff
|
||||
Launchd template.HTML
|
||||
@@ -735,6 +744,7 @@ const diffHTMLPageTemplate = `<!DOCTYPE html>
|
||||
</li>
|
||||
{{- end }}
|
||||
{{- if .Launchd }}<li><a href="#launchd">launchd Config</a></li>{{ end }}
|
||||
{{- if .Sandbox }}<li><a href="#sandbox-profiles">Sandbox Profiles</a></li>{{ end }}
|
||||
<li><a href="#dsc">DSC</a>
|
||||
<ul>
|
||||
{{- if .OldWebkit }}<li><a href="#webkit">WebKit</a></li>{{ end }}
|
||||
@@ -862,6 +872,11 @@ const diffHTMLPageTemplate = `<!DOCTYPE html>
|
||||
{{ .Launchd }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Sandbox }}
|
||||
<h2 id="sandbox-profiles">Sandbox Profiles</h2>
|
||||
{{ .Sandbox }}
|
||||
{{- end }}
|
||||
|
||||
<h2 id="dsc">DSC</h2>
|
||||
{{- if .OldWebkit }}
|
||||
<h3 id="webkit">WebKit</h3>
|
||||
@@ -1261,6 +1276,7 @@ func (d *Diff) renderHTML() (string, error) {
|
||||
Features: convertPlistDiff(d.Features),
|
||||
Files: convertFileDiff(d.Files),
|
||||
Ents: renderMarkdownFragment(d.Ents),
|
||||
Sandbox: renderMarkdownFragment(d.Sandbox),
|
||||
KDKs: renderMarkdownFragment(d.KDKs),
|
||||
Launchd: renderMarkdownFragment(d.Launchd),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package diff
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -163,6 +164,91 @@ func TestRenderHTMLIncludesIBootFilesAndFeatureFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderHTMLIncludesSandboxProfiles(t *testing.T) {
|
||||
d := newHTMLTestDiff("Test Diff")
|
||||
d.Sandbox = "### Collection\n\n#### Changed (1)\n\n##### locationd\n\n```diff\n-(deny default)\n+(allow default)\n```\n"
|
||||
|
||||
rendered := mustRenderHTML(t, d)
|
||||
|
||||
for _, needle := range []string{
|
||||
`href="#sandbox-profiles"`,
|
||||
`id="sandbox-profiles"`,
|
||||
`locationd`,
|
||||
`class="diff-add"`,
|
||||
`class="diff-del"`,
|
||||
} {
|
||||
if !strings.Contains(rendered, needle) {
|
||||
t.Fatalf("rendered HTML missing %q", needle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringIncludesSandboxProfiles(t *testing.T) {
|
||||
d := newHTMLTestDiff("Test Diff")
|
||||
d.Sandbox = "### Collection\n\n#### New (1)\n\n##### locationd\n\n```scheme\n(version 1)\n```\n"
|
||||
|
||||
rendered := d.String()
|
||||
|
||||
for _, needle := range []string{
|
||||
"## Sandbox Profiles",
|
||||
"### Collection",
|
||||
"##### locationd",
|
||||
} {
|
||||
if !strings.Contains(rendered, needle) {
|
||||
t.Fatalf("rendered Markdown missing %q", needle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONIncludesSandboxProfiles(t *testing.T) {
|
||||
d := newHTMLTestDiff("Test Diff")
|
||||
d.Sandbox = "### Collection\n\n#### Changed (1)\n"
|
||||
|
||||
data, err := json.Marshal(d)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal returned error: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), `"sandbox"`) {
|
||||
t.Fatalf("JSON missing sandbox field: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderSandboxProfileDiffMarkdown(t *testing.T) {
|
||||
oldDocs := sandboxProfileDocuments{
|
||||
sandboxDiffSourceCollection: {
|
||||
"locationd": "(version 1)\n(deny default)\n",
|
||||
"removed": "(version 1)\n",
|
||||
},
|
||||
}
|
||||
newDocs := sandboxProfileDocuments{
|
||||
sandboxDiffSourceCollection: {
|
||||
"locationd": "(version 1)\n(allow default)\n",
|
||||
"added": "(version 1)\n(deny default)\n",
|
||||
},
|
||||
}
|
||||
|
||||
rendered, err := renderSandboxProfileDiffMarkdown(oldDocs, newDocs)
|
||||
if err != nil {
|
||||
t.Fatalf("renderSandboxProfileDiffMarkdown returned error: %v", err)
|
||||
}
|
||||
|
||||
for _, needle := range []string{
|
||||
"### Collection",
|
||||
"#### New (1)",
|
||||
"##### added",
|
||||
"#### Removed (1)",
|
||||
"##### removed",
|
||||
"#### Changed (1)",
|
||||
"##### locationd",
|
||||
"+(allow default)",
|
||||
"-(deny default)",
|
||||
} {
|
||||
if !strings.Contains(rendered, needle) {
|
||||
t.Fatalf("rendered sandbox diff missing %q:\n%s", needle, rendered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTitleToFilenameSanitizesDiffReportName(t *testing.T) {
|
||||
d := New(&Config{
|
||||
Title: "23D8133__iPhone17,1 .vs 23D771330a__iPhone17,1",
|
||||
|
||||
@@ -219,6 +219,21 @@ func (d *Diff) Markdown() error {
|
||||
out.WriteString(fmt.Sprintf("- [%s](%s)\n\n", "Entitlements DIFF", "Entitlements.md"))
|
||||
}
|
||||
|
||||
// SUB-SECTION: Sandbox
|
||||
if len(d.Sandbox) > 0 {
|
||||
out.WriteString("### Sandbox Profiles\n\n")
|
||||
fname := filepath.Join(d.conf.Output, "Sandbox.md")
|
||||
log.Debugf("Creating diff Sandbox Markdown: %s", fname)
|
||||
f, err := os.Create(fname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create diff Sandbox Markdown: %w", err)
|
||||
}
|
||||
fmt.Fprintf(f, "## Sandbox Profiles\n\n")
|
||||
fmt.Fprintf(f, "%s", d.Sandbox)
|
||||
f.Close()
|
||||
out.WriteString(fmt.Sprintf("- [%s](%s)\n\n", "Sandbox Profiles DIFF", "Sandbox.md"))
|
||||
}
|
||||
|
||||
// SECTION: Firmware
|
||||
if d.Firmwares != nil && (len(d.Firmwares.New) > 0 || len(d.Firmwares.Removed) > 0 || len(d.Firmwares.Updated) > 0) {
|
||||
out.WriteString("## Firmware\n\n")
|
||||
|
||||
@@ -36,6 +36,7 @@ func TestUnsupportedFlagsForOTAMode(t *testing.T) {
|
||||
Features: true,
|
||||
Entitlements: true,
|
||||
Files: true,
|
||||
Sandbox: true,
|
||||
CStrings: true,
|
||||
},
|
||||
expected: nil,
|
||||
@@ -83,6 +84,11 @@ func TestUnsupportedFlagsForDirectoryMode(t *testing.T) {
|
||||
conf: Config{LowMemory: true},
|
||||
expected: []string{"--low-memory"},
|
||||
},
|
||||
{
|
||||
name: "sandbox blocked",
|
||||
conf: Config{Sandbox: true},
|
||||
expected: []string{"--sandbox"},
|
||||
},
|
||||
{
|
||||
name: "supported flags not blocked",
|
||||
conf: Config{
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
//go:build sandbox
|
||||
|
||||
package diff
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/blacktop/go-macho"
|
||||
"github.com/blacktop/go-macho/pkg/fixupchains"
|
||||
"github.com/blacktop/go-macho/types"
|
||||
"github.com/blacktop/ipsw/pkg/sandbox"
|
||||
"github.com/blacktop/ipsw/pkg/sandbox/normalize"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSandboxDiffNormalizedNodes = 1000
|
||||
maxSandboxDiffOutputBytes = 32 << 20
|
||||
)
|
||||
|
||||
func (d *Diff) parseSandboxProfiles() (string, error) {
|
||||
oldDocs, err := collectSandboxProfileDocuments(&d.Old)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("old sandbox profiles: %w", err)
|
||||
}
|
||||
newDocs, err := collectSandboxProfileDocuments(&d.New)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new sandbox profiles: %w", err)
|
||||
}
|
||||
if len(oldDocs) == 0 && len(newDocs) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return renderSandboxProfileDiffMarkdown(oldDocs, newDocs)
|
||||
}
|
||||
|
||||
func collectSandboxProfileDocuments(ctx *Context) (sandboxProfileDocuments, error) {
|
||||
if ctx.Kernel.Path == "" {
|
||||
return nil, fmt.Errorf("kernelcache path is empty")
|
||||
}
|
||||
|
||||
kernel, err := macho.Open(ctx.Kernel.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open kernelcache: %w", err)
|
||||
}
|
||||
defer kernel.Close()
|
||||
|
||||
var fixups map[uint64]uint64
|
||||
if kernel.FileTOC.FileHeader.Type == types.MH_FILESET {
|
||||
fixups, err = buildSandboxDiffFixupMap(kernel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build fileset fixup map: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
out := make(sandboxProfileDocuments)
|
||||
for _, source := range sandboxDiffSourceOrder {
|
||||
profiles, err := renderSandboxSourceProfiles(kernel, fixups, source)
|
||||
if err != nil {
|
||||
if isSandboxSourceUnavailable(err) {
|
||||
log.WithError(err).Debugf("skipping unavailable %s sandbox source", source)
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("%s: %w", source, err)
|
||||
}
|
||||
if len(profiles) > 0 {
|
||||
out[source] = profiles
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func renderSandboxSourceProfiles(kernel *macho.File, fixups map[uint64]uint64, source string) (map[string]string, error) {
|
||||
conf := &sandbox.Config{Kernel: kernel}
|
||||
if fixups != nil {
|
||||
conf.Fixups = fixups
|
||||
}
|
||||
|
||||
sbObj, err := sandbox.NewSandbox(conf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create sandbox parser: %w", err)
|
||||
}
|
||||
|
||||
switch source {
|
||||
case sandboxDiffSourceCollection:
|
||||
if _, err := sbObj.GetCollectionData(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load collection data: %w", err)
|
||||
}
|
||||
if err := sbObj.ParseSandboxCollection(); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse collection data: %w", err)
|
||||
}
|
||||
case sandboxDiffSourceProtobox:
|
||||
if _, err := sbObj.GetProtoboxCollectionData(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load protobox data: %w", err)
|
||||
}
|
||||
if err := sbObj.ParseProtoboxCollection(); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse protobox data: %w", err)
|
||||
}
|
||||
case sandboxDiffSourceProfile:
|
||||
if _, err := sbObj.GetPlatformProfileData(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load platform profile data: %w", err)
|
||||
}
|
||||
if err := sbObj.ParseSandboxProfile(); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse platform profile data: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported sandbox source %q", source)
|
||||
}
|
||||
|
||||
return renderSandboxProfiles(sbObj, source)
|
||||
}
|
||||
|
||||
func renderSandboxProfiles(sbObj *sandbox.Sandbox, source string) (map[string]string, error) {
|
||||
out := make(map[string]string, len(sbObj.Profiles))
|
||||
limit := maxSandboxDiffNormalizedNodes
|
||||
if source == sandboxDiffSourceProfile {
|
||||
// 0 disables the per-operation node budget. The platform profile is
|
||||
// one large standalone document, unlike collection profile entries.
|
||||
limit = 0
|
||||
}
|
||||
|
||||
for idx, prof := range sbObj.Profiles {
|
||||
name := sandboxProfileDocumentName(source, prof, idx)
|
||||
formatted, diags, err := normalize.FormatCompilerSafeProfileWithDiagnostics(
|
||||
sbObj,
|
||||
prof,
|
||||
limit,
|
||||
maxSandboxDiffOutputBytes,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to render %s: %w", name, err)
|
||||
}
|
||||
if strings.TrimSpace(formatted) == "" {
|
||||
continue
|
||||
}
|
||||
if len(diags) > 0 {
|
||||
log.Warnf("%s/%s: %d sandbox operation(s) skipped due to budget limits",
|
||||
source, name, len(diags))
|
||||
}
|
||||
if _, exists := out[name]; exists {
|
||||
name = uniqueSandboxProfileDocumentName(out, name, idx)
|
||||
}
|
||||
out[name] = formatted
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func sandboxProfileDocumentName(source string, prof sandbox.Profile, idx int) string {
|
||||
if prof.Name != "" {
|
||||
return prof.Name
|
||||
}
|
||||
if source == sandboxDiffSourceProfile {
|
||||
return "platform"
|
||||
}
|
||||
return fmt.Sprintf("profile_%03d", idx)
|
||||
}
|
||||
|
||||
func uniqueSandboxProfileDocumentName(existing map[string]string, name string, idx int) string {
|
||||
candidate := fmt.Sprintf("%s#%d", name, idx)
|
||||
for suffix := 2; ; suffix++ {
|
||||
if _, exists := existing[candidate]; !exists {
|
||||
return candidate
|
||||
}
|
||||
candidate = fmt.Sprintf("%s#%d.%d", name, idx, suffix)
|
||||
}
|
||||
}
|
||||
|
||||
func isSandboxSourceUnavailable(err error) bool {
|
||||
return errors.Is(err, sandbox.ErrSandboxSourceUnavailable)
|
||||
}
|
||||
|
||||
func buildSandboxDiffFixupMap(kernel *macho.File) (map[uint64]uint64, error) {
|
||||
fixups := make(map[uint64]uint64)
|
||||
if !kernel.HasFixups() {
|
||||
return fixups, nil
|
||||
}
|
||||
dcf, err := kernel.DyldChainedFixups()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, start := range dcf.Starts {
|
||||
if start.PageStarts == nil {
|
||||
continue
|
||||
}
|
||||
for _, fixup := range start.Fixups {
|
||||
if rebase, ok := fixup.(fixupchains.Rebase); ok {
|
||||
fixups[rebase.Raw()] = uint64(rebase.Offset()) + kernel.GetBaseAddress()
|
||||
}
|
||||
}
|
||||
}
|
||||
return fixups, nil
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package diff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/blacktop/ipsw/internal/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
sandboxDiffSourceCollection = "collection"
|
||||
sandboxDiffSourceProtobox = "protobox"
|
||||
sandboxDiffSourceProfile = "profile"
|
||||
)
|
||||
|
||||
var sandboxDiffSourceOrder = []string{
|
||||
sandboxDiffSourceCollection,
|
||||
sandboxDiffSourceProtobox,
|
||||
sandboxDiffSourceProfile,
|
||||
}
|
||||
|
||||
type sandboxProfileDocuments map[string]map[string]string
|
||||
|
||||
func renderSandboxProfileDiffMarkdown(oldDocs, newDocs sandboxProfileDocuments) (string, error) {
|
||||
var b strings.Builder
|
||||
|
||||
for _, source := range sandboxProfileSourceNames(oldDocs, newDocs) {
|
||||
oldProfiles := oldDocs[source]
|
||||
newProfiles := newDocs[source]
|
||||
section, err := renderSandboxSourceDiffMarkdown(source, oldProfiles, newProfiles)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if section == "" {
|
||||
continue
|
||||
}
|
||||
if b.Len() > 0 {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
b.WriteString(section)
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func sandboxProfileSourceNames(oldDocs, newDocs sandboxProfileDocuments) []string {
|
||||
seen := make(map[string]bool)
|
||||
var names []string
|
||||
for _, source := range sandboxDiffSourceOrder {
|
||||
if len(oldDocs[source]) > 0 || len(newDocs[source]) > 0 {
|
||||
names = append(names, source)
|
||||
seen[source] = true
|
||||
}
|
||||
}
|
||||
|
||||
var extras []string
|
||||
for source := range oldDocs {
|
||||
if !seen[source] {
|
||||
extras = append(extras, source)
|
||||
seen[source] = true
|
||||
}
|
||||
}
|
||||
for source := range newDocs {
|
||||
if !seen[source] {
|
||||
extras = append(extras, source)
|
||||
}
|
||||
}
|
||||
slices.Sort(extras)
|
||||
return append(names, extras...)
|
||||
}
|
||||
|
||||
func renderSandboxSourceDiffMarkdown(source string, oldProfiles, newProfiles map[string]string) (string, error) {
|
||||
var removed []string
|
||||
var added []string
|
||||
var changed []string
|
||||
|
||||
for name := range oldProfiles {
|
||||
if _, ok := newProfiles[name]; !ok {
|
||||
removed = append(removed, name)
|
||||
}
|
||||
}
|
||||
for name := range newProfiles {
|
||||
oldText, ok := oldProfiles[name]
|
||||
newText := newProfiles[name]
|
||||
switch {
|
||||
case !ok:
|
||||
added = append(added, name)
|
||||
case oldText != newText:
|
||||
changed = append(changed, name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(removed) == 0 && len(added) == 0 && len(changed) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
slices.Sort(removed)
|
||||
slices.Sort(added)
|
||||
slices.Sort(changed)
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "### %s\n\n", sandboxSourceTitle(source))
|
||||
|
||||
if len(added) > 0 {
|
||||
fmt.Fprintf(&b, "#### New (%d)\n\n", len(added))
|
||||
for _, name := range added {
|
||||
writeSandboxProfileDocument(&b, name, newProfiles[name])
|
||||
}
|
||||
}
|
||||
|
||||
if len(removed) > 0 {
|
||||
fmt.Fprintf(&b, "#### Removed (%d)\n\n", len(removed))
|
||||
for _, name := range removed {
|
||||
writeSandboxProfileDocument(&b, name, oldProfiles[name])
|
||||
}
|
||||
}
|
||||
|
||||
if len(changed) > 0 {
|
||||
fmt.Fprintf(&b, "#### Changed (%d)\n\n", len(changed))
|
||||
for _, name := range changed {
|
||||
out, err := utils.GitDiff(
|
||||
ensureTrailingNewline(oldProfiles[name]),
|
||||
ensureTrailingNewline(newProfiles[name]),
|
||||
&utils.GitDiffConfig{Color: false, Tool: "git"},
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to diff sandbox profile %s: %w", name, err)
|
||||
}
|
||||
if strings.TrimSpace(out) == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&b, "##### %s\n\n```diff\n%s\n```\n\n", name, strings.TrimRight(out, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimRight(b.String(), "\n") + "\n", nil
|
||||
}
|
||||
|
||||
func sandboxSourceTitle(source string) string {
|
||||
switch source {
|
||||
case sandboxDiffSourceCollection:
|
||||
return "Collection"
|
||||
case sandboxDiffSourceProtobox:
|
||||
return "Protobox/Autobox"
|
||||
case sandboxDiffSourceProfile:
|
||||
return "Platform Profile"
|
||||
default:
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
func writeSandboxProfileDocument(b *strings.Builder, name, body string) {
|
||||
fmt.Fprintf(b, "##### %s\n\n```scheme\n%s\n```\n\n", name, strings.TrimRight(body, "\n"))
|
||||
}
|
||||
|
||||
func ensureTrailingNewline(value string) string {
|
||||
if strings.HasSuffix(value, "\n") {
|
||||
return value
|
||||
}
|
||||
return value + "\n"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !sandbox
|
||||
|
||||
package diff
|
||||
|
||||
func (d *Diff) parseSandboxProfiles() (string, error) {
|
||||
return "", ErrSandboxDiffUnavailable
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build !sandbox
|
||||
|
||||
package diff
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseSandboxProfilesStubReturnsUnavailable(t *testing.T) {
|
||||
_, err := (&Diff{}).parseSandboxProfiles()
|
||||
if !errors.Is(err, ErrSandboxDiffUnavailable) {
|
||||
t.Fatalf("parseSandboxProfiles() error = %v, want %v", err, ErrSandboxDiffUnavailable)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package diff
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrSandboxDiffUnavailable = errors.New("sandbox diff support is not built; rebuild with -tags sandbox")
|
||||
@@ -0,0 +1,34 @@
|
||||
//go:build sandbox
|
||||
|
||||
package diff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/blacktop/ipsw/pkg/sandbox"
|
||||
"github.com/blacktop/ipsw/pkg/sandbox/normalize"
|
||||
)
|
||||
|
||||
func TestIsSandboxSourceUnavailableUsesSentinel(t *testing.T) {
|
||||
err := fmt.Errorf("failed to load collection data: %w", sandbox.ErrSandboxSourceUnavailable)
|
||||
if !isSandboxSourceUnavailable(err) {
|
||||
t.Fatal("expected sandbox source unavailable sentinel to be skipped")
|
||||
}
|
||||
|
||||
if isSandboxSourceUnavailable(normalize.ErrFormattedOutputTooLarge) {
|
||||
t.Fatal("formatter budget errors must not be treated as unavailable sources")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueSandboxProfileDocumentNameAvoidsExistingSuffix(t *testing.T) {
|
||||
existing := map[string]string{
|
||||
"profile": "first",
|
||||
"profile#1": "second",
|
||||
}
|
||||
|
||||
got := uniqueSandboxProfileDocumentName(existing, "profile", 1)
|
||||
if got != "profile#1.2" {
|
||||
t.Fatalf("uniqueSandboxProfileDocumentName() = %q, want %q", got, "profile#1.2")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user