FileManager.default.enumerator causes crash when attempting to access unreadable URL - swift

On the face of it, it seems like one should take care when creating directory enumerators, but my code takes a url chosen by the user and it is possible to pick a protected folder in the NSOpenPanel which is then passed in to create the directory enumeration. For example, the user could choose the User/Guest/Desktop folder which is unreadable by a non-guest user.
My handling code (once the url[s] are chosen) is:
if let enumerator: FileManager.DirectoryEnumerator = FileManager.default.enumerator(at: the_url, includingPropertiesForKeys: [URLResourceKey.isReadableKey], options: [.skipsHiddenFiles, .skipsPackageDescendants], errorHandler: { (unreadable_url, error) -> Bool in
print ("Enum error")
DispatchQueue.main.sync(execute: { () -> Void in
self.setStatusText("Cant read url", colour: .red) //Code to update UI on main thread
error_occured = true
})
return false
}) {
//process contents
while let nested_item = enumerator.nextObject() { //....<Crashes
// ..
}
}
Thing is if the url is unreadable (typically because it's protected) then the app crashes at the while.. line with error.
Thread 6: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
No other information is output in Xcode's console. It appears that the enumerator ivar passes the non-nil test despite being unusable and the error block isn't executed so the while statement fails.
My solution has been to check the URLResourceKey.isReadableKey prior to the if let.. line, but that seems like a double-check which shouldn't be necessary.
Is my code wrong or is this a bug?

Related

Got an error when dragging files using NSEvent. (macOS)

I wanna drag files to my window and then perform actions.
I tried to use snippets below provided in this answer to distinguish whether you're dragging a file or a window.
// In my window controller
class MyWindowController: NSWindowController {
init() {
// Some initialization steps below are omitted
let win = NSWindow(...)
super.init(window: win)
let contentView = DropView(frame: win.frame)
win.contentView?.addSubview(contentView)
registerGlobalMouseEvent()
}
func registerGlobalMouseEvent() {
self.window?.acceptsMouseMovedEvents = true
NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDragged, handler: { [self] event in
// Codes below will cause errors
let pasteBoard = NSPasteboard(name: .drag)
guard let fileNames = pasteBoard.propertyList(forType: .init(rawValue: "NSFilenamesPboardType")) as? NSArray else { return }
let changeCount = pasteBoard.changeCount
if fileNames.count > 0 && lastChangeCount != changeCount {
lastChangeCount = changeCount
// My actions when dragging
}
})
}
}
Then I ran my codes and started dragging, I got three errors:
[sandbox] Failed to get a sandbox extension
[Framework] Failed to issue sandbox extension for /Users/roy/Downloads/test.txt with error 1
[default] Failed to issue sandbox token for URL: 'file:///Users/roy/Downloads/test.txt' with error: 'Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted" UserInfo={NSLocalizedDescription=Cannot issue a sandbox extension for file "/Users/roy/Downloads/test.txt": Operation not permitted}'
 
But when I just do
NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDragged, handler: { [self] event in
// My actions
})
, then everything went fine.
 
The first error seems harmless since it didn't prevent my app from running.
The second and the third ones are deadly and directly caused my app to crash.
I wonder if there are any problems in his code? Any useful thoughts would be great! :)
 
You need to know about Bookmarks and Security Scoped URLs when working with sandbox . A dragged URL gives your app process permission just once to read or read/write a “user selected file” depending on how you configure entitlements.
You can save a bookmark (blob of data) to keep access over subsequent sessions as long as the file isn’t updated by another process at which point the bookmark becomes stale and you will need to encourage the user to select the file again.
Handing a URL to another process across an XPC boundary like sharing requires that you own the file so may involve a copy to your sandbox cache.
e.g:
let dragurl = url_of_dragged_file //at this point you have at-least read access
let cachepath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).last!
let cachedir = URL(fileURLWithPath: cachepath)
let cacheurl = cachedir
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension(dragurl.pathExtension)
try FileManager.default.copyItem(at: dragurl, to: cacheurl)
At this point you have a copy in your local sandbox cache that can be handed off to a share sheet.
So I finally got a solution for this. :)
It appears that it indeed have something to do with the snippets I mentioned above, and here's the correction:
NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDragged, handler: { [self] event in
let pasteboard = NSPasteboard(name: .drag)
let changeCount = pasteboard.changeCount
if lastChangeCount != changeCount {
lastChangeCount = changeCount
if pasteboard.canReadObject(forClasses: [NSURL.self], options: [:]) {
/// actions
}
}
})
In this way, I got no errors and my codes run perfectly!

Validity time of the URL from an NSItemProvider

I’m writing a sharing extension that will accept images and perform some action with them. Within a method of my UIViewController subclass, I can access URLs to a particular representation of the files by writing this:
guard let context = self.extensionContext else {
return
}
guard let items = context.inputItems as? [NSExtensionItem] else {
return
}
for item in items {
guard let attachments = item.attachments else {
continue
}
for attachment in attachments {
guard attachment.hasItemConformingToTypeIdentifier("public.jpeg") else {
continue
}
attachment.loadFileRepresentation(forTypeIdentifier: "public.jpeg") { (url, error) in
if let url = url {
// How long is this "url" valid?
}
}
}
}
In the block I pass to loadFileRepresentation(forTypeIdentifier:completionHandler:), I’m given the URL to a file—in this case, a JPEG. Can I assume anything about how long this URL is valid? Specifically, is it safe to write the URL itself to some shared storage area so that my app can access the pointed-to file later? Or should I assume that the URL is ephemeral and that, if I want access to the file it points at, I should make my own copy of that file within this block?
The documentation for loadFileRepresentation states:
This method writes a copy of the file’s data to a temporary file, which the system deletes when the completion handler returns.
So url is valid to the closing curly brace of the completion handler.
You need to copy the file to a known location with the sandbox before you are done in the completion handler if you need access to the file beyond the completion handler.

