Parse multipart response for image download in ios

Solution 1:

I also did have problems with http-multipart response. I wrote a category for NSData. Code below:

NSData+MultipartResponses.h

#import <Foundation/Foundation.h>

@interface NSData (MultipartResponses)

- (NSArray *)multipartArray;
- (NSDictionary *)multipartDictionary;

@end

NSData+MultipartResponses.m

#import "NSData+MultipartResponses.h"

@implementation NSData (MultipartResponses)

static NSMutableDictionary *parseHeaders(const char *headers)
{
   NSMutableDictionary *dict=[NSMutableDictionary dictionary];
   int max=strlen(headers);
   int start=0;
   int cursor=0;
   while(cursor<max)
   {
      while((headers[cursor]!=':')&&(headers[cursor]!='='))
      {
         cursor++;
      }
      NSString *key=[[NSString alloc] initWithBytes:(headers+start) length:(cursor-start) encoding:NSASCIIStringEncoding];
      cursor++;

      while(headers[cursor]==' ')
      {
         cursor++;
      }
      start=cursor;
      while(headers[cursor]&&(headers[cursor]!=';')&&((headers[cursor]!=13)||(headers[cursor+1]!=10)))
      {
         cursor++;
      }

      NSString *value;
      if((headers[start]=='"')&&(headers[cursor-1]=='"'))
      {
         value=[[NSString alloc] initWithBytes:(headers+start+1) length:(cursor-start-2) encoding:NSASCIIStringEncoding];
      }
      else
      {
         value=[[NSString alloc] initWithBytes:(headers+start) length:(cursor-start) encoding:NSASCIIStringEncoding];
      }
      [dict setObject:value forKey:key];

      if(headers[cursor]==';')
      {
         cursor++;
      }
      else
      {
         cursor+=2;
      }

      while(headers[cursor]==' ')
      {
         cursor++;
      }
      start=cursor;
   }
   return dict;
}

- (NSDictionary *)multipartDictionaryWithBoundary:(NSString *)boundary
{
   NSMutableDictionary *dict=[NSMutableDictionary dictionary];

   const char *bytes=(const char *)[self bytes];
   const char *pattern=[boundary cStringUsingEncoding:NSUTF8StringEncoding];

   int cursor=0;
   int start=0;
   int max=[self length];
   int keyNo=0;
   while(cursor<max)
   {
      if(bytes[cursor]==pattern[0])
      {
         int i;
         int patternLength=strlen(pattern);
         BOOL match=YES;
         for(i=0; i<patternLength; i++)
         {
            if(bytes[cursor+i]!=pattern[i])
            {
               match=NO;
               break;
            }
         }
         if(match)
         {
            if(start!=0)
            {
               int startOfHeaders=start+2;
               int cursor2=startOfHeaders;
               while((bytes[cursor2]!=(char)0x0d)||(bytes[cursor2+1]!=(char)0x0a)||(bytes[cursor2+2]!=(char)0x0d)||(bytes[cursor2+3]!=(char)0x0a))
               {
                  cursor2++;
                  if(cursor2+4==max)
                  {
                     break;
                  }
               }
               if(cursor2+4==max)
               {
                  break;
               }
               else
               {
                  int lengthOfHeaders=cursor2-startOfHeaders;
                  char *headers=(char *)malloc((lengthOfHeaders+1)*sizeof(char));
                  strncpy(headers, bytes+startOfHeaders, lengthOfHeaders);
                  headers[lengthOfHeaders]=0;

                  NSMutableDictionary *item=parseHeaders(headers);

                  int startOfData=cursor2+4;
                  int lengthOfData=cursor-startOfData-2;

                  if(([item valueForKey:@"Content-Type"]==nil)&&([item valueForKey:@"filename"]==nil))
                  {
                     NSString *string=[[NSString alloc] initWithBytes:(bytes+startOfData) length:lengthOfData encoding:NSUTF8StringEncoding];
                     keyNo++;
                     [dict setObject:string forKey:[NSString stringWithFormat:@"%d", keyNo]];
                  }
                  else
                  {
                     NSData *data=[NSData dataWithBytes:(bytes+startOfData) length:lengthOfData];
                     [item setObject:data forKey:@"data"];
                     keyNo++;
                     [dict setObject:item forKey:[NSString stringWithFormat:@"%d", keyNo]];
                  }
               }
            }
            cursor=cursor+patternLength-1;
            start=cursor+1;
         }
      }
      cursor++;
   }

   return dict;
}

- (NSArray *)multipartArray
{
   NSDictionary *dict=[self multipartDictionary];
   NSArray *keys=[[dict allKeys] sortedArrayUsingSelector:@selector(localizedStandardCompare:)];
   NSMutableArray *array=[NSMutableArray array];
   for(NSString *key in keys)
   {
      [array addObject:dict[key]];
   }
   return array;
}

