Wednesday, February 27, 2013

A Better NSData Description

NSData has not always returned the HEX string of its data.  There was a time when it behaved far more modestly (and usefully in my opinion) when it just indicated it's pointer and length.  Let's take advantage of the method swizzling tools we learned about in a previous post so that we can get NSData's description to be something more useful than a data dump - and we'll make it configurable too so that at runtime you can decide how NSData's description should behave.  I'll also throw in an optimal NSData-to-hex-string method for good measure, hopefully to help counteract the awful practice some developers have adopted of using the NSData description for that serialization.

Putting NSData's description on a diet!





So let's utilize our awesome method swizzling to be able to configure the description of NSData.  The information we can show are pretty simple:

  1. The object info: the class name and the object pointer.  Ex// NSData:0xdeadbeef
  2. The data length: Ex// length=128
  3. The data in HEX: Ex// DEADBEEF 01234567
So let's define those pieces of information as an enum mask:

typedef enum
{
    NSDataDescriptionOption_None       = 0,            // behaves as OS default
    NSDataDescriptionOption_ObjectInfo = 1 << 0,
    NSDataDescriptionOption_Length     = 1 << 1,
    NSDataDescriptionOption_Data       = 1 << 2,
    
    NSDataDescriptionOption_ObjectInfoAndLength = NSDataDescriptionOption_ObjectInfo | NSDataDescriptionOption_Length,
    NSDataDescriptionOption_ObjectInfoAndData   = NSDataDescriptionOption_ObjectInfo | NSDataDescriptionOption_Data,
    NSDataDescriptionOption_LengthAndData       = NSDataDescriptionOption_Length | NSDataDescriptionOption_Data,
    NSDataDescriptionOption_AllOptions          = NSDataDescriptionOption_ObjectInfo | NSDataDescriptionOption_Length | NSDataDescriptionOption_Data,
} NSDataDescriptionOptions;


Now we'll declare a category specifically for configuring the description of NSData objects:


@interface NSData (Description)

+ (NSDataDescriptionOptions) descriptionOptions;
+ (NSDataDescriptionOptions) setDescriptionOptions:(NSDataDescriptionOptions)options; // returns the previous options

@end


Now let's get into the swizzling:


static NSDataDescriptionOptions s_options = NSDataDescriptionOption_None; // OS default

@implementation NSData (Description)

+ (NSDataDescriptionOptions) descriptionOptions
{
    @synchronized (self) {
        return s_options;
    }
}

+ (NSDataDescriptionOptions) setDescriptionOptions:(NSDataDescriptionOptions)options
{
    @synchronized(self) {
        if (s_options != options)
        {
            if (NSDataDescriptionOption_None == s_options ||
                NSDataDescriptionOption_None == options)
            {
                // Swizzle - either to the custom description or back to the native description
                SwizzleInstanceMethods([NSData class], @selector(description), @selector(_configuredDescription));
            }

            NSDataDescriptionOptions tmp = s_options;
            s_options = options;
            options = tmp;
        }
        return options;
    }
}

#pragma mark - Internal

- (NSString*) _configuredDescription
{
    NSDataDescriptionOptions options = s_options;
    NSMutableString* dsc = [NSMutableString string];
    [dsc appendString:@"<"];

    if (NSDataDescriptionOption_ObjectInfo & options)
    {
        [dsc appendFormat:@"%@:%p", NSStringFromClass([self class]), self];
    }

    if (NSDataDescriptionOption_Length & options)
    {
        if (dsc.length > 1)
            [dsc appendString:@" "];
        [dsc appendFormat:@"length=%d", self.length];
    }

    if (NSDataDescriptionOption_Data & options)
    {
        if (dsc.length > 1)
            [dsc appendString:@" "];
        [dsc appendString:[self hexStringValueWithDelimter:@" " everyNBytes:4]]; // hexStringValue will be shown at the end of this post as a "bonus"
    }

    [dsc appendString:@">"];
    return dsc;  // you could return [[dsc copy] autorelease], but it's not a big issue if someone mutates the return string and copying a potentially large string (like when the Data option is set) is memory consuming
}

