123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- /*
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
- #import "RCTMultipartStreamReader.h"
- #import <QuartzCore/CAAnimation.h>
- #define CRLF @"\r\n"
- @implementation RCTMultipartStreamReader {
- __strong NSInputStream *_stream;
- __strong NSString *_boundary;
- CFTimeInterval _lastDownloadProgress;
- }
- - (instancetype)initWithInputStream:(NSInputStream *)stream boundary:(NSString *)boundary
- {
- if (self = [super init]) {
- _stream = stream;
- _boundary = boundary;
- _lastDownloadProgress = CACurrentMediaTime();
- }
- return self;
- }
- - (NSDictionary *)parseHeaders:(NSData *)data
- {
- NSMutableDictionary *headers = [NSMutableDictionary new];
- NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
- NSArray<NSString *> *lines = [text componentsSeparatedByString:CRLF];
- for (NSString *line in lines) {
- NSUInteger location = [line rangeOfString:@":"].location;
- if (location == NSNotFound) {
- continue;
- }
- NSString *key = [line substringToIndex:location];
- NSString *value = [[line substringFromIndex:location + 1]
- stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
- [headers setValue:value forKey:key];
- }
- return headers;
- }
- - (void)emitChunk:(NSData *)data headers:(NSDictionary *)headers callback:(RCTMultipartCallback)callback done:(BOOL)done
- {
- NSData *marker = [CRLF CRLF dataUsingEncoding:NSUTF8StringEncoding];
- NSRange range = [data rangeOfData:marker options:0 range:NSMakeRange(0, data.length)];
- if (range.location == NSNotFound) {
- callback(nil, data, done);
- } else if (headers != nil) {
- // If headers were parsed already just use that to avoid doing it twice.
- NSInteger bodyStart = range.location + marker.length;
- NSData *bodyData = [data subdataWithRange:NSMakeRange(bodyStart, data.length - bodyStart)];
- callback(headers, bodyData, done);
- } else {
- NSData *headersData = [data subdataWithRange:NSMakeRange(0, range.location)];
- NSInteger bodyStart = range.location + marker.length;
- NSData *bodyData = [data subdataWithRange:NSMakeRange(bodyStart, data.length - bodyStart)];
- callback([self parseHeaders:headersData], bodyData, done);
- }
- }
- - (void)emitProgress:(NSDictionary *)headers
- contentLength:(NSUInteger)contentLength
- final:(BOOL)final
- callback:(RCTMultipartProgressCallback)callback
- {
- if (headers == nil) {
- return;
- }
- // Throttle progress events so we don't send more that around 60 per second.
- CFTimeInterval currentTime = CACurrentMediaTime();
- NSInteger headersContentLength = headers[@"Content-Length"] != nil ? [headers[@"Content-Length"] integerValue] : 0;
- if (callback && (currentTime - _lastDownloadProgress > 0.016 || final)) {
- _lastDownloadProgress = currentTime;
- callback(headers, @(headersContentLength), @(contentLength));
- }
- }
- - (BOOL)readAllPartsWithCompletionCallback:(RCTMultipartCallback)callback
- progressCallback:(RCTMultipartProgressCallback)progressCallback
- {
- NSInteger chunkStart = 0;
- NSInteger bytesSeen = 0;
- NSData *delimiter =
- [[NSString stringWithFormat:@"%@--%@%@", CRLF, _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding];
- NSData *closeDelimiter =
- [[NSString stringWithFormat:@"%@--%@--%@", CRLF, _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding];
- NSMutableData *content = [[NSMutableData alloc] initWithCapacity:1];
- NSDictionary *currentHeaders = nil;
- NSUInteger currentHeadersLength = 0;
- const NSUInteger bufferLen = 4 * 1024;
- uint8_t buffer[bufferLen];
- [_stream open];
- while (true) {
- BOOL isCloseDelimiter = NO;
- // Search only a subset of chunk that we haven't seen before + few bytes
- // to allow for the edge case when the delimiter is cut by read call
- NSInteger searchStart = MAX(bytesSeen - (NSInteger)closeDelimiter.length, chunkStart);
- NSRange remainingBufferRange = NSMakeRange(searchStart, content.length - searchStart);
- // Check for delimiters.
- NSRange range = [content rangeOfData:delimiter options:0 range:remainingBufferRange];
- if (range.location == NSNotFound) {
- isCloseDelimiter = YES;
- range = [content rangeOfData:closeDelimiter options:0 range:remainingBufferRange];
- }
- if (range.location == NSNotFound) {
- if (currentHeaders == nil) {
- // Check for the headers delimiter.
- NSData *headersMarker = [CRLF CRLF dataUsingEncoding:NSUTF8StringEncoding];
- NSRange headersRange = [content rangeOfData:headersMarker options:0 range:remainingBufferRange];
- if (headersRange.location != NSNotFound) {
- NSData *headersData = [content subdataWithRange:NSMakeRange(chunkStart, headersRange.location - chunkStart)];
- currentHeadersLength = headersData.length;
- currentHeaders = [self parseHeaders:headersData];
- }
- } else {
- // When headers are loaded start sending progress callbacks.
- [self emitProgress:currentHeaders
- contentLength:content.length - currentHeadersLength
- final:NO
- callback:progressCallback];
- }
- bytesSeen = content.length;
- NSInteger bytesRead = [_stream read:buffer maxLength:bufferLen];
- if (bytesRead <= 0 || _stream.streamError) {
- return NO;
- }
- [content appendBytes:buffer length:bytesRead];
- continue;
- }
- NSInteger chunkEnd = range.location;
- NSInteger length = chunkEnd - chunkStart;
- bytesSeen = chunkEnd;
- // Ignore preamble
- if (chunkStart > 0) {
- NSData *chunk = [content subdataWithRange:NSMakeRange(chunkStart, length)];
- [self emitProgress:currentHeaders
- contentLength:chunk.length - currentHeadersLength
- final:YES
- callback:progressCallback];
- [self emitChunk:chunk headers:currentHeaders callback:callback done:isCloseDelimiter];
- currentHeaders = nil;
- currentHeadersLength = 0;
- }
- if (isCloseDelimiter) {
- return YES;
- }
- chunkStart = chunkEnd + delimiter.length;
- }
- }
- @end
|