8

I am writing a preferences editor tool (see http://www.tempel.org/PrefsEditor). It is effectively a GUI version of the defaults command.

I have trouble reading (let alone writing) preferences of random sandboxed applications, though.

For instance, when I try to get the keys of the Maps app, I get NULL returned:

CFArrayRef prefs = CFPreferencesCopyKeyList (CFSTR("com.apple.Maps"), kCFPreferencesCurrentUser, kCFPreferencesAnyHost);

However, the defaults command is able to read those prefs:

defaults read com.apple.Maps

I like to know how the defaults command accomplishes this, trying to do the same in my tool.

Thomas Tempelmann
  • 11,045
  • 8
  • 74
  • 149
  • 1
    By not being sandboxed? – trojanfoe Dec 20 '13 at 14:23
  • 1
    @trojanfoe is correct. The OS build-in commands are not sandboxed, so they have unlimited access to the system. Being sandboxed, you are limited to the data you can access -- much like `chroot` in the unix world. – Donovan Dec 20 '13 at 15:02
  • The solution, of course, is not to run in a sandboxed environment. Of course, this also means exclusion from the App Store. – Donovan Dec 20 '13 at 15:03
  • 3
    What gives you the idea that my app would be sandboxed? Where do I say that? I am talking about reading prefs of OTHER apps that are sandboxed. How can this be misunderstood? – Thomas Tempelmann Dec 20 '13 at 21:13

3 Answers3

4

try that:

CFPropertyListRef prop = CFPreferencesCopyValue(CFSTR("ElementsVersion"),
CFSTR("/Users/karsten/Library/Containers/com.apple.Maps/Data/Library/Preferences/com.apple.Maps"),
CFSTR("kCFPreferencesCurrentUser"),
CFSTR("kCFPreferencesAnyHost"));

seems you need the path to the file, not just the bundle-id

Karsten
  • 2,772
  • 17
  • 22
4

Karsten’s answer is correct but for the sake of completeness, the defaults command uses the undocumented _CFPreferencesCopyApplicationMap() function to retrieve the full URL of the preferences.

#import <CoreFoundation/CoreFoundation.h>

extern CFDictionaryRef _CFPreferencesCopyApplicationMap(CFStringRef userName, CFStringRef hostName);

int main(int argc, char *argv[])
{
    @autoreleasepool
    {
        CFDictionaryRef applicationMap = _CFPreferencesCopyApplicationMap(kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
        CFArrayRef urls = CFDictionaryGetValue(applicationMap, CFSTR("com.apple.mail"));
        CFShow(urls);
        CFRelease(applicationMap);
    }
}
0xced
  • 25,219
  • 10
  • 103
  • 255
  • Another awesome answer. I'll keep that in mind as a fallback method in case the other way gets broken in the future. Did you use Hopper to figure this out, BTW? (I was going to do that myself if Karsten hadn't come up with the answer) – Thomas Tempelmann Dec 21 '13 at 13:22
  • 1
    I also used Hopper but I found it with `(lldb) br set -r CFPreferencesCopy.*` first. – 0xced Dec 21 '13 at 13:31
1

I've added to 0xced's excellent answer so that the code can be packaged into a command-line tool that accepts the bundle ID as an argument. Forgive me if this is obvious to experienced Mac programmers, but as someone who has never used CoreFoundation I found this to be non-trivial.

#import <CoreFoundation/CoreFoundation.h>

extern CFDictionaryRef _CFPreferencesCopyApplicationMap(CFStringRef userName, CFStringRef hostName);

int main(int argc, char *argv[]) {
    @autoreleasepool {
        if (argc < 2) {
            // Print usage string & exit.
            fprintf(stderr, "usage: GetPrefDomains bundle_id\n");
            exit(1);
        }
        
        // Get the bundle ID from the first command-line argument.
        CFStringRef bundleID = CFStringCreateWithCString(NULL, argv[1], kCFStringEncodingUTF8);
        
        // Get the list of preference domain urls.
        CFDictionaryRef applicationMap = _CFPreferencesCopyApplicationMap(kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
        CFArrayRef urls = CFDictionaryGetValue(applicationMap, bundleID);
        
        // If no urls exist (invalid bundle ID), exit.
        if (!urls) {
            fprintf(stderr, "No preference domains found.\n");
            exit(0);
        }
        
        // Print the paths to the preference domains.
        CFIndex urlsCount = CFArrayGetCount(urls);
        for (int i = 0; i < urlsCount; i++) {
            CFURLRef url = CFArrayGetValueAtIndex(urls, i);
            CFStringRef path = CFURLCopyPath(url);
            printf("%s\n", CFStringGetCStringPtr(path, kCFStringEncodingUTF8));
        }
        
        // Clean up.
        CFRelease(bundleID);
        CFRelease(applicationMap);
    }
}

Save the code as GetPrefDomains.m, compile, and invoke as: GetPrefDomains com.apple.mail

This was useful to me because surprisingly the defaults command is case-sensitive and misbehaves silently with certain Apple applications that are under the filesystem protections in SIP on Mojave 10.14 or later (Safari & Mail, most notably). Add in the fact that Apple's capitalization rules are not consistent (com.apple.mail vs. com.apple.Notes), sandboxed preference paths, and the fact that that the filesystem is not case-sensitive and you quickly run into some very frustrating edge cases.

Thomas Tempelmann
  • 11,045
  • 8
  • 74
  • 149
David P.
  • 335
  • 3
  • 12