Dearchiving array of floats in 10.13.2 broken

)I've already reported this bug and raised a DTS incident to get it looked at, but I'm also putting it out there in case anyone else runs into it, or can suggest a workaround within app code. (Bug # 35926492)


10.13.2 breaks the function [-NSKeyedDearchiver decodeArrayOfObjCType:count:at:] when the obj-C type is @encode(float) ("f"). It now throws an exception as follows: *** -[_NSKeyedCoderOldStyleArray initWithCoder:]: unable to decode element in array of size (4) and count (6).

Note that all OS up to 10.13.1 work fine.


A test project reproduces the issue with the following code:


- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{

  NSString* path = [@"~/Desktop/testArchive.plist" stringByExpandingTildeInPath];

  float testValues[] = {1.0, 2.0, 3.5, 4.6, 3.1415926, 0.0};

  NSMutableData* testArchive = [NSMutableData data];

  NSKeyedArchiver* archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:testArchive];

  [archiver encodeArrayOfObjCType:@encode(float) count:6 at:testValues];
  [archiver finishEncoding];

  [testArchive writeToFile:path atomically:YES];

     // now read it back in and see if it can be dearchived - in 10.13.2, this will throw an exception.

  NSData* readArchive = [NSData dataWithContentsOfFile:path];
  NSKeyedUnarchiver* unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:readArchive];

  float readValues[6];

  [unarchiver decodeArrayOfObjCType:@encode(float) count:6 at:readValues]; //<--- throws
  [unarchiver finishDecoding];

  for( int i = 0; i < 6; ++i )
       NSAssert( readValues[i] == testValues[i], @"dearchived data does not match values archived");
}


Note that there are probably better, more robust ways to archive a list of floats. I'm not looking for suggestions about that. The issue is that there are thousands of archives out there in customer land that I need to be able to read.


I've tried substituting the class _NSKeyedCoderOldStyleArray with my own to see if I can read the archive keys directly - I can get NS.count, NS.size and NS.type OK, but $0..$n, nope. Quincey Morris suggested that the actual keys may not literally be these, which is what the archive file contains. I've tried without the leading $ symbol, but with no luck. Someone may know how the keys are changed when dearchiving which might give me a possible workaround.

Replies

Replace your "float" types with "CGFloat".

It's not going to work. The encoded data is 4 bytes per value, and CGFloat is 8 bytes on some platforms (on all modern platforms, in fact). On top of that, the C type — @encode(float), aka "f" — specified in the parameter to the decode method must match what's in the archive, so specifying the wrong type also causes the decode to fail.

I tested it on 10.13.2 and It worked when I replaced all float types and the encode parameter type as well.

Did you encode with @encode(float) and decode with @encode(CGFloat)?

Have you tried changing the decodingFailurePolicy to NSDecodingFailurePolicySetErrorAndReturn to see if you get an error with any useful information?

I am having the same problem. Even writing with NSKeyedArchiver and reading with NSKeyedUnarchiver with same object and build on 10.13.2 is failing. And of course existing archives are unreadable.


I submitted a bug also. Hopefully there is some solution upcoming to this issue, please let us know if they respond to your bug. I noticed that iMovie had a number of crash reports recently with NSKeyArchiver is the crash logs.

Could you replace


float testValues[] = {1.0, 2.0, 3.5, 4.6, 3.1415926, 0.0};

by


testValues = (float *)malloc(sizeof(float) * 6);

to be sure to have the correct size ?

Hi,


I think people are missing the point. Existing data that used NSKeyedArchiver can no longer be decoded on 10.13.2. Any attempts to change object decoded into just results in an archive mismatch. This is new behavior that was introduced on the last update.


Has anyone tried 10.13.3 beta to see if it was fixed?

I run 10.13.2 and have data encoded with NSKeyedArchiver ; they are decoded so far without problem with unarchiver.decodeObject(forKey:)


What do you mean exactly by :

Any attempts to change object decoded into just results in an archive mismatch.


I do modify data and archive back then dearchive without problem.

This is a problem with a single API ("decodeArray(ofObjCType:count:at:)" is the Swift version) and a single Obj-C type encoding string (@encode(float), aka "f"). You won't see a problem unless you use "encodeArray(ofObjCType:count:at:)" to encode the data. The equivalent code using @encode(double) for both encoding and decoding works just fine. And, no, trying to trick it by decoding with a different C type of the same size as float (e.g. @encode(uint32_t)) doesn't work either.


Note that this is a non-keyed encode, which is legal but unusual in a keyed archive.

