aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Database/Snapshot/FSnapshotUtilities.m
diff options
context:
space:
mode:
Diffstat (limited to 'Firebase/Database/Snapshot/FSnapshotUtilities.m')
-rw-r--r--Firebase/Database/Snapshot/FSnapshotUtilities.m301
1 files changed, 301 insertions, 0 deletions
diff --git a/Firebase/Database/Snapshot/FSnapshotUtilities.m b/Firebase/Database/Snapshot/FSnapshotUtilities.m
new file mode 100644
index 0000000..1b83430
--- /dev/null
+++ b/Firebase/Database/Snapshot/FSnapshotUtilities.m
@@ -0,0 +1,301 @@
+/*
+ * 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 "FSnapshotUtilities.h"
+#import "FEmptyNode.h"
+#import "FLeafNode.h"
+#import "FConstants.h"
+#import "FUtilities.h"
+#import "FChildrenNode.h"
+#import "FLLRBValueNode.h"
+#import "FValidation.h"
+#import "FMaxNode.h"
+#import "FNamedNode.h"
+#import "FCompoundWrite.h"
+
+@implementation FSnapshotUtilities
+
++ (id<FNode>) nodeFrom:(id)val {
+ return [FSnapshotUtilities nodeFrom:val priority:nil];
+}
+
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority {
+ return [FSnapshotUtilities nodeFrom:val priority:priority withValidationFrom:@"nodeFrom:priority:"];
+}
+
++ (id<FNode>) nodeFrom:(id)val withValidationFrom:(NSString *)fn {
+ return [FSnapshotUtilities nodeFrom:val priority:nil withValidationFrom:fn];
+}
+
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority withValidationFrom:(NSString *)fn {
+ return [FSnapshotUtilities nodeFrom:val priority:priority withValidationFrom:fn atDepth:0 path:[[NSMutableArray alloc] init]];
+}
+
++ (id<FNode>) nodeFrom:(id)val priority:(id)aPriority withValidationFrom:(NSString *)fn atDepth:(int)depth path:(NSMutableArray *)path {
+ @autoreleasepool {
+ return [FSnapshotUtilities internalNodeFrom:val priority:aPriority withValidationFrom:fn atDepth:depth path:path];
+ }
+}
+
++ (id<FNode>) internalNodeFrom:(id)val priority:(id)aPriority withValidationFrom:(NSString *)fn atDepth:(int)depth path:(NSMutableArray *)path {
+
+
+ if (depth > kFirebaseMaxObjectDepth) {
+ NSRange range;
+ range.location = 0;
+ range.length = 100;
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Max object depth exceeded: %@...", fn, pathString] userInfo:nil];
+ }
+
+ if (val == nil || val == [NSNull null]) {
+ // Null is a valid type to store
+ return [FEmptyNode emptyNode];
+ }
+
+ [FValidation validateFrom:fn isValidPriorityValue:aPriority withPath:path];
+ id<FNode> priority = [FSnapshotUtilities nodeFrom:aPriority];
+
+ id value = val;
+ BOOL isLeafNode = NO;
+
+ if([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dict = val;
+ if(dict[kPayloadPriority] != nil) {
+ id rawPriority = [dict objectForKey:kPayloadPriority];
+ [FValidation validateFrom:fn isValidPriorityValue:rawPriority withPath:path];
+ priority = [FSnapshotUtilities nodeFrom:rawPriority];
+ }
+
+ if(dict[kPayloadValue] != nil) {
+ value = [dict objectForKey:kPayloadValue];
+ if ([FValidation validateFrom:fn isValidLeafValue:value withPath:path]) {
+ isLeafNode = YES;
+ } else {
+ @throw [[NSException alloc]
+ initWithName:@"InvalidLeafValueType"
+ reason:[NSString stringWithFormat:@"(%@) Invalid data type used with .value. Can only use "
+ "NSString and NSNumber or be null. Found %@ instead.",
+ fn, [[value class] description]] userInfo:nil];
+ }
+ }
+ }
+
+ if([FValidation validateFrom:fn isValidLeafValue:value withPath:path]) {
+ isLeafNode = YES;
+ }
+
+ if (isLeafNode) {
+ return [[FLeafNode alloc] initWithValue:value withPriority:priority];
+ }
+
+ // Unlike with JS, we have to handle the dictionary and array cases separately.
+ if ([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dval = (NSDictionary *)value;
+ NSMutableDictionary *children = [NSMutableDictionary dictionaryWithCapacity:dval.count];
+
+ // Avoid creating a million newPaths by appending to old one
+ for (id keyId in dval) {
+ [FValidation validateFrom:fn validDictionaryKey:keyId withPath:path];
+ NSString* key = (NSString*)keyId;
+
+ if (![key hasPrefix:kPayloadMetadataPrefix]) {
+ [path addObject:key];
+ id<FNode> childNode = [FSnapshotUtilities nodeFrom:dval[key] priority:nil withValidationFrom:fn atDepth:depth + 1 path:path];
+ [path removeLastObject];
+
+ if (![childNode isEmpty]) {
+ children[key] = childNode;
+ }
+ }
+ }
+
+ if ([children count] == 0) {
+ return [FEmptyNode emptyNode];
+ } else {
+ FImmutableSortedDictionary *childrenDict = [FImmutableSortedDictionary fromDictionary:children
+ withComparator:[FUtilities keyComparator]];
+ return [[FChildrenNode alloc] initWithPriority:priority children:childrenDict];
+ }
+ } else if([value isKindOfClass:[NSArray class]]) {
+ NSArray* aval = (NSArray *)value;
+ NSMutableDictionary* children = [NSMutableDictionary dictionaryWithCapacity:aval.count];
+
+ for(int i = 0; i < [aval count]; i++) {
+ NSString* key = [NSString stringWithFormat:@"%i", i];
+ [path addObject:key];
+ id<FNode> childNode = [FSnapshotUtilities nodeFrom:[aval objectAtIndex:i] priority:nil withValidationFrom:fn atDepth:depth + 1 path:path];
+ [path removeLastObject];
+
+ if (![childNode isEmpty]) {
+ children[key] = childNode;
+ }
+ }
+
+ if ([children count] == 0) {
+ return [FEmptyNode emptyNode];
+ } else {
+ FImmutableSortedDictionary *childrenDict = [FImmutableSortedDictionary fromDictionary:children
+ withComparator:[FUtilities keyComparator]];
+ return [[FChildrenNode alloc] initWithPriority:priority children:childrenDict];
+ }
+ } else {
+ 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 object of type %@ at %@. "
+ "Can only store objects of type NSNumber, NSString, NSDictionary, and NSArray.",
+ fn, [[value class] description], pathString] userInfo:nil];
+ }
+}
+
++ (FCompoundWrite *) compoundWriteFromDictionary:(NSDictionary *)values withValidationFrom:(NSString *)fn {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+
+ NSMutableArray *updatePaths = [NSMutableArray arrayWithCapacity:values.count];
+ for (NSString *keyId in values) {
+ id value = values[keyId];
+ [FValidation validateFrom:fn validUpdateDictionaryKey:keyId withValue:value];
+
+ FPath* path = [FPath pathWithString:keyId];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:value withValidationFrom:fn];
+
+ [updatePaths addObject:path];
+ compoundWrite = [compoundWrite addWrite:node atPath:path];
+ }
+
+ // Check that the update paths are not descendants of each other.
+ [updatePaths sortUsingComparator:^NSComparisonResult(FPath* left, FPath* right) {
+ return [left compare:right];
+ }];
+ FPath *prevPath = nil;
+ for (FPath *path in updatePaths) {
+ if (prevPath != nil && [prevPath contains:path]) {
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid path in object. Path (%@) is an ancestor of (%@).", fn, prevPath, path] userInfo:nil];
+ }
+ prevPath = path;
+ }
+
+ return compoundWrite;
+}
+
++ (void)validatePriorityNode:(id <FNode>)priorityNode {
+ assert(priorityNode != nil);
+ if (priorityNode.isLeafNode) {
+ id val = priorityNode.val;
+ if ([val isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* valDict __unused = (NSDictionary*)val;
+ NSAssert(valDict[kServerValueSubKey] != nil, @"Priority can't be object unless it's a deferred value");
+ } else {
+ NSString *jsType __unused = [FUtilities getJavascriptType:val];
+ NSAssert(jsType == kJavaScriptString || jsType == kJavaScriptNumber, @"Priority of unexpected type.");
+ }
+ } else {
+ NSAssert(priorityNode == [FMaxNode maxNode] || priorityNode.isEmpty, @"Priority of unexpected type.");
+ }
+ // Don't call getPriority() on MAX_NODE to avoid hitting assertion.
+ NSAssert(priorityNode == [FMaxNode maxNode] || priorityNode.getPriority.isEmpty,
+ @"Priority nodes can't have a priority of their own.");
+}
+
++ (void)appendHashRepresentationForLeafNode:(FLeafNode *)leafNode
+ toString:(NSMutableString *)string
+ hashVersion:(FDataHashVersion)hashVersion {
+ NSAssert(hashVersion == FDataHashVersionV1 || hashVersion == FDataHashVersionV2,
+ @"Unknown hash version: %lu", (unsigned long)hashVersion);
+ if (!leafNode.getPriority.isEmpty) {
+ [string appendString:@"priority:"];
+ [FSnapshotUtilities appendHashRepresentationForLeafNode:leafNode.getPriority toString:string hashVersion:hashVersion];
+ [string appendString:@":"];
+ }
+
+ NSString *jsType = [FUtilities getJavascriptType:leafNode.val];
+ [string appendString:jsType];
+ [string appendString:@":"];
+
+
+ if (jsType == kJavaScriptBoolean) {
+ NSString *boolString = [leafNode.val boolValue] ? kJavaScriptTrue : kJavaScriptFalse;
+ [string appendString:boolString];
+ } else if (jsType == kJavaScriptNumber) {
+ NSString *numberString = [FUtilities ieee754StringForNumber:leafNode.val];
+ [string appendString:numberString];
+ } else if (jsType == kJavaScriptString) {
+ if (hashVersion == FDataHashVersionV1) {
+ [string appendString:leafNode.val];
+ } else {
+ NSAssert(hashVersion == FDataHashVersionV2, @"Invalid hash version found");
+ [FSnapshotUtilities appendHashV2RepresentationForString:leafNode.val toString:string];
+ }
+ } else {
+ [NSException raise:NSInvalidArgumentException format:@"Unknown value for hashing: %@", leafNode];
+ }
+}
+
++ (void)appendHashV2RepresentationForString:(NSString *)string
+ toString:(NSMutableString *)mutableString {
+ string = [string stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
+ string = [string stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
+ [mutableString appendString:@"\""];
+ [mutableString appendString:string];
+ [mutableString appendString:@"\""];
+}
+
++ (NSUInteger)estimateLeafNodeSize:(FLeafNode *)leafNode {
+ NSString *jsType = [FUtilities getJavascriptType:leafNode.val];
+ // These values are somewhat arbitrary, but we don't need an exact value so prefer performance over exact value
+ NSUInteger valueSize;
+ if (jsType == kJavaScriptNumber) {
+ valueSize = 8; // estimate each float with 8 bytes
+ } else if (jsType == kJavaScriptBoolean) {
+ valueSize = 4; // true or false need roughly 4 bytes
+ } else if (jsType == kJavaScriptString) {
+ valueSize = 2 + [leafNode.val length]; // add 2 for quotes
+ } else {
+ [NSException raise:NSInvalidArgumentException format:@"Unknown leaf type: %@", leafNode];
+ return 0;
+ }
+
+ if (leafNode.getPriority.isEmpty) {
+ return valueSize;
+ } else {
+ // Account for extra overhead due to the extra JSON object and the ".value" and ".priority" keys, colons, comma
+ NSUInteger leafPriorityOverhead = 2 + 8 + 11 + 2 + 1;
+ return leafPriorityOverhead + valueSize + [FSnapshotUtilities estimateLeafNodeSize:leafNode.getPriority];
+ }
+}
+
++ (NSUInteger)estimateSerializedNodeSize:(id<FNode>)node {
+ if ([node isEmpty]) {
+ return 4; // null keyword
+ } else if ([node isLeafNode]) {
+ return [FSnapshotUtilities estimateLeafNodeSize:node];
+ } else {
+ NSAssert([node isKindOfClass:[FChildrenNode class]], @"Unexpected node type: %@", [node class]);
+ __block NSUInteger sum = 1; // opening brackets
+ [((FChildrenNode *)node) enumerateChildrenAndPriorityUsingBlock:^(NSString *key, id<FNode>child, BOOL *stop) {
+ sum += key.length;
+ sum += 4; // quotes around key and colon and (comma or closing bracket)
+ sum += [FSnapshotUtilities estimateSerializedNodeSize:child];
+ }];
+ return sum;
+ }
+}
+
+@end