mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
Refactor diff jobs into task-owned renderers with persistent cache support, root-confined walkers, and quieter Mach-O/Firmware output.
1716 lines
53 KiB
Go
1716 lines
53 KiB
Go
package diff
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"html"
|
||
"html/template"
|
||
"maps"
|
||
"os"
|
||
"path/filepath"
|
||
"slices"
|
||
"strings"
|
||
|
||
"github.com/apex/log"
|
||
mcmd "github.com/blacktop/ipsw/internal/commands/macho"
|
||
"github.com/blacktop/ipsw/internal/utils"
|
||
"github.com/gomarkdown/markdown"
|
||
mdhtml "github.com/gomarkdown/markdown/html"
|
||
)
|
||
|
||
// {{ range $index, $element := .Ents }}
|
||
// - [{{ $element }}](#{{ $element | urlize }})
|
||
// {{ end -}}
|
||
|
||
const diffMarkdownTemplate = `
|
||
- [{{ .Title }}](#{{ .Title | slug }})
|
||
- [Inputs](#inputs)
|
||
- [Kernel](#kernel)
|
||
- [Version](#version)
|
||
{{- if .Kexts }}
|
||
- [Kexts](#kexts)
|
||
{{- if .Kexts.New }}
|
||
- [🆕 NEW ({{ len .Kexts.New }})](#-new)
|
||
{{- end }}
|
||
{{- if .Kexts.Removed }}
|
||
- [❌ Removed ({{ len .Kexts.Removed }})](#-removed)
|
||
{{- end }}
|
||
{{- if .Kexts.Updated }}
|
||
- [⬆️ Updated ({{ len .Kexts.Updated }})](#️-updated)
|
||
{{- end }}
|
||
{{- end }}
|
||
{{- if .Machos }}
|
||
- [Machos](#machos)
|
||
{{- end }}
|
||
{{- if .Ents }}
|
||
- [🔑 Entitlements](#entitlements)
|
||
{{- end }}
|
||
{{- if .Sandbox }}
|
||
- [Sandbox Profiles](#sandbox-profiles)
|
||
{{- end }}
|
||
- [DSC](#dsc)
|
||
- [WebKit](#webkit)
|
||
- [Dylibs](#dylibs)
|
||
{{- if .Dylibs }}
|
||
{{- if .Dylibs.New }}
|
||
- [🆕 NEW ({{ len .Dylibs.New }})](#-new-2)
|
||
{{- end }}
|
||
{{- if .Dylibs.Removed }}
|
||
- [❌ Removed ({{ len .Dylibs.Removed }})](#️-removed-2)
|
||
{{- end }}
|
||
{{- if .Dylibs.Updated }}
|
||
- [⬆️ Updated ({{ len .Dylibs.Updated }})](#️-updated-2)
|
||
{{- end }}
|
||
{{- end }}
|
||
|
||
|
||
# {{ .Title }}
|
||
|
||
## Inputs
|
||
|
||
- {{ .Old.IPSWPath | base }}
|
||
- {{ .New.IPSWPath | base }}
|
||
|
||
## Kernel
|
||
{{ if .Old.Kernel.Version }}
|
||
### Version
|
||
|
||
| iOS | Version | Build | Date |
|
||
| :-------------------------------------- | :-------------------------------------- | :----------------------------------- | :------------------------------------ |
|
||
| {{ .Old.Version }} *({{ .Old.Build }})* | {{ .Old.Kernel.Version.KernelVersion.Darwin }} | {{ .Old.Kernel.Version.KernelVersion.XNU }} | {{ .Old.Kernel.Version.KernelVersion.Date.Format "Mon, 02Jan2006 15:04:05 MST" }} |
|
||
| {{ .New.Version }} *({{ .New.Build }})* | {{ .New.Kernel.Version.KernelVersion.Darwin }} | {{ .New.Kernel.Version.KernelVersion.XNU }} | {{ .New.Kernel.Version.KernelVersion.Date.Format "Mon, 02Jan2006 15:04:05 MST" }} |
|
||
{{ end -}}
|
||
{{ if .Kexts }}
|
||
### Kexts
|
||
{{ if .Kexts.New }}
|
||
### 🆕 NEW
|
||
{{ range .Kexts.New }}
|
||
- {{ . | code }}
|
||
{{ end }}
|
||
{{ end -}}
|
||
{{- if .Kexts.Removed }}
|
||
### ❌ Removed
|
||
{{ range .Kexts.Removed }}
|
||
- {{ . | code }}
|
||
{{ end }}
|
||
{{ end -}}
|
||
{{- if .Kexts.Updated }}
|
||
### ⬆️ Updated
|
||
<details>
|
||
<summary><i>View Updated</i></summary>
|
||
|
||
{{ range $key, $value := .Kexts.Updated }}
|
||
#### {{ $key | base }}
|
||
> {{ $key | code }}
|
||
{{ $value | noescape }}
|
||
{{ end }}
|
||
|
||
</details>
|
||
{{ end -}}
|
||
{{ end -}}
|
||
{{ if .KDKs }}
|
||
## KDKs
|
||
- {{ .Old.KDK | code}}
|
||
- {{ .New.KDK | code}}
|
||
|
||
{{ .KDKs | noescape }}
|
||
{{ end -}}
|
||
|
||
{{- if .Machos }}
|
||
## MachOs
|
||
{{- range machoVolumes .Machos }}
|
||
### {{ .Name }}
|
||
{{ if .Diff.New }}
|
||
#### 🆕 NEW
|
||
{{ range .Diff.New }}
|
||
- {{ . | code }}
|
||
{{ end }}
|
||
{{ end -}}
|
||
{{- if .Diff.Removed }}
|
||
#### ❌ Removed
|
||
{{ range .Diff.Removed }}
|
||
- {{ . | code }}
|
||
{{ end }}
|
||
{{ end -}}
|
||
{{- if .Diff.Updated }}
|
||
#### ⬆️ Updated
|
||
<details>
|
||
<summary><i>View Updated</i></summary>
|
||
|
||
{{ range $key, $value := .Diff.Updated }}
|
||
##### {{ $key | base }}
|
||
> {{ $key | code }}
|
||
{{ $value | noescape }}
|
||
{{ end }}
|
||
|
||
</details>
|
||
{{ end -}}
|
||
{{- end }}
|
||
{{- end }}
|
||
{{ if .Ents }}
|
||
### 🔑 Entitlements
|
||
{{- range entsVolumes .Ents }}
|
||
#### {{ .Name }}
|
||
<details>
|
||
<summary><i>View Entitlements</i></summary>
|
||
|
||
{{ .Content | noescape }}
|
||
|
||
</details>
|
||
{{- end }}
|
||
{{ end -}}
|
||
|
||
{{ if .Sandbox }}
|
||
## Sandbox Profiles
|
||
{{ .Sandbox | noescape }}
|
||
{{ end -}}
|
||
|
||
{{- if .Firmwares }}
|
||
## Firmwares
|
||
{{ if .Firmwares.New }}
|
||
### 🆕 NEW
|
||
{{ range .Firmwares.New }}
|
||
- {{ . | code }}
|
||
{{ end }}
|
||
{{ end -}}
|
||
{{- if .Firmwares.Removed }}
|
||
### ❌ Removed
|
||
{{ range .Firmwares.Removed }}
|
||
- {{ . | code }}
|
||
{{ end }}
|
||
{{ end -}}
|
||
{{- if .Firmwares.Updated }}
|
||
### ⬆️ Updated
|
||
<details>
|
||
<summary><i>View Updated</i></summary>
|
||
|
||
{{ range $key, $value := .Firmwares.Updated }}
|
||
#### {{ $key | base }}
|
||
> {{ $key | code }}
|
||
{{ $value | noescape }}
|
||
{{ end }}
|
||
|
||
</details>
|
||
{{ end -}}
|
||
{{ end -}}
|
||
|
||
{{ if .Launchd }}
|
||
## launchd Config
|
||
{{ .Launchd | noescape }}
|
||
{{ end }}
|
||
|
||
{{ if .Localizations }}
|
||
## Localizations
|
||
{{- range plistVolumes .Localizations }}
|
||
### {{ .Name }}
|
||
{{ if .Diff.New }}
|
||
#### 🆕 NEW
|
||
{{ range $key, $value := .Diff.New }}
|
||
##### {{ $key | locbase }}
|
||
> {{ $key | code }}
|
||
~~~text
|
||
{{ $value }}
|
||
~~~
|
||
{{ end }}
|
||
{{ end -}}
|
||
{{- if .Diff.Removed }}
|
||
#### ❌ Removed
|
||
{{ range .Diff.Removed }}
|
||
- {{ . | code }}
|
||
{{ end }}
|
||
{{ end -}}
|
||
{{- if .Diff.Updated }}
|
||
#### ⬆️ Updated
|
||
<details>
|
||
<summary><i>View Updated</i></summary>
|
||
|
||
{{ range $key, $value := .Diff.Updated }}
|
||
##### {{ $key | locbase }}
|
||
> {{ $key | code }}
|
||
{{ $value | noescape }}
|
||
{{ end }}
|
||
|
||
</details>
|
||
{{ end -}}
|
||
{{- end }}
|
||
{{- end }}
|
||
|
||
## DSC
|
||
|
||
{{ if .Old.Webkit }}
|
||
### WebKit
|
||
|
||
| iOS | Version |
|
||
| :-------------------------------------- | :---------------- |
|
||
| {{ .Old.Version }} *({{ .Old.Build }})* | {{ .Old.Webkit }} |
|
||
| {{ .New.Version }} *({{ .New.Build }})* | {{ .New.Webkit }} |
|
||
{{ end -}}
|
||
{{- if .Dylibs }}
|
||
### Dylibs
|
||
{{ if .Dylibs.New }}
|
||
#### 🆕 NEW
|
||
{{ range .Dylibs.New }}
|
||
- {{ . | code }}
|
||
{{ end }}
|
||
{{ end -}}
|
||
{{- if .Dylibs.Removed }}
|
||
#### ❌ Removed
|
||
{{ range .Dylibs.Removed }}
|
||
- {{ . | code }}
|
||
{{ end }}
|
||
{{ end -}}
|
||
{{- if .Dylibs.Updated }}
|
||
#### ⬆️ Updated
|
||
<details>
|
||
<summary><i>View Updated</i></summary>
|
||
|
||
{{ range $key, $value := .Dylibs.Updated }}
|
||
##### {{ $key | base }}
|
||
> {{ $key | code }}
|
||
{{ $value | noescape }}
|
||
{{ end }}
|
||
|
||
</details>
|
||
{{ end -}}
|
||
{{ end -}}
|
||
`
|
||
|
||
var diffMDTmpl = template.Must(template.New("diff").
|
||
Funcs(template.FuncMap{
|
||
"noescape": func(value any) template.HTML {
|
||
return template.HTML(fmt.Sprint(value))
|
||
},
|
||
"base": func(value string) template.HTML {
|
||
return template.HTML(fmt.Sprintf("`%s`", filepath.Base(value)))
|
||
},
|
||
"locbase": func(value string) template.HTML {
|
||
return template.HTML(fmt.Sprintf("`%s`", localizationDisplayName(value)))
|
||
},
|
||
"code": func(value string) template.HTML {
|
||
return template.HTML(fmt.Sprintf("`%s`", value))
|
||
},
|
||
"slug": func(value string) string {
|
||
return utils.Slugify(value)
|
||
},
|
||
"machoVolumes": templateMachoVolumes,
|
||
"plistVolumes": templatePlistVolumes,
|
||
"entsVolumes": templateEntsVolumes,
|
||
}).
|
||
Parse(diffMarkdownTemplate))
|
||
|
||
// templateMachoVolumes returns the sorted-and-filtered per-volume MachOs
|
||
// entries for use inside Markdown/HTML templates. It mirrors the slice that
|
||
// the HTML page renderer pre-builds (convertMachoVolumeDiff) so both
|
||
// renderers walk the same ordered structure.
|
||
func templateMachoVolumes(m map[string]*mcmd.MachoDiff) []templateMachoVolume {
|
||
if len(m) == 0 {
|
||
return nil
|
||
}
|
||
out := make([]templateMachoVolume, 0, len(m))
|
||
for _, vol := range sortedVolumeKeys(m) {
|
||
md := m[vol]
|
||
if !machoDiffHasContent(md) {
|
||
continue
|
||
}
|
||
out = append(out, templateMachoVolume{Name: vol, Diff: md})
|
||
}
|
||
return out
|
||
}
|
||
|
||
type templateMachoVolume struct {
|
||
Name string
|
||
Diff *mcmd.MachoDiff
|
||
}
|
||
|
||
func templatePlistVolumes(m map[string]*PlistDiff) []templatePlistVolume {
|
||
if len(m) == 0 {
|
||
return nil
|
||
}
|
||
out := make([]templatePlistVolume, 0, len(m))
|
||
for _, vol := range sortedVolumeKeys(m) {
|
||
pd := m[vol]
|
||
if !plistDiffHasContent(pd) {
|
||
continue
|
||
}
|
||
out = append(out, templatePlistVolume{Name: vol, Diff: pd})
|
||
}
|
||
return out
|
||
}
|
||
|
||
type templatePlistVolume struct {
|
||
Name string
|
||
Diff *PlistDiff
|
||
}
|
||
|
||
func templateEntsVolumes(m map[string]string) []templateEntsVolume {
|
||
if len(m) == 0 {
|
||
return nil
|
||
}
|
||
keys := make([]string, 0, len(m))
|
||
for k := range m {
|
||
keys = append(keys, k)
|
||
}
|
||
keys = sortVolumeNames(keys)
|
||
out := make([]templateEntsVolume, 0, len(keys))
|
||
for _, vol := range keys {
|
||
rendered := m[vol]
|
||
if !entitlementsDiffHasContent(rendered) {
|
||
continue
|
||
}
|
||
out = append(out, templateEntsVolume{Name: vol, Content: rendered})
|
||
}
|
||
return out
|
||
}
|
||
|
||
type templateEntsVolume struct {
|
||
Name string
|
||
Content string
|
||
}
|
||
|
||
// htmlTaskFuncs is the FuncMap shared between the outer page template and
|
||
// the per-task sub-templates that render in-scope sections.
|
||
var htmlTaskFuncs = template.FuncMap{
|
||
"dict": func(pairs ...any) map[string]any {
|
||
m := make(map[string]any, len(pairs)/2)
|
||
for i := 0; i < len(pairs)-1; i += 2 {
|
||
m[pairs[i].(string)] = pairs[i+1]
|
||
}
|
||
return m
|
||
},
|
||
"slug": utils.Slugify,
|
||
}
|
||
|
||
var diffHTMLTmpl = template.Must(template.New("diff-html-page").
|
||
Funcs(htmlTaskFuncs).
|
||
Parse(diffHTMLPageTemplate))
|
||
|
||
// htmlTaskPartials defines the shared sub-templates that per-task HTML
|
||
// renderers compose into. machoDiffSection and plistDiffSection are the
|
||
// same blocks the outer page template uses, factored so per-task templates
|
||
// can produce byte-identical output without duplicating their bodies.
|
||
const htmlTaskPartials = `
|
||
{{ define "machoDiffSection" }}
|
||
{{- if .Diff.New }}
|
||
<h4 id="{{ .Prefix }}-new">New</h4>
|
||
<ul>{{ range .Diff.New }}<li><code>{{ . }}</code></li>{{ end }}</ul>
|
||
{{- end }}
|
||
{{- if .Diff.Removed }}
|
||
<h4 id="{{ .Prefix }}-removed">Removed</h4>
|
||
<ul>{{ range .Diff.Removed }}<li><code>{{ . }}</code></li>{{ end }}</ul>
|
||
{{- end }}
|
||
{{- if .Diff.Updated }}
|
||
<h4 id="{{ .Prefix }}-updated">Updated</h4>
|
||
<details>
|
||
<summary>View Updated ({{ len .Diff.Updated }})</summary>
|
||
{{- range .Diff.Updated }}
|
||
<div class="diff-entry">
|
||
<h5>{{ .Name }}</h5>
|
||
<div class="diff-entry-path"><code>{{ .Path }}</code></div>
|
||
<pre><code>{{ .Diff }}</code></pre>
|
||
</div>
|
||
{{- end }}
|
||
</details>
|
||
{{- end }}
|
||
{{ end }}
|
||
|
||
{{ define "plistDiffSection" }}
|
||
{{- if .Diff.New }}
|
||
<h4 id="{{ .Prefix }}-new">New</h4>
|
||
<details>
|
||
<summary>View New ({{ len .Diff.New }})</summary>
|
||
{{- range .Diff.New }}
|
||
<div class="diff-entry">
|
||
<h5>{{ .Name }}</h5>
|
||
<div class="diff-entry-path"><code>{{ .Path }}</code></div>
|
||
{{ .Content }}
|
||
</div>
|
||
{{- end }}
|
||
</details>
|
||
{{- end }}
|
||
{{- if .Diff.Removed }}
|
||
<h4 id="{{ .Prefix }}-removed">Removed</h4>
|
||
<ul>{{ range .Diff.Removed }}<li><code>{{ . }}</code></li>{{ end }}</ul>
|
||
{{- end }}
|
||
{{- if .Diff.Updated }}
|
||
<h4 id="{{ .Prefix }}-updated">Updated</h4>
|
||
<details>
|
||
<summary>View Updated ({{ len .Diff.Updated }})</summary>
|
||
{{- range .Diff.Updated }}
|
||
<div class="diff-entry">
|
||
<h5>{{ .Name }}</h5>
|
||
<div class="diff-entry-path"><code>{{ .Path }}</code></div>
|
||
{{ .Content }}
|
||
</div>
|
||
{{- end }}
|
||
</details>
|
||
{{- end }}
|
||
{{ end }}
|
||
`
|
||
|
||
// newHTMLTaskTemplate parses body in a new template that also has the
|
||
// shared partials registered, so per-task renderers can call
|
||
// {{ template "machoDiffSection" ... }} and {{ template "plistDiffSection" ... }}.
|
||
func newHTMLTaskTemplate(name, body string) (*template.Template, error) {
|
||
tmpl := template.New(name).Funcs(htmlTaskFuncs)
|
||
if _, err := tmpl.Parse(htmlTaskPartials); err != nil {
|
||
return nil, err
|
||
}
|
||
if _, err := tmpl.Parse(body); err != nil {
|
||
return nil, err
|
||
}
|
||
return tmpl, nil
|
||
}
|
||
|
||
// executeHTMLTaskTemplate renders body (after composing it with the shared
|
||
// partials) against data and returns the result as template.HTML.
|
||
func executeHTMLTaskTemplate(name, body string, data any) (template.HTML, error) {
|
||
tmpl, err := newHTMLTaskTemplate(name, body)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
var buf bytes.Buffer
|
||
if err := tmpl.Execute(&buf, data); err != nil {
|
||
return "", err
|
||
}
|
||
return template.HTML(buf.String()), nil
|
||
}
|
||
|
||
func (d *Diff) String() string {
|
||
var buf bytes.Buffer
|
||
if err := diffMDTmpl.Execute(&buf, d); err != nil {
|
||
return fmt.Errorf("failed to execute diff template: %s", err).Error()
|
||
}
|
||
return buf.String()
|
||
}
|
||
|
||
// ToJSON saves the diff as a JSON file
|
||
func (d *Diff) ToJSON() error {
|
||
report, err := buildReport(d)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
dat, err := json.MarshalIndent(report, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if len(d.conf.Output) > 0 {
|
||
if err := os.MkdirAll(d.conf.Output, 0755); err != nil {
|
||
return err
|
||
}
|
||
fname := filepath.Join(d.conf.Output, d.TitleToFilename()+".json")
|
||
log.Infof("Creating JSON diff file: %s", fname)
|
||
return os.WriteFile(fname, dat, 0644)
|
||
}
|
||
|
||
fmt.Println(string(dat))
|
||
|
||
return nil
|
||
}
|
||
|
||
type diffHTMLPageData struct {
|
||
Title string
|
||
OldInput string
|
||
NewInput string
|
||
OldVersion string
|
||
OldBuild string
|
||
NewVersion string
|
||
NewBuild string
|
||
|
||
HasKernelVersion bool
|
||
OldKernelDarwin string
|
||
OldKernelXNU string
|
||
OldKernelDate string
|
||
NewKernelDarwin string
|
||
NewKernelXNU string
|
||
NewKernelDate string
|
||
|
||
// TOC-only converted views: every in-scope task (mount-based or
|
||
// top-level) keeps its sidebar TOC entry while the section bodies
|
||
// themselves render via the per-task HTML fragments below. KDKs and
|
||
// Sandbox are kept as plain template.HTML / string so the TOC's
|
||
// `{{- if .KDKs }}` / `{{- if .Sandbox }}` truthiness checks keep
|
||
// working without re-marshalling task fragments.
|
||
Kexts *htmlMachoDiff
|
||
KDKs template.HTML
|
||
Sandbox template.HTML
|
||
Firmwares *htmlMachoDiff
|
||
IBoot *htmlIBootDiff
|
||
Machos []htmlMachoVolume
|
||
Ents []htmlEntsVolume
|
||
Features []htmlPlistVolume
|
||
Files *htmlFileDiff
|
||
Localizations []htmlPlistVolume
|
||
|
||
// Per-task body fragments for both the mount-based (kexts/kdks/...)
|
||
// and top-level (kexts/kdks/firmwares/iboot/sandbox) sections. Each
|
||
// is produced by calling the owning task's HTML() method; if Empty()
|
||
// is true the section emits no heading or body.
|
||
KextsFragment htmlSectionFragment
|
||
KDKsFragment htmlSectionFragment
|
||
MachosFragment htmlSectionFragment
|
||
EntsFragment htmlSectionFragment
|
||
FirmwaresFragment htmlSectionFragment
|
||
IBootFragment htmlSectionFragment
|
||
LaunchdFragment htmlSectionFragment
|
||
SandboxFragment htmlSectionFragment
|
||
FilesFragment htmlSectionFragment
|
||
LocalizationsFragment htmlSectionFragment
|
||
FeaturesFragment htmlSectionFragment
|
||
|
||
OldWebkit string
|
||
NewWebkit string
|
||
Dylibs *htmlMachoDiff
|
||
}
|
||
|
||
// htmlSectionFragment is the outer-template view of a per-task HTML fragment.
|
||
// renderHTML builds one of these per in-scope task by combining the task's
|
||
// HTMLFragment (Heading + Body) with the section's stable anchor ID.
|
||
type htmlSectionFragment struct {
|
||
Anchor string
|
||
Heading string
|
||
Body template.HTML
|
||
Empty bool
|
||
}
|
||
|
||
type htmlMachoDiff struct {
|
||
New []string
|
||
Removed []string
|
||
Updated []htmlUpdatedEntry
|
||
}
|
||
|
||
// htmlMachoVolume groups a single volume's MachoDiff for per-volume HTML
|
||
// rendering. Used by the d.Machos shape (cross-DMG).
|
||
type htmlMachoVolume struct {
|
||
Name string
|
||
Diff *htmlMachoDiff
|
||
}
|
||
|
||
type htmlUpdatedEntry struct {
|
||
Name string
|
||
Path string
|
||
Diff template.HTML
|
||
}
|
||
|
||
type htmlNamedList struct {
|
||
Name string
|
||
Items []string
|
||
}
|
||
|
||
type htmlIBootDiff struct {
|
||
Versions []string
|
||
New []htmlNamedList
|
||
Removed []htmlNamedList
|
||
}
|
||
|
||
type htmlPlistEntry struct {
|
||
Name string
|
||
Path string
|
||
Content template.HTML
|
||
}
|
||
|
||
type htmlPlistDiff struct {
|
||
New []htmlPlistEntry
|
||
Removed []string
|
||
Updated []htmlPlistEntry
|
||
}
|
||
|
||
// htmlPlistVolume groups a single volume's PlistDiff for per-volume HTML
|
||
// rendering (used by d.Features and d.Localizations).
|
||
type htmlPlistVolume struct {
|
||
Name string
|
||
Diff *htmlPlistDiff
|
||
}
|
||
|
||
// htmlEntsVolume groups a single volume's rendered entitlements diff for
|
||
// per-volume HTML rendering.
|
||
type htmlEntsVolume struct {
|
||
Name string
|
||
Content template.HTML
|
||
}
|
||
|
||
func convertEntitlementsDiff(m map[string]string) []htmlEntsVolume {
|
||
if len(m) == 0 {
|
||
return nil
|
||
}
|
||
keys := make([]string, 0, len(m))
|
||
for k := range m {
|
||
keys = append(keys, k)
|
||
}
|
||
keys = sortVolumeNames(keys)
|
||
out := make([]htmlEntsVolume, 0, len(keys))
|
||
for _, vol := range keys {
|
||
rendered := m[vol]
|
||
if !entitlementsDiffHasContent(rendered) {
|
||
continue
|
||
}
|
||
out = append(out, htmlEntsVolume{
|
||
Name: vol,
|
||
Content: renderMarkdownFragment(rendered),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
type htmlFileDiff struct {
|
||
New []htmlNamedList
|
||
Removed []htmlNamedList
|
||
}
|
||
|
||
const diffHTMLPageTemplate = `<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="color-scheme" content="light dark">
|
||
<title>{{ .Title }}</title>
|
||
<style>
|
||
:root {
|
||
--radius: 18px;
|
||
--background: oklch(0.985 0.004 247.858);
|
||
--foreground: oklch(0.205 0.01 257.417);
|
||
--card: oklch(0.999 0 0);
|
||
--card-foreground: oklch(0.205 0.01 257.417);
|
||
--muted: oklch(0.968 0.006 247.896);
|
||
--muted-foreground: oklch(0.473 0.013 255.508);
|
||
--primary: oklch(0.598 0.112 221.723);
|
||
--primary-foreground: oklch(0.985 0 0);
|
||
--accent: oklch(0.929 0.03 214.91);
|
||
--accent-foreground: oklch(0.278 0.033 256.848);
|
||
--border: oklch(0.923 0.01 252.096);
|
||
--shadow-soft: 0 24px 60px -32px oklch(0.188 0.012 257.417 / 0.28);
|
||
--shadow-panel: 0 18px 40px -28px oklch(0.188 0.012 257.417 / 0.22);
|
||
--code-bg: oklch(0.25 0.014 253.1);
|
||
--code-fg: oklch(0.96 0.004 247.858);
|
||
--diff-add: oklch(0.30 0.10 145);
|
||
--diff-add-bg: oklch(0.96 0.03 145);
|
||
--diff-del: oklch(0.38 0.10 25);
|
||
--diff-del-bg: oklch(0.96 0.03 25);
|
||
--diff-mod: oklch(0.40 0.10 75);
|
||
--diff-mod-bg: oklch(0.96 0.03 75);
|
||
--hero-wash: radial-gradient(circle at top left, oklch(0.96 0.04 215 / 0.9), transparent 38%),
|
||
radial-gradient(circle at top right, oklch(0.92 0.025 250 / 0.75), transparent 34%),
|
||
linear-gradient(135deg, oklch(0.995 0.002 247.858), oklch(0.975 0.01 247.896));
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
:root {
|
||
--background: oklch(0.14 0.012 257.417);
|
||
--foreground: oklch(0.96 0.004 247.858);
|
||
--card: oklch(0.19 0.014 255.508);
|
||
--card-foreground: oklch(0.96 0.004 247.858);
|
||
--muted: oklch(0.24 0.012 255.508);
|
||
--muted-foreground: oklch(0.7 0.013 255.508);
|
||
--primary: oklch(0.72 0.104 221.723);
|
||
--primary-foreground: oklch(0.205 0.01 257.417);
|
||
--accent: oklch(0.28 0.02 224.12);
|
||
--accent-foreground: oklch(0.96 0.004 247.858);
|
||
--border: oklch(0.30 0.012 255.508);
|
||
--shadow-soft: 0 30px 80px -40px oklch(0.02 0 0 / 0.65);
|
||
--shadow-panel: 0 22px 46px -32px oklch(0.02 0 0 / 0.56);
|
||
--code-bg: oklch(0.15 0.01 255.508);
|
||
--code-fg: oklch(0.92 0.004 247.858);
|
||
--diff-add: oklch(0.72 0.16 145);
|
||
--diff-add-bg: oklch(0.22 0.04 145);
|
||
--diff-del: oklch(0.72 0.16 25);
|
||
--diff-del-bg: oklch(0.22 0.04 25);
|
||
--diff-mod: oklch(0.76 0.12 75);
|
||
--diff-mod-bg: oklch(0.22 0.03 75);
|
||
--hero-wash: radial-gradient(circle at top left, oklch(0.26 0.036 215 / 0.55), transparent 38%),
|
||
radial-gradient(circle at top right, oklch(0.22 0.02 250 / 0.5), transparent 34%),
|
||
linear-gradient(135deg, oklch(0.18 0.012 257.417), oklch(0.15 0.012 255.508));
|
||
}
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
html { scroll-behavior: smooth; }
|
||
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
color: var(--foreground);
|
||
background:
|
||
radial-gradient(circle at top, oklch(0.9 0.02 215 / 0.18), transparent 30%),
|
||
linear-gradient(180deg, color-mix(in oklab, var(--background) 92%, white 8%), var(--background));
|
||
font-family: "Avenir Next", "Aptos", "Segoe UI", sans-serif;
|
||
line-height: 1.65;
|
||
}
|
||
|
||
body::before {
|
||
content: "";
|
||
position: fixed;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
background-image: linear-gradient(to right, color-mix(in oklab, var(--border) 40%, transparent) 1px, transparent 1px),
|
||
linear-gradient(to bottom, color-mix(in oklab, var(--border) 35%, transparent) 1px, transparent 1px);
|
||
background-size: 32px 32px;
|
||
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.35), transparent 75%);
|
||
opacity: 0.28;
|
||
}
|
||
|
||
a { color: color-mix(in oklab, var(--primary) 82%, var(--foreground) 18%); text-decoration: none; }
|
||
a:hover { color: var(--primary); }
|
||
code, pre, kbd { font-family: "IBM Plex Mono", "SFMono-Regular", "SF Mono", Consolas, monospace; }
|
||
|
||
.report-shell { position: relative; max-width: 1500px; margin: 0 auto; padding: 28px 22px 72px; }
|
||
|
||
.panel {
|
||
background: color-mix(in oklab, var(--card) 94%, transparent 6%);
|
||
color: var(--card-foreground);
|
||
border: 1px solid color-mix(in oklab, var(--border) 90%, transparent 10%);
|
||
border-radius: calc(var(--radius) + 2px);
|
||
box-shadow: var(--shadow-panel);
|
||
backdrop-filter: blur(16px);
|
||
}
|
||
|
||
.report-hero { position: relative; overflow: hidden; margin-bottom: 24px; padding: 28px; background: var(--hero-wash); }
|
||
|
||
.report-hero::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: auto -10% -35% auto;
|
||
width: 320px; height: 320px;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle, color-mix(in oklab, var(--primary) 32%, transparent 68%) 0%, transparent 72%);
|
||
opacity: 0.9;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.hero-topline {
|
||
display: inline-flex; align-items: center; gap: 10px;
|
||
margin-bottom: 16px; padding: 8px 12px;
|
||
border-radius: 999px;
|
||
background: color-mix(in oklab, var(--card) 80%, transparent 20%);
|
||
border: 1px solid color-mix(in oklab, var(--border) 80%, transparent 20%);
|
||
color: var(--muted-foreground);
|
||
font-size: 0.78rem; letter-spacing: 0.14em; text-transform: uppercase;
|
||
}
|
||
|
||
.hero-grid {
|
||
position: relative; z-index: 1;
|
||
display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(280px, 1fr);
|
||
gap: 18px; align-items: start;
|
||
}
|
||
|
||
.hero-title {
|
||
margin: 0;
|
||
font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;
|
||
font-size: clamp(2.2rem, 4vw, 4rem);
|
||
line-height: 1.02; letter-spacing: -0.04em;
|
||
text-wrap: balance;
|
||
}
|
||
|
||
.hero-subtitle { max-width: 62ch; margin: 14px 0 4px; color: var(--muted-foreground); font-size: 1rem; }
|
||
|
||
.hero-meta { display: grid; gap: 12px; }
|
||
.meta-card {
|
||
padding: 16px 18px; border-radius: calc(var(--radius) - 4px);
|
||
background: color-mix(in oklab, var(--card) 82%, transparent 18%);
|
||
border: 1px solid color-mix(in oklab, var(--border) 76%, transparent 24%);
|
||
box-shadow: var(--shadow-soft);
|
||
}
|
||
.meta-label { display: block; margin-bottom: 6px; color: var(--muted-foreground); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
|
||
.meta-value { display: block; font-size: 1rem; font-weight: 700; word-break: break-word; }
|
||
.meta-note { display: block; margin-top: 6px; color: var(--muted-foreground); font-size: 0.92rem; }
|
||
|
||
.report-layout { display: grid; grid-template-columns: minmax(240px, 300px) minmax(0, 1fr); gap: 24px; align-items: start; }
|
||
.report-layout.no-sidebar { grid-template-columns: minmax(0, 1fr); }
|
||
|
||
.report-sidebar { position: sticky; top: 22px; padding: 18px; }
|
||
.sidebar-title { margin: 0 0 14px; font-size: 0.8rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted-foreground); }
|
||
|
||
.toc ul { list-style: none; margin: 0; padding: 0; }
|
||
.toc ul ul { margin-top: 6px; margin-left: 10px; padding-left: 10px; border-left: 1px solid color-mix(in oklab, var(--border) 82%, transparent 18%); }
|
||
.toc li { margin: 2px 0; }
|
||
.toc a {
|
||
display: block; padding: 8px 10px; border-radius: 12px;
|
||
color: var(--muted-foreground); font-size: 0.92rem; line-height: 1.35;
|
||
transition: background-color 120ms ease, color 120ms ease, transform 120ms ease;
|
||
}
|
||
.toc a:hover { background: color-mix(in oklab, var(--accent) 70%, transparent 30%); color: var(--foreground); transform: translateX(2px); }
|
||
|
||
.report-main { min-width: 0; padding: 30px 32px; }
|
||
|
||
.rc h2, .rc h3, .rc h4, .rc h5 { scroll-margin-top: 28px; }
|
||
.rc h2 {
|
||
margin: 2.5rem 0 1rem; padding-top: 1.4rem;
|
||
border-top: 1px solid color-mix(in oklab, var(--border) 88%, transparent 12%);
|
||
font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;
|
||
font-size: clamp(1.55rem, 2vw, 2rem); line-height: 1.15; letter-spacing: -0.03em;
|
||
text-wrap: balance;
|
||
}
|
||
.rc h2:first-of-type { margin-top: 0; padding-top: 0; border-top: 0; }
|
||
.rc h3, .rc h4, .rc h5 {
|
||
margin: 1.6rem 0 0.7rem; font-size: 0.82rem; font-weight: 800;
|
||
letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted-foreground);
|
||
}
|
||
.rc p, .rc li, .rc blockquote, .rc summary { font-size: 0.98rem; }
|
||
.rc p { max-width: 74ch; }
|
||
.rc ul, .rc ol { padding-left: 1.35rem; }
|
||
.rc li + li { margin-top: 0.28rem; }
|
||
.rc li::marker { color: color-mix(in oklab, var(--primary) 75%, var(--foreground) 25%); }
|
||
|
||
.rc table {
|
||
width: 100%; display: block; overflow-x: auto;
|
||
margin: 1.2rem 0 1.6rem; border-collapse: separate; border-spacing: 0;
|
||
border: 1px solid color-mix(in oklab, var(--border) 88%, transparent 12%);
|
||
border-radius: calc(var(--radius) - 6px);
|
||
background: color-mix(in oklab, var(--card) 92%, transparent 8%);
|
||
}
|
||
.rc thead th {
|
||
background: color-mix(in oklab, var(--muted) 84%, transparent 16%);
|
||
color: var(--foreground); font-size: 0.82rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase;
|
||
}
|
||
.rc caption {
|
||
caption-side: bottom;
|
||
padding: 12px 14px 0;
|
||
color: var(--muted-foreground);
|
||
font-size: 0.9rem;
|
||
text-align: left;
|
||
}
|
||
.rc th, .rc td { padding: 12px 14px; border-bottom: 1px solid color-mix(in oklab, var(--border) 82%, transparent 18%); text-align: left; white-space: nowrap; }
|
||
.rc tr:last-child td { border-bottom: 0; }
|
||
.rc tbody tr:nth-child(even) { background: color-mix(in oklab, var(--muted) 48%, transparent 52%); }
|
||
|
||
.rc blockquote {
|
||
margin: 1.1rem 0; padding: 0.9rem 1rem;
|
||
border-left: 3px solid color-mix(in oklab, var(--primary) 68%, transparent 32%);
|
||
border-radius: 0 14px 14px 0;
|
||
background: color-mix(in oklab, var(--accent) 52%, transparent 48%);
|
||
color: var(--foreground);
|
||
}
|
||
|
||
.rc code {
|
||
padding: 0.18rem 0.44rem; border-radius: 0.55rem;
|
||
background: color-mix(in oklab, var(--muted) 72%, transparent 28%);
|
||
color: var(--foreground); font-size: 0.88em;
|
||
}
|
||
|
||
.rc pre {
|
||
overflow-x: auto; margin: 1rem 0 1.4rem; padding: 16px 18px;
|
||
border: 1px solid color-mix(in oklab, var(--border) 60%, transparent 40%);
|
||
border-radius: calc(var(--radius) - 4px);
|
||
background: linear-gradient(180deg, color-mix(in oklab, var(--code-bg) 88%, black 12%), var(--code-bg));
|
||
color: var(--code-fg);
|
||
box-shadow: inset 0 1px 0 color-mix(in oklab, white 8%, transparent 92%);
|
||
}
|
||
.rc pre code { padding: 0; background: transparent; color: inherit; border-radius: 0; font-size: 0.9rem; }
|
||
|
||
.rc .diff-add { color: var(--diff-add); background: var(--diff-add-bg); display: inline-block; width: 100%; padding: 0 4px; margin: 0 -4px; border-radius: 3px; }
|
||
.rc .diff-del { color: var(--diff-del); background: var(--diff-del-bg); display: inline-block; width: 100%; padding: 0 4px; margin: 0 -4px; border-radius: 3px; }
|
||
.rc .diff-mod { color: var(--diff-mod); background: var(--diff-mod-bg); display: inline-block; width: 100%; padding: 0 4px; margin: 0 -4px; border-radius: 3px; }
|
||
|
||
.rc details {
|
||
margin: 1.1rem 0 1.5rem; padding: 0.45rem 1rem 1rem;
|
||
border: 1px solid color-mix(in oklab, var(--border) 85%, transparent 15%);
|
||
border-radius: calc(var(--radius) - 4px);
|
||
background: color-mix(in oklab, var(--card) 90%, transparent 10%);
|
||
}
|
||
.rc details[open] { box-shadow: var(--shadow-soft); }
|
||
.rc summary {
|
||
display: flex; align-items: center; justify-content: space-between; gap: 16px;
|
||
cursor: pointer; list-style: none; font-weight: 700; color: var(--foreground); padding: 0.6rem 0;
|
||
transition: color 150ms ease;
|
||
}
|
||
.rc summary:hover { color: var(--primary); }
|
||
.rc summary::-webkit-details-marker { display: none; }
|
||
.rc summary::after {
|
||
content: "+"; display: inline-flex; align-items: center; justify-content: center;
|
||
width: 1.6rem; height: 1.6rem; border-radius: 999px;
|
||
background: color-mix(in oklab, var(--accent) 72%, transparent 28%);
|
||
color: var(--accent-foreground); font-size: 1rem; font-weight: 700; flex-shrink: 0;
|
||
transition: transform 200ms ease, background-color 150ms ease;
|
||
}
|
||
.rc summary:hover::after { background: color-mix(in oklab, var(--primary) 30%, var(--accent) 70%); }
|
||
.rc details[open] summary::after { content: "\2212"; transform: rotate(180deg); }
|
||
|
||
.rc .diff-entry { margin: 1.5rem 0; padding-top: 1rem; border-top: 1px solid color-mix(in oklab, var(--border) 60%, transparent 40%); }
|
||
.rc .diff-entry:first-child { border-top: 0; padding-top: 0; margin-top: 0.5rem; }
|
||
.rc .diff-entry-path {
|
||
display: block; margin: 0.3rem 0 0.6rem; padding: 0.5rem 0.8rem;
|
||
border-left: 3px solid color-mix(in oklab, var(--primary) 68%, transparent 32%);
|
||
border-radius: 0 10px 10px 0;
|
||
background: color-mix(in oklab, var(--accent) 40%, transparent 60%);
|
||
color: var(--muted-foreground); font-size: 0.85rem;
|
||
}
|
||
.rc .diff-entry-path code { background: transparent; padding: 0; font-size: 0.82rem; }
|
||
|
||
.rc hr { margin: 2rem 0; border: 0; border-top: 1px solid color-mix(in oklab, var(--border) 88%, transparent 12%); }
|
||
|
||
a:focus-visible, summary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 6px; }
|
||
.toc a.active { background: color-mix(in oklab, var(--primary) 15%, transparent 85%); color: var(--foreground); font-weight: 600; }
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; }
|
||
}
|
||
|
||
@media (max-width: 1100px) {
|
||
.hero-grid, .report-layout, .report-layout.no-sidebar { grid-template-columns: minmax(0, 1fr); }
|
||
.report-sidebar { position: static; }
|
||
.report-main { padding: 24px 22px; }
|
||
}
|
||
|
||
@media (max-width: 700px) {
|
||
.report-shell { padding: 18px 14px 40px; }
|
||
.report-hero { padding: 22px 18px; }
|
||
.hero-title { font-size: 2rem; }
|
||
.report-main { padding: 20px 16px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="report-shell">
|
||
<header class="report-hero panel">
|
||
<div class="hero-topline">
|
||
<span>ipsw</span>
|
||
<span>Diff Report</span>
|
||
</div>
|
||
<div class="hero-grid">
|
||
<section>
|
||
<h1 class="hero-title">{{ .Title }}</h1>
|
||
</section>
|
||
<section class="hero-meta">
|
||
<div class="meta-card">
|
||
<span class="meta-label">Old Input</span>
|
||
<span class="meta-value">{{ .OldInput }}</span>
|
||
{{- if .OldVersion }}<span class="meta-note">{{ .OldVersion }} · {{ .OldBuild }}</span>{{ end }}
|
||
</div>
|
||
<div class="meta-card">
|
||
<span class="meta-label">New Input</span>
|
||
<span class="meta-value">{{ .NewInput }}</span>
|
||
{{- if .NewVersion }}<span class="meta-note">{{ .NewVersion }} · {{ .NewBuild }}</span>{{ end }}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</header>
|
||
<div class="report-layout">
|
||
<aside class="report-sidebar panel">
|
||
<h2 class="sidebar-title">Contents</h2>
|
||
<nav class="toc">
|
||
<ul>
|
||
<li><a href="#inputs">Inputs</a></li>
|
||
<li><a href="#kernel">Kernel</a>
|
||
{{- if .HasKernelVersion }}<ul><li><a href="#kernel-version">Version</a></li></ul>{{ end }}
|
||
</li>
|
||
{{- if .Kexts }}
|
||
<li><a href="#kexts">Kexts</a>
|
||
<ul>
|
||
{{- if .Kexts.New }}<li><a href="#kexts-new">New ({{ len .Kexts.New }})</a></li>{{ end }}
|
||
{{- if .Kexts.Removed }}<li><a href="#kexts-removed">Removed ({{ len .Kexts.Removed }})</a></li>{{ end }}
|
||
{{- if .Kexts.Updated }}<li><a href="#kexts-updated">Updated ({{ len .Kexts.Updated }})</a></li>{{ end }}
|
||
</ul>
|
||
</li>
|
||
{{- end }}
|
||
{{- if .Machos }}
|
||
<li><a href="#machos">MachOs</a>
|
||
<ul>
|
||
{{- range .Machos }}
|
||
<li><a href="#machos-{{ .Name | slug }}">{{ .Name }}</a></li>
|
||
{{- end }}
|
||
</ul>
|
||
</li>
|
||
{{- end }}
|
||
{{- if .Ents }}<li><a href="#entitlements">Entitlements</a></li>{{ end }}
|
||
{{- if .Firmwares }}
|
||
<li><a href="#firmwares">Firmwares</a>
|
||
<ul>
|
||
{{- if .Firmwares.New }}<li><a href="#fw-new">New ({{ len .Firmwares.New }})</a></li>{{ end }}
|
||
{{- if .Firmwares.Removed }}<li><a href="#fw-removed">Removed ({{ len .Firmwares.Removed }})</a></li>{{ end }}
|
||
{{- if .Firmwares.Updated }}<li><a href="#fw-updated">Updated ({{ len .Firmwares.Updated }})</a></li>{{ end }}
|
||
</ul>
|
||
</li>
|
||
{{- end }}
|
||
{{- if .IBoot }}
|
||
<li><a href="#iboot">iBoot</a>
|
||
<ul>
|
||
{{- if .IBoot.New }}<li><a href="#iboot-new">New ({{ len .IBoot.New }})</a></li>{{ end }}
|
||
{{- if .IBoot.Removed }}<li><a href="#iboot-removed">Removed ({{ len .IBoot.Removed }})</a></li>{{ end }}
|
||
</ul>
|
||
</li>
|
||
{{- end }}
|
||
{{- if not .LaunchdFragment.Empty }}<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 }}
|
||
{{- if .Dylibs }}
|
||
<li><a href="#dylibs">Dylibs</a>
|
||
<ul>
|
||
{{- if .Dylibs.New }}<li><a href="#dylibs-new">New ({{ len .Dylibs.New }})</a></li>{{ end }}
|
||
{{- if .Dylibs.Removed }}<li><a href="#dylibs-removed">Removed ({{ len .Dylibs.Removed }})</a></li>{{ end }}
|
||
{{- if .Dylibs.Updated }}<li><a href="#dylibs-updated">Updated ({{ len .Dylibs.Updated }})</a></li>{{ end }}
|
||
</ul>
|
||
</li>
|
||
{{- end }}
|
||
</ul>
|
||
</li>
|
||
{{- if .Files }}
|
||
<li><a href="#files">Files</a>
|
||
<ul>
|
||
{{- if .Files.New }}<li><a href="#files-new">New ({{ len .Files.New }})</a></li>{{ end }}
|
||
{{- if .Files.Removed }}<li><a href="#files-removed">Removed ({{ len .Files.Removed }})</a></li>{{ end }}
|
||
</ul>
|
||
</li>
|
||
{{- end }}
|
||
{{- if .Localizations }}
|
||
<li><a href="#localizations">Localizations</a>
|
||
<ul>
|
||
{{- range .Localizations }}
|
||
<li><a href="#localizations-{{ .Name | slug }}">{{ .Name }}</a></li>
|
||
{{- end }}
|
||
</ul>
|
||
</li>
|
||
{{- end }}
|
||
{{- if .Features }}
|
||
<li><a href="#feature-flags">Feature Flags</a>
|
||
<ul>
|
||
{{- range .Features }}
|
||
<li><a href="#features-{{ .Name | slug }}">{{ .Name }}</a></li>
|
||
{{- end }}
|
||
</ul>
|
||
</li>
|
||
{{- end }}
|
||
</ul>
|
||
</nav>
|
||
</aside>
|
||
<main class="report-main panel">
|
||
<article class="rc">
|
||
|
||
<h2 id="inputs">Inputs</h2>
|
||
<ul>
|
||
<li><code>{{ .OldInput }}</code></li>
|
||
<li><code>{{ .NewInput }}</code></li>
|
||
</ul>
|
||
|
||
<h2 id="kernel">Kernel</h2>
|
||
{{- if .HasKernelVersion }}
|
||
<h3 id="kernel-version">Version</h3>
|
||
<table>
|
||
<caption>Kernel version comparison across the old and new inputs.</caption>
|
||
<thead><tr><th>iOS</th><th>Version</th><th>Build</th><th>Date</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>{{ .OldVersion }} <em>({{ .OldBuild }})</em></td><td>{{ .OldKernelDarwin }}</td><td>{{ .OldKernelXNU }}</td><td>{{ .OldKernelDate }}</td></tr>
|
||
<tr><td>{{ .NewVersion }} <em>({{ .NewBuild }})</em></td><td>{{ .NewKernelDarwin }}</td><td>{{ .NewKernelXNU }}</td><td>{{ .NewKernelDate }}</td></tr>
|
||
</tbody>
|
||
</table>
|
||
{{- end }}
|
||
|
||
{{- if not .KextsFragment.Empty }}{{ .KextsFragment.Body }}{{- end }}
|
||
|
||
{{- if not .KDKsFragment.Empty }}{{ .KDKsFragment.Body }}{{- end }}
|
||
|
||
{{- if not .MachosFragment.Empty }}
|
||
<h2 id="{{ .MachosFragment.Anchor }}">{{ .MachosFragment.Heading }}</h2>{{ .MachosFragment.Body }}
|
||
{{- end }}
|
||
{{- if not .EntsFragment.Empty }}
|
||
<h2 id="{{ .EntsFragment.Anchor }}">{{ .EntsFragment.Heading }}</h2>{{ .EntsFragment.Body }}
|
||
{{- end }}
|
||
|
||
{{- if not .FirmwaresFragment.Empty }}{{ .FirmwaresFragment.Body }}{{- end }}
|
||
|
||
{{- if not .IBootFragment.Empty }}{{ .IBootFragment.Body }}{{- end }}
|
||
|
||
{{- if not .LaunchdFragment.Empty }}
|
||
<h2 id="{{ .LaunchdFragment.Anchor }}">{{ .LaunchdFragment.Heading }}</h2>{{ .LaunchdFragment.Body }}
|
||
{{- end }}
|
||
|
||
{{- if not .SandboxFragment.Empty }}{{ .SandboxFragment.Body }}{{- end }}
|
||
|
||
<h2 id="dsc">DSC</h2>
|
||
{{- if .OldWebkit }}
|
||
<h3 id="webkit">WebKit</h3>
|
||
<table>
|
||
<caption>WebKit version comparison across the old and new inputs.</caption>
|
||
<thead><tr><th>iOS</th><th>Version</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>{{ .OldVersion }} <em>({{ .OldBuild }})</em></td><td>{{ .OldWebkit }}</td></tr>
|
||
<tr><td>{{ .NewVersion }} <em>({{ .NewBuild }})</em></td><td>{{ .NewWebkit }}</td></tr>
|
||
</tbody>
|
||
</table>
|
||
{{- end }}
|
||
|
||
{{- if .Dylibs }}
|
||
<h3 id="dylibs">Dylibs</h3>
|
||
{{- template "machoDiffSection" dict "Prefix" "dylibs" "Diff" .Dylibs }}
|
||
{{- end }}
|
||
|
||
{{- if not .FilesFragment.Empty }}
|
||
<h2 id="{{ .FilesFragment.Anchor }}">{{ .FilesFragment.Heading }}</h2>{{ .FilesFragment.Body }}
|
||
{{- end }}
|
||
{{- if not .LocalizationsFragment.Empty }}
|
||
<h2 id="{{ .LocalizationsFragment.Anchor }}">{{ .LocalizationsFragment.Heading }}</h2>{{ .LocalizationsFragment.Body }}
|
||
{{- end }}
|
||
{{- if not .FeaturesFragment.Empty }}
|
||
<h2 id="{{ .FeaturesFragment.Anchor }}">{{ .FeaturesFragment.Heading }}</h2>{{ .FeaturesFragment.Body }}
|
||
{{- end }}
|
||
|
||
</article>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
(function(){
|
||
var links=document.querySelectorAll('.toc a[href^="#"]');
|
||
if(!links.length)return;
|
||
var ids=[];links.forEach(function(a){ids.push(a.getAttribute('href').slice(1));});
|
||
var io=new IntersectionObserver(function(entries){
|
||
entries.forEach(function(e){
|
||
if(e.isIntersecting){
|
||
links.forEach(function(a){a.classList.remove('active');});
|
||
var match=document.querySelector('.toc a[href="#'+e.target.id+'"]');
|
||
if(match)match.classList.add('active');
|
||
}
|
||
});
|
||
},{rootMargin:'-20% 0px -60% 0px'});
|
||
ids.forEach(function(id){var el=document.getElementById(id);if(el)io.observe(el);});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|
||
{{ define "machoDiffSection" }}
|
||
{{- if .Diff.New }}
|
||
<h4 id="{{ .Prefix }}-new">New</h4>
|
||
<ul>{{ range .Diff.New }}<li><code>{{ . }}</code></li>{{ end }}</ul>
|
||
{{- end }}
|
||
{{- if .Diff.Removed }}
|
||
<h4 id="{{ .Prefix }}-removed">Removed</h4>
|
||
<ul>{{ range .Diff.Removed }}<li><code>{{ . }}</code></li>{{ end }}</ul>
|
||
{{- end }}
|
||
{{- if .Diff.Updated }}
|
||
<h4 id="{{ .Prefix }}-updated">Updated</h4>
|
||
<details>
|
||
<summary>View Updated ({{ len .Diff.Updated }})</summary>
|
||
{{- range .Diff.Updated }}
|
||
<div class="diff-entry">
|
||
<h5>{{ .Name }}</h5>
|
||
<div class="diff-entry-path"><code>{{ .Path }}</code></div>
|
||
<pre><code>{{ .Diff }}</code></pre>
|
||
</div>
|
||
{{- end }}
|
||
</details>
|
||
{{- end }}
|
||
{{ end }}
|
||
|
||
{{ define "plistDiffSection" }}
|
||
{{- if .Diff.New }}
|
||
<h4 id="{{ .Prefix }}-new">New</h4>
|
||
<details>
|
||
<summary>View New ({{ len .Diff.New }})</summary>
|
||
{{- range .Diff.New }}
|
||
<div class="diff-entry">
|
||
<h5>{{ .Name }}</h5>
|
||
<div class="diff-entry-path"><code>{{ .Path }}</code></div>
|
||
{{ .Content }}
|
||
</div>
|
||
{{- end }}
|
||
</details>
|
||
{{- end }}
|
||
{{- if .Diff.Removed }}
|
||
<h4 id="{{ .Prefix }}-removed">Removed</h4>
|
||
<ul>{{ range .Diff.Removed }}<li><code>{{ . }}</code></li>{{ end }}</ul>
|
||
{{- end }}
|
||
{{- if .Diff.Updated }}
|
||
<h4 id="{{ .Prefix }}-updated">Updated</h4>
|
||
<details>
|
||
<summary>View Updated ({{ len .Diff.Updated }})</summary>
|
||
{{- range .Diff.Updated }}
|
||
<div class="diff-entry">
|
||
<h5>{{ .Name }}</h5>
|
||
<div class="diff-entry-path"><code>{{ .Path }}</code></div>
|
||
{{ .Content }}
|
||
</div>
|
||
{{- end }}
|
||
</details>
|
||
{{- end }}
|
||
{{ end }}
|
||
`
|
||
|
||
func renderCodeBlock(raw string) template.HTML {
|
||
if raw == "" {
|
||
return ""
|
||
}
|
||
|
||
var buf strings.Builder
|
||
buf.WriteString("<pre><code>")
|
||
buf.WriteString(html.EscapeString(raw))
|
||
buf.WriteString("</code></pre>")
|
||
return template.HTML(buf.String())
|
||
}
|
||
|
||
func stripDiffFence(raw string) string {
|
||
trimmed := strings.TrimSpace(raw)
|
||
if !strings.HasPrefix(trimmed, "```diff") {
|
||
return raw
|
||
}
|
||
|
||
body := strings.TrimPrefix(trimmed, "```diff")
|
||
body = strings.TrimPrefix(body, "\r\n")
|
||
body = strings.TrimPrefix(body, "\n")
|
||
|
||
if idx := strings.LastIndex(body, "\n```"); idx >= 0 {
|
||
return body[:idx]
|
||
}
|
||
if before, ok := strings.CutSuffix(body, "```"); ok {
|
||
return before
|
||
}
|
||
|
||
return raw
|
||
}
|
||
|
||
func renderMarkdownChunk(md string) string {
|
||
if strings.TrimSpace(md) == "" {
|
||
return ""
|
||
}
|
||
|
||
renderer := mdhtml.NewRenderer(mdhtml.RendererOptions{
|
||
Flags: mdhtml.CommonFlags,
|
||
})
|
||
return string(markdown.ToHTML([]byte(md), nil, renderer))
|
||
}
|
||
|
||
func highlightDiff(raw string) template.HTML {
|
||
var buf strings.Builder
|
||
first := true
|
||
for line := range strings.SplitSeq(raw, "\n") {
|
||
if !first {
|
||
buf.WriteByte('\n')
|
||
}
|
||
first = false
|
||
escaped := html.EscapeString(line)
|
||
switch {
|
||
case strings.HasPrefix(line, "+"):
|
||
buf.WriteString(`<span class="diff-add">`)
|
||
buf.WriteString(escaped)
|
||
buf.WriteString("</span>")
|
||
case strings.HasPrefix(line, "-"):
|
||
buf.WriteString(`<span class="diff-del">`)
|
||
buf.WriteString(escaped)
|
||
buf.WriteString("</span>")
|
||
case strings.HasPrefix(line, "~"):
|
||
buf.WriteString(`<span class="diff-mod">`)
|
||
buf.WriteString(escaped)
|
||
buf.WriteString("</span>")
|
||
default:
|
||
buf.WriteString(escaped)
|
||
}
|
||
}
|
||
return template.HTML(buf.String())
|
||
}
|
||
|
||
func renderHighlightedDiffBlock(raw string) string {
|
||
if raw == "" {
|
||
return ""
|
||
}
|
||
|
||
var buf strings.Builder
|
||
buf.WriteString("<pre><code>")
|
||
buf.WriteString(string(highlightDiff(raw)))
|
||
buf.WriteString("</code></pre>")
|
||
return buf.String()
|
||
}
|
||
|
||
func renderMarkdownFragment(md string) template.HTML {
|
||
if md == "" {
|
||
return ""
|
||
}
|
||
|
||
var out strings.Builder
|
||
var markdownBuf strings.Builder
|
||
var diffBuf strings.Builder
|
||
inDiffFence := false
|
||
|
||
flushMarkdown := func() {
|
||
if markdownBuf.Len() == 0 {
|
||
return
|
||
}
|
||
out.WriteString(renderMarkdownChunk(markdownBuf.String()))
|
||
markdownBuf.Reset()
|
||
}
|
||
|
||
flushDiff := func() {
|
||
if diffBuf.Len() == 0 {
|
||
return
|
||
}
|
||
out.WriteString(renderHighlightedDiffBlock(diffBuf.String()))
|
||
diffBuf.Reset()
|
||
}
|
||
|
||
lines := strings.Split(md, "\n")
|
||
for idx, line := range lines {
|
||
trimmed := strings.TrimSpace(line)
|
||
|
||
if inDiffFence {
|
||
if trimmed == "```" {
|
||
flushDiff()
|
||
inDiffFence = false
|
||
continue
|
||
}
|
||
if diffBuf.Len() > 0 {
|
||
diffBuf.WriteByte('\n')
|
||
}
|
||
diffBuf.WriteString(line)
|
||
continue
|
||
}
|
||
|
||
if trimmed == "```diff" {
|
||
flushMarkdown()
|
||
inDiffFence = true
|
||
continue
|
||
}
|
||
|
||
markdownBuf.WriteString(line)
|
||
if idx < len(lines)-1 {
|
||
markdownBuf.WriteByte('\n')
|
||
}
|
||
}
|
||
|
||
if inDiffFence {
|
||
markdownBuf.WriteString("```diff\n")
|
||
markdownBuf.WriteString(diffBuf.String())
|
||
}
|
||
|
||
flushMarkdown()
|
||
|
||
return template.HTML(out.String())
|
||
}
|
||
|
||
func convertMachoDiff(md *mcmd.MachoDiff) *htmlMachoDiff {
|
||
if md == nil {
|
||
return nil
|
||
}
|
||
h := &htmlMachoDiff{
|
||
New: md.New,
|
||
Removed: md.Removed,
|
||
}
|
||
for _, k := range slices.Sorted(maps.Keys(md.Updated)) {
|
||
h.Updated = append(h.Updated, htmlUpdatedEntry{
|
||
Name: filepath.Base(k),
|
||
Path: k,
|
||
Diff: highlightDiff(stripDiffFence(md.Updated[k])),
|
||
})
|
||
}
|
||
return h
|
||
}
|
||
|
||
// convertPlistVolumeDiff turns a per-volume PlistDiff map into an ordered
|
||
// slice the HTML template iterates. Empty volumes are omitted. nameForPath
|
||
// chooses the display name used for entry headings.
|
||
func convertPlistVolumeDiff(m map[string]*PlistDiff, nameForPath func(string) string) []htmlPlistVolume {
|
||
if len(m) == 0 {
|
||
return nil
|
||
}
|
||
out := make([]htmlPlistVolume, 0, len(m))
|
||
for _, vol := range sortedVolumeKeys(m) {
|
||
pd := m[vol]
|
||
if !plistDiffHasContent(pd) {
|
||
continue
|
||
}
|
||
out = append(out, htmlPlistVolume{
|
||
Name: vol,
|
||
Diff: convertPlistDiffWithName(pd, nameForPath),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// convertMachoVolumeDiff turns the per-volume d.Machos map into an ordered
|
||
// slice the HTML template iterates. Empty volumes are omitted; volumes are
|
||
// emitted in canonical order (filesystem, SystemOS, AppOS, ExclaveOS) with
|
||
// any others trailing alphabetically.
|
||
func convertMachoVolumeDiff(m map[string]*mcmd.MachoDiff) []htmlMachoVolume {
|
||
if len(m) == 0 {
|
||
return nil
|
||
}
|
||
out := make([]htmlMachoVolume, 0, len(m))
|
||
for _, vol := range sortedVolumeKeys(m) {
|
||
md := m[vol]
|
||
if !machoDiffHasContent(md) {
|
||
continue
|
||
}
|
||
out = append(out, htmlMachoVolume{
|
||
Name: vol,
|
||
Diff: convertMachoDiff(md),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func sortedNamedLists(m map[string][]string) []htmlNamedList {
|
||
var out []htmlNamedList
|
||
for _, k := range slices.Sorted(maps.Keys(m)) {
|
||
out = append(out, htmlNamedList{
|
||
Name: k,
|
||
Items: slices.Sorted(slices.Values(m[k])),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func convertIBootDiff(ib *IBootDiff) *htmlIBootDiff {
|
||
if ib == nil {
|
||
return nil
|
||
}
|
||
|
||
out := &htmlIBootDiff{
|
||
Versions: append([]string(nil), ib.Versions...),
|
||
New: sortedNamedLists(ib.New),
|
||
Removed: sortedNamedLists(ib.Removed),
|
||
}
|
||
|
||
if len(out.Versions) == 0 && len(out.New) == 0 && len(out.Removed) == 0 {
|
||
return nil
|
||
}
|
||
|
||
return out
|
||
}
|
||
|
||
func convertPlistDiffWithName(pd *PlistDiff, nameForPath func(string) string) *htmlPlistDiff {
|
||
if pd == nil {
|
||
return nil
|
||
}
|
||
|
||
out := &htmlPlistDiff{
|
||
Removed: slices.Sorted(slices.Values(pd.Removed)),
|
||
}
|
||
|
||
for _, k := range slices.Sorted(maps.Keys(pd.New)) {
|
||
out.New = append(out.New, htmlPlistEntry{
|
||
Name: nameForPath(k),
|
||
Path: k,
|
||
Content: renderCodeBlock(pd.New[k]),
|
||
})
|
||
}
|
||
|
||
for _, k := range slices.Sorted(maps.Keys(pd.Updated)) {
|
||
out.Updated = append(out.Updated, htmlPlistEntry{
|
||
Name: nameForPath(k),
|
||
Path: k,
|
||
Content: renderMarkdownFragment(pd.Updated[k]),
|
||
})
|
||
}
|
||
|
||
if len(out.New) == 0 && len(out.Removed) == 0 && len(out.Updated) == 0 {
|
||
return nil
|
||
}
|
||
|
||
return out
|
||
}
|
||
|
||
func convertFileDiff(fd *FileDiff) *htmlFileDiff {
|
||
if fd == nil {
|
||
return nil
|
||
}
|
||
|
||
order := []string{"IPSW", "filesystem", "SystemOS", "AppOS", "ExclaveOS"}
|
||
out := &htmlFileDiff{}
|
||
|
||
for _, name := range order {
|
||
if items, ok := fd.New[name]; ok && len(items) > 0 {
|
||
out.New = append(out.New, htmlNamedList{Name: name, Items: slices.Sorted(slices.Values(items))})
|
||
}
|
||
}
|
||
|
||
for _, name := range order {
|
||
if items, ok := fd.Removed[name]; ok && len(items) > 0 {
|
||
out.Removed = append(out.Removed, htmlNamedList{Name: name, Items: slices.Sorted(slices.Values(items))})
|
||
}
|
||
}
|
||
|
||
if len(out.New) == 0 && len(out.Removed) == 0 {
|
||
return nil
|
||
}
|
||
|
||
return out
|
||
}
|
||
|
||
// htmlSectionAnchors maps each in-scope renderer's JSONKey() to the stable
|
||
// section anchor ID the outer template embeds in <h2 id="...">. These IDs
|
||
// must not change without coordinating with downstream link consumers.
|
||
//
|
||
// Top-level task fragments (kexts, kdks, firmwares, iboot, sandbox)
|
||
// embed their own `<hN id="...">` heading inside Body, so they do not
|
||
// participate in the outer-template anchor wrapping handled by this
|
||
// map; they still appear here as documentation of the canonical
|
||
// section ID each fragment owns.
|
||
var htmlSectionAnchors = map[string]string{
|
||
"kexts": "kexts",
|
||
"kdks": "kdks",
|
||
"machos": "machos",
|
||
"ents": "entitlements",
|
||
"firmwares": "firmwares",
|
||
"iboot": "iboot",
|
||
"launchd": "launchd",
|
||
"sandbox": "sandbox-profiles",
|
||
"files": "files",
|
||
"localizations": "localizations",
|
||
"features": "feature-flags",
|
||
}
|
||
|
||
// buildHTMLSectionFragment calls r.HTML(), then wraps the result in the
|
||
// outer-template view that the page renderer expects. Empty tasks produce
|
||
// a fragment with Empty=true so the outer template renders nothing.
|
||
func buildHTMLSectionFragment(r interface {
|
||
JSONKey() string
|
||
Empty() bool
|
||
HTML() (HTMLFragment, error)
|
||
}) (htmlSectionFragment, error) {
|
||
anchor, ok := htmlSectionAnchors[r.JSONKey()]
|
||
if !ok {
|
||
return htmlSectionFragment{}, fmt.Errorf("no html anchor registered for task %q", r.JSONKey())
|
||
}
|
||
if r.Empty() {
|
||
return htmlSectionFragment{Anchor: anchor, Empty: true}, nil
|
||
}
|
||
frag, err := r.HTML()
|
||
if err != nil {
|
||
return htmlSectionFragment{}, err
|
||
}
|
||
return htmlSectionFragment{
|
||
Anchor: anchor,
|
||
Heading: frag.Heading,
|
||
Body: frag.Body,
|
||
}, nil
|
||
}
|
||
|
||
func (d *Diff) renderHTML() (string, error) {
|
||
var htmlBuf bytes.Buffer
|
||
|
||
// Top-level tasks (kexts/kdks/firmwares/iboot/sandbox) and mount-based
|
||
// renderers (machos/ents/launchd/files/localizations/features) all
|
||
// expose HTML()/Empty(); build one HTML fragment per task in source order.
|
||
kexts := newKextsTask(d)
|
||
kdks := newKDKsTask(d)
|
||
machos := newMachosRenderer(d.Machos)
|
||
ents := newEntsRenderer(d.Ents)
|
||
firmwares := newFirmwaresTask(d)
|
||
iboot := newIBootTask(d)
|
||
launchd := newLaunchdRenderer(d.Launchd)
|
||
sandbox := newSandboxTask(d)
|
||
files := newFilesRenderer(d.Files)
|
||
locs := newLocsRenderer(d.Localizations)
|
||
features := newFeaturesRenderer(d.Features)
|
||
|
||
kextsFrag, err := buildHTMLSectionFragment(kexts)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
kdksFrag, err := buildHTMLSectionFragment(kdks)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
machosFrag, err := buildHTMLSectionFragment(machos)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
entsFrag, err := buildHTMLSectionFragment(ents)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
firmwaresFrag, err := buildHTMLSectionFragment(firmwares)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
ibootFrag, err := buildHTMLSectionFragment(iboot)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
launchdFrag, err := buildHTMLSectionFragment(launchd)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
sandboxFrag, err := buildHTMLSectionFragment(sandbox)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
filesFrag, err := buildHTMLSectionFragment(files)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
locsFrag, err := buildHTMLSectionFragment(locs)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
featuresFrag, err := buildHTMLSectionFragment(features)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
data := diffHTMLPageData{
|
||
Title: d.Title,
|
||
OldInput: filepath.Base(d.Old.IPSWPath),
|
||
NewInput: filepath.Base(d.New.IPSWPath),
|
||
OldVersion: d.Old.Version,
|
||
OldBuild: d.Old.Build,
|
||
NewVersion: d.New.Version,
|
||
NewBuild: d.New.Build,
|
||
OldWebkit: d.Old.Webkit,
|
||
NewWebkit: d.New.Webkit,
|
||
Kexts: convertMachoDiff(d.Kexts),
|
||
Machos: convertMachoVolumeDiff(d.Machos),
|
||
Dylibs: convertMachoDiff(d.Dylibs),
|
||
Firmwares: convertMachoDiff(d.Firmwares),
|
||
IBoot: convertIBootDiff(d.IBoot),
|
||
Features: convertPlistVolumeDiff(d.Features, filepath.Base),
|
||
Files: convertFileDiff(d.Files),
|
||
Localizations: convertPlistVolumeDiff(d.Localizations, localizationDisplayName),
|
||
Ents: convertEntitlementsDiff(d.Ents),
|
||
KDKs: renderMarkdownFragment(d.KDKs),
|
||
Sandbox: renderMarkdownFragment(d.Sandbox),
|
||
KextsFragment: kextsFrag,
|
||
KDKsFragment: kdksFrag,
|
||
MachosFragment: machosFrag,
|
||
EntsFragment: entsFrag,
|
||
FirmwaresFragment: firmwaresFrag,
|
||
IBootFragment: ibootFrag,
|
||
LaunchdFragment: launchdFrag,
|
||
SandboxFragment: sandboxFrag,
|
||
FilesFragment: filesFrag,
|
||
LocalizationsFragment: locsFrag,
|
||
FeaturesFragment: featuresFrag,
|
||
}
|
||
|
||
if d.Old.Kernel.Version != nil {
|
||
data.HasKernelVersion = true
|
||
data.OldKernelDarwin = d.Old.Kernel.Version.KernelVersion.Darwin
|
||
data.OldKernelXNU = d.Old.Kernel.Version.KernelVersion.XNU
|
||
data.OldKernelDate = d.Old.Kernel.Version.KernelVersion.Date.Format(
|
||
"Mon, 02 Jan 2006 15:04:05 MST",
|
||
)
|
||
}
|
||
if d.New.Kernel.Version != nil {
|
||
data.NewKernelDarwin = d.New.Kernel.Version.KernelVersion.Darwin
|
||
data.NewKernelXNU = d.New.Kernel.Version.KernelVersion.XNU
|
||
data.NewKernelDate = d.New.Kernel.Version.KernelVersion.Date.Format(
|
||
"Mon, 02 Jan 2006 15:04:05 MST",
|
||
)
|
||
}
|
||
|
||
if err := diffHTMLTmpl.Execute(&htmlBuf, data); err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return htmlBuf.String(), nil
|
||
}
|
||
|
||
func (d *Diff) ToHTML() error {
|
||
rendered, err := d.renderHTML()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if len(d.conf.Output) == 0 {
|
||
fmt.Println(rendered)
|
||
return nil
|
||
}
|
||
|
||
if err := os.MkdirAll(d.conf.Output, 0755); err != nil {
|
||
return err
|
||
}
|
||
|
||
fname := filepath.Join(d.conf.Output, d.TitleToFilename()+".html")
|
||
log.Infof("Creating HTML diff file: %s", fname)
|
||
return os.WriteFile(fname, []byte(rendered), 0644)
|
||
}
|