aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Database/Utilities/FValidation.m
diff options
context:
space:
mode:
Diffstat (limited to 'Firebase/Database/Utilities/FValidation.m')
-rw-r--r--Firebase/Database/Utilities/FValidation.m312
1 files changed, 312 insertions, 0 deletions
diff --git a/Firebase/Database/Utilities/FValidation.m b/Firebase/Database/Utilities/FValidation.m
new file mode 100644
index 0000000..c4c6b2b
--- /dev/null
+++ b/Firebase/Database/Utilities/FValidation.m
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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 "FValidation.h"
+#import "FConstants.h"
+#import "FParsedUrl.h"
+#import "FTypedefs.h"
+
+
+// Have to escape: * ? + [ ( ) { } ^ $ | \ . /
+// See: https://developer.apple.com/library/mac/#documentation/Foundation/Reference/NSRegularExpression_Class/Reference/Reference.html
+
+NSString *const kInvalidPathCharacters = @"[].#$";
+NSString *const kInvalidKeyCharacters = @"[].#$/";
+
+@implementation FValidation
+
++ (void) validateFrom:(NSString *)fn writablePath:(FPath *)path {
+ if([[path getFront] isEqualToString:kDotInfoPrefix]) {
+ @throw [[NSException alloc] initWithName:@"WritablePathValidation" reason:[NSString stringWithFormat:@"(%@) failed to path %@: Can't modify data under %@", fn, [path description], kDotInfoPrefix] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString*)fn knownEventType:(FIRDataEventType)event {
+ switch (event) {
+ case FIRDataEventTypeValue:
+ case FIRDataEventTypeChildAdded:
+ case FIRDataEventTypeChildChanged:
+ case FIRDataEventTypeChildMoved:
+ case FIRDataEventTypeChildRemoved:
+ return;
+ break;
+ default:
+ @throw [[NSException alloc] initWithName:@"KnownEventTypeValidation" reason:[NSString stringWithFormat:@"(%@) Unknown event type: %d", fn, (int) event] userInfo:nil];
+ break;
+ }
+}
+
++ (BOOL) isValidPathString:(NSString *)pathString {
+ static dispatch_once_t token;
+ static NSCharacterSet *badPathChars = nil;
+ dispatch_once(&token, ^{
+ badPathChars = [NSCharacterSet characterSetWithCharactersInString:kInvalidPathCharacters];
+ });
+ return pathString != nil && [pathString length] != 0 &&
+ [pathString rangeOfCharacterFromSet:badPathChars].location == NSNotFound;
+}
+
++ (void) validateFrom:(NSString *)fn validPathString:(NSString *)pathString {
+ if(! [self isValidPathString:pathString]) {
+ @throw [[NSException alloc] initWithName:@"InvalidPathValidation" reason:[NSString stringWithFormat:@"(%@) Must be a non-empty string and not contain '.' '#' '$' '[' or ']'", fn] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString *)fn validRootPathString:(NSString *)pathString {
+ static dispatch_once_t token;
+ static NSRegularExpression *dotInfoRegex = nil;
+ dispatch_once(&token, ^{
+ dotInfoRegex = [NSRegularExpression regularExpressionWithPattern:@"^\\/*\\.info(\\/|$)" options:0 error:nil];
+ });
+
+ NSString *tempPath = pathString;
+ // HACK: Obj-C regex are kinda' slow. Do a plain string search first before bothering with the regex.
+ if ([pathString rangeOfString:@".info"].location != NSNotFound) {
+ tempPath = [dotInfoRegex stringByReplacingMatchesInString:pathString options:0 range:NSMakeRange(0, pathString.length) withTemplate:@"/"];
+ }
+ [self validateFrom:fn validPathString:tempPath];
+}
+
++ (BOOL) isValidKey:(NSString *)key {
+ static dispatch_once_t token;
+ static NSCharacterSet *badKeyChars = nil;
+ dispatch_once(&token, ^{
+ badKeyChars = [NSCharacterSet characterSetWithCharactersInString:kInvalidKeyCharacters];
+ });
+ return key != nil && key.length > 0 && [key rangeOfCharacterFromSet:badKeyChars].location == NSNotFound;
+}
+
++ (void) validateFrom:(NSString *)fn validKey:(NSString *)key {
+ if (![self isValidKey:key]) {
+ @throw [[NSException alloc] initWithName:@"InvalidKeyValidation" reason:[NSString stringWithFormat:@"(%@) Must be a non-empty string and not contain '/' '.' '#' '$' '[' or ']'", fn] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString *)fn validURL:(FParsedUrl *)parsedUrl {
+ NSString* pathString = [parsedUrl.path description];
+ [self validateFrom:fn validRootPathString:pathString];
+}
+
+#pragma mark -
+#pragma mark Authentication validation
+
++ (BOOL) stringNonempty:(NSString *)str {
+ return str != nil && ![str isKindOfClass:[NSNull class]] && str.length > 0;
+}
+
++ (void) validateToken:(NSString *)token {
+ if (![FValidation stringNonempty:token]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't have empty string or nil for custom token"];
+ }
+}
+
+#pragma mark -
+#pragma mark Handling authentication errors
+
+/**
+* This function immediately calls the callback.
+* It assumes that it is not on FirebaseWorker thread.
+* It assumes it's on a user-controlled thread.
+*/
++ (void) handleError:(NSError *)error withUserCallback:(fbt_void_nserror_id)userCallback {
+ if (userCallback) {
+ userCallback(error, nil);
+ }
+}
+
+/**
+* This function immediately calls the callback.
+* It assumes that it is not on FirebaseWorker thread.
+* It assumes it's on a user-controlled thread.
+*/
++ (void) handleError:(NSError *)error withSuccessCallback:(fbt_void_nserror)userCallback {
+ if (userCallback) {
+ userCallback(error);
+ }
+}
+
+#pragma mark -
+#pragma mark Snapshot validation
+
++ (BOOL) validateFrom:(NSString*)fn isValidLeafValue:(id)value withPath:(NSArray*)path {
+ if ([value isKindOfClass:[NSString class]]) {
+ // Try to avoid conversion to bytes if possible
+ NSString* theString = value;
+ if ([theString maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kFirebaseMaxLeafSize &&
+ [theString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kFirebaseMaxLeafSize) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) String exceeds max size of %u utf8 bytes: %@", fn, (int)kFirebaseMaxLeafSize, pathString] userInfo:nil];
+ }
+ return YES;
+ }
+
+ else if ([value isKindOfClass:[NSNumber class]]) {
+ // Cannot store NaN, but otherwise can store NSNumbers.
+ if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store NaN at path: %@.", fn, pathString] userInfo:nil];
+ }
+ return YES;
+ }
+
+ else if ([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dval = value;
+ if (dval[kServerValueSubKey] != nil) {
+ if ([dval count] > 1) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store other keys with server value keys.%@.", fn, pathString] userInfo:nil];
+ }
+ return YES;
+ }
+ return NO;
+ }
+
+ else if (value == [NSNull null] || value == nil) {
+ // Null is valid type to store at leaf
+ return YES;
+ }
+
+ return NO;
+}
+
++ (NSString*) parseAndValidateKey:(id)keyId fromFunction:(NSString*)fn path:(NSArray*)path {
+ if (![keyId isKindOfClass:[NSString class]]) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Non-string keys are not allowed in object at path: %@", fn, pathString] userInfo:nil];
+ }
+ return (NSString*)keyId;
+}
+
++ (void) validateFrom:(NSString*)fn validDictionaryKey:(id)keyId withPath:(NSArray*)path {
+ NSString *key = [self parseAndValidateKey:keyId fromFunction:fn path:path];
+ if (![key isEqualToString:kPayloadPriority] && ![key isEqualToString:kPayloadValue] && ![key isEqualToString:kServerValueSubKey] && ![FValidation isValidKey:key]) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid key in object at path: %@. Keys must be non-empty and cannot contain '/' '.' '#' '$' '[' or ']'", fn, pathString] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString*)fn validUpdateDictionaryKey:(id)keyId withValue:(id)value {
+ FPath *path = [FPath pathWithString:[self parseAndValidateKey:keyId fromFunction:fn path:@[]]];
+ __block NSInteger keyNum = 0;
+ [path enumerateComponentsUsingBlock:^void (NSString *key, BOOL *stop) {
+ if ([key isEqualToString:kPayloadPriority] && keyNum == [path length] - 1) {
+ [self validateFrom:fn isValidPriorityValue:value withPath:@[]];
+ } else {
+ keyNum++;
+
+ if (![FValidation isValidKey:key]) {
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid key in object. Keys must be non-empty and cannot contain '.' '#' '$' '[' or ']'", fn] userInfo:nil];
+ }
+ }
+ }];
+}
+
++ (void) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path {
+ [self validateFrom:fn isValidPriorityValue:value withPath:path throwError:YES];
+}
+
+/**
+* Returns YES if priority is valid.
+*/
++ (BOOL)validatePriorityValue:value {
+ return [self validateFrom:nil isValidPriorityValue:value withPath:nil throwError:NO];
+}
+
+/**
+* Helper for validating priorities. If passed YES for throwError, it'll throw descriptive errors on validation
+* problems. Else, it'll just return YES/NO.
+*/
++ (BOOL) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path throwError:(BOOL)throwError {
+ if ([value isKindOfClass:[NSNumber class]]) {
+ if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store NaN as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ } else if (value == (id) kCFBooleanFalse || value == (id) kCFBooleanTrue) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store true/false as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+ }
+ else if ([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *dval = value;
+ if (dval[kServerValueSubKey] != nil) {
+ if ([dval count] > 1) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store other keys with server value keys as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+ } else {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store an NSDictionary as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+ }
+ else if ([value isKindOfClass:[NSArray class]]) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store an NSArray as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+
+ // It's valid!
+ return YES;
+}
+@end