0

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):

  1. Create a new Xcode project using the macOS Document App template (Interface: XIB, Language: Obj-C)

  2. 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"
  3. 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 …)

  4. 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
  1. With a text editor, create a file "foo.txt" with contents "bar" on the Desktop

  2. 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

  3. 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

  4. Save the document file to the Desktop as "test.test"

  5. 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)

  6. 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];
}
tajmahal
  • 1,665
  • 3
  • 16
  • 29
  • 1
    When do you call `startAccessingSecurityScopedResource` and `stopAccessingSecurityScopedResource`? – Willeke Jun 04 '23 at 22:13
  • 1
    Does this answer your question? [Mac OS In App Sandbox Entitlements Directory Read Issue](https://stackoverflow.com/questions/21923768/mac-os-in-app-sandbox-entitlements-directory-read-issue) – Richard Barber Jun 04 '23 at 22:39
  • @Willeke: Right before and after I try to read the file with `[NSData dataWithContentsOfURL:mp3FileURL`. But it's failing much earlier already, before I even have the URL. To be able to call `startAccessingSecurityScopedResource` on the URL, I first need to retrieve it from the bookmark with `URLByResolvingBookmarkData:options:relativeToURL:bookmarkDataIsStale:error:` which is exactly the call that triggers the error message … – tajmahal Jun 04 '23 at 23:15
  • @Richard Barber: The user asking the question you'e referring to has a similar usecase to mine, but unfortunately, that question doesn't even have an accepted answer. Second answer mentions "Stored as a security scoped bookmark, after which it can be accessed freely. ", which is the most reasonable solution, and which is exactly what I'm already trying to do. Unfortunately, all of the links in the comments of that answer are broken, but I was able to retrieve some using archive.org. However, they just seem to describe the same things I'm already doing … – tajmahal Jun 04 '23 at 23:20
  • 1
    Post a [mre] please. – Willeke Jun 05 '23 at 08:16
  • @Willeke: Edited the question to include a minimal reproducible example. Hopefully I didn't miss anything. – tajmahal Jun 06 '23 at 00:48
  • 1
    Is `self.fileURL` the same in `dataOfType` and `readFromURL`? – Willeke Jun 06 '23 at 02:27
  • Apparently, in dataOfType, it's still nil … good point! But how should I get the file URL in dataOfType? After all, it's set by the Cocoa document architecture only after the method has returned. Or how could I get notified about the document's file URL change and update the bookmark? – tajmahal Jun 06 '23 at 09:24
  • 1
    Implement `writeToURL:ofType:error:` instead of `dataOfType:error:`. Like you implemented`readFromURL:ofType:error:` instead of `readFromData:ofType:error:`. – Willeke Jun 06 '23 at 12:25
  • @Willeke: I replaced the method as you suggested (see updated question). At first, it didn't work because even though I now had the URL for the (temporary) document file provided to me by the document architecture, creating the bookmark failed because the file at the new URL didn't exist yet. After looking at https://stackoverflow.com/questions/15611663/apple-sandbox-disable-nsdocuments-atomic-save-for-document-scoped-bookmarks , I incorporated the creation of a temporary file at the given URL. Now, saving and loading again actually works with user-saved files, but … – tajmahal Jun 06 '23 at 14:42
  • … loading now fails with autosaved ones, even though `writeToURL:ofType:error:` is called in that case as well. I get: `error resolving bookmark: Error Domain=NSCocoaErrorDomain Code=259 "The file couldn’t be opened because it isn’t in the correct format."` The question at https://stackoverflow.com/questions/15611663/apple-sandbox-disable-nsdocuments-atomic-save-for-document-scoped-bookmarks is also still unanswered … is there a way to get document-scoped bookmarks and NSDocument to work at all?! I haven't found a single complete example of this anywhere on the Internet yet. – tajmahal Jun 06 '23 at 14:45
  • Also, setting `atomically:` to `NO` doesn't seem to make a difference for me … – tajmahal Jun 06 '23 at 15:25
  • Do you save the bookmark or `@"Temp"`? Do you create a new relative bookmark when the document is saved to a different url? – Willeke Jun 06 '23 at 17:12
  • I just save `@"Temp"` at the provided `url` so that a file will exist there when creating the bookmark. Saving the actual file (including the newly created bookmark) happens in the return statement. In the minimal example, I don't recreate the bookmark when the document url changes for the sake of simplicity. – tajmahal Jun 06 '23 at 17:30
  • Maybe this helps: [App Sandbox: document-scoped bookmark not resolving; not returning any error](https://stackoverflow.com/questions/10259692/app-sandbox-document-scoped-bookmark-not-resolving-not-returning-any-error) – Willeke Jun 08 '23 at 10:59
  • The answer over there suggests to change the `atomically:` parameter to `NO` in `writeToURL:atomically`. I double-checked that and it seems that with `atomically:YES`, it doesn't work at all, and with `atomically:NO`, it works for files saved by the user, but _not_ with autosaved ones … which is really weird since in both instances, the same `writeToURL:ofType:error:`method is being called for writing the file to disk. – tajmahal Jun 08 '23 at 12:33
  • Ok, so it seems that this is indeed linked to the file being moved by the Cocoa autosave mechanics after being saved. When autosaving, the url provided by the Cocoa autosave mechanics in the `url` parameter in `writeToURL:ofType:error:` is something like `file:///var/folders/r6/x3h3jrg53ln5q4hh_qrz5khr0000gn/T/com.username.SecurityBookmarkExample/TemporaryItems/NSIRD_SecurityBookmarkExample_4vnpmA/Unsaved%20SecurityBookmarkExample%20Document.test`, but on reopening that autosaved file, the url given to `readFromUrl:ofType:error:` is … – tajmahal Jun 08 '23 at 12:51
  • … `file:///Users/tobias/Library/Containers/com.username.SecurityBookmarkExample/Data/Library/Autosave%20Information/Unsaved%20SecurityBookmarkExample%20Document.test`. How can this be resolved? Can the autosave system somehow be prevented from saving the file to a temp location first, and instead directly write to the final location? – tajmahal Jun 08 '23 at 12:51

0 Answers0