1/* 2 * Copyright (c) Meta Platforms, Inc. and affiliates. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 */ 7 8#import "ABI47_0_0RCTAsyncLocalStorage.h" 9 10#import <Foundation/Foundation.h> 11 12#import <CommonCrypto/CommonCryptor.h> 13#import <CommonCrypto/CommonDigest.h> 14#import <ABI47_0_0FBReactNativeSpec/ABI47_0_0FBReactNativeSpec.h> 15 16#import <ABI47_0_0React/ABI47_0_0RCTConvert.h> 17#import <ABI47_0_0React/ABI47_0_0RCTLog.h> 18#import <ABI47_0_0React/ABI47_0_0RCTUtils.h> 19 20#import "ABI47_0_0CoreModulesPlugins.h" 21 22static NSString *const ABI47_0_0RCTStorageDirectory = @"ABI47_0_0RCTAsyncLocalStorage_V1"; 23static NSString *const ABI47_0_0RCTManifestFileName = @"manifest.json"; 24static const NSUInteger ABI47_0_0RCTInlineValueThreshold = 1024; 25 26#pragma mark - Static helper functions 27 28static NSDictionary *ABI47_0_0RCTErrorForKey(NSString *key) 29{ 30 if (![key isKindOfClass:[NSString class]]) { 31 return ABI47_0_0RCTMakeAndLogError(@"Invalid key - must be a string. Key: ", key, @{@"key" : key}); 32 } else if (key.length < 1) { 33 return ABI47_0_0RCTMakeAndLogError(@"Invalid key - must be at least one character. Key: ", key, @{@"key" : key}); 34 } else { 35 return nil; 36 } 37} 38 39static void ABI47_0_0RCTAppendError(NSDictionary *error, NSMutableArray<NSDictionary *> **errors) 40{ 41 if (error && errors) { 42 if (!*errors) { 43 *errors = [NSMutableArray new]; 44 } 45 [*errors addObject:error]; 46 } 47} 48 49static NSString *ABI47_0_0RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut) 50{ 51 if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { 52 NSError *error; 53 NSStringEncoding encoding; 54 NSString *entryString = [NSString stringWithContentsOfFile:filePath usedEncoding:&encoding error:&error]; 55 NSDictionary *extraData = @{@"key" : ABI47_0_0RCTNullIfNil(key)}; 56 57 if (error) { 58 if (errorOut) 59 *errorOut = ABI47_0_0RCTMakeError(@"Failed to read storage file.", error, extraData); 60 return nil; 61 } 62 63 if (encoding != NSUTF8StringEncoding) { 64 if (errorOut) 65 *errorOut = ABI47_0_0RCTMakeError(@"Incorrect encoding of storage file: ", @(encoding), extraData); 66 return nil; 67 } 68 return entryString; 69 } 70 71 return nil; 72} 73 74// Only merges objects - all other types are just clobbered (including arrays) 75static BOOL ABI47_0_0RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) 76{ 77 BOOL modified = NO; 78 for (NSString *key in source) { 79 id sourceValue = source[key]; 80 id destinationValue = destination[key]; 81 if ([sourceValue isKindOfClass:[NSDictionary class]]) { 82 if ([destinationValue isKindOfClass:[NSDictionary class]]) { 83 if ([destinationValue classForCoder] != [NSMutableDictionary class]) { 84 destinationValue = [destinationValue mutableCopy]; 85 } 86 if (ABI47_0_0RCTMergeRecursive(destinationValue, sourceValue)) { 87 destination[key] = destinationValue; 88 modified = YES; 89 } 90 } else { 91 destination[key] = [sourceValue copy]; 92 modified = YES; 93 } 94 } else if (![source isEqual:destinationValue]) { 95 destination[key] = [sourceValue copy]; 96 modified = YES; 97 } 98 } 99 return modified; 100} 101 102// NOTE(nikki93): We replace with scoped implementations of: 103// ABI47_0_0RCTGetStorageDirectory() 104// ABI47_0_0RCTGetManifestFilePath() 105// ABI47_0_0RCTGetMethodQueue() 106// ABI47_0_0RCTGetCache() 107// ABI47_0_0RCTDeleteStorageDirectory() 108 109#define ABI47_0_0RCTGetStorageDirectory() _storageDirectory 110#define ABI47_0_0RCTGetManifestFilePath() _manifestFilePath 111#define ABI47_0_0RCTGetMethodQueue() self.methodQueue 112#define ABI47_0_0RCTGetCache() self.cache 113 114static NSDictionary *ABI47_0_0RCTDeleteStorageDirectory(NSString *storageDirectory) 115{ 116 NSError *error; 117 [[NSFileManager defaultManager] removeItemAtPath:storageDirectory error:&error]; 118 return error ? ABI47_0_0RCTMakeError(@"Failed to delete storage directory.", error, nil) : nil; 119} 120#define ABI47_0_0RCTDeleteStorageDirectory() ABI47_0_0RCTDeleteStorageDirectory(_storageDirectory) 121 122#pragma mark - ABI47_0_0RCTAsyncLocalStorage 123 124@interface ABI47_0_0RCTAsyncLocalStorage () <ABI47_0_0NativeAsyncLocalStorageSpec> 125 126@property (nonatomic, copy) NSString *storageDirectory; 127@property (nonatomic, copy) NSString *manifestFilePath; 128 129@end 130 131@implementation ABI47_0_0RCTAsyncLocalStorage { 132 BOOL _haveSetup; 133 // The manifest is a dictionary of all keys with small values inlined. Null values indicate values that are stored 134 // in separate files (as opposed to nil values which don't exist). The manifest is read off disk at startup, and 135 // written to disk after all mutations. 136 NSMutableDictionary<NSString *, NSString *> *_manifest; 137 NSCache *_cache; 138 dispatch_once_t _cacheOnceToken; 139} 140 141// NOTE(nikki93): Prevents the module from being auto-initialized and allows us to pass our own `storageDirectory` 142+ (NSString *)moduleName { return @"ABI47_0_0RCTAsyncLocalStorage"; } 143- (instancetype)initWithStorageDirectory:(NSString *)storageDirectory 144{ 145 if ((self = [super init])) { 146 _storageDirectory = storageDirectory; 147 _manifestFilePath = [ABI47_0_0RCTGetStorageDirectory() stringByAppendingPathComponent:ABI47_0_0RCTManifestFileName]; 148 } 149 return self; 150} 151 152// NOTE(nikki93): Use the default `methodQueue` since instances have different storage directories 153@synthesize methodQueue = _methodQueue; 154 155- (NSCache *)cache 156{ 157 dispatch_once(&_cacheOnceToken, ^{ 158 _cache = [NSCache new]; 159 _cache.totalCostLimit = 2 * 1024 * 1024; // 2MB 160 161 // Clear cache in the event of a memory warning 162 [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) { 163 [_cache removeAllObjects]; 164 }]; 165 }); 166 return _cache; 167} 168 169- (void)clearAllData 170{ 171 dispatch_async(ABI47_0_0RCTGetMethodQueue(), ^{ 172 [self->_manifest removeAllObjects]; 173 [ABI47_0_0RCTGetCache() removeAllObjects]; 174 ABI47_0_0RCTDeleteStorageDirectory(); 175 }); 176} 177 178- (void)invalidate 179{ 180 if (_clearOnInvalidate) { 181 [ABI47_0_0RCTGetCache() removeAllObjects]; 182 ABI47_0_0RCTDeleteStorageDirectory(); 183 } 184 _clearOnInvalidate = NO; 185 [_manifest removeAllObjects]; 186 _haveSetup = NO; 187} 188 189- (BOOL)isValid 190{ 191 return _haveSetup; 192} 193 194- (void)dealloc 195{ 196 [self invalidate]; 197} 198 199- (NSString *)_filePathForKey:(NSString *)key 200{ 201 NSString *safeFileName = ABI47_0_0RCTMD5Hash(key); 202 return [ABI47_0_0RCTGetStorageDirectory() stringByAppendingPathComponent:safeFileName]; 203} 204 205- (NSDictionary *)_ensureSetup 206{ 207 ABI47_0_0RCTAssertThread(ABI47_0_0RCTGetMethodQueue(), @"Must be executed on storage thread"); 208 209 NSError *error = nil; 210 // NOTE(nikki93): `withIntermediateDirectories:YES` makes this idempotent 211 [[NSFileManager defaultManager] createDirectoryAtPath:ABI47_0_0RCTGetStorageDirectory() 212 withIntermediateDirectories:YES 213 attributes:nil 214 error:&error]; 215 if (error) { 216 return ABI47_0_0RCTMakeError(@"Failed to create storage directory.", error, nil); 217 } 218 if (!_haveSetup) { 219 NSDictionary *errorOut; 220 NSString *serialized = ABI47_0_0RCTReadFile(ABI47_0_0RCTGetManifestFilePath(), ABI47_0_0RCTManifestFileName, &errorOut); 221 _manifest = serialized ? ABI47_0_0RCTJSONParseMutable(serialized, &error) : [NSMutableDictionary new]; 222 if (error) { 223 ABI47_0_0RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); 224 _manifest = [NSMutableDictionary new]; 225 } 226 _haveSetup = YES; 227 } 228 return nil; 229} 230 231- (NSDictionary *)_writeManifest:(NSMutableArray<NSDictionary *> **)errors 232{ 233 NSError *error; 234 NSString *serialized = ABI47_0_0RCTJSONStringify(_manifest, &error); 235 [serialized writeToFile:ABI47_0_0RCTGetManifestFilePath() atomically:YES encoding:NSUTF8StringEncoding error:&error]; 236 NSDictionary *errorOut; 237 if (error) { 238 errorOut = ABI47_0_0RCTMakeError(@"Failed to write manifest file.", error, nil); 239 ABI47_0_0RCTAppendError(errorOut, errors); 240 } 241 return errorOut; 242} 243 244- (NSDictionary *)_appendItemForKey:(NSString *)key toArray:(NSMutableArray<NSArray<NSString *> *> *)result 245{ 246 NSDictionary *errorOut = ABI47_0_0RCTErrorForKey(key); 247 if (errorOut) { 248 return errorOut; 249 } 250 NSString *value = [self _getValueForKey:key errorOut:&errorOut]; 251 [result addObject:@[ key, ABI47_0_0RCTNullIfNil(value) ]]; // Insert null if missing or failure. 252 return errorOut; 253} 254 255- (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut 256{ 257 NSString *value = _manifest[key]; // nil means missing, null means there may be a data file, else: NSString 258 if (value == (id)kCFNull) { 259 value = [ABI47_0_0RCTGetCache() objectForKey:key]; 260 if (!value) { 261 NSString *filePath = [self _filePathForKey:key]; 262 value = ABI47_0_0RCTReadFile(filePath, key, errorOut); 263 if (value) { 264 [ABI47_0_0RCTGetCache() setObject:value forKey:key cost:value.length]; 265 } else { 266 // file does not exist after all, so remove from manifest (no need to save 267 // manifest immediately though, as cost of checking again next time is negligible) 268 [_manifest removeObjectForKey:key]; 269 } 270 } 271 } 272 return value; 273} 274 275- (NSDictionary *)_writeEntry:(NSArray<NSString *> *)entry changedManifest:(BOOL *)changedManifest 276{ 277 if (entry.count != 2) { 278 return ABI47_0_0RCTMakeAndLogError(@"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); 279 } 280 NSString *key = entry[0]; 281 NSDictionary *errorOut = ABI47_0_0RCTErrorForKey(key); 282 if (errorOut) { 283 return errorOut; 284 } 285 if (![entry[1] isKindOfClass:[NSString class]]) { 286 return ABI47_0_0RCTMakeAndLogError(@"Invalid value for entry - must be a string. Got entry: ", entry, nil); 287 } 288 NSString *value = entry[1]; 289 290 NSString *filePath = [self _filePathForKey:key]; 291 NSError *error; 292 if (value.length <= ABI47_0_0RCTInlineValueThreshold) { 293 if (_manifest[key] == (id)kCFNull) { 294 // If the value already existed but wasn't inlined, remove the old file. 295 [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; 296 [ABI47_0_0RCTGetCache() removeObjectForKey:key]; 297 } 298 *changedManifest = YES; 299 _manifest[key] = value; 300 return nil; 301 } 302 [value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error]; 303 [ABI47_0_0RCTGetCache() setObject:value forKey:key cost:value.length]; 304 if (error) { 305 errorOut = ABI47_0_0RCTMakeError(@"Failed to write value.", error, @{@"key" : key}); 306 } else if (_manifest[key] != (id)kCFNull) { 307 *changedManifest = YES; 308 _manifest[key] = (id)kCFNull; 309 } 310 return errorOut; 311} 312 313#pragma mark - Exported JS Functions 314 315ABI47_0_0RCT_EXPORT_METHOD(multiGet : (NSArray<NSString *> *)keys callback : (ABI47_0_0RCTResponseSenderBlock)callback) 316{ 317 NSDictionary *errorOut = [self _ensureSetup]; 318 if (errorOut) { 319 callback(@[ @[ errorOut ], (id)kCFNull ]); 320 return; 321 } 322 NSMutableArray<NSDictionary *> *errors; 323 NSMutableArray<NSArray<NSString *> *> *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; 324 for (NSString *key in keys) { 325 id keyError; 326 id value = [self _getValueForKey:key errorOut:&keyError]; 327 [result addObject:@[ key, ABI47_0_0RCTNullIfNil(value) ]]; 328 ABI47_0_0RCTAppendError(keyError, &errors); 329 } 330 callback(@[ ABI47_0_0RCTNullIfNil(errors), result ]); 331} 332 333ABI47_0_0RCT_EXPORT_METHOD(multiSet : (NSArray<NSArray<NSString *> *> *)kvPairs callback : (ABI47_0_0RCTResponseSenderBlock)callback) 334{ 335 NSDictionary *errorOut = [self _ensureSetup]; 336 if (errorOut) { 337 callback(@[ @[ errorOut ] ]); 338 return; 339 } 340 BOOL changedManifest = NO; 341 NSMutableArray<NSDictionary *> *errors; 342 for (NSArray<NSString *> *entry in kvPairs) { 343 NSDictionary *keyError = [self _writeEntry:entry changedManifest:&changedManifest]; 344 ABI47_0_0RCTAppendError(keyError, &errors); 345 } 346 if (changedManifest) { 347 [self _writeManifest:&errors]; 348 } 349 callback(@[ ABI47_0_0RCTNullIfNil(errors) ]); 350} 351 352ABI47_0_0RCT_EXPORT_METHOD(multiMerge : (NSArray<NSArray<NSString *> *> *)kvPairs callback : (ABI47_0_0RCTResponseSenderBlock)callback) 353{ 354 NSDictionary *errorOut = [self _ensureSetup]; 355 if (errorOut) { 356 callback(@[ @[ errorOut ] ]); 357 return; 358 } 359 BOOL changedManifest = NO; 360 NSMutableArray<NSDictionary *> *errors; 361 for (__strong NSArray<NSString *> *entry in kvPairs) { 362 NSDictionary *keyError; 363 NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; 364 if (!keyError) { 365 if (value) { 366 NSError *jsonError; 367 NSMutableDictionary *mergedVal = ABI47_0_0RCTJSONParseMutable(value, &jsonError); 368 if (ABI47_0_0RCTMergeRecursive(mergedVal, ABI47_0_0RCTJSONParse(entry[1], &jsonError))) { 369 entry = @[ entry[0], ABI47_0_0RCTNullIfNil(ABI47_0_0RCTJSONStringify(mergedVal, NULL)) ]; 370 } 371 if (jsonError) { 372 keyError = ABI47_0_0RCTJSErrorFromNSError(jsonError); 373 } 374 } 375 if (!keyError) { 376 keyError = [self _writeEntry:entry changedManifest:&changedManifest]; 377 } 378 } 379 ABI47_0_0RCTAppendError(keyError, &errors); 380 } 381 if (changedManifest) { 382 [self _writeManifest:&errors]; 383 } 384 callback(@[ ABI47_0_0RCTNullIfNil(errors) ]); 385} 386 387ABI47_0_0RCT_EXPORT_METHOD(multiRemove : (NSArray<NSString *> *)keys callback : (ABI47_0_0RCTResponseSenderBlock)callback) 388{ 389 NSDictionary *errorOut = [self _ensureSetup]; 390 if (errorOut) { 391 callback(@[ @[ errorOut ] ]); 392 return; 393 } 394 NSMutableArray<NSDictionary *> *errors; 395 BOOL changedManifest = NO; 396 for (NSString *key in keys) { 397 NSDictionary *keyError = ABI47_0_0RCTErrorForKey(key); 398 if (!keyError) { 399 if (_manifest[key] == (id)kCFNull) { 400 NSString *filePath = [self _filePathForKey:key]; 401 [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; 402 [ABI47_0_0RCTGetCache() removeObjectForKey:key]; 403 } 404 if (_manifest[key]) { 405 changedManifest = YES; 406 [_manifest removeObjectForKey:key]; 407 } 408 } 409 ABI47_0_0RCTAppendError(keyError, &errors); 410 } 411 if (changedManifest) { 412 [self _writeManifest:&errors]; 413 } 414 callback(@[ ABI47_0_0RCTNullIfNil(errors) ]); 415} 416 417ABI47_0_0RCT_EXPORT_METHOD(clear : (ABI47_0_0RCTResponseSenderBlock)callback) 418{ 419 [_manifest removeAllObjects]; 420 [ABI47_0_0RCTGetCache() removeAllObjects]; 421 NSDictionary *error = ABI47_0_0RCTDeleteStorageDirectory(); 422 callback(@[ ABI47_0_0RCTNullIfNil(error) ]); 423} 424 425ABI47_0_0RCT_EXPORT_METHOD(getAllKeys : (ABI47_0_0RCTResponseSenderBlock)callback) 426{ 427 NSDictionary *errorOut = [self _ensureSetup]; 428 if (errorOut) { 429 callback(@[ errorOut, (id)kCFNull ]); 430 } else { 431 callback(@[ (id)kCFNull, _manifest.allKeys ]); 432 } 433} 434 435- (std::shared_ptr<ABI47_0_0facebook::ABI47_0_0React::TurboModule>)getTurboModule: 436 (const ABI47_0_0facebook::ABI47_0_0React::ObjCTurboModule::InitParams &)params 437{ 438 return std::make_shared<ABI47_0_0facebook::ABI47_0_0React::NativeAsyncLocalStorageSpecJSI>(params); 439} 440 441@end 442 443Class ABI47_0_0RCTAsyncLocalStorageCls(void) 444{ 445 return ABI47_0_0RCTAsyncLocalStorage.class; 446} 447