Files

372 lines
11 KiB
Go

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 TestWalkVolumeAPFSFuseRootPrefix(t *testing.T) {
root := t.TempDir()
writePlist(t, root, "/root/System/Library/LaunchDaemons/com.apple.rooted.plist", map[string]any{
"Label": "com.apple.rooted",
"Program": "/usr/libexec/rooted",
}, plist.XMLFormat)
writePlist(t, root, "/root/Applications/Foo.app/XPCServices/FooService.xpc/Info.plist", map[string]any{
"CFBundleIdentifier": "com.apple.FooService",
"CFBundleExecutable": "FooService",
"XPCService": map[string]any{
"ServiceType": "Application",
},
}, plist.XMLFormat)
records, err := WalkVolume(root, "FileSystem")
if err != nil {
t.Fatal(err)
}
if len(records) != 2 {
t.Fatalf("expected launchd and XPC records under apfs-fuse root, got %d", len(records))
}
if findRecord(records, "/System/Library/LaunchDaemons/com.apple.rooted.plist") == nil {
t.Fatalf("missing launchd daemon with normalized path: %#v", records)
}
if findRecord(records, "/Applications/Foo.app/XPCServices/FooService.xpc/Info.plist") == nil {
t.Fatalf("missing XPC bundle with normalized path: %#v", records)
}
for _, record := range records {
if strings.HasPrefix(record.PlistPath, "/root/") {
t.Fatalf("plist_path kept apfs-fuse root prefix: %#v", record)
}
}
}
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
}