URL Bookmarks: yes and no

A bookmark provides a persistent reference to a file-system resource. When you resolve a bookmark, you obtain a URL to the resource’s current location. A bookmark’s association with a file-system resource (typically a file or folder) usually continues to work if the user moves or renames the resource, or if the user relaunches your app or restarts the system.

From Apple SDK documentation I learn that URL Bookmarking is the ultimate way to keep track of a file location.

This is especially important since changes to iOS 8

Beginning in iOS 8, the Documents and Library directories are no longer siblings of your application's bundle.

In practice it means that absolute path to a file stored in Documents or Library (or any other directory) is valid only for this run, and will change next time app will launch. An implementation detail, that makes things a little less obvious. As one of the solution to persist a reference to a file is use of URL Bookmark.

What is URL Bookmark exactly?

It's a piece of metadata that we can use to locate a file. The piece of data in form of NSData that can be stored on disk and used to resolve a filesystem path to the file at given moment. Sounds like an ideal candidate to replace absolute path to the file. Under the hood it's a data structure that store identifiers and more informations (eg. it is used to grant security scoped access to files).

Sounds great, what's wrong with that?

The way URL Bookmarking is designed and implemented, makes it untrusty source of truth. Bookmark data relies on two main sources to locate the files:

  1. Absolute path, that is a subject to change for iOS application (as mentioned above), or anly file.
  2. The inode (index node) that is a metadata of the actual file. When I move a file from Directory1 to Directory2, the file system updates metadata - do not move the actual data.

When an absolute path to a file changes from /my/directory/file to /another/directory/file, the inode value associated to the file doesn't change - metadata of inode changes.

When inode changes, there's an absolute path to the file that can recover the bookmark. At this point, the bookmark needs to refresh its metadata (bookmark data is stale).

If both happens at the same time, the bookmark is lost: it can't locate the file by the path nor by the inode value

Lost bookmark

The recipe to misuse a bookmark is this:

  1. Create bookmark from the file
  2. Update a file content atomically
  3. Move a file
  4. Try to use previously stored bookmark 💥
do {
  let fileURL = URL(fileURLWithPath: "/tmp/file.txt")
  
  // Create bookmark
  let bookmark = try fileURL.bookmarkData()

  // Write to a file atomically
  try "foobar".write(to: fileURL, atomically: true, encoding: .utf8)

  // Move file to /tmp/file.txt.bak
  try FileManager.default.moveItem(at: fileURL, to: fileURL.appendingPathExtension("bak"))

  // Try to use bookmark 💥 Error: The file doesn’t exist.
  var bookmarkIsStale = false
  try URL(resolvingBookmarkData: bookmark, bookmarkDataIsStale: &bookmarkIsStale)
} catch {
  print(error)
}

https://gist.github.com/krzyzanowskim/086f4f025856ea436f805c632c45e854

by changing a single line in the above example, I can make it work again:

  try "foobar".write(to: fileURL, atomically: false, encoding: .utf8)
  var bookmarkIsStale = false
  let resolvedURL = try URL(resolvingBookmarkData: bookmark, bookmarkDataIsStale: &bookmarkIsStale)

  if bookmarkIsStale {
    print("Bookmark is stale. renewing.")
    // If bookmark data is stored, all occurences have to be updated.
    bookmark = try resolvedURL.bookmarkData()
  }

this is due to how "atomically" affect the write operation

Documentation says:

the data is written to a backup location, and then—assuming no errors occur—the backup location is renamed to the name specified by aURL

what it means is that effectively: create a new file → new resource with a new inode value is create on the filesystem at the same path.

Rules of URL bookmarks

  • Always check if bookmark data is stale (bookmarkDataIsStale)
  • if data is stale, update ALL bookmark data - including all the bookmarks possibly stored elsewhere.
  • be careful with atomic updates - need discipline to not use it.
  • If need persistence and atomic writes better use something else - updating stored bookmark after every save is cumbersome and error prone.

Read more: Apple's BookmarkData - exposed!, APFS, safe saves, inodes and the volfs file system

Credits: Header photo by Emily Rudolph