0

Does NSMutableDictionary now truncate data as a string, or return ellipses for long data? I use this feature to save a plist with different colors in it. This has worked fine (with minor modifications) since around 2005.

But last month, I think after an OS update, I noticed all of my data was starting to get corrupted. I've narrowed it down to this. When I run this code...

    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
            NSError *error = nil;
            [dict setObject:[NSKeyedArchiver archivedDataWithRootObject:[NSColor redColor] requiringSecureCoding:NO error:&error] forKey:@"backdropColor"];
            
            NSString *test = [dict description];

Note that before MacOS 10.13, you can use this code, which has the same bug.

            [dict setObject:[NSArchiver archivedDataWithRootObject: [NSColor redColor]] forKey:@"backdropColor"];

When I run either, I get the following result:

    backdropColor = {length = 3576, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 00000d88 };

See the ... ? That's not supposed to be there. It used to fill in that ... with all of the data.

I can't find any documentation that explains a change, and while this code has remained unchanged for years, it's now corrupted months of work for one of my users already.

  • 1
    Can you elaborate on how this has caused data corruption for your users? The printed value above is the `-description` of an `NSData`, the format of which did indeed change a little while back. An object's description is not something that is necessarily expected to stay stable over time — it's mostly meant to be a human-consumable value, not a machine-consumable one. Are the values themselves actually changing? (Because that is indeed a difference) – Itai Ferber Jul 05 '20 at 22:18
  • 1
    To clarify: are you parsing `-description` in any way, or are you suggestion that the ellipses are proof of data corruption? (Because that is simply caused by the format changing.) – Itai Ferber Jul 05 '20 at 22:20
  • The values are different. Description used to contain the entire data, now it contains ... as a truncation. – Chilton Webb Jul 05 '20 at 22:34
  • If description changed recently, that's possibly it. That gives me something to look at. – Chilton Webb Jul 05 '20 at 22:36
  • How are you actually using the description? The description itself has changed format but the underlying `NSData` may be exactly the same bit-for-bit. Are you relying on the actual value of the description string? – Itai Ferber Jul 05 '20 at 22:36
  • 1
    (If you're going to be testing: I believe the description changed in macOS Catalina) – Itai Ferber Jul 05 '20 at 22:37
  • Yep, I'm saving NSData description to a plist, and reading it back. This code is based on the old Sketch code, which archived objects using this method. Is there a newer / better way to do this? I need to store an NSData object as a human readable string. – Chilton Webb Jul 05 '20 at 22:41
  • 1
    Oh, yeah, that’s absolutely unsafe — `description` is not meant to be a serialization format. If you have to maintain backwards compatibility, it should be reasonably simple to recreate the description format using your own method and use that going forward. Alternatively, if you don’t care about that, you can roll your own. How human-readable does the output need to be? (Presumably base64-encoding won’t work?) – Itai Ferber Jul 05 '20 at 23:33
  • Since it is based on Apple's example code and was suggested for years as the correct method at WWDCs, I'll assume this is a recent change. Currently, I throw a ton of objects into NSData and then export that using the description to a plist, which can then be easily parsed back into an NSData object later. That's all it needs to do, correct dump an NSData object out and read it back in. – Chilton Webb Jul 06 '20 at 00:12
  • I looked at Apple's sample code, and they retired that one in 2012. So I think this is a moment where I need to move this to something more modern. Thank you for your help! – Chilton Webb Jul 06 '20 at 00:22
  • 1
    FYI, it appeared in iOS 13 (apparently Catalina for macOS equivalent). iOS developer quickly saw the bad use of description appear since it was use for the push token. Other threads: https://stackoverflow.com/questions/57710188/push-notification-not-register-to-the-app-on-ios-13 https://stackoverflow.com/questions/9372815/how-can-i-convert-my-device-token-nsdata-into-an-nsstring etc. – Larme Jul 06 '20 at 12:22
  • That's frustrating. I see a lot of people blaming the developers for using something 'unsafe'. Well this is what Apple recommend for well over a decade. We didn't all come up with this idea of serializing and deserializing data using description on our own. Thank you for that link. – Chilton Webb Jul 07 '20 at 12:29

1 Answers1

2

Turning some of our comments into an answer:

-[NSObject description] is not meant to be a general-purpose parsing/serialization format, and over time, the descriptions of objects may change. In macOS Catalina, the description for NSData changed to truncate contents in the middle to avoid full display of enormous data blobs.

Currently, I throw a ton of objects into NSData and then export that using the description to a plist, which can then be easily parsed back into an NSData object later. That's all it needs to do, correct dump an NSData object out and read it back in.

Based on your minimal requirements, the simplest resolution for your problem is simply storing NSData objects directly in your plist, instead of their -descriptions. The plist format natively supports binary data, and all Foundation tools (like NSPropertyListSerialization) will accept NSData instances directly for writing out to disk.

If you would like to explicitly convert your binary data into a safely round-trippable string, consider converting it to a base64-encoded string using -[NSData base64EncodedStringWithOptions:], storing the string in the plist, and retrieving later with -[NSData initWithBase64EncodedString:options:].

However, if you require backwards compatibility with the old format (e.g. versions of your app running on macOS Catalina and newer must be able to save files readable on older versions of macOS and your app), you will need to write your own method for replicating the format.

Itai Ferber
  • 28,308
  • 5
  • 77
  • 83