aboutsummaryrefslogtreecommitdiff
path: root/Foundation
diff options
context:
space:
mode:
Diffstat (limited to 'Foundation')
-rw-r--r--Foundation/GTMBase64.h183
-rw-r--r--Foundation/GTMBase64.m697
-rw-r--r--Foundation/GTMBase64Test.m437
-rw-r--r--Foundation/GTMCalculatedRange.h13
-rw-r--r--Foundation/GTMCalculatedRange.m34
-rw-r--r--Foundation/GTMCalculatedRangeTest.m28
-rw-r--r--Foundation/GTMGeometryUtils.h27
-rw-r--r--Foundation/GTMGeometryUtils.m10
-rw-r--r--Foundation/GTMGeometryUtilsTest.m28
-rw-r--r--Foundation/GTMHTTPFetcher.h500
-rw-r--r--Foundation/GTMHTTPFetcher.m1889
-rw-r--r--Foundation/GTMHTTPFetcherTest.m543
-rw-r--r--Foundation/GTMNSData+zlib.h11
-rw-r--r--Foundation/GTMNSData+zlib.m30
-rw-r--r--Foundation/GTMNSData+zlibTest.m36
-rw-r--r--Foundation/GTMNSEnumerator+Filter.m2
-rw-r--r--Foundation/GTMNSString+HTML.m37
-rw-r--r--Foundation/GTMNSString+HTMLTest.m3
-rw-r--r--Foundation/GTMNSString+Utilities.h41
-rw-r--r--Foundation/GTMNSString+Utilities.m48
-rw-r--r--Foundation/GTMNSString+UtilitiesTest.m62
-rw-r--r--Foundation/GTMNSString+XML.m92
-rw-r--r--Foundation/GTMNSString+XMLTest.m6
-rw-r--r--Foundation/GTMObjC2Runtime.h16
-rw-r--r--Foundation/GTMObjC2Runtime.m2
-rw-r--r--Foundation/GTMObjectSingleton.h6
-rw-r--r--Foundation/GTMProgressMonitorInputStream.h73
-rw-r--r--Foundation/GTMProgressMonitorInputStream.m187
-rw-r--r--Foundation/GTMRegex.h12
-rw-r--r--Foundation/GTMRegex.m50
-rw-r--r--Foundation/GTMRegexTest.m50
-rw-r--r--Foundation/GTMScriptRunnerTest.m2
-rw-r--r--Foundation/GTMSystemVersion.h2
-rw-r--r--Foundation/GTMSystemVersion.m46
-rw-r--r--Foundation/TestData/GTMHTTPFetcherTestPage.html9
-rwxr-xr-xFoundation/TestData/GTMHTTPFetcherTestServer274
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> "
+ "&nbsp;&nbsp;&nbsp;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 = @"&nbsp;&nbsp;<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:@"&nbsp;&nbsp;<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 = @"&nbsp;&nbsp;<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:@"&nbsp;&nbsp;<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 (&lt;) and less than 11 (&thetasym;)
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ب&lt;تdef&amp;"],
@"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[] = {
@"&gt;",
};
-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(&regexData_);
+ [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()