aboutsummaryrefslogtreecommitdiff
path: root/Foundation
diff options
context:
space:
mode:
authorGravatar thomasvl <thomasvl@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2008-01-28 20:19:42 +0000
committerGravatar thomasvl <thomasvl@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2008-01-28 20:19:42 +0000
commit2a5219567634ab7ab74314ff3615132becadff4a (patch)
tree8e6f447544e5eaf460da741bf57771f929b4a70c /Foundation
initial drop of a few sources to start things out
Diffstat (limited to 'Foundation')
-rw-r--r--Foundation/GTMCalculatedRange.h101
-rw-r--r--Foundation/GTMCalculatedRange.m146
-rw-r--r--Foundation/GTMCalculatedRangeTest.m88
-rw-r--r--Foundation/GTMGarbageCollection.h43
-rw-r--r--Foundation/GTMNSData+zlib.h83
-rw-r--r--Foundation/GTMNSData+zlib.m246
-rw-r--r--Foundation/GTMNSData+zlibTest.m183
-rw-r--r--Foundation/GTMNSEnumerator+Filter.h53
-rw-r--r--Foundation/GTMNSEnumerator+Filter.m167
-rw-r--r--Foundation/GTMNSEnumerator+FilterTest.m68
-rw-r--r--Foundation/GTMNSString+HTML.h66
-rw-r--r--Foundation/GTMNSString+HTML.m514
-rw-r--r--Foundation/GTMNSString+HTMLTest.m240
-rw-r--r--Foundation/GTMNSString+XML.h39
-rw-r--r--Foundation/GTMNSString+XML.m162
-rw-r--r--Foundation/GTMNSString+XMLTest.m44
-rw-r--r--Foundation/GTMObjectSingleton.h69
-rw-r--r--Foundation/GTMSystemVersion.h45
-rw-r--r--Foundation/GTMSystemVersion.m104
-rw-r--r--Foundation/GTMSystemVersionTest.m47
20 files changed, 2508 insertions, 0 deletions
diff --git a/Foundation/GTMCalculatedRange.h b/Foundation/GTMCalculatedRange.h
new file mode 100644
index 0000000..5823ba6
--- /dev/null
+++ b/Foundation/GTMCalculatedRange.h
@@ -0,0 +1,101 @@
+//
+// GTMCalculatedRange.h
+//
+// This is a collection that allows you to calculate a value based on
+// defined stops in a range.
+//
+// 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 <Cocoa/Cocoa.h>
+
+/// Allows you to calculate a value based on defined stops in a range.
+//
+/// For example if you have a range from 0.0 to 1.0 where the stop
+/// located at 0.0 is red and the stop located at 1.0 is blue,
+/// the value based on the position 0.5 would come out as purple assuming
+/// that the valueAtPosition function calculates a purely linear mapping between
+/// the stops at 0.0 and 1.0. Stops have indices and are sorted from lowest to
+/// highest. The example above would have 2 stops. Stop 0 would be red and stop
+/// 1 would be blue.
+///
+/// Subclasses of GTMCalculatedRange are expected to override the valueAtPosition:
+/// method to return a value based on the position passed in, and the stops
+/// that are currently set in the range. Stops do not necessarily have to
+/// be the same type as the values that are calculated, but normally they are.
+@interface GTMCalculatedRange : NSObject {
+ NSMutableArray *storage_;
+}
+
+// Adds a stop to the range at |position|. If there is already a stop
+// at position |position| it is replaced.
+//
+// Args:
+// item: the object to place at |position|.
+// position: the position in the range to put |item|.
+//
+- (void)insertStop:(id)item atPosition:(float)position;
+
+// Removes a stop from the range at |position|.
+//
+// Args:
+// position: the position in the range to remove |item|.
+//
+// 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;
+
+// Removes stop |index| from the range. Stops are ordered
+// based on position where index of x < index of y if position
+// of x < position of y.
+//
+// Args:
+// item: the object to place at |position|.
+// position: the position in the range to put |item|.
+//
+- (void)removeStopAtIndex:(unsigned int)index;
+
+// Returns the number of stops in the range.
+//
+// Returns:
+// number of stops
+- (unsigned int)stopCount;
+
+// Returns the value at position |position|.
+// This function should be overridden by subclasses to calculate a
+// value for any given range.
+// The default implementation returns a value if there happens to be
+// a stop for the given position. Otherwise it returns nil.
+//
+// Args:
+// position: the position to calculate a value for.
+//
+// Returns:
+// value for position
+- (id)valueAtPosition:(float)position;
+
+// Returns the |index|'th stop and position in the set.
+// Throws an exception if out of range.
+//
+// Args:
+// index: the index of the stop
+// outPosition: a pointer to a value to be filled in with a position.
+// this can be NULL, in which case no position is returned.
+//
+// Returns:
+// the stop at the index.
+- (id)stopAtIndex:(unsigned int)index position:(float*)outPosition;
+@end
diff --git a/Foundation/GTMCalculatedRange.m b/Foundation/GTMCalculatedRange.m
new file mode 100644
index 0000000..8562d8a
--- /dev/null
+++ b/Foundation/GTMCalculatedRange.m
@@ -0,0 +1,146 @@
+//
+// GTMCalculatedRange.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 "GTMCalculatedRange.h"
+
+// Our internal storage type. It keeps track of an item and it's
+// position.
+@interface GTMCalculatedRangeStopPrivate : NSObject {
+ id item_; // the item (STRONG)
+ float position_; //
+}
++ (id)stopWithObject:(id)item position:(float)inPosition;
+- (id)initWithObject:(id)item position:(float)inPosition;
+- (id)item;
+- (float)position;
+@end
+
+
+@implementation GTMCalculatedRangeStopPrivate
++ (id)stopWithObject:(id)item position:(float)inPosition {
+ return [[[[self class] alloc] initWithObject:item position:inPosition] autorelease];
+}
+
+- (id)initWithObject:(id)item position:(float)inPosition {
+ self = [super init];
+ if (self != nil) {
+ item_ = [item retain];
+ position_ = inPosition;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [item_ release];
+ [super dealloc];
+}
+
+- (id)item {
+ return item_;
+}
+
+- (float)position {
+ return position_;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat: @"%f %@", position_, item_];
+}
+@end
+
+@implementation GTMCalculatedRange
+- (id)init {
+ self = [super init];
+ if (self != nil) {
+ storage_ = [[NSMutableArray arrayWithCapacity:0] retain];
+ }
+ return self;
+}
+- (void)dealloc {
+ [storage_ release];
+ [super dealloc];
+}
+
+- (void)insertStop:(id)item atPosition:(float)position {
+ unsigned int index = 0;
+ NSEnumerator *theEnumerator = [storage_ objectEnumerator];
+ GTMCalculatedRangeStopPrivate *theStop;
+ while (nil != (theStop = [theEnumerator nextObject])) {
+ if ([theStop position] < position) {
+ index += 1;
+ }
+ else if ([theStop position] == position) {
+ [storage_ removeObjectAtIndex:index];
+ }
+ }
+ [storage_ insertObject:[GTMCalculatedRangeStopPrivate stopWithObject:item position:position]
+ atIndex:index];
+}
+
+- (BOOL)removeStopAtPosition:(float)position {
+ unsigned int index = 0;
+ BOOL foundStop = NO;
+ NSEnumerator *theEnumerator = [storage_ objectEnumerator];
+ GTMCalculatedRangeStopPrivate *theStop;
+ while (nil != (theStop = [theEnumerator nextObject])) {
+ if ([theStop position] == position) {
+ break;
+ } else {
+ index += 1;
+ }
+ }
+ if (nil != theStop) {
+ [self removeStopAtIndex:index];
+ foundStop = YES;
+ }
+ return foundStop;
+}
+
+- (void)removeStopAtIndex:(unsigned int)index {
+ [storage_ removeObjectAtIndex:index];
+}
+
+- (unsigned int)stopCount {
+ return [storage_ count];
+}
+
+- (id)stopAtIndex:(unsigned int)index position:(float*)outPosition {
+ GTMCalculatedRangeStopPrivate *theStop = [storage_ objectAtIndex:index];
+ if (nil != outPosition) {
+ *outPosition = [theStop position];
+ }
+ return [theStop item];
+}
+
+- (id)valueAtPosition:(float)position {
+ id theValue = nil;
+ GTMCalculatedRangeStopPrivate *theStop;
+ NSEnumerator *theEnumerator = [storage_ objectEnumerator];
+ while (nil != (theStop = [theEnumerator nextObject])) {
+ if ([theStop position] == position) {
+ theValue = [theStop item];
+ break;
+ }
+ }
+ return theValue;
+}
+
+- (NSString *)description {
+ return [storage_ description];
+}
+@end
diff --git a/Foundation/GTMCalculatedRangeTest.m b/Foundation/GTMCalculatedRangeTest.m
new file mode 100644
index 0000000..cd336f0
--- /dev/null
+++ b/Foundation/GTMCalculatedRangeTest.m
@@ -0,0 +1,88 @@
+//
+// GTMCalculatedRangeTest.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 <SenTestingKit/SenTestingKit.h>
+#import "GTMCalculatedRange.h"
+#import "GTMSenTestCase.h"
+
+@interface GTMCalculatedRangeTest : SenTestCase {
+ GTMCalculatedRange *range_;
+}
+@end
+
+@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;
+
+- (void)setUp {
+ range_ = [[GTMCalculatedRange alloc] init];
+ for(unsigned int i = kStringCount; i > 0; --i) {
+ [range_ insertStop:kStrings[kStringCount - i] atPosition: 1.0f / i];
+ }
+}
+
+- (void)tearDown {
+ [range_ release];
+}
+
+- (void)testInsertStop {
+ NSString *theString = @"I smell the blood of an Englishman!";
+ [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");
+}
+
+- (void)testRemoveStopAtPosition {
+ STAssertFalse([range_ removeStopAtPosition: kOddPosition], @"Was able to remove non-existant stop");
+ STAssertTrue([range_ removeStopAtPosition: kExistingPosition], @"Was unable to remove good stop");
+ STAssertEquals([range_ stopCount], kStringCount - 1, @"Removing stop should adjust stop count");
+}
+
+- (void)testRemoveStopAtIndex {
+ STAssertThrows([range_ removeStopAtIndex: kStringCount], @"Was able to remove non-existant stop");
+ STAssertNoThrow([range_ removeStopAtIndex: kStringCount - 1], @"Was unable to remove good stop");
+ STAssertEquals([range_ stopCount], kStringCount - 1, @"Removing stop should adjust stop count");
+}
+
+- (void)testStopCount {
+ STAssertEquals([range_ stopCount], kStringCount, @"Bad stop count");
+}
+
+- (void)testValueAtPosition {
+ STAssertEqualObjects([range_ valueAtPosition: kExistingPosition], kStrings[kExisitingIndex], nil);
+ STAssertNotEqualObjects([range_ valueAtPosition: kExistingPosition], kStrings[kStringCount - 1], nil);
+ STAssertNil([range_ valueAtPosition: kOddPosition], nil);
+}
+
+- (void)testStopAtIndex {
+ float thePosition;
+
+ STAssertEqualObjects([range_ stopAtIndex:kStringCount - 1 position:nil], kStrings[kStringCount - 1], nil);
+ STAssertEqualObjects([range_ stopAtIndex:kExisitingIndex position:&thePosition], kStrings[kExisitingIndex], nil);
+ STAssertEquals(thePosition, kExistingPosition, nil);
+ STAssertNotEqualObjects([range_ stopAtIndex:kStringCount - 1 position:nil], kStrings[2], nil);
+ STAssertThrows([range_ stopAtIndex:kStringCount position:nil], nil);
+}
+
+
+@end
diff --git a/Foundation/GTMGarbageCollection.h b/Foundation/GTMGarbageCollection.h
new file mode 100644
index 0000000..4a7ac56
--- /dev/null
+++ b/Foundation/GTMGarbageCollection.h
@@ -0,0 +1,43 @@
+//
+// GTMGarbageCollection.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.
+//
+
+#import <Foundation/Foundation.h>
+
+// This allows us to easily move our code from GC to non GC.
+// They are no-ops unless we are require Leopard or above.
+// See
+// http://developer.apple.com/documentation/Cocoa/Conceptual/GarbageCollection/index.html
+// for details.
+// General use would be
+// CFTypeRef type = ...
+// return [GTMNSMakeCollectable(type) autorelease];
+
+#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1050
+
+FOUNDATION_STATIC_INLINE id GTMNSMakeCollectable(CFTypeRef cf) {
+ return NSMakeCollectable(cf);
+}
+
+#else
+
+FOUNDATION_STATIC_INLINE id GTMNSMakeCollectable(CFTypeRef cf) {
+ // NSMakeCollectable handles NULLs just fine and returns nil as expected.
+ return (id)cf;
+}
+
+#endif
diff --git a/Foundation/GTMNSData+zlib.h b/Foundation/GTMNSData+zlib.h
new file mode 100644
index 0000000..2c937c9
--- /dev/null
+++ b/Foundation/GTMNSData+zlib.h
@@ -0,0 +1,83 @@
+//
+// GTMNSData+zlib.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.
+//
+
+#import <Foundation/Foundation.h>
+
+/// Helpers for dealing w/ zlib inflate/deflate calls.
+@interface NSData (GTMZLibAdditions)
+
+/// Return an autoreleased NSData w/ the result of gzipping the bytes.
+//
+// Uses the default compression level.
++ (NSData *)gtm_dataByGzippingBytes:(const void *)bytes
+ length:(unsigned)length;
+
+/// Return an autoreleased NSData w/ the result of gzipping the payload of |data|.
+//
+// Uses the default compression level.
++ (NSData *)gtm_dataByGzippingData:(NSData *)data;
+
+/// Return an autoreleased NSData w/ the result of gzipping the bytes using |level| compression level.
+//
+// |level| can be 1-9, any other values will be clipped to that range.
++ (NSData *)gtm_dataByGzippingBytes:(const void *)bytes
+ length:(unsigned)length
+ compressionLevel:(int)level;
+
+/// Return an autoreleased NSData w/ the result of gzipping the payload of |data| using |level| compression level.
++ (NSData *)gtm_dataByGzippingData:(NSData *)data
+ compressionLevel:(int)level;
+
+// NOTE: deflate is *NOT* gzip. deflate is a "zlib" stream. pick which one
+// you really want to create. (the inflate api will handle either)
+
+/// Return an autoreleased NSData w/ the result of deflating the bytes.
+//
+// Uses the default compression level.
++ (NSData *)gtm_dataByDeflatingBytes:(const void *)bytes
+ length:(unsigned)length;
+
+/// Return an autoreleased NSData w/ the result of deflating the payload of |data|.
+//
+// Uses the default compression level.
++ (NSData *)gtm_dataByDeflatingData:(NSData *)data;
+
+/// Return an autoreleased NSData w/ the result of deflating the bytes using |level| compression level.
+//
+// |level| can be 1-9, any other values will be clipped to that range.
++ (NSData *)gtm_dataByDeflatingBytes:(const void *)bytes
+ length:(unsigned)length
+ compressionLevel:(int)level;
+
+/// Return an autoreleased NSData w/ the result of deflating the payload of |data| using |level| compression level.
++ (NSData *)gtm_dataByDeflatingData:(NSData *)data
+ compressionLevel:(int)level;
+
+
+/// Return an autoreleased NSData w/ the result of decompressing the bytes.
+//
+// The bytes to decompress can be zlib or gzip payloads.
++ (NSData *)gtm_dataByInflatingBytes:(const void *)bytes
+ length:(unsigned)length;
+
+/// Return an autoreleased NSData w/ the result of decompressing the payload of |data|.
+//
+// The data to decompress can be zlib or gzip payloads.
++ (NSData *)gtm_dataByInflatingData:(NSData *)data;
+
+@end
diff --git a/Foundation/GTMNSData+zlib.m b/Foundation/GTMNSData+zlib.m
new file mode 100644
index 0000000..89a906a
--- /dev/null
+++ b/Foundation/GTMNSData+zlib.m
@@ -0,0 +1,246 @@
+//
+// GTMNSData+zlib.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 "GTMNSData+zlib.h"
+#import <zlib.h>
+
+#define kChunkSize 1024
+
+@interface NSData (GTMZlibAdditionsPrivate)
++ (NSData *)gtm_dataByCompressingBytes:(const void *)bytes
+ length:(unsigned)length
+ compressionLevel:(int)level
+ useGzip:(BOOL)useGzip;
+@end
+
+@implementation NSData (GTMZlibAdditionsPrivate)
++ (NSData *)gtm_dataByCompressingBytes:(const void *)bytes
+ length:(unsigned)length
+ compressionLevel:(int)level
+ useGzip:(BOOL)useGzip {
+ if (!bytes || !length) return nil;
+
+ if (level < Z_BEST_SPEED)
+ level = Z_BEST_SPEED;
+ else if (level > Z_BEST_COMPRESSION)
+ level = Z_BEST_COMPRESSION;
+
+ z_stream strm;
+ bzero(&strm, sizeof(z_stream));
+
+ int windowBits = 15; // the default
+ int memLevel = 8; // the default
+ if (useGzip)
+ windowBits += 16; // enable gzip header instead of zlib header
+ int retCode;
+ if ((retCode = deflateInit2(&strm, level, Z_DEFLATED, windowBits,
+ memLevel, Z_DEFAULT_STRATEGY)) != Z_OK) {
+#ifdef DEBUG
+ NSLog(@"Failed to init for deflate w/ level %d, error %d",
+ level, retCode);
+#endif
+ return nil;
+ }
+
+ // hint the size at 1/4 the input size
+ NSMutableData *result = [NSMutableData dataWithCapacity:(length/4)];
+ unsigned char output[kChunkSize];
+
+ // setup the input
+ strm.avail_in = length;
+ strm.next_in = (unsigned char*)bytes;
+
+ // loop to collect the data
+ do {
+ // update what we're passing in
+ strm.avail_out = kChunkSize;
+ strm.next_out = output;
+ retCode = deflate(&strm, Z_FINISH);
+ if ((retCode != Z_OK) && (retCode != Z_STREAM_END)) {
+#ifdef DEBUG
+ NSLog(@"Error trying to deflate some of the payload, error %d",
+ retCode);
+#endif
+ deflateEnd(&strm);
+ return nil;
+ }
+ // collect what we got
+ unsigned gotBack = kChunkSize - strm.avail_out;
+ if (gotBack > 0) {
+ [result appendBytes:output length:gotBack];
+ }
+
+ } while (retCode == Z_OK);
+
+#ifdef DEBUG
+ if (strm.avail_in != 0) {
+ NSLog(@"thought we finished deflate w/o using all input, %u bytes left",
+ strm.avail_in);
+ }
+ if (retCode != Z_STREAM_END) {
+ NSLog(@"thought we finished deflate w/o getting a result of stream end, code %d",
+ retCode);
+ }
+#endif
+
+ // clean up
+ deflateEnd(&strm);
+
+ return result;
+} // gtm_dataByCompressingBytes:length:compressionLevel:useGzip:
+
+
+@end
+
+
+@implementation NSData (GTMZLibAdditions)
+
++ (NSData *)gtm_dataByGzippingBytes:(const void *)bytes
+ length:(unsigned)length {
+ return [self gtm_dataByCompressingBytes:bytes
+ length:length
+ compressionLevel:Z_DEFAULT_COMPRESSION
+ useGzip:YES];
+} // gtm_dataByGzippingBytes:length:
+
++ (NSData *)gtm_dataByGzippingData:(NSData *)data {
+ return [self gtm_dataByCompressingBytes:[data bytes]
+ length:[data length]
+ compressionLevel:Z_DEFAULT_COMPRESSION
+ useGzip:YES];
+} // gtm_dataByGzippingData:
+
++ (NSData *)gtm_dataByGzippingBytes:(const void *)bytes
+ length:(unsigned)length
+ compressionLevel:(int)level {
+ return [self gtm_dataByCompressingBytes:bytes
+ length:length
+ compressionLevel:level
+ useGzip:YES];
+} // gtm_dataByGzippingBytes:length:level:
+
++ (NSData *)gtm_dataByGzippingData:(NSData *)data
+ compressionLevel:(int)level {
+ return [self gtm_dataByCompressingBytes:[data bytes]
+ length:[data length]
+ compressionLevel:level
+ useGzip:YES];
+} // gtm_dataByGzippingData:level:
+
++ (NSData *)gtm_dataByDeflatingBytes:(const void *)bytes
+ length:(unsigned)length {
+ return [self gtm_dataByCompressingBytes:bytes
+ length:length
+ compressionLevel:Z_DEFAULT_COMPRESSION
+ useGzip:NO];
+} // gtm_dataByDeflatingBytes:length:
+
++ (NSData *)gtm_dataByDeflatingData:(NSData *)data {
+ return [self gtm_dataByCompressingBytes:[data bytes]
+ length:[data length]
+ compressionLevel:Z_DEFAULT_COMPRESSION
+ useGzip:NO];
+} // gtm_dataByDeflatingData:
+
++ (NSData *)gtm_dataByDeflatingBytes:(const void *)bytes
+ length:(unsigned)length
+ compressionLevel:(int)level {
+ return [self gtm_dataByCompressingBytes:bytes
+ length:length
+ compressionLevel:level
+ useGzip:NO];
+} // gtm_dataByDeflatingBytes:length:level:
+
++ (NSData *)gtm_dataByDeflatingData:(NSData *)data
+ compressionLevel:(int)level {
+ return [self gtm_dataByCompressingBytes:[data bytes]
+ length:[data length]
+ compressionLevel:level
+ useGzip:NO];
+} // gtm_dataByDeflatingData:level:
+
++ (NSData *)gtm_dataByInflatingBytes:(const void *)bytes
+ length:(unsigned)length {
+ if (!bytes || !length) return nil;
+
+ z_stream strm;
+ bzero(&strm, sizeof(z_stream));
+
+ // setup the input
+ strm.avail_in = length;
+ strm.next_in = (unsigned char*)bytes;
+
+ int windowBits = 15; // 15 to enable any window size
+ windowBits += 32; // and +32 to enable zlib or gzip header detection.
+ int retCode;
+ if ((retCode = inflateInit2(&strm, windowBits)) != Z_OK) {
+#ifdef DEBUG
+ NSLog(@"Failed to init for inflate, error %d", retCode);
+#endif
+ return nil;
+ }
+
+ // hint the size at 4x the input size
+ NSMutableData *result = [NSMutableData dataWithCapacity:(length*4)];
+ unsigned char output[kChunkSize];
+
+ // loop to collect the data
+ do {
+ // update what we're passing in
+ strm.avail_out = kChunkSize;
+ strm.next_out = output;
+ retCode = inflate(&strm, Z_NO_FLUSH);
+ if ((retCode != Z_OK) && (retCode != Z_STREAM_END)) {
+#ifdef DEBUG
+ NSLog(@"Error trying to inflate some of the payload, error %d",
+ retCode);
+#endif
+ inflateEnd(&strm);
+ return nil;
+ }
+ // collect what we got
+ unsigned gotBack = kChunkSize - strm.avail_out;
+ if (gotBack > 0) {
+ [result appendBytes:output length:gotBack];
+ }
+
+ } while (retCode == Z_OK);
+
+#ifdef DEBUG
+ if (strm.avail_in != 0) {
+ NSLog(@"thought we finished inflate w/o using all input, %u bytes left",
+ strm.avail_in);
+ }
+ if (retCode != Z_STREAM_END) {
+ NSLog(@"thought we finished inflate w/o getting a result of stream end, code %d",
+ retCode);
+ }
+#endif
+
+ // clean up
+ inflateEnd(&strm);
+
+ return result;
+} // gtm_dataByInflatingBytes:length:
+
++ (NSData *)gtm_dataByInflatingData:(NSData *)data {
+ return [self gtm_dataByInflatingBytes:[data bytes]
+ length:[data length]];
+} // gtm_dataByInflatingData:
+
+@end
diff --git a/Foundation/GTMNSData+zlibTest.m b/Foundation/GTMNSData+zlibTest.m
new file mode 100644
index 0000000..f18735d
--- /dev/null
+++ b/Foundation/GTMNSData+zlibTest.m
@@ -0,0 +1,183 @@
+//
+// GTMNSData+zlibTest.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 "GTMNSData+zlib.h"
+#import <stdlib.h> // for randiom/srandomdev
+#import <zlib.h>
+
+#import <SenTestingKit/SenTestingKit.h>
+
+@interface GTMNSData_zlibTest : SenTestCase
+@end
+
+
+static void FillWithRandom(char *data, unsigned long len) {
+ char *max = data + len;
+ for ( ; data < max ; ++data) {
+ *data = random() & 0xFF;
+ }
+}
+
+static BOOL HasGzipHeader(NSData *data) {
+ // very simple check
+ if ([data length] > 2) {
+ const unsigned char *bytes = [data bytes];
+ return (bytes[0] == 0x1f) && (bytes[1] == 0x8b);
+ }
+ return NO;
+}
+
+
+@implementation GTMNSData_zlibTest
+
+- (void)setUp {
+ // seed random from /dev/random
+ srandomdev();
+}
+
+- (void)testInflateDeflate {
+ // generate a range of sizes w/ random content
+ for (int n = 0 ; n < 2 ; ++n) {
+ for (int x = 1 ; x < 128 ; ++x) {
+ NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init];
+ STAssertNotNil(localPool, @"failed to alloc local pool");
+
+ NSMutableData *data = [NSMutableData data];
+ STAssertNotNil(data, @"failed to alloc data block");
+
+ // first pass small blocks, second pass, larger ones, but second pass
+ // avoid making them multimples of 128.
+ [data setLength:((n*x*128) + x)];
+ FillWithRandom([data mutableBytes], [data length]);
+
+ // w/ *Bytes apis, default level
+ NSData *deflated = [NSData gtm_dataByDeflatingBytes:[data bytes] length:[data length]];
+ STAssertNotNil(deflated, @"failed to deflate data block");
+ STAssertTrue([deflated length] > 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");
+ STAssertTrue([dataPrime length] > 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");
+ STAssertTrue([deflated length] > 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");
+ STAssertTrue([dataPrime length] > 0, @"failed to inflate data block");
+ STAssertEqualObjects(data, dataPrime, @"failed to round trip via *Data apis");
+
+ // loop over the compression levels
+ for (int level = 1 ; level < 9 ; ++level) {
+ // w/ *Bytes apis, using our level
+ deflated = [NSData gtm_dataByDeflatingBytes:[data bytes]
+ length:[data length]
+ compressionLevel:level];
+ STAssertNotNil(deflated, @"failed to deflate data block");
+ STAssertTrue([deflated length] > 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");
+ STAssertTrue([dataPrime length] > 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");
+ STAssertTrue([deflated length] > 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");
+ STAssertTrue([dataPrime length] > 0, @"failed to inflate data block");
+ STAssertEqualObjects(data, dataPrime, @"failed to round trip via *Data apis");
+ }
+
+ [localPool release];
+ }
+ }
+}
+
+- (void)testInflateGzip {
+ // generate a range of sizes w/ random content
+ for (int n = 0 ; n < 2 ; ++n) {
+ for (int x = 1 ; x < 128 ; ++x) {
+ NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init];
+ STAssertNotNil(localPool, @"failed to alloc local pool");
+
+ NSMutableData *data = [NSMutableData data];
+ STAssertNotNil(data, @"failed to alloc data block");
+
+ // first pass small blocks, second pass, larger ones, but second pass
+ // avoid making them multimples of 128.
+ [data setLength:((n*x*128) + x)];
+ FillWithRandom([data mutableBytes], [data length]);
+
+ // w/ *Bytes apis, default level
+ NSData *gzipped = [NSData gtm_dataByGzippingBytes:[data bytes] length:[data length]];
+ STAssertNotNil(gzipped, @"failed to gzip data block");
+ STAssertTrue([gzipped length] > 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");
+ STAssertTrue([dataPrime length] > 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");
+ STAssertTrue([gzipped length] > 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");
+ STAssertTrue([dataPrime length] > 0, @"failed to inflate data block");
+ STAssertEqualObjects(data, dataPrime, @"failed to round trip via *Data apis");
+
+ // loop over the compression levels
+ for (int level = 1 ; level < 9 ; ++level) {
+ // w/ *Bytes apis, using our level
+ gzipped = [NSData gtm_dataByGzippingBytes:[data bytes]
+ length:[data length]
+ compressionLevel:level];
+ STAssertNotNil(gzipped, @"failed to gzip data block");
+ STAssertTrue([gzipped length] > 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");
+ STAssertTrue([dataPrime length] > 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");
+ STAssertTrue([gzipped length] > 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");
+ STAssertTrue([dataPrime length] > 0, @"failed to inflate data block");
+ STAssertEqualObjects(data, dataPrime, @"failed to round trip via *Data apis");
+ }
+
+ [localPool release];
+ }
+ }
+}
+
+@end
diff --git a/Foundation/GTMNSEnumerator+Filter.h b/Foundation/GTMNSEnumerator+Filter.h
new file mode 100644
index 0000000..8e10d93
--- /dev/null
+++ b/Foundation/GTMNSEnumerator+Filter.h
@@ -0,0 +1,53 @@
+//
+// GTMNSEnumerator+Filter.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.
+//
+
+#import <Foundation/Foundation.h>
+
+/// A generic category for methods that allow us to filter enumeratable
+/// containers, inspired by C++ Standard Library's use of iterators.
+/// Like in C++, these assume the underlying container is not modified during
+/// the lifetime of the iterator.
+///
+@interface NSEnumerator (GTMEnumeratorFilterAdditions)
+
+/// @argument predicate - the function return BOOL. will be applied to each element
+/// @argument argument - optional argument to pass to predicate
+/// @returns an enumerator that contains only elements where [element sel:argument] is true
+- (NSEnumerator *)gtm_filteredEnumeratorByMakingEachObjectPerformSelector:(SEL)predicate
+ withObject:(id)argument;
+
+/// @argument selector - the function return a transformed object. will be applied to each element
+/// @argument argument - optional argument to pass to transformer
+/// @returns an enumerator that contains the transformed elements
+- (NSEnumerator *)gtm_enumeratorByMakingEachObjectPerformSelector:(SEL)selector
+ withObject:(id)argument;
+
+/// @argument target - receiver for each method
+/// @argument predicate - as in, [target predicate: [self nextObject]], return a BOOL
+/// @returns an enumerator that contains only elements where [element sel:argument] is true
+- (NSEnumerator *)gtm_filteredEnumeratorByTarget:(id)target
+ performOnEachSelector:(SEL)predicate;
+
+/// @argument target - receiver for each method
+/// @argument sel - as in, [target selector: [self nextObject]], return a transformed object
+/// @returns an enumerator that contains the transformed elements
+- (NSEnumerator *)gtm_enumeratorByTarget:(id)target
+ performOnEachSelector:(SEL)selector;
+
+@end
+
diff --git a/Foundation/GTMNSEnumerator+Filter.m b/Foundation/GTMNSEnumerator+Filter.m
new file mode 100644
index 0000000..60c85f3
--- /dev/null
+++ b/Foundation/GTMNSEnumerator+Filter.m
@@ -0,0 +1,167 @@
+//
+// GTMNSEnumerator+Filter.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 "GTMNSEnumerator+Filter.h"
+
+// a private subclass of NSEnumerator that does all the work.
+// public interface just returns one of these.
+// This top level class contains all the additional boilerplate. Specific
+// behavior is in the subclasses.
+@interface GTMEnumerator : NSEnumerator {
+ @protected
+ NSEnumerator *base_; // STRONG
+ SEL operation_; // either a predicate or a transform depending on context.
+ id other_; // STRONG, may be nil
+}
+- (id)nextObject;
+- (BOOL)filterObject:(id)obj returning:(id *)resultp;
+@end
+@implementation GTMEnumerator
+- (id)initWithBase:(NSEnumerator *)base
+ sel:(SEL)filter
+ object:(id)optionalOther {
+ self = [super init];
+ if (self) {
+
+ // specializing a nil enumerator returns nil.
+ if (nil == base) {
+ [self release];
+ return nil;
+ }
+
+ base_ = [base retain];
+ operation_ = filter;
+ other_ = [optionalOther retain];
+ }
+ return self;
+}
+
+// it is an error to call this initializer.
+- (id)init {
+ [self doesNotRecognizeSelector:_cmd];
+ return nil;
+}
+
+- (void)dealloc {
+ [base_ release];
+ [other_ release];
+ [super dealloc];
+}
+
+- (id)nextObject {
+ for (id obj = [base_ nextObject]; obj; obj = [base_ nextObject]) {
+ id result = nil;
+ if ([self filterObject:obj returning:&result]) {
+ return result;
+ }
+ }
+ return nil;
+}
+
+// subclasses must override
+- (BOOL)filterObject:(id)obj returning:(id *)resultp {
+ [self doesNotRecognizeSelector:_cmd];
+ return NO;
+}
+@end
+
+// a transformer, for each item in the enumerator, returns a f(item).
+@interface GTMEnumeratorTransformer : GTMEnumerator
+- (BOOL)filterObject:(id)obj returning:(id *)resultp;
+@end
+@implementation GTMEnumeratorTransformer
+- (BOOL)filterObject:(id)obj returning:(id *)resultp {
+ *resultp = [obj performSelector:operation_ withObject:other_];
+ return nil != *resultp;
+}
+@end
+
+// a transformer, for each item in the enumerator, returns a f(item).
+// a target transformer swaps the target and the argument.
+@interface GTMEnumeratorTargetTransformer : GTMEnumerator
+- (BOOL)filterObject:(id)obj returning:(id *)resultp;
+@end
+@implementation GTMEnumeratorTargetTransformer
+- (BOOL)filterObject:(id)obj returning:(id *)resultp {
+ *resultp = [other_ performSelector:operation_ withObject:obj];
+ return nil != *resultp;
+}
+@end
+
+// a filter, for each item in the enumerator, if(f(item)) { returns item. }
+@interface GTMEnumeratorFilter : GTMEnumerator
+- (BOOL)filterObject:(id)obj returning:(id *)resultp;
+@end
+@implementation GTMEnumeratorFilter
+// We must take care here, since Intel leaves junk in high bytes of return register
+// for predicates that return BOOL.
+- (BOOL)filterObject:(id)obj returning:(id *)resultp {
+ *resultp = obj;
+ // intptr_t is an integer the same size as a pointer. <stdint.h>
+ return (BOOL) (intptr_t) [obj performSelector:operation_ withObject:other_];
+}
+@end
+
+// a target filter, for each item in the enumerator, if(f(item)) { returns item. }
+// a target transformer swaps the target and the argument.
+@interface GTMEnumeratorTargetFilter : GTMEnumerator
+- (BOOL)filterObject:(id)obj returning:(id *)resultp;
+@end
+@implementation GTMEnumeratorTargetFilter
+// We must take care here, since Intel leaves junk in high bytes of return register
+// for predicates that return BOOL.
+- (BOOL)filterObject:(id)obj returning:(id *)resultp {
+ *resultp = obj;
+ // intptr_t is an integer the same size as a pointer. <stdint.h>
+ return (BOOL) (intptr_t) [other_ performSelector:operation_ withObject:obj];
+}
+@end
+
+@implementation NSEnumerator (GTMEnumeratorFilterAdditions)
+
+- (NSEnumerator *)gtm_filteredEnumeratorByMakingEachObjectPerformSelector:(SEL)selector
+ withObject:(id)argument {
+ return [[[GTMEnumeratorFilter alloc] initWithBase:self
+ sel:selector
+ object:argument] autorelease];
+}
+
+- (NSEnumerator *)gtm_enumeratorByMakingEachObjectPerformSelector:(SEL)selector
+ withObject:(id)argument {
+ return [[[GTMEnumeratorTransformer alloc] initWithBase:self
+ sel:selector
+ object:argument] autorelease];
+}
+
+
+- (NSEnumerator *)gtm_filteredEnumeratorByTarget:(id)target
+ performOnEachSelector:(SEL)selector {
+ return [[[GTMEnumeratorTargetFilter alloc] initWithBase:self
+ sel:selector
+ object:target] autorelease];
+}
+
+- (NSEnumerator *)gtm_enumeratorByTarget:(id)target
+ performOnEachSelector:(SEL)selector {
+ return [[[GTMEnumeratorTargetTransformer alloc] initWithBase:self
+ sel:selector
+ object:target] autorelease];
+}
+
+@end
+
diff --git a/Foundation/GTMNSEnumerator+FilterTest.m b/Foundation/GTMNSEnumerator+FilterTest.m
new file mode 100644
index 0000000..99bde01
--- /dev/null
+++ b/Foundation/GTMNSEnumerator+FilterTest.m
@@ -0,0 +1,68 @@
+//
+// GTMNSEnumerator+FilterTest.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 "GTMNSEnumerator+Filter.h"
+
+@interface GTMNSEnumerator_FilterTest : SenTestCase
+@end
+
+@implementation GTMNSEnumerator_FilterTest
+
+// test using an NSSet enumerator.
+- (void)testEnumeratorByMakingEachObjectPerformSelector {
+ NSSet *numbers = [NSSet setWithObjects: @"1", @"2", @"3", nil];
+ NSEnumerator *e = [[numbers objectEnumerator]
+ gtm_enumeratorByMakingEachObjectPerformSelector:@selector(stringByAppendingString:)
+ withObject:@" "];
+ NSMutableSet *trailingSpaces = [NSMutableSet set];
+ id obj;
+ while (nil != (obj = [e nextObject])) {
+ [trailingSpaces addObject:obj];
+ }
+ NSSet *trailingSpacesGood = [NSSet setWithObjects: @"1 ", @"2 ", @"3 ", nil];
+ STAssertEqualObjects(trailingSpaces, trailingSpacesGood, @"");
+ NSSet *empty = [NSSet set];
+ NSEnumerator *ee = [[empty objectEnumerator]
+ gtm_enumeratorByMakingEachObjectPerformSelector:@selector(stringByAppendingString:)
+ withObject:@" "];
+
+ NSMutableSet *emptySpaces = [NSMutableSet set];
+ while (nil != (obj = [ee nextObject])) {
+ [emptySpaces addObject:obj];
+ }
+ STAssertEqualObjects(empty, emptySpaces, @"");
+}
+
+// test using an NSDictionary enumerator.
+- (void)testFilteredEnumeratorByMakingEachObjectPerformSelector {
+ NSDictionary *numbers = [NSDictionary dictionaryWithObjectsAndKeys: @"1", @"1", @"", @"", @"3", @"3", nil];
+
+ // |length| filters out length 0 objects
+ NSEnumerator *e = [[numbers objectEnumerator]
+ gtm_filteredEnumeratorByMakingEachObjectPerformSelector:@selector(length)
+ withObject:nil];
+
+ NSArray *lengths = [e allObjects];
+ NSArray *lengthsGood = [NSArray arrayWithObjects: @"1", @"3", nil];
+ STAssertEqualObjects(lengths, lengthsGood, @"");
+}
+
+
+
+@end
diff --git a/Foundation/GTMNSString+HTML.h b/Foundation/GTMNSString+HTML.h
new file mode 100644
index 0000000..1273cc3
--- /dev/null
+++ b/Foundation/GTMNSString+HTML.h
@@ -0,0 +1,66 @@
+//
+// GTMNSString+HTML.h
+// Dealing with NSStrings that contain HTML
+//
+// 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>
+
+/// Utilities for NSStrings containing HTML
+@interface NSString (GTMNSStringHTMLAdditions)
+
+/// Get a string where internal characters that need escaping for HTML are escaped
+//
+/// For example, '&' become '&amp;'. This will only cover characters from table
+/// A.2.2 of http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters
+/// which is what you want for a unicode encoded webpage. If you have a ascii
+/// or non-encoded webpage, please use stringByEscapingAsciiHTML which will
+/// encode all characters.
+///
+/// For obvious reasons this call is only safe once.
+//
+// Returns:
+// Autoreleased NSString
+//
+- (NSString *)gtm_stringByEscapingForHTML;
+
+/// Get a string where internal characters that need escaping for HTML are escaped
+//
+/// For example, '&' become '&amp;'
+/// All non-mapped characters (unicode that don't have a &keyword; mapping)
+/// will be converted to the appropriate &#xxx; value. If your webpage is
+/// unicode encoded (UTF16 or UTF8) use stringByEscapingHTML instead as it is
+/// faster, and produces less bloated and more readable HTML (as long as you
+/// are using a unicode compliant HTML reader).
+///
+/// For obvious reasons this call is only safe once.
+//
+// Returns:
+// Autoreleased NSString
+//
+- (NSString *)gtm_stringByEscapingForAsciiHTML;
+
+/// Get a string where internal characters that are escaped for HTML are unescaped
+//
+/// For example, '&amp;' becomes '&'
+/// Handles &#32; and &#x32; cases as well
+///
+// Returns:
+// Autoreleased NSString
+//
+- (NSString *)gtm_stringByUnescapingFromHTML;
+
+@end
diff --git a/Foundation/GTMNSString+HTML.m b/Foundation/GTMNSString+HTML.m
new file mode 100644
index 0000000..f9e99dc
--- /dev/null
+++ b/Foundation/GTMNSString+HTML.m
@@ -0,0 +1,514 @@
+//
+// GTMNSString+HTML.m
+// Dealing with NSStrings that contain HTML
+//
+// 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 "GTMNSString+HTML.h"
+
+typedef struct {
+ NSString *escapeSequence;
+ unichar uchar;
+} HTMLEscapeMap;
+
+// Taken from http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters
+// Ordered by uchar lowest to highest for bsearching
+static HTMLEscapeMap gAsciiHTMLEscapeMap[] = {
+ // A.2.2. Special characters
+ { @"&quot;", 34 },
+ { @"&amp;", 38 },
+ { @"&apos;", 39 },
+ { @"&lt;", 60 },
+ { @"&gt;", 62 },
+
+ // A.2.1. Latin-1 characters
+ { @"&nbsp;", 160 },
+ { @"&iexcl;", 161 },
+ { @"&cent;", 162 },
+ { @"&pound;", 163 },
+ { @"&curren;", 164 },
+ { @"&yen;", 165 },
+ { @"&brvbar;", 166 },
+ { @"&sect;", 167 },
+ { @"&uml;", 168 },
+ { @"&copy;", 169 },
+ { @"&ordf;", 170 },
+ { @"&laquo;", 171 },
+ { @"&not;", 172 },
+ { @"&shy;", 173 },
+ { @"&reg;", 174 },
+ { @"&macr;", 175 },
+ { @"&deg;", 176 },
+ { @"&plusmn;", 177 },
+ { @"&sup2;", 178 },
+ { @"&sup3;", 179 },
+ { @"&acute;", 180 },
+ { @"&micro;", 181 },
+ { @"&para;", 182 },
+ { @"&middot;", 183 },
+ { @"&cedil;", 184 },
+ { @"&sup1;", 185 },
+ { @"&ordm;", 186 },
+ { @"&raquo;", 187 },
+ { @"&frac14;", 188 },
+ { @"&frac12;", 189 },
+ { @"&frac34;", 190 },
+ { @"&iquest;", 191 },
+ { @"&Agrave;", 192 },
+ { @"&Aacute;", 193 },
+ { @"&Acirc;", 194 },
+ { @"&Atilde;", 195 },
+ { @"&Auml;", 196 },
+ { @"&Aring;", 197 },
+ { @"&AElig;", 198 },
+ { @"&Ccedil;", 199 },
+ { @"&Egrave;", 200 },
+ { @"&Eacute;", 201 },
+ { @"&Ecirc;", 202 },
+ { @"&Euml;", 203 },
+ { @"&Igrave;", 204 },
+ { @"&Iacute;", 205 },
+ { @"&Icirc;", 206 },
+ { @"&Iuml;", 207 },
+ { @"&ETH;", 208 },
+ { @"&Ntilde;", 209 },
+ { @"&Ograve;", 210 },
+ { @"&Oacute;", 211 },
+ { @"&Ocirc;", 212 },
+ { @"&Otilde;", 213 },
+ { @"&Ouml;", 214 },
+ { @"&times;", 215 },
+ { @"&Oslash;", 216 },
+ { @"&Ugrave;", 217 },
+ { @"&Uacute;", 218 },
+ { @"&Ucirc;", 219 },
+ { @"&Uuml;", 220 },
+ { @"&Yacute;", 221 },
+ { @"&THORN;", 222 },
+ { @"&szlig;", 223 },
+ { @"&agrave;", 224 },
+ { @"&aacute;", 225 },
+ { @"&acirc;", 226 },
+ { @"&atilde;", 227 },
+ { @"&auml;", 228 },
+ { @"&aring;", 229 },
+ { @"&aelig;", 230 },
+ { @"&ccedil;", 231 },
+ { @"&egrave;", 232 },
+ { @"&eacute;", 233 },
+ { @"&ecirc;", 234 },
+ { @"&euml;", 235 },
+ { @"&igrave;", 236 },
+ { @"&iacute;", 237 },
+ { @"&icirc;", 238 },
+ { @"&iuml;", 239 },
+ { @"&eth;", 240 },
+ { @"&ntilde;", 241 },
+ { @"&ograve;", 242 },
+ { @"&oacute;", 243 },
+ { @"&ocirc;", 244 },
+ { @"&otilde;", 245 },
+ { @"&ouml;", 246 },
+ { @"&divide;", 247 },
+ { @"&oslash;", 248 },
+ { @"&ugrave;", 249 },
+ { @"&uacute;", 250 },
+ { @"&ucirc;", 251 },
+ { @"&uuml;", 252 },
+ { @"&yacute;", 253 },
+ { @"&thorn;", 254 },
+ { @"&yuml;", 255 },
+
+ // A.2.2. Special characters cont'd
+ { @"&OElig;", 338 },
+ { @"&oelig;", 339 },
+ { @"&Scaron;", 352 },
+ { @"&scaron;", 353 },
+ { @"&Yuml;", 376 },
+
+ // A.2.3. Symbols
+ { @"&fnof;", 402 },
+
+ // A.2.2. Special characters cont'd
+ { @"&circ;", 710 },
+ { @"&tilde;", 732 },
+
+ // A.2.3. Symbols cont'd
+ { @"&Alpha;", 913 },
+ { @"&Beta;", 914 },
+ { @"&Gamma;", 915 },
+ { @"&Delta;", 916 },
+ { @"&Epsilon;", 917 },
+ { @"&Zeta;", 918 },
+ { @"&Eta;", 919 },
+ { @"&Theta;", 920 },
+ { @"&Iota;", 921 },
+ { @"&Kappa;", 922 },
+ { @"&Lambda;", 923 },
+ { @"&Mu;", 924 },
+ { @"&Nu;", 925 },
+ { @"&Xi;", 926 },
+ { @"&Omicron;", 927 },
+ { @"&Pi;", 928 },
+ { @"&Rho;", 929 },
+ { @"&Sigma;", 931 },
+ { @"&Tau;", 932 },
+ { @"&Upsilon;", 933 },
+ { @"&Phi;", 934 },
+ { @"&Chi;", 935 },
+ { @"&Psi;", 936 },
+ { @"&Omega;", 937 },
+ { @"&alpha;", 945 },
+ { @"&beta;", 946 },
+ { @"&gamma;", 947 },
+ { @"&delta;", 948 },
+ { @"&epsilon;", 949 },
+ { @"&zeta;", 950 },
+ { @"&eta;", 951 },
+ { @"&theta;", 952 },
+ { @"&iota;", 953 },
+ { @"&kappa;", 954 },
+ { @"&lambda;", 955 },
+ { @"&mu;", 956 },
+ { @"&nu;", 957 },
+ { @"&xi;", 958 },
+ { @"&omicron;", 959 },
+ { @"&pi;", 960 },
+ { @"&rho;", 961 },
+ { @"&sigmaf;", 962 },
+ { @"&sigma;", 963 },
+ { @"&tau;", 964 },
+ { @"&upsilon;", 965 },
+ { @"&phi;", 966 },
+ { @"&chi;", 967 },
+ { @"&psi;", 968 },
+ { @"&omega;", 969 },
+ { @"&thetasym;", 977 },
+ { @"&upsih;", 978 },
+ { @"&piv;", 982 },
+
+ // A.2.2. Special characters cont'd
+ { @"&ensp;", 8194 },
+ { @"&emsp;", 8195 },
+ { @"&thinsp;", 8201 },
+ { @"&zwnj;", 8204 },
+ { @"&zwj;", 8205 },
+ { @"&lrm;", 8206 },
+ { @"&rlm;", 8207 },
+ { @"&ndash;", 8211 },
+ { @"&mdash;", 8212 },
+ { @"&lsquo;", 8216 },
+ { @"&rsquo;", 8217 },
+ { @"&sbquo;", 8218 },
+ { @"&ldquo;", 8220 },
+ { @"&rdquo;", 8221 },
+ { @"&bdquo;", 8222 },
+ { @"&dagger;", 8224 },
+ { @"&Dagger;", 8225 },
+ // A.2.3. Symbols cont'd
+ { @"&bull;", 8226 },
+ { @"&hellip;", 8230 },
+
+ // A.2.2. Special characters cont'd
+ { @"&permil;", 8240 },
+
+ // A.2.3. Symbols cont'd
+ { @"&prime;", 8242 },
+ { @"&Prime;", 8243 },
+
+ // A.2.2. Special characters cont'd
+ { @"&lsaquo;", 8249 },
+ { @"&rsaquo;", 8250 },
+
+ // A.2.3. Symbols cont'd
+ { @"&oline;", 8254 },
+ { @"&frasl;", 8260 },
+
+ // A.2.2. Special characters cont'd
+ { @"&euro;", 8364 },
+
+ // A.2.3. Symbols cont'd
+ { @"&image;", 8465 },
+ { @"&weierp;", 8472 },
+ { @"&real;", 8476 },
+ { @"&trade;", 8482 },
+ { @"&alefsym;", 8501 },
+ { @"&larr;", 8592 },
+ { @"&uarr;", 8593 },
+ { @"&rarr;", 8594 },
+ { @"&darr;", 8595 },
+ { @"&harr;", 8596 },
+ { @"&crarr;", 8629 },
+ { @"&lArr;", 8656 },
+ { @"&uArr;", 8657 },
+ { @"&rArr;", 8658 },
+ { @"&dArr;", 8659 },
+ { @"&hArr;", 8660 },
+ { @"&forall;", 8704 },
+ { @"&part;", 8706 },
+ { @"&exist;", 8707 },
+ { @"&empty;", 8709 },
+ { @"&nabla;", 8711 },
+ { @"&isin;", 8712 },
+ { @"&notin;", 8713 },
+ { @"&ni;", 8715 },
+ { @"&prod;", 8719 },
+ { @"&sum;", 8721 },
+ { @"&minus;", 8722 },
+ { @"&lowast;", 8727 },
+ { @"&radic;", 8730 },
+ { @"&prop;", 8733 },
+ { @"&infin;", 8734 },
+ { @"&ang;", 8736 },
+ { @"&and;", 8743 },
+ { @"&or;", 8744 },
+ { @"&cap;", 8745 },
+ { @"&cup;", 8746 },
+ { @"&int;", 8747 },
+ { @"&there4;", 8756 },
+ { @"&sim;", 8764 },
+ { @"&cong;", 8773 },
+ { @"&asymp;", 8776 },
+ { @"&ne;", 8800 },
+ { @"&equiv;", 8801 },
+ { @"&le;", 8804 },
+ { @"&ge;", 8805 },
+ { @"&sub;", 8834 },
+ { @"&sup;", 8835 },
+ { @"&nsub;", 8836 },
+ { @"&sube;", 8838 },
+ { @"&supe;", 8839 },
+ { @"&oplus;", 8853 },
+ { @"&otimes;", 8855 },
+ { @"&perp;", 8869 },
+ { @"&sdot;", 8901 },
+ { @"&lceil;", 8968 },
+ { @"&rceil;", 8969 },
+ { @"&lfloor;", 8970 },
+ { @"&rfloor;", 8971 },
+ { @"&lang;", 9001 },
+ { @"&rang;", 9002 },
+ { @"&loz;", 9674 },
+ { @"&spades;", 9824 },
+ { @"&clubs;", 9827 },
+ { @"&hearts;", 9829 },
+ { @"&diams;", 9830 }
+};
+
+// Taken from http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters
+// This is table A.2.2 Special Characters
+static HTMLEscapeMap gUnicodeHTMLEscapeMap[] = {
+ // C0 Controls and Basic Latin
+ { @"&quot;", 34 },
+ { @"&amp;", 38 },
+ { @"&apos;", 39 },
+ { @"&lt;", 60 },
+ { @"&gt;", 62 },
+
+ // Latin Extended-A
+ { @"&OElig;", 338 },
+ { @"&oelig;", 339 },
+ { @"&Scaron;", 352 },
+ { @"&scaron;", 353 },
+ { @"&Yuml;", 376 },
+
+ // Spacing Modifier Letters
+ { @"&circ;", 710 },
+ { @"&tilde;", 732 },
+
+ // General Punctuation
+ { @"&ensp;", 8194 },
+ { @"&emsp;", 8195 },
+ { @"&thinsp;", 8201 },
+ { @"&zwnj;", 8204 },
+ { @"&zwj;", 8205 },
+ { @"&lrm;", 8206 },
+ { @"&rlm;", 8207 },
+ { @"&ndash;", 8211 },
+ { @"&mdash;", 8212 },
+ { @"&lsquo;", 8216 },
+ { @"&rsquo;", 8217 },
+ { @"&sbquo;", 8218 },
+ { @"&ldquo;", 8220 },
+ { @"&rdquo;", 8221 },
+ { @"&bdquo;", 8222 },
+ { @"&dagger;", 8224 },
+ { @"&Dagger;", 8225 },
+ { @"&permil;", 8240 },
+ { @"&lsaquo;", 8249 },
+ { @"&rsaquo;", 8250 },
+ { @"&euro;", 8364 },
+};
+
+
+// Utility function for Bsearching table above
+static int escapeMapCompare(const void *ucharVoid, const void *mapVoid) {
+ unichar *uchar = (unichar*)ucharVoid;
+ HTMLEscapeMap *map = (HTMLEscapeMap*)mapVoid;
+ int val;
+ if (*uchar > map->uchar) {
+ val = 1;
+ } else if (*uchar < map->uchar) {
+ val = -1;
+ } else {
+ val = 0;
+ }
+ return val;
+}
+
+@implementation NSString (GTMNSStringHTMLAdditions)
+
+- (NSString *)gtm_stringByEscapingHTMLUsingTable:(HTMLEscapeMap*)table
+ ofSize:(int)size
+ escapingUnicode:(BOOL)escapeUnicode {
+ NSMutableString *finalString = [NSMutableString string];
+ int length = [self length];
+ require_quiet(length != 0, cantConvertAnything);
+
+ unichar *buffer = malloc(sizeof(unichar) * length);
+ require_action(buffer, cantAllocBuffer, finalString = nil);
+ unichar *buffer2 = malloc(sizeof(unichar) * length);
+ require_action(buffer2, cantAllocBuffer2, finalString = nil);
+
+ [self getCharacters:buffer];
+ int buffer2Length = 0;
+
+ for (int i = 0; i < length; ++i) {
+ HTMLEscapeMap *val = bsearch(&buffer[i], table,
+ size / sizeof(HTMLEscapeMap),
+ sizeof(HTMLEscapeMap), escapeMapCompare);
+ if (val || (escapeUnicode && buffer[i] > 127)) {
+ if (buffer2Length) {
+ CFStringRef buffer2String =
+ CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
+ buffer2, buffer2Length,
+ kCFAllocatorNull);
+ require_action(buffer2String, cantCreateString, finalString = nil);
+ [finalString appendString:(NSString*)buffer2String];
+ CFRelease(buffer2String);
+ buffer2Length = 0;
+ }
+ if (val) {
+ [finalString appendString:val->escapeSequence];
+ }
+ else if (escapeUnicode && buffer[i] > 127) {
+ [finalString appendFormat:@"&#%d;", buffer[i]];
+ } else {
+ // Should never get here. Assert in debug.
+ require_action(NO, cantCreateString, finalString = nil);
+ }
+ } else {
+ buffer2[buffer2Length] = buffer[i];
+ buffer2Length += 1;
+ }
+ }
+ if (buffer2Length) {
+ CFStringRef buffer2String =
+ CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
+ buffer2, buffer2Length,
+ kCFAllocatorNull);
+ require_action(buffer2String, cantCreateString, finalString = nil);
+ [finalString appendString:(NSString*)buffer2String];
+ CFRelease(buffer2String);
+ }
+cantCreateString:
+ free(buffer2);
+cantAllocBuffer2:
+ free(buffer);
+cantAllocBuffer:
+cantConvertAnything:
+ return finalString;
+}
+
+- (NSString *)gtm_stringByEscapingForHTML {
+ return [self gtm_stringByEscapingHTMLUsingTable:gUnicodeHTMLEscapeMap
+ ofSize:sizeof(gUnicodeHTMLEscapeMap)
+ escapingUnicode:NO];
+} // gtm_stringByEscapingHTML
+
+- (NSString *)gtm_stringByEscapingForAsciiHTML {
+ return [self gtm_stringByEscapingHTMLUsingTable:gAsciiHTMLEscapeMap
+ ofSize:sizeof(gAsciiHTMLEscapeMap)
+ escapingUnicode:YES];
+} // gtm_stringByEscapingAsciiHTML
+
+- (NSString *)gtm_stringByUnescapingFromHTML {
+ NSRange range = NSMakeRange(0, [self length]);
+ NSRange subrange = [self rangeOfString:@"&" options:NSBackwardsSearch range:range];
+
+ // if no ampersands, we've got a quick way out
+ if (subrange.length == 0) return self;
+ NSMutableString *finalString = [NSMutableString stringWithString:self];
+ do {
+ NSRange semiColonRange = NSMakeRange(subrange.location, NSMaxRange(range) - subrange.location);
+ semiColonRange = [self rangeOfString:@";" options:0 range:semiColonRange];
+ range = NSMakeRange(0, subrange.location);
+ // if we don't find a semicolon in the range, we don't have a sequence
+ if (semiColonRange.location == NSNotFound) {
+ continue;
+ }
+ NSRange escapeRange = NSMakeRange(subrange.location, semiColonRange.location - subrange.location + 1);
+ NSString *escapeString = [self substringWithRange:escapeRange];
+ unsigned 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] == '#') {
+ unichar char2 = [escapeString characterAtIndex:2];
+ if (char2 == 'x' || char2 == 'X') {
+ // Hex escape squences &#xa3;
+ NSString *hexSequence = [escapeString substringWithRange:NSMakeRange(3, length - 4)];
+ NSScanner *scanner = [NSScanner scannerWithString:hexSequence];
+ unsigned value;
+ if ([scanner scanHexInt:&value] &&
+ value < USHRT_MAX &&
+ value > 0
+ && [scanner scanLocation] == length - 4) {
+ unichar uchar = value;
+ NSString *charString = [NSString stringWithCharacters:&uchar length:1];
+ [finalString replaceCharactersInRange:escapeRange withString:charString];
+ }
+
+ } else {
+ // Decimal Sequences &#123;
+ NSString *numberSequence = [escapeString substringWithRange:NSMakeRange(2, length - 3)];
+ NSScanner *scanner = [NSScanner scannerWithString:numberSequence];
+ int value;
+ if ([scanner scanInt:&value] &&
+ value < USHRT_MAX &&
+ value > 0
+ && [scanner scanLocation] == length - 3) {
+ unichar uchar = value;
+ NSString *charString = [NSString stringWithCharacters:&uchar length:1];
+ [finalString replaceCharactersInRange:escapeRange withString:charString];
+ }
+ }
+ } else {
+ // "standard" sequences
+ for (unsigned i = 0; i < sizeof(gAsciiHTMLEscapeMap) / sizeof(HTMLEscapeMap); ++i) {
+ if ([escapeString isEqualToString:gAsciiHTMLEscapeMap[i].escapeSequence]) {
+ [finalString replaceCharactersInRange:escapeRange withString:[NSString stringWithCharacters:&gAsciiHTMLEscapeMap[i].uchar length:1]];
+ break;
+ }
+ }
+ }
+ }
+ } while ((subrange = [self rangeOfString:@"&" options:NSBackwardsSearch range:range]).length != 0);
+ return finalString;
+} // gtm_stringByUnescapingHTML
+
+
+
+@end
diff --git a/Foundation/GTMNSString+HTMLTest.m b/Foundation/GTMNSString+HTMLTest.m
new file mode 100644
index 0000000..b60ed9d
--- /dev/null
+++ b/Foundation/GTMNSString+HTMLTest.m
@@ -0,0 +1,240 @@
+//
+// GTMNSString+HTMLTest.m
+//
+// Copyright 2005-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 "GTMSenTestCase.h"
+#import "GTMNSString+HTML.h"
+
+@interface GTMNSString_HTMLTest : SenTestCase
+@end
+
+@implementation GTMNSString_HTMLTest
+
+- (void)testStringByEscapingHTML {
+ unichar chars[] =
+ { 34, 38, 39, 60, 62, 338, 339, 352, 353, 376, 710, 732,
+ 8194, 8195, 8201, 8204, 8205, 8206, 8207, 8211, 8212, 8216, 8217, 8218,
+ 8220, 8221, 8222, 8224, 8225, 8240, 8249, 8250, 8364, };
+
+ NSString *string1 = [NSString stringWithCharacters:chars
+ length:sizeof(chars) / sizeof(unichar)];
+ NSString *string2 =
+ @"&quot;&amp;&apos;&lt;&gt;&OElig;&oelig;&Scaron;&scaron;&Yuml;"
+ "&circ;&tilde;&ensp;&emsp;&thinsp;&zwnj;&zwj;&lrm;&rlm;&ndash;"
+ "&mdash;&lsquo;&rsquo;&sbquo;&ldquo;&rdquo;&bdquo;&dagger;&Dagger;"
+ "&permil;&lsaquo;&rsaquo;&euro;";
+
+ STAssertEqualObjects([string1 gtm_stringByEscapingForHTML],
+ string2,
+ @"HTML escaping failed");
+
+ STAssertEqualObjects([@"<this & that>" gtm_stringByEscapingForHTML],
+ @"&lt;this &amp; that&gt;",
+ @"HTML escaping failed");
+ NSString *string = [NSString stringWithUTF8String:"パン・&ド・カンパーニュ"];
+ NSString *escapeStr = [NSString stringWithUTF8String:"パン・&amp;ド・カンパーニュ"];
+ STAssertEqualObjects([string gtm_stringByEscapingForHTML],
+ escapeStr,
+ @"HTML escaping failed");
+
+ string = [NSString stringWithUTF8String:"abcا1ب<تdef&"];
+ STAssertEqualObjects([string gtm_stringByEscapingForHTML],
+ [NSString stringWithUTF8String:"abcا1ب&lt;تdef&amp;"],
+ @"HTML escaping failed");
+} // testStringByEscapingHTML
+
+- (void)testStringByEscapingAsciiHTML {
+ unichar chars[] =
+ { 34, 38, 39, 60, 62, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170,
+ 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185,
+ 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200,
+ 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215,
+ 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230,
+ 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245,
+ 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 338, 339, 352, 353, 376,
+ 402, 710, 732, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924,
+ 925, 926, 927, 928, 929, 931, 932, 933, 934, 935, 936, 937, 945, 946, 947,
+ 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962,
+ 963, 964, 965, 966, 967, 968, 969, 977, 978, 982, 8194, 8195, 8201, 8204,
+ 8205, 8206, 8207, 8211, 8212, 8216, 8217, 8218, 8220, 8221, 8222, 8224, 8225,
+ 8226, 8230, 8240, 8242, 8243, 8249, 8250, 8254, 8260, 8364, 8472, 8465, 8476,
+ 8482, 8501, 8592, 8593, 8594, 8595, 8596, 8629, 8656, 8657, 8658, 8659, 8660,
+ 8704, 8706, 8707, 8709, 8711, 8712, 8713, 8715, 8719, 8721, 8722, 8727, 8730,
+ 8733, 8734, 8736, 8743, 8744, 8745, 8746, 8747, 8756, 8764, 8773, 8776, 8800,
+ 8801, 8804, 8805, 8834, 8835, 8836, 8838, 8839, 8853, 8855, 8869, 8901, 8968,
+ 8969, 8970, 8971, 9001, 9002, 9674, 9824, 9827, 9829, 9830 };
+
+ NSString *string1 = [NSString stringWithCharacters:chars
+ length:sizeof(chars) / sizeof(unichar)];
+ NSString *string2 =
+ @"&quot;&amp;&apos;&lt;&gt;&nbsp;&iexcl;&cent;&pound;&curren;&yen;"
+ "&brvbar;&sect;&uml;&copy;&ordf;&laquo;&not;&shy;&reg;&macr;&deg;"
+ "&plusmn;&sup2;&sup3;&acute;&micro;&para;&middot;&cedil;&sup1;"
+ "&ordm;&raquo;&frac14;&frac12;&frac34;&iquest;&Agrave;&Aacute;"
+ "&Acirc;&Atilde;&Auml;&Aring;&AElig;&Ccedil;&Egrave;&Eacute;"
+ "&Ecirc;&Euml;&Igrave;&Iacute;&Icirc;&Iuml;&ETH;&Ntilde;&Ograve;"
+ "&Oacute;&Ocirc;&Otilde;&Ouml;&times;&Oslash;&Ugrave;&Uacute;"
+ "&Ucirc;&Uuml;&Yacute;&THORN;&szlig;&agrave;&aacute;&acirc;&atilde;"
+ "&auml;&aring;&aelig;&ccedil;&egrave;&eacute;&ecirc;&euml;&igrave;"
+ "&iacute;&icirc;&iuml;&eth;&ntilde;&ograve;&oacute;&ocirc;&otilde;"
+ "&ouml;&divide;&oslash;&ugrave;&uacute;&ucirc;&uuml;&yacute;&thorn;"
+ "&yuml;&OElig;&oelig;&Scaron;&scaron;&Yuml;&fnof;&circ;&tilde;"
+ "&Alpha;&Beta;&Gamma;&Delta;&Epsilon;&Zeta;&Eta;&Theta;&Iota;"
+ "&Kappa;&Lambda;&Mu;&Nu;&Xi;&Omicron;&Pi;&Rho;&Sigma;&Tau;"
+ "&Upsilon;&Phi;&Chi;&Psi;&Omega;&alpha;&beta;&gamma;&delta;"
+ "&epsilon;&zeta;&eta;&theta;&iota;&kappa;&lambda;&mu;&nu;&xi;"
+ "&omicron;&pi;&rho;&sigmaf;&sigma;&tau;&upsilon;&phi;&chi;&psi;"
+ "&omega;&thetasym;&upsih;&piv;&ensp;&emsp;&thinsp;&zwnj;&zwj;"
+ "&lrm;&rlm;&ndash;&mdash;&lsquo;&rsquo;&sbquo;&ldquo;&rdquo;"
+ "&bdquo;&dagger;&Dagger;&bull;&hellip;&permil;&prime;&Prime;"
+ "&lsaquo;&rsaquo;&oline;&frasl;&euro;&weierp;&image;&real;&trade;"
+ "&alefsym;&larr;&uarr;&rarr;&darr;&harr;&crarr;&lArr;&uArr;&rArr;"
+ "&dArr;&hArr;&forall;&part;&exist;&empty;&nabla;&isin;&notin;&ni;"
+ "&prod;&sum;&minus;&lowast;&radic;&prop;&infin;&ang;&and;&or;"
+ "&cap;&cup;&int;&there4;&sim;&cong;&asymp;&ne;&equiv;&le;&ge;"
+ "&sub;&sup;&nsub;&sube;&supe;&oplus;&otimes;&perp;&sdot;&lceil;"
+ "&rceil;&lfloor;&rfloor;&lang;&rang;&loz;&spades;&clubs;&hearts;"
+ "&diams;";
+
+ STAssertEqualObjects([string1 gtm_stringByEscapingForAsciiHTML],
+ string2,
+ @"HTML escaping failed");
+
+ STAssertEqualObjects([@"<this & that>" gtm_stringByEscapingForAsciiHTML],
+ @"&lt;this &amp; that&gt;",
+ @"HTML escaping failed");
+ NSString *string = [NSString stringWithUTF8String:"パン・ド・カンパーニュ"];
+ STAssertEqualObjects([string gtm_stringByEscapingForAsciiHTML],
+ @"&#12497;&#12531;&#12539;&#12489;&#12539;&#12459;"
+ "&#12531;&#12497;&#12540;&#12491;&#12517;",
+ @"HTML escaping failed");
+
+ // Mix in some right - to left
+ string = [NSString stringWithUTF8String:"abcا1ب<تdef&"];
+ STAssertEqualObjects([string gtm_stringByEscapingForAsciiHTML],
+ @"abc&#1575;1&#1576;&lt;&#1578;def&amp;",
+ @"HTML escaping failed");
+} // stringByEscapingAsciiHTML
+
+- (void)testStringByUnescapingHTML {
+ NSString *string1 =
+ @"&quot;&amp;&apos;&lt;&gt;&nbsp;&iexcl;&cent;&pound;&curren;&yen;"
+ "&brvbar;&sect;&uml;&copy;&ordf;&laquo;&not;&shy;&reg;&macr;&deg;"
+ "&plusmn;&sup2;&sup3;&acute;&micro;&para;&middot;&cedil;&sup1;"
+ "&ordm;&raquo;&frac14;&frac12;&frac34;&iquest;&Agrave;&Aacute;"
+ "&Acirc;&Atilde;&Auml;&Aring;&AElig;&Ccedil;&Egrave;&Eacute;"
+ "&Ecirc;&Euml;&Igrave;&Iacute;&Icirc;&Iuml;&ETH;&Ntilde;&Ograve;"
+ "&Oacute;&Ocirc;&Otilde;&Ouml;&times;&Oslash;&Ugrave;&Uacute;"
+ "&Ucirc;&Uuml;&Yacute;&THORN;&szlig;&agrave;&aacute;&acirc;&atilde;"
+ "&auml;&aring;&aelig;&ccedil;&egrave;&eacute;&ecirc;&euml;&igrave;"
+ "&iacute;&icirc;&iuml;&eth;&ntilde;&ograve;&oacute;&ocirc;&otilde;"
+ "&ouml;&divide;&oslash;&ugrave;&uacute;&ucirc;&uuml;&yacute;&thorn;"
+ "&yuml;&OElig;&oelig;&Scaron;&scaron;&Yuml;&fnof;&circ;&tilde;"
+ "&Alpha;&Beta;&Gamma;&Delta;&Epsilon;&Zeta;&Eta;&Theta;&Iota;"
+ "&Kappa;&Lambda;&Mu;&Nu;&Xi;&Omicron;&Pi;&Rho;&Sigma;&Tau;"
+ "&Upsilon;&Phi;&Chi;&Psi;&Omega;&alpha;&beta;&gamma;&delta;"
+ "&epsilon;&zeta;&eta;&theta;&iota;&kappa;&lambda;&mu;&nu;&xi;"
+ "&omicron;&pi;&rho;&sigmaf;&sigma;&tau;&upsilon;&phi;&chi;&psi;"
+ "&omega;&thetasym;&upsih;&piv;&ensp;&emsp;&thinsp;&zwnj;&zwj;"
+ "&lrm;&rlm;&ndash;&mdash;&lsquo;&rsquo;&sbquo;&ldquo;&rdquo;"
+ "&bdquo;&dagger;&Dagger;&bull;&hellip;&permil;&prime;&Prime;"
+ "&lsaquo;&rsaquo;&oline;&frasl;&euro;&weierp;&image;&real;&trade;"
+ "&alefsym;&larr;&uarr;&rarr;&darr;&harr;&crarr;&lArr;&uArr;&rArr;"
+ "&dArr;&hArr;&forall;&part;&exist;&empty;&nabla;&isin;&notin;&ni;"
+ "&prod;&sum;&minus;&lowast;&radic;&prop;&infin;&ang;&and;&or;"
+ "&cap;&cup;&int;&there4;&sim;&cong;&asymp;&ne;&equiv;&le;&ge;"
+ "&sub;&sup;&nsub;&sube;&supe;&oplus;&otimes;&perp;&sdot;&lceil;"
+ "&rceil;&lfloor;&rfloor;&lang;&rang;&loz;&spades;&clubs;&hearts;"
+ "&diams;";
+
+ unichar chars[] =
+ { 34, 38, 39, 60, 62, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170,
+ 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185,
+ 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200,
+ 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215,
+ 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230,
+ 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245,
+ 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 338, 339, 352, 353, 376,
+ 402, 710, 732, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924,
+ 925, 926, 927, 928, 929, 931, 932, 933, 934, 935, 936, 937, 945, 946, 947,
+ 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962,
+ 963, 964, 965, 966, 967, 968, 969, 977, 978, 982, 8194, 8195, 8201, 8204,
+ 8205, 8206, 8207, 8211, 8212, 8216, 8217, 8218, 8220, 8221, 8222, 8224, 8225,
+ 8226, 8230, 8240, 8242, 8243, 8249, 8250, 8254, 8260, 8364, 8472, 8465, 8476,
+ 8482, 8501, 8592, 8593, 8594, 8595, 8596, 8629, 8656, 8657, 8658, 8659, 8660,
+ 8704, 8706, 8707, 8709, 8711, 8712, 8713, 8715, 8719, 8721, 8722, 8727, 8730,
+ 8733, 8734, 8736, 8743, 8744, 8745, 8746, 8747, 8756, 8764, 8773, 8776, 8800,
+ 8801, 8804, 8805, 8834, 8835, 8836, 8838, 8839, 8853, 8855, 8869, 8901, 8968,
+ 8969, 8970, 8971, 9001, 9002, 9674, 9824, 9827, 9829, 9830 };
+
+ NSString *string2 = [NSString stringWithCharacters:chars
+ length:sizeof(chars) / sizeof(unichar)];
+ STAssertEqualObjects([string1 gtm_stringByUnescapingFromHTML],
+ string2,
+ @"HTML unescaping failed");
+
+ STAssertEqualObjects([@"&#65;&#x42;&#X43;" gtm_stringByUnescapingFromHTML],
+ @"ABC", @"HTML unescaping failed");
+
+ STAssertEqualObjects([@"" gtm_stringByUnescapingFromHTML],
+ @"", @"HTML unescaping failed");
+
+ STAssertEqualObjects([@"&#65;&Bang;&#X43;" gtm_stringByUnescapingFromHTML],
+ @"A&Bang;C", @"HTML unescaping failed");
+
+ STAssertEqualObjects([@"&#65&Bang;&#X43;" gtm_stringByUnescapingFromHTML],
+ @"&#65&Bang;C", @"HTML unescaping failed");
+
+ STAssertEqualObjects([@"&#65;&Bang;&#X43" gtm_stringByUnescapingFromHTML],
+ @"A&Bang;&#X43", @"HTML unescaping failed");
+
+ STAssertEqualObjects([@"&#65A;" gtm_stringByUnescapingFromHTML],
+ @"&#65A;", @"HTML unescaping failed");
+
+ STAssertEqualObjects([@"&" gtm_stringByUnescapingFromHTML],
+ @"&", @"HTML unescaping failed");
+
+ STAssertEqualObjects([@"&;" gtm_stringByUnescapingFromHTML],
+ @"&;", @"HTML unescaping failed");
+
+ STAssertEqualObjects([@"&x;" gtm_stringByUnescapingFromHTML],
+ @"&x;", @"HTML unescaping failed");
+
+ STAssertEqualObjects([@"&X;" gtm_stringByUnescapingFromHTML],
+ @"&X;", @"HTML unescaping failed");
+
+ STAssertEqualObjects([@";" gtm_stringByUnescapingFromHTML],
+ @";", @"HTML unescaping failed");
+
+ STAssertEqualObjects([@"&lt;this &amp; that&gt;" gtm_stringByUnescapingFromHTML],
+ @"<this & that>", @"HTML unescaping failed");
+
+
+} // testStringByUnescapingHTML
+
+- (void)testStringRoundtrippingEscapedHTML {
+ NSString *string = [NSString stringWithUTF8String:"This test ©™®๒०᠐٧"];
+ STAssertEqualObjects(string,
+ [[string gtm_stringByEscapingForHTML] gtm_stringByUnescapingFromHTML],
+ @"HTML Roundtripping failed");
+ string = [NSString stringWithUTF8String:"This test ©™®๒०᠐٧"];
+ STAssertEqualObjects(string,
+ [[string gtm_stringByEscapingForAsciiHTML] gtm_stringByUnescapingFromHTML],
+ @"HTML Roundtripping failed");
+}
+@end
diff --git a/Foundation/GTMNSString+XML.h b/Foundation/GTMNSString+XML.h
new file mode 100644
index 0000000..ed2d161
--- /dev/null
+++ b/Foundation/GTMNSString+XML.h
@@ -0,0 +1,39 @@
+//
+// GTMNSString+XML.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.
+//
+
+#import <Foundation/Foundation.h>
+
+/// Utilities for NSStrings containing XML
+@interface NSString (GTMNSStringXMLAdditions)
+
+/// Get a string where characters that need escaping for XML are escaped and invalid characters removed
+//
+/// This call escapes '&', '<, '>', '\'', '"' per the xml spec and removes all
+/// invalid characters as defined by Section 2.2 of the xml spec.
+///
+/// For obvious reasons this call is only safe once.
+//
+// Returns:
+// Autoreleased NSString
+//
+- (NSString *)gtm_stringByEscapingForXML;
+
+// There is no stringByUnescapingFromXML because the XML parser will do this.
+// The above api is here just incase you need to create XML yourself.
+
+@end
diff --git a/Foundation/GTMNSString+XML.m b/Foundation/GTMNSString+XML.m
new file mode 100644
index 0000000..edc82a0
--- /dev/null
+++ b/Foundation/GTMNSString+XML.m
@@ -0,0 +1,162 @@
+//
+// GTMNSString+XML.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 "GTMNSString+XML.h"
+
+typedef enum {
+ kGMXMLCharModeEncodeQUOT = 0,
+ kGMXMLCharModeEncodeAMP = 1,
+ kGMXMLCharModeEncodeAPOS = 2,
+ kGMXMLCharModeEncodeLT = 3,
+ kGMXMLCharModeEncodeGT = 4,
+ kGMXMLCharModeValid = 99,
+ kGMXMLCharModeInvalid = 100,
+} GMXMLCharMode;
+
+static NSString *gXMLEntityList[] = {
+ // this must match the above order
+ @"&quot;",
+ @"&amp;",
+ @"&apos;",
+ @"&lt;",
+ @"&gt;",
+};
+
+FOUNDATION_STATIC_INLINE GMXMLCharMode XMLModeForUnichar(unichar c) {
+
+ // Per XML spec Section 2.2 Characters
+ // ( http://www.w3.org/TR/REC-xml/#charsets )
+ //
+ // Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] |
+ // [#x10000-#x10FFFF]
+
+ if (c <= 0xd7ff) {
+ if (c >= 0x20) {
+ switch (c) {
+ case 34:
+ return kGMXMLCharModeEncodeQUOT;
+ case 38:
+ return kGMXMLCharModeEncodeAMP;
+ case 39:
+ return kGMXMLCharModeEncodeAPOS;
+ case 60:
+ return kGMXMLCharModeEncodeLT;
+ case 62:
+ return kGMXMLCharModeEncodeGT;
+ default:
+ return kGMXMLCharModeValid;
+ }
+ } else {
+ if (c == '\n')
+ return kGMXMLCharModeValid;
+ if (c == '\r')
+ return kGMXMLCharModeValid;
+ if (c == '\t')
+ return kGMXMLCharModeValid;
+ return kGMXMLCharModeInvalid;
+ }
+ }
+
+ if (c < 0xE000)
+ return kGMXMLCharModeInvalid;
+
+ if (c <= 0xFFFD)
+ return kGMXMLCharModeValid;
+
+ // unichar can't have the following values
+ // if (c < 0x10000)
+ // return kGMXMLCharModeInvalid;
+ // if (c <= 0x10FFFF)
+ // return kGMXMLCharModeValid;
+
+ return kGMXMLCharModeInvalid;
+} // XMLModeForUnichar
+
+@implementation NSString (GTMNSStringXMLAdditions)
+
+- (NSString *)gtm_stringByEscapingForXML {
+ NSMutableString *finalString = [NSMutableString string];
+ int length = [self length];
+ require_quiet(length != 0, cantConvertAnything);
+
+ // see if we can just use the interal version
+ BOOL freeBuffer = NO;
+ unichar *buffer = (unichar*)CFStringGetCharactersPtr((CFStringRef)self);
+ if (!buffer) {
+ // nope, alloc buffer and fetch the chars ourselves
+ buffer = malloc(sizeof(unichar) * length);
+ if (!buffer) return nil;
+ freeBuffer = YES;
+ [self getCharacters:buffer];
+ }
+
+ unichar *goodRun = buffer;
+ int goodRunLength = 0;
+
+ for (int i = 0; i < length; ++i) {
+
+ GMXMLCharMode cMode = XMLModeForUnichar(buffer[i]);
+
+ if (cMode == kGMXMLCharModeValid) {
+ // goes as is
+ goodRunLength += 1;
+ } else {
+ // it's something we have to encode or something invalid
+
+ // start by adding what we already collected (if anything)
+ if (goodRunLength) {
+ CFStringRef goodRunString =
+ CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
+ goodRun, goodRunLength,
+ kCFAllocatorNull);
+ require_action(goodRunString != NULL, cantCreateString, finalString = nil);
+ [finalString appendString:(NSString*)goodRunString];
+ CFRelease(goodRunString);
+ goodRunLength = 0;
+ }
+
+ // if it wasn't invalid, add the encoded version
+ if (cMode != kGMXMLCharModeInvalid) {
+ // add this encoded
+ [finalString appendString:gXMLEntityList[cMode]];
+ }
+
+ // update goodRun to point to the next unichar
+ goodRun = buffer + i + 1;
+ }
+ }
+
+ // anything left to add?
+ if (goodRunLength) {
+ CFStringRef goodRunString =
+ CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
+ goodRun, goodRunLength,
+ kCFAllocatorNull);
+ require_action(goodRunString != NULL, cantCreateString2, finalString = nil);
+ [finalString appendString:(NSString*)goodRunString];
+ CFRelease(goodRunString);
+ }
+cantCreateString:
+cantCreateString2:
+ if (freeBuffer)
+ free(buffer);
+cantConvertAnything:
+ return finalString;
+} // gtm_stringByEscapingForXML
+
+@end
diff --git a/Foundation/GTMNSString+XMLTest.m b/Foundation/GTMNSString+XMLTest.m
new file mode 100644
index 0000000..c53ce64
--- /dev/null
+++ b/Foundation/GTMNSString+XMLTest.m
@@ -0,0 +1,44 @@
+//
+// NSString+XMLTest.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 "GTMNSString+XML.h"
+
+
+@interface GTMNSString_XMLTest : SenTestCase
+@end
+
+@implementation GTMNSString_XMLTest
+
+- (void)testStringByEscapingForXML {
+ unichar chars[] =
+ { 0, 'z', 1, 'z', 4, 'z', 5, 'z', 34, 'z', 38, 'z', 39, 'z',
+ 60, 'z', 62, 'z', ' ', 'z', 0xd800, 'z', 0xDFFF, 'z', 0xFFFE,
+ 0xFFFF, 'z' };
+
+ NSString *string1 = [NSString stringWithCharacters:chars
+ length:sizeof(chars) / sizeof(unichar)];
+ NSString *string2 = @"zzzz&quot;z&amp;z&apos;z&lt;z&gt;z zzzz";
+
+ STAssertEqualObjects([string1 gtm_stringByEscapingForXML],
+ string2,
+ @"Escaped for XML failed");
+}
+
+@end
diff --git a/Foundation/GTMObjectSingleton.h b/Foundation/GTMObjectSingleton.h
new file mode 100644
index 0000000..ff03ea0
--- /dev/null
+++ b/Foundation/GTMObjectSingleton.h
@@ -0,0 +1,69 @@
+//
+// GTMObjectSingleton.h
+// Macro to implement methods for a singleton
+//
+// Copyright 2005-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 macro implements the various methods needed to make a safe singleton.
+//
+/// This Singleton pattern was taken from:
+/// http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaFundamentals/CocoaObjects/chapter_3_section_10.html
+///
+/// Sample usage:
+///
+/// SINGLETON_BOILERPLATE(GMSomeUsefulManager, sharedSomeUsefulManager)
+/// (with no trailing semicolon)
+///
+#define GTMOBJECT_SINGLETON_BOILERPLATE(_object_name_, _shared_obj_name_) \
+static _object_name_ *z##_shared_obj_name_ = nil; \
++ (_object_name_ *)_shared_obj_name_ { \
+ @synchronized(self) { \
+ if (z##_shared_obj_name_ == nil) { \
+ /* Note that 'self' may not be the same as _object_name_ */ \
+ /* first assignment done in allocWithZone but we must reassign in case init fails */ \
+ z##_shared_obj_name_ = [[self alloc] init]; \
+ GTMDebugAssert((z##_shared_obj_name_ != nil), @"didn't catch singleton allocation"); \
+ } \
+ } \
+ return z##_shared_obj_name_; \
+} \
++ (id)allocWithZone:(NSZone *)zone { \
+ @synchronized(self) { \
+ if (z##_shared_obj_name_ == nil) { \
+ z##_shared_obj_name_ = [super allocWithZone:zone]; \
+ return z##_shared_obj_name_; \
+ } \
+ } \
+ \
+ /* We can't return the shared instance, because it's been init'd */ \
+ GTMDebugAssert(NO, @"use the singleton API, not alloc+init"); \
+ return nil; \
+} \
+- (id)retain { \
+ return self; \
+} \
+- (unsigned int)retainCount { \
+ return UINT_MAX; \
+} \
+- (void)release { \
+} \
+- (id)autorelease { \
+ return self; \
+} \
+- (id)copyWithZone:(NSZone *)zone { \
+ return self; \
+} \
+
diff --git a/Foundation/GTMSystemVersion.h b/Foundation/GTMSystemVersion.h
new file mode 100644
index 0000000..c9f9d75
--- /dev/null
+++ b/Foundation/GTMSystemVersion.h
@@ -0,0 +1,45 @@
+//
+// GTMSystemVersion.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.
+//
+
+#import <Foundation/Foundation.h>
+
+/// A class for getting information about what system we are running on
+@interface GTMSystemVersion : NSObject
+
+/// Returns YES if running on 10.3, NO otherwise.
++ (BOOL)isPanther;
+
+/// Returns YES if running on 10.4, NO otherwise.
++ (BOOL)isTiger;
+
+/// Returns YES if running on 10.5, NO otherwise.
++ (BOOL)isLeopard;
+
+/// Returns a YES/NO if the system is 10.3 or better
++ (BOOL)isPantherOrGreater;
+
+/// Returns a YES/NO if the system is 10.4 or better
++ (BOOL)isTigerOrGreater;
+
+/// Returns a YES/NO if the system is 10.5 or better
++ (BOOL)isLeopardOrGreater;
+
+/// Returns the current system version major.minor.bugFix
++ (void)getMajor:(long*)major minor:(long*)minor bugFix:(long*)bugFix;
+
+@end
diff --git a/Foundation/GTMSystemVersion.m b/Foundation/GTMSystemVersion.m
new file mode 100644
index 0000000..d9b1923
--- /dev/null
+++ b/Foundation/GTMSystemVersion.m
@@ -0,0 +1,104 @@
+//
+// GTMSystemVersion.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 "GTMSystemVersion.h"
+#import <Carbon/Carbon.h>
+#import <stdlib.h>
+
+@implementation GTMSystemVersion
+
++ (BOOL)isPanther {
+ long major, minor;
+ [self getMajor:&major minor:&minor bugFix:nil];
+ return major == 10 && minor == 3;
+}
+
++ (BOOL)isTiger {
+ long major, minor;
+ [self getMajor:&major minor:&minor bugFix:nil];
+ return major == 10 && minor == 4;
+}
+
++ (BOOL)isLeopard {
+ long major, minor;
+ [self getMajor:&major minor:&minor bugFix:nil];
+ return major == 10 && minor == 5;
+}
+
++ (BOOL)isPantherOrGreater {
+ long major, minor;
+ [self getMajor:&major minor:&minor bugFix:nil];
+ return (major > 10) || (major == 10 && minor >= 3);
+}
+
++ (BOOL)isTigerOrGreater {
+ long major, minor;
+ [self getMajor:&major minor:&minor bugFix:nil];
+ return (major > 10) || (major == 10 && minor >= 4);
+}
+
++ (BOOL)isLeopardOrGreater {
+ long major, minor;
+ [self getMajor:&major minor:&minor bugFix:nil];
+ return (major > 10) || (major == 10 && minor >= 5);
+}
+
++ (void)getMajor:(long*)major minor:(long*)minor bugFix:(long*)bugFix {
+ long binaryCodedDec;
+
+ if (major) {
+ require_noerr(Gestalt(gestaltSystemVersionMajor, major), failedGestalt);
+ }
+ if (minor) {
+ require_noerr(Gestalt(gestaltSystemVersionMinor, minor), failedGestalt);
+ }
+ if (bugFix) {
+ require_noerr(Gestalt(gestaltSystemVersionBugFix, bugFix), failedGestalt);
+ }
+ return;
+
+failedGestalt:
+ // gestaltSystemVersionMajor et al are only on 10.4 and above, so they
+ // could fail if we have this code on 10.3.
+ if (Gestalt(gestaltSystemVersion, &binaryCodedDec)) {
+ // not much else we can do...
+ if (major) *major = 0;
+ if (minor) *minor = 0;
+ if (bugFix) *bugFix = 0;
+ return;
+ }
+
+ // Note that this code will return x.9.9 for any system rev parts that are
+ // greater than 9 (ie 10.10.10 will be 10.9.9. This shouldn't ever be a
+ // problem as the code above takes care of this for any system above 10.4.
+ if (major) {
+ int msb = (binaryCodedDec & 0x0000F000L) >> 12;
+ msb *= 10;
+ int lsb = (binaryCodedDec & 0x00000F00L) >> 8;
+ *major = msb + lsb;
+ }
+ if (minor) {
+ *minor = (binaryCodedDec & 0x000000F0L) >> 4;
+ }
+ if (bugFix) {
+ *bugFix = (binaryCodedDec & 0x0000000FL);
+ }
+
+}
+
+@end
diff --git a/Foundation/GTMSystemVersionTest.m b/Foundation/GTMSystemVersionTest.m
new file mode 100644
index 0000000..57b22aa
--- /dev/null
+++ b/Foundation/GTMSystemVersionTest.m
@@ -0,0 +1,47 @@
+//
+// GTMSystemVersionTest.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 "GTMSystemVersion.h"
+
+@interface GTMSystemVersionTest : SenTestCase
+@end
+
+@implementation GTMSystemVersionTest
+- (void)testBasics {
+ long major;
+ long minor;
+ long bugFix;
+
+ [GTMSystemVersion getMajor:&major minor:&minor bugFix:&bugFix];
+ STAssertTrue(major >= 10 && minor >= 3 && bugFix >= 0, nil);
+ STAssertTrue([GTMSystemVersion isPantherOrGreater], nil);
+ if (minor > 3) {
+ STAssertTrue([GTMSystemVersion isTigerOrGreater], nil);
+ } else {
+ STAssertFalse([GTMSystemVersion isTigerOrGreater], nil);
+ }
+ if (minor > 4) {
+ STAssertTrue([GTMSystemVersion isLeopardOrGreater], nil);
+ } else {
+ STAssertFalse([GTMSystemVersion isLeopardOrGreater], nil);
+ }
+ [GTMSystemVersion getMajor:nil minor:nil bugFix:nil];
+}
+
+@end