Thanks for the precision. Effectively, I do not use this way, so could not get the problem.

The issue we have is that existing archives have an array of objects inwhich each object contains an array of floats. So a bit more complicated but same basic problem. So far I have been unable to find a way to decode existing archives without rewriting the entire unarchiver.


Day 6 and no response to my bug submission I hope you are having better luck QuinceyMorris. My customers aren't too happy.

One option that you have until the fix is pushed out is to modify the archived data to allow you to decode your floats as doubles instead. Since floats are losslessly convertible to double values, decoding an array of doubles should give you the same values as you had before, just in a larger container at runtime.


Unfortunately, _NSKeyedCoderOldStyleArray verifies that the ObjC type you gave it on encode is the same as the ObjC type you use on decode, so you can't just request to [decodeArrayOfObjCType:@encode(double) count:... at:...]. However, what you can do is modify the archive to change the ObjC type inside to double (until a fix for this is released, at least) — this is certainly a hacky fix, but might suit your needs in the meantime:


#import <Foundation/Foundation.h>

int main(int argc, char *argv[]) {
    @autoreleasepool {
        const float array[] = {1.0, 2.0, 3.5, 4.6, 3.1415926, 0.0};
        const NSUInteger count = sizeof(array) / sizeof(array[0]);

        NSKeyedArchiver *archiver = [NSKeyedArchiver new];
        archiver.outputFormat = NSPropertyListXMLFormat_v1_0;
        [archiver encodeArrayOfObjCType:@encode(float) count:count at:array];

        NSError *error = nil;
        NSDictionary *archive = [NSPropertyListSerialization propertyListWithData:archiver.encodedData options:NSPropertyListMutableContainers format:NULL error:&error];
        if (!archive) {
            NSLog(@"%@", error);
            return EXIT_FAILURE;
        }
       
        /*
         archive looks like this:

            {
                "$archiver" = NSKeyedArchiver;
                "$objects" =     (
                                  "$null",
                                  {
                                      "$0" = 1;
                                      "$1" = 2;
                                      "$2" = "3.5";
                                      "$3" = "4.599999904632568";
                                      "$4" = "3.141592502593994";
                                      "$5" = 0;
                                      "$class" = "<CFKeyedArchiverUID 0x101d20510 [0x1015ef730]>{value = 2}";
                                      "NS.count" = 6;
                                      "NS.size" = 4;
                                      "NS.type" = 102; // @encode(float)
                                  },
                                  {
                                      "$classes" =             (
                                                                "_NSKeyedCoderOldStyleArray",
                                                                NSObject
                                                                );
                                      "$classname" = "_NSKeyedCoderOldStyleArray";
                                  }
                                  );
                "$top" =     {
                    "$0" = "<CFKeyedArchiverUID 0x101d20df0 [0x1015ef730]>{value = 1}";
                };
                "$version" = 100000;
            }

         Modify "NS.type" = 102 to "NS.type" = 100 (@encode(double))
        This will of course depend on the location of the array in your archive; if not consistent, there are ways of looking it up.
         */
        archive[@"$objects"][1][@"NS.type"] = @(*@encode(double));

        NSData *reencodedData = [NSPropertyListSerialization dataWithPropertyList:archive format:NSPropertyListBinaryFormat_v1_0 options:0 error:&error];
        if (!reencodedData) {
            NSLog(@"%@", error);
            return EXIT_FAILURE;
        }

        NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:reencodedData];
        double decoded[count];
        memset(decoded, sizeof(double), count);

        [unarchiver decodeArrayOfObjCType:@encode(double) count:count at:decoded];
        for (NSUInteger i = 0; i < count; i += 1) {
            NSLog(@"%f == %f: %d", array[i], decoded[i], array[i] == decoded[i]);
        }
    }
}


This requires you to know the location of the encoded array at runtime, but if it's in a consistent spot in the archive every time, this should be relatively easy.

Thanks for the possible workaround. I'm looking into it to see how feasible it is within our actual production code (rather than the very much simplified test case).


In the meantime, I have figured out a similarly hacky workaround that relies on a private method that I can share offline with anyone interested in it. I don't want to put it up on the forum because I wouldn't consider it a really good solution due to the private API usage. But if someone's in a hole and it's the only way out, drop me a line. I can't guarantee that Apple would allow it in an App Store app however.

Hi Mapdiva,


Having many files that I can not reopen, I am interested in your solution. Thank you for telling me how to proceed to get your source. Thank you very much.