- (NSDictionary *)multipartDictionary
{
   const char *bytes=(const char *)[self bytes];
   int cursor=0;
   int max=[self length];
   while(cursor<max)
   {
      if(bytes[cursor]==0x0d)
      {
         break;
      }
      else
      {
         cursor++;
      }
   }
   char *pattern=(char *)malloc((cursor+1)*sizeof(char));
   strncpy(pattern, bytes, cursor);
   pattern[cursor]=0x00;
   NSString *boundary=[[NSString alloc] initWithCString:pattern encoding:NSUTF8StringEncoding];
   free(pattern);
   return [self multipartDictionaryWithBoundary:boundary];
}

@end

Solution 2:

This is an old topic but agree with @possen ("This does not work so great if your parts consist of say UTF-8 and binary data because it tries to treat it as UTF-8, binary data will cause the initWithData function to fail. Instead, you need to stream the data in and handle each type separately for each encoding based upon the content-type.").

If this helps, this is a Swift implementation that handle binary image.

if let multiparts = responseData?.multipartArray(withBoundary: boundary) {
    for part in multiparts {
        if part.contentType == "application/json" {
            let a = try? JSONDecoder().decode(YOUR_DECODABLE_STRUCT.self, from: part.body)
        } else if part.contentType == "image/jpg" {
            let imageData = part.body
        }
    }                    
}
extension Data {
    
    func multipartArray(withBoundary boundary: String, key: String = "Content-Type:") -> [(contentType: String, body: Data)]? {
        func extractBody(_ data: Data) -> Data? {
            guard let startOfLine = key.data(using: .utf8) else { return nil }
            guard let endOfLine = "\r\n".data(using: .utf8) else { return nil }
            var result: Data? = nil
            var pos = data.startIndex
            while let r1 = data[pos...].range(of: startOfLine)
            {
                if let r2 = data[r1.upperBound...].range(of: endOfLine) {
                    pos = r2.upperBound
                }
            }
            
            if pos < data.endIndex {
                result = data[(pos+2)...]
            }
            return result
        }
        
        let multiparts = components(separatedBy: ("--" + boundary))
        var result: [(String, Data)]? = nil
        for part in multiparts
            .enumerated()
            .map({ index, data -> Data in
                if index == multiparts.count-1 {
                    return data.dropLast(2)
                } else {
                    return data
                }
            })
        {
            for contentTypeData in part.slices(between: key, and: "\r") {
                if let contentType = String(data: contentTypeData, encoding: .utf8),
                   let body = extractBody(part)
                {
                    if result == nil {
                        result = [(String, Data)]()
                    }
                    result?.append(
                        (contentType.trimmingCharacters(in: .whitespacesAndNewlines), body)
                    )
                } else {
                    continue
                }
            }
        }
        return result
    }
    
    func slices(between from: String, and to: String) -> [Data] {
        guard let from = from.data(using: .utf8) else { return [] }
        guard let to = to.data(using: .utf8) else { return [] }
        return slices(between: from, and: to)
    }
    
    func slices(between from: Data, and to: Data) -> [Data] {
        var chunks: [Data] = []
        var pos = startIndex
        while let r1 = self[pos...].range(of: from),
              let r2 = self[r1.upperBound...].range(of: to)
        {
            chunks.append(self[r1.upperBound..<r2.lowerBound])
            pos = r1.upperBound
        }
        return chunks
    }
    
    func components(separatedBy separator: String) -> [Data] {
        guard let separator = separator.data(using: .utf8)  else { return [] }
        return components(separatedBy: separator)
    }
    
    func components(separatedBy separator: Data) -> [Data] {
        var chunks: [Data] = []
        var pos = startIndex
        while let r = self[pos...].range(of: separator) {
            if r.lowerBound > pos {
                chunks.append(self[pos..<r.lowerBound])
            }
            pos = r.upperBound
        }
        if pos < endIndex {
            chunks.append(self[pos..<endIndex])
        }
        return chunks
    }
}

Solution 3:

For me, your code didn't work. Instead, I rewrote that code in pure objective-c: Take care that (my) boundaries in this code always have additional -- in front of the next boundary and a final boundary - those are stripped. An NSArray is returned with a NSDictionary for each part, containing the keys "headers" as NSDictionary and "body" as NSData

- (NSArray *)multipartArrayWithBoundary:(NSString *)boundary
{
    NSString *data = [[[NSString alloc] initWithData:self encoding:NSUTF8StringEncoding] autorelease];

    NSArray *multiparts = [data componentsSeparatedByString:[@"--" stringByAppendingString:boundary]]; // remove boundaries
    multiparts = [multiparts subarrayWithRange:NSMakeRange(1, [multiparts count]-2)]; // continued removing of boundaries

    NSMutableArray *toReturn = [NSMutableArray arrayWithCapacity:2];
    for(NSString *part in multiparts)
    {
        part = [part stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
        NSArray *separated = [part componentsSeparatedByString:@"\n\n"];

        NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithCapacity:3];
        for(NSString *headerLine in [[separated objectAtIndex:0] componentsSeparatedByString:@"\n"])
        {
            NSArray *keyVal = [headerLine componentsSeparatedByString:@":"];

            [headers setObject:[[keyVal objectAtIndex:1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] forKey:[[keyVal objectAtIndex:0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
        }

        [toReturn addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[separated objectAtIndex:1] dataUsingEncoding:NSUTF8StringEncoding], @"body", headers, @"headers", nil]];
    }

    return toReturn;
}