diff options
author | thomasvl <thomasvl@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2008-01-28 20:19:42 +0000 |
---|---|---|
committer | thomasvl <thomasvl@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2008-01-28 20:19:42 +0000 |
commit | 2a5219567634ab7ab74314ff3615132becadff4a (patch) | |
tree | 8e6f447544e5eaf460da741bf57771f929b4a70c /Foundation |
initial drop of a few sources to start things out
Diffstat (limited to 'Foundation')
-rw-r--r-- | Foundation/GTMCalculatedRange.h | 101 | ||||
-rw-r--r-- | Foundation/GTMCalculatedRange.m | 146 | ||||
-rw-r--r-- | Foundation/GTMCalculatedRangeTest.m | 88 | ||||
-rw-r--r-- | Foundation/GTMGarbageCollection.h | 43 | ||||
-rw-r--r-- | Foundation/GTMNSData+zlib.h | 83 | ||||
-rw-r--r-- | Foundation/GTMNSData+zlib.m | 246 | ||||
-rw-r--r-- | Foundation/GTMNSData+zlibTest.m | 183 | ||||
-rw-r--r-- | Foundation/GTMNSEnumerator+Filter.h | 53 | ||||
-rw-r--r-- | Foundation/GTMNSEnumerator+Filter.m | 167 | ||||
-rw-r--r-- | Foundation/GTMNSEnumerator+FilterTest.m | 68 | ||||
-rw-r--r-- | Foundation/GTMNSString+HTML.h | 66 | ||||
-rw-r--r-- | Foundation/GTMNSString+HTML.m | 514 | ||||
-rw-r--r-- | Foundation/GTMNSString+HTMLTest.m | 240 | ||||
-rw-r--r-- | Foundation/GTMNSString+XML.h | 39 | ||||
-rw-r--r-- | Foundation/GTMNSString+XML.m | 162 | ||||
-rw-r--r-- | Foundation/GTMNSString+XMLTest.m | 44 | ||||
-rw-r--r-- | Foundation/GTMObjectSingleton.h | 69 | ||||
-rw-r--r-- | Foundation/GTMSystemVersion.h | 45 | ||||
-rw-r--r-- | Foundation/GTMSystemVersion.m | 104 | ||||
-rw-r--r-- | Foundation/GTMSystemVersionTest.m | 47 |
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 '&'. 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 '&' +/// 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, '&' becomes '&' +/// Handles   and 2 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 + { @""", 34 }, + { @"&", 38 }, + { @"'", 39 }, + { @"<", 60 }, + { @">", 62 }, + + // A.2.1. Latin-1 characters + { @" ", 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 }, + + // A.2.2. Special characters cont'd + { @"Œ", 338 }, + { @"œ", 339 }, + { @"Š", 352 }, + { @"š", 353 }, + { @"Ÿ", 376 }, + + // A.2.3. Symbols + { @"ƒ", 402 }, + + // A.2.2. Special characters cont'd + { @"ˆ", 710 }, + { @"˜", 732 }, + + // A.2.3. Symbols cont'd + { @"Α", 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 }, + + // A.2.2. Special characters cont'd + { @" ", 8194 }, + { @" ", 8195 }, + { @" ", 8201 }, + { @"‌", 8204 }, + { @"‍", 8205 }, + { @"‎", 8206 }, + { @"‏", 8207 }, + { @"–", 8211 }, + { @"—", 8212 }, + { @"‘", 8216 }, + { @"’", 8217 }, + { @"‚", 8218 }, + { @"“", 8220 }, + { @"”", 8221 }, + { @"„", 8222 }, + { @"†", 8224 }, + { @"‡", 8225 }, + // A.2.3. Symbols cont'd + { @"•", 8226 }, + { @"…", 8230 }, + + // A.2.2. Special characters cont'd + { @"‰", 8240 }, + + // A.2.3. Symbols cont'd + { @"′", 8242 }, + { @"″", 8243 }, + + // A.2.2. Special characters cont'd + { @"‹", 8249 }, + { @"›", 8250 }, + + // A.2.3. Symbols cont'd + { @"‾", 8254 }, + { @"⁄", 8260 }, + + // A.2.2. Special characters cont'd + { @"€", 8364 }, + + // A.2.3. Symbols cont'd + { @"ℑ", 8465 }, + { @"℘", 8472 }, + { @"ℜ", 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 } +}; + +// 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 + { @""", 34 }, + { @"&", 38 }, + { @"'", 39 }, + { @"<", 60 }, + { @">", 62 }, + + // Latin Extended-A + { @"Œ", 338 }, + { @"œ", 339 }, + { @"Š", 352 }, + { @"š", 353 }, + { @"Ÿ", 376 }, + + // Spacing Modifier Letters + { @"ˆ", 710 }, + { @"˜", 732 }, + + // General Punctuation + { @" ", 8194 }, + { @" ", 8195 }, + { @" ", 8201 }, + { @"‌", 8204 }, + { @"‍", 8205 }, + { @"‎", 8206 }, + { @"‏", 8207 }, + { @"–", 8211 }, + { @"—", 8212 }, + { @"‘", 8216 }, + { @"’", 8217 }, + { @"‚", 8218 }, + { @"“", 8220 }, + { @"”", 8221 }, + { @"„", 8222 }, + { @"†", 8224 }, + { @"‡", 8225 }, + { @"‰", 8240 }, + { @"‹", 8249 }, + { @"›", 8250 }, + { @"€", 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 (<) and less than 11 (ϑ) + if (length > 3 && length < 11) { + if ([escapeString characterAtIndex:1] == '#') { + unichar char2 = [escapeString characterAtIndex:2]; + if (char2 == 'x' || char2 == 'X') { + // Hex escape squences £ + 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 { + 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 = + @""&'<>ŒœŠšŸ" + "ˆ˜   ‌‍‎‏–" + "—‘’‚“”„†‡" + "‰‹›€"; + + STAssertEqualObjects([string1 gtm_stringByEscapingForHTML], + string2, + @"HTML escaping failed"); + + STAssertEqualObjects([@"<this & that>" gtm_stringByEscapingForHTML], + @"<this & that>", + @"HTML escaping failed"); + NSString *string = [NSString stringWithUTF8String:"パン・&ド・カンパーニュ"]; + NSString *escapeStr = [NSString stringWithUTF8String:"パン・&ド・カンパーニュ"]; + STAssertEqualObjects([string gtm_stringByEscapingForHTML], + escapeStr, + @"HTML escaping failed"); + + string = [NSString stringWithUTF8String:"abcا1ب<تdef&"]; + STAssertEqualObjects([string gtm_stringByEscapingForHTML], + [NSString stringWithUTF8String:"abcا1ب<تdef&"], + @"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 = + @""&'<> ¡¢£¤¥" + "¦§¨©ª«¬­®¯°" + "±²³´µ¶·¸¹" + "º»¼½¾¿ÀÁ" + "ÂÃÄÅÆÇÈÉ" + "ÊËÌÍÎÏÐÑÒ" + "ÓÔÕÖ×ØÙÚ" + "ÛÜÝÞßàáâã" + "äåæçèéêëì" + "íîïðñòóôõ" + "ö÷øùúûüýþ" + "ÿŒœŠšŸƒˆ˜" + "ΑΒΓΔΕΖΗΘΙ" + "ΚΛΜΝΞΟΠΡΣΤ" + "ΥΦΧΨΩαβγδ" + "εζηθικλμνξ" + "οπρςστυφχψ" + "ωϑϒϖ   ‌‍" + "‎‏–—‘’‚“”" + "„†‡•…‰′″" + "‹›‾⁄€℘ℑℜ™" + "ℵ←↑→↓↔↵⇐⇑⇒" + "⇓⇔∀∂∃∅∇∈∉∋" + "∏∑−∗√∝∞∠∧∨" + "∩∪∫∴∼≅≈≠≡≤≥" + "⊂⊃⊄⊆⊇⊕⊗⊥⋅⌈" + "⌉⌊⌋⟨⟩◊♠♣♥" + "♦"; + + STAssertEqualObjects([string1 gtm_stringByEscapingForAsciiHTML], + string2, + @"HTML escaping failed"); + + STAssertEqualObjects([@"<this & that>" gtm_stringByEscapingForAsciiHTML], + @"<this & that>", + @"HTML escaping failed"); + NSString *string = [NSString stringWithUTF8String:"パン・ド・カンパーニュ"]; + STAssertEqualObjects([string gtm_stringByEscapingForAsciiHTML], + @"パン・ド・カ" + "ンパーニュ", + @"HTML escaping failed"); + + // Mix in some right - to left + string = [NSString stringWithUTF8String:"abcا1ب<تdef&"]; + STAssertEqualObjects([string gtm_stringByEscapingForAsciiHTML], + @"abcا1ب<تdef&", + @"HTML escaping failed"); +} // stringByEscapingAsciiHTML + +- (void)testStringByUnescapingHTML { + NSString *string1 = + @""&'<> ¡¢£¤¥" + "¦§¨©ª«¬­®¯°" + "±²³´µ¶·¸¹" + "º»¼½¾¿ÀÁ" + "ÂÃÄÅÆÇÈÉ" + "ÊËÌÍÎÏÐÑÒ" + "ÓÔÕÖ×ØÙÚ" + "ÛÜÝÞßàáâã" + "äåæçèéêëì" + "íîïðñòóôõ" + "ö÷øùúûüýþ" + "ÿŒœŠšŸƒˆ˜" + "ΑΒΓΔΕΖΗΘΙ" + "ΚΛΜΝΞΟΠΡΣΤ" + "ΥΦΧΨΩαβγδ" + "εζηθικλμνξ" + "οπρςστυφχψ" + "ωϑϒϖ   ‌‍" + "‎‏–—‘’‚“”" + "„†‡•…‰′″" + "‹›‾⁄€℘ℑℜ™" + "ℵ←↑→↓↔↵⇐⇑⇒" + "⇓⇔∀∂∃∅∇∈∉∋" + "∏∑−∗√∝∞∠∧∨" + "∩∪∫∴∼≅≈≠≡≤≥" + "⊂⊃⊄⊆⊇⊕⊗⊥⋅⌈" + "⌉⌊⌋⟨⟩◊♠♣♥" + "♦"; + + 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([@"ABC" gtm_stringByUnescapingFromHTML], + @"ABC", @"HTML unescaping failed"); + + STAssertEqualObjects([@"" gtm_stringByUnescapingFromHTML], + @"", @"HTML unescaping failed"); + + STAssertEqualObjects([@"A&Bang;C" gtm_stringByUnescapingFromHTML], + @"A&Bang;C", @"HTML unescaping failed"); + + STAssertEqualObjects([@"A&Bang;C" gtm_stringByUnescapingFromHTML], + @"A&Bang;C", @"HTML unescaping failed"); + + STAssertEqualObjects([@"A&Bang;C" gtm_stringByUnescapingFromHTML], + @"A&Bang;C", @"HTML unescaping failed"); + + STAssertEqualObjects([@"AA;" gtm_stringByUnescapingFromHTML], + @"AA;", @"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([@"<this & that>" 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 + @""", + @"&", + @"'", + @"<", + @">", +}; + +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"z&z'z<z>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 |