swiftexceptiondelegatesnskeyedunarchivernssecurecoding

`cannotDecodeObjectOfClassName` not invoked in `NSKeyedArchiverDelegate`


I am trying to catch the NSKeyedUnarchiver unarchiving exception NSInvalidUnarchiveOperationException where an unknown class is being decoded securely via NSSecureCoding protocol.

The solution I am using is based on a related NSKeyedUnarchiverDelegate SO post by implementing the delegate protocol NSKeyedUnarchiverDelegate so I can listen and respond to the exception via unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:). However, this delegate method doesn't seem to be called when unknown class is encountered during decoding.

Here's the code snippet that I use for securely unarchiving an array object.

func securelyUnarchiveArrayOfCustomObject(from url: URL, for key: String) -> [MyCustomClass]? {
    guard let data = try? Data(contentsOf: url) else {
        os_log("Unable to locate data at given url.path: %@", log: OSLog.default, type: .error, url.path)
        return nil
    }

    let unarchiver = NSKeyedUnarchiver(forReadingWith: data)
    let delegate = UnarchiverDelegate()     // Prevents `NSInvalidUnarchiveOperationException` crash
    unarchiver.delegate = delegate
    unarchiver.requiresSecureCoding = true  // Prevents object substitution attack

    let allowedClasses = [NSArray.self] // Will decode without problem if using [NSArray.self, MyCustomClass.self]
    let decodedObject = unarchiver.decodeObject(of: allowedClasses, forKey: key)
    let images = decodedObject as! [ImageWithCaption]?
    unarchiver.finishDecoding()

    return images
}

where my UnarchiverDelegate is implemented just like in the original NSKeyedUnarchiverDelegate SO post I pointed to. Under my setup, decodeObject(of: allowedClasses, forKey: key) doesn't throw an exception, but instead raises a runtime exception:

'NSInvalidUnarchiveOperationException', reason: 
'value for key 'NS.objects' was of unexpected class 
'MyCustomClassProject.MyCustomClass'. Allowed classes are '{(
    NSArray
)}'.'

This is supposedly just the kind of exception that NSKeyedUnarchiverDelegate's unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:) should be invoked, based on its documentation:

Informs the delegate that the class with a given name is not available during decoding.

But in my case, this method isn't invoked with the snippet above (even though other delegate methods, like unarchiverWillFinish(_:) or unarchiver(_:didDecode:) are invoked normally when decoding doesn't encounter a problem.

Unlike in the original post, I cannot use class function like decodeTopLevelObjectForKey where I can handle exception with try?, because I need to support secure encoding and decoding with NSSecureCoding protocol, like discussed here. This forces me to use decodeObject(of:forKey), which doesn't throw any exception that I can handle, and, again, it doesn't inform my delegate before throwing runtime exception that leads to app crash either.

Under what scenarios is the delegate method unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:) actually invoked? How can I listen and react to the NSInvalidUnarchiveOperationException under my NSSecureCoding setup so I can avoid runtime crash when the decoding isn't successful?


Solution

  • Someone helped me on Apple Dev Forum. For details, see:

    https://forums.developer.apple.com/thread/76664