The Mythical LSSetApplicationForItem by Ben Artin

Since the day Snow Leopard came out, much has been said about creator codes, preferred applications, and Universal Type Identifiers. Regardless of whether you favor the Leopard behavior — in which a Mac OS 9-style creator code trumps a file‘s extension — or the Snow Leopard behavior — in which a Mac OS 9-style creator code is completely ignored — you, as a developer, may run into a case when you need to make sure that a particular file will open with a particular app when the user double-clicks it in the Finder.

We had the same need in Fetch; we allow Fetch users to specify which app a downloaded file will open with, because users sometimes want a downloaded file to open with a different application from a locally created file of the same type.

The general idea is to add a ‘usro’ resource with ID 0 to the file. This will take you about ten lines of code — then you will spend the next hundred lines of code doing things that make it all work well for the user.

void
FileRef::SetApplication(
  const string& inIdentifier,
  U::ApplicationRef inApplication)
{

This function takes two arguments (besides the implicit this, which represents the file you are changing): the Universal Type Identifier for the file’s type (such as ‘public.html’), and a reference to the application you want the file to open with.

An U::ApplicationRef is our wrapper around application bundles. It provides helpers for looking up bundle IDs, finding apps from a bundle ID, etc. Similarly, a FileRef is our wrapper around a Carbon FSRef. I am not including code for such wrappers here; they’s self-explanatory.

First, clean up any existing ‘usro’ resource in the file:

string path = inApplication.GetFile()->GetPath();
// Put a usro(0) resource in 
U::ResourceFileRef resFile(*this, fsRdWrPerm);
::Handle oldUsro = ::Get1Resource('usro', 0);
if (oldUsro) {
  ::RemoveResource(oldUsro);
}

Next, create a new ‘usro’ resource and add it to the file:

// Must be 1028 bytes long or Tiger freaks out
Handle newUsro(::NewHandleClear(PATH_MAX + sizeof(UInt32)));
if (path.length() > PATH_MAX - 1) {
  path.erase(path.begin() + PATH_MAX - 1, path.end());
}

**reinterpret_cast<UInt32**>(newUsro.Get()) = path.length();
copy(
  path.begin(), path.end(), 
  *reinterpret_cast<char**>(newUsro.Get()) + sizeof(UInt32)
);

::AddResource(newUsro.Get(), 'usro', 0, "\p");
ThrowIfResourceError();
newUsro.Abandon();

I think what that comment is trying to tell me is that Launch Services in some versions of Mac OS X 10.4 Tiger did not recognize a ‘usro’ resource unless it was exactly 1028 bytes long. Aside from that, this snippet contains the most salient information about the ‘usro’ resource: its first four bytes contain the length of the filesystem path to the application which will open when the file is double-clicked; the remaining 1024 bytes contain the path itself.

It’s rather interesting that we aren’t doing anything to make the 4 bytes of length at the front of the resource be of any particular endianness. According to our version history, this code predates the PPC-Intel transition, and therefore I can only assume that it used to work on PPC computers as well as it works on Intel computers today. This suggests that copying a file from a PPC computer to an Intel computer might render its ‘usro’ resource useless, depending on whether Launch Services knows to interpret ‘usro’ resources with non-native endianness.

If all you are looking for is a file that opens in the given app when double-clicked, you are all set. However, your users will probably be confused because the file’s icon will not reflect its new affiliation — you might now have a text file that opens in BBEdit, but the Finder will continue to show it with its default icon, which probably belongs to TextEdit. It would be nice to do something about that.

The app’s info dictionary holds information about which icon should be used:

// Track down the custom icon we should use for this file
string extension = GetFileNameExtension(this->GetName());

optional<string> iconFile;
U::CFDictionaryRef appInfo = inApplication.GetInfoDictionary();

// If the app has no info dictionary, it is probably an old Carbon or
// Classic app and may require the file creator to be set, so we set it
if (not appInfo) {
  this->SetCreatorAndType(inApplication.GetSignature(), this->GetType());
} else {
  this->SetCreatorAndType(kLSUnknownCreator, kLSUnknownType);
}

You may be inclined to summarily ignore old apps that don't have an info dictionary. If your customer support sometimes feels like archeology, you probably want to keep that section of the code.

Finally, extract the icons:

if (appInfo) {
  iconFile = IconFileInAppForType(appInfo, inIdentifier);
  if (not iconFile) {
     iconFile = IconFileInAppForExtension(appInfo, extension);
  }
}

I’ll elaborate on IconFileInAppForType and IconFileInAppForExtension later; for now, just assume you found the icon file name.

// Load the custom icon from app's resources or fall back on default
IconFamilyHandle iconFamily = 0;

if (iconFile) {
  // If the app is a bundle, use CFBundle to get at the icon
  if (not inApplication.GetFile()->IsFile()) {
    U::CFURLRef appURL(U::CFAdopt(::CFURLCreateFromFSRef(
      kCFAllocatorDefault,
      &inApplication.GetFile()->Get()
    )));
    
    U::CFURLRef icon = U::CFAdopt(::CFBundleCopyResourceURLInDirectory(
      appURL.get(),
      lexical_cast<U::CFStringRef>(iconFile.get()).get(),
      CFSTR("icns"),
      0
    ));

    // Try without extension if we can't find it with extension
    if (not icon) {
      icon = U::CFAdopt(::CFBundleCopyResourceURLInDirectory(
        appURL.get(),
        lexical_cast<U::CFStringRef>(iconFile.get()).get(),
        0,
        0
      ));
    }
    
    if (icon) {
      FSRef iconFile;
      if (::CFURLGetFSRef(icon.get(), &iconFile)) {
        ::ReadIconFromFSRef(&iconFile, &iconFamily);
      }
    }
  } else {
    SInt16 resID = lexical_cast<SInt16>(*iconFile);

    U::ResourceFileRef appResFile(*inApplication.GetFile(), fsRdPerm);
    
    // Try icns resource first
    iconFamily = reinterpret_cast<IconFamilyHandle>(
      ::Get1Resource(kIconFamilyType, resID)
    );
    if (iconFamily) {
      ::DetachResource(reinterpret_cast< ::Handle(iconFamily));
    }
    // Otherwise, use icon suite resources
    else {
      IconSuiteRef iconSuite = 0;
      ThrowIfOSError(::GetIconSuite(
        &iconSuite, resID, 
        kSelectorAllAvailableData
      ));
      ::IconSuiteToIconFamily(
        iconSuite, kSelectorAllAvailableData, 
        &iconFamily
      );
      ::DisposeIconSuite(iconSuite, true);
    }
  }
}

What just happened here? First off, an IconFamilyHandle is the Carbon way of representing the collection of several different resolutions of the same icon. I don’t know if there’s a newfangled way of managing those, without getting tangled in APIs from the last millennium, so IconFamilyHandle it is.

Second, an app might be a bundle (a maze of twisty little files and directories), or it might be a monolithic file (a maze of twisty little resources).

If it’s a bundle, then the icon filename you got earlier refers to an icon file somewhere inside the bundle — which CFBundle APIs will happily hand over to you. Except when they won’t, because some developers put the complete icon filename (including the extension) in their info dictionary, and some omit the extension, letting the OS tack on ‘icns’ as needed. Therefore, you need to look for the icon in two places.

If the app is not a bundle, then the icon filename you got from the info dictionary is not a filename at all — it’s a resource ID. It might refer to a single ‘icns’ resource (in modern apps) or it might be a collection of individual resources (of which there are approximately zillion, with such prosaic names as ‘t8mk’ and ‘ics#’). If you are looking at a single ‘icns’ resource, then a simple Get1Resource will fetch it. Otherwise, to load the myriad different separate resources, use GetIconSuite, and then convert the result to an icon family.

There's also the case when you find no icon at all — it’s better to give the document a generic icon (which will leave the user uninformed about which app will open the document) than leave it with no custom icon (which will probably suggest that the file will open in some app other than the one you just set it to open with):

if (not iconFamily) {
  static U::Icon genericIcon = U::Icon::FromTypeCreator(
    kSystemIconsCreator, kGenericDocumentIcon
  );
  ThrowIfOSError(::IconRefToIconFamily(
    genericIcon.Get(), kSelectorAllAvailableData, 
    &iconFamily
  ));
}

Alright! Now you have the icon. Remove the old icon, write the new one to the file, and let the Finder know that the file has a custom icon:

U::Handle newIcon(reinterpret_cast< ::Handle>(iconFamily));

// Put a custom icon resource in
::Handle oldIcon = ::Get1Resource(kIconFamilyType, kCustomIconResource);
if (oldIcon) {
  ::RemoveResource(oldIcon);
}

::AddResource(newIcon.Get(), kIconFamilyType, kCustomIconResource, "\p");
ThrowIfResourceError();
::SetResInfo(newIcon.Get(), kCustomIconResource, "\pBinding Override");
ThrowIfResourceError();
newIcon.Abandon();

// Set the custom icon flag
FSCatalogInfo info;
ThrowIfOSError(::FSGetCatalogInfo(
  &this->Get(), kFSCatInfoFinderInfo, 
  &info, 0, 0, 0
));
FileInfo& finderInfo = *reinterpret_cast<FileInfo*>(&info.finderInfo);
finderInfo.finderFlags |= kHasCustomIcon;
ThrowIfOSError(::FSSetCatalogInfo(&this->Get(), kFSCatInfoFinderInfo, &info));

And now you are done. Oh, wait… maybe some day you will want to undo this, and let the file open with its default application:

void
FileRef::ResetApplication()
{
  // Remove usro(0) resource, icns(-16455), and clear the custom icon bit
  U::ResourceFileRef resFile(*this, fsRdWrPerm);
  ::Handle oldUsro = ::Get1Resource('usro', 0);
  if (oldUsro) {
    ::RemoveResource(oldUsro);
  }
  ::Handle oldIcons = ::Get1Resource(kIconFamilyType, kCustomIconResource);
  if (oldIcons) {
    ::RemoveResource(oldIcons);
  }

  FSCatalogInfo info;
  ThrowIfOSError(::FSGetCatalogInfo(&this->Get(), kFSCatInfoFinderInfo, &info, 0, 0, 0));
  FileInfo& finderInfo = *reinterpret_cast<FileInfo*>(&info.finderInfo);
  finderInfo.finderFlags &= ~kHasCustomIcon;
  ThrowIfOSError(::FSSetCatalogInfo(&this->Get(), kFSCatInfoFinderInfo, &info));
}

If you made it this far, I suppose you probably want to know how to get the icon file name from an app’s info dictionary. It’s a simple matter of looking things up in CFBundleDocumentTypes (for apps that haven't adopted UTIs yet) or LSItemContentTypes (for those that have).

optional<string>
IconFileInAppForExtension(
  U::CFDictionaryRef inAppInfo,
  const std::string& inExtension)
{
  return IconFileInAppForKeyValue(
    inAppInfo, 
    CFSTR("CFBundleTypeExtensions"), inExtension
  );
}

optional<string>
IconFileInAppForType(
  U::CFDictionaryRef inAppInfo,
  const std::string& inExtension)
{
  return IconFileInAppForKeyValue(
    inAppInfo, 
    CFSTR("LSItemContentTypes"), inExtension
  );
}

optional<string>
IconFileInAppForKeyValue(
  U::CFDictionaryRef inAppInfo,
  CFStringRef inKey,
  const std::string& inValue)
{
  U::CFArrayRef docTypes = cf_cast<U::CFArrayRef>(::CFDictionaryGetValue(
    inAppInfo.get(), CFSTR("CFBundleDocumentTypes")
  ));

  if (docTypes) {
    U::CFStringRef valueStr = lexical_cast<U::CFStringRef>(inValue);
    
    for (
      CFIndex i = 0;
      i < ::CFArrayGetCount(docTypes.get());
      ++i
    ) {
      U::CFDictionaryRef docType = cf_cast<U::CFDictionaryRef>(
        ::CFArrayGetValueAtIndex(docTypes.get(), i)
      );

      U::CFArrayRef values = cf_cast<U::CFArrayRef>(
        ::CFDictionaryGetValue(docType.get(), inKey)
      );
      if (values) {
        for (
          CFIndex j = 0;
          j < ::CFArrayGetCount(values.get());
          ++j
        ) {
          U::CFStringRef appValue = cf_cast<U::CFStringRef>(
            ::CFArrayGetValueAtIndex(values.get(), j)
          );
          if (
            ::CFStringCompare(
              appValue.get(), valueStr.get(), 
              kCFCompareCaseInsensitive
            ) == kCFCompareEqualTo
          ) {
            U::CFStringRef iconFileName = cf_cast<U::CFStringRef>(
              ::CFDictionaryGetValue(docType.get(), CFSTR("CFBundleTypeIconFile"))
            );
            if (iconFileName) {
              return optional<string>(lexical_cast<string>(iconFileName));
            }
          }
        }
      }
    }
  }
  return optional<string>();
}

Of course, if you are only ever going to use this code to set the creator of a file to an application that you control, then you can skip most of the backwards compatibility cases. You won’t need the code for resource-fork based apps or apps without info dictionaries, and probably not even for apps that don’t use UTIs.

In other news:

Comments

  • I think the easiest way to do this is probably to execute an embedded NSString as an AppleScript from within an Objective-C program (see Apple Technical Note TN2084). The AppleScript code to set the default app is simply

    tell application “System Events” to set default application of file theFilePath to applicationAlias

    and this doesn’t use any private API’s.

    NormM October 28, 2009
  • Interesting; I was not aware of this functionality in System Events. It looks like it’s only been available since Leopard — if you support Tiger (as Fetch does), you still need to do it manually.

    Ben Artin October 28, 2009
  • I still don’t get why we would want to have this behavior. If I want my files to open with a specific application, I pick that application myself. Frankly, it feels kind of System 7 to do it the other way.

    cmpwi October 28, 2009
  • Seems like this is a *lot* of code for something undocumented and fragile. Especially when *all* of it can be replaced by a single function call.

    Rosyna October 28, 2009
  • The first part of this code (dealing with ‘usro’ resources) is undocumented, but the bulk of it (dealing with custom icons) is well-documented.

    It’s great there has been a supported mechanism for doing this since Leopard. Use it if you can; I will.

    Ben Artin October 28, 2009
  • I’d tag a call to FNNotify() on the end of the custom icon bit myself, to make sure the Finder updates its view of that item immediately. Add this after changing the Finder info:

    ThrowIfOSError(::FNNotify(&this->Get(), kFNDirectoryModifiedMessage, kNilOptions);

    Jim Dovey October 28, 2009
  • The reason why making the user do it isn’t a good idea is when you’re on Snow Leopard, which ONLY uses the filename extension’s defaults for ALL applications of that type OR the usro resource.

    So let’s say you’re doing some development, and you want the HTML files you’re working on to by default, open in your IDE, and not safari. Well, prior to Snow, this was simple and reliable to do. With Snow, you have three options:

    1) Change the default OS handler for ALL .html files. That sucks.

    2) Every time you create a new file that you’re working on, manually change the default application. Then remember to change them all back. Oh yeah. That’s some sweet user friendlyness there.

    3) Use a custom extension, and hope no one else ever uses it.

    Snow’s evisceration of a feature that is used more than people think is an astoundingly bad idea.

    John C. Welch October 28, 2009
  • Holy reimplementation, Batman! When I think that it probably duplicates Apple code, but it is necessary to do so since it’s not a public API, I cry a little inside. However, even if usro is undocumented, it is not a compatibility problem to write one (attempting to read one might be), simply because, since this is persistent storage, the Mac OS X updates have to keep supporting the existing format.

    One small thing that might be missing is support for apps that come in bundles, and the file type doesn’t have an extension but is defined only in term of a four char type code. However I suspect it’s a case exceptional enough that you’ve not run into it, especially as apps that support files that are downloaded by ftp (and therefore come only with an extension at first) added support for extensions as soon as they switched to the plist declaration.

    Surprising that the 32-bit length at the start of the usro (which is a bit redundant with the resource length anyway) is simply in the native endian format of the machine. I guess it’s not a problem as, given this mechanism relies on the full path of the application, it’s going to break as soon as the file is transferred to another machine anyway (with the possible exception of migration).

    In the end, I would humbly suggest that developers not use this as a direct replacement for the lost creator code functionality. This user override has different semantics from the creator, and the behavior is going to be different (for instance in Leopard you could have a type->application mapping that would override any creator code for files of this particular type, which isn’t going to be possible here). However in your particular use case, which is “giving away” a particular file on behalf of the user (who explicitly asked for it), then it makes sense to manipulate the user override.

    Pierre Lebeaupin October 28, 2009
  • > Frankly, it feels kind of
    > System 7 to do it the other
    > way.

    So what? Some people want the System 7 feeling. Not sure why you want them not to have it.

    The debate itself is probably off-topic, but the key thing to understand is that just because 2 files have the same filename extension, that does not mean they are used in the same way, or have anything in common at all outside the format of their bits, which is actually only of interest to developers and computer science people.

    For example, audio tracks made with Logic Pro and a CD rip made with iTunes may both have an “.aif” extension, but the Logic Pro files won’t even play in iTunes because their bit-depth and sample rate is too high. These files have nothing in common. AIFF is used so that Logic Pro tracks can be read by Pro Tools and so that iTunes CD rips can be read by other jukeboxes, not so that my audio tracks and CD rips could be treated the same by LaunchServices.

    More food for thought is the “.f4v” file, which is an MPEG-4 “.m4v” that only opens in FlashPlayer. Change the extension to the standard one and it runs in every MPEG-4 player. So if you have a file called “intro.f4v” you have 3 pieces of information shoved into the filename: name (intro), creator (f), and file type (4v). So no, I don’t think being more modern than System 7 has just fixed everything.

    Hamranhansenhansen October 28, 2009
  • I’ve done up an ObjC version using the 10.6 APIs and wrapped it into a command-line utility, if anyone’s interested:

    http://github.com/AlanQuatermain/SetAppAffinity

    Jim Dovey October 29, 2009
  1. Page 1

Leave a comment

If you haven’t left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won’t appear on the entry. Thanks for waiting.

  • We will never post or share your email address.

Fetch visitor is writing…