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;
}