FSnapshotUtilities.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "FSnapshotUtilities.h"
  17. #import "FEmptyNode.h"
  18. #import "FLeafNode.h"
  19. #import "FConstants.h"
  20. #import "FUtilities.h"
  21. #import "FChildrenNode.h"
  22. #import "FLLRBValueNode.h"
  23. #import "FValidation.h"
  24. #import "FMaxNode.h"
  25. #import "FNamedNode.h"
  26. #import "FCompoundWrite.h"
  27. @implementation FSnapshotUtilities
  28. + (id<FNode>) nodeFrom:(id)val {
  29. return [FSnapshotUtilities nodeFrom:val priority:nil];
  30. }
  31. + (id<FNode>) nodeFrom:(id)val priority:(id)priority {
  32. return [FSnapshotUtilities nodeFrom:val priority:priority withValidationFrom:@"nodeFrom:priority:"];
  33. }
  34. + (id<FNode>) nodeFrom:(id)val withValidationFrom:(NSString *)fn {
  35. return [FSnapshotUtilities nodeFrom:val priority:nil withValidationFrom:fn];
  36. }
  37. + (id<FNode>) nodeFrom:(id)val priority:(id)priority withValidationFrom:(NSString *)fn {
  38. return [FSnapshotUtilities nodeFrom:val priority:priority withValidationFrom:fn atDepth:0 path:[[NSMutableArray alloc] init]];
  39. }
  40. + (id<FNode>) nodeFrom:(id)val priority:(id)aPriority withValidationFrom:(NSString *)fn atDepth:(int)depth path:(NSMutableArray *)path {
  41. @autoreleasepool {
  42. return [FSnapshotUtilities internalNodeFrom:val priority:aPriority withValidationFrom:fn atDepth:depth path:path];
  43. }
  44. }
  45. + (id<FNode>) internalNodeFrom:(id)val priority:(id)aPriority withValidationFrom:(NSString *)fn atDepth:(int)depth path:(NSMutableArray *)path {
  46. if (depth > kFirebaseMaxObjectDepth) {
  47. NSRange range;
  48. range.location = 0;
  49. range.length = 100;
  50. NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  51. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Max object depth exceeded: %@...", fn, pathString] userInfo:nil];
  52. }
  53. if (val == nil || val == [NSNull null]) {
  54. // Null is a valid type to store
  55. return [FEmptyNode emptyNode];
  56. }
  57. [FValidation validateFrom:fn isValidPriorityValue:aPriority withPath:path];
  58. id<FNode> priority = [FSnapshotUtilities nodeFrom:aPriority];
  59. id value = val;
  60. BOOL isLeafNode = NO;
  61. if([value isKindOfClass:[NSDictionary class]]) {
  62. NSDictionary* dict = val;
  63. if(dict[kPayloadPriority] != nil) {
  64. id rawPriority = [dict objectForKey:kPayloadPriority];
  65. [FValidation validateFrom:fn isValidPriorityValue:rawPriority withPath:path];
  66. priority = [FSnapshotUtilities nodeFrom:rawPriority];
  67. }
  68. if(dict[kPayloadValue] != nil) {
  69. value = [dict objectForKey:kPayloadValue];
  70. if ([FValidation validateFrom:fn isValidLeafValue:value withPath:path]) {
  71. isLeafNode = YES;
  72. } else {
  73. @throw [[NSException alloc]
  74. initWithName:@"InvalidLeafValueType"
  75. reason:[NSString stringWithFormat:@"(%@) Invalid data type used with .value. Can only use "
  76. "NSString and NSNumber or be null. Found %@ instead.",
  77. fn, [[value class] description]] userInfo:nil];
  78. }
  79. }
  80. }
  81. if([FValidation validateFrom:fn isValidLeafValue:value withPath:path]) {
  82. isLeafNode = YES;
  83. }
  84. if (isLeafNode) {
  85. return [[FLeafNode alloc] initWithValue:value withPriority:priority];
  86. }
  87. // Unlike with JS, we have to handle the dictionary and array cases separately.
  88. if ([value isKindOfClass:[NSDictionary class]]) {
  89. NSDictionary* dval = (NSDictionary *)value;
  90. NSMutableDictionary *children = [NSMutableDictionary dictionaryWithCapacity:dval.count];
  91. // Avoid creating a million newPaths by appending to old one
  92. for (id keyId in dval) {
  93. [FValidation validateFrom:fn validDictionaryKey:keyId withPath:path];
  94. NSString* key = (NSString*)keyId;
  95. if (![key hasPrefix:kPayloadMetadataPrefix]) {
  96. [path addObject:key];
  97. id<FNode> childNode = [FSnapshotUtilities nodeFrom:dval[key] priority:nil withValidationFrom:fn atDepth:depth + 1 path:path];
  98. [path removeLastObject];
  99. if (![childNode isEmpty]) {
  100. children[key] = childNode;
  101. }
  102. }
  103. }
  104. if ([children count] == 0) {
  105. return [FEmptyNode emptyNode];
  106. } else {
  107. FImmutableSortedDictionary *childrenDict = [FImmutableSortedDictionary fromDictionary:children
  108. withComparator:[FUtilities keyComparator]];
  109. return [[FChildrenNode alloc] initWithPriority:priority children:childrenDict];
  110. }
  111. } else if([value isKindOfClass:[NSArray class]]) {
  112. NSArray* aval = (NSArray *)value;
  113. NSMutableDictionary* children = [NSMutableDictionary dictionaryWithCapacity:aval.count];
  114. for(int i = 0; i < [aval count]; i++) {
  115. NSString* key = [NSString stringWithFormat:@"%i", i];
  116. [path addObject:key];
  117. id<FNode> childNode = [FSnapshotUtilities nodeFrom:[aval objectAtIndex:i] priority:nil withValidationFrom:fn atDepth:depth + 1 path:path];
  118. [path removeLastObject];
  119. if (![childNode isEmpty]) {
  120. children[key] = childNode;
  121. }
  122. }
  123. if ([children count] == 0) {
  124. return [FEmptyNode emptyNode];
  125. } else {
  126. FImmutableSortedDictionary *childrenDict = [FImmutableSortedDictionary fromDictionary:children
  127. withComparator:[FUtilities keyComparator]];
  128. return [[FChildrenNode alloc] initWithPriority:priority children:childrenDict];
  129. }
  130. } else {
  131. NSRange range;
  132. range.location = 0;
  133. range.length = MIN(path.count, 50);
  134. NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  135. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData"
  136. reason:[NSString stringWithFormat:@"(%@) Cannot store object of type %@ at %@. "
  137. "Can only store objects of type NSNumber, NSString, NSDictionary, and NSArray.",
  138. fn, [[value class] description], pathString] userInfo:nil];
  139. }
  140. }
  141. + (FCompoundWrite *) compoundWriteFromDictionary:(NSDictionary *)values withValidationFrom:(NSString *)fn {
  142. FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
  143. NSMutableArray *updatePaths = [NSMutableArray arrayWithCapacity:values.count];
  144. for (NSString *keyId in values) {
  145. id value = values[keyId];
  146. [FValidation validateFrom:fn validUpdateDictionaryKey:keyId withValue:value];
  147. FPath* path = [FPath pathWithString:keyId];
  148. id<FNode> node = [FSnapshotUtilities nodeFrom:value withValidationFrom:fn];
  149. [updatePaths addObject:path];
  150. compoundWrite = [compoundWrite addWrite:node atPath:path];
  151. }
  152. // Check that the update paths are not descendants of each other.
  153. [updatePaths sortUsingComparator:^NSComparisonResult(FPath* left, FPath* right) {
  154. return [left compare:right];
  155. }];
  156. FPath *prevPath = nil;
  157. for (FPath *path in updatePaths) {
  158. if (prevPath != nil && [prevPath contains:path]) {
  159. @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid path in object. Path (%@) is an ancestor of (%@).", fn, prevPath, path] userInfo:nil];
  160. }
  161. prevPath = path;
  162. }
  163. return compoundWrite;
  164. }
  165. + (void)validatePriorityNode:(id <FNode>)priorityNode {
  166. assert(priorityNode != nil);
  167. if (priorityNode.isLeafNode) {
  168. id val = priorityNode.val;
  169. if ([val isKindOfClass:[NSDictionary class]]) {
  170. NSDictionary* valDict __unused = (NSDictionary*)val;
  171. NSAssert(valDict[kServerValueSubKey] != nil, @"Priority can't be object unless it's a deferred value");
  172. } else {
  173. NSString *jsType __unused = [FUtilities getJavascriptType:val];
  174. NSAssert(jsType == kJavaScriptString || jsType == kJavaScriptNumber, @"Priority of unexpected type.");
  175. }
  176. } else {
  177. NSAssert(priorityNode == [FMaxNode maxNode] || priorityNode.isEmpty, @"Priority of unexpected type.");
  178. }
  179. // Don't call getPriority() on MAX_NODE to avoid hitting assertion.
  180. NSAssert(priorityNode == [FMaxNode maxNode] || priorityNode.getPriority.isEmpty,
  181. @"Priority nodes can't have a priority of their own.");
  182. }
  183. + (void)appendHashRepresentationForLeafNode:(FLeafNode *)leafNode
  184. toString:(NSMutableString *)string
  185. hashVersion:(FDataHashVersion)hashVersion {
  186. NSAssert(hashVersion == FDataHashVersionV1 || hashVersion == FDataHashVersionV2,
  187. @"Unknown hash version: %lu", (unsigned long)hashVersion);
  188. if (!leafNode.getPriority.isEmpty) {
  189. [string appendString:@"priority:"];
  190. [FSnapshotUtilities appendHashRepresentationForLeafNode:leafNode.getPriority toString:string hashVersion:hashVersion];
  191. [string appendString:@":"];
  192. }
  193. NSString *jsType = [FUtilities getJavascriptType:leafNode.val];
  194. [string appendString:jsType];
  195. [string appendString:@":"];
  196. if (jsType == kJavaScriptBoolean) {
  197. NSString *boolString = [leafNode.val boolValue] ? kJavaScriptTrue : kJavaScriptFalse;
  198. [string appendString:boolString];
  199. } else if (jsType == kJavaScriptNumber) {
  200. NSString *numberString = [FUtilities ieee754StringForNumber:leafNode.val];
  201. [string appendString:numberString];
  202. } else if (jsType == kJavaScriptString) {
  203. if (hashVersion == FDataHashVersionV1) {
  204. [string appendString:leafNode.val];
  205. } else {
  206. NSAssert(hashVersion == FDataHashVersionV2, @"Invalid hash version found");
  207. [FSnapshotUtilities appendHashV2RepresentationForString:leafNode.val toString:string];
  208. }
  209. } else {
  210. [NSException raise:NSInvalidArgumentException format:@"Unknown value for hashing: %@", leafNode];
  211. }
  212. }
  213. + (void)appendHashV2RepresentationForString:(NSString *)string
  214. toString:(NSMutableString *)mutableString {
  215. string = [string stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
  216. string = [string stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
  217. [mutableString appendString:@"\""];
  218. [mutableString appendString:string];
  219. [mutableString appendString:@"\""];
  220. }
  221. + (NSUInteger)estimateLeafNodeSize:(FLeafNode *)leafNode {
  222. NSString *jsType = [FUtilities getJavascriptType:leafNode.val];
  223. // These values are somewhat arbitrary, but we don't need an exact value so prefer performance over exact value
  224. NSUInteger valueSize;
  225. if (jsType == kJavaScriptNumber) {
  226. valueSize = 8; // estimate each float with 8 bytes
  227. } else if (jsType == kJavaScriptBoolean) {
  228. valueSize = 4; // true or false need roughly 4 bytes
  229. } else if (jsType == kJavaScriptString) {
  230. valueSize = 2 + [leafNode.val length]; // add 2 for quotes
  231. } else {
  232. [NSException raise:NSInvalidArgumentException format:@"Unknown leaf type: %@", leafNode];
  233. return 0;
  234. }
  235. if (leafNode.getPriority.isEmpty) {
  236. return valueSize;
  237. } else {
  238. // Account for extra overhead due to the extra JSON object and the ".value" and ".priority" keys, colons, comma
  239. NSUInteger leafPriorityOverhead = 2 + 8 + 11 + 2 + 1;
  240. return leafPriorityOverhead + valueSize + [FSnapshotUtilities estimateLeafNodeSize:leafNode.getPriority];
  241. }
  242. }
  243. + (NSUInteger)estimateSerializedNodeSize:(id<FNode>)node {
  244. if ([node isEmpty]) {
  245. return 4; // null keyword
  246. } else if ([node isLeafNode]) {
  247. return [FSnapshotUtilities estimateLeafNodeSize:node];
  248. } else {
  249. NSAssert([node isKindOfClass:[FChildrenNode class]], @"Unexpected node type: %@", [node class]);
  250. __block NSUInteger sum = 1; // opening brackets
  251. [((FChildrenNode *)node) enumerateChildrenAndPriorityUsingBlock:^(NSString *key, id<FNode>child, BOOL *stop) {
  252. sum += key.length;
  253. sum += 4; // quotes around key and colon and (comma or closing bracket)
  254. sum += [FSnapshotUtilities estimateSerializedNodeSize:child];
  255. }];
  256. return sum;
  257. }
  258. }
  259. @end