I'm writing a document-based app that needs to work with mp3 files, so I'm storing a reference to the mp3 file the user selected in my app's document file. I've learnt that my app can no longer access the mp3 file after it's been closed and reopened again due to macOS app sandboxing, so I've tried to incorporate the respective sandboxing mechanics in to my app.
As stated here (https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox#4144047), I'm storing a document-relative bookmark to the mp3 file in my app's document file (using NSKeyedArchiver
/NSKeyedUnarchiver
in dataOfType:error:
/readFromURL:ofType:error:
), which I'm creating like this:
[mp3FileURL startAccessingSecurityScopedResource];
NSData *bookmarkData = [mp3FileURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:myDocumentFileURL error:&error];
[mp3FileURL stopAccessingSecurityScopedResource];
I'm subsequently trying to get it back from my stored data like so:
NSURL *mp3FileURL = [NSURL URLByResolvingBookmarkData:bookmarkData options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:myDocumentFileURL bookmarkDataIsStale:&isStale error:&error];
Curiously, while this is working fine for an autosaved file in the app's container, it doesn't work for an identical file saved to the Desktop with the call to URLByResolvingBookmarkData:options:relativeToURL:bookmarkDataIsStale:error:
failing with the following error:
Error Domain=NSCocoaErrorDomain Code=256 "Failed to retrieve collection-scope key" UserInfo={NSDebugDescription=Failed to retrieve collection-scope key}
I've also added com.apple.security.files.bookmarks.document-scope = YES
to the app's entitlements file, but I don't see how this would matter.
Any idea what's going on here?
EDIT: Here comes the minimal example:
Steps to reproduce (not sure if all of them are actually necessary -- running on Apple M2 on macOS 12.6.6, Xcode 14.2):
Create a new Xcode project using the macOS Document App template (Interface: XIB, Language: Obj-C)
In the target config under "Info":
- Change the default Document Type to:
- Identifier: "com.example.test"
- Add another Document Type:
- Name: "Text"
- Identifier: "public.plain-text"
- Role: Viewer
- Change the existing Imported Type Identifier to:
- Description: "Test"
- Extensions: "test"
- Identifier: "com.example.test"
- Conforms To: "public.data"
- Change the default Document Type to:
In the entitlements file, add a Boolean key "com.apple.security.files.bookmarks.document-scope" with value "YES" (doesn't seem to make any difference at all for me though …)
Replace Document.m with the following code:
#import "Document.h"
@interface Document ()
@property (strong, nonatomic) NSURL *txtFileURL;
@property (strong, nonatomic) NSData *bookmark;
@end
@implementation Document
+ (BOOL)autosavesInPlace
{
return YES;
}
- (NSString *)windowNibName
{
return @"Document";
}
- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError
{
if (self.bookmark == nil) {
NSError *error = nil;
self.bookmark = [self.txtFileURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:self.fileURL error:&error];
if (error != nil) {
NSLog(@"error creating bookmark: %@", error);
}
}
return [NSKeyedArchiver archivedDataWithRootObject:self.bookmark];
}
- (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError *__autoreleasing _Nullable *)outError
{
if ([typeName isEqual:@"public.plain-text"]) {
self.fileURL = nil;
self.fileType = @"com.example.test";
self.txtFileURL = url;
} else if ([typeName isEqual:@"com.example.test"]) {
self.bookmark = [NSKeyedUnarchiver unarchiveObjectWithData:[NSData dataWithContentsOfURL:url]];
NSError *error = nil;
BOOL isStale = NO;
self.txtFileURL = [[NSURL alloc] initByResolvingBookmarkData:self.bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:self.fileURL bookmarkDataIsStale:&isStale error:&error];
NSLog(@"isStale = %@", isStale ? @"YES" : @"NO");
if (error != nil) {
NSLog(@"error resolving bookmark: %@", error);
}
} else {
return NO;
}
[self updateChangeCount:NSChangeDone];
NSLog(@"txtFileURL = %@", self.txtFileURL);
[self.txtFileURL startAccessingSecurityScopedResource];
NSString *str = [NSString stringWithContentsOfURL:self.txtFileURL];
[self.txtFileURL stopAccessingSecurityScopedResource];
NSLog(@"txt file contents: %@", str);
return YES;
}
@end
With a text editor, create a file "foo.txt" with contents "bar" on the Desktop
Run the test app and drag foo.txt to its Dock icon to create a new document containing a reference to foo.txt; the app will be able to open foo.txt and correctly print its content "bar" to the debug console
Quit the app without saving or closing the newly created document; it will correctly restore the autosaved document containing the reference to foo.txt and again print its content "bar" to the debug console
Save the document file to the Desktop as "test.test"
Close the saved document file "test.test" and open it again in the app (doesn't even matter if you quit the app in-between or not)
Result (for me, at least): 'error resolving bookmark: Error Domain=NSCocoaErrorDomain Code=256 "Failed to retrieve collection-scope key"' in the Debug console, app is unable to resolve the bookmark and reopen the file
EDIT2: Replace writeToURL:ofType:error:
with dataOfType:error:
in minimal example:
- (BOOL)writeToURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError *__autoreleasing _Nullable *)outError
{
NSString *temp = @"Temp";
NSError *error;
[temp writeToURL:url atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (error != nil) {
NSLog(@"error creating temp file: %@", error);
}
if (self.bookmark == nil) {
NSError *error = nil;
self.bookmark = [self.txtFileURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:url error:&error];
if (error != nil) {
NSLog(@"error creating bookmark: %@", error);
}
}
NSLog(@"writeToURL: url: %@", url);
return [[NSKeyedArchiver archivedDataWithRootObject:self.bookmark] writeToURL:url atomically:YES];
}