A background URLSession with identifier already exists

I have an S3Service which is a singleton that manages all the S3 related uploads and downloads.
When I upload the first image it works fine but if I try to upload an Image consecutively It gives me this warning and the completion block never gets called.
A background URLSession with identifier com.amazonaws.AWSS3TransferUtility.Identifier.TransferManager already exists.
This is how I upload method looks:
if let data = image.jpegData(compressionQuality: 0.5) {
let transferUtility = AWSS3TransferUtility.s3TransferUtility(forKey: S3Service.TRANSFER_MANAGER_KEY)
transferUtility.uploadUsingMultiPart(data: data, bucket: EnvironmentUtils.getBucketName(), key: filename, contentType: "image/jpg", expression: nil, completionHandler: { task,error in
if let error = error {
print(error.localizedDescription)
} else {
print("Image upload success")
}
})
}
The call to register transfer utility AWSS3TransferUtility.register(with: serviceconfig, forKey: KEY) was causing the above issue. There are two things that should be kept in mind.
The AWSS3TransferUtility should be registered only once per Application session. Then we can use AWSS3TransferUtility.S3TransferUtilityForKey to get the instance wherever needed.
If these are for different users within the app, ( e.g. sign-up) and if we want to keep AWSS3TransferUtility separate for each user, register AWSS3TransferUtility with a different key (preferably the same key for the same user) and look up using that key.

Swfit FileManager crashes with EXC_BAD_ACCESS when copying a file a second time

I'm writing a MacOS application with a process that creates a temp folder in the Application Support directory, generates a few new files in that folder and then also copies some user-specified files into it, and then finally copies the full contents of my temp folder into a user-specified final location. Everything works perfectly the first time you run this export process after launching the app, but crashes with an EXC_BAD_ACCESS error if it gets run more than once. The application-generated files are always created and written fine, but the crash always occurs when the FileManager attempts to copy one of the existing user-selected files, even though it passes my guard statements checking that the existing file is readable and the destination path is writable. If you re-launch the app, it will again run the first time no problem, but crashes the second.
Here is the relevant code:
let fm = FileManager.default
guard let existingINIURL = currentExportProfile!.existingINIFileURL else {
return (false, "No INI file location provided.")
}
guard fm.fileExists(atPath: existingINIURL.path), fm.isReadableFile(atPath: existingINIURL.path) else {
return (false, "Could not find INI file at specified path: \(existingINIURL.path) or path is not readable.")
}
guard let outputURL = exportTempFilesURL?.appendingPathComponent("OUTPUT", isDirectory: true), fm.fileExists(atPath: outputURL.path) else {
return (false, "Problem accessing temp staging path")
}
guard fm.isReadableFile(atPath: existingINIURL.path) else {
return (false, "Existing file is not readable")
}
guard fm.isWritableFile(atPath: outputURL.path) else {
return (false, "Destination \(outputURL.path) is not writable")
}
do {
try fm.copyItem(at: existingINIURL, to: outputURL.appendingPathComponent("CONTROL.INI"))
return (true, nil)
} catch let error as NSError {
Swift.print("Failed to copy existing INI with error: \(error)")
return (false, "Failed to copy existing INI file with error: \(error)")
}
The EXC_BAD_ACCESS crash always occurs at the line:
try fm.copyItem(at: existingINIURL, to: outputURL.appendingPathComponent("CONTROL.INI"))
And since it's an access error of course it never reaches the catch statement to give me any indication of what the problem was.
Interesting note: Instead of copying with .appendingPathComponent("CONTROL.INI"), I tried copying the file with its current name and then renaming the copied file in a separate step. When I did this it seemed to be working at first, but it actually only made it so that it worked maybe 3-4 times before crashing the same way instead of always crashing on the second attempt with the same error, still at the fm.copyItem line.
Has anyone encountered any similar issues with FileManager?
Thanks!
I figured it out. It was because I forget I had set a delegate on the default FileManager at one point that no longer existed after the first cycle. After I changed the function that needed a delegate to use its own FileManager instance the problem went away.

Swift loadItem closure not running

I am writing a share extension, but my closure that would capture and save the shared attachment is not running. How can I find out why? The switch branch executes, the attachment is there. There is no error message, it just never runs.
if let contents = content.attachments as? [NSItemProvider] {
for attachment in contents {
let fType = attachment.registeredTypeIdentifiers[0]
if attachment.hasItemConformingToTypeIdentifier(fType) {
switch fType {
case kUTTypeImage as String as String:
do {
attachment.loadItem(forTypeIdentifier: fType, options: nil, completionHandler: { data, error in
print("AppImage")
let url = data as! URL
if let imageData = try? Data(contentsOf: url) {
self.appImage = UIImage(data: imageData)
self.saveImage(image: self.appImage!)
}
})
} // public image case
Once completeRequestReturningItems (CRRI) is executed, your completion handlers for loadItem will no longer be called (they are effectively canceled at that point). Therefore you must synchronize your asynchronous tasks to ensure that you don't execute CRRI until your completion handlers have finished or until you no longer care. From your comments, it sounds like you are invoking loadItem and immediately proceeding to call CRRI.
See answers to this related question: iOS 8 Share extension loadItemForTypeIdentifier:options:completionHandler: completion closure not executing
I prefer the answer there that uses a dispatch group.