Observations of the Curious Problem of NSDocument-Based App Changing the File Extension

Users have reported problems with TableFlip saving their files recently. One wrote about it in the Zettelkasten forums, if you want to see the problem in context.

To reproduce the problem: when you open foo.txt in TableFlip and a text editor, then change the file in the editor rapidly, TableFlip would show a “You don’t have permissions” error once you tried to save changes from TableFlip later.

That’s a sandboxing error. Since the user opened the file in TableFlip, and since TableFlip is a NSDocument-based app with proper URL bookmark handling, this didn’t make sense at first: where did the permission go? Why do I lose permissions at all? Also, TableFlip did not show this error if you only open the file in TableFlip and save changes there, so the file itself was okay up until the external editor changes something.

So it’s related to external changes. In debug builds, I was able to trigger the “reload on external changes” code path without needing another app open, and it produced the problem even quicker. What did the external changes do that makes the persmissions go bonkers?

TableFlip Changes the File Extension During the write Operation

An error I saw in the Xcode console right before the error was presented:

NSFileSandboxingRequestRelatedItemExtension: an error was received from 
  pboxd instead of a token. Domain: NSPOSIXErrorDomain, code: 1
2020-03-26 13:04:13.691060+0100 TableFlip[19188:445816] 
  -[NSFileCoordinator itemAtURL:willMoveToURL:] could not get a sandbox 
  extension. oldURL: file:///.../Documents/foo.txt, newURL: file:///.../Documents/foo.md
  • I never called NSFileCoordinator.itemAtURL:willMoveToURL:, this is part of whatever is build into NSDocument.
  • I never instructed the app to change the file extension from .txt to .md. But the move command boils down to a file extension change. That’s interesting.
  • Symbolic breakpoints on NSFileCoordinator.itemAtURL:willMoveToURL: are not triggered, so there was no way to see when this happens in the code.

In my NSDocument subclass, I override the write variant NSDocument.write(to:ofType:for:originalContentsURL:) and don’t move anything there; something must be handled wrongly elsewhere. There, it turns out that the incoming url and absoluteOriginalContentsURL parameters are not what I expected, though:

  • url is file:///var/folders/62/8k21681d08z9lhq8h433z3rh0000gp/T/de.christiantietze.TableFlip/TemporaryItems/(A%20Document%20Being%20Saved%20By%20TableFlip%202)/foo.md
  • absoluteOriginalContentsURL is file:///.../Documents/foo.md

Remember that the file, when I opened it, was file:///.../Documents/foo.txt, not .md. This tells me:

  1. TableFlip reads a foo.txt from disk,
  2. saves changes atomically using a temporary directory in the sandbox with the .md extension,
  3. apparently wants to move the result to the same directory as the original file;
  4. since the sandbox granted access to the .txt only, not the parent directory, moving a new file into that directory is not permitted.

That’s apparently what “could not get a sandbox extension” means.

Now with the weird error messages out of the way and their corresponding dialogs popping in my face – why is the file extension changing in the first place?

Is the file type reported wrong? NSDocument.writableTypes(for:) is not called when the extension changes. It’s not called a whole lot when opening an existing file at all. That’s useful to know, since it rules out a source of problems in code already.

There still was one hideous source of error left that I didn’t pay close attention to up to this point: myself.

Find out what Christian discovered when he looked into the Mirror of Truth in the next episode!