Trigger permission dialog for file access from kind of user supplied path.

I have the following situation:

  • My SwiftUI App for macOS is using App Sandbox and is currently configured for read/write access for all the locations selectable in XCode
  • I have added a file selector using a button and NSOpenPanel() to let the user select a folder containing a database file, to which I successfully get permissions using URL.bookmarkData() and URL.startAccessingSecurityScopedResource()
  • I then try to read file paths from the database file and open those but I instantly get a permission error without a permission dialog/prompt appearing
    • In my test I am using paths to files in my iCloud Drive folder
    • I added all file/folder related usage string entries to the Info.plist for testing

I think this is weird, since I can paste one of those file:// URLs from the database into a (non-Safari) browser and it shows the native permission dialog/prompt before downloading the file as expected.

Is there any usage string that's not shown in the Info.plist Dropdown in XCode that I need to add to my app in order for this to work?

Answered by DTS Engineer in 788986022

Hi,

Is this maybe the wrong way to construct the path url?

Yes, that's the issue here.

Because of how URL mixes both file paths and network paths, some methods do COMPLETELY the wrong thing when called on the wrong URL type. Looking at your code in particular:

 if let percentDecoded = "\(dbPath)\(relativeFilePathFromDb)".removingPercentEncoding,

I don't know what your source data was here, but it would be unusual to percent encode (or any broad, text based modification) a file path.

    let normalizedUrl = URL(string: url.path, relativeTo: URL(fileURLWithPath: "/"))?.standardizedFileURL {

URL(string:) or it's variant should NEVER be used with file paths. Per the documentation, it parses the input paths based on RFC 3986/1808/1738, which makes the final output... difficult to predict. It often leaves basic ASCII paths unmodified but will completely mangle more complicated unicode characters (for example, emoji), opening the door to all sorts of unexpected failures.

When working with file paths, you should:

-Create paths using fileURL(withPath:) or any of the other initializers that specifically reference "fileURL". However, note that direct URL creation is relatively unusual in a sandboxed app. It's more typical to start with a URL the system "gives" you then modify it using:

-Use appendPathComponent(_:) or one of it's variants to add directory or file names.

-Use deleteLastPathComponent() or one of it's variants to remove directories or file from the path.

Also, as general guidance, I strongly recommend against converting URLs into strings or creating URLs from strings. You should think of a file URL as an opaque reference object that you can modify, NOT simply a "wrapper" around the "true" string path. Three major reasons for this:

  1. The URL object itself is what holds the security scope data, so converting back and for between URL and String can leave you with a URL that startAccessingSecurityScopedResource fails on, even though the user gave you access to that path.

  2. Using simple string paths to reference files was somewhat reasonable in a world of pure ASCII. It's broken down very badly in a world where all major file system have ended up using a different unicode variant for name storage.

  3. Most URLs the system will give you are actually file reference URLs, not "path" references. The documentation has a detailed description of the difference, but the quick summary is that a file reference URL references a specific object in the file system, NOT a specific location. That means it can (generally) provide a valid reference to the same object across moves or renames within the file system, something a path URL can obviously not do.

-Kevin Elliott
DTS Engineer, CoreOS/Hardware

If, just for the sake of testing, you put this database in your home directory, do you see the same problem?

The goal of the above test is to rule iCloud Drive out of the equation, because the next step depend on whether this is an iCloud Drive issue or something else.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Yes, the same happens in the home folder as well.

After reading each file path from the database, I am trying to access each file like this:

 if let percentDecoded = "\(dbPath)\(relativeFilePathFromDb)".removingPercentEncoding,
    let url = URL(string: percentDecoded),
    let normalizedUrl = URL(string: url.path, relativeTo: URL(fileURLWithPath: "/"))?.standardizedFileURL {

    print("\(normalizedUrl)") // this outputs the expected path
    let success = normalizedUrl.startAccessingSecurityScopedResource()
    if success { // this is always false
        // ...
    } else {
        print("Unable to access file!")
    }
    normalizedUrl.stopAccessingSecurityScopedResource()
}

Is this maybe the wrong way to construct the path url?

Hi,

Is this maybe the wrong way to construct the path url?

Yes, that's the issue here.

Because of how URL mixes both file paths and network paths, some methods do COMPLETELY the wrong thing when called on the wrong URL type. Looking at your code in particular:

 if let percentDecoded = "\(dbPath)\(relativeFilePathFromDb)".removingPercentEncoding,

I don't know what your source data was here, but it would be unusual to percent encode (or any broad, text based modification) a file path.

    let normalizedUrl = URL(string: url.path, relativeTo: URL(fileURLWithPath: "/"))?.standardizedFileURL {

URL(string:) or it's variant should NEVER be used with file paths. Per the documentation, it parses the input paths based on RFC 3986/1808/1738, which makes the final output... difficult to predict. It often leaves basic ASCII paths unmodified but will completely mangle more complicated unicode characters (for example, emoji), opening the door to all sorts of unexpected failures.

When working with file paths, you should:

-Create paths using fileURL(withPath:) or any of the other initializers that specifically reference "fileURL". However, note that direct URL creation is relatively unusual in a sandboxed app. It's more typical to start with a URL the system "gives" you then modify it using:

-Use appendPathComponent(_:) or one of it's variants to add directory or file names.

-Use deleteLastPathComponent() or one of it's variants to remove directories or file from the path.

Also, as general guidance, I strongly recommend against converting URLs into strings or creating URLs from strings. You should think of a file URL as an opaque reference object that you can modify, NOT simply a "wrapper" around the "true" string path. Three major reasons for this:

  1. The URL object itself is what holds the security scope data, so converting back and for between URL and String can leave you with a URL that startAccessingSecurityScopedResource fails on, even though the user gave you access to that path.

  2. Using simple string paths to reference files was somewhat reasonable in a world of pure ASCII. It's broken down very badly in a world where all major file system have ended up using a different unicode variant for name storage.

  3. Most URLs the system will give you are actually file reference URLs, not "path" references. The documentation has a detailed description of the difference, but the quick summary is that a file reference URL references a specific object in the file system, NOT a specific location. That means it can (generally) provide a valid reference to the same object across moves or renames within the file system, something a path URL can obviously not do.

-Kevin Elliott
DTS Engineer, CoreOS/Hardware

Trigger permission dialog for file access from kind of user supplied path.
 
 
Q