FSnapshotUtilities.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  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 "FirebaseDatabase/Sources/Snapshot/FSnapshotUtilities.h"
  17. #import "FirebaseDatabase/Sources/Constants/FConstants.h"
  18. #import "FirebaseDatabase/Sources/FMaxNode.h"
  19. #import "FirebaseDatabase/Sources/FNamedNode.h"
  20. #import "FirebaseDatabase/Sources/Snapshot/FChildrenNode.h"
  21. #import "FirebaseDatabase/Sources/Snapshot/FCompoundWrite.h"
  22. #import "FirebaseDatabase/Sources/Snapshot/FEmptyNode.h"
  23. #import "FirebaseDatabase/Sources/Snapshot/FLeafNode.h"
  24. #import "FirebaseDatabase/Sources/Utilities/FUtilities.h"
  25. #import "FirebaseDatabase/Sources/Utilities/FValidation.h"
  26. #import "FirebaseDatabase/Sources/third_party/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.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
  33. priority:priority
  34. withValidationFrom:@"nodeFrom:priority:"];
  35. }
  36. + (id<FNode>)nodeFrom:(id)val withValidationFrom:(NSString *)fn {
  37. return [FSnapshotUtilities nodeFrom:val priority:nil withValidationFrom:fn];
  38. }
  39. + (id<FNode>)nodeFrom:(id)val
  40. priority:(id)priority
  41. withValidationFrom:(NSString *)fn {
  42. return [FSnapshotUtilities nodeFrom:val
  43. priority:priority
  44. withValidationFrom:fn
  45. atDepth:0
  46. path:[[NSMutableArray alloc] init]];
  47. }
  48. + (id<FNode>)nodeFrom:(id)val
  49. priority:(id)aPriority
  50. withValidationFrom:(NSString *)fn
  51. atDepth:(int)depth
  52. path:(NSMutableArray *)path {
  53. @autoreleasepool {
  54. return [FSnapshotUtilities internalNodeFrom:val
  55. priority:aPriority
  56. withValidationFrom:fn
  57. atDepth:depth
  58. path:path];
  59. }
  60. }
  61. + (id<FNode>)internalNodeFrom:(id)val
  62. priority:(id)aPriority
  63. withValidationFrom:(NSString *)fn
  64. atDepth:(int)depth
  65. path:(NSMutableArray *)path {
  66. if (depth > kFirebaseMaxObjectDepth) {
  67. NSRange range;
  68. range.location = 0;
  69. range.length = 100;
  70. NSString *pathString =
  71. [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  72. @throw [[NSException alloc]
  73. initWithName:@"InvalidFirebaseData"
  74. reason:[NSString stringWithFormat:
  75. @"(%@) Max object depth exceeded: %@...",
  76. fn, pathString]
  77. userInfo:nil];
  78. }
  79. if (val == nil || val == [NSNull null]) {
  80. // Null is a valid type to store
  81. return [FEmptyNode emptyNode];
  82. }
  83. [FValidation validateFrom:fn isValidPriorityValue:aPriority withPath:path];
  84. id<FNode> priority = [FSnapshotUtilities nodeFrom:aPriority];
  85. id value = val;
  86. BOOL isLeafNode = NO;
  87. if ([value isKindOfClass:[NSDictionary class]]) {
  88. NSDictionary *dict = val;
  89. if (dict[kPayloadPriority] != nil) {
  90. id rawPriority = [dict objectForKey:kPayloadPriority];
  91. [FValidation validateFrom:fn
  92. isValidPriorityValue:rawPriority
  93. withPath:path];
  94. priority = [FSnapshotUtilities nodeFrom:rawPriority];
  95. }
  96. if (dict[kPayloadValue] != nil) {
  97. value = [dict objectForKey:kPayloadValue];
  98. if ([FValidation validateFrom:fn
  99. isValidLeafValue:value
  100. withPath:path]) {
  101. isLeafNode = YES;
  102. } else {
  103. @throw [[NSException alloc]
  104. initWithName:@"InvalidLeafValueType"
  105. reason:[NSString stringWithFormat:
  106. @"(%@) Invalid data type used "
  107. @"with .value. Can only use "
  108. "NSString and NSNumber or be "
  109. "null. Found %@ instead.",
  110. fn, [[value class] description]]
  111. userInfo:nil];
  112. }
  113. }
  114. }
  115. if ([FValidation validateFrom:fn isValidLeafValue:value withPath:path]) {
  116. isLeafNode = YES;
  117. }
  118. if (isLeafNode) {
  119. return [[FLeafNode alloc] initWithValue:value withPriority:priority];
  120. }
  121. // Unlike with JS, we have to handle the dictionary and array cases
  122. // separately.
  123. if ([value isKindOfClass:[NSDictionary class]]) {
  124. NSDictionary *dval = (NSDictionary *)value;
  125. NSMutableDictionary *children =
  126. [NSMutableDictionary dictionaryWithCapacity:dval.count];
  127. // Avoid creating a million newPaths by appending to old one
  128. for (id keyId in dval) {
  129. [FValidation validateFrom:fn
  130. validDictionaryKey:keyId
  131. withPath:path];
  132. NSString *key = (NSString *)keyId;
  133. if (![key hasPrefix:kPayloadMetadataPrefix]) {
  134. [path addObject:key];
  135. id<FNode> childNode = [FSnapshotUtilities nodeFrom:dval[key]
  136. priority:nil
  137. withValidationFrom:fn
  138. atDepth:depth + 1
  139. path:path];
  140. [path removeLastObject];
  141. if (![childNode isEmpty]) {
  142. children[key] = childNode;
  143. }
  144. }
  145. }
  146. if ([children count] == 0) {
  147. return [FEmptyNode emptyNode];
  148. } else {
  149. FImmutableSortedDictionary *childrenDict =
  150. [FImmutableSortedDictionary
  151. fromDictionary:children
  152. withComparator:[FUtilities keyComparator]];
  153. return [[FChildrenNode alloc] initWithPriority:priority
  154. children:childrenDict];
  155. }
  156. } else if ([value isKindOfClass:[NSArray class]]) {
  157. NSArray *aval = (NSArray *)value;
  158. NSMutableDictionary *children =
  159. [NSMutableDictionary dictionaryWithCapacity:aval.count];
  160. for (int i = 0; i < [aval count]; i++) {
  161. NSString *key = [NSString stringWithFormat:@"%i", i];
  162. [path addObject:key];
  163. id<FNode> childNode =
  164. [FSnapshotUtilities nodeFrom:[aval objectAtIndex:i]
  165. priority:nil
  166. withValidationFrom:fn
  167. atDepth:depth + 1
  168. path:path];
  169. [path removeLastObject];
  170. if (![childNode isEmpty]) {
  171. children[key] = childNode;
  172. }
  173. }
  174. if ([children count] == 0) {
  175. return [FEmptyNode emptyNode];
  176. } else {
  177. FImmutableSortedDictionary *childrenDict =
  178. [FImmutableSortedDictionary
  179. fromDictionary:children
  180. withComparator:[FUtilities keyComparator]];
  181. return [[FChildrenNode alloc] initWithPriority:priority
  182. children:childrenDict];
  183. }
  184. } else {
  185. NSRange range;
  186. range.location = 0;
  187. range.length = MIN(path.count, 50);
  188. NSString *pathString =
  189. [[path subarrayWithRange:range] componentsJoinedByString:@"."];
  190. @throw [[NSException alloc]
  191. initWithName:@"InvalidFirebaseData"
  192. reason:[NSString
  193. stringWithFormat:
  194. @"(%@) Cannot store object of type %@ at %@. "
  195. "Can only store objects of type NSNumber, "
  196. "NSString, NSDictionary, and NSArray.",
  197. fn, [[value class] description], pathString]
  198. userInfo:nil];
  199. }
  200. }
  201. + (FCompoundWrite *)compoundWriteFromDictionary:(NSDictionary *)values
  202. withValidationFrom:(NSString *)fn {
  203. FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
  204. NSMutableArray *updatePaths =
  205. [NSMutableArray arrayWithCapacity:values.count];
  206. for (NSString *keyId in values) {
  207. id value = values[keyId];
  208. [FValidation validateFrom:fn
  209. validUpdateDictionaryKey:keyId
  210. withValue:value];
  211. FPath *path = [FPath pathWithString:keyId];
  212. id<FNode> node = [FSnapshotUtilities nodeFrom:value
  213. withValidationFrom:fn];
  214. [updatePaths addObject:path];
  215. compoundWrite = [compoundWrite addWrite:node atPath:path];
  216. }
  217. // Check that the update paths are not descendants of each other.
  218. [updatePaths
  219. sortUsingComparator:^NSComparisonResult(FPath *left, FPath *right) {
  220. return [left compare:right];
  221. }];
  222. FPath *prevPath = nil;
  223. for (FPath *path in updatePaths) {
  224. if (prevPath != nil && [prevPath contains:path]) {
  225. @throw [[NSException alloc]
  226. initWithName:@"InvalidFirebaseData"
  227. reason:[NSString stringWithFormat:
  228. @"(%@) Invalid path in object. Path "
  229. @"(%@) is an ancestor of (%@).",
  230. fn, prevPath, path]
  231. userInfo:nil];
  232. }
  233. prevPath = path;
  234. }
  235. return compoundWrite;
  236. }
  237. + (void)validatePriorityNode:(id<FNode>)priorityNode {
  238. assert(priorityNode != nil);
  239. if (priorityNode.isLeafNode) {
  240. id val = priorityNode.val;
  241. if ([val isKindOfClass:[NSDictionary class]]) {
  242. NSDictionary *valDict __unused = (NSDictionary *)val;
  243. NSAssert(valDict[kServerValueSubKey] != nil,
  244. @"Priority can't be object unless it's a deferred value");
  245. } else {
  246. NSString *jsType __unused = [FUtilities getJavascriptType:val];
  247. NSAssert(jsType == kJavaScriptString || jsType == kJavaScriptNumber,
  248. @"Priority of unexpected type.");
  249. }
  250. } else {
  251. NSAssert(priorityNode == [FMaxNode maxNode] || priorityNode.isEmpty,
  252. @"Priority of unexpected type.");
  253. }
  254. // Don't call getPriority() on MAX_NODE to avoid hitting assertion.
  255. NSAssert(priorityNode == [FMaxNode maxNode] ||
  256. priorityNode.getPriority.isEmpty,
  257. @"Priority nodes can't have a priority of their own.");
  258. }
  259. + (void)appendHashRepresentationForLeafNode:(FLeafNode *)leafNode
  260. toString:(NSMutableString *)string
  261. hashVersion:(FDataHashVersion)hashVersion {
  262. NSAssert(hashVersion == FDataHashVersionV1 ||
  263. hashVersion == FDataHashVersionV2,
  264. @"Unknown hash version: %lu", (unsigned long)hashVersion);
  265. if (!leafNode.getPriority.isEmpty) {
  266. [string appendString:@"priority:"];
  267. [FSnapshotUtilities
  268. appendHashRepresentationForLeafNode:leafNode.getPriority
  269. toString:string
  270. hashVersion:hashVersion];
  271. [string appendString:@":"];
  272. }
  273. NSString *jsType = [FUtilities getJavascriptType:leafNode.val];
  274. [string appendString:jsType];
  275. [string appendString:@":"];
  276. if (jsType == kJavaScriptBoolean) {
  277. NSString *boolString =
  278. [leafNode.val boolValue] ? kJavaScriptTrue : kJavaScriptFalse;
  279. [string appendString:boolString];
  280. } else if (jsType == kJavaScriptNumber) {
  281. NSString *numberString =
  282. [FUtilities ieee754StringForNumber:leafNode.val];
  283. [string appendString:numberString];
  284. } else if (jsType == kJavaScriptString) {
  285. if (hashVersion == FDataHashVersionV1) {
  286. [string appendString:leafNode.val];
  287. } else {
  288. NSAssert(hashVersion == FDataHashVersionV2,
  289. @"Invalid hash version found");
  290. [FSnapshotUtilities appendHashV2RepresentationForString:leafNode.val
  291. toString:string];
  292. }
  293. } else {
  294. [NSException raise:NSInvalidArgumentException
  295. format:@"Unknown value for hashing: %@", leafNode];
  296. }
  297. }
  298. + (void)appendHashV2RepresentationForString:(NSString *)string
  299. toString:(NSMutableString *)mutableString {
  300. string = [string stringByReplacingOccurrencesOfString:@"\\"
  301. withString:@"\\\\"];
  302. string = [string stringByReplacingOccurrencesOfString:@"\""
  303. withString:@"\\\""];
  304. [mutableString appendString:@"\""];
  305. [mutableString appendString:string];
  306. [mutableString appendString:@"\""];
  307. }
  308. + (NSUInteger)estimateLeafNodeSize:(FLeafNode *)leafNode {
  309. NSString *jsType = [FUtilities getJavascriptType:leafNode.val];
  310. // These values are somewhat arbitrary, but we don't need an exact value so
  311. // prefer performance over exact value
  312. NSUInteger valueSize;
  313. if (jsType == kJavaScriptNumber) {
  314. valueSize = 8; // estimate each float with 8 bytes
  315. } else if (jsType == kJavaScriptBoolean) {
  316. valueSize = 4; // true or false need roughly 4 bytes
  317. } else if (jsType == kJavaScriptString) {
  318. valueSize = 2 + [leafNode.val length]; // add 2 for quotes
  319. } else {
  320. [NSException raise:NSInvalidArgumentException
  321. format:@"Unknown leaf type: %@", leafNode];
  322. return 0;
  323. }
  324. if (leafNode.getPriority.isEmpty) {
  325. return valueSize;
  326. } else {
  327. // Account for extra overhead due to the extra JSON object and the
  328. // ".value" and ".priority" keys, colons, comma
  329. NSUInteger leafPriorityOverhead = 2 + 8 + 11 + 2 + 1;
  330. return leafPriorityOverhead + valueSize +
  331. [FSnapshotUtilities estimateLeafNodeSize:leafNode.getPriority];
  332. }
  333. }
  334. + (NSUInteger)estimateSerializedNodeSize:(id<FNode>)node {
  335. if ([node isEmpty]) {
  336. return 4; // null keyword
  337. } else if ([node isLeafNode]) {
  338. return [FSnapshotUtilities estimateLeafNodeSize:node];
  339. } else {
  340. NSAssert([node isKindOfClass:[FChildrenNode class]],
  341. @"Unexpected node type: %@", [node class]);
  342. __block NSUInteger sum = 1; // opening brackets
  343. [((FChildrenNode *)node) enumerateChildrenAndPriorityUsingBlock:^(
  344. NSString *key, id<FNode> child,
  345. BOOL *stop) {
  346. sum += key.length;
  347. sum +=
  348. 4; // quotes around key and colon and (comma or closing bracket)
  349. sum += [FSnapshotUtilities estimateSerializedNodeSize:child];
  350. }];
  351. return sum;
  352. }
  353. }
  354. @end