Files
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

111 lines
3.9 KiB
Go

package diff
import (
mcmd "github.com/blacktop/ipsw/internal/commands/macho"
)
// buildReport assembles the stable per-section report DTO that the
// JSON output path marshals. Mount-based jobs (machos, ents, features,
// localizations, launchd, files) and the migrated top-level tasks
// (kexts, kdks, firmwares, iboot, sandbox) contribute via their
// JSON() payloads keyed by JSONKey() (duck-typed through
// [reportContributor]); the still-inlined DSC/Dylibs section comes
// straight off the [Diff] fields. The map preserves the legacy
// `omitempty` shape: zero values for each contributing field are
// dropped so the JSON output remains byte-compatible with the pre-DTO
// `json.MarshalIndent(d, …)` path modulo whitespace normalization (the
// parity tests cover both raw bytes and map round-trip equivalence).
func buildReport(d *Diff) (map[string]any, error) {
report := make(map[string]any, 13)
// Title — top-level scalar, legacy `Diff.Title` had `omitempty`.
if d.Title != "" {
report["title"] = d.Title
}
// DSC/Dylibs stays mount-bound; preserved directly off the [Diff]
// field until the DSC task migration lands.
if d.Dylibs != nil {
report["dylibs"] = d.Dylibs
}
// Migrated top-level tasks contribute through their renderers.
addMachoDiffIfNotNil(report, newKextsTask(d))
addStringIfNotEmpty(report, newKDKsTask(d))
addMachoDiffIfNotNil(report, newFirmwaresTask(d))
addIBootIfNotNil(report, newIBootTask(d))
addStringIfNotEmpty(report, newSandboxTask(d))
// Mount-based jobs route through their renderers. JSON() returns the
// underlying typed field, so the per-key zero-value guards below
// reproduce the original `omitempty` behavior.
addMapIfNotEmpty(report, newMachosRenderer(d.Machos))
addMapIfNotEmpty(report, newEntsRenderer(d.Ents))
addMapIfNotEmpty(report, newFeaturesRenderer(d.Features))
addMapIfNotEmpty(report, newLocsRenderer(d.Localizations))
addPtrIfNotNil(report, newFilesRenderer(d.Files))
addStringIfNotEmpty(report, newLaunchdRenderer(d.Launchd))
return report, nil
}
// reportContributor is the minimal renderer surface buildReport relies on.
// Each helper below narrows the JSON() return type so the `omitempty`
// equivalent guard is exact, not reflection-based.
type reportContributor interface {
JSONKey() string
JSON() any
}
// addMapIfNotEmpty embeds the renderer's payload when JSON() returns a
// non-empty map. Mirrors `omitempty` on a `map[K]V` field.
func addMapIfNotEmpty(report map[string]any, r reportContributor) {
switch v := r.JSON().(type) {
case map[string]string:
if len(v) > 0 {
report[r.JSONKey()] = v
}
case map[string]*PlistDiff:
if len(v) > 0 {
report[r.JSONKey()] = v
}
case map[string]*mcmd.MachoDiff:
if len(v) > 0 {
report[r.JSONKey()] = v
}
}
}
// addPtrIfNotNil embeds the renderer's payload when JSON() returns a
// non-nil pointer. Mirrors `omitempty` on a `*T` field.
func addPtrIfNotNil(report map[string]any, r reportContributor) {
if v, ok := r.JSON().(*FileDiff); ok && v != nil {
report[r.JSONKey()] = v
}
}
// addStringIfNotEmpty embeds the renderer's payload when JSON() returns a
// non-empty string. Mirrors `omitempty` on a `string` field.
func addStringIfNotEmpty(report map[string]any, r reportContributor) {
if s, ok := r.JSON().(string); ok && s != "" {
report[r.JSONKey()] = s
}
}
// addMachoDiffIfNotNil embeds the renderer's payload when JSON() returns
// a non-nil [*mcmd.MachoDiff]. Mirrors `omitempty` on a `*mcmd.MachoDiff`
// field.
func addMachoDiffIfNotNil(report map[string]any, r reportContributor) {
if v, ok := r.JSON().(*mcmd.MachoDiff); ok && v != nil {
report[r.JSONKey()] = v
}
}
// addIBootIfNotNil embeds the renderer's payload when JSON() returns a
// non-nil [*IBootDiff]. Mirrors `omitempty` on a `*IBootDiff` field.
func addIBootIfNotNil(report map[string]any, r reportContributor) {
if v, ok := r.JSON().(*IBootDiff); ok && v != nil {
report[r.JSONKey()] = v
}
}