sogo/SoObjects/SOGo/NSString+Utilities.m

862 lines
25 KiB
Objective-C

/* NSString+Utilities.m - this file is part of SOGo
*
* Copyright (C) 2006-2019 Inverse inc.
*
* This file is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This file is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#import <EOControl/EOQualifier.h>
#import <NGExtensions/NSDictionary+misc.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NGBase64Coding.h>
#import <NGMime/NGMimeHeaderFieldGenerator.h>
#import <SBJson/SBJsonParser.h>
#import "NSArray+Utilities.h"
#import "NSDictionary+URL.h"
#import "NSString+Utilities.h"
static NSMutableCharacterSet *urlNonEndingChars = nil;
static NSMutableCharacterSet *urlAfterEndingChars = nil;
static NSMutableCharacterSet *schemaStartChars = nil;
static NSMutableCharacterSet *emailStartChars = nil;
static NSString **cssEscapingStrings = NULL;
static unichar *cssEscapingCharacters = NULL;
static int cssEscapingCount;
@implementation NSString (SOGoURLExtension)
- (NSString *) composeURLWithAction: (NSString *) action
parameters: (NSDictionary *) urlParameters
andHash: (BOOL) useHash
{
NSMutableString *completeURL;
completeURL = [NSMutableString new];
[completeURL autorelease];
[completeURL appendString: [self urlWithoutParameters]];
if (![completeURL hasSuffix: @"/"])
[completeURL appendString: @"/"];
[completeURL appendString: action];
if (urlParameters)
[completeURL appendString: [urlParameters asURLParameters]];
if (useHash)
[completeURL appendString: @"#"];
return completeURL;
}
- (NSString *) hostlessURL
{
NSString *newURL;
NSRange hostR, locationR;
if ([self hasPrefix: @"/"])
{
newURL = [self copy];
[newURL autorelease];
}
else
{
hostR = [self rangeOfString: @"://"];
locationR = [[self substringFromIndex: (hostR.location + hostR.length)]
rangeOfString: @"/"];
newURL = [self substringFromIndex: (hostR.location + hostR.length
+ locationR.location)];
}
return newURL;
}
- (NSString *) urlWithoutParameters;
{
NSRange r;
NSString *newUrl;
r = [self rangeOfString:@"?" options: NSBackwardsSearch];
if (r.length > 0)
newUrl = [self substringToIndex: NSMaxRange (r) - 1];
else
newUrl = self;
return newUrl;
}
- (NSRange) _rangeOfURLInRange: (NSRange) refRange
withPrefixChars: (NSCharacterSet *) startChars
{
int start, length;
NSRange workRange;
// [urlNonEndingChars addCharactersInString: @">&=,.:;\t \r\n"];
// [urlAfterEndingChars addCharactersInString: @"()[]{}&;<\t \r\n"];
start = refRange.location;
if (start > 0)
start--; // Start with the character before the refRange
while (start > -1
&& [startChars characterIsMember:
[self characterAtIndex: start]])
start--;
start++;
length = [self length];
// In [UIxMailPartTextViewer flatContentAsString], we first escape HTML entities and then
// add URLs. Therefore, the brackets (inequality signs <>) have been encoded at this point.
if (length > (start + 4)
&& [[self substringWithRange: NSMakeRange (start, 4)] compare: @"&lt;"] == NSOrderedSame)
start += 4;
length -= start;
workRange = [self rangeOfCharacterFromSet: urlAfterEndingChars
options: NSLiteralSearch range: NSMakeRange (start, length)];
if (workRange.location != NSNotFound)
length = workRange.location - start;
while
(length > 0
&& [urlNonEndingChars characterIsMember:
[self characterAtIndex: (start + length - 1)]])
length--;
// Remove trailing ">"
if (([self length] >= start + length + 1)
&& [[self substringWithRange: NSMakeRange (start, length + 1)] hasSuffix: @"&gt;"])
length -= 3;
return NSMakeRange (start, length);
}
- (void) _handleURLs: (NSMutableString *) selfCopy
textToMatch: (NSString *) match
urlPrefixChars: (NSCharacterSet *) startChars
prefix: (NSString *) prefix
inRanges: (NSMutableArray *) ranges
{
NSEnumerator *enumRanges;
NSMutableArray *newRanges;
NSRange matchRange, currentUrlRange, rest;
NSRange *rangePtr;
NSString *urlText, *newUrlText;
unsigned int length, matchLength, offset;
newRanges = [NSMutableArray array];
matchLength = [match length];
matchRange = [selfCopy rangeOfString: match];
while (matchRange.location != NSNotFound)
{
currentUrlRange = [selfCopy _rangeOfURLInRange: matchRange
withPrefixChars: startChars];
if (![ranges hasRangeIntersection: currentUrlRange])
if (currentUrlRange.length > matchLength)
[newRanges addNonNSObject: &currentUrlRange
withSize: sizeof (NSRange)
copy: YES];
rest.location = NSMaxRange (currentUrlRange);
length = [selfCopy length];
rest.length = length - rest.location;
matchRange = [selfCopy rangeOfString: match
options: 0 range: rest];
}
// Make the substitutions, keep track of the new offset
offset = 0;
enumRanges = [newRanges objectEnumerator];
while ((rangePtr = [[enumRanges nextObject] pointerValue]))
{
rangePtr->location += offset;
urlText = [selfCopy substringFromRange: *rangePtr];
newUrlText = [NSString stringWithFormat: @"<a rel=\"noopener\" href=\"%@%@\">%@</a>",
([urlText hasPrefix: prefix]? @"" : prefix),
urlText, urlText];
[selfCopy replaceCharactersInRange: *rangePtr
withString: newUrlText];
offset += ([newUrlText length] - [urlText length]);
// Add range for further substitutions
currentUrlRange = NSMakeRange (rangePtr->location, [newUrlText length]);
[ranges addNonNSObject: &currentUrlRange
withSize: sizeof (NSRange)
copy: YES];
}
[newRanges freeNonNSObjects];
}
- (NSString *) stringByDetectingURLs
{
NSMutableString *selfCopy;
NSMutableArray *ranges;
if (!urlNonEndingChars)
{
urlNonEndingChars = [NSMutableCharacterSet new];
[urlNonEndingChars addCharactersInString: @"=,.:;&()\t \r\n"];
}
if (!urlAfterEndingChars)
{
urlAfterEndingChars = [NSMutableCharacterSet new];
[urlAfterEndingChars addCharactersInString: @"()[]\t \r\n"];
}
if (!schemaStartChars)
{
schemaStartChars = [NSMutableCharacterSet new];
[schemaStartChars addCharactersInString: @"abcdefghijklmnopqrstuvwxyz"
@"ABCDEFGHIJKLMNOPQRSTUVWXYZ"];
}
if (!emailStartChars)
{
emailStartChars = [NSMutableCharacterSet new];
[emailStartChars addCharactersInString: @"abcdefghijklmnopqrstuvwxyz"
@"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
@"01234567890"
@"!#$%&'*+-/=?^`{|}~."];
}
ranges = [NSMutableArray array];
selfCopy = [NSMutableString stringWithString: self];
[self _handleURLs: selfCopy
textToMatch: @"://"
urlPrefixChars: schemaStartChars
prefix: @""
inRanges: ranges];
[self _handleURLs: selfCopy
textToMatch: @"@"
urlPrefixChars: emailStartChars
prefix: @"mailto:"
inRanges: ranges];
[ranges freeNonNSObjects];
return selfCopy;
}
- (NSString *) asSafeJSString
{
NSMutableString *representation;
representation = [NSMutableString stringWithString: self];
[representation replaceString: @"\\" withString: @"\\\\"];
[representation replaceString: @"\"" withString: @"\\\""];
[representation replaceString: @"/" withString: @"\\/"];
[representation replaceString: @"\f" withString: @"\\f"];
[representation replaceString: @"\n" withString: @"\\n"];
[representation replaceString: @"\r" withString: @"\\r"];
[representation replaceString: @"\t" withString: @"\\t"];
[representation replaceString: @"\0" withString: @""];
return representation;
}
- (NSString *) doubleQuotedString
{
return [NSString stringWithFormat: @"\"%@\"", [self asSafeJSString]];
}
//
// See http://www.hackcraft.net/xmlUnicode/
//
// XML1.0 and XML1.1 allow different characters in different contexts,
// but for the most part I will only describe the XML1.0 usage, XML1.1
// usage is analogous. The first definition that is relevant here is
// that of a Char: [2] Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] |
// [#xE000-#xFFFD] | [#x10000-#x10FFFF] /* any Unicode character,
// excluding the surrogate blocks, FFFE, and FFFF. */
//
// This defines which characters can be used in an XML1.0 document. It
// is clearly very liberal, banning only some of the control
// characters and the noncharacters U+FFFE and U+FFFF. Indeed it is
// somewhat too liberal in my view since it allows other noncharacters
// (the code points from U+FDD0 to U+FDEF inclusive and the last 2
// code points in each plane, from U+1FFFE & U+1FFFF through to
// U+10FFFE & U+10FFFF, are noncharacters) but the production quoted
// above allows them.
//
- (NSString *) safeString
{
NSData *data;
NSString *s;
const wchar_t *buf;
wchar_t *start, c;
int len, i, j;
data = [self dataUsingEncoding: NSUTF32LittleEndianStringEncoding];
len = [data length];
buf = [data bytes];
start = (wchar_t *)calloc(len, sizeof(wchar_t));
for (i = 0, j = 0; i < len/4; i++)
{
c = buf[i];
if (c == 0x0 ||
c == 0x9 ||
c == 0xA ||
(c >= 0x20 && c < 0x300) || // Skip combining diacritical marks
(c > 0x36F && c < 0xD7FF) ||
(c >= 0xE000 && c < 0xFE00) || // Skip variation selectors
(c > 0xFE0F && c <= 0xFFFD) ||
(c >= (wchar_t)0x10000 && c <= (wchar_t)0x10FFFF))
{
start[j] = c;
j++;
}
}
s = [[NSString alloc] initWithBytesNoCopy: start length: j*sizeof(wchar_t) encoding: NSUTF32LittleEndianStringEncoding freeWhenDone: YES];
return AUTORELEASE(s);
}
- (NSString *) safeStringByEscapingXMLString
{
return [self safeStringByEscapingXMLString: NO];
}
//
// This is a copy from NSString+XMLEscaping.m from SOPE.
// The difference here is that we use wchar_t instead of unichar.
// This is needed to get the rigth numeric character reference.
// e.g. SMILING FACE WITH OPEN MOUTH
// ok: wchar_t -> &#128515; wrong: unichar -> &#55357; &#56835;
//
// We avoid naming it like the one in SOPE since if the ActiveSync
// bundle is loaded, it'll overwrite the one provided by SOPE.
//
- (NSString *) safeStringByEscapingXMLString: (BOOL) encodeCR
{
NSData *data;
register unsigned i, len, j;
register wchar_t *buf;
const wchar_t *chars;
unsigned escapeCount;
if ([self length] == 0) return @"";
data = [self dataUsingEncoding: NSUTF32StringEncoding];
chars = [data bytes];
len = [data length]/4;
/* check for characters to escape ... */
for (i = 0, escapeCount = 0; i < len; i++)
{
switch (chars[i]) {
case '&': case '"': case '<': case '>': case '\r':
escapeCount++;
break;
default:
if (chars[i] < 0x20 || chars[i] > 127)
escapeCount++;
break;
}
}
/* nothing to escape ... */
if (escapeCount == 0 )
return [[self copy] autorelease];
buf = calloc((len + 5) + (escapeCount * 16), sizeof(wchar_t));
for (i = 0, j = 0; i < len; i++)
{
switch (chars[i])
{
/* escape special chars */
case '&':
buf[j] = '&'; j++; buf[j] = 'a'; j++; buf[j] = 'm'; j++;
buf[j] = 'p'; j++; buf[j] = ';'; j++;
break;
case '"':
buf[j] = '&'; j++; buf[j] = 'q'; j++; buf[j] = 'u'; j++;
buf[j] = 'o'; j++; buf[j] = 't'; j++; buf[j] = ';'; j++;
break;
case '<':
buf[j] = '&'; j++; buf[j] = 'l'; j++; buf[j] = 't'; j++;
buf[j] = ';'; j++;
break;
case '>':
buf[j] = '&'; j++; buf[j] = 'g'; j++; buf[j] = 't'; j++;
buf[j] = ';'; j++;
break;
case '\r':
if (encodeCR) // falls back to default if we don't encode
{
buf[j] = '&'; j++; buf[j] = '#'; j++; buf[j] = '1'; j++;
buf[j] = '3'; j++; buf[j] = ';'; j++;
break;
}
default:
/* escape big chars */
if (chars[i] > 127)
{
unsigned char nbuf[32];
unsigned int k;
sprintf((char *)nbuf, "&#%i;", (int)chars[i]);
for (k = 0; nbuf[k] != '\0'; k++)
{
buf[j] = nbuf[k];
j++;
}
}
else if (chars[i] == 0x9 || chars[i] == 0xA || chars[i] == 0xD || chars[i] >= 0x20)
{ // ignore any unsupported control character
/* nothing to escape */
buf[j] = chars[i];
j++;
}
break;
}
}
self = [[NSString alloc] initWithBytesNoCopy: buf
length: (j*sizeof(wchar_t))
encoding: NSUTF32StringEncoding
freeWhenDone: YES];
return [self autorelease];
}
- (NSString *) jsonRepresentation
{
NSString *cleanedString;
// Escape double quotes and remove control characters
cleanedString = [[self safeString] doubleQuotedString];
return cleanedString;
}
- (void) _setupCSSEscaping
{
NSArray *strings, *characters;
int count;
strings = [NSArray arrayWithObjects: @"_U_", @"_D_", @"_H_", @"_A_", @"_S_",
@"_C_", @"_SC_",
@"_CO_", @"_SP_", @"_SQ_", @"_DQ_",
@"_LP_", @"_RP_", @"_LS_", @"_RS_", @"_LC_", @"_RC_",
@"_AM_", @"_P_", @"_DS_", nil];
[strings retain];
cssEscapingStrings = [strings asPointersOfObjects];
characters = [NSArray arrayWithObjects: @"_", @".", @"#", @"@", @"*",
@":", @";",
@",", @" ", @"'", @"\"",
@"(", @")", @"[", @"]", @"{", @"}",
@"&", @"+", @"$", nil];
cssEscapingCount = [strings count];
cssEscapingCharacters = NSZoneMalloc (NULL,
(cssEscapingCount + 1)
* sizeof (unichar));
for (count = 0; count < cssEscapingCount; count++)
*(cssEscapingCharacters + count) = [[characters objectAtIndex: count] characterAtIndex: 0];
*(cssEscapingCharacters + cssEscapingCount) = 0;
}
- (int) _cssCharacterIndex: (unichar) character
{
int idx, count;
idx = -1;
for (count = 0; idx == -1 && count < cssEscapingCount; count++)
if (*(cssEscapingCharacters + count) == character)
idx = count;
return idx;
}
- (NSString *) asCSSIdentifier
{
NSCharacterSet *numericSet;
NSMutableString *cssIdentifier;
unichar currentChar;
int count, max, idx;
if (!cssEscapingStrings)
[self _setupCSSEscaping];
cssIdentifier = [NSMutableString string];
numericSet = [NSCharacterSet decimalDigitCharacterSet];
max = [self length];
if (max > 0)
{
if ([numericSet characterIsMember: [self characterAtIndex: 0]])
// A CSS identifier can't start with a digit; we add an underscore
[cssIdentifier appendString: @"_"];
for (count = 0; count < max; count++)
{
currentChar = [self characterAtIndex: count];
idx = [self _cssCharacterIndex: currentChar];
if (idx > -1)
[cssIdentifier appendString: cssEscapingStrings[idx]];
else
[cssIdentifier appendFormat: @"%C", currentChar];
}
}
return cssIdentifier;
}
- (int) _cssStringIndex: (NSString *) string
{
int idx, count;
idx = -1;
for (count = 0; idx == -1 && count < cssEscapingCount; count++)
if ([string hasPrefix: *(cssEscapingStrings + count)])
idx = count;
return idx;
}
- (NSString *) fromCSSIdentifier
{
NSCharacterSet *numericSet;
NSMutableString *newString;
NSString *currentString;
int count, length, max, idx;
unichar currentChar;
if (!cssEscapingStrings)
[self _setupCSSEscaping];
numericSet = [NSCharacterSet decimalDigitCharacterSet];
newString = [NSMutableString string];
max = [self length];
count = 0;
if (max > 0
&& [self characterAtIndex: 0] == '_'
&& [numericSet characterIsMember: [self characterAtIndex: 1]])
{
/* If the identifier starts with an underscore followed by a digit,
we remove the underscore */
count = 1;
}
for (; count < max - 2; count++)
{
currentChar = [self characterAtIndex: count];
if (currentChar == '_')
{
/* The difficulty here is that most escaping strings are 3 chars
long except one. Therefore we must juggle a little bit with the
lengths in order to avoid an overflow exception. */
length = 4;
if (count + length > max)
length = max - count;
currentString = [self substringFromRange: NSMakeRange (count, length)];
idx = [self _cssStringIndex: currentString];
if (idx > -1)
{
[newString appendFormat: @"%C", cssEscapingCharacters[idx]];
count += [cssEscapingStrings[idx] length] - 1;
}
else
[newString appendFormat: @"%C", currentChar];
}
else
[newString appendFormat: @"%C", currentChar];
}
currentString = [self substringFromRange: NSMakeRange (count, max - count)];
[newString appendString: currentString];
return newString;
}
- (NSString *) pureEMailAddress
{
NSString *pureAddress;
NSRange delimiter;
delimiter = [self rangeOfString: @"<"];
if (delimiter.location == NSNotFound)
pureAddress = self;
else
{
pureAddress = [self substringFromIndex: NSMaxRange (delimiter)];
delimiter = [pureAddress rangeOfString: @">"];
if (delimiter.location != NSNotFound)
pureAddress = [pureAddress substringToIndex: delimiter.location];
}
return pureAddress;
}
- (NSString *) asQPSubjectString: (NSString *) encoding
{
return [NGMimeHeaderFieldGenerator encodeQuotedPrintableText: self];
}
- (BOOL) caseInsensitiveMatches: (NSString *) match
{
EOQualifier *sq;
NSString *format;
format = [NSString stringWithFormat:
@"(description isCaseInsensitiveLike: '%@')",
match];
sq = [EOQualifier qualifierWithQualifierFormat: format];
return [(id<EOQualifierEvaluation>)sq evaluateWithObject: self];
}
//
// To decompose the DN extracted from a SSL certificate
// using the XN_FLAG_SEP_MULTILINE flag.
//
- (NSArray *) componentsFromMultilineDN
{
NSArray *pair;
NSEnumerator *componentsEnum, *rdnComponentsEnum;
NSMutableArray *components;
NSString *component, *pairString;
components = [NSMutableArray array];
componentsEnum = [[self componentsSeparatedByString: @"\n"] objectEnumerator];
while (( component = [componentsEnum nextObject] ))
{
rdnComponentsEnum = [[component componentsSeparatedByString: @" + "] objectEnumerator];
while (( pairString = [rdnComponentsEnum nextObject] ))
{
pair = [pairString componentsSeparatedByString: @"="];
if ([pair count] == 2)
[components addObject: [NSArray arrayWithObjects:
[pair objectAtIndex: 0],
[pair objectAtIndex: 1], nil]];
}
}
return components;
}
#if LIB_FOUNDATION_LIBRARY
- (BOOL) boolValue
{
return !([self isEqualToString: @"0"]
|| [self isEqualToString: @"NO"]);
}
#endif
- (int) timeValue
{
int time;
NSInteger i;
if ([self length] > 0)
{
i = [self rangeOfString: @":"].location;
if (i == NSNotFound)
time = [self intValue];
else
time = [[self substringToIndex: i] intValue];
}
else
time = -1;
return time;
}
- (BOOL) isJSONString
{
NSDictionary *jsonData;
#warning this method is a quick and dirty way of detecting the file-format
jsonData = [self objectFromJSONString];
return (jsonData != nil);
}
- (id) objectFromJSONString
{
SBJsonParser *parser;
NSArray *object;
NSError *error;
NSString *unescaped, *json;
object = nil;
if ([self length] > 0)
{
parser = [SBJsonParser new];
[parser autorelease];
error = nil;
/* Parse it this way so we can parse simple values, like "null" */
json = [NSString stringWithFormat: @"[%@]", self];
object = [parser objectWithString: json
error: &error];
if (error)
{
[self errorWithFormat: @"json parser: %@,"
@" attempting once more after unescaping...", error];
unescaped = [json stringByReplacingString: @"\\\\"
withString: @"\\"];
object = [parser objectWithString: unescaped
error: &error];
if (error)
{
[self errorWithFormat: @"total failure. Original string is: %@", self];
object = nil;
}
else
[self logWithFormat: @"initial object deserialized successfully!"];
}
}
return [object objectAtIndex: 0];
}
- (NSString *) asSafeSQLString
{
return [[[self stringByReplacingString: @"\\" withString: @"\\\\"]
stringByReplacingString: @"'" withString: @"\\'"]
stringByReplacingString: @"\%" withString: @"\\%"];
}
- (NSUInteger) countOccurrencesOfString: (NSString *) substring
{
NSRange matchRange, substrRange;
BOOL done = NO;
NSUInteger selfLen, substrLen, count = 0;
selfLen = [self length];
substrLen = [substring length];
matchRange = NSMakeRange (0, selfLen);
while (!done && matchRange.length > 0)
{
substrRange = [self rangeOfString: substring options: 0 range: matchRange];
if (substrRange.location == NSNotFound)
done = YES;
else
{
count++;
matchRange.location = substrRange.location + 1;
if (matchRange.location + substrLen > selfLen)
done = YES;
else
matchRange.length = selfLen - matchRange.location;
}
}
return count;
}
- (NSString *) stringByReplacingPrefix: (NSString *) oldPrefix
withPrefix: (NSString *) newPrefix
{
NSUInteger oldPrefixLength;
NSString *newString;
if (![self hasPrefix: oldPrefix])
[NSException raise: NSInvalidArgumentException
format: @"string does not have the specified prefix"];
oldPrefixLength = [oldPrefix length];
newString = [NSString stringWithFormat: @"%@%@",
newPrefix,
[self substringFromIndex: oldPrefixLength]];
return newString;
}
- (NSString *) encryptWithKey: (NSString *) theKey
{
NSMutableData *encryptedPassword;
NSMutableString *key;
NSString *result;
NSUInteger i, passLength, theKeyLength, keyLength;
unichar p, k, e;
if ([theKey length] > 0)
{
// The length of the key must be greater (or equal) than
// the length of the password
key = [NSMutableString string];
keyLength = 0;
passLength = [self length];
theKeyLength = [theKey length];
while (keyLength < passLength)
{
[key appendString: theKey];
keyLength += theKeyLength;
}
encryptedPassword = [NSMutableData data];
for (i = 0; i < passLength; i++)
{
p = [self characterAtIndex: i];
k = [key characterAtIndex: i];
e = p ^ k;
[encryptedPassword appendBytes: (void *)&e length: 2];
}
result = [encryptedPassword stringByEncodingBase64];
}
else
result = nil;
return result;
}
- (NSString *) decryptWithKey: (NSString *) theKey
{
NSMutableString *result;
NSMutableString *key;
NSData *decoded;
unichar *decryptedPassword;
NSUInteger i, theKeyLength, keyLength, decodedLength;
unichar p, k;
if ([theKey length] > 0)
{
decoded = [self dataByDecodingBase64];
decryptedPassword = (unichar *)[decoded bytes];
// The length of the key must be greater (or equal) than
// the length of the password
key = [NSMutableString string];
keyLength = 0;
decodedLength = ([decoded length] / 2); /* 1 unichar = 2 bytes/char */
theKeyLength = [theKey length];
while (keyLength < decodedLength)
{
[key appendString: theKey];
keyLength += theKeyLength;
}
result = [NSMutableString string];
for (i = 0; i < decodedLength; i++)
{
k = [key characterAtIndex: i];
p = decryptedPassword[i] ^ k;
[result appendFormat: @"%C", p];
}
}
else
result = nil;
return result;
}
@end