@end

We'll use a static variable to keep track of what configuration has been set.  When the configuration is None the description method will be the native implementation, otherwise it's our custom _configuredDescription method.

We'll synchronize on both getting and setting the description options, but as an added tool for synchronization we'll have the setDescriptionOptions: class method return the previous NSDataDescriptionOptions.

For the setDescriptionOptions: we take advantage of our method swizzling, knowing that if we are transitioning from the native description to a custom description we need to swizzle and if we are transitioning from our custom description to the native one we just swizzle back.  So simple!  We also swap the static options with the provided options so that we can return the options variable as the previously used description options.

Implementing the actual new description is also straightforward, it's just a matter of adding all the matching pieces of information to the description in a priority order and having the pieces be separated by a space and surrounded in triangle braces.

I did put in some handwaving with the hexStringValueWithDelimeter:everyNBytes: method.  Now, remembering that the description method for NSData has changed in the past and could change in the future, it is better to not presume that the description is a HEX string and that's why we will implement our own NSData to HEX string method and use that.


@interface NSData (Serialize)

- (NSString*) hexStringValue;
- (NSString*) hexStringValueWithDelimeter:(NSString*)delim everyNBytes:(NSUInteger)nBytes;

@end

// can change the base char to be 'a' for lowercase hex strings
#define HEX_ALPHA_BASE_CHAR 'A'

NS_INLINE void byteToHexComponents(unsigned char byte, unichar* pBig, unichar* pLil)
{
    assert(pBig && pLil);
    unsigned char c = byte / 16;
    if (c < 10)
    {
        c += '0';
    }
    else
    {
        c += HEX_ALPHA_BASE_CHAR - 10;
    }
    *pBig = c;
    c     = byte % 16;
    if (c < 10)
    {
        c += '0';
    }
    else
    {
        c += HEX_ALPHA_BASE_CHAR - 10;
    }
    *pLil = c;
}

@implementation NSData (Serialize)

- (NSString*) hexStringValue
{
    return [self hexStringValueWithDelimeter:nil everyNBytes:0]; // no delimeter
}

- (NSString*) hexStringValueWithDelimeter:(NSString*)delim everyNBytes:(NSUInteger)nBytes
{
    NSUInteger     len       = self.length;
    NSUInteger     newLength = 0;
    BOOL           doDelim   = nBytes > 0 && delim.length;
    if (doDelim)
    {
         newLength = (len / nBytes) * delim.length;
         if ((len % nBytes) == 0 && newLength > 0)
             newLength -= delim.length;
    }
    newLength += len*2; // each byte turns into 2 HEX chars
    unichar*       hexChars    = (unichar*)malloc(sizeof(unichar) * newLength);
    unichar*       hexCharsPtr = hexChars;
    unsigned char* bytes       = (unsigned char*)self.bytes;

    // By pulling out the implementation of getCharacters:range: for reuse, we optimize out the ObjC class hierarchy traversal for the implementation while in our loop
    SEL            getCharsSel = @selector(getCharacters:range:);
    IMP            getCharsImp = [delim methodForSelector:getCharsSel];
    NSRange        getCharsRng = NSMakeRange(0, delim.length);
    for (NSUInteger i = 0; i < len; i++)
    {
        if (doDelim && (i > 0) && (i % nBytes == 0))
        {
            getCharsImp(delim, getCharsSel, hexCharsPtr, getCharsRng);
            hexCharsPtr += getCharsRng.length;
        }
        byteToHexComponents(bytes[i], hexCharsPtr++, hexCharsPtr++);
    }
    assert(hexCharsPtr - newLength == hexChars);
    return [[[NSString alloc] initWithCharactersNoCopy:hexChars
                                                length:newLength
                                          freeWhenDone:YES] autorelease];
}

@end
Find this code on github

3 comments: