diff --git a/BugSplat.h b/BugSplat.h index 73c9c84..7b35116 100644 --- a/BugSplat.h +++ b/BugSplat.h @@ -127,6 +127,41 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) BOOL autoSubmitCrashReport; +/** + * Add an attribute and value to the crash report. + * Attributes and values represent app supplied keys and values to associate with a crash report. + * Attributes and values will be bundled up in a BugSplatAttachment as NSData, with a filename of CrashContext.xml, MIME type of "application/xml" and encoding of "UTF-8". + * + * IMPORTANT: For iOS, only one BugSplatAttachment is currently supported. + * If BugSplatDelegate's method `- (BugSplatAttachment *)attachmentForBugSplat:(BugSplat *)bugSplat` returns a non-nil BugSplatAttachment, + * BugSplat will send that BugSplatAttachment, not the BugSplatAttachment that would otherwise be created due to adding attributes and values using this method. + * + * NOTES: + * + * This method may be called multiple times, once per attribute+value pair. + * Attributes are backed by an NSDictionary so attribute names must be unique. + * If the attribute does not exist, it will be added to attributes dictionary. + * If attribute already exists, the value will be replaced in the dictionary. + * If attribute already exists, and the value is nil, the attribute will be removed from the dictionary. + * + * When this method is called, the following preprocessing occurs: + * 1. attribute will first have white space and newlines removed from both the beginning and end of the String. + * + * 2. attribute will then be processed by an XML escaping routine which looks for escapable characters ",',&,<, and > + * See: https://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents + * Any XML comment blocks or CDATA blocks found will disable XML escaping within the block. + * + * 3. values will then be processed by an XML escaping routine which looks for escapable characters ",',&,<, and > + * Any XML comment blocks or CDATA blocks found will disable XML escaping within the block. + * + * 4. After processing both attribute and value for XML escape characters, the attribute+value pair will be stored in an NSDictionary. + * + * If a crash occurs, attributes and values will be bundled up in a BugSplatAttachment as NSData, with a filename of CrashContext.xml, MIME type of "application/xml" + * and encoding of "UTF-8". The attachment will be included with the crash data (except as noted above regarding iOS BugSplatAttachment limitation). + * + */ +- (void)setValue:(nullable NSString *)value forAttribute:(NSString *)attribute; + // macOS specific API #if TARGET_OS_OSX /*! diff --git a/BugSplat.m b/BugSplat.m index 0ad703c..851202d 100644 --- a/BugSplat.m +++ b/BugSplat.m @@ -6,11 +6,24 @@ #import #import +#import "BugSplatUtilities.h" NSString *const kHockeyIdentifierPlaceholder = @"b0cf675cb9334a3e96eda0764f95e38e"; // Just to satisfy Hockey since this is required @interface BugSplat() +/** + * Attributes represent app supplied keys and values additional to the crash report. + * Attributes will be bundled up in a BugSplatAttachment as NSData, with a filename of CrashContext.xml, MIME type of "application/xml" and encoding of "UTF-8". + * + * NOTES: + * + * + * IMPORTANT: For iOS, if BugSplatDelegate's method `- (BugSplatAttachment *)attachmentForBugSplat:(BugSplat *)bugSplat` returns a non-nil BugSplatAttachment, + * attributes will be ignored (NOT be included in the Crash Report). This is a current limitation of the iOS BugSplat API. + */ +@property (nonatomic, nullable) NSMutableDictionary *attributes; + @end @implementation BugSplat @@ -119,6 +132,28 @@ - (void)setAutoSubmitCrashReport:(BOOL)autoSubmitCrashReport } +- (void)setValue:(nullable NSString *)value forAttribute:(NSString *)attribute +{ + if (_attributes == nil && value != nil) { + _attributes = [NSMutableDictionary dictionary]; + } + + // clean up attribute and values + // See: https://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents + + // first remove newlines and whitespace from prefix or suffix of an attribute since these will be nodes in the XML document + NSString *cleanedUpAttribute = [attribute stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; + NSString *escapedAttribute = [cleanedUpAttribute stringByEscapingXMLCharactersIgnoringCDataAndComments]; + + // escape xml characters in value + NSString *escapedValue = [value stringByEscapingXMLCharactersIgnoringCDataAndComments]; + + NSLog(@"BugSplat adding attribute [_attributes setValue%@ forKey:%@]", escapedValue, escapedAttribute); + + // add to _attributes dictionary + [_attributes setValue:escapedValue forKey:escapedAttribute]; +} + #if TARGET_OS_OSX @@ -172,22 +207,34 @@ -(BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManage if ([_delegate respondsToSelector:@selector(attachmentForBugSplat:)]) { BugSplatAttachment *attachment = [_delegate attachmentForBugSplat:self]; - - return [[BITHockeyAttachment alloc] initWithFilename:attachment.filename - hockeyAttachmentData:attachment.attachmentData - contentType:attachment.contentType]; + + if (attachment) + { + return [[BITHockeyAttachment alloc] initWithFilename:attachment.filename + hockeyAttachmentData:attachment.attachmentData + contentType:attachment.contentType]; + } } - + + // no delegate provided BugSplatAttachment, send attributes as attributesAttachment if present + BugSplatAttachment *attributesAttachment = [self bugSplatAttachmentWithAttributes:self.attributes]; + if (attributesAttachment) + { + return [[BITHockeyAttachment alloc] initWithFilename:attributesAttachment.filename + hockeyAttachmentData:attributesAttachment.attachmentData + contentType:attributesAttachment.contentType]; + } + return nil; } // MacOS - (NSArray *)attachmentsForCrashManager:(BITCrashManager *)crashManager { + NSMutableArray *attachments = [[NSMutableArray alloc] init]; + if ([_delegate respondsToSelector:@selector(attachmentsForBugSplat:)]) { - NSMutableArray *attachments = [[NSMutableArray alloc] init]; - NSArray *bugsplatAttachments = [_delegate attachmentsForBugSplat:self]; for (BugSplatAttachment *attachment in bugsplatAttachments) @@ -199,7 +246,6 @@ -(BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManage [attachments addObject:hockeyAttachment]; } - return [attachments copy]; } else if ([_delegate respondsToSelector:@selector(attachmentForBugSplat:)]) { @@ -215,7 +261,22 @@ -(BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManage hockeyAttachmentData:attachment.attachmentData contentType:attachment.contentType]; - return @[hockeyAttachment]; + [attachments addObject:hockeyAttachment]; + } + + // include attributes as attributesAttachment if present + BugSplatAttachment *attributesAttachment = [self bugSplatAttachmentWithAttributes:self.attributes]; + if (attributesAttachment) + { + BITHockeyAttachment *hockeyAttachment = [[BITHockeyAttachment alloc] initWithFilename:attributesAttachment.filename + hockeyAttachmentData:attributesAttachment.attachmentData + contentType:attributesAttachment.contentType]; + [attachments addObject:hockeyAttachment]; + } + + if ([attachments count] > 0) + { + return [attachments copy]; } return nil; @@ -272,4 +333,50 @@ - (void)crashManagerDidFinishSendingCrashReport:(BITCrashManager *)crashManager } } +/** + * If attributes are present, bundle them up as a BugSplatAttachment containing + * NSData created from NSString representing an XML file, filename of CrashContext.xml, MIME type of "application/xml" and encoding of "UTF-8". + */ +- (BugSplatAttachment *)bugSplatAttachmentWithAttributes:(NSDictionary *)attributes +{ + if (attributes == nil || [attributes count] == 0) + { + return nil; + } + + // prepare XML as stringData from attributes + // NOTE: If NSXMLDocument was available for iOS, that would be the better choice for building our XMLDocument... + + NSMutableString *stringData = [NSMutableString new]; + [stringData appendString:@"\n"]; + [stringData appendString:@"\n"]; + + // for each attribute:value pair, add value row to the XML stringData + for (NSString *attribute in attributes.allKeys) { + NSString *value = attributes[attribute]; + [stringData appendFormat:@"<%@>", attribute]; + [stringData appendString:value]; + [stringData appendFormat:@"", attribute]; + [stringData appendString:@"\n"]; + } + + [stringData appendString:@"\n"]; + + NSData *data = [stringData dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]; + + if (data) + { + // debug logging + NSString *debugString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (debugString) + { + NSLog(@"BugSplat adding attributes as BugSplatAttachment with contents: [%@]", debugString); + } + + return [[BugSplatAttachment alloc] initWithFilename:@"CrashContext.xml" attachmentData:data contentType:@"UTF-8"]; + } + + return nil; +} + @end diff --git a/BugSplat.xcodeproj/project.pbxproj b/BugSplat.xcodeproj/project.pbxproj index 157bbc4..b8b2c11 100644 --- a/BugSplat.xcodeproj/project.pbxproj +++ b/BugSplat.xcodeproj/project.pbxproj @@ -11,6 +11,10 @@ 6341B28D2BDC514C007EE8AC /* HockeySDK.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 63598C3C2B9BBD6600770E43 /* HockeySDK.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 63598C432BA0CCD500770E43 /* BugSplat.m in Sources */ = {isa = PBXBuildFile; fileRef = 63598C422BA0CCD500770E43 /* BugSplat.m */; }; 63598C442BA0CCD500770E43 /* BugSplat.m in Sources */ = {isa = PBXBuildFile; fileRef = 63598C422BA0CCD500770E43 /* BugSplat.m */; }; + 637DF0452D49A49C00DDD8FE /* BugSplatUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 637DF0442D49A49C00DDD8FE /* BugSplatUtilities.h */; }; + 637DF0462D49A49C00DDD8FE /* BugSplatUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 637DF0442D49A49C00DDD8FE /* BugSplatUtilities.h */; }; + 637DF0482D49A50800DDD8FE /* BugSplatUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 637DF0472D49A50800DDD8FE /* BugSplatUtilities.m */; }; + 637DF0492D49A50800DDD8FE /* BugSplatUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 637DF0472D49A50800DDD8FE /* BugSplatUtilities.m */; }; 638D38DC2BA0CEE900DC37A8 /* BugSplatDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6344718C2B9695C500EBEBEB /* BugSplatDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 638D38DD2BA0CEEA00DC37A8 /* BugSplatDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6344718C2B9695C500EBEBEB /* BugSplatDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 63C6E2032B92852500AED3E3 /* BugSplat.h in Headers */ = {isa = PBXBuildFile; fileRef = 63C6E1FE2B9283B000AED3E3 /* BugSplat.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -53,6 +57,8 @@ 6344718C2B9695C500EBEBEB /* BugSplatDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugSplatDelegate.h; sourceTree = ""; }; 63598C3C2B9BBD6600770E43 /* HockeySDK.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = HockeySDK.xcframework; path = Vendor/HockeySDK.xcframework; sourceTree = ""; }; 63598C422BA0CCD500770E43 /* BugSplat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugSplat.m; sourceTree = ""; }; + 637DF0442D49A49C00DDD8FE /* BugSplatUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugSplatUtilities.h; sourceTree = ""; }; + 637DF0472D49A50800DDD8FE /* BugSplatUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BugSplatUtilities.m; sourceTree = ""; }; 63C6E1ED2B92831A00AED3E3 /* BugSplatMac.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BugSplatMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 63C6E1FC2B9283B000AED3E3 /* BugSplat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BugSplat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 63C6E1FE2B9283B000AED3E3 /* BugSplat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugSplat.h; sourceTree = ""; }; @@ -90,6 +96,8 @@ 63E2337C2B9710420066C7FC /* BugSplatAttachment.h */, 63E2337A2B9710410066C7FC /* BugSplatAttachment.m */, 6344718C2B9695C500EBEBEB /* BugSplatDelegate.h */, + 637DF0442D49A49C00DDD8FE /* BugSplatUtilities.h */, + 637DF0472D49A50800DDD8FE /* BugSplatUtilities.m */, 63C6E1EE2B92831A00AED3E3 /* Products */, 63C6E2062B9285F400AED3E3 /* Frameworks */, ); @@ -123,6 +131,7 @@ 63D006FA2BBF785B00587FBF /* BugSplatMac.h in Headers */, 638D38DD2BA0CEEA00DC37A8 /* BugSplatDelegate.h in Headers */, 63E233822B9710420066C7FC /* BugSplatAttachment.h in Headers */, + 637DF0452D49A49C00DDD8FE /* BugSplatUtilities.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -133,6 +142,7 @@ 63E233862B9715B50066C7FC /* BugSplat.h in Headers */, 638D38DC2BA0CEE900DC37A8 /* BugSplatDelegate.h in Headers */, 63E233842B9711AB0066C7FC /* BugSplatAttachment.h in Headers */, + 637DF0462D49A49C00DDD8FE /* BugSplatUtilities.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -236,6 +246,7 @@ buildActionMask = 2147483647; files = ( 63E233802B9710420066C7FC /* BugSplatAttachment.m in Sources */, + 637DF0482D49A50800DDD8FE /* BugSplatUtilities.m in Sources */, 63598C432BA0CCD500770E43 /* BugSplat.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -245,6 +256,7 @@ buildActionMask = 2147483647; files = ( 63E233852B9711B10066C7FC /* BugSplatAttachment.m in Sources */, + 637DF0492D49A50800DDD8FE /* BugSplatUtilities.m in Sources */, 63598C442BA0CCD500770E43 /* BugSplat.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/BugSplatDelegate.h b/BugSplatDelegate.h index f1cf594..22b8a57 100644 --- a/BugSplatDelegate.h +++ b/BugSplatDelegate.h @@ -70,8 +70,7 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSString *)applicationKeyForBugSplat:(BugSplat *)bugSplat signal:(NSString *)signal exceptionName:(NSString *)exceptionName exceptionReason:(NSString *)exceptionReason API_AVAILABLE(macosx(10.13)); -/** Return a collection of BugsplatAttachment objects providing an NSData object the crash report - being processed should contain +/** Return a collection of BugsplatAttachment objects providing an NSData object the crash report being processed should contain Example implementation: @@ -87,8 +86,8 @@ NS_ASSUME_NONNULL_BEGIN // MARK: - BugSplatDelegate (iOS) -/** Return a collection of BugSplatAttachment objects providing an NSData object the crash report - being processed should contain +/** Return a BugSplatAttachment object providing an NSData object the crash report being processed should contain + NOTE: If this method returns a non-nil BugSplatAttachment, any attributes added via setAttribute:value: to BugSplat will NOT be included in the Crash Report. Example implementation: diff --git a/BugSplatUtilities.h b/BugSplatUtilities.h new file mode 100644 index 0000000..6e350d2 --- /dev/null +++ b/BugSplatUtilities.h @@ -0,0 +1,53 @@ +// +// BugSplatUtilities.h +// +// Copyright © BugSplat, LLC. All rights reserved. +// + +#import + +@interface NSArray (XMLArrayUtility) + +/** + * Given an array of tokenPairRange values, remove any tokenPairRange values that are + * either fully contained within another range, or overlapping (partially contained) within another range. + * return: validTokenPairRange in ascending range order by location. + */ ++ (NSArray *)validTokenPairRanges:(NSArray *)tokenPairRanges; + +@end + + +@interface NSString (XMLStringUtility) + +/** + * Utility method to clean up special characters found in attribute or value strings + * + * NOTE: API based on macOS 10.3+ only API CFStringRef CFXMLCreateStringByEscapingEntities(CFAllocatorRef allocator, CFStringRef string, CFDictionaryRef entitiesDictionary) + * This method considers CDATA, and Comments, but currently omits Processing Instructions when escaping is done on the receiver. + */ +- (NSString *)stringByEscapingXMLCharactersIgnoringCDataAndComments; + + +/** + * Utility method to clean up special characters found in attribute or value strings + * + * NOTE: API based on macOS 10.3+ only API CFStringRef CFXMLCreateStringByEscapingEntities(CFAllocatorRef allocator, CFStringRef string, CFDictionaryRef entitiesDictionary) + * This method does not consider CDATA, Comments, nor Processing Instructions when escaping is done on the receiver. See other methods to first identify these escape-exclusion ranges. + */ +- (NSString *)stringByEscapingSpecialXMLCharacters; + +/** + * Given a start token and end token pair, search receiver, returning array of NSValue objects containing NSRange of each pair found + * Return Array will be empty if no pairs are found. + * Error will be nil if no parsing errors occur. + */ +- (NSArray *)tokenPairRangesForStartToken:(NSString *)startToken endToken:(NSString *)endToken error:(NSError **)error; + +/** + * Given a string, and an ascendingExclusionRanges sorted array (based on this receiver), escape the 5 special XML characters + * within the receiver, taking care not to escape any characters within the exclusionRanges. + */ +- (NSString *)stringByXMLEscapingWithExclusionRanges:(NSArray *)ascendingExclusionRanges; + +@end diff --git a/BugSplatUtilities.m b/BugSplatUtilities.m new file mode 100644 index 0000000..e178d44 --- /dev/null +++ b/BugSplatUtilities.m @@ -0,0 +1,302 @@ +// +// BugSplatUtilities.m +// +// Copyright © BugSplat, LLC. All rights reserved. +// + +#import "BugSplatUtilities.h" + + +@implementation NSArray (XMLArrayUtility) + +/** + * Given an array of tokenPairRange values, remove any tokenPairRange values that are + * either fully contained within another range, or overlapping (partially contained) within another range. + * - return: validTokenPairRange in least to greatest order. + */ ++ (NSArray *)validTokenPairRanges:(NSArray *)tokenPairRanges +{ + // sanity check if there are 1 or less ranges to begin with, exit early + if (tokenPairRanges.count <= 1) { + return tokenPairRanges; + } + + // first sort the input tokenPairRanges by ascending range.location order + NSArray *sortedTokenPairRanges = [tokenPairRanges sortedArrayUsingComparator:^NSComparisonResult(NSValue * _Nonnull rangeValue1, NSValue * _Nonnull rangeValue2) { + if (rangeValue1.rangeValue.location < rangeValue2.rangeValue.location) { + return NSOrderedAscending; + } else if (rangeValue1.rangeValue.location > rangeValue2.rangeValue.location) { + return NSOrderedDescending; + } else { + return NSOrderedSame; + } + }]; + + // given at least 2 ranges in sortedTokenPairRanges + // remove first range since this is valid since the location is not bounded by any other range + + // given sorted array of NSValue Ranges, remove any contained, partially contained/overlapping ranges. + NSMutableArray *sortedRanges = [NSMutableArray arrayWithArray:sortedTokenPairRanges]; + + NSValue *validRange = [sortedRanges firstObject]; + [sortedRanges removeObjectAtIndex:0]; + NSMutableArray *validRanges = [NSMutableArray arrayWithArray:@[validRange]]; + + // loop over sortedRanges comparing to validRange looking for containment or overlaps + do { + NSValue *checkRange = [sortedRanges firstObject]; + [sortedRanges removeObjectAtIndex:0]; + if (validRange.rangeValue.location + validRange.rangeValue.length <= checkRange.rangeValue.location) { + [validRanges addObject:checkRange]; + validRange = checkRange; // this becomes the next valid range to use for containment checking with remaining sortedRanges + } + + } while (sortedRanges.count > 0); + + return [validRanges copy]; +} + +@end + + +@implementation NSString (XMLStringUtility) + +/** + * Utility method to clean up special characters found in attribute or value strings + * + * NOTE: API based on macOS 10.3+ only API CFStringRef CFXMLCreateStringByEscapingEntities(CFAllocatorRef allocator, CFStringRef string, CFDictionaryRef entitiesDictionary) + * This method considers CDATA, and Comments, but currently omits Processing Instructions when escaping is done on the receiver. + */ +- (NSString *)stringByEscapingXMLCharactersIgnoringCDataAndComments +{ + NSError *error = nil; + NSArray *cDataTokenPairRanges = [self tokenPairRangesForStartToken:@"" error:&error]; + NSLog(@"CDATA token pair ranges found: %ld", cDataTokenPairRanges.count); + if (error != nil) { + NSLog(@"CDATA parsing error was found!"); + } + + error = nil; // reset + // does not check for invalid use of -- within the comment pair, nor for invalid ending --->, nor for <--- invalid starts + NSArray *commentTokenPairRanges = [self tokenPairRangesForStartToken:@"" error:&error]; + NSLog(@"Comment token pair ranges found: %ld", commentTokenPairRanges.count); + if (error != nil) { + NSLog(@"XML Comment parsing error was found!"); + } + + // processing instructions not currently supported + // NSArray *processingInstructionsTokenPairRanges = [testString tokenPairRangesForStartToken:@"" error:&error]; + + // sum of CDATA and COMMENT ranges + NSMutableArray *exclusionRanges = [NSMutableArray arrayWithArray:cDataTokenPairRanges]; + [exclusionRanges addObjectsFromArray:commentTokenPairRanges]; + + NSArray *validTokenPairRanges = [NSArray validTokenPairRanges:exclusionRanges]; + + NSLog(@"valid token pair ranges found: %ld", validTokenPairRanges.count); + for (NSValue *rangeValue in validTokenPairRanges) { + NSLog(@"range found: loc: %lu, len: %lu", rangeValue.rangeValue.location, rangeValue.rangeValue.length); + } + + NSString *escapedString = [self stringByXMLEscapingWithExclusionRanges:validTokenPairRanges]; + NSLog(@"before escape: %@", self); + NSLog(@"after escape: %@", escapedString); + + return escapedString; +} + +/** + * Utility method to clean up special characters found in attribute or value strings + * + * NOTE: API based on macOS 10.3+ only API CFStringRef CFXMLCreateStringByEscapingEntities(CFAllocatorRef allocator, CFStringRef string, CFDictionaryRef entitiesDictionary) + * This method does not consider CDATA, Comments, nor Processing Instructions when escaping is done on the receiver. See other methods to first identify these escape-exclusion ranges. + */ +- (NSString *)stringByEscapingSpecialXMLCharacters +{ + // Standard XML characters to escape + // See: https://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents + NSMutableDictionary *escapingDictionary = [NSMutableDictionary dictionaryWithObjects:@[@""", @"'", @"<", @">", @"&"] + forKeys:@[@"\"", @"'", @"<", @">", @"&"]]; + + // because '&' is a special character and is also used to escape all the special characters, replace & first before the others + // special attention must be used to only escape '&' if it is NOT already in the form of an escape code (a key in escaping dictionary) + + // find any existing escaped sequence ranges + NSMutableArray *existingEscapedRanges = [NSMutableArray new]; + + // for each escapeSequence, find all the ranges within this string + BOOL isEndReached = NO; + for (NSString *escapeSequence in escapingDictionary.objectEnumerator) { + NSRange rangeOfEscapeSequence; + NSUInteger location = 0; + do { + rangeOfEscapeSequence = [self rangeOfString:escapeSequence options:NSCaseInsensitiveSearch range:NSMakeRange(location, self.length - location)]; + if (rangeOfEscapeSequence.length != 0) + { + [existingEscapedRanges addObject:[NSValue valueWithRange:rangeOfEscapeSequence]]; + + // move location to just after escapeSequence + location = rangeOfEscapeSequence.location + rangeOfEscapeSequence.length; + + // check if at or past end of string + if (location >= self.length) + { + isEndReached = YES; + } + } + } while (rangeOfEscapeSequence.length != 0 && !isEndReached); + } + + // find all ranges of '&' within string + NSMutableArray *ampersandRanges = [NSMutableArray new]; + [self enumerateSubstringsInRange:NSMakeRange(0, self.length) + options:NSStringEnumerationByComposedCharacterSequences + usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) { + if ([substring isEqualToString:@"&"]) + { + [ampersandRanges addObject:[NSValue valueWithRange:substringRange]]; + } + }]; + + // remove any ampersandRanges which correspond to a range within existingEscapedRanges + for (NSValue *escapedRangeValue in existingEscapedRanges) { + NSValue *removeValue = [NSValue valueWithRange:NSMakeRange([escapedRangeValue rangeValue].location, 1)]; // length 1 for range of '&' + [ampersandRanges removeObject:removeValue]; + } + + // in reverse order to avoid incorrect range values after replacement, replace '&' with '&' + NSString *escapedOutput = self; + for (NSValue *rangeValue in [ampersandRanges reverseObjectEnumerator]) { + escapedOutput = [escapedOutput stringByReplacingOccurrencesOfString:@"&" + withString:@"&" + options:NSCaseInsensitiveSearch + range:[rangeValue rangeValue]]; + } + + // '&' has been replaced with '&' but only when '&' was not part of an escape sequence + // remove '&' key/value pair since it was already carefully escaped above + escapingDictionary[@"&"] = nil; + + // now, escape the other special characters + for (NSString *target in escapingDictionary.allKeys) { + NSString *replacement = escapingDictionary[target]; + escapedOutput = [escapedOutput stringByReplacingOccurrencesOfString:target withString:replacement]; + } + + return escapedOutput; +} + +/** + * Given a string, and an ascendingExclusionRanges sorted array (based on this receiver), escape the 5 special XML characters + * within the receiver, taking care not to escape any characters within the exclusionRanges. + */ +- (NSString *)stringByXMLEscapingWithExclusionRanges:(NSArray *)ascendingExclusionRanges +{ + // sanity check, if no ascendingExclusionRanges, just escape the whole string and return + if (ascendingExclusionRanges == nil || [ascendingExclusionRanges count] == 0) { // no exclusion ranges + return [self stringByEscapingSpecialXMLCharacters]; + } + + // divide receiver into substrings, escaping substrings outside of the exclusion ranges + NSUInteger substringLoc = 0; + + // carry results here + NSMutableString *escapedString = [NSMutableString new]; + + for (NSValue *exclusionRange in ascendingExclusionRanges) { + + if (exclusionRange.rangeValue.location - substringLoc > 0) { // if a substring before exclusionRange, escape and add it first + NSUInteger substringLength = exclusionRange.rangeValue.location - substringLoc; + NSString *substring = [self substringWithRange:NSMakeRange(substringLoc, substringLength)]; + [escapedString appendString:[substring stringByEscapingSpecialXMLCharacters]]; + } + + // add exclusionString without escaping + NSString *exclusionString = [self substringWithRange:NSMakeRange(exclusionRange.rangeValue.location, exclusionRange.rangeValue.length)]; + [escapedString appendString:exclusionString]; + + // adjust substringLoc to just after exclusionRange + substringLoc = exclusionRange.rangeValue.location + exclusionRange.rangeValue.length; + } + + // any remaining substring after last exclusionRange? + if (self.length - substringLoc > 0) { + NSUInteger substringLength = self.length - substringLoc; + NSString *substring = [self substringWithRange:NSMakeRange(substringLoc, substringLength)]; + [escapedString appendString:[substring stringByEscapingSpecialXMLCharacters]]; + } + + return [escapedString copy]; +} + +/** + * Given a start token and end token pair, search receiver, returning array of NSValue objects containing NSRange of each pair found + * Return Array will be empty if no pairs are found. + * Error will be nil if no parsing errors occur. + */ +- (NSArray *)tokenPairRangesForStartToken:(NSString *)startToken endToken:(NSString *)endToken error:(NSError **)error +{ + // Standard XML characters to escape + // See: https://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents + // + // + // + // + + NSMutableArray *tokenPairValueRanges = [NSMutableArray new]; + BOOL endOfStringReached = NO; + NSUInteger startTokenSearchRangeLocation = 0; + NSUInteger startTokenSearchRangeLength = self.length; + + // search for start and end token pairs until end of string is reached + do { + if (startToken.length <= startTokenSearchRangeLength) { + NSRange startTokenSearchRange = NSMakeRange(startTokenSearchRangeLocation, self.length - startTokenSearchRangeLocation); + NSRange startTokenRange = [self rangeOfString:startToken options:NSLiteralSearch range:startTokenSearchRange]; + + if (startTokenRange.length != 0) { // start token found, now look for end token + + + NSLog(@"startToken %@ found at Range: loc: %lu, len: %lu", startToken, startTokenRange.location, startTokenRange.length); + + NSUInteger endTokenSearchRangeLocation = startTokenRange.location + startTokenRange.length; + NSUInteger endTokenSearchRangeLength = self.length - endTokenSearchRangeLocation; + + // check if enough length is left to find end token + if (endToken.length <= endTokenSearchRangeLength) { + // search for end token + NSRange endTokenSearchRange = NSMakeRange(endTokenSearchRangeLocation, endTokenSearchRangeLength); + NSRange endTokenRange = [self rangeOfString:endToken options:NSLiteralSearch range:endTokenSearchRange]; + + if (endTokenRange.length != 0) { // end token found + + NSLog(@"endToken %@ found at Range: loc: %lu, len: %lu", endToken, endTokenRange.location, endTokenRange.length); + + // token pair range begins at startRange.location and ends at endRange.location + endRange.length + NSRange tokenPairRange = NSMakeRange(startTokenRange.location, endTokenRange.location - startTokenRange.location + endTokenRange.length); + [tokenPairValueRanges addObject:[NSValue valueWithRange:tokenPairRange]]; + + // adjust startTokenSearchRangeLocation and startTokenSearchRangeLength + startTokenSearchRangeLocation = endTokenRange.location + endTokenRange.length; + startTokenSearchRangeLength = self.length - startTokenSearchRangeLocation; + + } else { // invalid string - missing expected endToken + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:1 userInfo:nil]; + } + } else { // invalid string - not enough length remaining to find end token + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:2 userInfo:nil]; + } + } else { + // no start token found - not an error condition + endOfStringReached = YES; + } + } else { + endOfStringReached = YES; + } + + } while (*error == nil && !endOfStringReached); + + return [tokenPairValueRanges copy]; +} + +@end diff --git a/Example_Apps/BugSplatTest-SwiftUI-SPM/BugSplatTest-SwiftUI-SPM/BugSplatTest_SwiftUI_SPMApp.swift b/Example_Apps/BugSplatTest-SwiftUI-SPM/BugSplatTest-SwiftUI-SPM/BugSplatTest_SwiftUI_SPMApp.swift index 897e53e..4496a83 100644 --- a/Example_Apps/BugSplatTest-SwiftUI-SPM/BugSplatTest-SwiftUI-SPM/BugSplatTest_SwiftUI_SPMApp.swift +++ b/Example_Apps/BugSplatTest-SwiftUI-SPM/BugSplatTest-SwiftUI-SPM/BugSplatTest_SwiftUI_SPMApp.swift @@ -2,7 +2,7 @@ // BugSplatTest_SwiftUI_SPMApp.swift // BugSplatTest-SwiftUI-SPM // -// Created by David Ferrero on 1/23/25. +// Copyright © BugSplat, LLC. All rights reserved. // import SwiftUI diff --git a/Example_Apps/BugSplatTest-SwiftUI-SPM/BugSplatTest-SwiftUI-SPM/ContentView.swift b/Example_Apps/BugSplatTest-SwiftUI-SPM/BugSplatTest-SwiftUI-SPM/ContentView.swift index fbeeb31..38f0bf5 100644 --- a/Example_Apps/BugSplatTest-SwiftUI-SPM/BugSplatTest-SwiftUI-SPM/ContentView.swift +++ b/Example_Apps/BugSplatTest-SwiftUI-SPM/BugSplatTest-SwiftUI-SPM/ContentView.swift @@ -2,7 +2,7 @@ // ContentView.swift // BugSplatTest-SwiftUI-SPM // -// Created by David Ferrero on 1/23/25. +// Copyright © BugSplat, LLC. All rights reserved. // import SwiftUI diff --git a/Example_Apps/BugSplatTest-SwiftUI/BugSplatTest-SwiftUI/BugSplatTest-SwiftUIApp.swift b/Example_Apps/BugSplatTest-SwiftUI/BugSplatTest-SwiftUI/BugSplatTest-SwiftUIApp.swift index b9b271a..b4f5dd8 100644 --- a/Example_Apps/BugSplatTest-SwiftUI/BugSplatTest-SwiftUI/BugSplatTest-SwiftUIApp.swift +++ b/Example_Apps/BugSplatTest-SwiftUI/BugSplatTest-SwiftUI/BugSplatTest-SwiftUIApp.swift @@ -23,9 +23,18 @@ struct BugSplatTestSwiftUIApp: App { override init() { super.init() - BugSplat.shared().delegate = self -// BugSplat.shared().autoSubmitCrashReport = false - BugSplat.shared().start() + let bugSplat = BugSplat.shared() + bugSplat.delegate = self +// bugSplat.autoSubmitCrashReport = false + + // Add some attribute and value pairs to be included in a crash report + bugSplat.setValue("Value of Plain Attribute", forAttribute: "PlainAttribute") + bugSplat.setValue("Value of not so plain Attribute", forAttribute: "NotSoPlainAttribute") + bugSplat.setValue("Launch Date Value", forAttribute: "CDATAExample") + bugSplat.setValue(" \(Date.now)", forAttribute: "CommentExample") + bugSplat.setValue("This value will get XML escaping because of 'this' and & and < and >", forAttribute: "EscapingExample") + + bugSplat.start() } // MARK: BugSplatDelegate diff --git a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/AppDelegate.h b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/AppDelegate.h index 4e7b0f9..e1beb8b 100644 --- a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/AppDelegate.h +++ b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/AppDelegate.h @@ -2,7 +2,7 @@ // AppDelegate.h // BugSplatTest-UIKit-ObjC // -// Created by David Ferrero on 4/26/24. +// Copyright © BugSplat, LLC. All rights reserved. // #import diff --git a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/AppDelegate.m b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/AppDelegate.m index c24e491..ad8cd38 100644 --- a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/AppDelegate.m +++ b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/AppDelegate.m @@ -2,7 +2,7 @@ // AppDelegate.m // BugSplatTest-UIKit-ObjC // -// Created by David Ferrero on 4/26/24. +// Copyright © BugSplat, LLC. All rights reserved. // #import "AppDelegate.h" diff --git a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/SceneDelegate.h b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/SceneDelegate.h index f1e71e7..babba80 100644 --- a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/SceneDelegate.h +++ b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/SceneDelegate.h @@ -2,7 +2,7 @@ // SceneDelegate.h // BugSplatTest-UIKit-ObjC // -// Created by David Ferrero on 4/26/24. +// Copyright © BugSplat, LLC. All rights reserved. // #import diff --git a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/SceneDelegate.m b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/SceneDelegate.m index 3782b52..ddc47e3 100644 --- a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/SceneDelegate.m +++ b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/SceneDelegate.m @@ -2,7 +2,7 @@ // SceneDelegate.m // BugSplatTest-UIKit-ObjC // -// Created by David Ferrero on 4/26/24. +// Copyright © BugSplat, LLC. All rights reserved. // #import "SceneDelegate.h" diff --git a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/ViewController.h b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/ViewController.h index fc53428..1b254b4 100644 --- a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/ViewController.h +++ b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/ViewController.h @@ -2,7 +2,7 @@ // ViewController.h // BugSplatTest-UIKit-ObjC // -// Created by David Ferrero on 4/26/24. +// Copyright © BugSplat, LLC. All rights reserved. // #import diff --git a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/ViewController.m b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/ViewController.m index 3416a14..e3ae309 100644 --- a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/ViewController.m +++ b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/ViewController.m @@ -2,7 +2,7 @@ // ViewController.m // BugSplatTest-UIKit-ObjC // -// Created by David Ferrero on 4/26/24. +// Copyright © BugSplat, LLC. All rights reserved. // #import "ViewController.h" diff --git a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/main.m b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/main.m index 522c0dc..8763a3c 100644 --- a/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/main.m +++ b/Example_Apps/BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC/main.m @@ -2,7 +2,7 @@ // main.m // BugSplatTest-UIKit-ObjC // -// Created by David Ferrero on 4/26/24. +// Copyright © BugSplat, LLC. All rights reserved. // #import diff --git a/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/AppDelegate.h b/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/AppDelegate.h index db2d508..e92dd76 100644 --- a/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/AppDelegate.h +++ b/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/AppDelegate.h @@ -2,7 +2,7 @@ // AppDelegate.h // BugSplatTest-macOS-UIKit-ObjC // -// Created by David Ferrero on 4/26/24. +// Copyright © BugSplat, LLC. All rights reserved. // #import diff --git a/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/AppDelegate.m b/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/AppDelegate.m index b72e160..823b34c 100644 --- a/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/AppDelegate.m +++ b/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/AppDelegate.m @@ -2,7 +2,7 @@ // AppDelegate.m // BugSplatTest-macOS-UIKit-ObjC // -// Created by David Ferrero on 4/26/24. +// Copyright © BugSplat, LLC. All rights reserved. // #import "AppDelegate.h" diff --git a/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/ViewController.h b/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/ViewController.h index b737902..00d7ffb 100644 --- a/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/ViewController.h +++ b/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/ViewController.h @@ -2,7 +2,7 @@ // ViewController.h // BugSplatTest-macOS-UIKit-ObjC // -// Created by David Ferrero on 4/26/24. +// Copyright © BugSplat, LLC. All rights reserved. // #import diff --git a/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/ViewController.m b/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/ViewController.m index bcc5bb5..5a9cb06 100644 --- a/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/ViewController.m +++ b/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/ViewController.m @@ -2,7 +2,7 @@ // ViewController.m // BugSplatTest-macOS-UIKit-ObjC // -// Created by David Ferrero on 4/26/24. +// Copyright © BugSplat, LLC. All rights reserved. // #import "ViewController.h" diff --git a/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/main.m b/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/main.m index da3018a..4eba970 100644 --- a/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/main.m +++ b/Example_Apps/BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC/main.m @@ -2,7 +2,7 @@ // main.m // BugSplatTest-macOS-UIKit-ObjC // -// Created by David Ferrero on 4/26/24. +// Copyright © BugSplat, LLC. All rights reserved. // #import