From fb9dcaa608e0e52e403eb28b8d4d6d279616fcc8 Mon Sep 17 00:00:00 2001 From: iska Date: Mon, 15 Sep 2014 23:24:16 +0200 Subject: [PATCH] Add several checks and error reporting while processing the input stream Control and undefined character, isolated surrogates and carriage returns are handled according to the standard --- HTMLKit.xcodeproj/project.pbxproj | 10 +++ HTMLKit/HTMLInputStreamReader.h | 3 +- HTMLKit/HTMLInputStreamReader.m | 93 +++++++++++++++++++++++++-- HTMLKit/HTMLInputStreamReaderErrors.h | 28 ++++++++ HTMLKit/HTMLInputStreamReaderErrors.m | 44 +++++++++++++ 5 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 HTMLKit/HTMLInputStreamReaderErrors.h create mode 100644 HTMLKit/HTMLInputStreamReaderErrors.m diff --git a/HTMLKit.xcodeproj/project.pbxproj b/HTMLKit.xcodeproj/project.pbxproj index 63c1e95..c3ede11 100644 --- a/HTMLKit.xcodeproj/project.pbxproj +++ b/HTMLKit.xcodeproj/project.pbxproj @@ -26,6 +26,9 @@ 625A150819C78ABA00AD0C32 /* HTMLInputStreamReader.h in Headers */ = {isa = PBXBuildFile; fileRef = 625A150619C78ABA00AD0C32 /* HTMLInputStreamReader.h */; }; 625A150919C78ABA00AD0C32 /* HTMLInputStreamReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 625A150719C78ABA00AD0C32 /* HTMLInputStreamReader.m */; }; 625A150A19C78ABA00AD0C32 /* HTMLInputStreamReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 625A150719C78ABA00AD0C32 /* HTMLInputStreamReader.m */; }; + 625A150D19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.h in Headers */ = {isa = PBXBuildFile; fileRef = 625A150B19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.h */; }; + 625A150E19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.m in Sources */ = {isa = PBXBuildFile; fileRef = 625A150C19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.m */; }; + 625A150F19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.m in Sources */ = {isa = PBXBuildFile; fileRef = 625A150C19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -79,6 +82,8 @@ 625A14EC19C7834100AD0C32 /* HTMLKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HTMLKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 625A150619C78ABA00AD0C32 /* HTMLInputStreamReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTMLInputStreamReader.h; sourceTree = ""; }; 625A150719C78ABA00AD0C32 /* HTMLInputStreamReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTMLInputStreamReader.m; sourceTree = ""; }; + 625A150B19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTMLInputStreamReaderErrors.h; sourceTree = ""; }; + 625A150C19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTMLInputStreamReaderErrors.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -170,6 +175,8 @@ 625A14BD19C7829400AD0C32 /* HTMLKit.m */, 625A150619C78ABA00AD0C32 /* HTMLInputStreamReader.h */, 625A150719C78ABA00AD0C32 /* HTMLInputStreamReader.m */, + 625A150B19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.h */, + 625A150C19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.m */, 625A14B619C7829400AD0C32 /* Supporting Files */, ); name = "Source Code"; @@ -212,6 +219,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 625A150D19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.h in Headers */, 625A150419C783EB00AD0C32 /* HTMLKit.h in Headers */, 625A150819C78ABA00AD0C32 /* HTMLInputStreamReader.h in Headers */, ); @@ -359,6 +367,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 625A150F19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.m in Sources */, 625A150A19C78ABA00AD0C32 /* HTMLInputStreamReader.m in Sources */, 625A14BE19C7829400AD0C32 /* HTMLKit.m in Sources */, ); @@ -376,6 +385,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 625A150E19C78BE900AD0C32 /* HTMLInputStreamReaderErrors.m in Sources */, 625A150919C78ABA00AD0C32 /* HTMLInputStreamReader.m in Sources */, 625A150219C783DE00AD0C32 /* HTMLKit.m in Sources */, ); diff --git a/HTMLKit/HTMLInputStreamReader.h b/HTMLKit/HTMLInputStreamReader.h index 8b8213d..f59d394 100644 --- a/HTMLKit/HTMLInputStreamReader.h +++ b/HTMLKit/HTMLInputStreamReader.h @@ -7,8 +7,7 @@ // #import - -typedef void (^ HTMLStreamReaderErrorCallback)(NSError *error); +#import "HTMLInputStreamReaderErrors.h" /** * HTML Input Stream Reader processor conforming to the HTML standard diff --git a/HTMLKit/HTMLInputStreamReader.m b/HTMLKit/HTMLInputStreamReader.m index 01e7938..e373791 100644 --- a/HTMLKit/HTMLInputStreamReader.m +++ b/HTMLKit/HTMLInputStreamReader.m @@ -8,10 +8,57 @@ #import "HTMLInputStreamReader.h" +#pragma mark Constants & Inlines + static UTF32Char const REPLACEMENT = 0xFFFD; static UTF32Char const LINE_FEED = 0x000A; static UTF32Char const CARRIAGE_RETURN = 0x000D; +NS_INLINE BOOL isControlOrUndefinedCharacter(UTF32Char character) +{ + return ((character >= 0x0001 && character <= 0x0008) || + (character >= 0x000E && character <= 0x001F) || + (character >= 0x007F && character <= 0x009F) || + (character >= 0xFDD0 && character <= 0xFDEF) || + character == 0x000B || + character == 0xFFFE || + character == 0xFFFF || + character == 0x1FFFE || + character == 0x1FFFF || + character == 0x2FFFE || + character == 0x2FFFF || + character == 0x3FFFE || + character == 0x3FFFF || + character == 0x4FFFE || + character == 0x4FFFF || + character == 0x5FFFE || + character == 0x5FFFF || + character == 0x6FFFE || + character == 0x6FFFF || + character == 0x7FFFE || + character == 0x7FFFF || + character == 0x8FFFE || + character == 0x8FFFF || + character == 0x9FFFE || + character == 0x9FFFF || + character == 0xAFFFE || + character == 0xAFFFF || + character == 0xBFFFE || + character == 0xBFFFF || + character == 0xCFFFE || + character == 0xCFFFF || + character == 0xDFFFE || + character == 0xDFFFF || + character == 0xEFFFE || + character == 0xEFFFF || + character == 0xFFFFE || + character == 0xFFFFF || + character == 0x10FFFE || + character == 0x10FFFF); +} + +#pragma mark - HTMLInputStreamReader + @interface HTMLInputStreamReader () { NSString *_string; @@ -19,7 +66,7 @@ static UTF32Char const CARRIAGE_RETURN = 0x000D; CFStringInlineBuffer _buffer; NSUInteger _location; UTF32Char _currentInputCharacter; - + NSUInteger _consume; HTMLStreamReaderErrorCallback _errorCallback; } @end @@ -52,20 +99,58 @@ static UTF32Char const CARRIAGE_RETURN = 0x000D; { if (_location >= _string.length) return EOF; + _consume = 1; UTF32Char nextInputCharacter = CFStringGetCharacterFromInlineBuffer(&_buffer, _location); if (nextInputCharacter == 0) return EOF; - if (nextInputCharacter == CARRIAGE_RETURN) return LINE_FEED; - if (CFStringIsSurrogateLowCharacter(nextInputCharacter)) return REPLACEMENT; + if (nextInputCharacter == CARRIAGE_RETURN) { + UniChar next = CFStringGetCharacterFromInlineBuffer(&_buffer, _location + 1); + if (next == LINE_FEED) _consume++; + return LINE_FEED; + } + if (CFStringIsSurrogateLowCharacter(nextInputCharacter)) { + [HTMLInputStreamReaderErrors reportParseError:HTMLStreamReaderErrorIsolatedLowSurrogate + atLocation:_location + andCallback:_errorCallback]; + return REPLACEMENT; + } if (CFStringIsSurrogateHighCharacter(nextInputCharacter)) { UniChar surrogateLow = CFStringGetCharacterFromInlineBuffer(&_buffer, _location + 1); - if (CFStringIsSurrogateLowCharacter(surrogateLow) == NO) return REPLACEMENT; + if (CFStringIsSurrogateLowCharacter(surrogateLow) == NO) { + [HTMLInputStreamReaderErrors reportParseError:HTMLStreamReaderErrorIsolatedHighSurrogate + atLocation:_location + andCallback:_errorCallback]; + return REPLACEMENT; + } nextInputCharacter = CFStringGetLongCharacterForSurrogatePair(nextInputCharacter, surrogateLow); } + if (isControlOrUndefinedCharacter(nextInputCharacter)) { + [HTMLInputStreamReaderErrors reportParseError:HTMLStreamReaderErrorControlOrUndefined + atLocation:_location + andCallback:_errorCallback]; + } + return nextInputCharacter; } +- (UTF32Char)consumeNextInputCharacter +{ + UTF32Char nextInputCharacter = [self nextInputCharacter]; + _location += _consume; + _scanner.scanLocation = _location; + _currentInputCharacter = nextInputCharacter; + + return nextInputCharacter; +} + +- (void)unconsumeCurrentInputCharacter +{ + _location -= _consume; + _scanner.scanLocation = _location; + _consume = 0; +} + @end diff --git a/HTMLKit/HTMLInputStreamReaderErrors.h b/HTMLKit/HTMLInputStreamReaderErrors.h new file mode 100644 index 0000000..4bbc760 --- /dev/null +++ b/HTMLKit/HTMLInputStreamReaderErrors.h @@ -0,0 +1,28 @@ +// +// HTMLInputStreamReaderErrors.h +// HTMLKit +// +// Created by Iska on 15/09/14. +// Copyright (c) 2014 BrainCookie. All rights reserved. +// + +#import + +extern NSString * const HTMLStreamReaderErrorDomain; + +typedef NS_ENUM(NSUInteger, HTMLStreamReaderError) +{ + HTMLStreamReaderErrorIsolatedLowSurrogate = 100, + HTMLStreamReaderErrorIsolatedHighSurrogate = 200, + HTMLStreamReaderErrorControlOrUndefined = 300 +}; + +typedef void (^ HTMLStreamReaderErrorCallback)(NSError *error); + +@interface HTMLInputStreamReaderErrors : NSObject + ++ (void)reportParseError:(HTMLStreamReaderError)parseError + atLocation:(NSUInteger)location + andCallback:(HTMLStreamReaderErrorCallback)callback; + +@end diff --git a/HTMLKit/HTMLInputStreamReaderErrors.m b/HTMLKit/HTMLInputStreamReaderErrors.m new file mode 100644 index 0000000..e6399c5 --- /dev/null +++ b/HTMLKit/HTMLInputStreamReaderErrors.m @@ -0,0 +1,44 @@ +// +// HTMLInputStreamReaderErrors.m +// HTMLKit +// +// Created by Iska on 15/09/14. +// Copyright (c) 2014 BrainCookie. All rights reserved. +// + +#import "HTMLInputStreamReaderErrors.h" + +NSString * const HTMLStreamReaderErrorDomain = @"HTMLStreamReaderErrorDomain"; + +static inline NSString * ReasonStringForError(HTMLStreamReaderError error) +{ + switch (error) { + case HTMLStreamReaderErrorIsolatedLowSurrogate: + return @"Non-Unicode character found (an isolated low surrogate)"; + case HTMLStreamReaderErrorIsolatedHighSurrogate: + return @"Non-Unicode character found (an isolated high surrogate)"; + case HTMLStreamReaderErrorControlOrUndefined: + return @"A control/undefined character found"; + default: + break; + } +} + +@implementation HTMLInputStreamReaderErrors + ++ (void)reportParseError:(HTMLStreamReaderError)parseError atLocation:(NSUInteger)location andCallback:(HTMLStreamReaderErrorCallback)callback +{ + if (callback == nil) return; + + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey : [NSString stringWithFormat:@"HTML Stream parse error at [%ld]", location], + NSLocalizedFailureReasonErrorKey : ReasonStringForError(parseError) + }; + + NSError *error = [[NSError alloc] initWithDomain:HTMLStreamReaderErrorDomain + code:parseError + userInfo:userInfo]; + callback(error); +} + +@end