feat: add launchd command to stream plist metadata as JSONL

This commit is contained in:
blacktop
2026-05-06 19:17:34 -06:00
parent 1f519f9393
commit fe32f0a823
3 changed files with 986 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
/*
Copyright © 2018-2026 blacktop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"fmt"
"os"
"strings"
"github.com/MakeNowJust/heredoc/v2"
"github.com/apex/log"
"github.com/blacktop/ipsw/pkg/launchd"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
rootCmd.AddCommand(launchdCmd)
launchdCmd.Flags().String("format", "jsonl", "Output format (only jsonl is supported)")
launchdCmd.Flags().String("pem-db", "", "AEA PEM DB JSON file path")
viper.BindPFlag("launchd.format", launchdCmd.Flags().Lookup("format"))
viper.BindPFlag("launchd.pem-db", launchdCmd.Flags().Lookup("pem-db"))
launchdCmd.MarkZshCompPositionalArgumentFile(1, "*.ipsw", "*.zip")
}
var launchdCmd = &cobra.Command{
Use: "launchd <IPSW>",
Short: "Stream launchd and XPC plist metadata as JSONL",
Args: cobra.ExactArgs(1),
SilenceErrors: true,
SilenceUsage: true,
Hidden: true,
Example: heredoc.Doc(`
# Emit launchd/XPC metadata from an IPSW as deterministic JSONL
ipsw launchd iPhone18,1_26.0_23A5276f_Restore.ipsw --format jsonl`),
RunE: func(cmd *cobra.Command, args []string) error {
format := strings.ToLower(strings.TrimSpace(viper.GetString("launchd.format")))
if format != "jsonl" {
return fmt.Errorf("unsupported --format %q (only \"jsonl\" is supported)", viper.GetString("launchd.format"))
}
records, skipped, err := launchd.WalkIPSW(expandPath(args[0]), &launchd.IPSWConfig{
PemDB: viper.GetString("launchd.pem-db"),
})
for _, skip := range skipped {
log.Warnf("skipped %s volume: %v", skip.Volume, skip.Err)
}
if err != nil {
return err
}
out, err := launchd.EncodeJSONL(records)
if err != nil {
return err
}
_, err = os.Stdout.Write(out)
return err
},
}
+568
View File
@@ -0,0 +1,568 @@
package launchd
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/blacktop/go-plist"
"github.com/blacktop/ipsw/internal/commands/mount"
)
const (
SourceKindLaunchdDaemon = "launchd_daemon"
SourceKindLaunchdAgent = "launchd_agent"
SourceKindNanoLaunchdDaemon = "nano_launchd_daemon"
SourceKindXPCBundle = "xpc_bundle"
SourceKindAppXPC = "app_xpc"
)
type ipswVolume struct {
name string
typ string
}
type launchdPlistDir struct {
kind string
dir string
}
var ipswFilesystemVolumes = []ipswVolume{
{name: "AppOS", typ: "app"},
{name: "FileSystem", typ: "fs"},
{name: "SystemOS", typ: "sys"},
{name: "ExclaveOS", typ: "exc"},
}
var launchdPlistDirs = []launchdPlistDir{
{kind: SourceKindLaunchdDaemon, dir: "/System/Library/LaunchDaemons"},
{kind: SourceKindLaunchdAgent, dir: "/System/Library/LaunchAgents"},
{kind: SourceKindNanoLaunchdDaemon, dir: "/System/Library/NanoLaunchDaemons"},
}
var launchdCapturedTopLevelKeys = map[string]bool{
"Label": true,
"Program": true,
"ProgramArguments": true,
"MachServices": true,
"SandboxProfile": true,
"POSIXSpawnType": true,
"ProcessType": true,
"CFBundleIdentifier": true,
}
var bundleCapturedTopLevelKeys = map[string]bool{
"CFBundleIdentifier": true,
"CFBundleExecutable": true,
"Program": true,
"ProgramArguments": true,
"SeatbeltProfiles": true,
"POSIXSpawnType": true,
"ProcessType": true,
}
type Record struct {
SourceKind string `json:"source_kind"`
PlistPath string `json:"plist_path"`
Volume string `json:"volume"`
PlistDigest string `json:"plist_digest"`
Label string `json:"label"`
Program string `json:"program"`
BundleID string `json:"bundle_id"`
MachServices []string `json:"mach_services"`
SandboxProfile string `json:"sandbox_profile"`
ServiceType string `json:"service_type"`
Extra map[string]any `json:"extra"`
}
type IPSWConfig struct {
PemDB string
}
type SkippedVolume struct {
Volume string
Type string
Err error
}
func (s SkippedVolume) Error() string {
if s.Err == nil {
return fmt.Sprintf("%s: skipped", s.Volume)
}
return fmt.Sprintf("%s: %v", s.Volume, s.Err)
}
func WalkIPSW(path string, cfg *IPSWConfig) ([]Record, []SkippedVolume, error) {
if cfg == nil {
cfg = &IPSWConfig{}
}
var records []Record
var skipped []SkippedVolume
mounted := 0
seenDMGs := make(map[string]struct{})
for _, vol := range ipswFilesystemVolumes {
ctx, err := mount.DmgInIPSW(path, vol.typ, &mount.Config{PemDB: cfg.PemDB})
if err != nil {
skipped = append(skipped, SkippedVolume{Volume: vol.name, Type: vol.typ, Err: err})
continue
}
dmgPath := filepath.Clean(ctx.DmgPath)
if _, seen := seenDMGs[dmgPath]; seen {
if !ctx.AlreadyMounted {
if err := ctx.Unmount(); err != nil {
skipped = append(skipped, SkippedVolume{Volume: vol.name, Type: vol.typ, Err: fmt.Errorf("unmount failed: %w", err)})
}
}
continue
}
seenDMGs[dmgPath] = struct{}{}
mounted++
volumeRecords, walkErr := WalkVolume(ctx.MountPoint, vol.name)
var unmountErr error
if !ctx.AlreadyMounted {
unmountErr = ctx.Unmount()
}
if walkErr != nil {
skipped = append(skipped, SkippedVolume{Volume: vol.name, Type: vol.typ, Err: walkErr})
if unmountErr != nil {
skipped = append(skipped, SkippedVolume{Volume: vol.name, Type: vol.typ, Err: fmt.Errorf("unmount failed: %w", unmountErr)})
}
continue
}
records = append(records, volumeRecords...)
if unmountErr != nil {
skipped = append(skipped, SkippedVolume{Volume: vol.name, Type: vol.typ, Err: fmt.Errorf("unmount failed: %w", unmountErr)})
}
}
if mounted == 0 {
return nil, skipped, errors.New("no IPSW filesystem DMGs mounted successfully")
}
SortRecords(records)
return records, skipped, nil
}
func WalkVolume(root, volume string) ([]Record, error) {
var candidates []plistCandidate
seen := make(map[string]struct{})
addCandidate := func(kind, path string) {
rel := relativePlistPath(root, path)
key := kind + "\x00" + rel
if _, ok := seen[key]; ok {
return
}
seen[key] = struct{}{}
candidates = append(candidates, plistCandidate{kind: kind, path: path})
}
for _, spec := range launchdPlistDirs {
matches, err := filepath.Glob(filepath.Join(root, filepath.FromSlash(spec.dir), "*.plist"))
if err != nil {
return nil, err
}
for _, match := range matches {
addCandidate(spec.kind, match)
}
}
if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
if os.IsPermission(err) {
return nil
}
return err
}
if d.IsDir() || d.Name() != "Info.plist" {
return nil
}
rel := relativePlistPath(root, path)
switch {
case isXPCInfoPlist(rel):
addCandidate(SourceKindXPCBundle, path)
case strings.HasSuffix(rel, ".app/Info.plist"):
addCandidate(SourceKindAppXPC, path)
}
return nil
}); err != nil {
return nil, err
}
sort.Slice(candidates, func(i, j int) bool {
if candidates[i].path != candidates[j].path {
return candidates[i].path < candidates[j].path
}
return candidates[i].kind < candidates[j].kind
})
records := make([]Record, 0, len(candidates))
for _, candidate := range candidates {
record, ok := recordFromFile(root, volume, candidate.kind, candidate.path)
if ok {
records = append(records, record)
}
}
SortRecords(records)
return records, nil
}
func EncodeJSONL(records []Record) ([]byte, error) {
SortRecords(records)
var buf bytes.Buffer
for _, record := range records {
line, err := MarshalRecord(record)
if err != nil {
return nil, err
}
buf.Write(line)
buf.WriteByte('\n')
}
return buf.Bytes(), nil
}
func SortRecords(records []Record) {
sort.SliceStable(records, func(i, j int) bool {
if records[i].Volume != records[j].Volume {
return records[i].Volume < records[j].Volume
}
if records[i].PlistPath != records[j].PlistPath {
return records[i].PlistPath < records[j].PlistPath
}
return records[i].SourceKind < records[j].SourceKind
})
}
func MarshalRecord(record Record) ([]byte, error) {
if record.Extra == nil {
record.Extra = map[string]any{}
}
top := map[string]any{
"source_kind": record.SourceKind,
"plist_path": record.PlistPath,
"volume": record.Volume,
"plist_digest": record.PlistDigest,
"label": record.Label,
"program": record.Program,
"bundle_id": record.BundleID,
"mach_services": sortedStringSlice(record.MachServices),
"sandbox_profile": record.SandboxProfile,
"service_type": record.ServiceType,
"extra": canonicalJSONValue(record.Extra),
}
return marshalCompactJSON(top)
}
type plistCandidate struct {
kind string
path string
}
func recordFromFile(root, volume, sourceKind, path string) (Record, bool) {
rel := relativePlistPath(root, path)
raw, err := os.ReadFile(path)
if err != nil {
return parseErrorRecord(sourceKind, rel, volume, "", err), true
}
digest := rawDigest(raw)
doc, canonicalDigest, err := parsePlist(raw)
if err != nil {
return parseErrorRecord(sourceKind, rel, volume, digest, err), true
}
digest = canonicalDigest
if sourceKind == SourceKindAppXPC && !hasXPCService(doc) {
return Record{}, false
}
record := Record{
SourceKind: sourceKind,
PlistPath: rel,
Volume: volume,
PlistDigest: digest,
Label: labelFor(sourceKind, doc),
BundleID: stringValue(doc["CFBundleIdentifier"]),
MachServices: machServicesFor(sourceKind, doc),
Program: programFor(sourceKind, rel, doc),
Extra: extraFor(sourceKind, doc),
}
record.SandboxProfile = sandboxProfileFor(sourceKind, doc)
record.ServiceType = serviceTypeFor(doc)
return record, true
}
func parseErrorRecord(sourceKind, relPath, volume, digest string, err error) Record {
return Record{
SourceKind: sourceKind,
PlistPath: relPath,
Volume: volume,
PlistDigest: digest,
MachServices: []string{},
Extra: map[string]any{"parse_error": err.Error()},
}
}
func parsePlist(raw []byte) (map[string]any, string, error) {
var doc map[string]any
if _, err := plist.Unmarshal(raw, &doc); err != nil {
return nil, "", err
}
canonical, err := plist.Marshal(doc, plist.XMLFormat)
if err != nil {
return nil, "", err
}
sum := sha256.Sum256(canonical)
return doc, hex.EncodeToString(sum[:]), nil
}
func rawDigest(raw []byte) string {
sum := sha256.Sum256(raw)
return hex.EncodeToString(sum[:])
}
func relativePlistPath(root, path string) string {
rel, err := filepath.Rel(root, path)
if err != nil {
rel = path
}
return "/" + filepath.ToSlash(filepath.Clean(rel))
}
func isXPCInfoPlist(rel string) bool {
return strings.HasSuffix(rel, ".xpc/Info.plist") ||
strings.HasSuffix(rel, ".xpc/Contents/Info.plist")
}
func labelFor(sourceKind string, doc map[string]any) string {
if isLaunchdKind(sourceKind) {
return stringValue(doc["Label"])
}
return stringValue(doc["CFBundleIdentifier"])
}
func programFor(sourceKind, rel string, doc map[string]any) string {
if program := stringValue(doc["Program"]); program != "" {
return program
}
if args := stringSlice(doc["ProgramArguments"]); len(args) > 0 {
return args[0]
}
executable := stringValue(doc["CFBundleExecutable"])
if executable == "" {
return ""
}
if strings.Contains(executable, "/") {
return filepath.ToSlash(filepath.Clean(executable))
}
if sourceKind == SourceKindXPCBundle && strings.HasSuffix(rel, ".xpc/Contents/Info.plist") {
return filepath.ToSlash(filepath.Join("Contents", "MacOS", executable))
}
return executable
}
func machServicesFor(sourceKind string, doc map[string]any) []string {
if isLaunchdKind(sourceKind) {
if machServices, ok := mapValue(doc["MachServices"]); ok {
keys := make([]string, 0, len(machServices))
for key := range machServices {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
return []string{}
}
bundleID := stringValue(doc["CFBundleIdentifier"])
if bundleID == "" {
return []string{}
}
return []string{bundleID}
}
func sandboxProfileFor(sourceKind string, doc map[string]any) string {
if isLaunchdKind(sourceKind) {
return stringValue(doc["SandboxProfile"])
}
if profiles := stringSlice(doc["SeatbeltProfiles"]); len(profiles) > 0 {
return profiles[0]
}
if xpc, ok := mapValue(doc["XPCService"]); ok {
if profiles := stringSlice(xpc["SeatbeltProfiles"]); len(profiles) > 0 {
return profiles[0]
}
}
return ""
}
func serviceTypeFor(doc map[string]any) string {
if xpc, ok := mapValue(doc["XPCService"]); ok {
if serviceType := stringValue(xpc["ServiceType"]); serviceType != "" {
return serviceType
}
}
if spawnType := stringValue(doc["POSIXSpawnType"]); spawnType != "" {
return spawnType
}
return stringValue(doc["ProcessType"])
}
func extraFor(sourceKind string, doc map[string]any) map[string]any {
extra := make(map[string]any)
captured := capturedTopLevelKeys(sourceKind)
for key, value := range doc {
if captured[key] {
continue
}
if key == "XPCService" {
if xpcExtra, ok := xpcServiceExtra(value); ok {
extra[key] = xpcExtra
}
continue
}
extra[key] = value
}
return canonicalJSONValue(extra).(map[string]any)
}
func capturedTopLevelKeys(sourceKind string) map[string]bool {
if isLaunchdKind(sourceKind) {
return launchdCapturedTopLevelKeys
}
return bundleCapturedTopLevelKeys
}
func xpcServiceExtra(value any) (map[string]any, bool) {
xpc, ok := mapValue(value)
if !ok {
return nil, false
}
out := make(map[string]any)
for key, val := range xpc {
if key == "ServiceType" || key == "SeatbeltProfiles" {
continue
}
out[key] = val
}
if len(out) == 0 {
return nil, false
}
return canonicalJSONValue(out).(map[string]any), true
}
func hasXPCService(doc map[string]any) bool {
_, ok := mapValue(doc["XPCService"])
return ok
}
func isLaunchdKind(sourceKind string) bool {
switch sourceKind {
case SourceKindLaunchdDaemon, SourceKindLaunchdAgent, SourceKindNanoLaunchdDaemon:
return true
default:
return false
}
}
func mapValue(v any) (map[string]any, bool) {
m, ok := v.(map[string]any)
return m, ok
}
func stringValue(v any) string {
s, _ := stringValueOK(v)
return s
}
func stringValueOK(v any) (string, bool) {
switch value := v.(type) {
case string:
return value, true
case fmt.Stringer:
return value.String(), true
default:
return "", false
}
}
func stringSlice(v any) []string {
switch values := v.(type) {
case []string:
return append([]string(nil), values...)
case []any:
out := make([]string, 0, len(values))
for _, value := range values {
if s, ok := stringValueOK(value); ok {
out = append(out, s)
}
}
return out
default:
return nil
}
}
func sortedStringSlice(values []string) []string {
out := append([]string{}, values...)
sort.Strings(out)
return out
}
func canonicalJSONValue(v any) any {
switch value := v.(type) {
case map[string]any:
out := make(map[string]any, len(value))
for key, sub := range value {
out[key] = canonicalJSONValue(sub)
}
return out
case []any:
out := make([]any, 0, len(value))
for _, sub := range value {
out = append(out, canonicalJSONValue(sub))
}
sort.Slice(out, func(i, j int) bool {
return canonicalJSONKey(out[i]) < canonicalJSONKey(out[j])
})
return out
case []string:
out := append([]string(nil), value...)
sort.Strings(out)
return out
case []byte:
return base64.StdEncoding.EncodeToString(value)
case time.Time:
return value.UTC().Format(time.RFC3339Nano)
default:
return value
}
}
func canonicalJSONKey(v any) string {
data, err := marshalCompactJSON(v)
if err != nil {
return fmt.Sprintf("%T:%v", v, v)
}
return string(data)
}
func marshalCompactJSON(v any) ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
return nil, err
}
return bytes.TrimSuffix(buf.Bytes(), []byte("\n")), nil
}
+337
View File
@@ -0,0 +1,337 @@
package launchd
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/blacktop/go-plist"
)
func TestWalkVolumeLaunchdMachServices(t *testing.T) {
root := t.TempDir()
writePlist(t, root, "/System/Library/LaunchDaemons/com.apple.test.plist", map[string]any{
"Label": "com.apple.test",
"CFBundleIdentifier": "com.apple.test.bundle",
"ProgramArguments": []string{"/usr/libexec/testd", "--flag"},
"MachServices": map[string]any{
"com.apple.test.b": true,
"com.apple.test.a": map[string]any{"ResetAtClose": true},
},
"SandboxProfile": "testd",
"POSIXSpawnType": "Adaptive",
"RunAtLoad": true,
}, plist.XMLFormat)
records, err := WalkVolume(root, "SystemOS")
if err != nil {
t.Fatal(err)
}
if len(records) != 1 {
t.Fatalf("expected 1 record, got %d", len(records))
}
record := records[0]
if record.SourceKind != SourceKindLaunchdDaemon {
t.Fatalf("source kind = %q", record.SourceKind)
}
if record.Label != "com.apple.test" {
t.Fatalf("label = %q", record.Label)
}
if record.BundleID != "com.apple.test.bundle" {
t.Fatalf("bundle id = %q", record.BundleID)
}
if record.Program != "/usr/libexec/testd" {
t.Fatalf("program = %q", record.Program)
}
if got, want := strings.Join(record.MachServices, ","), "com.apple.test.a,com.apple.test.b"; got != want {
t.Fatalf("mach services = %q, want %q", got, want)
}
if record.SandboxProfile != "testd" {
t.Fatalf("sandbox profile = %q", record.SandboxProfile)
}
if record.ServiceType != "Adaptive" {
t.Fatalf("service type = %q", record.ServiceType)
}
if record.Extra["RunAtLoad"] != true {
t.Fatalf("RunAtLoad missing from extra: %#v", record.Extra)
}
if _, ok := record.Extra["MachServices"]; ok {
t.Fatalf("captured MachServices leaked into extra: %#v", record.Extra)
}
if _, ok := record.Extra["CFBundleIdentifier"]; ok {
t.Fatalf("captured CFBundleIdentifier leaked into extra: %#v", record.Extra)
}
}
func TestWalkVolumeXPCBundleLayouts(t *testing.T) {
root := t.TempDir()
writePlist(t, root, "/System/Library/PrivateFrameworks/Foo.framework/XPCServices/FooService.xpc/Info.plist", map[string]any{
"CFBundleIdentifier": "com.apple.FooService",
"CFBundleExecutable": "FooService",
"XPCService": map[string]any{
"ServiceType": "Application",
"SeatbeltProfiles": []string{"foo-sandbox"},
"JoinExistingSession": true,
},
}, plist.XMLFormat)
writePlist(t, root, "/System/Library/PrivateFrameworks/Bar.framework/XPCServices/BarService.xpc/Contents/Info.plist", map[string]any{
"CFBundleIdentifier": "com.apple.BarService",
"CFBundleExecutable": "BarService",
"XPCService": map[string]any{
"ServiceType": "Application",
},
}, plist.XMLFormat)
records, err := WalkVolume(root, "SystemOS")
if err != nil {
t.Fatal(err)
}
if len(records) != 2 {
t.Fatalf("expected 2 records, got %d", len(records))
}
flat := findRecord(records, "/System/Library/PrivateFrameworks/Foo.framework/XPCServices/FooService.xpc/Info.plist")
if flat == nil {
t.Fatal("missing flat XPC record")
}
if flat.Program != "FooService" {
t.Fatalf("flat program = %q", flat.Program)
}
if got := strings.Join(flat.MachServices, ","); got != "com.apple.FooService" {
t.Fatalf("flat mach services = %q", got)
}
if flat.SandboxProfile != "foo-sandbox" {
t.Fatalf("flat sandbox profile = %q", flat.SandboxProfile)
}
if flat.ServiceType != "Application" {
t.Fatalf("flat service type = %q", flat.ServiceType)
}
xpcExtra, ok := flat.Extra["XPCService"].(map[string]any)
if !ok || xpcExtra["JoinExistingSession"] != true {
t.Fatalf("unexpected XPCService extra: %#v", flat.Extra["XPCService"])
}
if _, ok := xpcExtra["ServiceType"]; ok {
t.Fatalf("captured ServiceType leaked into extra: %#v", xpcExtra)
}
contents := findRecord(records, "/System/Library/PrivateFrameworks/Bar.framework/XPCServices/BarService.xpc/Contents/Info.plist")
if contents == nil {
t.Fatal("missing Contents XPC record")
}
if contents.Program != "Contents/MacOS/BarService" {
t.Fatalf("Contents program = %q", contents.Program)
}
}
func TestWalkVolumeAppXPCAndPlainApp(t *testing.T) {
root := t.TempDir()
writePlist(t, root, "/Applications/HasService.app/Info.plist", map[string]any{
"CFBundleIdentifier": "com.apple.HasService",
"CFBundleExecutable": "HasService",
"XPCService": map[string]any{
"ServiceType": "Application",
},
}, plist.XMLFormat)
writePlist(t, root, "/Applications/Plain.app/Info.plist", map[string]any{
"CFBundleIdentifier": "com.apple.Plain",
"CFBundleExecutable": "Plain",
}, plist.XMLFormat)
records, err := WalkVolume(root, "AppOS")
if err != nil {
t.Fatal(err)
}
if len(records) != 1 {
t.Fatalf("expected only the app with XPCService, got %d records", len(records))
}
record := records[0]
if record.SourceKind != SourceKindAppXPC {
t.Fatalf("source kind = %q", record.SourceKind)
}
if record.Program != "HasService" {
t.Fatalf("program = %q", record.Program)
}
}
func TestWalkVolumeMissingProgram(t *testing.T) {
root := t.TempDir()
writePlist(t, root, "/System/Library/LaunchAgents/com.apple.noprogram.plist", map[string]any{
"Label": "com.apple.noprogram",
}, plist.XMLFormat)
records, err := WalkVolume(root, "SystemOS")
if err != nil {
t.Fatal(err)
}
if len(records) != 1 {
t.Fatalf("expected 1 record, got %d", len(records))
}
if records[0].Program != "" {
t.Fatalf("program = %q", records[0].Program)
}
}
func TestWalkVolumeProgramArgumentsKeepsEmptyFirstArgument(t *testing.T) {
root := t.TempDir()
writePlist(t, root, "/System/Library/LaunchDaemons/com.apple.emptyarg.plist", map[string]any{
"Label": "com.apple.emptyarg",
"ProgramArguments": []any{"", "/usr/libexec/should-not-shift"},
}, plist.XMLFormat)
records, err := WalkVolume(root, "SystemOS")
if err != nil {
t.Fatal(err)
}
if len(records) != 1 {
t.Fatalf("expected 1 record, got %d", len(records))
}
if records[0].Program != "" {
t.Fatalf("program = %q, want empty ProgramArguments[0]", records[0].Program)
}
}
func TestWalkVolumeParseErrorPassthrough(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "System/Library/LaunchAgents/bad.plist")
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte("not a plist"), 0644); err != nil {
t.Fatal(err)
}
records, err := WalkVolume(root, "SystemOS")
if err != nil {
t.Fatal(err)
}
if len(records) != 1 {
t.Fatalf("expected 1 record, got %d", len(records))
}
record := records[0]
if record.SourceKind != SourceKindLaunchdAgent {
t.Fatalf("source kind = %q", record.SourceKind)
}
if record.PlistDigest == "" {
t.Fatal("parse error record has empty digest")
}
if record.Extra["parse_error"] == "" {
t.Fatalf("parse_error missing from extra: %#v", record.Extra)
}
}
func TestEncodeJSONLDeterministic(t *testing.T) {
records := []Record{
{
SourceKind: SourceKindXPCBundle,
PlistPath: "/b.plist",
Volume: "SystemOS",
PlistDigest: "2",
MachServices: []string{"z", "a"},
SandboxProfile: "",
Extra: map[string]any{
"array": []any{"b", "a"},
"dict": map[string]any{"z": []any{"2", "1"}},
"dsl<filter>": "kept literal",
},
},
{
SourceKind: SourceKindLaunchdDaemon,
PlistPath: "/a.plist",
Volume: "AppOS",
PlistDigest: "1",
Extra: map[string]any{},
},
}
first, err := EncodeJSONL(records)
if err != nil {
t.Fatal(err)
}
second, err := EncodeJSONL(records)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(first, second) {
t.Fatalf("encoding changed across runs:\n%s\n%s", first, second)
}
lines := bytes.Split(bytes.TrimSuffix(first, []byte("\n")), []byte("\n"))
if len(lines) != 2 {
t.Fatalf("expected 2 lines, got %d", len(lines))
}
if !bytes.Contains(lines[0], []byte(`"volume":"AppOS"`)) {
t.Fatalf("records not sorted by volume/path: %s", first)
}
if !bytes.Contains(lines[0], []byte(`"mach_services":[]`)) {
t.Fatalf("empty mach services encoded as non-array: %s", lines[0])
}
if !bytes.Contains(lines[1], []byte(`"mach_services":["a","z"]`)) {
t.Fatalf("mach services not sorted: %s", lines[1])
}
if !bytes.Contains(lines[1], []byte(`"array":["a","b"]`)) {
t.Fatalf("extra array not sorted: %s", lines[1])
}
if bytes.Contains(lines[1], []byte(`\u003c`)) || bytes.Contains(lines[1], []byte(`\u003e`)) {
t.Fatalf("HTML-sensitive characters were escaped: %s", lines[1])
}
var decoded map[string]any
if err := json.Unmarshal(lines[1], &decoded); err != nil {
t.Fatalf("invalid json: %v", err)
}
}
func TestPlistDigestEquivalence(t *testing.T) {
doc := map[string]any{
"Label": "com.apple.digest",
"RunAtLoad": true,
"MachServices": map[string]any{
"com.apple.digest": true,
},
}
xmlData, err := plist.Marshal(doc, plist.XMLFormat)
if err != nil {
t.Fatal(err)
}
binData, err := plist.Marshal(doc, plist.BinaryFormat)
if err != nil {
t.Fatal(err)
}
_, xmlDigest, err := parsePlist(xmlData)
if err != nil {
t.Fatal(err)
}
_, binDigest, err := parsePlist(binData)
if err != nil {
t.Fatal(err)
}
if xmlDigest != binDigest {
t.Fatalf("digest mismatch: xml=%s bin=%s", xmlDigest, binDigest)
}
}
func writePlist(t *testing.T, root, rel string, doc map[string]any, format int) {
t.Helper()
path := filepath.Join(root, filepath.FromSlash(strings.TrimPrefix(rel, "/")))
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatal(err)
}
data, err := plist.Marshal(doc, format)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
}
func findRecord(records []Record, path string) *Record {
for i := range records {
if records[i].PlistPath == path {
return &records[i]
}
}
return nil
}