package diff
import (
"context"
"html/template"
"github.com/blacktop/ipsw/internal/diff/storage"
"github.com/blacktop/ipsw/internal/search"
)
// Side identifies which IPSW (old or new) a Mach-O handler is being invoked
// against during the shared per-volume walk.
type Side string
const (
SideOld Side = "old"
SideNew Side = "new"
)
// HTMLFragment is the per-Task contribution to the final HTML report. Body
// uses html/template so renderers may compose pre-escaped fragments without
// re-escaping them at the outer template level.
type HTMLFragment struct {
Heading string
Body template.HTML
}
// MachoScanHandler is invoked once per Mach-O encountered during a shared
// per-volume walk. The same (path, *macho.File) is fed to every registered
// handler before the next binary is opened.
//
// The type is defined in internal/search so that ForEachMachoInMountMulti
// can fan handlers out without a cyclic dependency on internal/diff. Diff
// re-exports the alias here so task implementations read naturally.
type MachoScanHandler = search.MachoScanHandler
// Task is the minimal contract every diff dimension implements: a stable
// identifier used for logs, cache scoping, and ordering. Specialized
// behaviors (rendering, caching, per-volume work) are layered via the
// optional interfaces below.
//
// Report rendering is intentionally not modeled as an interface here. Each
// section is rendered by a small per-section renderer (jobs_*.go) or
// TopLevelTask (tasks_*.go) that exposes Markdown/HTML/JSON/JSONKey/Empty
// methods; buildReport duck-types them through the narrow reportContributor
// interface in report.go, and md.go / format.go call the concrete renderers
// directly. The renderers and the TopLevelTasks deliberately keep distinct
// shapes (the renderers are not Tasks), so a single shared RenderTask
// interface would not unify them.
type Task interface {
// Name returns the stable cache and log identifier for this task.
Name() string
}
// CacheableTask is implemented by tasks that participate in idempotent
// reruns. The orchestrator consults Version/OptionsHash/InputHash to build a
// storage.Scope, queries the store's completion sentinel, and on a hit calls
// Hydrate to rebuild render/report state before any rendering occurs. On a
// miss it calls persistTo after a successful fresh walk and only then
// MarkComplete(scope).
//
// Hydrate (load) and persistTo (store) are inverses and both are required:
// a task that could be marked complete without writing its rows would leave
// an empty completed scope that hydrates to bogus empty output on the next
// run. Keeping both on one interface makes that state unrepresentable.
type CacheableTask interface {
Task
// Version is bumped when the cache payload layout or output semantics
// change in a way that invalidates prior runs.
Version() int
// OptionsHash digests every output-affecting option (allow/block
// lists, verbosity, task-specific flags).
OptionsHash() string
// InputHash digests the task-scope old/new input set (e.g. relevant
// DMG or kernelcache pair digest).
InputHash() string
// Hydrate rebuilds render/report state from a cache hit so the task
// can render without re-running ProcessVolume or Parse.
Hydrate(scope storage.Scope, store storage.Store) error
// persistTo writes the task's final result rows under scope. It runs
// only after a successful fresh walk + Finalize and before MarkComplete.
persistTo(scope storage.Scope, store storage.Store) error
}
// MountTask is the subset of Task that needs per-volume mount roots.
type MountTask interface {
Task
// Needs reports whether this task wants the given volume type
// ("fs", "sys", "app", "exc").
Needs(typ string) bool
// ProcessVolume runs once per applicable volume after both sides are
// mounted. Implementations scan both roots, store per-volume diff
// results internally, and release raw scan data before returning.
ProcessVolume(typ, oldRoot, newRoot string) error
// Finalize is called once after every applicable volume has been
// processed (or skipped). Implementations aggregate per-volume
// results into the final shape consumers expect.
Finalize() error
}
// MachoWalkTask is a mount-scoped Task that participates in the shared
// per-volume Mach-O walk. The orchestrator collects handlers from every
// active task and feeds each Mach-O to all of them before moving on.
type MachoWalkTask interface {
Task
// Needs reports whether this task wants the given volume type.
Needs(typ string) bool
// BeginVolume initializes per-volume state or cache buckets.
BeginVolume(typ string) error
// MachoHandler returns the handler that consumes Mach-Os for the
// given volume and side. Returning nil opts out of that side for
// this volume.
MachoHandler(typ string, side Side) MachoScanHandler
// EndVolume runs after both sides have been walked; tasks perform
// fold/removal checks here.
EndVolume(typ string) error
// Finalize is called once after every volume has been processed.
Finalize() error
}
// MachoVolumeAborter is optionally implemented by [MachoWalkTask]s that keep
// per-volume state which must be discarded after a handler failure. The
// orchestrator calls it before suppressing the task's EndVolume hook.
type MachoVolumeAborter interface {
AbortVolume(typ string)
}
// SessionFallbackTask is optionally implemented by [MountTask]s whose absent
// side should still receive a root resolved by the mount session itself.
// mount.Session.Root("sys") falls back to the filesystem DMG on pre-cryptex
// IPSWs — exactly where the DSC lives on those builds — while the strict
// per-volume resolver (volumeResolves) deliberately has no such fallback
// because the walk-based jobs must not re-scan filesystem content under the
// SystemOS label. The orchestrator hands the session-resolved fallback root
// ONLY to tasks implementing this interface; every other active job still
// sees the absent side as an empty root.
type SessionFallbackTask interface {
// WantsSessionFallback reports whether the task wants the session-
// resolved root for a side whose typ is absent per the strict resolver.
WantsSessionFallback(typ string) bool
}
// TopLevelTask runs after all mounts have closed and needs no per-volume
// state. Kexts, KDKs, firmwares, iBoot, and sandbox parse here.
type TopLevelTask interface {
Task
// Parse runs the task against the fully populated Diff. ctx allows
// cancellation of long-running parse work.
Parse(ctx context.Context, d *Diff) error
}
// TaskSetup is the optional storage-aware setup hook.
type TaskSetup interface {
// Setup runs before the volume loop starts and receives the chosen
// store backend.
Setup(store storage.Store) error
}
// TaskCleanup is implemented by tasks that need to release resources after
// the full diff completes (regardless of success or error).
type TaskCleanup interface {
// Cleanup releases task-owned resources.
Cleanup() error
}