diff options
Diffstat (limited to 'Foundation')
36 files changed, 5131 insertions, 355 deletions
diff --git a/Foundation/GTMBase64.h b/Foundation/GTMBase64.h new file mode 100644 index 0000000..169b1c3 --- /dev/null +++ b/Foundation/GTMBase64.h @@ -0,0 +1,183 @@ +// +// GTMBase64.h +// +// Copyright 2006-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +#import <Foundation/Foundation.h> +#import "GTMDefines.h" + +// GTMBase64 +// +/// Helper for handling Base64 and WebSafeBase64 encodings +// +/// The webSafe methods use different character set and also the results aren't +/// always padded to a multiple of 4 characters. This is done so the resulting +/// data can be used in urls and url query arguments without needing any +/// encoding. You must use the webSafe* methods together, the data does not +/// interop with the RFC methods. +// +@interface GTMBase64 : NSObject + +// +// Standard Base64 (RFC) handling +// + +// encodeData: +// +/// Base64 encodes contents of the NSData object. +// +/// Returns: +/// A new autoreleased NSData with the encoded payload. nil for any error. +// ++(NSData *)encodeData:(NSData *)data; + +// decodeData: +// +/// Base64 decodes contents of the NSData object. +// +/// Returns: +/// A new autoreleased NSData with the decoded payload. nil for any error. +// ++(NSData *)decodeData:(NSData *)data; + +// encodeBytes:length: +// +/// Base64 encodes the data pointed at by |bytes|. +// +/// Returns: +/// A new autoreleased NSData with the encoded payload. nil for any error. +// ++(NSData *)encodeBytes:(const void *)bytes length:(NSUInteger)length; + +// decodeBytes:length: +// +/// Base64 decodes the data pointed at by |bytes|. +// +/// Returns: +/// A new autoreleased NSData with the encoded payload. nil for any error. +// ++(NSData *)decodeBytes:(const void *)bytes length:(NSUInteger)length; + +// stringByEncodingData: +// +/// Base64 encodes contents of the NSData object. +// +/// Returns: +/// A new autoreleased NSString with the encoded payload. nil for any error. +// ++(NSString *)stringByEncodingData:(NSData *)data; + +// stringByEncodingBytes:length: +// +/// Base64 encodes the data pointed at by |bytes|. +// +/// Returns: +/// A new autoreleased NSString with the encoded payload. nil for any error. +// ++(NSString *)stringByEncodingBytes:(const void *)bytes length:(NSUInteger)length; + +// decodeString: +// +/// Base64 decodes contents of the NSString. +// +/// Returns: +/// A new autoreleased NSData with the decoded payload. nil for any error. +// ++(NSData *)decodeString:(NSString *)string; + +// +// Modified Base64 encoding so the results can go onto urls. +// +// The changes are in the characters generated and also allows the result to +// not be padded to a multiple of 4. +// Must use the matching call to encode/decode, won't interop with the +// RFC versions. +// + +// webSafeEncodeData:padded: +// +/// WebSafe Base64 encodes contents of the NSData object. If |padded| is YES +/// then padding characters are added so the result length is a multiple of 4. +// +/// Returns: +/// A new autoreleased NSData with the encoded payload. nil for any error. +// ++(NSData *)webSafeEncodeData:(NSData *)data + padded:(BOOL)padded; + +// webSafeDecodeData: +// +/// WebSafe Base64 decodes contents of the NSData object. +// +/// Returns: +/// A new autoreleased NSData with the decoded payload. nil for any error. +// ++(NSData *)webSafeDecodeData:(NSData *)data; + +// webSafeEncodeBytes:length:padded: +// +/// WebSafe Base64 encodes the data pointed at by |bytes|. If |padded| is YES +/// then padding characters are added so the result length is a multiple of 4. +// +/// Returns: +/// A new autoreleased NSData with the encoded payload. nil for any error. +// ++(NSData *)webSafeEncodeBytes:(const void *)bytes + length:(NSUInteger)length + padded:(BOOL)padded; + +// webSafeDecodeBytes:length: +// +/// WebSafe Base64 decodes the data pointed at by |bytes|. +// +/// Returns: +/// A new autoreleased NSData with the encoded payload. nil for any error. +// ++(NSData *)webSafeDecodeBytes:(const void *)bytes length:(NSUInteger)length; + +// stringByWebSafeEncodingData:padded: +// +/// WebSafe Base64 encodes contents of the NSData object. If |padded| is YES +/// then padding characters are added so the result length is a multiple of 4. +// +/// Returns: +/// A new autoreleased NSString with the encoded payload. nil for any error. +// ++(NSString *)stringByWebSafeEncodingData:(NSData *)data + padded:(BOOL)padded; + +// stringByWebSafeEncodingBytes:length:padded: +// +/// WebSafe Base64 encodes the data pointed at by |bytes|. If |padded| is YES +/// then padding characters are added so the result length is a multiple of 4. +// +/// Returns: +/// A new autoreleased NSString with the encoded payload. nil for any error. +// ++(NSString *)stringByWebSafeEncodingBytes:(const void *)bytes + length:(NSUInteger)length + padded:(BOOL)padded; + +// webSafeDecodeString: +// +/// WebSafe Base64 decodes contents of the NSString. +// +/// Returns: +/// A new autoreleased NSData with the decoded payload. nil for any error. +// ++(NSData *)webSafeDecodeString:(NSString *)string; + +@end diff --git a/Foundation/GTMBase64.m b/Foundation/GTMBase64.m new file mode 100644 index 0000000..06d0414 --- /dev/null +++ b/Foundation/GTMBase64.m @@ -0,0 +1,697 @@ +// +// GTMBase64.m +// +// Copyright 2006-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +#import "GTMBase64.h" +#import "GTMDefines.h" + +static const char *kBase64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static const char *kWebSafeBase64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +static const char kBase64PaddingChar = '='; +static const char kBase64InvalidChar = 99; + +static const char kBase64DecodeChars[] = { + // This array was generated by the following code: + // #include <sys/time.h> + // #include <stdlib.h> + // #include <string.h> + // main() + // { + // static const char Base64[] = + // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + // char *pos; + // int idx, i, j; + // printf(" "); + // for (i = 0; i < 255; i += 8) { + // for (j = i; j < i + 8; j++) { + // pos = strchr(Base64, j); + // if ((pos == NULL) || (j == 0)) + // idx = 99; + // else + // idx = pos - Base64; + // if (idx == 99) + // printf(" %2d, ", idx); + // else + // printf(" %2d/*%c*/,", idx, j); + // } + // printf("\n "); + // } + // } + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 62/*+*/, 99, 99, 99, 63/*/ */, + 52/*0*/, 53/*1*/, 54/*2*/, 55/*3*/, 56/*4*/, 57/*5*/, 58/*6*/, 59/*7*/, + 60/*8*/, 61/*9*/, 99, 99, 99, 99, 99, 99, + 99, 0/*A*/, 1/*B*/, 2/*C*/, 3/*D*/, 4/*E*/, 5/*F*/, 6/*G*/, + 7/*H*/, 8/*I*/, 9/*J*/, 10/*K*/, 11/*L*/, 12/*M*/, 13/*N*/, 14/*O*/, + 15/*P*/, 16/*Q*/, 17/*R*/, 18/*S*/, 19/*T*/, 20/*U*/, 21/*V*/, 22/*W*/, + 23/*X*/, 24/*Y*/, 25/*Z*/, 99, 99, 99, 99, 99, + 99, 26/*a*/, 27/*b*/, 28/*c*/, 29/*d*/, 30/*e*/, 31/*f*/, 32/*g*/, + 33/*h*/, 34/*i*/, 35/*j*/, 36/*k*/, 37/*l*/, 38/*m*/, 39/*n*/, 40/*o*/, + 41/*p*/, 42/*q*/, 43/*r*/, 44/*s*/, 45/*t*/, 46/*u*/, 47/*v*/, 48/*w*/, + 49/*x*/, 50/*y*/, 51/*z*/, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 +}; + +static const char kWebSafeBase64DecodeChars[] = { + // This array was generated by the following code: + // #include <sys/time.h> + // #include <stdlib.h> + // #include <string.h> + // main() + // { + // static const char Base64[] = + // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + // char *pos; + // int idx, i, j; + // printf(" "); + // for (i = 0; i < 255; i += 8) { + // for (j = i; j < i + 8; j++) { + // pos = strchr(Base64, j); + // if ((pos == NULL) || (j == 0)) + // idx = 99; + // else + // idx = pos - Base64; + // if (idx == 99) + // printf(" %2d, ", idx); + // else + // printf(" %2d/*%c*/,", idx, j); + // } + // printf("\n "); + // } + // } + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 62/*-*/, 99, 99, + 52/*0*/, 53/*1*/, 54/*2*/, 55/*3*/, 56/*4*/, 57/*5*/, 58/*6*/, 59/*7*/, + 60/*8*/, 61/*9*/, 99, 99, 99, 99, 99, 99, + 99, 0/*A*/, 1/*B*/, 2/*C*/, 3/*D*/, 4/*E*/, 5/*F*/, 6/*G*/, + 7/*H*/, 8/*I*/, 9/*J*/, 10/*K*/, 11/*L*/, 12/*M*/, 13/*N*/, 14/*O*/, + 15/*P*/, 16/*Q*/, 17/*R*/, 18/*S*/, 19/*T*/, 20/*U*/, 21/*V*/, 22/*W*/, + 23/*X*/, 24/*Y*/, 25/*Z*/, 99, 99, 99, 99, 63/*_*/, + 99, 26/*a*/, 27/*b*/, 28/*c*/, 29/*d*/, 30/*e*/, 31/*f*/, 32/*g*/, + 33/*h*/, 34/*i*/, 35/*j*/, 36/*k*/, 37/*l*/, 38/*m*/, 39/*n*/, 40/*o*/, + 41/*p*/, 42/*q*/, 43/*r*/, 44/*s*/, 45/*t*/, 46/*u*/, 47/*v*/, 48/*w*/, + 49/*x*/, 50/*y*/, 51/*z*/, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 +}; + + +// Tests a charact to see if it's a whitespace character. +// +// Returns: +// YES if the character is a whitespace character. +// NO if the character is not a whitespace character. +// +FOUNDATION_STATIC_INLINE BOOL IsSpace(unsigned char c) { + // we use our own mapping here because we don't want anything w/ locale + // support. + static BOOL kSpaces[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // 0-9 + 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 10-19 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20-29 + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, // 30-39 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40-49 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 50-59 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60-69 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 70-79 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80-89 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 90-99 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 100-109 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 110-119 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 120-129 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 130-139 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 140-149 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 150-159 + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-169 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 170-179 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 180-189 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 190-199 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 200-209 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 210-219 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 220-229 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 230-239 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 240-249 + 0, 0, 0, 0, 0, 1, // 250-255 + }; + return kSpaces[c]; +} + +// Calculate how long the data will be once it's base64 encoded. +// +// Returns: +// The guessed encoded length for a source length +// +FOUNDATION_STATIC_INLINE NSUInteger CalcEncodedLength(NSUInteger srcLen, + BOOL padded) { + NSUInteger intermediate_result = 8 * srcLen + 5; + NSUInteger len = intermediate_result / 6; + if (padded) { + len = ((len + 3) / 4) * 4; + } + return len; +} + +// Tries to calculate how long the data will be once it's base64 decoded. +// Unlinke the above, this is always an upperbound, since the source data +// could have spaces and might end with the padding characters on them. +// +// Returns: +// The guessed decoded length for a source length +// +FOUNDATION_STATIC_INLINE NSUInteger GuessDecodedLength(NSUInteger srcLen) { + return (srcLen + 3) / 4 * 3; +} + + +@interface GTMBase64 (PrivateMethods) + ++(NSData *)baseEncode:(const void *)bytes + length:(NSUInteger)length + charset:(const char *)charset + padded:(BOOL)padded; + ++(NSData *)baseDecode:(const void *)bytes + length:(NSUInteger)length + charset:(const char*)charset + requirePadding:(BOOL)requirePadding; + ++(NSUInteger)baseEncode:(const char *)srcBytes + srcLen:(NSUInteger)srcLen + destBytes:(char *)destBytes + destLen:(NSUInteger)destLen + charset:(const char *)charset + padded:(BOOL)padded; + ++(NSUInteger)baseDecode:(const char *)srcBytes + srcLen:(NSUInteger)srcLen + destBytes:(char *)destBytes + destLen:(NSUInteger)destLen + charset:(const char *)charset + requirePadding:(BOOL)requirePadding; + +@end + + +@implementation GTMBase64 + +// +// Standard Base64 (RFC) handling +// + ++(NSData *)encodeData:(NSData *)data { + return [self baseEncode:[data bytes] + length:[data length] + charset:kBase64EncodeChars + padded:YES]; +} + ++(NSData *)decodeData:(NSData *)data { + return [self baseDecode:[data bytes] + length:[data length] + charset:kBase64DecodeChars + requirePadding:YES]; +} + ++(NSData *)encodeBytes:(const void *)bytes length:(NSUInteger)length { + return [self baseEncode:bytes + length:length + charset:kBase64EncodeChars + padded:YES]; +} + ++(NSData *)decodeBytes:(const void *)bytes length:(NSUInteger)length { + return [self baseDecode:bytes + length:length + charset:kBase64DecodeChars + requirePadding:YES]; +} + ++(NSString *)stringByEncodingData:(NSData *)data { + NSString *result = nil; + NSData *converted = [self baseEncode:[data bytes] + length:[data length] + charset:kBase64EncodeChars + padded:YES]; + if (converted) { + result = [[[NSString alloc] initWithData:converted + encoding:NSASCIIStringEncoding] autorelease]; + } + return result; +} + ++(NSString *)stringByEncodingBytes:(const void *)bytes length:(NSUInteger)length { + NSString *result = nil; + NSData *converted = [self baseEncode:bytes + length:length + charset:kBase64EncodeChars + padded:YES]; + if (converted) { + result = [[[NSString alloc] initWithData:converted + encoding:NSASCIIStringEncoding] autorelease]; + } + return result; +} + ++(NSData *)decodeString:(NSString *)string { + NSData *result = nil; + NSData *data = [string dataUsingEncoding:NSASCIIStringEncoding]; + if (data) { + result = [self baseDecode:[data bytes] + length:[data length] + charset:kBase64DecodeChars + requirePadding:YES]; + } + return result; +} + +// +// Modified Base64 encoding so the results can go onto urls. +// +// The changes are in the characters generated and also the result isn't +// padded to a multiple of 4. +// Must use the matching call to encode/decode, won't interop with the +// RFC versions. +// + ++(NSData *)webSafeEncodeData:(NSData *)data + padded:(BOOL)padded { + return [self baseEncode:[data bytes] + length:[data length] + charset:kWebSafeBase64EncodeChars + padded:padded]; +} + ++(NSData *)webSafeDecodeData:(NSData *)data { + return [self baseDecode:[data bytes] + length:[data length] + charset:kWebSafeBase64DecodeChars + requirePadding:NO]; +} + ++(NSData *)webSafeEncodeBytes:(const void *)bytes + length:(NSUInteger)length + padded:(BOOL)padded { + return [self baseEncode:bytes + length:length + charset:kWebSafeBase64EncodeChars + padded:padded]; +} + ++(NSData *)webSafeDecodeBytes:(const void *)bytes length:(NSUInteger)length { + return [self baseDecode:bytes + length:length + charset:kWebSafeBase64DecodeChars + requirePadding:NO]; +} + ++(NSString *)stringByWebSafeEncodingData:(NSData *)data + padded:(BOOL)padded { + NSString *result = nil; + NSData *converted = [self baseEncode:[data bytes] + length:[data length] + charset:kWebSafeBase64EncodeChars + padded:padded]; + if (converted) { + result = [[[NSString alloc] initWithData:converted + encoding:NSASCIIStringEncoding] autorelease]; + } + return result; +} + ++(NSString *)stringByWebSafeEncodingBytes:(const void *)bytes + length:(NSUInteger)length + padded:(BOOL)padded { + NSString *result = nil; + NSData *converted = [self baseEncode:bytes + length:length + charset:kWebSafeBase64EncodeChars + padded:padded]; + if (converted) { + result = [[[NSString alloc] initWithData:converted + encoding:NSASCIIStringEncoding] autorelease]; + } + return result; +} + ++(NSData *)webSafeDecodeString:(NSString *)string { + NSData *result = nil; + NSData *data = [string dataUsingEncoding:NSASCIIStringEncoding]; + if (data) { + result = [self baseDecode:[data bytes] + length:[data length] + charset:kWebSafeBase64DecodeChars + requirePadding:NO]; + } + return result; +} + +@end + +@implementation GTMBase64 (PrivateMethods) + +// +// baseEncode:length:charset:padded: +// +// Does the common lifting of creating the dest NSData. it creates & sizes the +// data for the results. |charset| is the characters to use for the encoding +// of the data. |padding| controls if the encoded data should be padded to a +// multiple of 4. +// +// Returns: +// an autorelease NSData with the encoded data, nil if any error. +// ++(NSData *)baseEncode:(const void *)bytes + length:(NSUInteger)length + charset:(const char *)charset + padded:(BOOL)padded { + // how big could it be? + NSUInteger maxLength = CalcEncodedLength(length, padded); + // make space + NSMutableData *result = [NSMutableData data]; + [result setLength:maxLength]; + // do it + NSUInteger finalLength = [self baseEncode:bytes + srcLen:length + destBytes:[result mutableBytes] + destLen:[result length] + charset:charset + padded:padded]; + if (finalLength) { + _GTMDevAssert(finalLength == maxLength, @"how did we calc the length wrong?"); + } else { + // shouldn't happen, this means we ran out of space + result = nil; + } + return result; +} + +// +// baseDecode:length:charset:requirePadding: +// +// Does the common lifting of creating the dest NSData. it creates & sizes the +// data for the results. |charset| is the characters to use for the decoding +// of the data. +// +// Returns: +// an autorelease NSData with the decoded data, nil if any error. +// +// ++(NSData *)baseDecode:(const void *)bytes + length:(NSUInteger)length + charset:(const char *)charset + requirePadding:(BOOL)requirePadding { + // could try to calculate what it will end up as + NSUInteger maxLength = GuessDecodedLength(length); + // make space + NSMutableData *result = [NSMutableData data]; + [result setLength:maxLength]; + // do it + NSUInteger finalLength = [self baseDecode:bytes + srcLen:length + destBytes:[result mutableBytes] + destLen:[result length] + charset:charset + requirePadding:requirePadding]; + if (finalLength) { + if (finalLength != maxLength) { + // resize down to how big it was + [result setLength:finalLength]; + } + } else { + // either an error in the args, or we ran out of space + result = nil; + } + return result; +} + +// +// baseEncode:srcLen:destBytes:destLen:charset:padded: +// +// Encodes the buffer into the larger. returns the length of the encoded +// data, or zero for an error. +// |charset| is the characters to use for the encoding +// |padded| tells if the result should be padded to a multiple of 4. +// +// Returns: +// the length of the encoded data. zero if any error. +// ++(NSUInteger)baseEncode:(const char *)srcBytes + srcLen:(NSUInteger)srcLen + destBytes:(char *)destBytes + destLen:(NSUInteger)destLen + charset:(const char *)charset + padded:(BOOL)padded { + if (!srcLen || !destLen || !srcBytes || !destBytes) { + return 0; + } + + char *curDest = destBytes; + const unsigned char *curSrc = (const unsigned char *)(srcBytes); + + // Three bytes of data encodes to four characters of cyphertext. + // So we can pump through three-byte chunks atomically. + while (srcLen > 2) { + // space? + _GTMDevAssert(destLen >= 4, @"our calc for encoded length was wrong"); + curDest[0] = charset[curSrc[0] >> 2]; + curDest[1] = charset[((curSrc[0] & 0x03) << 4) + (curSrc[1] >> 4)]; + curDest[2] = charset[((curSrc[1] & 0x0f) << 2) + (curSrc[2] >> 6)]; + curDest[3] = charset[curSrc[2] & 0x3f]; + + curDest += 4; + curSrc += 3; + srcLen -= 3; + destLen -= 4; + } + + // now deal with the tail (<=2 bytes) + switch (srcLen) { + case 0: + // Nothing left; nothing more to do. + break; + case 1: + // One byte left: this encodes to two characters, and (optionally) + // two pad characters to round out the four-character cypherblock. + _GTMDevAssert(destLen >= 2, @"our calc for encoded length was wrong"); + curDest[0] = charset[curSrc[0] >> 2]; + curDest[1] = charset[(curSrc[0] & 0x03) << 4]; + curDest += 2; + destLen -= 2; + if (padded) { + _GTMDevAssert(destLen >= 2, @"our calc for encoded length was wrong"); + curDest[0] = kBase64PaddingChar; + curDest[1] = kBase64PaddingChar; + curDest += 2; + destLen -= 2; + } + break; + case 2: + // Two bytes left: this encodes to three characters, and (optionally) + // one pad character to round out the four-character cypherblock. + _GTMDevAssert(destLen >= 3, @"our calc for encoded length was wrong"); + curDest[0] = charset[curSrc[0] >> 2]; + curDest[1] = charset[((curSrc[0] & 0x03) << 4) + (curSrc[1] >> 4)]; + curDest[2] = charset[(curSrc[1] & 0x0f) << 2]; + curDest += 3; + destLen -= 3; + if (padded) { + _GTMDevAssert(destLen >= 1, @"our calc for encoded length was wrong"); + curDest[0] = kBase64PaddingChar; + curDest += 1; + destLen -= 1; + } + break; + } + // return the length + return (curDest - destBytes); +} + +// +// baseDecode:srcLen:destBytes:destLen:charset:requirePadding: +// +// Decodes the buffer into the larger. returns the length of the decoded +// data, or zero for an error. +// |charset| is the character decoding buffer to use +// +// Returns: +// the length of the encoded data. zero if any error. +// ++(NSUInteger)baseDecode:(const char *)srcBytes + srcLen:(NSUInteger)srcLen + destBytes:(char *)destBytes + destLen:(NSUInteger)destLen + charset:(const char *)charset + requirePadding:(BOOL)requirePadding { + if (!srcLen || !destLen || !srcBytes || !destBytes) { + return 0; + } + + int decode; + NSUInteger destIndex = 0; + int state = 0; + char ch = 0; + while (srcLen-- && (ch = *srcBytes++) != 0) { + if (IsSpace(ch)) // Skip whitespace + continue; + + if (ch == kBase64PaddingChar) + break; + + decode = charset[(unsigned int)ch]; + if (decode == kBase64InvalidChar) + return 0; + + // Four cyphertext characters decode to three bytes. + // Therefore we can be in one of four states. + switch (state) { + case 0: + // We're at the beginning of a four-character cyphertext block. + // This sets the high six bits of the first byte of the + // plaintext block. + _GTMDevAssert(destIndex < destLen, @"our calc for decoded length was wrong"); + destBytes[destIndex] = decode << 2; + state = 1; + break; + case 1: + // We're one character into a four-character cyphertext block. + // This sets the low two bits of the first plaintext byte, + // and the high four bits of the second plaintext byte. + _GTMDevAssert((destIndex+1) < destLen, @"our calc for decoded length was wrong"); + destBytes[destIndex] |= decode >> 4; + destBytes[destIndex+1] = (decode & 0x0f) << 4; + destIndex++; + state = 2; + break; + case 2: + // We're two characters into a four-character cyphertext block. + // This sets the low four bits of the second plaintext + // byte, and the high two bits of the third plaintext byte. + // However, if this is the end of data, and those two + // bits are zero, it could be that those two bits are + // leftovers from the encoding of data that had a length + // of two mod three. + _GTMDevAssert((destIndex+1) < destLen, @"our calc for decoded length was wrong"); + destBytes[destIndex] |= decode >> 2; + destBytes[destIndex+1] = (decode & 0x03) << 6; + destIndex++; + state = 3; + break; + case 3: + // We're at the last character of a four-character cyphertext block. + // This sets the low six bits of the third plaintext byte. + _GTMDevAssert(destIndex < destLen, @"our calc for decoded length was wrong"); + destBytes[destIndex] |= decode; + destIndex++; + state = 0; + break; + } + } + + // We are done decoding Base-64 chars. Let's see if we ended + // on a byte boundary, and/or with erroneous trailing characters. + if (ch == kBase64PaddingChar) { // We got a pad char + if ((state == 0) || (state == 1)) { + return 0; // Invalid '=' in first or second position + } + if (srcLen == 0) { + if (state == 2) { // We run out of input but we still need another '=' + return 0; + } + // Otherwise, we are in state 3 and only need this '=' + } else { + if (state == 2) { // need another '=' + while ((ch = *srcBytes++) && (srcLen-- > 0)) { + if (!IsSpace(ch)) + break; + } + if (ch != kBase64PaddingChar) { + return 0; + } + } + // state = 1 or 2, check if all remain padding is space + while ((ch = *srcBytes++) && (srcLen-- > 0)) { + if (!IsSpace(ch)) { + return 0; + } + } + } + } else { + // We ended by seeing the end of the string. + + if (requirePadding) { + // If we require padding, then anything but state 0 is an error. + if (state != 0) { + return 0; + } + } else { + // Make sure we have no partial bytes lying around. Note that we do not + // require trailing '=', so states 2 and 3 are okay too. + if (state == 1) { + return 0; + } + } + } + + // If then next piece of output was valid and got written to it means we got a + // very carefully crafted input that appeared valid but contains some trailing + // bits past the real length, so just toss the thing. + if ((destIndex < destLen) && + (destBytes[destIndex] != 0)) { + return 0; + } + + return destIndex; +} + +@end diff --git a/Foundation/GTMBase64Test.m b/Foundation/GTMBase64Test.m new file mode 100644 index 0000000..358c6ec --- /dev/null +++ b/Foundation/GTMBase64Test.m @@ -0,0 +1,437 @@ +// +// GTMBase64Test.m +// +// Copyright 2006-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +#import "GTMSenTestCase.h" +#import "GTMBase64.h" +#include <stdlib.h> // for randiom/srandomdev + +static void FillWithRandom(char *data, NSUInteger len) { + char *max = data + len; + for ( ; data < max ; ++data) { + *data = random() & 0xFF; + } +} + +static BOOL NoEqualChar(NSData *data) { + const char *scan = [data bytes]; + const char *max = scan + [data length]; + for ( ; scan < max ; ++scan) { + if (*scan == '=') { + return NO; + } + } + return YES; +} + +@interface GTMBase64Test : SenTestCase +@end + +@implementation GTMBase64Test + +- (void)setUp { + // seed random from /dev/random + srandomdev(); +} + +- (void)testBase64 { + // generate a range of sizes w/ random content + for (int x = 1 ; x < 1024 ; ++x) { + NSMutableData *data = [NSMutableData data]; + STAssertNotNil(data, @"failed to alloc data block"); + + [data setLength:x]; + FillWithRandom([data mutableBytes], [data length]); + + // w/ *Bytes apis + NSData *encoded = [GTMBase64 encodeBytes:[data bytes] length:[data length]]; + STAssertEquals(([encoded length] % 4), (NSUInteger)0, + @"encoded size via *Bytes apis should be a multiple of 4"); + NSData *dataPrime = [GTMBase64 decodeBytes:[encoded bytes] + length:[encoded length]]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip via *Bytes apis"); + + // w/ *Data apis + encoded = [GTMBase64 encodeData:data]; + STAssertEquals(([encoded length] % 4), (NSUInteger)0, + @"encoded size via *Data apis should be a multiple of 4"); + dataPrime = [GTMBase64 decodeData:encoded]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip via *Data apis"); + + // Bytes to String and back + NSString *encodedString = [GTMBase64 stringByEncodingBytes:[data bytes] + length:[data length]]; + STAssertEquals(([encodedString length] % 4), (NSUInteger)0, + @"encoded size for Bytes to Strings should be a multiple of 4"); + dataPrime = [GTMBase64 decodeString:encodedString]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip for Bytes to Strings"); + + // Data to String and back + encodedString = [GTMBase64 stringByEncodingData:data]; + STAssertEquals(([encodedString length] % 4), (NSUInteger)0, + @"encoded size for Data to Strings should be a multiple of 4"); + dataPrime = [GTMBase64 decodeString:encodedString]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip for Bytes to Strings"); + } + + { + // now test all byte values + NSMutableData *data = [NSMutableData data]; + STAssertNotNil(data, @"failed to alloc data block"); + + [data setLength:256]; + unsigned char *scan = (unsigned char*)[data mutableBytes]; + for (int x = 0 ; x <= 255 ; ++x) { + *scan++ = x; + } + + // w/ *Bytes apis + NSData *encoded = [GTMBase64 encodeBytes:[data bytes] length:[data length]]; + STAssertEquals(([encoded length] % 4), (NSUInteger)0, + @"encoded size via *Bytes apis should be a multiple of 4"); + NSData *dataPrime = [GTMBase64 decodeBytes:[encoded bytes] + length:[encoded length]]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip via *Bytes apis"); + + // w/ *Data apis + encoded = [GTMBase64 encodeData:data]; + STAssertEquals(([encoded length] % 4), (NSUInteger)0, + @"encoded size via *Data apis should be a multiple of 4"); + dataPrime = [GTMBase64 decodeData:encoded]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip via *Data apis"); + + // Bytes to String and back + NSString *encodedString = [GTMBase64 stringByEncodingBytes:[data bytes] + length:[data length]]; + STAssertEquals(([encodedString length] % 4), (NSUInteger)0, + @"encoded size for Bytes to Strings should be a multiple of 4"); + dataPrime = [GTMBase64 decodeString:encodedString]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip for Bytes to Strings"); + + // Data to String and back + encodedString = [GTMBase64 stringByEncodingData:data]; + STAssertEquals(([encodedString length] % 4), (NSUInteger)0, + @"encoded size for Data to Strings should be a multiple of 4"); + dataPrime = [GTMBase64 decodeString:encodedString]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip for Data to Strings"); + } + + { + // test w/ a mix of spacing characters + + // generate some data, encode it, and add spaces + NSMutableData *data = [NSMutableData data]; + STAssertNotNil(data, @"failed to alloc data block"); + + [data setLength:253]; // should get some padding chars on the end + FillWithRandom([data mutableBytes], [data length]); + + NSString *encodedString = [GTMBase64 stringByEncodingData:data]; + NSMutableString *encodedAndSpaced = + [[encodedString mutableCopy] autorelease]; + + NSString *spaces[] = { @"\t", @"\n", @"\r", @" " }; + const NSUInteger numSpaces = sizeof(spaces) / sizeof(NSString*); + for (int x = 0 ; x < 512 ; ++x) { + NSUInteger offset = random() % ([encodedAndSpaced length] + 1); + [encodedAndSpaced insertString:spaces[random() % numSpaces] + atIndex:offset]; + } + + // we'll need it as data for apis + NSData *encodedAsData = + [encodedAndSpaced dataUsingEncoding:NSASCIIStringEncoding]; + STAssertNotNil(encodedAsData, @"failed to extract from string"); + STAssertEquals([encodedAsData length], [encodedAndSpaced length], + @"lengths for encoded string and data didn't match?"); + + // all the decode modes + NSData *dataPrime = [GTMBase64 decodeData:encodedAsData]; + STAssertEqualObjects(data, dataPrime, + @"failed Data decode w/ spaces"); + dataPrime = [GTMBase64 decodeBytes:[encodedAsData bytes] + length:[encodedAsData length]]; + STAssertEqualObjects(data, dataPrime, + @"failed Bytes decode w/ spaces"); + dataPrime = [GTMBase64 decodeString:encodedAndSpaced]; + STAssertEqualObjects(data, dataPrime, + @"failed String decode w/ spaces"); + } +} + +- (void)testWebSafeBase64 { + // loop to test w/ and w/o padding + for (int paddedLoop = 0; paddedLoop < 2 ; ++paddedLoop) { + BOOL padded = (paddedLoop == 1); + + // generate a range of sizes w/ random content + for (int x = 1 ; x < 1024 ; ++x) { + NSMutableData *data = [NSMutableData data]; + STAssertNotNil(data, @"failed to alloc data block"); + + [data setLength:x]; + FillWithRandom([data mutableBytes], [data length]); + + // w/ *Bytes apis + NSData *encoded = [GTMBase64 webSafeEncodeBytes:[data bytes] + length:[data length] + padded:padded]; + if (padded) { + STAssertEquals(([encoded length] % 4), (NSUInteger)0, + @"encoded size via *Bytes apis should be a multiple of 4"); + } else { + STAssertTrue(NoEqualChar(encoded), + @"encoded via *Bytes apis had a base64 padding char"); + } + NSData *dataPrime = [GTMBase64 webSafeDecodeBytes:[encoded bytes] + length:[encoded length]]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip via *Bytes apis"); + + // w/ *Data apis + encoded = [GTMBase64 webSafeEncodeData:data padded:padded]; + if (padded) { + STAssertEquals(([encoded length] % 4), (NSUInteger)0, + @"encoded size via *Data apis should be a multiple of 4"); + } else { + STAssertTrue(NoEqualChar(encoded), + @"encoded via *Data apis had a base64 padding char"); + } + dataPrime = [GTMBase64 webSafeDecodeData:encoded]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip via *Data apis"); + + // Bytes to String and back + NSString *encodedString = + [GTMBase64 stringByWebSafeEncodingBytes:[data bytes] + length:[data length] + padded:padded]; + if (padded) { + STAssertEquals(([encoded length] % 4), (NSUInteger)0, + @"encoded size via *Bytes apis should be a multiple of 4"); + } else { + STAssertTrue(NoEqualChar(encoded), + @"encoded via Bytes to Strings had a base64 padding char"); + } + dataPrime = [GTMBase64 webSafeDecodeString:encodedString]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip for Bytes to Strings"); + + // Data to String and back + encodedString = + [GTMBase64 stringByWebSafeEncodingData:data padded:padded]; + if (padded) { + STAssertEquals(([encoded length] % 4), (NSUInteger)0, + @"encoded size via *Data apis should be a multiple of 4"); + } else { + STAssertTrue(NoEqualChar(encoded), + @"encoded via Data to Strings had a base64 padding char"); + } + dataPrime = [GTMBase64 webSafeDecodeString:encodedString]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip for Data to Strings"); + } + + { + // now test all byte values + NSMutableData *data = [NSMutableData data]; + STAssertNotNil(data, @"failed to alloc data block"); + + [data setLength:256]; + unsigned char *scan = (unsigned char*)[data mutableBytes]; + for (int x = 0 ; x <= 255 ; ++x) { + *scan++ = x; + } + + // w/ *Bytes apis + NSData *encoded = + [GTMBase64 webSafeEncodeBytes:[data bytes] + length:[data length] + padded:padded]; + if (padded) { + STAssertEquals(([encoded length] % 4), (NSUInteger)0, + @"encoded size via *Bytes apis should be a multiple of 4"); + } else { + STAssertTrue(NoEqualChar(encoded), + @"encoded via *Bytes apis had a base64 padding char"); + } + NSData *dataPrime = [GTMBase64 webSafeDecodeBytes:[encoded bytes] + length:[encoded length]]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip via *Bytes apis"); + + // w/ *Data apis + encoded = [GTMBase64 webSafeEncodeData:data padded:padded]; + if (padded) { + STAssertEquals(([encoded length] % 4), (NSUInteger)0, + @"encoded size via *Data apis should be a multiple of 4"); + } else { + STAssertTrue(NoEqualChar(encoded), + @"encoded via *Data apis had a base64 padding char"); + } + dataPrime = [GTMBase64 webSafeDecodeData:encoded]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip via *Data apis"); + + // Bytes to String and back + NSString *encodedString = + [GTMBase64 stringByWebSafeEncodingBytes:[data bytes] + length:[data length] + padded:padded]; + if (padded) { + STAssertEquals(([encoded length] % 4), (NSUInteger)0, + @"encoded size via *Bytes apis should be a multiple of 4"); + } else { + STAssertTrue(NoEqualChar(encoded), + @"encoded via Bytes to Strings had a base64 padding char"); + } + dataPrime = [GTMBase64 webSafeDecodeString:encodedString]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip for Bytes to Strings"); + + // Data to String and back + encodedString = + [GTMBase64 stringByWebSafeEncodingData:data padded:padded]; + if (padded) { + STAssertEquals(([encoded length] % 4), (NSUInteger)0, + @"encoded size via *Data apis should be a multiple of 4"); + } else { + STAssertTrue(NoEqualChar(encoded), + @"encoded via Data to Strings had a base64 padding char"); + } + dataPrime = [GTMBase64 webSafeDecodeString:encodedString]; + STAssertEqualObjects(data, dataPrime, + @"failed to round trip for Data to Strings"); + } + + { + // test w/ a mix of spacing characters + + // generate some data, encode it, and add spaces + NSMutableData *data = [NSMutableData data]; + STAssertNotNil(data, @"failed to alloc data block"); + + [data setLength:253]; // should get some padding chars on the end + FillWithRandom([data mutableBytes], [data length]); + + NSString *encodedString = [GTMBase64 stringByWebSafeEncodingData:data + padded:padded]; + NSMutableString *encodedAndSpaced = + [[encodedString mutableCopy] autorelease]; + + NSString *spaces[] = { @"\t", @"\n", @"\r", @" " }; + const NSUInteger numSpaces = sizeof(spaces) / sizeof(NSString*); + for (int x = 0 ; x < 512 ; ++x) { + NSUInteger offset = random() % ([encodedAndSpaced length] + 1); + [encodedAndSpaced insertString:spaces[random() % numSpaces] + atIndex:offset]; + } + + // we'll need it as data for apis + NSData *encodedAsData = + [encodedAndSpaced dataUsingEncoding:NSASCIIStringEncoding]; + STAssertNotNil(encodedAsData, @"failed to extract from string"); + STAssertEquals([encodedAsData length], [encodedAndSpaced length], + @"lengths for encoded string and data didn't match?"); + + // all the decode modes + NSData *dataPrime = [GTMBase64 webSafeDecodeData:encodedAsData]; + STAssertEqualObjects(data, dataPrime, + @"failed Data decode w/ spaces"); + dataPrime = [GTMBase64 webSafeDecodeBytes:[encodedAsData bytes] + length:[encodedAsData length]]; + STAssertEqualObjects(data, dataPrime, + @"failed Bytes decode w/ spaces"); + dataPrime = [GTMBase64 webSafeDecodeString:encodedAndSpaced]; + STAssertEqualObjects(data, dataPrime, + @"failed String decode w/ spaces"); + } + } // paddedLoop +} + +- (void)testErrors { + const int something = 0; + NSString *nonAscString = [NSString stringWithUTF8String:"This test ©™®๒०᠐٧"]; + + STAssertNil([GTMBase64 encodeData:nil], @"it worked?"); + STAssertNil([GTMBase64 decodeData:nil], @"it worked?"); + STAssertNil([GTMBase64 encodeBytes:NULL length:10], @"it worked?"); + STAssertNil([GTMBase64 encodeBytes:&something length:0], @"it worked?"); + STAssertNil([GTMBase64 decodeBytes:NULL length:10], @"it worked?"); + STAssertNil([GTMBase64 decodeBytes:&something length:0], @"it worked?"); + STAssertNil([GTMBase64 stringByEncodingData:nil], @"it worked?"); + STAssertNil([GTMBase64 stringByEncodingBytes:NULL length:10], @"it worked?"); + STAssertNil([GTMBase64 stringByEncodingBytes:&something length:0], @"it worked?"); + STAssertNil([GTMBase64 decodeString:nil], @"it worked?"); + // test some pads at the end that aren't right + STAssertNil([GTMBase64 decodeString:@"=="], @"it worked?"); // just pads + STAssertNil([GTMBase64 decodeString:@"vw="], @"it worked?"); // missing pad (in state 2) + STAssertNil([GTMBase64 decodeString:@"vw"], @"it worked?"); // missing pad (in state 2) + STAssertNil([GTMBase64 decodeString:@"NNw"], @"it worked?"); // missing pad (in state 3) + STAssertNil([GTMBase64 decodeString:@"vw=v"], @"it worked?"); // missing pad, has something else + STAssertNil([GTMBase64 decodeString:@"v="], @"it worked?"); // missing a needed char, has pad instead + STAssertNil([GTMBase64 decodeString:@"v"], @"it worked?"); // missing a needed char + STAssertNil([GTMBase64 decodeString:@"vw== vw"], @"it worked?"); + STAssertNil([GTMBase64 decodeString:nonAscString], @"it worked?"); + STAssertNil([GTMBase64 decodeString:@"@@@not valid###"], @"it worked?"); + // carefully crafted bad input to make sure we don't overwalk + STAssertNil([GTMBase64 decodeString:@"WD=="], @"it worked?"); + + STAssertNil([GTMBase64 webSafeEncodeData:nil padded:YES], @"it worked?"); + STAssertNil([GTMBase64 webSafeDecodeData:nil], @"it worked?"); + STAssertNil([GTMBase64 webSafeEncodeBytes:NULL length:10 padded:YES], + @"it worked?"); + STAssertNil([GTMBase64 webSafeEncodeBytes:&something length:0 padded:YES], + @"it worked?"); + STAssertNil([GTMBase64 webSafeDecodeBytes:NULL length:10], @"it worked?"); + STAssertNil([GTMBase64 webSafeDecodeBytes:&something length:0], @"it worked?"); + STAssertNil([GTMBase64 stringByWebSafeEncodingData:nil padded:YES], + @"it worked?"); + STAssertNil([GTMBase64 stringByWebSafeEncodingBytes:NULL + length:10 + padded:YES], + @"it worked?"); + STAssertNil([GTMBase64 stringByWebSafeEncodingBytes:&something + length:0 + padded:YES], + @"it worked?"); + STAssertNil([GTMBase64 webSafeDecodeString:nil], @"it worked?"); + // test some pads at the end that aren't right + STAssertNil([GTMBase64 webSafeDecodeString:@"=="], @"it worked?"); // just pad chars + STAssertNil([GTMBase64 webSafeDecodeString:@"aw="], @"it worked?"); // missing pad + STAssertNil([GTMBase64 webSafeDecodeString:@"aw=a"], @"it worked?"); // missing pad, has something else + STAssertNil([GTMBase64 webSafeDecodeString:@"a"], @"it worked?"); // missing a needed char + STAssertNil([GTMBase64 webSafeDecodeString:@"a="], @"it worked?"); // missing a needed char, has pad instead + STAssertNil([GTMBase64 webSafeDecodeString:@"aw== a"], @"it worked?"); // missing pad + STAssertNil([GTMBase64 webSafeDecodeString:nonAscString], @"it worked?"); + STAssertNil([GTMBase64 webSafeDecodeString:@"@@@not valid###"], @"it worked?"); + // carefully crafted bad input to make sure we don't overwalk + STAssertNil([GTMBase64 webSafeDecodeString:@"WD=="], @"it worked?"); + + // make sure our local helper is working right + STAssertFalse(NoEqualChar([NSData dataWithBytes:"aa=zz" length:5]), @""); +} + +@end diff --git a/Foundation/GTMCalculatedRange.h b/Foundation/GTMCalculatedRange.h index 5f51b3e..5710da6 100644 --- a/Foundation/GTMCalculatedRange.h +++ b/Foundation/GTMCalculatedRange.h @@ -20,6 +20,7 @@ // #import <Foundation/Foundation.h> +#import "GTMDefines.h" /// Allows you to calculate a value based on defined stops in a range. // @@ -46,7 +47,7 @@ // item: the object to place at |position|. // position: the position in the range to put |item|. // -- (void)insertStop:(id)item atPosition:(float)position; +- (void)insertStop:(id)item atPosition:(CGFloat)position; // Removes a stop from the range at |position|. // @@ -56,7 +57,7 @@ // Returns: // YES if there is a stop at |position| that has been removed // NO if there is not a stop at the |position| -- (BOOL)removeStopAtPosition:(float)position; +- (BOOL)removeStopAtPosition:(CGFloat)position; // Removes stop |index| from the range. Stops are ordered // based on position where index of x < index of y if position @@ -66,13 +67,13 @@ // item: the object to place at |position|. // position: the position in the range to put |item|. // -- (void)removeStopAtIndex:(unsigned int)index; +- (void)removeStopAtIndex:(NSUInteger)index; // Returns the number of stops in the range. // // Returns: // number of stops -- (unsigned int)stopCount; +- (NSUInteger)stopCount; // Returns the value at position |position|. // This function should be overridden by subclasses to calculate a @@ -85,7 +86,7 @@ // // Returns: // value for position -- (id)valueAtPosition:(float)position; +- (id)valueAtPosition:(CGFloat)position; // Returns the |index|'th stop and position in the set. // Throws an exception if out of range. @@ -97,5 +98,5 @@ // // Returns: // the stop at the index. -- (id)stopAtIndex:(unsigned int)index position:(float*)outPosition; +- (id)stopAtIndex:(NSUInteger)index position:(CGFloat*)outPosition; @end diff --git a/Foundation/GTMCalculatedRange.m b/Foundation/GTMCalculatedRange.m index 8562d8a..435ad65 100644 --- a/Foundation/GTMCalculatedRange.m +++ b/Foundation/GTMCalculatedRange.m @@ -22,21 +22,21 @@ // position. @interface GTMCalculatedRangeStopPrivate : NSObject { id item_; // the item (STRONG) - float position_; // + CGFloat position_; // } -+ (id)stopWithObject:(id)item position:(float)inPosition; -- (id)initWithObject:(id)item position:(float)inPosition; ++ (id)stopWithObject:(id)item position:(CGFloat)inPosition; +- (id)initWithObject:(id)item position:(CGFloat)inPosition; - (id)item; -- (float)position; +- (CGFloat)position; @end @implementation GTMCalculatedRangeStopPrivate -+ (id)stopWithObject:(id)item position:(float)inPosition { ++ (id)stopWithObject:(id)item position:(CGFloat)inPosition { return [[[[self class] alloc] initWithObject:item position:inPosition] autorelease]; } -- (id)initWithObject:(id)item position:(float)inPosition { +- (id)initWithObject:(id)item position:(CGFloat)inPosition { self = [super init]; if (self != nil) { item_ = [item retain]; @@ -54,7 +54,7 @@ return item_; } -- (float)position { +- (CGFloat)position { return position_; } @@ -76,8 +76,8 @@ [super dealloc]; } -- (void)insertStop:(id)item atPosition:(float)position { - unsigned int index = 0; +- (void)insertStop:(id)item atPosition:(CGFloat)position { + NSUInteger index = 0; NSEnumerator *theEnumerator = [storage_ objectEnumerator]; GTMCalculatedRangeStopPrivate *theStop; while (nil != (theStop = [theEnumerator nextObject])) { @@ -85,15 +85,17 @@ index += 1; } else if ([theStop position] == position) { + // remove and stop the enum since we just modified the object [storage_ removeObjectAtIndex:index]; + break; } } [storage_ insertObject:[GTMCalculatedRangeStopPrivate stopWithObject:item position:position] - atIndex:index]; + atIndex:index]; } -- (BOOL)removeStopAtPosition:(float)position { - unsigned int index = 0; +- (BOOL)removeStopAtPosition:(CGFloat)position { + NSUInteger index = 0; BOOL foundStop = NO; NSEnumerator *theEnumerator = [storage_ objectEnumerator]; GTMCalculatedRangeStopPrivate *theStop; @@ -111,15 +113,15 @@ return foundStop; } -- (void)removeStopAtIndex:(unsigned int)index { +- (void)removeStopAtIndex:(NSUInteger)index { [storage_ removeObjectAtIndex:index]; } -- (unsigned int)stopCount { +- (NSUInteger)stopCount { return [storage_ count]; } -- (id)stopAtIndex:(unsigned int)index position:(float*)outPosition { +- (id)stopAtIndex:(NSUInteger)index position:(CGFloat*)outPosition { GTMCalculatedRangeStopPrivate *theStop = [storage_ objectAtIndex:index]; if (nil != outPosition) { *outPosition = [theStop position]; @@ -127,7 +129,7 @@ return [theStop item]; } -- (id)valueAtPosition:(float)position { +- (id)valueAtPosition:(CGFloat)position { id theValue = nil; GTMCalculatedRangeStopPrivate *theStop; NSEnumerator *theEnumerator = [storage_ objectEnumerator]; diff --git a/Foundation/GTMCalculatedRangeTest.m b/Foundation/GTMCalculatedRangeTest.m index 0c374c1..1d716c8 100644 --- a/Foundation/GTMCalculatedRangeTest.m +++ b/Foundation/GTMCalculatedRangeTest.m @@ -26,14 +26,14 @@ @implementation GTMCalculatedRangeTest NSString *kStrings[] = { @"Fee", @"Fi", @"Fo", @"Fum" }; -const unsigned int kStringCount = sizeof(kStrings) / sizeof(NSString*); -const float kOddPosition = 0.14159265f; -const float kExistingPosition = 0.5f; -const unsigned int kExisitingIndex = 2; +const NSUInteger kStringCount = sizeof(kStrings) / sizeof(NSString*); +const CGFloat kOddPosition = 0.14159265f; +const CGFloat kExistingPosition = 0.5f; +const NSUInteger kExisitingIndex = 2; - (void)setUp { range_ = [[GTMCalculatedRange alloc] init]; - for(unsigned int i = kStringCount; i > 0; --i) { + for(NSUInteger i = kStringCount; i > 0; --i) { [range_ insertStop:kStrings[kStringCount - i] atPosition: 1.0f / i]; } } @@ -43,12 +43,22 @@ const unsigned int kExisitingIndex = 2; } - (void)testInsertStop { + // new position NSString *theString = @"I smell the blood of an Englishman!"; - [range_ insertStop:theString atPosition: kOddPosition]; + [range_ insertStop:theString atPosition:kOddPosition]; STAssertEquals([range_ stopCount], kStringCount + 1, @"Stop count was bad"); NSString *getString = [range_ valueAtPosition:kOddPosition]; STAssertNotNil(getString, @"String was bad"); STAssertEquals(theString, getString, @"Stops weren't equal"); + // existing position + NSString *theStringTake2 = @"I smell the blood of an Englishman! Take 2"; + [range_ insertStop:theStringTake2 atPosition:kOddPosition]; + STAssertEquals([range_ stopCount], kStringCount + 1, @"Stop count was bad"); + getString = [range_ valueAtPosition:kOddPosition]; + STAssertNotNil(getString, @"String was bad"); + STAssertEquals(theStringTake2, getString, @"Stops weren't equal"); + STAssertNotEquals(theString, getString, @"Should be the new value"); + STAssertNotEqualObjects(theString, getString, @"Should be the new value"); } - (void)testRemoveStopAtPosition { @@ -74,7 +84,7 @@ const unsigned int kExisitingIndex = 2; } - (void)testStopAtIndex { - float thePosition; + CGFloat thePosition; STAssertEqualObjects([range_ stopAtIndex:kStringCount - 1 position:nil], kStrings[kStringCount - 1], nil); STAssertEqualObjects([range_ stopAtIndex:kExisitingIndex position:&thePosition], kStrings[kExisitingIndex], nil); @@ -83,5 +93,9 @@ const unsigned int kExisitingIndex = 2; STAssertThrows([range_ stopAtIndex:kStringCount position:nil], nil); } +- (void)testDescription { + // we expect a description of atleast a few chars + STAssertGreaterThan([[range_ description] length], (NSUInteger)10, nil); +} @end diff --git a/Foundation/GTMGeometryUtils.h b/Foundation/GTMGeometryUtils.h index 32c8745..a58ac13 100644 --- a/Foundation/GTMGeometryUtils.h +++ b/Foundation/GTMGeometryUtils.h @@ -19,15 +19,17 @@ // the License. // -#include <Foundation/Foundation.h> +#import <Foundation/Foundation.h> +#import "GTMDefines.h" -typedef enum { +enum { GTMScaleProportionally = 0, // Fit proportionally GTMScaleToFit, // Forced fit (distort if necessary) GTMScaleNone // Don't scale (clip) -} GTMScaling; +}; +typedef NSUInteger GTMScaling; -typedef enum { +enum { GTMRectAlignCenter = 0, GTMRectAlignTop, GTMRectAlignTopLeft, @@ -37,7 +39,8 @@ typedef enum { GTMRectAlignBottomLeft, GTMRectAlignBottomRight, GTMRectAlignRight -} GTMRectAlignment; +}; +typedef NSUInteger GTMRectAlignment; #pragma mark Miscellaneous @@ -49,10 +52,14 @@ typedef enum { // // Returns: // Distance -CG_INLINE float GTMDistanceBetweenPoints(NSPoint pt1, NSPoint pt2) { - float dX = pt1.x - pt2.x; - float dY = pt1.y - pt2.y; +CG_INLINE CGFloat GTMDistanceBetweenPoints(NSPoint pt1, NSPoint pt2) { + CGFloat dX = pt1.x - pt2.x; + CGFloat dY = pt1.y - pt2.y; +#if CGFLOAT_IS_DOUBLE + return sqrt(dX * dX + dY * dY); +#else return sqrtf(dX * dX + dY * dY); +#endif } #pragma mark - @@ -314,7 +321,7 @@ CG_INLINE CGRect GTMCGRectOfSize(CGSize size) { // // Returns: // Converted Rect -CG_INLINE NSRect GTMNSRectScale(NSRect inRect, float xScale, float yScale) { +CG_INLINE NSRect GTMNSRectScale(NSRect inRect, CGFloat xScale, CGFloat yScale) { return NSMakeRect(inRect.origin.x, inRect.origin.y, inRect.size.width * xScale, inRect.size.height * yScale); } @@ -328,7 +335,7 @@ CG_INLINE NSRect GTMNSRectScale(NSRect inRect, float xScale, float yScale) { // // Returns: // Converted Rect -CG_INLINE CGRect GTMCGRectScale(CGRect inRect, float xScale, float yScale) { +CG_INLINE CGRect GTMCGRectScale(CGRect inRect, CGFloat xScale, CGFloat yScale) { return CGRectMake(inRect.origin.x, inRect.origin.y, inRect.size.width * xScale, inRect.size.height * yScale); } diff --git a/Foundation/GTMGeometryUtils.m b/Foundation/GTMGeometryUtils.m index f5b38dc..0e893ff 100644 --- a/Foundation/GTMGeometryUtils.m +++ b/Foundation/GTMGeometryUtils.m @@ -79,13 +79,13 @@ NSRect GTMAlignRectangles(NSRect alignee, NSRect aligner, GTMRectAlignment align NSRect GTMScaleRectangleToSize(NSRect scalee, NSSize size, GTMScaling scaling) { switch (scaling) { case GTMScaleProportionally: { - float height = NSHeight(scalee); - float width = NSWidth(scalee); + CGFloat height = NSHeight(scalee); + CGFloat width = NSWidth(scalee); if (isnormal(height) && isnormal(width) && (height > size.height || width > size.width)) { - float horiz = size.width / width; - float vert = size.height / height; - float newScale = horiz < vert ? horiz : vert; + CGFloat horiz = size.width / width; + CGFloat vert = size.height / height; + CGFloat newScale = horiz < vert ? horiz : vert; scalee = GTMNSRectScale(scalee, newScale, newScale); } break; diff --git a/Foundation/GTMGeometryUtilsTest.m b/Foundation/GTMGeometryUtilsTest.m index 2fb0c68..606ea6b 100644 --- a/Foundation/GTMGeometryUtilsTest.m +++ b/Foundation/GTMGeometryUtilsTest.m @@ -64,11 +64,11 @@ - (void)testGTMDistanceBetweenPoints { NSPoint pt1 = NSMakePoint(0, 0); NSPoint pt2 = NSMakePoint(3, 4); - STAssertEquals(GTMDistanceBetweenPoints(pt1, pt2), 5.0f, nil); - STAssertEquals(GTMDistanceBetweenPoints(pt2, pt1), 5.0f, nil); + STAssertEquals(GTMDistanceBetweenPoints(pt1, pt2), (CGFloat)5.0, nil); + STAssertEquals(GTMDistanceBetweenPoints(pt2, pt1), (CGFloat)5.0, nil); pt1 = NSMakePoint(1, 1); pt2 = NSMakePoint(1, 1); - STAssertEquals(GTMDistanceBetweenPoints(pt1, pt2), 0.0f, nil); + STAssertEquals(GTMDistanceBetweenPoints(pt1, pt2), (CGFloat)0.0, nil); } - (void)testGTMAlignRectangles { @@ -109,37 +109,37 @@ CGPoint cgPoint = GTMCGMidLeft(cgRect); STAssertEquals(point.x, cgPoint.x, nil); STAssertEquals(point.y, cgPoint.y, nil); - STAssertEqualsWithAccuracy(point.y, 1.0f, 0.01f, nil); - STAssertEqualsWithAccuracy(point.x, 0.0f, 0.01f, nil); + STAssertEqualsWithAccuracy(point.y, (CGFloat)1.0, (CGFloat)0.01, nil); + STAssertEqualsWithAccuracy(point.x, (CGFloat)0.0, (CGFloat)0.01, nil); point = GTMNSMidRight(rect); cgPoint = GTMCGMidRight(cgRect); STAssertEquals(point.x, cgPoint.x, nil); STAssertEquals(point.y, cgPoint.y, nil); - STAssertEqualsWithAccuracy(point.y, 1.0f, 0.01f, nil); - STAssertEqualsWithAccuracy(point.x, 2.0f, 0.01f, nil); + STAssertEqualsWithAccuracy(point.y, (CGFloat)1.0, (CGFloat)0.01, nil); + STAssertEqualsWithAccuracy(point.x, (CGFloat)2.0, (CGFloat)0.01, nil); point = GTMNSMidTop(rect); cgPoint = GTMCGMidTop(cgRect); STAssertEquals(point.x, cgPoint.x, nil); STAssertEquals(point.y, cgPoint.y, nil); - STAssertEqualsWithAccuracy(point.y, 2.0f, 0.01f, nil); - STAssertEqualsWithAccuracy(point.x, 1.0f, 0.01f, nil); + STAssertEqualsWithAccuracy(point.y, (CGFloat)2.0, (CGFloat)0.01, nil); + STAssertEqualsWithAccuracy(point.x, (CGFloat)1.0, (CGFloat)0.01, nil); point = GTMNSMidBottom(rect); cgPoint = GTMCGMidBottom(cgRect); STAssertEquals(point.x, cgPoint.x, nil); STAssertEquals(point.y, cgPoint.y, nil); - STAssertEqualsWithAccuracy(point.y, 0.0f, 0.01f, nil); - STAssertEqualsWithAccuracy(point.x, 1.0f, 0.01f, nil); + STAssertEqualsWithAccuracy(point.y, (CGFloat)0.0, (CGFloat)0.01, nil); + STAssertEqualsWithAccuracy(point.x, (CGFloat)1.0, (CGFloat)0.01, nil); } - (void)testGTMRectScaling { NSRect rect = NSMakeRect(1.0f, 2.0f, 5.0f, 10.0f); - NSRect rect2 = NSMakeRect(1.0f, 2.0f, 1.0f, 12.0f); - STAssertEquals(GTMNSRectScale(rect, 0.2f, 1.2f), + NSRect rect2 = NSMakeRect((CGFloat)1.0, (CGFloat)2.0, (CGFloat)1.0, (CGFloat)12.0); + STAssertEquals(GTMNSRectScale(rect, (CGFloat)0.2, (CGFloat)1.2), rect2, nil); - STAssertEquals(GTMCGRectScale(GTMNSRectToCGRect(rect), 0.2f, 1.2f), + STAssertEquals(GTMCGRectScale(GTMNSRectToCGRect(rect), (CGFloat)0.2, (CGFloat)1.2), GTMNSRectToCGRect(rect2), nil); } diff --git a/Foundation/GTMHTTPFetcher.h b/Foundation/GTMHTTPFetcher.h new file mode 100644 index 0000000..181283a --- /dev/null +++ b/Foundation/GTMHTTPFetcher.h @@ -0,0 +1,500 @@ +// +// GTMHTTPFetcher.h +// +// Copyright 2007-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +// This is essentially a wrapper around NSURLConnection for POSTs and GETs. +// If setPostData: is called, then POST is assumed. +// +// When would you use this instead of NSURLConnection? +// +// - When you just want the result from a GET or POST +// - When you want the "standard" behavior for connections (redirection handling +// an so on) +// - When you want to avoid cookie collisions with Safari and other applications +// - When you want to provide if-modified-since headers +// - When you need to set a credential for the http +// - When you want to avoid changing WebKit's cookies +// +// This is assumed to be a one-shot fetch request; don't reuse the object +// for a second fetch. +// +// The fetcher may be created auto-released, in which case it will release +// itself after the fetch completion callback. The fetcher +// is implicitly retained as long as a connection is pending. +// +// But if you may need to cancel the fetcher, allocate it with initWithRequest: +// and have the delegate release the fetcher in the callbacks. +// +// Sample usage: +// +// NSURLRequest *request = [NSURLRequest requestWithURL:myURL]; +// GTMHTTPFetcher* myFetcher = [GTMHTTPFetcher httpFetcherWithRequest:request]; +// +// [myFetcher setPostData:[postString dataUsingEncoding:NSUTF8StringEncoding]]; // for POSTs +// +// [myFetcher setCredential:[NSURLCredential authCredentialWithUsername:@"foo" +// password:@"bar"]]; // optional http credential +// +// [myFetcher setFetchHistory:myMutableDictionary]; // optional, for persisting modified-dates +// +// [myFetcher beginFetchWithDelegate:self +// didFinishSelector:@selector(myFetcher:finishedWithData:) +// didFailSelector:@selector(myFetcher:failedWithError:)]; +// +// Upon fetch completion, the callback selectors are invoked; they should have +// these signatures (you can use any callback method names you want so long as +// the signatures match these): +// +// - (void)myFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)retrievedData; +// - (void)myFetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error; +// +// NOTE: Fetches may retrieve data from the server even though the server +// returned an error. The failWithError selector is called when the server +// status is >= 300 (along with any server-supplied data, usually +// some html explaining the error). +// Status codes are at <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html> +// +// +// Proxies: +// +// Proxy handling is invisible so long as the system has a valid credential in +// the keychain, which is normally true (else most NSURL-based apps would have +// difficulty.) But when there is a proxy authetication error, the the fetcher +// will call the failedWithError: method with the NSURLChallenge in the error's +// userInfo. The error method can get the challenge info like this: +// +// NSURLAuthenticationChallenge *challenge +// = [[error userInfo] objectForKey:kGTMHTTPFetcherErrorChallengeKey]; +// BOOL isProxyChallenge = [[challenge protectionSpace] isProxy]; +// +// If a proxy error occurs, you can ask the user for the proxy username/password +// and call fetcher's setProxyCredential: to provide those for the +// next attempt to fetch. +// +// +// Cookies: +// +// There are three supported mechanisms for remembering cookies between fetches. +// +// By default, GTMHTTPFetcher uses a mutable array held statically to track +// cookies for all instantiated fetchers. This avoids server cookies being set +// by servers for the application from interfering with Safari cookie settings, +// and vice versa. The fetcher cookies are lost when the application quits. +// +// To rely instead on WebKit's global NSHTTPCookieStorage, call +// setCookieStorageMethod: with kGTMHTTPFetcherCookieStorageMethodSystemDefault. +// +// If you provide a fetch history (such as for periodic checks, described +// below) then the cookie storage mechanism is set to use the fetch +// history rather than the static storage. +// +// +// Fetching for periodic checks: +// +// The fetcher object can track "Last-modified" dates on returned data and +// provide an "If-modified-since" header. This allows the server to save +// bandwidth by providing a "Nothing changed" status message instead of response +// data. +// +// To get this behavior, provide a persistent mutable dictionary to setFetchHistory:, +// and look for the failedWithError: callback with code 304 +// (kGTMHTTPFetcherStatusNotModified) like this: +// +// - (void)myFetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error { +// if ([[error domain] isEqual:kGTMHTTPFetcherStatusDomain] && +// ([error code] == kGTMHTTPFetcherStatusNotModified)) { +// // [[error userInfo] objectForKey:kGTMHTTPFetcherStatusDataKey] is +// // empty; use the data from the previous finishedWithData: for this URL +// } else { +// // handle other server status code +// } +// } +// +// The fetchHistory mutable dictionary should be maintained by the client between +// fetches and given to each fetcher intended to have the If-modified-since header +// or the same cookie storage. +// +// +// Monitoring received data +// +// The optional received data selector should have the signature +// +// - (void)myFetcher:(GTMHTTPFetcher *)fetcher receivedData:(NSData *)dataReceivedSoFar; +// +// The bytes received so far are [dataReceivedSoFar length]. This number may go down +// if a redirect causes the download to begin again from a new server. +// If supplied by the server, the anticipated total download size is available as +// [[myFetcher response] expectedContentLength] (may be -1 for unknown +// download sizes.) +// +// +// Automatic retrying of fetches +// +// The fetcher can optionally create a timer and reattempt certain kinds of +// fetch failures (status codes 408, request timeout; 503, service unavailable; +// 504, gateway timeout; networking errors NSURLErrorTimedOut and +// NSURLErrorNetworkConnectionLost.) The user may set a retry selector to +// customize the type of errors which will be retried. +// +// Retries are done in an exponential-backoff fashion (that is, after 1 second, +// 2, 4, 8, and so on.) +// +// Enabling automatic retries looks like this: +// [myFetcher setIsRetryEnabled:YES]; +// +// With retries enabled, the success or failure callbacks are called only +// when no more retries will be attempted. Calling the fetcher's stopFetching +// method will terminate the retry timer, without the finished or failure +// selectors being invoked. +// +// Optionally, the client may set the maximum retry interval: +// [myFetcher setMaxRetryInterval:60.]; // in seconds; default is 600 seconds +// +// Also optionally, the client may provide a callback selector to determine +// if a status code or other error should be retried. +// [myFetcher setRetrySelector:@selector(myFetcher:willRetry:forError:)]; +// +// If set, the retry selector should have the signature: +// -(BOOL)fetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error +// and return YES to set the retry timer or NO to fail without additional +// fetch attempts. +// +// The retry method may return the |suggestedWillRetry| argument to get the +// default retry behavior. Server status codes are present in the error +// argument, and have the domain kGTMHTTPFetcherStatusDomain. The user's method +// may look something like this: +// +// -(BOOL)myFetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error { +// +// // perhaps examine [error domain] and [error code], or [fetcher retryCount] +// // +// // return YES to start the retry timer, NO to proceed to the failure +// // callback, or |suggestedWillRetry| to get default behavior for the +// // current error domain and code values. +// return suggestedWillRetry; +// } + + + +#pragma once + +#import <Foundation/Foundation.h> + +#import "GTMDefines.h" + +#undef _EXTERN +#undef _INITIALIZE_AS +#ifdef GTMHTTPFETCHER_DEFINE_GLOBALS +#define _EXTERN +#define _INITIALIZE_AS(x) =x +#else +#define _EXTERN extern +#define _INITIALIZE_AS(x) +#endif + +// notifications & errors +_EXTERN NSString* const kGTMHTTPFetcherErrorDomain _INITIALIZE_AS(@"com.google.GTMHTTPFetcher"); +_EXTERN NSString* const kGTMHTTPFetcherStatusDomain _INITIALIZE_AS(@"com.google.HTTPStatus"); +_EXTERN NSString* const kGTMHTTPFetcherErrorChallengeKey _INITIALIZE_AS(@"challenge"); +_EXTERN NSString* const kGTMHTTPFetcherStatusDataKey _INITIALIZE_AS(@"data"); // any data returns w/ a kGTMHTTPFetcherStatusDomain error + + +// fetch history mutable dictionary keys +_EXTERN NSString* const kGTMHTTPFetcherHistoryLastModifiedKey _INITIALIZE_AS(@"FetchHistoryLastModified"); +_EXTERN NSString* const kGTMHTTPFetcherHistoryDatedDataKey _INITIALIZE_AS(@"FetchHistoryDatedDataCache"); +_EXTERN NSString* const kGTMHTTPFetcherHistoryCookiesKey _INITIALIZE_AS(@"FetchHistoryCookies"); + +enum { + kGTMHTTPFetcherErrorDownloadFailed = -1, + kGTMHTTPFetcherErrorAuthenticationChallengeFailed = -2, + + kGTMHTTPFetcherStatusNotModified = 304 +}; + +enum { + kGTMHTTPFetcherCookieStorageMethodStatic = 0, + kGTMHTTPFetcherCookieStorageMethodFetchHistory = 1, + kGTMHTTPFetcherCookieStorageMethodSystemDefault = 2 +}; +typedef NSUInteger GTMHTTPFetcherCookieStorageMethod; + +/// async retrieval of an http get or post +@interface GTMHTTPFetcher : NSObject { + NSMutableURLRequest *request_; + NSURLConnection *connection_; // while connection_ is non-nil, delegate_ is retained + NSMutableData *downloadedData_; + NSURLCredential *credential_; // username & password + NSURLCredential *proxyCredential_; // credential supplied to proxy servers + NSData *postData_; + NSInputStream *postStream_; + NSMutableData *loggedStreamData_; + NSURLResponse *response_; // set in connection:didReceiveResponse: + id delegate_; // WEAK (though retained during an open connection) + SEL finishedSEL_; // should by implemented by delegate + SEL failedSEL_; // should be implemented by delegate + SEL receivedDataSEL_; // optional, set with setReceivedDataSelector + id userData_; // retained, if set by caller + NSArray *runLoopModes_; // optional, for 10.5 and later + NSMutableDictionary *fetchHistory_; // if supplied by the caller, used for Last-Modified-Since checks and cookies + BOOL shouldCacheDatedData_; // if true, remembers and returns data marked with a last-modified date + GTMHTTPFetcherCookieStorageMethod cookieStorageMethod_; // constant from above + + BOOL isRetryEnabled_; // user wants auto-retry + SEL retrySEL_; // optional; set with setRetrySelector + NSTimer *retryTimer_; + unsigned int retryCount_; + NSTimeInterval maxRetryInterval_; // default 600 seconds + NSTimeInterval minRetryInterval_; // random between 1 and 2 seconds + NSTimeInterval retryFactor_; // default interval multiplier is 2 + NSTimeInterval lastRetryInterval_; +} + +/// create a fetcher +// +// httpFetcherWithRequest will return an autoreleased fetcher, but if +// the connection is successfully created, the connection should retain the +// fetcher for the life of the connection as well. So the caller doesn't have +// to retain the fetcher explicitly unless they want to be able to cancel it. ++ (GTMHTTPFetcher *)httpFetcherWithRequest:(NSURLRequest *)request; + +// designated initializer +- (id)initWithRequest:(NSURLRequest *)request; + +- (NSMutableURLRequest *)request; +- (void)setRequest:(NSURLRequest *)theRequest; + +// setting the credential is optional; it is used if the connection receives +// an authentication challenge +- (NSURLCredential *)credential; +- (void)setCredential:(NSURLCredential *)theCredential; + +// setting the proxy credential is optional; it is used if the connection +// receives an authentication challenge from a proxy +- (NSURLCredential *)proxyCredential; +- (void)setProxyCredential:(NSURLCredential *)theCredential; + + +// if post data or stream is not set, then a GET retrieval method is assumed +- (NSData *)postData; +- (void)setPostData:(NSData *)theData; + +// beware: In 10.4, NSInputStream fails to copy or retain +// the data it was initialized with, contrary to docs. +// NOTE: if logging is enabled and GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING is +// 1, postStream will return a GTMProgressMonitorInputStream that wraps your +// stream (so the upload can be logged). +- (NSInputStream *)postStream; +- (void)setPostStream:(NSInputStream *)theStream; + +- (GTMHTTPFetcherCookieStorageMethod)cookieStorageMethod; +- (void)setCookieStorageMethod:(GTMHTTPFetcherCookieStorageMethod)method; + +// returns cookies from the currently appropriate cookie storage +- (NSArray *)cookiesForURL:(NSURL *)theURL; + +// the delegate is not retained except during the connection +- (id)delegate; +- (void)setDelegate:(id)theDelegate; + +// the delegate's optional receivedData selector has a signature like: +// - (void)myFetcher:(GTMHTTPFetcher *)fetcher receivedData:(NSData *)dataReceivedSoFar; +- (SEL)receivedDataSelector; +- (void)setReceivedDataSelector:(SEL)theSelector; + + +// retrying; see comments at the top of the file. Calling +// setIsRetryEnabled(YES) resets the min and max retry intervals. +- (BOOL)isRetryEnabled; +- (void)setIsRetryEnabled:(BOOL)flag; + +// retry selector is optional for retries. +// +// If present, it should have the signature: +// -(BOOL)fetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error +// and return YES to cause a retry. See comments at the top of this file. +- (SEL)retrySelector; +- (void)setRetrySelector:(SEL)theSel; + +// retry intervals must be strictly less than maxRetryInterval, else +// they will be limited to maxRetryInterval and no further retries will +// be attempted. Setting maxRetryInterval to 0.0 will reset it to the +// default value, 600 seconds. +- (NSTimeInterval)maxRetryInterval; +- (void)setMaxRetryInterval:(NSTimeInterval)secs; + +// Starting retry interval. Setting minRetryInterval to 0.0 will reset it +// to a random value between 1.0 and 2.0 seconds. Clients should normally not +// call this except for unit testing. +- (NSTimeInterval)minRetryInterval; +- (void)setMinRetryInterval:(NSTimeInterval)secs; + +// Multiplier used to increase the interval between retries, typically 2.0. +// Clients should not need to call this. +- (double)retryFactor; +- (void)setRetryFactor:(double)multiplier; + +// number of retries attempted +- (unsigned int)retryCount; + +// interval delay to precede next retry +- (NSTimeInterval)nextRetryInterval; + +/// Begin fetching the request. +// +/// |delegate| can optionally implement the two selectors |finishedSEL| and +/// |networkFailedSEL| or pass nil for them. +/// Returns YES if the fetch is initiated. Delegate is retained between +/// the beginFetch call until after the finish/fail callbacks. +// +// finishedSEL has a signature like: +// - (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data +// failedSEL has a signature like: +// - (void)fetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error +// + +- (BOOL)beginFetchWithDelegate:(id)delegate + didFinishSelector:(SEL)finishedSEL + didFailSelector:(SEL)networkFailedSEL; + +// Returns YES if this is in the process of fetching a URL +- (BOOL)isFetching; + +/// Cancel the fetch of the request that's currently in progress +- (void)stopFetching; + +/// return the status code from the server response +- (NSInteger)statusCode; + +/// the response, once it's been received +- (NSURLResponse *)response; +- (void)setResponse:(NSURLResponse *)response; + +// Fetch History is a useful, but a little complex at times... +// +// The caller should provide a mutable dictionary that can be used for storing +// Last-Modified-Since checks and cookie storage (setFetchHistory implicity +// calls setCookieStorageMethod w/ +// kGTMHTTPFetcherCookieStorageMethodFetchHistory if you passed a dictionary, +// kGTMHTTPFetcherCookieStorageMethodStatic if you passed nil. +// +// The caller can hold onto the dictionary to reuse the modification dates and +// cookies across multiple fetcher instances. +// +// With a fetch history dictionary setup, the http fetcher cache has the +// modification dates returned by the servers cached, and future fetches will +// return 304 to indicate the data hasn't changed since then (the data in the +// NSError object will be of length zero to show nothing was fetched). This +// reduces load on the server when the response data has not changed. See +// shouldCacheDatedData below for additional 304 support. +// +// Side effect: setFetchHistory: implicitly calls setCookieStorageMethod: +- (NSMutableDictionary *)fetchHistory; +- (void)setFetchHistory:(NSMutableDictionary *)fetchHistory; + +// For fetched data with a last-modified date, the fetcher can optionally cache +// the response data in the fetch history and return cached data instead of a +// 304 error. Set this to NO if you want to manually handle last-modified and +// status 304 (Not changed) rather than be delivered cached data from previous +// fetches. Default is NO. When a cache result is returned, the didFinish +// selector is called with the data, and [fetcher status] returns 200. +// +// If the caller has already provided a fetchHistory dictionary, they can also +// enable fetcher handling of 304 (not changed) status responses. By setting +// shouldCacheDatedData to YES, the fetcher will save any response that has a +// last modifed reply header into the fetchHistory. Then any future fetches +// using that same fetchHistory will automatically load the cached response and +// return it to the caller (with a status of 200) in place of the 304 server +// reply. +- (BOOL)shouldCacheDatedData; +- (void)setShouldCacheDatedData:(BOOL)flag; + +// Delete the last-modified dates and cached data from the fetch history. +- (void)clearDatedDataHistory; + +/// userData is retained for the convenience of the caller +- (id)userData; +- (void)setUserData:(id)theObj; + +// using the fetcher while a modal dialog is displayed requires setting the +// run-loop modes to include NSModalPanelRunLoopMode +// +// setting run loop modes does nothing if they are not supported, +// such as on 10.4 +- (NSArray *)runLoopModes; +- (void)setRunLoopModes:(NSArray *)modes; + ++ (BOOL)doesSupportRunLoopModes; ++ (NSArray *)defaultRunLoopModes; ++ (void)setDefaultRunLoopModes:(NSArray *)modes; + +// users who wish to replace GTMHTTPFetcher's use of NSURLConnection +// can do so globally here. The replacement should be a subclass of +// NSURLConnection. ++ (Class)connectionClass; ++ (void)setConnectionClass:(Class)theClass; + +@end + +// GTM HTTP Logging +// +// All traffic using GTMHTTPFetcher can be easily logged. Call +// +// [GTMHTTPFetcher setIsLoggingEnabled:YES]; +// +// to begin generating log files. +// +// Log files are put into a folder on the desktop called "GTMHTTPDebugLogs" +// unless another directory is specified with +setLoggingDirectory. +// +// Each run of an application gets a separate set of log files. An html +// file is generated to simplify browsing the run's http transactions. +// The html file includes javascript links for inline viewing of uploaded +// and downloaded data. +// +// A symlink is created in the logs folder to simplify finding the html file +// for the latest run of the application; the symlink is called +// +// AppName_http_log_newest.html +// +// For better viewing of XML logs, use Camino or Firefox rather than Safari. +// +// Projects may define GTM_HTTPFETCHER_ENABLE_LOGGING to 0 to remove all of the +// logging code (it defaults to 1). By default, any data uploaded via PUT/POST +// w/ and NSInputStream will not be logged. You can enable this logging by +// defining GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING to 1 (it defaults to 0). +// + +@interface GTMHTTPFetcher (GTMHTTPFetcherLogging) + +// Note: the default logs directory is ~/Desktop/GTMHTTPDebugLogs; it will be +// created as needed. If a custom directory is set, the directory should +// already exist. ++ (void)setLoggingDirectory:(NSString *)path; ++ (NSString *)loggingDirectory; + +// client apps can turn logging on and off ++ (void)setIsLoggingEnabled:(BOOL)flag; ++ (BOOL)isLoggingEnabled; + +// client apps can optionally specify process name and date string used in +// log file names ++ (void)setLoggingProcessName:(NSString *)str; ++ (NSString *)loggingProcessName; + ++ (void)setLoggingDateStamp:(NSString *)str; ++ (NSString *)loggingDateStamp; +@end diff --git a/Foundation/GTMHTTPFetcher.m b/Foundation/GTMHTTPFetcher.m new file mode 100644 index 0000000..9853d0d --- /dev/null +++ b/Foundation/GTMHTTPFetcher.m @@ -0,0 +1,1889 @@ +// +// GTMHTTPFetcher.m +// +// Copyright 2007-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +#define GTMHTTPFETCHER_DEFINE_GLOBALS 1 + +#import "GTMHTTPFetcher.h" +#import "GTMDebugSelectorValidation.h" + +@interface GTMHTTPFetcher (GTMHTTPFetcherLoggingInternal) +- (void)logFetchWithError:(NSError *)error; +- (void)logCapturePostStream; +@end + +// Make sure that if logging is disabled, the InputStream logging is also +// diabled. +#if !GTM_HTTPFETCHER_ENABLE_LOGGING +# undef GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING +# define GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING 0 +#endif // GTM_HTTPFETCHER_ENABLE_LOGGING + +#if GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING +#import "GTMProgressMonitorInputStream.h" +@interface GTMInputStreamLogger : GTMProgressMonitorInputStream +// GTMInputStreamLogger wraps any NSInputStream used for uploading so we can +// capture a copy of the data for the log +@end +#endif // !GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING + +#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4 +@interface NSURLConnection (LeopardMethodsOnTigerBuilds) +- (id)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL)startImmediately; +- (void)start; +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +@end +#endif + +NSString* const kGTMLastModifiedHeader = @"Last-Modified"; +NSString* const kGTMIfModifiedSinceHeader = @"If-Modified-Since"; + + +NSMutableArray* gGTMFetcherStaticCookies = nil; +Class gGTMFetcherConnectionClass = nil; +NSArray *gGTMFetcherDefaultRunLoopModes = nil; + +const NSTimeInterval kDefaultMaxRetryInterval = 60. * 10.; // 10 minutes + +@interface GTMHTTPFetcher (PrivateMethods) +- (void)setCookies:(NSArray *)newCookies + inArray:(NSMutableArray *)cookieStorageArray; +- (NSArray *)cookiesForURL:(NSURL *)theURL inArray:(NSMutableArray *)cookieStorageArray; +- (void)handleCookiesForResponse:(NSURLResponse *)response; +- (BOOL)shouldRetryNowForStatus:(NSInteger)status error:(NSError *)error; +- (void)destroyRetryTimer; +- (void)beginRetryTimer; +- (void)primeTimerWithNewTimeInterval:(NSTimeInterval)secs; +- (void)retryFetch; +@end + +@implementation GTMHTTPFetcher + ++ (GTMHTTPFetcher *)httpFetcherWithRequest:(NSURLRequest *)request { + return [[[GTMHTTPFetcher alloc] initWithRequest:request] autorelease]; +} + ++ (void)initialize { + if (!gGTMFetcherStaticCookies) { + gGTMFetcherStaticCookies = [[NSMutableArray alloc] init]; + } +} + +- (id)init { + return [self initWithRequest:nil]; +} + +- (id)initWithRequest:(NSURLRequest *)request { + if ((self = [super init]) != nil) { + + request_ = [request mutableCopy]; + + [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic]; + } + return self; +} + +// TODO: do we need finalize to call stopFetching? + +- (void)dealloc { + [self stopFetching]; // releases connection_ + + [request_ release]; + [downloadedData_ release]; + [credential_ release]; + [proxyCredential_ release]; + [postData_ release]; + [postStream_ release]; + [loggedStreamData_ release]; + [response_ release]; + [userData_ release]; + [runLoopModes_ release]; + [fetchHistory_ release]; + [self destroyRetryTimer]; + + [super dealloc]; +} + +#pragma mark - + +// Begin fetching the URL. |delegate| is not retained +// The delegate must provide and implement the finished and failed selectors. +// +// finishedSEL has a signature like: +// - (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data +// failedSEL has a signature like: +// - (void)fetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error + +- (BOOL)beginFetchWithDelegate:(id)delegate + didFinishSelector:(SEL)finishedSEL + didFailSelector:(SEL)failedSEL { + + GTMAssertSelectorNilOrImplementedWithArguments(delegate, finishedSEL, @encode(GTMHTTPFetcher *), @encode(NSData *), NULL); + GTMAssertSelectorNilOrImplementedWithArguments(delegate, failedSEL, @encode(GTMHTTPFetcher *), @encode(NSError *), NULL); + GTMAssertSelectorNilOrImplementedWithArguments(delegate, receivedDataSEL_, @encode(GTMHTTPFetcher *), @encode(NSData *), NULL); + GTMAssertSelectorNilOrImplementedWithArguments(delegate, retrySEL_, @encode(GTMHTTPFetcher *), @encode(BOOL), @encode(NSError *), NULL); + + if (connection_ != nil) { + _GTMDevAssert(connection_ != nil, + @"fetch object %@ being reused; this should never happen", + self); + goto CannotBeginFetch; + } + + if (request_ == nil) { + _GTMDevAssert(request_ != nil, @"beginFetchWithDelegate requires a request"); + goto CannotBeginFetch; + } + + [downloadedData_ release]; + downloadedData_ = nil; + + [self setDelegate:delegate]; + finishedSEL_ = finishedSEL; + failedSEL_ = failedSEL; + + if (postData_ || postStream_) { + if ([request_ HTTPMethod] == nil || [[request_ HTTPMethod] isEqual:@"GET"]) { + [request_ setHTTPMethod:@"POST"]; + } + + if (postData_) { + [request_ setHTTPBody:postData_]; + } else { + + // if logging is enabled, it needs a buffer to accumulate data from any + // NSInputStream used for uploading. Logging will wrap the input + // stream with a stream that lets us keep a copy the data being read. + if ([GTMHTTPFetcher isLoggingEnabled] && postStream_ != nil) { + loggedStreamData_ = [[NSMutableData alloc] init]; + [self logCapturePostStream]; + } + + [request_ setHTTPBodyStream:postStream_]; + } + } + + if (fetchHistory_) { + + // If this URL is in the history, set the Last-Modified header field + + // if we have a history, we're tracking across fetches, so we don't + // want to pull results from a cache + [request_ setCachePolicy:NSURLRequestReloadIgnoringCacheData]; + + NSDictionary* lastModifiedDict = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryLastModifiedKey]; + NSString* urlString = [[request_ URL] absoluteString]; + NSString* lastModifiedStr = [lastModifiedDict objectForKey:urlString]; + + // servers don't want last-modified-ifs on POSTs, so check for a body + if (lastModifiedStr + && [request_ HTTPBody] == nil + && [request_ HTTPBodyStream] == nil) { + + [request_ addValue:lastModifiedStr forHTTPHeaderField:kGTMIfModifiedSinceHeader]; + } + } + + // get cookies for this URL from our storage array, if + // we have a storage array + if (cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodSystemDefault) { + + NSArray *cookies = [self cookiesForURL:[request_ URL]]; + + if ([cookies count]) { + + NSDictionary *headerFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + NSString *cookieHeader = [headerFields objectForKey:@"Cookie"]; // key used in header dictionary + if (cookieHeader) { + [request_ addValue:cookieHeader forHTTPHeaderField:@"Cookie"]; // header name + } + } + } + + // finally, start the connection + + Class connectionClass = [[self class] connectionClass]; + + NSArray *runLoopModes = nil; + + if ([[self class] doesSupportRunLoopModes]) { + + // use the connection-specific run loop modes, if they were provided, + // or else use the GTMHTTPFetcher default run loop modes, if any + if (runLoopModes_) { + runLoopModes = runLoopModes_; + } else { + runLoopModes = gGTMFetcherDefaultRunLoopModes; + } + } + + if ([runLoopModes count] == 0) { + + // if no run loop modes were specified, then we'll start the connection + // on the current run loop in the current mode + connection_ = [[connectionClass connectionWithRequest:request_ + delegate:self] retain]; + } else { + + // schedule on current run loop in the specified modes + connection_ = [[connectionClass alloc] initWithRequest:request_ + delegate:self + startImmediately:NO]; + + for (int idx = 0; idx < [runLoopModes count]; idx++) { + NSString *mode = [runLoopModes objectAtIndex:idx]; + [connection_ scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:mode]; + } + [connection_ start]; + } + + if (!connection_) { + _GTMDevAssert(connection_ != nil, + @"beginFetchWithDelegate could not create a connection"); + goto CannotBeginFetch; + } + + // we'll retain the delegate only during the outstanding connection (similar + // to what Cocoa does with performSelectorOnMainThread:) since we'd crash + // if the delegate was released in the interim. We don't retain the selector + // at other times, to avoid vicious retain loops. This retain is balanced in + // the -stopFetch method. + [delegate_ retain]; + + downloadedData_ = [[NSMutableData alloc] init]; + return YES; + +CannotBeginFetch: + + if (failedSEL) { + + NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain + code:kGTMHTTPFetcherErrorDownloadFailed + userInfo:nil]; + + [[self retain] autorelease]; // in case the callback releases us + + [delegate performSelector:failedSEL_ + withObject:self + withObject:error]; + } + + return NO; +} + +// Returns YES if this is in the process of fetching a URL, or waiting to +// retry +- (BOOL)isFetching { + return (connection_ != nil || retryTimer_ != nil); +} + +// Returns the status code set in connection:didReceiveResponse: +- (NSInteger)statusCode { + + NSInteger statusCode; + + if (response_ != nil + && [response_ respondsToSelector:@selector(statusCode)]) { + + statusCode = [(NSHTTPURLResponse *)response_ statusCode]; + } else { + // Default to zero, in hopes of hinting "Unknown" (we can't be + // sure that things are OK enough to use 200). + statusCode = 0; + } + return statusCode; +} + +// Cancel the fetch of the URL that's currently in progress. +- (void)stopFetching { + [self destroyRetryTimer]; + + if (connection_) { + // in case cancelling the connection calls this recursively, we want + // to ensure that we'll only release the connection and delegate once, + // so first set connection_ to nil + + NSURLConnection* oldConnection = connection_; + connection_ = nil; + + // this may be called in a callback from the connection, so use autorelease + [oldConnection cancel]; + [oldConnection autorelease]; + + // balance the retain done when the connection was opened + [delegate_ release]; + } +} + +- (void)retryFetch { + + id holdDelegate = [[delegate_ retain] autorelease]; + + [self stopFetching]; + + [self beginFetchWithDelegate:holdDelegate + didFinishSelector:finishedSEL_ + didFailSelector:failedSEL_]; +} + +#pragma mark NSURLConnection Delegate Methods + +// +// NSURLConnection Delegate Methods +// + +// This method just says "follow all redirects", which _should_ be the default behavior, +// According to file:///Developer/ADC%20Reference%20Library/documentation/Cocoa/Conceptual/URLLoadingSystem +// but the redirects were not being followed until I added this method. May be +// a bug in the NSURLConnection code, or the documentation. +// +// In OS X 10.4.8 and earlier, the redirect request doesn't +// get the original's headers and body. This causes POSTs to fail. +// So we construct a new request, a copy of the original, with overrides from the +// redirect. +// +// Docs say that if redirectResponse is nil, just return the redirectRequest. + +- (NSURLRequest *)connection:(NSURLConnection *)connection + willSendRequest:(NSURLRequest *)redirectRequest + redirectResponse:(NSURLResponse *)redirectResponse { + + if (redirectRequest && redirectResponse) { + NSMutableURLRequest *newRequest = [[request_ mutableCopy] autorelease]; + // copy the URL + NSURL *redirectURL = [redirectRequest URL]; + NSURL *url = [newRequest URL]; + + // disallow scheme changes (say, from https to http) + NSString *redirectScheme = [url scheme]; + NSString *newScheme = [redirectURL scheme]; + NSString *newResourceSpecifier = [redirectURL resourceSpecifier]; + + if ([redirectScheme caseInsensitiveCompare:@"http"] == NSOrderedSame + && newScheme != nil + && [newScheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { + + // allow the change from http to https + redirectScheme = newScheme; + } + + NSString *newUrlString = [NSString stringWithFormat:@"%@:%@", + redirectScheme, newResourceSpecifier]; + + NSURL *newURL = [NSURL URLWithString:newUrlString]; + [newRequest setURL:newURL]; + + // any headers in the redirect override headers in the original. + NSDictionary *redirectHeaders = [redirectRequest allHTTPHeaderFields]; + if (redirectHeaders) { + NSEnumerator *enumerator = [redirectHeaders keyEnumerator]; + NSString *key; + while (nil != (key = [enumerator nextObject])) { + NSString *value = [redirectHeaders objectForKey:key]; + [newRequest setValue:value forHTTPHeaderField:key]; + } + } + redirectRequest = newRequest; + + // save cookies from the response + [self handleCookiesForResponse:redirectResponse]; + + // log the response we just received + [self setResponse:redirectResponse]; + [self logFetchWithError:nil]; + + // update the request for future logging + [self setRequest:redirectRequest]; +} + return redirectRequest; +} + +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { + + // this method is called when the server has determined that it + // has enough information to create the NSURLResponse + // it can be called multiple times, for example in the case of a + // redirect, so each time we reset the data. + [downloadedData_ setLength:0]; + + [self setResponse:response]; + + // save cookies from the response + [self handleCookiesForResponse:response]; +} + + +// handleCookiesForResponse: handles storage of cookies for responses passed to +// connection:willSendRequest:redirectResponse: and connection:didReceiveResponse: +- (void)handleCookiesForResponse:(NSURLResponse *)response { + + if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodSystemDefault) { + + // do nothing special for NSURLConnection's default storage mechanism + + } else if ([response respondsToSelector:@selector(allHeaderFields)]) { + + // grab the cookies from the header as NSHTTPCookies and store them either + // into our static array or into the fetchHistory + + NSDictionary *responseHeaderFields = [(NSHTTPURLResponse *)response allHeaderFields]; + if (responseHeaderFields) { + + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseHeaderFields + forURL:[response URL]]; + if ([cookies count] > 0) { + + NSMutableArray *cookieArray = nil; + + // static cookies are stored in gGTMFetcherStaticCookies; fetchHistory + // cookies are stored in fetchHistory_'s kGTMHTTPFetcherHistoryCookiesKey + + if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodStatic) { + + cookieArray = gGTMFetcherStaticCookies; + + } else if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodFetchHistory + && fetchHistory_ != nil) { + + cookieArray = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryCookiesKey]; + if (cookieArray == nil) { + cookieArray = [NSMutableArray array]; + [fetchHistory_ setObject:cookieArray forKey:kGTMHTTPFetcherHistoryCookiesKey]; + } + } + + if (cookieArray) { + @synchronized(cookieArray) { + [self setCookies:cookies inArray:cookieArray]; + } + } + } + } + } +} + +-(void)connection:(NSURLConnection *)connection + didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { + + if ([challenge previousFailureCount] <= 2) { + + NSURLCredential *credential = credential_; + + if ([[challenge protectionSpace] isProxy] && proxyCredential_ != nil) { + credential = proxyCredential_; + } + + // Here, if credential is still nil, then we *could* try to get it from + // NSURLCredentialStorage's defaultCredentialForProtectionSpace:. + // We don't, because we're assuming: + // + // - for server credentials, we only want ones supplied by the program + // calling http fetcher + // - for proxy credentials, if one were necessary and available in the + // keychain, it would've been found automatically by NSURLConnection + // and this challenge delegate method never would've been called + // anyway + + if (credential) { + // try the credential + [[challenge sender] useCredential:credential + forAuthenticationChallenge:challenge]; + return; + } + } + + // If we don't have credentials, or we've already failed auth 3x... + [[challenge sender] cancelAuthenticationChallenge:challenge]; + + // report the error, putting the challenge as a value in the userInfo + // dictionary + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:challenge + forKey:kGTMHTTPFetcherErrorChallengeKey]; + + NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain + code:kGTMHTTPFetcherErrorAuthenticationChallengeFailed + userInfo:userInfo]; + + [self connection:connection didFailWithError:error]; +} + + + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { + [downloadedData_ appendData:data]; + + if (receivedDataSEL_) { + [delegate_ performSelector:receivedDataSEL_ + withObject:self + withObject:downloadedData_]; + } +} + +- (void)updateFetchHistory { + + if (fetchHistory_) { + + NSString* urlString = [[request_ URL] absoluteString]; + if ([response_ respondsToSelector:@selector(allHeaderFields)]) { + NSDictionary *headers = [(NSHTTPURLResponse *)response_ allHeaderFields]; + NSString* lastModifiedStr = [headers objectForKey:kGTMLastModifiedHeader]; + + // get the dictionary mapping URLs to last-modified dates + NSMutableDictionary* lastModifiedDict = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryLastModifiedKey]; + if (!lastModifiedDict) { + lastModifiedDict = [NSMutableDictionary dictionary]; + [fetchHistory_ setObject:lastModifiedDict forKey:kGTMHTTPFetcherHistoryLastModifiedKey]; + } + + NSMutableDictionary* datedDataCache = nil; + if (shouldCacheDatedData_) { + // get the dictionary mapping URLs to cached, dated data + datedDataCache = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryDatedDataKey]; + if (!datedDataCache) { + datedDataCache = [NSMutableDictionary dictionary]; + [fetchHistory_ setObject:datedDataCache forKey:kGTMHTTPFetcherHistoryDatedDataKey]; + } + } + + NSInteger statusCode = [self statusCode]; + if (statusCode != kGTMHTTPFetcherStatusNotModified) { + + // save this last modified date string for successful results (<300) + // If there's no last modified string, clear the dictionary + // entry for this URL. Also cache or delete the data, if appropriate + // (when datedDataCache is non-nil.) + if (lastModifiedStr && statusCode < 300) { + [lastModifiedDict setValue:lastModifiedStr forKey:urlString]; + [datedDataCache setValue:downloadedData_ forKey:urlString]; + } else { + [lastModifiedDict removeObjectForKey:urlString]; + [datedDataCache removeObjectForKey:urlString]; + } + } + } + } +} + +// for error 304's ("Not Modified") where we've cached the data, return status +// 200 ("OK") to the caller (but leave the fetcher status as 304) +// and copy the cached data to downloadedData_. +// For other errors or if there's no cached data, just return the actual status. +- (NSInteger)statusAfterHandlingNotModifiedError { + + NSInteger status = [self statusCode]; + if (status == kGTMHTTPFetcherStatusNotModified && shouldCacheDatedData_) { + + // get the dictionary of URLs and data + NSString* urlString = [[request_ URL] absoluteString]; + + NSDictionary* datedDataCache = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryDatedDataKey]; + NSData* cachedData = [datedDataCache objectForKey:urlString]; + + if (cachedData) { + // copy our stored data, and forge the status to pass on to the delegate + [downloadedData_ setData:cachedData]; + status = 200; + } + } + return status; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection { + + [self updateFetchHistory]; + + [[self retain] autorelease]; // in case the callback releases us + + [self logFetchWithError:nil]; + + NSInteger status = [self statusAfterHandlingNotModifiedError]; + + if (status >= 300) { + + if ([self shouldRetryNowForStatus:status error:nil]) { + + [self beginRetryTimer]; + + } else { + // not retrying + + // did they want failure notifications? + if (failedSEL_) { + + NSDictionary *userInfo = + [NSDictionary dictionaryWithObject:downloadedData_ + forKey:kGTMHTTPFetcherStatusDataKey]; + NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain + code:status + userInfo:userInfo]; + + [delegate_ performSelector:failedSEL_ + withObject:self + withObject:error]; + } + // we're done fetching + [self stopFetching]; + } + + } else if (finishedSEL_) { + + // successful http status (under 300) + [delegate_ performSelector:finishedSEL_ + withObject:self + withObject:downloadedData_]; + [self stopFetching]; + } + +} + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { + + [self logFetchWithError:error]; + + if ([self shouldRetryNowForStatus:0 error:error]) { + + [self beginRetryTimer]; + + } else { + + if (failedSEL_) { + [[self retain] autorelease]; // in case the callback releases us + + [delegate_ performSelector:failedSEL_ + withObject:self + withObject:error]; + } + + [self stopFetching]; + } +} + +#pragma mark Retries + +- (BOOL)isRetryError:(NSError *)error { + + struct retryRecord { + NSString *const domain; + int code; + }; + + struct retryRecord retries[] = { + { kGTMHTTPFetcherStatusDomain, 408 }, // request timeout + { kGTMHTTPFetcherStatusDomain, 503 }, // service unavailable + { kGTMHTTPFetcherStatusDomain, 504 }, // request timeout + { NSURLErrorDomain, NSURLErrorTimedOut }, + { NSURLErrorDomain, NSURLErrorNetworkConnectionLost }, + { nil, 0 } + }; + + // NSError's isEqual always returns false for equal but distinct instances + // of NSError, so we have to compare the domain and code values explicitly + + for (int idx = 0; retries[idx].domain != nil; idx++) { + + if ([[error domain] isEqual:retries[idx].domain] + && [error code] == retries[idx].code) { + + return YES; + } + } + return NO; +} + + +// shouldRetryNowForStatus:error: returns YES if the user has enabled retries +// and the status or error is one that is suitable for retrying. "Suitable" +// means either the isRetryError:'s list contains the status or error, or the +// user's retrySelector: is present and returns YES when called. +- (BOOL)shouldRetryNowForStatus:(NSInteger)status + error:(NSError *)error { + + if ([self isRetryEnabled]) { + + if ([self nextRetryInterval] < [self maxRetryInterval]) { + + if (error == nil) { + // make an error for the status + error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain + code:status + userInfo:nil]; + } + + BOOL willRetry = [self isRetryError:error]; + + if (retrySEL_) { + NSMethodSignature *signature = [delegate_ methodSignatureForSelector:retrySEL_]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:retrySEL_]; + [invocation setTarget:delegate_]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&willRetry atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + + [invocation getReturnValue:&willRetry]; + } + + return willRetry; + } + } + + return NO; +} + +- (void)beginRetryTimer { + + NSTimeInterval nextInterval = [self nextRetryInterval]; + NSTimeInterval maxInterval = [self maxRetryInterval]; + + NSTimeInterval newInterval = MIN(nextInterval, maxInterval); + + [self primeTimerWithNewTimeInterval:newInterval]; +} + +- (void)primeTimerWithNewTimeInterval:(NSTimeInterval)secs { + + [self destroyRetryTimer]; + + lastRetryInterval_ = secs; + + retryTimer_ = [NSTimer scheduledTimerWithTimeInterval:secs + target:self + selector:@selector(retryTimerFired:) + userInfo:nil + repeats:NO]; + [retryTimer_ retain]; +} + +- (void)retryTimerFired:(NSTimer *)timer { + + [self destroyRetryTimer]; + + retryCount_++; + + [self retryFetch]; +} + +- (void)destroyRetryTimer { + + [retryTimer_ invalidate]; + [retryTimer_ autorelease]; + retryTimer_ = nil; +} + +- (unsigned int)retryCount { + return retryCount_; +} + +- (NSTimeInterval)nextRetryInterval { + // the next wait interval is the factor (2.0) times the last interval, + // but never less than the minimum interval + NSTimeInterval secs = lastRetryInterval_ * retryFactor_; + secs = MIN(secs, maxRetryInterval_); + secs = MAX(secs, minRetryInterval_); + + return secs; +} + +- (BOOL)isRetryEnabled { + return isRetryEnabled_; +} + +- (void)setIsRetryEnabled:(BOOL)flag { + + if (flag && !isRetryEnabled_) { + // We defer initializing these until the user calls setIsRetryEnabled + // to avoid seeding the random number generator if it's not needed. + // However, it means min and max intervals for this fetcher are reset + // as a side effect of calling setIsRetryEnabled. + // + // seed the random value, and make an initial retry interval + // random between 1.0 and 2.0 seconds + srandomdev(); + [self setMinRetryInterval:0.0]; + [self setMaxRetryInterval:kDefaultMaxRetryInterval]; + [self setRetryFactor:2.0]; + lastRetryInterval_ = 0.0; + } + isRetryEnabled_ = flag; +}; + +- (SEL)retrySelector { + return retrySEL_; +} + +- (void)setRetrySelector:(SEL)theSelector { + retrySEL_ = theSelector; +} + +- (NSTimeInterval)maxRetryInterval { + return maxRetryInterval_; +} + +- (void)setMaxRetryInterval:(NSTimeInterval)secs { + if (secs > 0) { + maxRetryInterval_ = secs; + } else { + maxRetryInterval_ = kDefaultMaxRetryInterval; + } +} + +- (double)minRetryInterval { + return minRetryInterval_; +} + +- (void)setMinRetryInterval:(NSTimeInterval)secs { + if (secs > 0) { + minRetryInterval_ = secs; + } else { + // set min interval to a random value between 1.0 and 2.0 seconds + // so that if multiple clients start retrying at the same time, they'll + // repeat at different times and avoid overloading the server + minRetryInterval_ = 1.0 + ((double)(random() & 0x0FFFF) / (double) 0x0FFFF); + } +} + +- (double)retryFactor { + return retryFactor_; +} + +- (void)setRetryFactor:(double)multiplier { + retryFactor_ = multiplier; +} + +#pragma mark Getters and Setters + +- (NSMutableURLRequest *)request { + return request_; +} + +- (void)setRequest:(NSURLRequest *)theRequest { + [request_ autorelease]; + request_ = [theRequest mutableCopy]; +} + +- (NSURLCredential *)credential { + return credential_; +} + +- (void)setCredential:(NSURLCredential *)theCredential { + [credential_ autorelease]; + credential_ = [theCredential retain]; +} + +- (NSURLCredential *)proxyCredential { + return proxyCredential_; +} + +- (void)setProxyCredential:(NSURLCredential *)theCredential { + [proxyCredential_ autorelease]; + proxyCredential_ = [theCredential retain]; +} + +- (NSData *)postData { + return postData_; +} + +- (void)setPostData:(NSData *)theData { + [postData_ autorelease]; + postData_ = [theData retain]; +} + +- (NSInputStream *)postStream { + return postStream_; +} + +- (void)setPostStream:(NSInputStream *)theStream { + [postStream_ autorelease]; + postStream_ = [theStream retain]; +} + +- (GTMHTTPFetcherCookieStorageMethod)cookieStorageMethod { + return cookieStorageMethod_; +} + +- (void)setCookieStorageMethod:(GTMHTTPFetcherCookieStorageMethod)method { + + cookieStorageMethod_ = method; + + if (method == kGTMHTTPFetcherCookieStorageMethodSystemDefault) { + [request_ setHTTPShouldHandleCookies:YES]; + } else { + [request_ setHTTPShouldHandleCookies:NO]; + } +} + +- (id)delegate { + return delegate_; +} + +- (void)setDelegate:(id)theDelegate { + + // we retain delegate_ only during the life of the connection + if (connection_) { + [delegate_ autorelease]; + delegate_ = [theDelegate retain]; + } else { + delegate_ = theDelegate; + } +} + +- (SEL)receivedDataSelector { + return receivedDataSEL_; +} + +- (void)setReceivedDataSelector:(SEL)theSelector { + receivedDataSEL_ = theSelector; +} + +- (NSURLResponse *)response { + return response_; +} + +- (void)setResponse:(NSURLResponse *)response { + [response_ autorelease]; + response_ = [response retain]; +} + +- (NSMutableDictionary *)fetchHistory { + return fetchHistory_; +} + +- (void)setFetchHistory:(NSMutableDictionary *)fetchHistory { + [fetchHistory_ autorelease]; + fetchHistory_ = [fetchHistory retain]; + + if (fetchHistory_ != nil) { + [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodFetchHistory]; + } else { + [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic]; + } +} + +- (void)setShouldCacheDatedData:(BOOL)flag { + shouldCacheDatedData_ = flag; + if (!flag) { + [self clearDatedDataHistory]; + } +} + +- (BOOL)shouldCacheDatedData { + return shouldCacheDatedData_; +} + +// delete last-modified dates and cached data from the fetch history +- (void)clearDatedDataHistory { + [fetchHistory_ removeObjectForKey:kGTMHTTPFetcherHistoryLastModifiedKey]; + [fetchHistory_ removeObjectForKey:kGTMHTTPFetcherHistoryDatedDataKey]; +} + +- (id)userData { + return userData_; +} + +- (void)setUserData:(id)theObj { + [userData_ autorelease]; + userData_ = [theObj retain]; +} + +- (NSArray *)runLoopModes { + return runLoopModes_; +} + +- (void)setRunLoopModes:(NSArray *)modes { + [runLoopModes_ autorelease]; + runLoopModes_ = [modes retain]; +} + ++ (BOOL)doesSupportRunLoopModes { + SEL sel = @selector(initWithRequest:delegate:startImmediately:); + return [NSURLConnection instancesRespondToSelector:sel]; +} + ++ (NSArray *)defaultRunLoopModes { + return gGTMFetcherDefaultRunLoopModes; +} + ++ (void)setDefaultRunLoopModes:(NSArray *)modes { + [gGTMFetcherDefaultRunLoopModes autorelease]; + gGTMFetcherDefaultRunLoopModes = [modes retain]; +} + ++ (Class)connectionClass { + if (gGTMFetcherConnectionClass == nil) { + gGTMFetcherConnectionClass = [NSURLConnection class]; + } + return gGTMFetcherConnectionClass; +} + ++ (void)setConnectionClass:(Class)theClass { + gGTMFetcherConnectionClass = theClass; +} + +#pragma mark Cookies + +// return a cookie from the array with the same name, domain, and path as the +// given cookie, or else return nil if none found +// +// Both the cookie being tested and all cookies in cookieStorageArray should +// be valid (non-nil name, domains, paths) +- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie + inArray:(NSArray *)cookieStorageArray { + + NSUInteger numberOfCookies = [cookieStorageArray count]; + NSString *name = [cookie name]; + NSString *domain = [cookie domain]; + NSString *path = [cookie path]; + + _GTMDevAssert(name && domain && path, + @"Invalid cookie (name:%@ domain:%@ path:%@)", + name, domain, path); + + for (NSUInteger idx = 0; idx < numberOfCookies; idx++) { + + NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx]; + + if ([[storedCookie name] isEqual:name] + && [[storedCookie domain] isEqual:domain] + && [[storedCookie path] isEqual:path]) { + + return storedCookie; + } + } + return nil; +} + +// remove any expired cookies from the array, excluding cookies with nil +// expirations +- (void)removeExpiredCookiesInArray:(NSMutableArray *)cookieStorageArray { + + // count backwards since we're deleting items from the array + for (NSInteger idx = [cookieStorageArray count] - 1; idx >= 0; idx--) { + + NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx]; + + NSDate *expiresDate = [storedCookie expiresDate]; + if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) { + [cookieStorageArray removeObjectAtIndex:idx]; + } + } +} + + +// retrieve all cookies appropriate for the given URL, considering +// domain, path, cookie name, expiration, security setting. +// Side effect: removed expired cookies from the storage array +- (NSArray *)cookiesForURL:(NSURL *)theURL inArray:(NSMutableArray *)cookieStorageArray { + + [self removeExpiredCookiesInArray:cookieStorageArray]; + + NSMutableArray *foundCookies = [NSMutableArray array]; + + // we'll prepend "." to the desired domain, since we want the + // actual domain "nytimes.com" to still match the cookie domain ".nytimes.com" + // when we check it below with hasSuffix + NSString *host = [theURL host]; + NSString *path = [theURL path]; + NSString *scheme = [theURL scheme]; + + NSString *domain = nil; + if ([host isEqual:@"localhost"]) { + // the domain stored into NSHTTPCookies for localhost is "localhost.local" + domain = @"localhost.local"; + } else { + if (host) { + domain = [@"." stringByAppendingString:host]; + } + } + + NSUInteger numberOfCookies = [cookieStorageArray count]; + for (NSUInteger idx = 0; idx < numberOfCookies; idx++) { + + NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx]; + + NSString *cookieDomain = [storedCookie domain]; + NSString *cookiePath = [storedCookie path]; + BOOL cookieIsSecure = [storedCookie isSecure]; + + BOOL domainIsOK = [domain hasSuffix:cookieDomain]; + BOOL pathIsOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath]; + BOOL secureIsOK = (!cookieIsSecure) || [scheme isEqual:@"https"]; + + if (domainIsOK && pathIsOK && secureIsOK) { + [foundCookies addObject:storedCookie]; + } + } + return foundCookies; +} + +// return cookies for the given URL using the current cookie storage method +- (NSArray *)cookiesForURL:(NSURL *)theURL { + + NSArray *cookies = nil; + NSMutableArray *cookieStorageArray = nil; + + if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodStatic) { + cookieStorageArray = gGTMFetcherStaticCookies; + } else if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodFetchHistory) { + cookieStorageArray = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryCookiesKey]; + } else { + // kGTMHTTPFetcherCookieStorageMethodSystemDefault + cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:theURL]; + } + + if (cookieStorageArray) { + + @synchronized(cookieStorageArray) { + + // cookiesForURL returns a new array of immutable NSCookie objects + // from cookieStorageArray + cookies = [self cookiesForURL:theURL + inArray:cookieStorageArray]; + } + } + return cookies; +} + + +// add all cookies in the array |newCookies| to the storage array, +// replacing cookies in the storage array as appropriate +// Side effect: removes expired cookies from the storage array +- (void)setCookies:(NSArray *)newCookies + inArray:(NSMutableArray *)cookieStorageArray { + + [self removeExpiredCookiesInArray:cookieStorageArray]; + + NSEnumerator *newCookieEnum = [newCookies objectEnumerator]; + NSHTTPCookie *newCookie; + + while ((newCookie = [newCookieEnum nextObject]) != nil) { + + if ([[newCookie name] length] > 0 + && [[newCookie domain] length] > 0 + && [[newCookie path] length] > 0) { + + // remove the cookie if it's currently in the array + NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie + inArray:cookieStorageArray]; + if (oldCookie) { + [cookieStorageArray removeObject:oldCookie]; + } + + // make sure the cookie hasn't already expired + NSDate *expiresDate = [newCookie expiresDate]; + if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) { + [cookieStorageArray addObject:newCookie]; + } + + } else { + _GTMDevAssert(NO, @"Cookie incomplete: %@", newCookie); + } + } +} +@end + +#pragma mark Logging + +// NOTE: Threads and Logging +// +// All the NSURLConnection callbacks happen on one thread, so we don't have +// to put any synchronization into the logging code. Yes, the state around +// logging (it's directory, etc.) could use it, but for now, that's punted. + + +// We don't invoke Leopard methods on 10.4, because we check if the methods are +// implemented before invoking it, but we need to be able to compile without +// warnings. +// This declaration means if you target <=10.4, this method will compile +// without complaint in this source, so you must test with +// -respondsToSelector:, too. +#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4 +@interface NSFileManager (LeopardMethodsOnTigerBuilds) +- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error; +@end +#endif +// The iPhone Foundation removes the deprecated removeFileAtPath:handler: +#if GTM_IPHONE_SDK +@interface NSFileManager (TigerMethodsOniPhoneBuilds) +- (BOOL)removeFileAtPath:(NSString *)path handler:(id)handler; +@end +#endif + +@implementation GTMHTTPFetcher (GTMHTTPFetcherLogging) + +// if GTM_HTTPFETCHER_ENABLE_LOGGING is defined by the user's project then +// logging code will be compiled into the framework + +#if !GTM_HTTPFETCHER_ENABLE_LOGGING +- (void)logFetchWithError:(NSError *)error {} + ++ (void)setLoggingDirectory:(NSString *)path {} ++ (NSString *)loggingDirectory {return nil;} + ++ (void)setIsLoggingEnabled:(BOOL)flag {} ++ (BOOL)isLoggingEnabled {return NO;} + ++ (void)setLoggingProcessName:(NSString *)str {} ++ (NSString *)loggingProcessName {return nil;} + ++ (void)setLoggingDateStamp:(NSString *)str {} ++ (NSString *)loggingDateStamp {return nil;} + +- (void)appendLoggedStreamData:(NSData *)newData {} +- (void)logCapturePostStream {} +#else // GTM_HTTPFETCHER_ENABLE_LOGGING + +// fetchers come and fetchers go, but statics are forever +static BOOL gIsLoggingEnabled = NO; +static NSString *gLoggingDirectoryPath = nil; +static NSString *gLoggingDateStamp = nil; +static NSString* gLoggingProcessName = nil; + ++ (void)setLoggingDirectory:(NSString *)path { + [gLoggingDirectoryPath autorelease]; + gLoggingDirectoryPath = [path copy]; +} + ++ (NSString *)loggingDirectory { + + if (!gLoggingDirectoryPath) { + +#if GTM_IPHONE_SDK + // default to a directory called GTMHTTPDebugLogs into a sandbox-safe + // directory that a devloper can find easily, the application home + NSArray *arr = [NSArray arrayWithObject:NSHomeDirectory()]; +#else + // default to a directory called GTMHTTPDebugLogs in the desktop folder + NSArray *arr = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, + NSUserDomainMask, YES); +#endif + + if ([arr count] > 0) { + NSString *const kGTMLogFolderName = @"GTMHTTPDebugLogs"; + + NSString *desktopPath = [arr objectAtIndex:0]; + NSString *logsFolderPath = [desktopPath stringByAppendingPathComponent:kGTMLogFolderName]; + + BOOL doesFolderExist; + BOOL isDir = NO; + NSFileManager *fileManager = [NSFileManager defaultManager]; + doesFolderExist = [fileManager fileExistsAtPath:logsFolderPath + isDirectory:&isDir]; + + if (!doesFolderExist) { + // make the directory + doesFolderExist = [fileManager createDirectoryAtPath:logsFolderPath + attributes:nil]; + } + + if (doesFolderExist) { + // it's there; store it in the global + gLoggingDirectoryPath = [logsFolderPath copy]; + } + } + } + return gLoggingDirectoryPath; +} + ++ (void)setIsLoggingEnabled:(BOOL)flag { + gIsLoggingEnabled = flag; +} + ++ (BOOL)isLoggingEnabled { + return gIsLoggingEnabled; +} + ++ (void)setLoggingProcessName:(NSString *)str { + [gLoggingProcessName release]; + gLoggingProcessName = [str copy]; +} + ++ (NSString *)loggingProcessName { + + // get the process name (once per run) replacing spaces with underscores + if (!gLoggingProcessName) { + + NSString *procName = [[NSProcessInfo processInfo] processName]; + NSMutableString *loggingProcessName; + loggingProcessName = [[NSMutableString alloc] initWithString:procName]; + + [loggingProcessName replaceOccurrencesOfString:@" " + withString:@"_" + options:0 + range:NSMakeRange(0, [gLoggingProcessName length])]; + gLoggingProcessName = loggingProcessName; + } + return gLoggingProcessName; +} + ++ (void)setLoggingDateStamp:(NSString *)str { + [gLoggingDateStamp release]; + gLoggingDateStamp = [str copy]; +} + ++ (NSString *)loggingDateStamp { + // we'll pick one date stamp per run, so a run that starts at a later second + // will get a unique results html file + if (!gLoggingDateStamp) { + // produce a string like 08-21_01-41-23PM + + NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease]; + [formatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + [formatter setDateFormat:@"M-dd_hh-mm-ssa"]; + + gLoggingDateStamp = [[formatter stringFromDate:[NSDate date]] retain] ; + } + return gLoggingDateStamp; +} + +- (NSString *)cleanParameterFollowing:(NSString *)paramName + fromString:(NSString *)originalStr { + // We don't want the password written to disk + // + // find "&Passwd=" in the string, and replace it and the stuff that + // follows it with "Passwd=_snip_" + + NSRange passwdRange = [originalStr rangeOfString:@"&Passwd="]; + if (passwdRange.location != NSNotFound) { + + // we found Passwd=; find the & that follows the parameter + NSUInteger origLength = [originalStr length]; + NSRange restOfString = NSMakeRange(passwdRange.location+1, + origLength - passwdRange.location - 1); + NSRange rangeOfFollowingAmp = [originalStr rangeOfString:@"&" + options:0 + range:restOfString]; + NSRange replaceRange; + if (rangeOfFollowingAmp.location == NSNotFound) { + // found no other & so replace to end of string + replaceRange = NSMakeRange(passwdRange.location, + rangeOfFollowingAmp.location - passwdRange.location); + } else { + // another parameter after &Passwd=foo + replaceRange = NSMakeRange(passwdRange.location, + rangeOfFollowingAmp.location - passwdRange.location); + } + + NSMutableString *result = [NSMutableString stringWithString:originalStr]; + NSString *replacement = [NSString stringWithFormat:@"%@_snip_", paramName]; + + [result replaceCharactersInRange:replaceRange withString:replacement]; + return result; + } + return originalStr; +} + +// stringFromStreamData creates a string given the supplied data +// +// If NSString can create a UTF-8 string from the data, then that is returned. +// +// Otherwise, this routine tries to find a MIME boundary at the beginning of +// the data block, and uses that to break up the data into parts. Each part +// will be used to try to make a UTF-8 string. For parts that fail, a +// replacement string showing the part header and <<n bytes>> is supplied +// in place of the binary data. + +- (NSString *)stringFromStreamData:(NSData *)data { + + if (data == nil) return nil; + + // optimistically, see if the whole data block is UTF-8 + NSString *streamDataStr = [[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding] autorelease]; + if (streamDataStr) return streamDataStr; + + // Munge a buffer by replacing non-ASCII bytes with underscores, + // and turn that munged buffer an NSString. That gives us a string + // we can use with NSScanner. + NSMutableData *mutableData = [NSMutableData dataWithData:data]; + unsigned char *bytes = [mutableData mutableBytes]; + + for (int idx = 0; idx < [mutableData length]; idx++) { + if (bytes[idx] > 0x7F || bytes[idx] == 0) { + bytes[idx] = '_'; + } + } + + NSString *mungedStr = [[[NSString alloc] initWithData:mutableData + encoding:NSUTF8StringEncoding] autorelease]; + if (mungedStr != nil) { + + // scan for the boundary string + NSString *boundary = nil; + NSScanner *scanner = [NSScanner scannerWithString:mungedStr]; + + if ([scanner scanUpToString:@"\r\n" intoString:&boundary] + && [boundary hasPrefix:@"--"]) { + + // we found a boundary string; use it to divide the string into parts + NSArray *mungedParts = [mungedStr componentsSeparatedByString:boundary]; + + // look at each of the munged parts in the original string, and try to + // convert those into UTF-8 + NSMutableArray *origParts = [NSMutableArray array]; + NSUInteger offset = 0; + for (int partIdx = 0; partIdx < [mungedParts count]; partIdx++) { + + NSString *mungedPart = [mungedParts objectAtIndex:partIdx]; + NSUInteger partSize = [mungedPart length]; + + NSRange range = NSMakeRange(offset, partSize); + NSData *origPartData = [data subdataWithRange:range]; + + NSString *origPartStr = [[[NSString alloc] initWithData:origPartData + encoding:NSUTF8StringEncoding] autorelease]; + if (origPartStr) { + // we could make this original part into UTF-8; use the string + [origParts addObject:origPartStr]; + } else { + // this part can't be made into UTF-8; scan the header, if we can + NSString *header = nil; + NSScanner *headerScanner = [NSScanner scannerWithString:mungedPart]; + if (![headerScanner scanUpToString:@"\r\n\r\n" intoString:&header]) { + // we couldn't find a header + header = @""; + } + + // make a part string with the header and <<n bytes>> + NSString *binStr = [NSString stringWithFormat:@"\r%@\r<<%u bytes>>\r", + header, partSize - [header length]]; + [origParts addObject:binStr]; + } + offset += partSize + [boundary length]; + } + + // rejoin the original parts + streamDataStr = [origParts componentsJoinedByString:boundary]; + } + } + + if (!streamDataStr) { + // give up; just make a string showing the uploaded bytes + streamDataStr = [NSString stringWithFormat:@"<<%u bytes>>", [data length]]; + } + return streamDataStr; +} + +// logFetchWithError is called following a successful or failed fetch attempt +// +// This method does all the work for appending to and creating log files + +- (void)logFetchWithError:(NSError *)error { + + if (![[self class] isLoggingEnabled]) return; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + + // TODO: add Javascript to display response data formatted in hex + + NSString *logDirectory = [[self class] loggingDirectory]; + NSString *processName = [[self class] loggingProcessName]; + NSString *dateStamp = [[self class] loggingDateStamp]; + + // each response's NSData goes into its own xml or txt file, though all + // responses for this run of the app share a main html file. This + // counter tracks all fetch responses for this run of the app. + static int zResponseCounter = 0; + zResponseCounter++; + + // file name for the html file containing plain text in a <textarea> + NSString *responseDataUnformattedFileName = nil; + + // file name for the "formatted" (raw) data file + NSString *responseDataFormattedFileName = nil; + NSUInteger responseDataLength = [downloadedData_ length]; + + NSURLResponse *response = [self response]; + NSString *responseBaseName = nil; + + // if there's response data, decide what kind of file to put it in based + // on the first bytes of the file or on the mime type supplied by the server + if (responseDataLength) { + NSString *responseDataExtn = nil; + + // generate a response file base name like + // SyncProto_http_response_10-16_01-56-58PM_3 + responseBaseName = [NSString stringWithFormat:@"%@_http_response_%@_%d", + processName, dateStamp, zResponseCounter]; + + NSString *dataStr = [[[NSString alloc] initWithData:downloadedData_ + encoding:NSUTF8StringEncoding] autorelease]; + if (dataStr) { + // we were able to make a UTF-8 string from the response data + + NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceCharacterSet]; + dataStr = [dataStr stringByTrimmingCharactersInSet:whitespaceSet]; + + // save a plain-text version of the response data in an html cile + // containing a wrapped, scrollable <textarea> + // + // we'll use <textarea rows="33" cols="108" readonly=true wrap=soft> + // </textarea> to fit inside our iframe + responseDataUnformattedFileName = [responseBaseName stringByAppendingPathExtension:@"html"]; + NSString *textFilePath = [logDirectory stringByAppendingPathComponent:responseDataUnformattedFileName]; + + NSString* wrapFmt = @"<textarea rows=\"33\" cols=\"108\" readonly=true" + " wrap=soft>\n%@\n</textarea>"; + NSString* wrappedStr = [NSString stringWithFormat:wrapFmt, dataStr]; + [wrappedStr writeToFile:textFilePath + atomically:NO + encoding:NSUTF8StringEncoding + error:nil]; + + // now determine the extension for the "formatted" file, which is really + // the raw data written with an appropriate extension + + // for known file types, we'll write the data to a file with the + // appropriate extension + if ([dataStr hasPrefix:@"<?xml"]) { + responseDataExtn = @"xml"; + } else if ([dataStr hasPrefix:@"<html"]) { + responseDataExtn = @"html"; + } else { + // add more types of identifiable text here + } + + } else if ([[response MIMEType] isEqual:@"image/jpeg"]) { + responseDataExtn = @"jpg"; + } else if ([[response MIMEType] isEqual:@"image/gif"]) { + responseDataExtn = @"gif"; + } else if ([[response MIMEType] isEqual:@"image/png"]) { + responseDataExtn = @"png"; + } else { + // add more non-text types here + } + + // if we have an extension, save the raw data in a file with that + // extension to be our "formatted" display file + if (responseDataExtn) { + responseDataFormattedFileName = [responseBaseName stringByAppendingPathExtension:responseDataExtn]; + NSString *formattedFilePath = [logDirectory stringByAppendingPathComponent:responseDataFormattedFileName]; + + [downloadedData_ writeToFile:formattedFilePath atomically:NO]; + } + } + + // we'll have one main html file per run of the app + NSString *htmlName = [NSString stringWithFormat:@"%@_http_log_%@.html", + processName, dateStamp]; + NSString *htmlPath =[logDirectory stringByAppendingPathComponent:htmlName]; + + // if the html file exists (from logging previous fetches) we don't need + // to re-write the header or the scripts + BOOL didFileExist = [fileManager fileExistsAtPath:htmlPath]; + + NSMutableString* outputHTML = [NSMutableString string]; + NSURLRequest *request = [self request]; + + // we need file names for the various div's that we're going to show and hide, + // names unique to this response's bundle of data, so we format our div + // names with the counter that we incremented earlier + NSString *requestHeadersName = [NSString stringWithFormat:@"RequestHeaders%d", zResponseCounter]; + NSString *postDataName = [NSString stringWithFormat:@"PostData%d", zResponseCounter]; + + NSString *responseHeadersName = [NSString stringWithFormat:@"ResponseHeaders%d", zResponseCounter]; + NSString *responseDataDivName = [NSString stringWithFormat:@"ResponseData%d", zResponseCounter]; + NSString *dataIFrameID = [NSString stringWithFormat:@"DataIFrame%d", zResponseCounter]; + + // we need a header to say we'll have UTF-8 text + if (!didFileExist) { + [outputHTML appendFormat:@"<html><head><meta http-equiv=\"content-type\" " + "content=\"text/html; charset=UTF-8\"><title>%@ HTTP fetch log %@</title>", + processName, dateStamp]; + } + + // write style sheets for each hideable element; each style sheet is + // customized with our current response number, since they'll share + // the html page with other responses + NSString *styleFormat = @"<style type=\"text/css\">div#%@ " + "{ margin: 0px 20px 0px 20px; display: none; }</style>\n"; + + [outputHTML appendFormat:styleFormat, requestHeadersName]; + [outputHTML appendFormat:styleFormat, postDataName]; + [outputHTML appendFormat:styleFormat, responseHeadersName]; + [outputHTML appendFormat:styleFormat, responseDataDivName]; + + if (!didFileExist) { + // write javascript functions. The first one shows/hides the layer + // containing the iframe. + NSString *scriptFormat = @"<script type=\"text/javascript\"> " + "function toggleLayer(whichLayer){ var style2 = document.getElementById(whichLayer).style; " + "style2.display = style2.display ? \"\":\"block\";}</script>\n"; + [outputHTML appendFormat:scriptFormat]; + + // the second function is passed the src file; if it's what's shown, it + // toggles the iframe's visibility. If some other src is shown, it shows + // the iframe and loads the new source. Note we want to load the source + // whenever we show the iframe too since Firefox seems to format it wrong + // when showing it if we don't reload it. + NSString *toggleIFScriptFormat = @"<script type=\"text/javascript\"> " + "function toggleIFrame(whichLayer,iFrameID,newsrc)" + "{ \n var iFrameElem=document.getElementById(iFrameID); " + "if (iFrameElem.src.indexOf(newsrc) != -1) { toggleLayer(whichLayer); } " + "else { document.getElementById(whichLayer).style.display=\"block\"; } " + "iFrameElem.src=newsrc; }</script>\n</head>\n<body>\n"; + [outputHTML appendFormat:toggleIFScriptFormat]; + } + + // now write the visible html elements + + // write the date & time + [outputHTML appendFormat:@"<b>%@</b><br>", [[NSDate date] description]]; + + // write the request URL + [outputHTML appendFormat:@"<b>request:</b> %@ <i>URL:</i> <code>%@</code><br>\n", + [request HTTPMethod], [request URL]]; + + // write the request headers, toggleable + NSDictionary *requestHeaders = [request allHTTPHeaderFields]; + if ([requestHeaders count]) { + NSString *requestHeadersFormat = @"<a href=\"javascript:toggleLayer('%@');\">" + "request headers (%d)</a><div id=\"%@\"><pre>%@</pre></div><br>\n"; + [outputHTML appendFormat:requestHeadersFormat, + requestHeadersName, // layer name + [requestHeaders count], + requestHeadersName, + [requestHeaders description]]; // description gives a human-readable dump + } else { + [outputHTML appendString:@"<i>Request headers: none</i><br>"]; + } + + // write the request post data, toggleable + NSData *postData = postData_; + if (loggedStreamData_) { + postData = loggedStreamData_; + } + + if ([postData length]) { + NSString *postDataFormat = @"<a href=\"javascript:toggleLayer('%@');\">" + "posted data (%d bytes)</a><div id=\"%@\">%@</div><br>\n"; + NSString *postDataStr = [self stringFromStreamData:postData]; + if (postDataStr) { + NSString *postDataTextAreaFmt = @"<pre>%@</pre>"; + if ([postDataStr rangeOfString:@"<"].location != NSNotFound) { + postDataTextAreaFmt = @"<textarea rows=\"15\" cols=\"100\"" + " readonly=true wrap=soft>\n%@\n</textarea>"; + } + NSString *cleanedPostData = [self cleanParameterFollowing:@"&Passwd=" + fromString:postDataStr]; + NSString *postDataTextArea = [NSString stringWithFormat: + postDataTextAreaFmt, cleanedPostData]; + + [outputHTML appendFormat:postDataFormat, + postDataName, // layer name + [postData length], + postDataName, + postDataTextArea]; + } + } else { + // no post data + } + + // write the response status, MIME type, URL + if (response) { + NSString *statusString = @""; + if ([response respondsToSelector:@selector(statusCode)]) { + NSInteger status = [(NSHTTPURLResponse *)response statusCode]; + statusString = @"200"; + if (status != 200) { + // purple for errors + statusString = [NSString stringWithFormat:@"<FONT COLOR=\"#FF00FF\">%d</FONT>", + status]; + } + } + + // show the response URL only if it's different from the request URL + NSString *responseURLStr = @""; + NSURL *responseURL = [response URL]; + + if (responseURL && ![responseURL isEqual:[request URL]]) { + NSString *responseURLFormat = @"<br><FONT COLOR=\"#FF00FF\">response URL:" + "</FONT> <code>%@</code>"; + responseURLStr = [NSString stringWithFormat:responseURLFormat, + [responseURL absoluteString]]; + } + + NSDictionary *responseHeaders = nil; + if ([response respondsToSelector:@selector(allHeaderFields)]) { + responseHeaders = [(NSHTTPURLResponse *)response allHeaderFields]; + } + [outputHTML appendFormat:@"<b>response:</b> <i>status:</i> %@ <i> " + " MIMEType:</i><code> %@</code>%@<br>\n", + statusString, + [response MIMEType], + responseURLStr, + responseHeaders ? [responseHeaders description] : @""]; + + // write the response headers, toggleable + if ([responseHeaders count]) { + + NSString *cookiesSet = [responseHeaders objectForKey:@"Set-Cookie"]; + + NSString *responseHeadersFormat = @"<a href=\"javascript:toggleLayer(" + "'%@');\">response headers (%d) %@</a><div id=\"%@\"><pre>%@</pre>" + "</div><br>\n"; + [outputHTML appendFormat:responseHeadersFormat, + responseHeadersName, + [responseHeaders count], + (cookiesSet ? @"<i>sets cookies</i>" : @""), + responseHeadersName, + [responseHeaders description]]; + + } else { + [outputHTML appendString:@"<i>Response headers: none</i><br>\n"]; + } + } + + // error + if (error) { + [outputHTML appendFormat:@"<b>error:</b> %@ <br>\n", [error description]]; + } + + // write the response data. We have links to show formatted and text + // versions, but they both show it in the same iframe, and both + // links also toggle visible/hidden + if (responseDataFormattedFileName || responseDataUnformattedFileName) { + + // response data, toggleable links -- formatted and text versions + if (responseDataFormattedFileName) { + [outputHTML appendFormat:@"response data (%d bytes) formatted <b>%@</b> ", + responseDataLength, + [responseDataFormattedFileName pathExtension]]; + + // inline (iframe) link + NSString *responseInlineFormattedDataNameFormat = @" <a " + "href=\"javascript:toggleIFrame('%@','%@','%@');\">inline</a>\n"; + [outputHTML appendFormat:responseInlineFormattedDataNameFormat, + responseDataDivName, // div ID + dataIFrameID, // iframe ID (for reloading) + responseDataFormattedFileName]; // src to reload + + // plain link (so the user can command-click it into another tab) + [outputHTML appendFormat:@" <a href=\"%@\">stand-alone</a><br>\n", + [responseDataFormattedFileName + stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + } + if (responseDataUnformattedFileName) { + [outputHTML appendFormat:@"response data (%d bytes) plain text ", + responseDataLength]; + + // inline (iframe) link + NSString *responseInlineDataNameFormat = @" <a href=\"" + "javascript:toggleIFrame('%@','%@','%@');\">inline</a> \n"; + [outputHTML appendFormat:responseInlineDataNameFormat, + responseDataDivName, // div ID + dataIFrameID, // iframe ID (for reloading) + responseDataUnformattedFileName]; // src to reload + + // plain link (so the user can command-click it into another tab) + [outputHTML appendFormat:@" <a href=\"%@\">stand-alone</a><br>\n", + [responseDataUnformattedFileName + stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + } + + // make the iframe + NSString *divHTMLFormat = @"<div id=\"%@\">%@</div><br>\n"; + NSString *src = responseDataFormattedFileName ? + responseDataFormattedFileName : responseDataUnformattedFileName; + NSString *escapedSrc = [src stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSString *iframeFmt = @" <iframe src=\"%@\" id=\"%@\" width=800 height=400>" + "\n<a href=\"%@\">%@</a>\n </iframe>\n"; + NSString *dataIFrameHTML = [NSString stringWithFormat:iframeFmt, + escapedSrc, dataIFrameID, escapedSrc, src]; + [outputHTML appendFormat:divHTMLFormat, + responseDataDivName, dataIFrameHTML]; + } else { + // could not parse response data; just show the length of it + [outputHTML appendFormat:@"<i>Response data: %d bytes </i>\n", + responseDataLength]; + } + + [outputHTML appendString:@"<br><hr><p>"]; + + // append the HTML to the main output file + const char* htmlBytes = [outputHTML UTF8String]; + NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:htmlPath + append:YES]; + [stream open]; + [stream write:(const uint8_t *) htmlBytes maxLength:strlen(htmlBytes)]; + [stream close]; + + // make a symlink to the latest html + NSString *symlinkName = [NSString stringWithFormat:@"%@_http_log_newest.html", + processName]; + NSString *symlinkPath = [logDirectory stringByAppendingPathComponent:symlinkName]; + + // removeFileAtPath might be going away, but removeItemAtPath does not exist + // in 10.4 + if ([fileManager respondsToSelector:@selector(removeFileAtPath:handler:)]) { + [fileManager removeFileAtPath:symlinkPath handler:nil]; + } else if ([fileManager respondsToSelector:@selector(removeItemAtPath:error:)]) { + // To make the next line compile when targeting 10.4, we declare + // removeItemAtPath:error: in an @interface above + [fileManager removeItemAtPath:symlinkPath error:NULL]; + } + + [fileManager createSymbolicLinkAtPath:symlinkPath pathContent:htmlPath]; +} + +- (void)logCapturePostStream { + +#if GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING + // This is called when beginning a fetch. The caller should have already + // verified that logging is enabled, and should have allocated + // loggedStreamData_ as a mutable object. + + // If we're logging, we need to wrap the upload stream with our monitor + // stream subclass that will call us back with the bytes being read from the + // stream + + // our wrapper will retain the old post stream + [postStream_ autorelease]; + + // length can be + postStream_ = [GTMInputStreamLogger inputStreamWithStream:postStream_ + length:0]; + [postStream_ retain]; + + // we don't really want monitoring callbacks; our subclass will be + // calling our appendLoggedStreamData: method at every read instead + [(GTMInputStreamLogger *)postStream_ setMonitorDelegate:self + selector:nil]; +#endif // GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING +} + +- (void)appendLoggedStreamData:(NSData *)newData { + [loggedStreamData_ appendData:newData]; +} + +#endif // GTM_HTTPFETCHER_ENABLE_LOGGING +@end + +#if GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING +@implementation GTMInputStreamLogger +- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len { + + // capture the read stream data, and pass it to the delegate to append to + NSInteger result = [super read:buffer maxLength:len]; + if (result >= 0) { + NSData *data = [NSData dataWithBytes:buffer length:result]; + [monitorDelegate_ appendLoggedStreamData:data]; + } + return result; +} +@end +#endif // GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING diff --git a/Foundation/GTMHTTPFetcherTest.m b/Foundation/GTMHTTPFetcherTest.m new file mode 100644 index 0000000..16c8d0f --- /dev/null +++ b/Foundation/GTMHTTPFetcherTest.m @@ -0,0 +1,543 @@ +// +// GTMHTTPFetcherTest.m +// +// Copyright 2007-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +#import <SenTestingKit/SenTestingKit.h> +#import <unistd.h> +#import "GTMHTTPFetcher.h" +#import "GTMSenTestCase.h" + +@interface GTMHTTPFetcherTest : SenTestCase { + // these ivars are checked after fetches, and are reset by resetFetchResponse + NSData *fetchedData_; + NSError *fetcherError_; + NSInteger fetchedStatus_; + NSURLResponse *fetchedResponse_; + NSMutableURLRequest *fetchedRequest_; + + // setup/teardown ivars + NSMutableDictionary *fetchHistory_; + NSTask *server_; // python http server + BOOL didServerLaunch_; // Tracks the state of our server + BOOL didServerDie_; + NSMutableData *launchBuffer_; // Storage for output from our server +} +@end + +@interface GTMHTTPFetcherTest (PrivateMethods) +- (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString + cachingDatedData:(BOOL)doCaching; + +- (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString + cachingDatedData:(BOOL)doCaching + retrySelector:(SEL)retrySel + maxRetryInterval:(NSTimeInterval)maxRetryInterval + userData:(id)userData; + +- (NSString *)fileURLStringToTestFileName:(NSString *)name; +@end + +@implementation GTMHTTPFetcherTest + +static const int kServerPortNumber = 54579; +static const NSTimeInterval kRunLoopInterval = 0.01; +// The bogus-fetch test can take >10s to pass. Pick something way higher +// to avoid failing. +static const NSTimeInterval kGiveUpInterval = 60.0; // bail on the test if 60 seconds elapse + +static NSString *const kValidFileName = @"GTMHTTPFetcherTestPage.html"; + +- (void)gotData:(NSNotification*)notification { + // our server sends out a string to confirm that it launched + NSFileHandle *handle = [notification object]; + NSData *launchMessageData = [handle availableData]; + [launchBuffer_ appendData:launchMessageData]; + NSString *launchStr = + [[[NSString alloc] initWithData:launchBuffer_ + encoding:NSUTF8StringEncoding] autorelease]; + didServerLaunch_ = + [launchStr rangeOfString:@"started GTMHTTPFetcherTestServer"].location != NSNotFound; + if (!didServerLaunch_) { + _GTMDevLog(@"gotData launching httpserver: %@", launchStr); + [handle readInBackgroundAndNotify]; + } +} + +- (void)didDie:(NSNotification*)notification { + _GTMDevLog(@"server died"); + didServerDie_ = YES; +} + + +- (void)setUp { + fetchHistory_ = [[NSMutableDictionary alloc] init]; + + // run the python http server, located in the Tests directory + NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; + STAssertNotNil(testBundle, nil); + + NSString *serverPath = + [testBundle pathForResource:@"GTMHTTPFetcherTestServer" ofType:@""]; + STAssertNotNil(serverPath, nil); + + NSArray *argArray = [NSArray arrayWithObjects:serverPath, + @"-p", [NSString stringWithFormat:@"%d", kServerPortNumber], + @"-r", [serverPath stringByDeletingLastPathComponent], nil]; + + server_ = [[NSTask alloc] init]; + [server_ setArguments:argArray]; + [server_ setLaunchPath:@"/usr/bin/python"]; + [server_ setEnvironment:[NSDictionary dictionary]]; // don't inherit anything from us + + // pipes will be cleaned up when server_ is torn down. + NSPipe *outputPipe = [NSPipe pipe]; + NSPipe *errorPipe = [NSPipe pipe]; + + [server_ setStandardOutput:outputPipe]; + [server_ setStandardError:errorPipe]; + + NSFileHandle *outputHandle = [outputPipe fileHandleForReading]; + NSFileHandle *errorHandle = [errorPipe fileHandleForReading]; + + didServerLaunch_ = NO; + didServerDie_ = NO; + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(gotData:) + name:NSFileHandleDataAvailableNotification + object:outputHandle]; + [center addObserver:self + selector:@selector(gotData:) + name:NSFileHandleDataAvailableNotification + object:errorHandle]; + [center addObserver:self + selector:@selector(didDie:) + name:NSTaskDidTerminateNotification + object:server_]; + + [launchBuffer_ autorelease]; + launchBuffer_ = [[NSMutableData data] retain]; + [outputHandle waitForDataInBackgroundAndNotify]; + [errorHandle waitForDataInBackgroundAndNotify]; + [server_ launch]; + + NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:kGiveUpInterval]; + while ((!didServerDie_ && !didServerLaunch_) && + [giveUpDate timeIntervalSinceNow] > 0) { + NSDate* loopIntervalDate = + [NSDate dateWithTimeIntervalSinceNow:kRunLoopInterval]; + [[NSRunLoop currentRunLoop] runUntilDate:loopIntervalDate]; + } + + [center removeObserver:self]; + + STAssertTrue(didServerLaunch_ && [server_ isRunning] && !didServerDie_, + @"Python http server not launched.\n" + "Args:%@\n" + "Environment:%@\n", + [argArray componentsJoinedByString:@" "], [server_ environment]); +} + +- (void)resetFetchResponse { + [fetchedData_ release]; + fetchedData_ = nil; + + [fetcherError_ release]; + fetcherError_ = nil; + + [fetchedRequest_ release]; + fetchedRequest_ = nil; + + [fetchedResponse_ release]; + fetchedResponse_ = nil; + + fetchedStatus_ = 0; +} + +- (void)tearDown { + [server_ terminate]; + [server_ waitUntilExit]; + [server_ release]; + server_ = nil; + [launchBuffer_ release]; + launchBuffer_ = nil; + [self resetFetchResponse]; + + [fetchHistory_ release]; + fetchHistory_ = nil; +} + +- (void)testValidFetch { + + NSString *urlString = [self fileURLStringToTestFileName:kValidFileName]; + + [self doFetchWithURLString:urlString cachingDatedData:YES]; + + STAssertNotNil(fetchedData_, + @"failed to fetch data, status:%ld error:%@, URL:%@", + (long)fetchedStatus_, fetcherError_, urlString); + STAssertNotNil(fetchedResponse_, + @"failed to get fetch response, status:%ld error:%@", + (long)fetchedStatus_, fetcherError_); + STAssertNotNil(fetchedRequest_, @"failed to get fetch request, URL %@", + urlString); + STAssertNil(fetcherError_, @"fetching data gave error: %@", fetcherError_); + STAssertEquals(fetchedStatus_, (NSInteger)200, + @"fetching data expected status 200, instead got %ld, for URL %@", + (long)fetchedStatus_, urlString); + + // no cookies should be sent with our first request + NSDictionary *headers = [fetchedRequest_ allHTTPHeaderFields]; + NSString *cookiesSent = [headers objectForKey:@"Cookie"]; + STAssertNil(cookiesSent, @"Cookies sent unexpectedly: %@", cookiesSent); + + + // cookies should have been set by the response; specifically, TestCookie + // should be set to the name of the file requested + NSDictionary *responseHeaders; + + responseHeaders = [(NSHTTPURLResponse *)fetchedResponse_ allHeaderFields]; + NSString *cookiesSetString = [responseHeaders objectForKey:@"Set-Cookie"]; + NSString *cookieExpected = [NSString stringWithFormat:@"TestCookie=%@", + kValidFileName]; + STAssertEqualObjects(cookiesSetString, cookieExpected, @"Unexpected cookie"); + + // make a copy of the fetched data to compare with our next fetch from the + // cache + NSData *originalFetchedData = [[fetchedData_ copy] autorelease]; + + // Now fetch again so the "If modified since" header will be set (because + // we're calling setFetchHistory: below) and caching ON, and verify that we + // got a good data from the cache, along with a "Not modified" status + + [self resetFetchResponse]; + + [self doFetchWithURLString:urlString cachingDatedData:YES]; + + STAssertEqualObjects(fetchedData_, originalFetchedData, + @"cache data mismatch"); + + STAssertNotNil(fetchedData_, + @"failed to fetch data, status:%ld error:%@, URL:%@", + (long)fetchedStatus_, fetcherError_, urlString); + STAssertNotNil(fetchedResponse_, + @"failed to get fetch response, status:%;d error:%@", + (long)fetchedStatus_, fetcherError_); + STAssertNotNil(fetchedRequest_, @"failed to get fetch request, URL %@", + urlString); + STAssertNil(fetcherError_, @"fetching data gave error: %@", fetcherError_); + + STAssertEquals(fetchedStatus_, (NSInteger)kGTMHTTPFetcherStatusNotModified, // 304 + @"fetching data expected status 304, instead got %ld, for URL %@", + (long)fetchedStatus_, urlString); + + // the TestCookie set previously should be sent with this request + cookiesSent = [[fetchedRequest_ allHTTPHeaderFields] objectForKey:@"Cookie"]; + STAssertEqualObjects(cookiesSent, cookieExpected, @"Cookie not sent"); + + // Now fetch twice without caching enabled, and verify that we got a + // "Not modified" status, along with a non-nil but empty NSData (which + // is normal for that status code) + + [self resetFetchResponse]; + + [fetchHistory_ removeAllObjects]; + + [self doFetchWithURLString:urlString cachingDatedData:NO]; + + STAssertEqualObjects(fetchedData_, originalFetchedData, + @"cache data mismatch"); + + [self resetFetchResponse]; + [self doFetchWithURLString:urlString cachingDatedData:NO]; + + STAssertNotNil(fetchedData_, @""); + STAssertEquals([fetchedData_ length], (NSUInteger)0, @"unexpected data"); + STAssertEquals(fetchedStatus_, (NSInteger)kGTMHTTPFetcherStatusNotModified, + @"fetching data expected status 304, instead got %d", fetchedStatus_); + STAssertNil(fetcherError_, @"unexpected error: %@", fetcherError_); + +} + +- (void)testBogusFetch { + // fetch a live, invalid URL + NSString *badURLString = @"http://localhost:86/"; + [self doFetchWithURLString:badURLString cachingDatedData:NO]; + + const int kServiceUnavailableStatus = 503; + + if (fetchedStatus_ == kServiceUnavailableStatus) { + // some proxies give a "service unavailable" error for bogus fetches + } else { + + if (fetchedData_) { + NSString *str = [[[NSString alloc] initWithData:fetchedData_ + encoding:NSUTF8StringEncoding] autorelease]; + STAssertNil(fetchedData_, @"fetched unexpected data: %@", str); + } + + STAssertNotNil(fetcherError_, @"failed to receive fetching error"); + STAssertEquals(fetchedStatus_, (NSInteger)0, + @"fetching data expected no status from no response, instead got %d", + fetchedStatus_); + } + + // fetch with a specific status code from our http server + [self resetFetchResponse]; + + NSString *invalidWebPageFile = + [kValidFileName stringByAppendingString:@"?status=400"]; + NSString *statusUrlString = + [self fileURLStringToTestFileName:invalidWebPageFile]; + + [self doFetchWithURLString:statusUrlString cachingDatedData:NO]; + + STAssertNotNil(fetchedData_, @"fetch lacked data with error info"); + STAssertNil(fetcherError_, @"expected bad status but got an error"); + STAssertEquals(fetchedStatus_, (NSInteger)400, + @"unexpected status, error=%@", fetcherError_); +} + +- (void)testRetryFetches { + + GTMHTTPFetcher *fetcher; + + NSString *invalidFile = [kValidFileName stringByAppendingString:@"?status=503"]; + NSString *urlString = [self fileURLStringToTestFileName:invalidFile]; + + SEL countRetriesSel = @selector(countRetriesFetcher:willRetry:forError:); + SEL fixRequestSel = @selector(fixRequestFetcher:willRetry:forError:); + + // + // test: retry until timeout, then expect failure with status message + // + + NSNumber *lotsOfRetriesNumber = [NSNumber numberWithInt:1000]; + + fetcher= [self doFetchWithURLString:urlString + cachingDatedData:NO + retrySelector:countRetriesSel + maxRetryInterval:5.0 // retry intervals of 1, 2, 4 + userData:lotsOfRetriesNumber]; + + STAssertNotNil(fetchedData_, @"error data is expected"); + STAssertEquals(fetchedStatus_, (NSInteger)503, nil); + STAssertEquals([fetcher retryCount], 3U, @"retry count unexpected"); + + // + // test: retry twice, then give up + // + [self resetFetchResponse]; + + NSNumber *twoRetriesNumber = [NSNumber numberWithInt:2]; + + fetcher= [self doFetchWithURLString:urlString + cachingDatedData:NO + retrySelector:countRetriesSel + maxRetryInterval:10.0 // retry intervals of 1, 2, 4, 8 + userData:twoRetriesNumber]; + + STAssertNotNil(fetchedData_, @"error data is expected"); + STAssertEquals(fetchedStatus_, (NSInteger)503, nil); + STAssertEquals([fetcher retryCount], 2U, @"retry count unexpected"); + + + // + // test: retry, making the request succeed on the first retry + // by fixing the URL + // + [self resetFetchResponse]; + + fetcher= [self doFetchWithURLString:urlString + cachingDatedData:NO + retrySelector:fixRequestSel + maxRetryInterval:30.0 // should only retry once due to selector + userData:lotsOfRetriesNumber]; + + STAssertNotNil(fetchedData_, @"data is expected"); + STAssertEquals(fetchedStatus_, (NSInteger)200, nil); + STAssertEquals([fetcher retryCount], 1U, @"retry count unexpected"); +} + +#pragma mark - + +- (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString + cachingDatedData:(BOOL)doCaching { + + return [self doFetchWithURLString:(NSString *)urlString + cachingDatedData:(BOOL)doCaching + retrySelector:nil + maxRetryInterval:0 + userData:nil]; +} + +- (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString + cachingDatedData:(BOOL)doCaching + retrySelector:(SEL)retrySel + maxRetryInterval:(NSTimeInterval)maxRetryInterval + userData:(id)userData { + + NSURL *url = [NSURL URLWithString:urlString]; + NSURLRequest *req = [NSURLRequest requestWithURL:url + cachePolicy:NSURLRequestReloadIgnoringCacheData + timeoutInterval:kGiveUpInterval]; + GTMHTTPFetcher *fetcher = [GTMHTTPFetcher httpFetcherWithRequest:req]; + + STAssertNotNil(fetcher, @"Failed to allocate fetcher"); + + // setting the fetch history will add the "If-modified-since" header + // to repeat requests + [fetcher setFetchHistory:fetchHistory_]; + if (doCaching != [fetcher shouldCacheDatedData]) { + // only set the value when it changes since setting it to nil clears out + // some of the state and our tests need the state between some non caching + // fetches. + [fetcher setShouldCacheDatedData:doCaching]; + } + + if (retrySel) { + [fetcher setIsRetryEnabled:YES]; + [fetcher setRetrySelector:retrySel]; + [fetcher setMaxRetryInterval:maxRetryInterval]; + [fetcher setUserData:userData]; + + // we force a minimum retry interval for unit testing; otherwise, + // we'd have no idea how many retries will occur before the max + // retry interval occurs, since the minimum would be random + [fetcher setMinRetryInterval:1.0]; + } + + BOOL isFetching = + [fetcher beginFetchWithDelegate:self + didFinishSelector:@selector(testFetcher:finishedWithData:) + didFailSelector:@selector(testFetcher:failedWithError:)]; + STAssertTrue(isFetching, @"Begin fetch failed"); + + if (isFetching) { + + // Give time for the fetch to happen, but give up if 10 seconds elapse with + // no response + NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:kGiveUpInterval]; + while ((!fetchedData_ && !fetcherError_) && + [giveUpDate timeIntervalSinceNow] > 0) { + NSDate* loopIntervalDate = + [NSDate dateWithTimeIntervalSinceNow:kRunLoopInterval]; + [[NSRunLoop currentRunLoop] runUntilDate:loopIntervalDate]; + } + } + + return fetcher; +} + +- (NSString *)fileURLStringToTestFileName:(NSString *)name { + + // we need to create http URLs referring to the desired + // resource to be found by the python http server running locally + + // return a localhost:port URL for the test file + NSString *urlString = [NSString stringWithFormat:@"http://localhost:%d/%@", + kServerPortNumber, name]; + + + // we exclude the "?status=" that would indicate that the URL + // should cause a retryable error + NSRange range = [name rangeOfString:@"?status="]; + if (range.length > 0) { + name = [name substringToIndex:range.location]; + } + + // we exclude the ".auth" extension that would indicate that the URL + // should be tested with authentication + if ([[name pathExtension] isEqual:@"auth"]) { + name = [name stringByDeletingPathExtension]; + } + + // just for sanity, let's make sure we see the file locally, so + // we can expect the Python http server to find it too + NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; + STAssertNotNil(testBundle, nil); + + NSString *filePath = + [testBundle pathForResource:[name stringByDeletingPathExtension] + ofType:[name pathExtension]]; + STAssertNotNil(filePath, nil); + + return urlString; +} + + + +- (void)testFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data { + fetchedData_ = [data copy]; + fetchedStatus_ = [fetcher statusCode]; // this implicitly tests that the fetcher has kept the response + fetchedRequest_ = [[fetcher request] retain]; + fetchedResponse_ = [[fetcher response] retain]; +} + +- (void)testFetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error { + // if it's a status error, don't hang onto the error, just the status/data + if ([[error domain] isEqual:kGTMHTTPFetcherStatusDomain]) { + fetchedData_ = [[[error userInfo] objectForKey:kGTMHTTPFetcherStatusDataKey] copy]; + fetchedStatus_ = [error code]; // this implicitly tests that the fetcher has kept the response + } else { + fetcherError_ = [error retain]; + fetchedStatus_ = [fetcher statusCode]; + } +} + + +// Selector for allowing up to N retries, where N is an NSNumber in the +// fetcher's userData +- (BOOL)countRetriesFetcher:(GTMHTTPFetcher *)fetcher + willRetry:(BOOL)suggestedWillRetry + forError:(NSError *)error { + + int count = [fetcher retryCount]; + int allowedRetryCount = [[fetcher userData] intValue]; + + BOOL shouldRetry = (count < allowedRetryCount); + + STAssertEquals([fetcher nextRetryInterval], pow(2.0, [fetcher retryCount]), + @"unexpected next retry interval (expected %f, was %f)", + pow(2.0, [fetcher retryCount]), + [fetcher nextRetryInterval]); + + return shouldRetry; +} + +// Selector for retrying and changing the request to one that will succeed +- (BOOL)fixRequestFetcher:(GTMHTTPFetcher *)fetcher + willRetry:(BOOL)suggestedWillRetry + forError:(NSError *)error { + + STAssertEquals([fetcher nextRetryInterval], pow(2.0, [fetcher retryCount]), + @"unexpected next retry interval (expected %f, was %f)", + pow(2.0, [fetcher retryCount]), + [fetcher nextRetryInterval]); + + // fix it - change the request to a URL which does not have a status value + NSString *urlString = [self fileURLStringToTestFileName:kValidFileName]; + + NSURL *url = [NSURL URLWithString:urlString]; + [fetcher setRequest:[NSURLRequest requestWithURL:url]]; + + return YES; // do the retry fetch; it should succeed now +} + +@end + diff --git a/Foundation/GTMNSData+zlib.h b/Foundation/GTMNSData+zlib.h index 2c937c9..df31d24 100644 --- a/Foundation/GTMNSData+zlib.h +++ b/Foundation/GTMNSData+zlib.h @@ -17,6 +17,7 @@ // #import <Foundation/Foundation.h> +#import "GTMDefines.h" /// Helpers for dealing w/ zlib inflate/deflate calls. @interface NSData (GTMZLibAdditions) @@ -25,7 +26,7 @@ // // Uses the default compression level. + (NSData *)gtm_dataByGzippingBytes:(const void *)bytes - length:(unsigned)length; + length:(NSUInteger)length; /// Return an autoreleased NSData w/ the result of gzipping the payload of |data|. // @@ -36,7 +37,7 @@ // // |level| can be 1-9, any other values will be clipped to that range. + (NSData *)gtm_dataByGzippingBytes:(const void *)bytes - length:(unsigned)length + length:(NSUInteger)length compressionLevel:(int)level; /// Return an autoreleased NSData w/ the result of gzipping the payload of |data| using |level| compression level. @@ -50,7 +51,7 @@ // // Uses the default compression level. + (NSData *)gtm_dataByDeflatingBytes:(const void *)bytes - length:(unsigned)length; + length:(NSUInteger)length; /// Return an autoreleased NSData w/ the result of deflating the payload of |data|. // @@ -61,7 +62,7 @@ // // |level| can be 1-9, any other values will be clipped to that range. + (NSData *)gtm_dataByDeflatingBytes:(const void *)bytes - length:(unsigned)length + length:(NSUInteger)length compressionLevel:(int)level; /// Return an autoreleased NSData w/ the result of deflating the payload of |data| using |level| compression level. @@ -73,7 +74,7 @@ // // The bytes to decompress can be zlib or gzip payloads. + (NSData *)gtm_dataByInflatingBytes:(const void *)bytes - length:(unsigned)length; + length:(NSUInteger)length; /// Return an autoreleased NSData w/ the result of decompressing the payload of |data|. // diff --git a/Foundation/GTMNSData+zlib.m b/Foundation/GTMNSData+zlib.m index 514477f..8ba1ddc 100644 --- a/Foundation/GTMNSData+zlib.m +++ b/Foundation/GTMNSData+zlib.m @@ -24,19 +24,25 @@ @interface NSData (GTMZlibAdditionsPrivate) + (NSData *)gtm_dataByCompressingBytes:(const void *)bytes - length:(unsigned)length + length:(NSUInteger)length compressionLevel:(int)level useGzip:(BOOL)useGzip; @end @implementation NSData (GTMZlibAdditionsPrivate) + (NSData *)gtm_dataByCompressingBytes:(const void *)bytes - length:(unsigned)length + length:(NSUInteger)length compressionLevel:(int)level useGzip:(BOOL)useGzip { if (!bytes || !length) { return nil; } + + // TODO: support 64bit inputs + // avail_in is a uInt, so if length > UINT_MAX we actually need to loop + // feeding the data until we've gotten it all in. not supporting this + // at the moment. + _GTMDevAssert(length <= UINT_MAX, @"Currently don't support >32bit lengths"); if (level == Z_DEFAULT_COMPRESSION) { // the default value is actually outside the range, so we have to let it @@ -70,7 +76,7 @@ unsigned char output[kChunkSize]; // setup the input - strm.avail_in = length; + strm.avail_in = (unsigned int)length; strm.next_in = (unsigned char*)bytes; // loop to collect the data @@ -119,7 +125,7 @@ @implementation NSData (GTMZLibAdditions) + (NSData *)gtm_dataByGzippingBytes:(const void *)bytes - length:(unsigned)length { + length:(NSUInteger)length { return [self gtm_dataByCompressingBytes:bytes length:length compressionLevel:Z_DEFAULT_COMPRESSION @@ -134,7 +140,7 @@ } // gtm_dataByGzippingData: + (NSData *)gtm_dataByGzippingBytes:(const void *)bytes - length:(unsigned)length + length:(NSUInteger)length compressionLevel:(int)level { return [self gtm_dataByCompressingBytes:bytes length:length @@ -151,7 +157,7 @@ } // gtm_dataByGzippingData:level: + (NSData *)gtm_dataByDeflatingBytes:(const void *)bytes - length:(unsigned)length { + length:(NSUInteger)length { return [self gtm_dataByCompressingBytes:bytes length:length compressionLevel:Z_DEFAULT_COMPRESSION @@ -166,7 +172,7 @@ } // gtm_dataByDeflatingData: + (NSData *)gtm_dataByDeflatingBytes:(const void *)bytes - length:(unsigned)length + length:(NSUInteger)length compressionLevel:(int)level { return [self gtm_dataByCompressingBytes:bytes length:length @@ -183,16 +189,22 @@ } // gtm_dataByDeflatingData:level: + (NSData *)gtm_dataByInflatingBytes:(const void *)bytes - length:(unsigned)length { + length:(NSUInteger)length { if (!bytes || !length) { return nil; } + + // TODO: support 64bit inputs + // avail_in is a uInt, so if length > UINT_MAX we actually need to loop + // feeding the data until we've gotten it all in. not supporting this + // at the moment. + _GTMDevAssert(length <= UINT_MAX, @"Currently don't support >32bit lengths"); z_stream strm; bzero(&strm, sizeof(z_stream)); // setup the input - strm.avail_in = length; + strm.avail_in = (unsigned int)length; strm.next_in = (unsigned char*)bytes; int windowBits = 15; // 15 to enable any window size diff --git a/Foundation/GTMNSData+zlibTest.m b/Foundation/GTMNSData+zlibTest.m index 38dbce3..1d09e35 100644 --- a/Foundation/GTMNSData+zlibTest.m +++ b/Foundation/GTMNSData+zlibTest.m @@ -105,12 +105,12 @@ static BOOL HasGzipHeader(NSData *data) { STAssertNil([NSData gtm_dataByInflatingData:data], nil); // test deflated data runs that end before they are done - for (int x = 1 ; x < [deflated length] ; ++x) { + for (NSUInteger x = 1 ; x < [deflated length] ; ++x) { STAssertNil([NSData gtm_dataByInflatingBytes:[deflated bytes] length:x], nil); } // test gzipped data runs that end before they are done - for (int x = 1 ; x < [gzipped length] ; ++x) { + for (NSUInteger x = 1 ; x < [gzipped length] ; ++x) { STAssertNil([NSData gtm_dataByInflatingBytes:[gzipped bytes] length:x], nil); } @@ -175,21 +175,21 @@ static BOOL HasGzipHeader(NSData *data) { // w/ *Bytes apis, default level NSData *deflated = [NSData gtm_dataByDeflatingBytes:[data bytes] length:[data length]]; STAssertNotNil(deflated, @"failed to deflate data block"); - STAssertGreaterThan([deflated length], 0U, @"failed to deflate data block"); + STAssertGreaterThan([deflated length], (NSUInteger)0, @"failed to deflate data block"); STAssertFalse(HasGzipHeader(deflated), @"has gzip header on zlib data"); NSData *dataPrime = [NSData gtm_dataByInflatingBytes:[deflated bytes] length:[deflated length]]; STAssertNotNil(dataPrime, @"failed to inflate data block"); - STAssertGreaterThan([dataPrime length], 0U, @"failed to inflate data block"); + STAssertGreaterThan([dataPrime length], (NSUInteger)0, @"failed to inflate data block"); STAssertEqualObjects(data, dataPrime, @"failed to round trip via *Bytes apis"); // w/ *Data apis, default level deflated = [NSData gtm_dataByDeflatingData:data]; STAssertNotNil(deflated, @"failed to deflate data block"); - STAssertGreaterThan([deflated length], 0U, @"failed to deflate data block"); + STAssertGreaterThan([deflated length], (NSUInteger)0, @"failed to deflate data block"); STAssertFalse(HasGzipHeader(deflated), @"has gzip header on zlib data"); dataPrime = [NSData gtm_dataByInflatingData:deflated]; STAssertNotNil(dataPrime, @"failed to inflate data block"); - STAssertGreaterThan([dataPrime length], 0U, @"failed to inflate data block"); + STAssertGreaterThan([dataPrime length], (NSUInteger)0, @"failed to inflate data block"); STAssertEqualObjects(data, dataPrime, @"failed to round trip via *Data apis"); // loop over the compression levels @@ -199,21 +199,21 @@ static BOOL HasGzipHeader(NSData *data) { length:[data length] compressionLevel:level]; STAssertNotNil(deflated, @"failed to deflate data block"); - STAssertGreaterThan([deflated length], 0U, @"failed to deflate data block"); + STAssertGreaterThan([deflated length], (NSUInteger)0, @"failed to deflate data block"); STAssertFalse(HasGzipHeader(deflated), @"has gzip header on zlib data"); dataPrime = [NSData gtm_dataByInflatingBytes:[deflated bytes] length:[deflated length]]; STAssertNotNil(dataPrime, @"failed to inflate data block"); - STAssertGreaterThan([dataPrime length], 0U, @"failed to inflate data block"); + STAssertGreaterThan([dataPrime length], (NSUInteger)0, @"failed to inflate data block"); STAssertEqualObjects(data, dataPrime, @"failed to round trip via *Bytes apis"); // w/ *Data apis, using our level deflated = [NSData gtm_dataByDeflatingData:data compressionLevel:level]; STAssertNotNil(deflated, @"failed to deflate data block"); - STAssertGreaterThan([deflated length], 0U, @"failed to deflate data block"); + STAssertGreaterThan([deflated length], (NSUInteger)0, @"failed to deflate data block"); STAssertFalse(HasGzipHeader(deflated), @"has gzip header on zlib data"); dataPrime = [NSData gtm_dataByInflatingData:deflated]; STAssertNotNil(dataPrime, @"failed to inflate data block"); - STAssertGreaterThan([dataPrime length], 0U, @"failed to inflate data block"); + STAssertGreaterThan([dataPrime length], (NSUInteger)0, @"failed to inflate data block"); STAssertEqualObjects(data, dataPrime, @"failed to round trip via *Data apis"); } @@ -240,21 +240,21 @@ static BOOL HasGzipHeader(NSData *data) { // w/ *Bytes apis, default level NSData *gzipped = [NSData gtm_dataByGzippingBytes:[data bytes] length:[data length]]; STAssertNotNil(gzipped, @"failed to gzip data block"); - STAssertGreaterThan([gzipped length], 0U, @"failed to gzip data block"); + STAssertGreaterThan([gzipped length], (NSUInteger)0, @"failed to gzip data block"); STAssertTrue(HasGzipHeader(gzipped), @"doesn't have gzip header on gzipped data"); NSData *dataPrime = [NSData gtm_dataByInflatingBytes:[gzipped bytes] length:[gzipped length]]; STAssertNotNil(dataPrime, @"failed to inflate data block"); - STAssertGreaterThan([dataPrime length], 0U, @"failed to inflate data block"); + STAssertGreaterThan([dataPrime length], (NSUInteger)0, @"failed to inflate data block"); STAssertEqualObjects(data, dataPrime, @"failed to round trip via *Bytes apis"); // w/ *Data apis, default level gzipped = [NSData gtm_dataByGzippingData:data]; STAssertNotNil(gzipped, @"failed to gzip data block"); - STAssertGreaterThan([gzipped length], 0U, @"failed to gzip data block"); + STAssertGreaterThan([gzipped length], (NSUInteger)0, @"failed to gzip data block"); STAssertTrue(HasGzipHeader(gzipped), @"doesn't have gzip header on gzipped data"); dataPrime = [NSData gtm_dataByInflatingData:gzipped]; STAssertNotNil(dataPrime, @"failed to inflate data block"); - STAssertGreaterThan([dataPrime length], 0U, @"failed to inflate data block"); + STAssertGreaterThan([dataPrime length], (NSUInteger)0, @"failed to inflate data block"); STAssertEqualObjects(data, dataPrime, @"failed to round trip via *Data apis"); // loop over the compression levels @@ -264,21 +264,21 @@ static BOOL HasGzipHeader(NSData *data) { length:[data length] compressionLevel:level]; STAssertNotNil(gzipped, @"failed to gzip data block"); - STAssertGreaterThan([gzipped length], 0U, @"failed to gzip data block"); + STAssertGreaterThan([gzipped length], (NSUInteger)0, @"failed to gzip data block"); STAssertTrue(HasGzipHeader(gzipped), @"doesn't have gzip header on gzipped data"); dataPrime = [NSData gtm_dataByInflatingBytes:[gzipped bytes] length:[gzipped length]]; STAssertNotNil(dataPrime, @"failed to inflate data block"); - STAssertGreaterThan([dataPrime length], 0U, @"failed to inflate data block"); + STAssertGreaterThan([dataPrime length], (NSUInteger)0, @"failed to inflate data block"); STAssertEqualObjects(data, dataPrime, @"failed to round trip via *Bytes apis"); // w/ *Data apis, using our level gzipped = [NSData gtm_dataByGzippingData:data compressionLevel:level]; STAssertNotNil(gzipped, @"failed to gzip data block"); - STAssertGreaterThan([gzipped length], 0U, @"failed to gzip data block"); + STAssertGreaterThan([gzipped length], (NSUInteger)0, @"failed to gzip data block"); STAssertTrue(HasGzipHeader(gzipped), @"doesn't have gzip header on gzipped data"); dataPrime = [NSData gtm_dataByInflatingData:gzipped]; STAssertNotNil(dataPrime, @"failed to inflate data block"); - STAssertGreaterThan([dataPrime length], 0U, @"failed to inflate data block"); + STAssertGreaterThan([dataPrime length], (NSUInteger)0, @"failed to inflate data block"); STAssertEqualObjects(data, dataPrime, @"failed to round trip via *Data apis"); } diff --git a/Foundation/GTMNSEnumerator+Filter.m b/Foundation/GTMNSEnumerator+Filter.m index 5ac3a7c..0fda2ca 100644 --- a/Foundation/GTMNSEnumerator+Filter.m +++ b/Foundation/GTMNSEnumerator+Filter.m @@ -45,7 +45,7 @@ // someone would have to subclass or directly create an object of this // class, and this class is private to this impl. - _GTMDevAssert(base, @"can't bas a nil base enumerator"); + _GTMDevAssert(base, @"can't initWithBase: a nil base enumerator"); base_ = [base retain]; operation_ = filter; other_ = [optionalOther retain]; diff --git a/Foundation/GTMNSString+HTML.m b/Foundation/GTMNSString+HTML.m index a6abb0e..5178ba9 100644 --- a/Foundation/GTMNSString+HTML.m +++ b/Foundation/GTMNSString+HTML.m @@ -19,8 +19,6 @@ #import "GTMDefines.h" #import "GTMNSString+HTML.h" -#import "GTMNSString+Utilities.h" -#import "GTMMethodCheck.h" typedef struct { NSString *escapeSequence; @@ -373,23 +371,38 @@ static int EscapeMapCompare(const void *ucharVoid, const void *mapVoid) { } @implementation NSString (GTMNSStringHTMLAdditions) -GTM_METHOD_CHECK(NSString, gtm_UTF16StringWithLength:); - (NSString *)gtm_stringByEscapingHTMLUsingTable:(HTMLEscapeMap*)table - ofSize:(int)size + ofSize:(NSUInteger)size escapingUnicode:(BOOL)escapeUnicode { - int length = [self length]; + NSUInteger length = [self length]; if (!length) { - return nil; + return self; } NSMutableString *finalString = [NSMutableString string]; - NSMutableData *data2 = [NSMutableData dataWithCapacity:sizeof(unichar) * length]; - const unichar *buffer = (const unichar *)[self gtm_UTF16StringWithLength:nil]; + + // this block is common between GTMNSString+HTML and GTMNSString+XML but + // it's so short that it isn't really worth trying to share. + const unichar *buffer = CFStringGetCharactersPtr((CFStringRef)self); + if (!buffer) { + size_t memsize = length * sizeof(UniChar); + + // nope, alloc buffer and fetch the chars ourselves + buffer = malloc(memsize); + if (!buffer) { + // COV_NF_START - Memory fail case + _GTMDevLog(@"couldn't alloc buffer"); + return nil; + // COV_NF_END + } + [self getCharacters:(void*)buffer]; + [NSData dataWithBytesNoCopy:(void*)buffer length:memsize]; + } if (!buffer || !data2) { - // COV_NF_BEGIN + // COV_NF_START _GTMDevLog(@"Unable to allocate buffer or data2"); return nil; // COV_NF_END @@ -397,9 +410,9 @@ GTM_METHOD_CHECK(NSString, gtm_UTF16StringWithLength:); unichar *buffer2 = (unichar *)[data2 mutableBytes]; - int buffer2Length = 0; + NSUInteger buffer2Length = 0; - for (int i = 0; i < length; ++i) { + for (NSUInteger i = 0; i < length; ++i) { HTMLEscapeMap *val = bsearch(&buffer[i], table, size / sizeof(HTMLEscapeMap), sizeof(HTMLEscapeMap), EscapeMapCompare); @@ -459,7 +472,7 @@ GTM_METHOD_CHECK(NSString, gtm_UTF16StringWithLength:); } NSRange escapeRange = NSMakeRange(subrange.location, semiColonRange.location - subrange.location + 1); NSString *escapeString = [self substringWithRange:escapeRange]; - unsigned length = [escapeString length]; + NSUInteger length = [escapeString length]; // a squence must be longer than 3 (<) and less than 11 (ϑ) if (length > 3 && length < 11) { if ([escapeString characterAtIndex:1] == '#') { diff --git a/Foundation/GTMNSString+HTMLTest.m b/Foundation/GTMNSString+HTMLTest.m index c7b931a..a56c5a5 100644 --- a/Foundation/GTMNSString+HTMLTest.m +++ b/Foundation/GTMNSString+HTMLTest.m @@ -55,6 +55,9 @@ STAssertEqualObjects([string gtm_stringByEscapingForHTML], [NSString stringWithUTF8String:"abcا1ب<تdef&"], @"HTML escaping failed"); + + // test empty string + STAssertEqualObjects([@"" gtm_stringByEscapingForHTML], @"", nil); } // testStringByEscapingHTML - (void)testStringByEscapingAsciiHTML { diff --git a/Foundation/GTMNSString+Utilities.h b/Foundation/GTMNSString+Utilities.h deleted file mode 100644 index 3b4ee00..0000000 --- a/Foundation/GTMNSString+Utilities.h +++ /dev/null @@ -1,41 +0,0 @@ -// -// GTMNSString+Utilities.h -// Misc NSString Utilities -// -// Copyright 2008 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy -// of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. -// - -#import <Foundation/Foundation.h> - -@interface NSString (GTMNSStringUtilitiesAdditions) - -// Returns a a UTF16 buffer. Avoids copying the data if at all -// possible for fastest possible/least memory access to the underlying -// unicode characters (UTF16). This returned buffer is NOT null terminated. -// *DANGER* -// Since we avoid copying data you can only be guaranteed access to -// the bytes of the data for the lifetime of the string that you have extracted -// the data from. This exists to allow speedy access to the underlying buffer -// and guaranteed memory cleanup if memory needs to be allocated. -// Do not free the returned pointer. -// -// Args: -// length - returns the number of unichars in the buffer. Send in nil if -// you don't care. -// -// Returns: -// pointer to the buffer. Nil on failure. -- (const unichar*)gtm_UTF16StringWithLength:(size_t*)length; -@end diff --git a/Foundation/GTMNSString+Utilities.m b/Foundation/GTMNSString+Utilities.m deleted file mode 100644 index 3419d43..0000000 --- a/Foundation/GTMNSString+Utilities.m +++ /dev/null @@ -1,48 +0,0 @@ -// -// GTMNSString+Utilities.m -// Misc NSString Utilities -// -// Copyright 2008 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy -// of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. -// - -#import "GTMDefines.h" -#import "GTMNSString+Utilities.h" - -@implementation NSString (GTMNSStringUtilitiesAdditions) - -- (const unichar*)gtm_UTF16StringWithLength:(size_t*)length { - size_t size = [self length]; - const UniChar *buffer = CFStringGetCharactersPtr((CFStringRef)self); - if (!buffer) { - size_t memsize = size * sizeof(UniChar); - - // nope, alloc buffer and fetch the chars ourselves - buffer = malloc(memsize); - if (!buffer) { - // COV_NF_BEGIN - Memory fail case - _GTMDevLog(@"couldn't alloc buffer"); - return nil; - // COV_NF_END - } - [self getCharacters:(void*)buffer]; - [NSData dataWithBytesNoCopy:(void*)buffer length:size]; - } - if (length) { - *length = size; - } - return buffer; -} - -@end diff --git a/Foundation/GTMNSString+UtilitiesTest.m b/Foundation/GTMNSString+UtilitiesTest.m deleted file mode 100644 index 8394aaf..0000000 --- a/Foundation/GTMNSString+UtilitiesTest.m +++ /dev/null @@ -1,62 +0,0 @@ -// -// GTMNSString+UtilitiesTest.m -// Misc NSString Utilities -// -// Copyright 2006-2008 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy -// of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. -// - - -#import "GTMSenTestCase.h" -#import "GTMNSString+Utilities.h" - -@interface GTMNSString_UtilitiesTest : SenTestCase -@end - -@implementation GTMNSString_UtilitiesTest - -- (void)testStringWithLength { - NSString *string = @""; - size_t length; - const unichar *buffer = [string gtm_UTF16StringWithLength:&length]; - STAssertNotNULL(buffer, @"Buffer shouldn't be NULL"); - STAssertEquals(length, 0LU, @"Length should be 0"); - - UniChar unicharBytes[] = { 0x50, 0x51, 0x52 }; - string = [[[NSString alloc] initWithCharactersNoCopy:unicharBytes - length:3 - freeWhenDone:NO] autorelease]; - buffer = [string gtm_UTF16StringWithLength:&length]; - STAssertEquals(buffer, - (const unichar*)unicharBytes, - @"Pointers should be equal"); - STAssertEquals(length, - 3UL, - nil); - - char utf8Bytes[] = { 0x50, 0x51, 0x52, 0x0 }; - string = [NSString stringWithUTF8String:utf8Bytes]; - buffer = [string gtm_UTF16StringWithLength:&length]; - STAssertNotEquals(buffer, - (const unichar*)utf8Bytes, - @"Pointers should not be equal"); - STAssertEquals(length, - 3UL, - nil); - buffer = [string gtm_UTF16StringWithLength:nil]; - STAssertNotEquals(buffer, - (const unichar*)utf8Bytes, - @"Pointers should not be equal"); -} -@end diff --git a/Foundation/GTMNSString+XML.m b/Foundation/GTMNSString+XML.m index 7ea97b4..bc2f130 100644 --- a/Foundation/GTMNSString+XML.m +++ b/Foundation/GTMNSString+XML.m @@ -19,18 +19,17 @@ #import "GTMDefines.h" #import "GTMNSString+XML.h" #import "GTMGarbageCollection.h" -#import "GTMNSString+Utilities.h" -#import "GTMMethodCheck.h" - -typedef enum { - kGMXMLCharModeEncodeQUOT = 0, - kGMXMLCharModeEncodeAMP = 1, - kGMXMLCharModeEncodeAPOS = 2, - kGMXMLCharModeEncodeLT = 3, - kGMXMLCharModeEncodeGT = 4, - kGMXMLCharModeValid = 99, - kGMXMLCharModeInvalid = 100, -} GMXMLCharMode; + +enum { + kGTMXMLCharModeEncodeQUOT = 0, + kGTMXMLCharModeEncodeAMP = 1, + kGTMXMLCharModeEncodeAPOS = 2, + kGTMXMLCharModeEncodeLT = 3, + kGTMXMLCharModeEncodeGT = 4, + kGTMXMLCharModeValid = 99, + kGTMXMLCharModeInvalid = 100, +}; +typedef NSUInteger GTMXMLCharMode; static NSString *gXMLEntityList[] = { // this must match the above order @@ -41,7 +40,7 @@ static NSString *gXMLEntityList[] = { @">", }; -FOUNDATION_STATIC_INLINE GMXMLCharMode XMLModeForUnichar(UniChar c) { +FOUNDATION_STATIC_INLINE GTMXMLCharMode XMLModeForUnichar(UniChar c) { // Per XML spec Section 2.2 Characters // ( http://www.w3.org/TR/REC-xml/#charsets ) @@ -53,42 +52,42 @@ FOUNDATION_STATIC_INLINE GMXMLCharMode XMLModeForUnichar(UniChar c) { if (c >= 0x20) { switch (c) { case 34: - return kGMXMLCharModeEncodeQUOT; + return kGTMXMLCharModeEncodeQUOT; case 38: - return kGMXMLCharModeEncodeAMP; + return kGTMXMLCharModeEncodeAMP; case 39: - return kGMXMLCharModeEncodeAPOS; + return kGTMXMLCharModeEncodeAPOS; case 60: - return kGMXMLCharModeEncodeLT; + return kGTMXMLCharModeEncodeLT; case 62: - return kGMXMLCharModeEncodeGT; + return kGTMXMLCharModeEncodeGT; default: - return kGMXMLCharModeValid; + return kGTMXMLCharModeValid; } } else { if (c == '\n') - return kGMXMLCharModeValid; + return kGTMXMLCharModeValid; if (c == '\r') - return kGMXMLCharModeValid; + return kGTMXMLCharModeValid; if (c == '\t') - return kGMXMLCharModeValid; - return kGMXMLCharModeInvalid; + return kGTMXMLCharModeValid; + return kGTMXMLCharModeInvalid; } } if (c < 0xE000) - return kGMXMLCharModeInvalid; + return kGTMXMLCharModeInvalid; if (c <= 0xFFFD) - return kGMXMLCharModeValid; + return kGTMXMLCharModeValid; // UniChar can't have the following values // if (c < 0x10000) - // return kGMXMLCharModeInvalid; + // return kGTMXMLCharModeInvalid; // if (c <= 0x10FFFF) - // return kGMXMLCharModeValid; + // return kGTMXMLCharModeValid; - return kGMXMLCharModeInvalid; + return kGTMXMLCharModeInvalid; } // XMLModeForUnichar @@ -103,26 +102,42 @@ static NSString *AutoreleasedCloneForXML(NSString *src, BOOL escaping) { // we can't use the CF call here because it leaves the invalid chars // in the string. - int length = [src length]; + NSUInteger length = [src length]; if (!length) { - return nil; + return src; } NSMutableString *finalString = [NSMutableString string]; - const UniChar *buffer = [src gtm_UTF16StringWithLength:nil]; - _GTMDevAssert(buffer, @"couldn't alloc buffer"); + + // this block is common between GTMNSString+HTML and GTMNSString+XML but + // it's so short that it isn't really worth trying to share. + const UniChar *buffer = CFStringGetCharactersPtr((CFStringRef)src); + if (!buffer) { + size_t memsize = length * sizeof(UniChar); + + // nope, alloc buffer and fetch the chars ourselves + buffer = malloc(memsize); + if (!buffer) { + // COV_NF_START - Memory fail case + _GTMDevLog(@"couldn't alloc buffer"); + return nil; + // COV_NF_END + } + [src getCharacters:(void*)buffer]; + [NSData dataWithBytesNoCopy:(void*)buffer length:memsize]; + } const UniChar *goodRun = buffer; - int goodRunLength = 0; + NSUInteger goodRunLength = 0; - for (int i = 0; i < length; ++i) { + for (NSUInteger i = 0; i < length; ++i) { - GMXMLCharMode cMode = XMLModeForUnichar(buffer[i]); + GTMXMLCharMode cMode = XMLModeForUnichar(buffer[i]); // valid chars go as is, and if we aren't doing entities, then // everything goes as is. - if ((cMode == kGMXMLCharModeValid) || - (!escaping && (cMode != kGMXMLCharModeInvalid))) { + if ((cMode == kGTMXMLCharModeValid) || + (!escaping && (cMode != kGTMXMLCharModeInvalid))) { // goes as is goodRunLength += 1; } else { @@ -137,7 +152,7 @@ static NSString *AutoreleasedCloneForXML(NSString *src, BOOL escaping) { } // if it wasn't invalid, add the encoded version - if (cMode != kGMXMLCharModeInvalid) { + if (cMode != kGTMXMLCharModeInvalid) { // add this encoded [finalString appendString:gXMLEntityList[cMode]]; } @@ -157,7 +172,6 @@ static NSString *AutoreleasedCloneForXML(NSString *src, BOOL escaping) { } // AutoreleasedCloneForXML @implementation NSString (GTMNSStringXMLAdditions) -GTM_METHOD_CHECK(NSString, gtm_UTF16StringWithLength:); - (NSString *)gtm_stringBySanitizingAndEscapingForXML { return AutoreleasedCloneForXML(self, YES); diff --git a/Foundation/GTMNSString+XMLTest.m b/Foundation/GTMNSString+XMLTest.m index 926708f..f1e964d 100644 --- a/Foundation/GTMNSString+XMLTest.m +++ b/Foundation/GTMNSString+XMLTest.m @@ -53,6 +53,9 @@ STAssertEqualObjects([ascString gtm_stringBySanitizingAndEscapingForXML], @"abcde\nf", @"Sanitize and Escape for XML from asc buffer failed"); + + // test empty string + STAssertEqualObjects([@"" gtm_stringBySanitizingAndEscapingForXML], @"", nil); } - (void)testStringBySanitizingToXMLSpec { @@ -82,6 +85,9 @@ STAssertEqualObjects([ascString gtm_stringBySanitizingToXMLSpec], @"abcde\nf", @"Sanitize and Escape for XML from asc buffer failed"); + + // test empty string + STAssertEqualObjects([@"" gtm_stringBySanitizingToXMLSpec], @"", nil); } @end diff --git a/Foundation/GTMObjC2Runtime.h b/Foundation/GTMObjC2Runtime.h index 325a752..ab34cfb 100644 --- a/Foundation/GTMObjC2Runtime.h +++ b/Foundation/GTMObjC2Runtime.h @@ -16,8 +16,8 @@ // the License. // -#import <objc/objc-runtime.h> -#import <objc/Object.h> +#import <objc/objc-api.h> +#import "GTMDefines.h" // These functions exist for code that we want to compile on both the < 10.5 // sdks and on the >= 10.5 sdks without warnings. It basically reimplements @@ -35,6 +35,18 @@ #define AT_REQUIRED #endif +// The file objc-runtime.h was moved to runtime.h and in Leopard, objc-runtime.h +// was just a wrapper around runtime.h. For the iPhone SDK, this objc-runtime.h +// is removed in the iPhoneOS2.0 SDK. +// +// The |Object| class was removed in the iPhone2.0 SDK too. +#if GTM_IPHONE_SDK +#import <objc/runtime.h> +#else +#import <objc/objc-runtime.h> +#import <objc/Object.h> +#endif + #if MAC_OS_X_VERSION_MIN_REQUIRED < 1050 #import "objc/Protocol.h" diff --git a/Foundation/GTMObjC2Runtime.m b/Foundation/GTMObjC2Runtime.m index 00a3c6e..df3c9ca 100644 --- a/Foundation/GTMObjC2Runtime.m +++ b/Foundation/GTMObjC2Runtime.m @@ -44,7 +44,7 @@ BOOL class_conformsToProtocol(Class cls, Protocol *protocol) { struct objc_protocol_list *protos; for (protos = cls->protocols; protos != NULL; protos = protos->next) { - for (int i = 0; i < protos->count; i++) { + for (long i = 0; i < protos->count; i++) { if ([protos->list[i] conformsTo:protocol]) { return YES; } diff --git a/Foundation/GTMObjectSingleton.h b/Foundation/GTMObjectSingleton.h index 116d232..4763b68 100644 --- a/Foundation/GTMObjectSingleton.h +++ b/Foundation/GTMObjectSingleton.h @@ -26,7 +26,7 @@ /// /// Sample usage: /// -/// SINGLETON_BOILERPLATE(GMSomeUsefulManager, sharedSomeUsefulManager) +/// GTMOBJECT_SINGLETON_BOILERPLATE(SomeUsefulManager, sharedSomeUsefulManager) /// (with no trailing semicolon) /// #define GTMOBJECT_SINGLETON_BOILERPLATE(_object_name_, _shared_obj_name_) \ @@ -57,8 +57,8 @@ static _object_name_ *z##_shared_obj_name_ = nil; \ - (id)retain { \ return self; \ } \ -- (unsigned int)retainCount { \ - return UINT_MAX; \ +- (NSUInteger)retainCount { \ + return NSUIntegerMax; \ } \ - (void)release { \ } \ diff --git a/Foundation/GTMProgressMonitorInputStream.h b/Foundation/GTMProgressMonitorInputStream.h new file mode 100644 index 0000000..d3da5cd --- /dev/null +++ b/Foundation/GTMProgressMonitorInputStream.h @@ -0,0 +1,73 @@ +// +// GTMProgressMonitorInputStream.m +// +// Copyright 2007-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +#import <Foundation/Foundation.h> + +// The monitored input stream calls back into the monitor delegate +// with the number of bytes and total size +// +// - (void)inputStream:(GTMProgressMonitorInputStream *)stream +// hasDeliveredByteCount:(unsigned long long)numberOfBytesRead +// ofTotalByteCount:(unsigned long long)dataLength; + +@interface GTMProgressMonitorInputStream : NSInputStream { + + NSInputStream *inputStream_; // encapsulated stream that does the work + + unsigned long long dataSize_; // size of data in the source + unsigned long long numBytesRead_; // bytes read from the input stream so far + + __weak id monitorDelegate_; // WEAK, not retained + SEL monitorSelector_; + + __weak id monitorSource_; // WEAK, not retained +} + +// Length is passed to the progress callback; it may be zero if the progress +// callback can handle that (mainly meant so the monitor delegate can update the +// bounds/position for a progress indicator. ++ (id)inputStreamWithStream:(NSInputStream *)input + length:(unsigned long long)length; + +- (id)initWithStream:(NSInputStream *)input + length:(unsigned long long)length; + +// The monitor is called when bytes have been read +// +// monitorDelegate should respond to a selector with a signature matching: +// +// - (void)inputStream:(GTMProgressMonitorInputStream *)stream +// hasDeliveredBytes:(unsigned long long)numReadSoFar +// ofTotalBytes:(unsigned long long)total +// +// |total| will be the length passed when this GTMProgressMonitorInputStream was +// created. + +- (void)setMonitorDelegate:(id)monitorDelegate // not retained + selector:(SEL)monitorSelector; +- (id)monitorDelegate; +- (SEL)monitorSelector; + +// The source argument lets the delegate know the source of this input stream. +// this class does nothing w/ this, it's just here to provide context to your +// monitorDelegate. +- (void)setMonitorSource:(id)source; // not retained +- (id)monitorSource; + +@end + diff --git a/Foundation/GTMProgressMonitorInputStream.m b/Foundation/GTMProgressMonitorInputStream.m new file mode 100644 index 0000000..2336268 --- /dev/null +++ b/Foundation/GTMProgressMonitorInputStream.m @@ -0,0 +1,187 @@ +// +// GTMProgressMonitorInputStream.m +// +// Copyright 2007-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +#import "GTMProgressMonitorInputStream.h" +#import "GTMDefines.h" +#import "GTMDebugSelectorValidation.h" + +@implementation GTMProgressMonitorInputStream + +// we'll forward all unhandled messages to the NSInputStream class +// or to the encapsulated input stream. This is needed +// for all messages sent to NSInputStream which aren't +// handled by our superclass; that includes various private run +// loop calls. ++ (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { + return [NSInputStream methodSignatureForSelector:selector]; +} + ++ (void)forwardInvocation:(NSInvocation*)invocation { + [invocation invokeWithTarget:[NSInputStream class]]; +} + +- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { + return [inputStream_ methodSignatureForSelector:selector]; +} + +- (void)forwardInvocation:(NSInvocation*)invocation { + [invocation invokeWithTarget:inputStream_]; +} + +#pragma mark - + ++ (id)inputStreamWithStream:(NSInputStream *)input + length:(unsigned long long)length { + + return [[[self alloc] initWithStream:input + length:length] autorelease]; +} + +- (id)initWithStream:(NSInputStream *)input + length:(unsigned long long)length { + + if ((self = [super init]) != nil) { + + inputStream_ = [input retain]; + dataSize_ = length; + } + return self; +} + +- (id)init { + _GTMDevAssert(NO, @"should call initWithStream:length:"); + return nil; +} + +- (void)dealloc { + [inputStream_ release]; + [super dealloc]; +} + +#pragma mark - + + +- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len { + + NSInteger numRead = [inputStream_ read:buffer maxLength:len]; + + if (numRead > 0) { + + numBytesRead_ += numRead; + + if (monitorDelegate_ && monitorSelector_) { + + // call the monitor delegate with the number of bytes read and the + // total bytes read + + NSMethodSignature *signature = [monitorDelegate_ methodSignatureForSelector:monitorSelector_]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:monitorSelector_]; + [invocation setTarget:monitorDelegate_]; + [invocation setArgument:&self atIndex:2]; + [invocation setArgument:&numBytesRead_ atIndex:3]; + [invocation setArgument:&dataSize_ atIndex:4]; + [invocation invoke]; + } + } + return numRead; +} + +- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len { + return [inputStream_ getBuffer:buffer length:len]; +} + +- (BOOL)hasBytesAvailable { + return [inputStream_ hasBytesAvailable]; +} + +#pragma mark Standard messages + +// Pass expected messages to our encapsulated stream. +// +// We want our encapsulated NSInputStream to handle the standard messages; +// we don't want the superclass to handle them. +- (void)open { + [inputStream_ open]; +} + +- (void)close { + [inputStream_ close]; +} + +- (id)delegate { + return [inputStream_ delegate]; +} + +- (void)setDelegate:(id)delegate { + [inputStream_ setDelegate:delegate]; +} + +- (id)propertyForKey:(NSString *)key { + return [inputStream_ propertyForKey:key]; +} +- (BOOL)setProperty:(id)property forKey:(NSString *)key { + return [inputStream_ setProperty:property forKey:key]; +} + +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { + [inputStream_ scheduleInRunLoop:aRunLoop forMode:mode]; +} +- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { + [inputStream_ removeFromRunLoop:aRunLoop forMode:mode]; +} + +- (NSStreamStatus)streamStatus { + return [inputStream_ streamStatus]; +} + +- (NSError *)streamError { + return [inputStream_ streamError]; +} + +#pragma mark Setters and getters + +- (void)setMonitorDelegate:(id)monitorDelegate + selector:(SEL)monitorSelector { + monitorDelegate_ = monitorDelegate; // non-retained + monitorSelector_ = monitorSelector; + GTMAssertSelectorNilOrImplementedWithArguments(monitorDelegate, + monitorSelector, + @encode(GTMProgressMonitorInputStream *), + @encode(unsigned long long), + @encode(unsigned long long), + NULL); +} + +- (id)monitorDelegate { + return monitorDelegate_; +} + +- (SEL)monitorSelector { + return monitorSelector_; +} + +- (void)setMonitorSource:(id)source { + monitorSource_ = source; // non-retained +} + +- (id)monitorSource { + return monitorSource_; +} + +@end diff --git a/Foundation/GTMRegex.h b/Foundation/GTMRegex.h index ee56b98..75cffe2 100644 --- a/Foundation/GTMRegex.h +++ b/Foundation/GTMRegex.h @@ -18,9 +18,10 @@ #import <Foundation/Foundation.h> #import <regex.h> +#import "GTMDefines.h" /// Options for controlling the behavior of the matches -typedef enum { +enum { kGTMRegexOptionIgnoreCase = 0x01, // Ignore case in matching, ie: 'a' matches 'a' or 'A' @@ -48,7 +49,8 @@ typedef enum { // and would also match // fooAAA\nbar -} GTMRegexOptions; +}; +typedef NSUInteger GTMRegexOptions; /// Global contants needed for errors from consuming patterns @@ -148,7 +150,7 @@ _EXTERN NSString* kGTMRegexPatternErrorErrorString _INITIALIZE_AS(@"patternError // Sub Patterns are basically the number of parenthesis blocks w/in the pattern. // ie: The pattern "foo((bar)|(baz))" has 3 sub patterns. // -- (int)subPatternCount; +- (NSUInteger)subPatternCount; /// Returns YES if the whole string |str| matches the pattern. - (BOOL)matchesString:(NSString *)str; @@ -266,7 +268,7 @@ _EXTERN NSString* kGTMRegexPatternErrorErrorString _INITIALIZE_AS(@"patternError @private NSData *utf8StrBuf_; regmatch_t *regMatches_; // STRONG: ie-we call free - int numRegMatches_; + NSUInteger numRegMatches_; BOOL isMatch_; } @@ -296,7 +298,7 @@ _EXTERN NSString* kGTMRegexPatternErrorErrorString _INITIALIZE_AS(@"patternError // 4: nil // 5: "baz" // -- (NSString *)subPatternString:(int)index; +- (NSString *)subPatternString:(NSUInteger)index; @end diff --git a/Foundation/GTMRegex.m b/Foundation/GTMRegex.m index c50ff2b..92eb576 100644 --- a/Foundation/GTMRegex.m +++ b/Foundation/GTMRegex.m @@ -47,7 +47,7 @@ static NSString *const kReplacementPattern = BOOL allSegments_; BOOL treatStartOfNewSegmentAsBeginningOfString_; regoff_t curParseIndex_; - regmatch_t *savedRegMatches_; + __strong regmatch_t *savedRegMatches_; } - (id)initWithRegex:(GTMRegex *)regex processString:(NSString *)str @@ -58,7 +58,7 @@ static NSString *const kReplacementPattern = @interface GTMRegexStringSegment (PrivateMethods) - (id)initWithUTF8StrBuf:(NSData *)utf8StrBuf regMatches:(regmatch_t *)regMatches - numRegMatches:(int)numRegMatches + numRegMatches:(NSUInteger)numRegMatches isMatch:(BOOL)isMatch; @end @@ -89,10 +89,10 @@ static NSString *const kReplacementPattern = // a unichar buffer and scanning that, along w/ pushing the data over in // chunks (when possible). - unsigned int len = [str length]; + NSUInteger len = [str length]; NSMutableString *result = [NSMutableString stringWithCapacity:len]; - for (unsigned int x = 0; x < len; ++x) { + for (NSUInteger x = 0; x < len; ++x) { unichar ch = [str characterAtIndex:x]; switch (ch) { case '^': @@ -190,6 +190,17 @@ static NSString *const kReplacementPattern = return self; } +- (void)finalize { + // we used pattern_ as our flag that we initialized the regex_t + if (pattern_) { + regfree(®exData_); + [pattern_ release]; + // play it safe and clear it since we use it as a flag for regexData_ + pattern_ = nil; + } + [super finalize]; +} + - (void)dealloc { // we used pattern_ as our flag that we initialized the regex_t if (pattern_) { @@ -201,7 +212,7 @@ static NSString *const kReplacementPattern = [super dealloc]; } -- (int)subPatternCount { +- (NSUInteger)subPatternCount { return regexData_.re_nsub; } @@ -223,7 +234,7 @@ static NSString *const kReplacementPattern = - (NSArray *)subPatternsOfString:(NSString *)str { NSArray *result = nil; - int count = regexData_.re_nsub + 1; + NSUInteger count = regexData_.re_nsub + 1; regmatch_t *regMatches = malloc(sizeof(regmatch_t) * count); if (!regMatches) return nil; // COV_NF_LINE - no real way to force this in a unittest @@ -243,22 +254,22 @@ static NSString *const kReplacementPattern = if ((regMatches[0].rm_so != 0) || (regMatches[0].rm_eo != [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding])) { // only matched a sub part of the string - return NO; + return nil; } NSMutableArray *buildResult = [NSMutableArray arrayWithCapacity:count]; - for (int x = 0 ; x < count ; ++x) { + for (NSUInteger x = 0 ; x < count ; ++x) { if ((regMatches[x].rm_so == -1) && (regMatches[x].rm_eo == -1)) { // add NSNull since it wasn't used [buildResult addObject:[NSNull null]]; } else { // fetch the string const char *base = utf8Str + regMatches[x].rm_so; - unsigned len = regMatches[x].rm_eo - regMatches[x].rm_so; + regoff_t len = regMatches[x].rm_eo - regMatches[x].rm_so; NSString *sub = [[[NSString alloc] initWithBytes:base - length:len + length:(NSUInteger)len encoding:NSUTF8StringEncoding] autorelease]; [buildResult addObject:sub]; } @@ -284,10 +295,10 @@ static NSString *const kReplacementPattern = flags:0]) { // fetch the string const char *base = utf8Str + regMatch.rm_so; - unsigned len = regMatch.rm_eo - regMatch.rm_so; + regoff_t len = regMatch.rm_eo - regMatch.rm_so; result = [[[NSString alloc] initWithBytes:base - length:len + length:(NSUInteger)len encoding:NSUTF8StringEncoding] autorelease]; } return result; @@ -497,6 +508,7 @@ static NSString *const kReplacementPattern = return self; } +// Don't need a finalize because savedRegMatches_ is marked __strong - (void)dealloc { if (savedRegMatches_) { free(savedRegMatches_); @@ -584,7 +596,7 @@ static NSString *const kReplacementPattern = isMatch = NO; // mark everything but the zero slot w/ not used - for (int x = [regex_ subPatternCount]; x > 0; --x) { + for (NSUInteger x = [regex_ subPatternCount]; x > 0; --x) { nextMatches[x].rm_so = nextMatches[x].rm_eo = -1; } nextMatches[0].rm_so = curParseIndex_; @@ -611,7 +623,7 @@ static NSString *const kReplacementPattern = if (allSegments_) { isMatch = NO; // mark everything but the zero slot w/ not used - for (int x = [regex_ subPatternCount]; x > 0; --x) { + for (NSUInteger x = [regex_ subPatternCount]; x > 0; --x) { nextMatches[x].rm_so = nextMatches[x].rm_eo = -1; } nextMatches[0].rm_so = curParseIndex_; @@ -685,7 +697,7 @@ static NSString *const kReplacementPattern = return [self subPatternString:0]; } -- (NSString *)subPatternString:(int)index { +- (NSString *)subPatternString:(NSUInteger)index { if ((index < 0) || (index > numRegMatches_)) return nil; @@ -695,9 +707,9 @@ static NSString *const kReplacementPattern = // fetch the string const char *base = (const char*)[utf8StrBuf_ bytes] + regMatches_[index].rm_so; - unsigned len = regMatches_[index].rm_eo - regMatches_[index].rm_so; + regoff_t len = regMatches_[index].rm_eo - regMatches_[index].rm_so; return [[[NSString alloc] initWithBytes:base - length:len + length:(NSUInteger)len encoding:NSUTF8StringEncoding] autorelease]; } @@ -705,7 +717,7 @@ static NSString *const kReplacementPattern = NSMutableString *result = [NSMutableString stringWithFormat:@"%@<%p> { isMatch=\"%s\", subPatterns=(", [self class], self, (isMatch_ ? "YES" : "NO")]; - for (int x = 0; x <= numRegMatches_; ++x) { + for (NSUInteger x = 0; x <= numRegMatches_; ++x) { NSString *format = @", \"%.*s\""; if (x == 0) format = @" \"%.*s\""; @@ -725,7 +737,7 @@ static NSString *const kReplacementPattern = - (id)initWithUTF8StrBuf:(NSData *)utf8StrBuf regMatches:(regmatch_t *)regMatches - numRegMatches:(int)numRegMatches + numRegMatches:(NSUInteger)numRegMatches isMatch:(BOOL)isMatch { self = [super init]; if (!self) return nil; diff --git a/Foundation/GTMRegexTest.m b/Foundation/GTMRegexTest.m index 22c571e..033b560 100644 --- a/Foundation/GTMRegexTest.m +++ b/Foundation/GTMRegexTest.m @@ -71,7 +71,7 @@ withError:&error] autorelease], nil); STAssertNotNil(error, nil); STAssertEqualObjects([error domain], kGTMRegexErrorDomain, nil); - STAssertEquals([error code], kGTMRegexPatternParseFailedError, nil); + STAssertEquals([error code], (NSInteger)kGTMRegexPatternParseFailedError, nil); NSDictionary *userInfo = [error userInfo]; STAssertNotNil(userInfo, @"failed to get userInfo from error"); STAssertEqualObjects([userInfo objectForKey:kGTMRegexPatternErrorPattern], @"(.", nil); @@ -106,7 +106,7 @@ withError:&error], nil); STAssertNotNil(error, nil); STAssertEqualObjects([error domain], kGTMRegexErrorDomain, nil); - STAssertEquals([error code], kGTMRegexPatternParseFailedError, nil); + STAssertEquals([error code], (NSInteger)kGTMRegexPatternParseFailedError, nil); userInfo = [error userInfo]; STAssertNotNil(userInfo, @"failed to get userInfo from error"); STAssertEqualObjects([userInfo objectForKey:kGTMRegexPatternErrorPattern], @"(.", nil); @@ -385,11 +385,11 @@ } - (void)testSubPatternCount { - STAssertEquals(0, [[GTMRegex regexWithPattern:@".*"] subPatternCount], nil); - STAssertEquals(1, [[GTMRegex regexWithPattern:@"(.*)"] subPatternCount], nil); - STAssertEquals(1, [[GTMRegex regexWithPattern:@"[fo]*(.*)[bar]*"] subPatternCount], nil); - STAssertEquals(3, [[GTMRegex regexWithPattern:@"([fo]*)(.*)([bar]*)"] subPatternCount], nil); - STAssertEquals(7, [[GTMRegex regexWithPattern:@"(([bar]*)|([fo]*))(.*)(([bar]*)|([fo]*))"] subPatternCount], nil); + STAssertEquals((NSUInteger)0, [[GTMRegex regexWithPattern:@".*"] subPatternCount], nil); + STAssertEquals((NSUInteger)1, [[GTMRegex regexWithPattern:@"(.*)"] subPatternCount], nil); + STAssertEquals((NSUInteger)1, [[GTMRegex regexWithPattern:@"[fo]*(.*)[bar]*"] subPatternCount], nil); + STAssertEquals((NSUInteger)3, [[GTMRegex regexWithPattern:@"([fo]*)(.*)([bar]*)"] subPatternCount], nil); + STAssertEquals((NSUInteger)7, [[GTMRegex regexWithPattern:@"(([bar]*)|([fo]*))(.*)(([bar]*)|([fo]*))"] subPatternCount], nil); } - (void)testMatchesString { @@ -418,15 +418,15 @@ - (void)testSubPatternsOfString { GTMRegex *regex = [GTMRegex regexWithPattern:@"(fo(o+))((bar)|(baz))"]; STAssertNotNil(regex, nil); - STAssertEquals(5, [regex subPatternCount], nil); + STAssertEquals((NSUInteger)5, [regex subPatternCount], nil); NSArray *subPatterns = [regex subPatternsOfString:@"foooooobaz"]; STAssertNotNil(subPatterns, nil); - STAssertEquals(6U, [subPatterns count], nil); + STAssertEquals((NSUInteger)6, [subPatterns count], nil); STAssertEqualStrings(@"foooooobaz", [subPatterns objectAtIndex:0], nil); STAssertEqualStrings(@"foooooo", [subPatterns objectAtIndex:1], nil); STAssertEqualStrings(@"ooooo", [subPatterns objectAtIndex:2], nil); STAssertEqualStrings(@"baz", [subPatterns objectAtIndex:3], nil); - STAssertTrue(([NSNull null] == [subPatterns objectAtIndex:4]), nil); + STAssertEqualObjects([NSNull null], [subPatterns objectAtIndex:4], nil); STAssertEqualStrings(@"baz", [subPatterns objectAtIndex:5], nil); // not there @@ -562,7 +562,7 @@ // now test the saved sub segments regex = [GTMRegex regexWithPattern:@"(foo)((bar)|(baz))"]; STAssertNotNil(regex, nil); - STAssertEquals(4, [regex subPatternCount], nil); + STAssertEquals((NSUInteger)4, [regex subPatternCount], nil); enumerator = [regex segmentEnumeratorForString:@"foobarxxfoobaz"]; STAssertNotNil(enumerator, nil); // "foobar" @@ -605,7 +605,7 @@ STAssertNotNil(enumerator, nil); NSArray *allSegments = [enumerator allObjects]; STAssertNotNil(allSegments, nil); - STAssertEquals(6U, [allSegments count], nil); + STAssertEquals((NSUInteger)6, [allSegments count], nil); // test we are getting the flags right for newline regex = [GTMRegex regexWithPattern:@"^a"]; @@ -737,7 +737,7 @@ // now test the saved sub segments regex = [GTMRegex regexWithPattern:@"(foo)((bar)|(baz))"]; STAssertNotNil(regex, nil); - STAssertEquals(4, [regex subPatternCount], nil); + STAssertEquals((NSUInteger)4, [regex subPatternCount], nil); enumerator = [regex matchSegmentEnumeratorForString:@"foobarxxfoobaz"]; STAssertNotNil(enumerator, nil); // "foobar" @@ -774,7 +774,7 @@ STAssertNotNil(enumerator, nil); NSArray *allSegments = [enumerator allObjects]; STAssertNotNil(allSegments, nil); - STAssertEquals(3U, [allSegments count], nil); + STAssertEquals((NSUInteger)3, [allSegments count], nil); // test we are getting the flags right for newline regex = [GTMRegex regexWithPattern:@"^a"]; @@ -880,23 +880,23 @@ // default options GTMRegex *regex = [GTMRegex regexWithPattern:@"a+"]; STAssertNotNil(regex, nil); - STAssertGreaterThan([[regex description] length], 10U, + STAssertGreaterThan([[regex description] length], (NSUInteger)10, @"failed to get a reasonable description for regex"); // enumerator NSEnumerator *enumerator = [regex segmentEnumeratorForString:@"aaabbbccc"]; STAssertNotNil(enumerator, nil); - STAssertGreaterThan([[enumerator description] length], 10U, + STAssertGreaterThan([[enumerator description] length], (NSUInteger)10, @"failed to get a reasonable description for regex enumerator"); // string segment GTMRegexStringSegment *seg = [enumerator nextObject]; STAssertNotNil(seg, nil); - STAssertGreaterThan([[seg description] length], 10U, + STAssertGreaterThan([[seg description] length], (NSUInteger)10, @"failed to get a reasonable description for regex string segment"); // regex w/ other options regex = [GTMRegex regexWithPattern:@"a+" options:(kGTMRegexOptionIgnoreCase | kGTMRegexOptionSupressNewlineSupport)]; STAssertNotNil(regex, nil); - STAssertGreaterThan([[regex description] length], 10U, + STAssertGreaterThan([[regex description] length], (NSUInteger)10, @"failed to get a reasonable description for regex w/ options"); } @@ -926,12 +926,12 @@ - (void)testSubPatternsOfPattern { NSArray *subPatterns = [@"foooooobaz" gtm_subPatternsOfPattern:@"(fo(o+))((bar)|(baz))"]; STAssertNotNil(subPatterns, nil); - STAssertEquals(6U, [subPatterns count], nil); + STAssertEquals((NSUInteger)6, [subPatterns count], nil); STAssertEqualStrings(@"foooooobaz", [subPatterns objectAtIndex:0], nil); STAssertEqualStrings(@"foooooo", [subPatterns objectAtIndex:1], nil); STAssertEqualStrings(@"ooooo", [subPatterns objectAtIndex:2], nil); STAssertEqualStrings(@"baz", [subPatterns objectAtIndex:3], nil); - STAssertTrue(([NSNull null] == [subPatterns objectAtIndex:4]), nil); + STAssertEqualObjects([NSNull null], [subPatterns objectAtIndex:4], nil); STAssertEqualStrings(@"baz", [subPatterns objectAtIndex:5], nil); // not there @@ -1089,7 +1089,7 @@ STAssertNotNil(enumerator, nil); NSArray *allSegments = [enumerator allObjects]; STAssertNotNil(allSegments, nil); - STAssertEquals(6U, [allSegments count], nil); + STAssertEquals((NSUInteger)6, [allSegments count], nil); } - (void)testMatchSegmentEnumeratorForPattern { @@ -1170,14 +1170,14 @@ STAssertNotNil(enumerator, nil); NSArray *allSegments = [enumerator allObjects]; STAssertNotNil(allSegments, nil); - STAssertEquals(3U, [allSegments count], nil); + STAssertEquals((NSUInteger)3, [allSegments count], nil); } - (void)testAllSubstringsMatchedByPattern { NSArray *segments = [@"afoobarbfooobaarfoobarzz" gtm_allSubstringsMatchedByPattern:@"foo+ba+r"]; STAssertNotNil(segments, nil); - STAssertEquals(3U, [segments count], nil); + STAssertEquals((NSUInteger)3, [segments count], nil); STAssertEqualStrings([segments objectAtIndex:0], @"foobar", nil); STAssertEqualStrings([segments objectAtIndex:1], @"fooobaar", nil); STAssertEqualStrings([segments objectAtIndex:2], @"foobar", nil); @@ -1185,12 +1185,12 @@ // test no match segments = [@"aaa" gtm_allSubstringsMatchedByPattern:@"foo+ba+r"]; STAssertNotNil(segments, nil); - STAssertEquals(0U, [segments count], nil); + STAssertEquals((NSUInteger)0, [segments count], nil); // test only match segments = [@"foobar" gtm_allSubstringsMatchedByPattern:@"foo+ba+r"]; STAssertNotNil(segments, nil); - STAssertEquals(1U, [segments count], nil); + STAssertEquals((NSUInteger)1, [segments count], nil); STAssertEqualStrings([segments objectAtIndex:0], @"foobar", nil); } diff --git a/Foundation/GTMScriptRunnerTest.m b/Foundation/GTMScriptRunnerTest.m index a4497b6..5229800 100644 --- a/Foundation/GTMScriptRunnerTest.m +++ b/Foundation/GTMScriptRunnerTest.m @@ -215,7 +215,7 @@ // make sure description doesn't choke GTMScriptRunner *sr = [GTMScriptRunner runner]; STAssertNotNil(sr, @"Script runner must not be nil"); - STAssertGreaterThan([[sr description] length], 10U, + STAssertGreaterThan([[sr description] length], (NSUInteger)10, @"expected a description of at least 10 chars"); } diff --git a/Foundation/GTMSystemVersion.h b/Foundation/GTMSystemVersion.h index 13c3c19..0f19596 100644 --- a/Foundation/GTMSystemVersion.h +++ b/Foundation/GTMSystemVersion.h @@ -43,6 +43,6 @@ // Returns a YES/NO if the system is 10.5 or better + (BOOL)isLeopardOrGreater; -#endif // GTM_IPHONE_SDK +#endif // GTM_MACOS_SDK @end diff --git a/Foundation/GTMSystemVersion.m b/Foundation/GTMSystemVersion.m index a2e4d7b..da767ae 100644 --- a/Foundation/GTMSystemVersion.m +++ b/Foundation/GTMSystemVersion.m @@ -17,20 +17,55 @@ // #import "GTMSystemVersion.h" +#if GTM_MACOS_SDK +#import <Carbon/Carbon.h> +#endif -static int sGTMSystemVersionMajor = 0; -static int sGTMSystemVersionMinor = 0; -static int sGTMSystemVersionBugFix = 0; +static SInt32 sGTMSystemVersionMajor = 0; +static SInt32 sGTMSystemVersionMinor = 0; +static SInt32 sGTMSystemVersionBugFix = 0; @implementation GTMSystemVersion + (void)initialize { if (self == [GTMSystemVersion class]) { + // Gestalt is the recommended way of getting the OS version (despite a + // comment to the contrary in the 10.4 headers and docs; see + // <http://lists.apple.com/archives/carbon-dev/2007/Aug/msg00089.html>). + // The iPhone doesn't have Gestalt though, so use the plist there. +#if GTM_MACOS_SDK + require_noerr(Gestalt(gestaltSystemVersionMajor, &sGTMSystemVersionMajor), failedGestalt); + require_noerr(Gestalt(gestaltSystemVersionMinor, &sGTMSystemVersionMinor), failedGestalt); + require_noerr(Gestalt(gestaltSystemVersionBugFix, &sGTMSystemVersionBugFix), failedGestalt); + + return; + + failedGestalt: + ; +#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_3 + // gestaltSystemVersionMajor et al are only on 10.4 and above, so they + // could fail when running on 10.3. + SInt32 binaryCodedDec; + OSStatus err = err = Gestalt(gestaltSystemVersion, &binaryCodedDec); + _GTMDevAssert(!err, @"Unable to get version from Gestalt"); + + // Note that this code will return x.9.9 for any system rev parts that are + // greater than 9 (i.e., 10.10.10 will be 10.9.9). This shouldn't ever be a + // problem as the code above takes care of 10.4+. + int msb = (binaryCodedDec & 0x0000F000L) >> 12; + msb *= 10; + int lsb = (binaryCodedDec & 0x00000F00L) >> 8; + sGTMSystemVersionMajor = msb + lsb; + sGTMSystemVersionMinor = (binaryCodedDec & 0x000000F0L) >> 4; + sGTMSystemVersionBugFix = (binaryCodedDec & 0x0000000FL); +#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_3 + +#else // GTM_MACOS_SDK NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSDictionary *systemVersionPlist = [NSDictionary dictionaryWithContentsOfFile:@"/System/Library/CoreServices/SystemVersion.plist"]; NSString *version = [systemVersionPlist objectForKey:@"ProductVersion"]; _GTMDevAssert(version, @"Unable to get version"); NSArray *versionInfo = [version componentsSeparatedByString:@"."]; - int length = [versionInfo count]; + NSUInteger length = [versionInfo count]; _GTMDevAssert(length > 1 && length < 4, @"Unparseable version %@", version); sGTMSystemVersionMajor = [[versionInfo objectAtIndex:0] intValue]; _GTMDevAssert(sGTMSystemVersionMajor != 0, @"Unknown version for %@", version); @@ -39,6 +74,7 @@ static int sGTMSystemVersionBugFix = 0; sGTMSystemVersionBugFix = [[versionInfo objectAtIndex:2] intValue]; } [pool release]; +#endif // GTM_MACOS_SDK } } @@ -82,6 +118,6 @@ static int sGTMSystemVersionBugFix = 0; (sGTMSystemVersionMajor == 10 && sGTMSystemVersionMinor >= 5); } -#endif // GTM_IPHONE_SDK +#endif // GTM_MACOS_SDK @end diff --git a/Foundation/TestData/GTMHTTPFetcherTestPage.html b/Foundation/TestData/GTMHTTPFetcherTestPage.html new file mode 100644 index 0000000..1f44469 --- /dev/null +++ b/Foundation/TestData/GTMHTTPFetcherTestPage.html @@ -0,0 +1,9 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test Page</title> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +</head> +<body> +This is a simple test page +</body> +</html> diff --git a/Foundation/TestData/GTMHTTPFetcherTestServer b/Foundation/TestData/GTMHTTPFetcherTestServer new file mode 100755 index 0000000..838c110 --- /dev/null +++ b/Foundation/TestData/GTMHTTPFetcherTestServer @@ -0,0 +1,274 @@ +#!/usr/bin/python +# +# Copyright 2006-2008 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# A simple server for testing the http calls + +"""A simple BaseHTTPRequestHandler for unit testing GTM Network code + +This http server is for use by GTMHTTPFetcherTest.m in testing +both authentication and object retrieval. + +Requests to the path /accounts/ClientLogin are assumed to be +for login; other requests are for object retrieval +""" + +__author__ = 'Google Inc.' + +import string +import cgi +import time +import os +import sys +import re +import mimetypes +import socket +from BaseHTTPServer import BaseHTTPRequestHandler +from BaseHTTPServer import HTTPServer +from optparse import OptionParser + +class ServerTimeoutException(Exception): + pass + + +class TimeoutServer(HTTPServer): + + """HTTP server for testing GTM network requests. + + This server will throw an exception if it receives no connections for + several minutes. We use this to ensure that the server will be cleaned + up if something goes wrong during the unit testing. + """ + + def get_request(self): + self.socket.settimeout(120.0) + result = None + while result is None: + try: + result = self.socket.accept() + except socket.timeout: + raise ServerTimeoutException + result[0].settimeout(None) + return result + + +class SimpleServer(BaseHTTPRequestHandler): + + """HTTP request handler for testing GTM network requests. + + This is an implementation of a request handler for BaseHTTPServer, + specifically designed for GTM network code usage. + + Normal requests for GET/POST/PUT simply retrieve the file from the + supplied path, starting in the current directory. A cookie called + TestCookie is set by the response header, with the value of the filename + requested. + + DELETE requests always succeed. + + Appending ?status=n results in a failure with status value n. + + Paths ending in .auth have the .auth extension stripped, and must have + an authorization header of "GoogleLogin auth=GoodAuthToken" to succeed. + + Successful results have a Last-Modified header set; if that header's value + ("thursday") is supplied in a request's "If-Modified-Since" header, the + result is 304 (Not Modified). + + Requests to /accounts/ClientLogin will fail if supplied with a body + containing Passwd=bad. If they contain logintoken and logincaptcha values, + those must be logintoken=CapToken&logincaptch=good to succeed. + """ + + def do_GET(self): + self.doAllRequests() + + def do_POST(self): + self.doAllRequests() + + def do_PUT(self): + self.doAllRequests() + + def do_DELETE(self): + self.doAllRequests() + + def doAllRequests(self): + # This method handles all expected incoming requests + # + # Requests to path /accounts/ClientLogin are assumed to be for signing in + # + # Other paths are for retrieving a local file. An .auth appended + # to a file path will require authentication (meaning the Authorization + # header must be present with the value "GoogleLogin auth=GoodAuthToken".) + # If the token is present, the file (without uthe .auth at the end) will + # be returned. + # + # HTTP Delete commands succeed but return no data. + # + # GData override headers (putting the verb in X-HTTP-Method-Override) + # are supported. + # + # Any auth password is valid except "bad", which will fail, and "captcha", + # which will fail unless the authentication request's post string includes + # "logintoken=CapToken&logincaptcha=good" + + # We will use a readable default result string since it should never show up + # in output + resultString = "default GTMHTTPFetcherTestServer result\n"; + resultStatus = 0 + headerType = "text/plain" + postString = "" + modifiedDate = "thursday" # clients should treat dates as opaque, generally + + # auth queries and some others may include post data + postLength = int(self.headers.getheader("Content-Length", "0")); + if postLength > 0: + postString = self.rfile.read(postLength) + + # auth queries and some GData queries include post data + ifModifiedSince = self.headers.getheader("If-Modified-Since", ""); + + # retrieve the auth header; require it if the file path ends + # with the string ".auth" + authorization = self.headers.getheader("Authorization", "") + if self.path.endswith(".auth"): + if authorization != "GoogleLogin auth=GoodAuthToken": + self.send_error(401,"Unauthorized: %s" % self.path) + return + self.path = self.path[:-5] # remove the .auth at the end + + overrideHeader = self.headers.getheader("X-HTTP-Method-Override", "") + httpCommand = self.command + if httpCommand == "POST" and len(overrideHeader) > 0: + httpCommand = overrideHeader + + try: + if self.path.endswith("/accounts/ClientLogin"): + # + # it's a sign-in attempt; it's good unless the password is "bad" or + # "captcha" + # + + # use regular expression to find the password + password = "" + searchResult = re.search("(Passwd=)([^&\n]*)", postString) + if searchResult: + password = searchResult.group(2) + + if password == "bad": + resultString = "Error=BadAuthentication\n" + resultStatus = 403 + + elif password == "captcha": + logintoken = "" + logincaptcha = "" + + # use regular expressions to find the captcha token and answer + searchResult = re.search("(logintoken=)([^&\n]*)", postString); + if searchResult: + logintoken = searchResult.group(2) + + searchResult = re.search("(logincaptcha=)([^&\n]*)", postString); + if searchResult: + logincaptcha = searchResult.group(2) + + # if the captcha token is "CapToken" and the answer is "good" + # then it's a valid sign in + if (logintoken == "CapToken") and (logincaptcha == "good"): + resultString = "SID=GoodSID\nLSID=GoodLSID\nAuth=GoodAuthToken\n" + resultStatus = 200 + else: + # incorrect captcha token or answer provided + resultString = ("Error=CaptchaRequired\nCaptchaToken=CapToken" + "\nCaptchaUrl=CapUrl\n") + resultStatus = 403 + + else: + # valid username/password + resultString = "SID=GoodSID\nLSID=GoodLSID\nAuth=GoodAuthToken\n" + resultStatus = 200 + + elif httpCommand == "DELETE": + # + # it's an object delete; read and return empty data + # + resultString = "" + resultStatus = 200 + headerType = "text/plain" + + else: + # queries that have something like "?status=456" should fail with the + # status code + searchResult = re.search("(status=)([0-9]+)", self.path) + if searchResult: + status = searchResult.group(2) + self.send_error(int(status), + "Test HTTP server status parameter: %s" % self.path) + return + + # if the client gave us back our not modified date, then say there's no + # change in the response + if ifModifiedSince == modifiedDate: + self.send_response(304) # Not Modified + return + + else: + # + # it's a file fetch; read and return the data file + # + f = open("." + self.path) + resultString = f.read() + f.close() + resultStatus = 200 + fileTypeInfo = mimetypes.guess_type("." + self.path) + headerType = fileTypeInfo[0] # first part of the tuple is mime type + + self.send_response(resultStatus) + self.send_header("Content-type", headerType) + self.send_header("Last-Modified", modifiedDate) + + cookieValue = os.path.basename("." + self.path) + self.send_header('Set-Cookie', 'TestCookie=%s' % cookieValue) + self.end_headers() + self.wfile.write(resultString) + + except IOError: + self.send_error(404,"File Not Found: %s" % self.path) + + +def main(): + try: + parser = OptionParser() + parser.add_option("-p", "--port", dest="port", help="Port to run server on", + type="int", default="80") + parser.add_option("-r", "--root", dest="root", help="Where to root server", + default=".") + (options, args) = parser.parse_args() + os.chdir(options.root) + server = TimeoutServer(("127.0.0.1", options.port), SimpleServer) + sys.stdout.write("started GTMHTTPFetcherTestServer," + " serving files from root directory %s..." % os.getcwd()); + sys.stdout.flush(); + server.serve_forever() + except KeyboardInterrupt: + print "^C received, shutting down server" + server.socket.close() + except ServerTimeoutException: + print "Too long since the last request, shutting down server" + server.socket.close() + + +if __name__ == "__main__": + main() |