1/**
2 * Copyright (c) Facebook, Inc. and its 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 "RNCAsyncStorage.h"
9
10#import <CommonCrypto/CommonCryptor.h>
11#import <CommonCrypto/CommonDigest.h>
12
13#import <React/RCTConvert.h>
14#import <React/RCTLog.h>
15#import <React/RCTUtils.h>
16
17// NOTE(kudo): Use Expo storage directory for backward compatibility
18//static NSString *const RCTStorageDirectory = @"RCTAsyncLocalStorage_V1";
19static NSString *const RCTStorageDirectory = @"RCTAsyncLocalStorage";
20static NSString *const RCTOldStorageDirectory = @"RNCAsyncLocalStorage_V1";
21static NSString *const RCTExpoStorageDirectory = @"RCTAsyncLocalStorage";
22static NSString *const RCTManifestFileName = @"manifest.json";
23static const NSUInteger RCTInlineValueThreshold = 1024;
24
25#pragma mark - Static helper functions
26
27static NSDictionary *RCTErrorForKey(NSString *key)
28{
29    if (![key isKindOfClass:[NSString class]]) {
30        return RCTMakeAndLogError(@"Invalid key - must be a string.  Key: ", key, @{@"key": key});
31    } else if (key.length < 1) {
32        return RCTMakeAndLogError(
33            @"Invalid key - must be at least one character.  Key: ", key, @{@"key": key});
34    } else {
35        return nil;
36    }
37}
38
39static BOOL RCTAsyncStorageSetExcludedFromBackup(NSString *path, NSNumber *isExcluded)
40{
41    NSFileManager *fileManager = [[NSFileManager alloc] init];
42
43    BOOL isDir;
44    BOOL exists = [fileManager fileExistsAtPath:path isDirectory:&isDir];
45    BOOL success = false;
46
47    if (isDir && exists) {
48        NSURL *pathUrl = [NSURL fileURLWithPath:path];
49        NSError *error = nil;
50        success = [pathUrl setResourceValue:isExcluded
51                                     forKey:NSURLIsExcludedFromBackupKey
52                                      error:&error];
53
54        if (!success) {
55            NSLog(@"Could not exclude AsyncStorage dir from backup %@", error);
56        }
57    }
58    return success;
59}
60
61static void RCTAppendError(NSDictionary *error, NSMutableArray<NSDictionary *> **errors)
62{
63    if (error && errors) {
64        if (!*errors) {
65            *errors = [NSMutableArray new];
66        }
67        [*errors addObject:error];
68    }
69}
70
71static NSArray<NSDictionary *> *RCTMakeErrors(NSArray<id<NSObject>> *results)
72{
73    NSMutableArray<NSDictionary *> *errors;
74    for (id object in results) {
75        if ([object isKindOfClass:[NSError class]]) {
76            NSError *error = (NSError *)object;
77            NSDictionary *keyError = RCTMakeError(error.localizedDescription, error, nil);
78            RCTAppendError(keyError, &errors);
79        }
80    }
81    return errors;
82}
83
84static NSString *RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut)
85{
86    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
87        NSError *error;
88        NSStringEncoding encoding;
89        NSString *entryString = [NSString stringWithContentsOfFile:filePath
90                                                      usedEncoding:&encoding
91                                                             error:&error];
92        NSDictionary *extraData = @{@"key": RCTNullIfNil(key)};
93
94        if (error) {
95            if (errorOut) {
96                *errorOut = RCTMakeError(@"Failed to read storage file.", error, extraData);
97            }
98            return nil;
99        }
100
101        if (encoding != NSUTF8StringEncoding) {
102            if (errorOut) {
103                *errorOut =
104                    RCTMakeError(@"Incorrect encoding of storage file: ", @(encoding), extraData);
105            }
106            return nil;
107        }
108        return entryString;
109    }
110
111    return nil;
112}
113
114// DO NOT USE
115// This is used internally to migrate data from the old file location to the new one.
116// Please use `RCTCreateStorageDirectoryPath` instead
117static NSString *RCTCreateStorageDirectoryPath_deprecated(NSString *storageDir)
118{
119    NSString *storageDirectoryPath;
120#if TARGET_OS_TV
121    storageDirectoryPath =
122        NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
123#else
124    storageDirectoryPath =
125        NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
126#endif
127    storageDirectoryPath = [storageDirectoryPath stringByAppendingPathComponent:storageDir];
128    return storageDirectoryPath;
129}
130
131static NSString *RCTCreateStorageDirectoryPath(NSString *storageDir)
132{
133    NSString *storageDirectoryPath = @"";
134
135#if TARGET_OS_TV
136    storageDirectoryPath =
137        NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
138#else
139    storageDirectoryPath =
140        NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES)
141            .firstObject;
142    // We should use the "Application Support/[bundleID]" folder for persistent data storage that's
143    // hidden from users
144    storageDirectoryPath = [storageDirectoryPath
145        stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]];
146#endif
147
148    // Per Apple's docs, all app content in Application Support must be within a subdirectory of the
149    // app's bundle identifier
150    storageDirectoryPath = [storageDirectoryPath stringByAppendingPathComponent:storageDir];
151
152    return storageDirectoryPath;
153}
154
155static NSString *RCTCreateManifestFilePath(NSString *storageDirectory)
156{
157    return [storageDirectory stringByAppendingPathComponent:RCTManifestFileName];
158}
159
160// Only merges objects - all other types are just clobbered (including arrays)
161static BOOL RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source)
162{
163    BOOL modified = NO;
164    for (NSString *key in source) {
165        id sourceValue = source[key];
166        id destinationValue = destination[key];
167        if ([sourceValue isKindOfClass:[NSDictionary class]]) {
168            if ([destinationValue isKindOfClass:[NSDictionary class]]) {
169                if ([destinationValue classForCoder] != [NSMutableDictionary class]) {
170                    destinationValue = [destinationValue mutableCopy];
171                }
172                if (RCTMergeRecursive(destinationValue, sourceValue)) {
173                    destination[key] = destinationValue;
174                    modified = YES;
175                }
176            } else {
177                destination[key] = [sourceValue copy];
178                modified = YES;
179            }
180        } else if (![source isEqual:destinationValue]) {
181            destination[key] = [sourceValue copy];
182            modified = YES;
183        }
184    }
185    return modified;
186}
187
188static BOOL RCTHasCreatedStorageDirectory = NO;
189
190// NOTE(nikki93): We replace with scoped implementations of:
191//   RCTGetStorageDirectory()
192//   RCTGetManifestFilePath()
193//   RCTGetMethodQueue()
194//   RCTGetCache()
195//   RCTDeleteStorageDirectory()
196
197#define RCTGetStorageDirectory() _storageDirectory
198#define RCTGetManifestFilePath() _manifestFilePath
199#define RCTGetMethodQueue() self.methodQueue
200#define RCTGetCache() self.cache
201
202static NSDictionary *RCTDeleteStorageDirectory(NSString *storageDirectory)
203{
204  NSError *error;
205  [[NSFileManager defaultManager] removeItemAtPath:storageDirectory error:&error];
206  return error ? RCTMakeError(@"Failed to delete storage directory.", error, nil) : nil;
207}
208#define RCTDeleteStorageDirectory() RCTDeleteStorageDirectory(_storageDirectory)
209
210static NSDate *RCTManifestModificationDate(NSString *manifestFilePath)
211{
212    NSDictionary *attributes =
213        [[NSFileManager defaultManager] attributesOfItemAtPath:manifestFilePath error:nil];
214    return [attributes fileModificationDate];
215}
216
217/**
218 * Creates an NSException used during Storage Directory Migration.
219 */
220static void RCTStorageDirectoryMigrationLogError(NSString *reason, NSError *error)
221{
222    RCTLogWarn(@"%@: %@", reason, error ? error.description : @"");
223}
224
225static void RCTStorageDirectoryCleanupOld(NSString *oldDirectoryPath)
226{
227    NSError *error;
228    if (![[NSFileManager defaultManager] removeItemAtPath:oldDirectoryPath error:&error]) {
229        RCTStorageDirectoryMigrationLogError(
230            @"Failed to remove old storage directory during migration", error);
231    }
232}
233
234static void _createStorageDirectory(NSString *storageDirectory, NSError **error)
235{
236    [[NSFileManager defaultManager] createDirectoryAtPath:storageDirectory
237                              withIntermediateDirectories:YES
238                                               attributes:nil
239                                                    error:error];
240}
241
242static void RCTStorageDirectoryMigrate(NSString *oldDirectoryPath,
243                                       NSString *newDirectoryPath,
244                                       BOOL shouldCleanupOldDirectory)
245{
246  assert(false);
247}
248
249/**
250 * Determine which of RCTOldStorageDirectory or RCTExpoStorageDirectory needs to migrated.
251 * If both exist, we remove the least recently modified and return the most recently modified.
252 * Otherwise, this will return the path to whichever directory exists.
253 * If no directory exists, then return nil.
254 */
255static NSString *RCTGetStoragePathForMigration()
256{
257    BOOL isDir;
258    NSString *oldStoragePath = RCTCreateStorageDirectoryPath_deprecated(RCTOldStorageDirectory);
259    NSString *expoStoragePath = RCTCreateStorageDirectoryPath_deprecated(RCTExpoStorageDirectory);
260    NSFileManager *fileManager = [NSFileManager defaultManager];
261    BOOL oldStorageDirectoryExists =
262        [fileManager fileExistsAtPath:oldStoragePath isDirectory:&isDir] && isDir;
263    BOOL expoStorageDirectoryExists =
264        [fileManager fileExistsAtPath:expoStoragePath isDirectory:&isDir] && isDir;
265
266    // Check if both the old storage directory and Expo storage directory exist
267    if (oldStorageDirectoryExists && expoStorageDirectoryExists) {
268        // If the old storage has been modified more recently than Expo storage, then clear Expo
269        // storage. Otherwise, clear the old storage.
270        if ([RCTManifestModificationDate(RCTCreateManifestFilePath(oldStoragePath))
271                compare:RCTManifestModificationDate(RCTCreateManifestFilePath(expoStoragePath))] ==
272            NSOrderedDescending) {
273            RCTStorageDirectoryCleanupOld(expoStoragePath);
274            return oldStoragePath;
275        } else {
276            RCTStorageDirectoryCleanupOld(oldStoragePath);
277            return expoStoragePath;
278        }
279    } else if (oldStorageDirectoryExists) {
280        return oldStoragePath;
281    } else if (expoStorageDirectoryExists) {
282        return expoStoragePath;
283    } else {
284        return nil;
285    }
286}
287
288/**
289 * This check is added to make sure that anyone coming from pre-1.2.2 does not lose cached data.
290 * Check that data is migrated from the old location to the new location
291 * fromStorageDirectory: the directory where the older data lives
292 * toStorageDirectory: the directory where the new data should live
293 * shouldCleanupOldDirectoryAndOverwriteNewDirectory: YES if we should delete the old directory's
294 * contents and overwrite the new directory's contents during the migration to the new directory
295 */
296static void
297RCTStorageDirectoryMigrationCheck(NSString *fromStorageDirectory,
298                                  NSString *toStorageDirectory,
299                                  BOOL shouldCleanupOldDirectoryAndOverwriteNewDirectory)
300{
301    NSError *error;
302    BOOL isDir;
303    NSFileManager *fileManager = [NSFileManager defaultManager];
304    // If the old directory exists, it means we may need to migrate old data to the new directory
305    if ([fileManager fileExistsAtPath:fromStorageDirectory isDirectory:&isDir] && isDir) {
306        // Check if the new storage directory location already exists
307        if ([fileManager fileExistsAtPath:toStorageDirectory]) {
308            // If new storage location exists, check if the new storage has been modified sooner in
309            // which case we may want to cleanup the old location
310            if ([RCTManifestModificationDate(RCTCreateManifestFilePath(toStorageDirectory))
311                    compare:RCTManifestModificationDate(
312                                RCTCreateManifestFilePath(fromStorageDirectory))] == 1) {
313                // If new location has been modified more recently, simply clean out old data
314                if (shouldCleanupOldDirectoryAndOverwriteNewDirectory) {
315                    RCTStorageDirectoryCleanupOld(fromStorageDirectory);
316                }
317            } else if (shouldCleanupOldDirectoryAndOverwriteNewDirectory) {
318                // If old location has been modified more recently, remove new storage and migrate
319                if (![fileManager removeItemAtPath:toStorageDirectory error:&error]) {
320                    RCTStorageDirectoryMigrationLogError(
321                        @"Failed to remove new storage directory during migration", error);
322                } else {
323                    RCTStorageDirectoryMigrate(fromStorageDirectory,
324                                               toStorageDirectory,
325                                               shouldCleanupOldDirectoryAndOverwriteNewDirectory);
326                }
327            }
328        } else {
329            // If new storage location doesn't exist, migrate data
330            RCTStorageDirectoryMigrate(fromStorageDirectory,
331                                       toStorageDirectory,
332                                       shouldCleanupOldDirectoryAndOverwriteNewDirectory);
333        }
334    }
335}
336
337#pragma mark - RNCAsyncStorage
338
339@interface RNCAsyncStorage ()
340
341@property (nonatomic, copy) NSString *storageDirectory;
342@property (nonatomic, copy) NSString *manifestFilePath;
343
344@end
345
346@implementation RNCAsyncStorage {
347    BOOL _haveSetup;
348    // The manifest is a dictionary of all keys with small values inlined.  Null values indicate
349    // values that are stored in separate files (as opposed to nil values which don't exist).  The
350    // manifest is read off disk at startup, and written to disk after all mutations.
351    NSMutableDictionary<NSString *, NSString *> *_manifest;
352    NSCache *_cache;
353    dispatch_once_t _cacheOnceToken;
354}
355
356// NOTE(nikki93): Prevents the module from being auto-initialized and allows us to pass our own `storageDirectory`
357+ (NSString *)moduleName { return @"RCTAsyncLocalStorage"; }
358- (instancetype)initWithStorageDirectory:(NSString *)storageDirectory
359{
360  if ((self = [super init])) {
361    _storageDirectory = storageDirectory;
362    _manifestFilePath = [RCTGetStorageDirectory() stringByAppendingPathComponent:RCTManifestFileName];
363  }
364  return self;
365}
366
367// NOTE(nikki93): Use the default `methodQueue` since instances have different storage directories
368@synthesize methodQueue = _methodQueue;
369
370- (NSCache *)cache
371{
372  dispatch_once(&_cacheOnceToken, ^{
373    _cache = [NSCache new];
374    _cache.totalCostLimit = 2 * 1024 * 1024; // 2MB
375
376    // Clear cache in the event of a memory warning
377    [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) {
378      [_cache removeAllObjects];
379    }];
380  });
381  return _cache;
382}
383
384+ (BOOL)requiresMainQueueSetup
385{
386    return NO;
387}
388
389- (instancetype)init
390{
391  assert(false);
392    if (!(self = [super init])) {
393        return nil;
394    }
395
396    // Get the path to any old storage directory that needs to be migrated. If multiple exist,
397    // the oldest are removed and the most recently modified is returned.
398    NSString *oldStoragePath = RCTGetStoragePathForMigration();
399    if (oldStoragePath != nil) {
400        // Migrate our deprecated path "Documents/.../RNCAsyncLocalStorage_V1" or
401        // "Documents/.../RCTAsyncLocalStorage" to "Documents/.../RCTAsyncLocalStorage_V1"
402        RCTStorageDirectoryMigrationCheck(
403            oldStoragePath, RCTCreateStorageDirectoryPath_deprecated(RCTStorageDirectory), YES);
404    }
405
406    // Migrate what's in "Documents/.../RCTAsyncLocalStorage_V1" to
407    // "Application Support/[bundleID]/RCTAsyncLocalStorage_V1"
408    RCTStorageDirectoryMigrationCheck(RCTCreateStorageDirectoryPath_deprecated(RCTStorageDirectory),
409                                      RCTCreateStorageDirectoryPath(RCTStorageDirectory),
410                                      NO);
411
412    return self;
413}
414
415- (void)clearAllData
416{
417    dispatch_async(RCTGetMethodQueue(), ^{
418      [self->_manifest removeAllObjects];
419      [RCTGetCache() removeAllObjects];
420      RCTDeleteStorageDirectory();
421    });
422}
423
424- (void)invalidate
425{
426    if (_clearOnInvalidate) {
427        [RCTGetCache() removeAllObjects];
428        RCTDeleteStorageDirectory();
429    }
430    _clearOnInvalidate = NO;
431    [_manifest removeAllObjects];
432    _haveSetup = NO;
433}
434
435- (BOOL)isValid
436{
437    return _haveSetup;
438}
439
440- (void)dealloc
441{
442    [self invalidate];
443}
444
445- (NSString *)_filePathForKey:(NSString *)key
446{
447    NSString *safeFileName = RCTMD5Hash(key);
448    return [RCTGetStorageDirectory() stringByAppendingPathComponent:safeFileName];
449}
450
451- (NSDictionary *)_ensureSetup
452{
453    RCTAssertThread(RCTGetMethodQueue(), @"Must be executed on storage thread");
454
455#if TARGET_OS_TV
456    RCTLogWarn(
457        @"Persistent storage is not supported on tvOS, your data may be removed at any point.");
458#endif
459
460    NSError *error = nil;
461    // NOTE(nikki93): `withIntermediateDirectories:YES` makes this idempotent
462    [[NSFileManager defaultManager] createDirectoryAtPath:RCTGetStorageDirectory()
463                              withIntermediateDirectories:YES
464                                               attributes:nil
465                                                    error:&error];
466    if (error) {
467      return RCTMakeError(@"Failed to create storage directory.", error, nil);
468    }
469
470    if (!_haveSetup) {
471        // iCloud backup exclusion
472        NSNumber *isExcludedFromBackup =
473            [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RCTAsyncStorageExcludeFromBackup"];
474        if (isExcludedFromBackup == nil) {
475            // by default, we want to exclude AsyncStorage data from backup
476            isExcludedFromBackup = @YES;
477        }
478        // NOTE(kudo): We don't enable iCloud backup for Expo Go
479        // RCTAsyncStorageSetExcludedFromBackup(RCTCreateStorageDirectoryPath(RCTStorageDirectory),
480        //                                      isExcludedFromBackup);
481
482        NSDictionary *errorOut = nil;
483        // NOTE(kudo): Keep data in Documents rather than Application Support for backward compatibility
484        // NSString *serialized = RCTReadFile(RCTCreateStorageDirectoryPath(RCTGetManifestFilePath())
485        NSString *serialized = RCTReadFile(RCTGetManifestFilePath(),
486                                           RCTManifestFileName,
487                                           &errorOut);
488        if (!serialized) {
489            if (errorOut) {
490                // We cannot simply create a new manifest in case the file does exist but we have no
491                // access to it. This can happen when data protection is enabled for the app and we
492                // are trying to read the manifest while the device is locked. (The app can be
493                // started by the system even if the device is locked due to e.g. a geofence event.)
494                RCTLogError(
495                    @"Could not open the existing manifest, perhaps data protection is "
496                    @"enabled?\n\n%@",
497                    errorOut);
498                return errorOut;
499            } else {
500                // We can get nil without errors only when the file does not exist.
501                RCTLogTrace(@"Manifest does not exist - creating a new one.\n\n%@", errorOut);
502                _manifest = [NSMutableDictionary new];
503            }
504        } else {
505            _manifest = RCTJSONParseMutable(serialized, &error);
506            if (!_manifest) {
507                RCTLogError(@"Failed to parse manifest - creating a new one.\n\n%@", error);
508                _manifest = [NSMutableDictionary new];
509            }
510        }
511        _haveSetup = YES;
512    }
513
514    return nil;
515}
516
517- (NSDictionary *)_writeManifest:(NSMutableArray<NSDictionary *> **)errors
518{
519    NSError *error;
520    NSString *serialized = RCTJSONStringify(_manifest, &error);
521    // NOTE(kudo): Keep data in Documents rather than Application Support for backward compatibility
522    // [serialized writeToFile:RCTCreateStorageDirectoryPath(RCTGetManifestFilePath())
523    [serialized writeToFile:RCTGetManifestFilePath()
524                 atomically:YES
525                   encoding:NSUTF8StringEncoding
526                      error:&error];
527    NSDictionary *errorOut;
528    if (error) {
529        errorOut = RCTMakeError(@"Failed to write manifest file.", error, nil);
530        RCTAppendError(errorOut, errors);
531    }
532    return errorOut;
533}
534
535- (NSDictionary *)_appendItemForKey:(NSString *)key
536                            toArray:(NSMutableArray<NSArray<NSString *> *> *)result
537{
538    NSDictionary *errorOut = RCTErrorForKey(key);
539    if (errorOut) {
540        return errorOut;
541    }
542    NSString *value = [self _getValueForKey:key errorOut:&errorOut];
543    [result addObject:@[key, RCTNullIfNil(value)]];  // Insert null if missing or failure.
544    return errorOut;
545}
546
547- (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut
548{
549    NSString *value =
550        _manifest[key];  // nil means missing, null means there may be a data file, else: NSString
551    if (value == (id)kCFNull) {
552        value = [RCTGetCache() objectForKey:key];
553        if (!value) {
554            NSString *filePath = [self _filePathForKey:key];
555            value = RCTReadFile(filePath, key, errorOut);
556            if (value) {
557                [RCTGetCache() setObject:value forKey:key cost:value.length];
558            } else {
559                // file does not exist after all, so remove from manifest (no need to save
560                // manifest immediately though, as cost of checking again next time is negligible)
561                [_manifest removeObjectForKey:key];
562            }
563        }
564    }
565    return value;
566}
567
568- (NSDictionary *)_writeEntry:(NSArray<NSString *> *)entry changedManifest:(BOOL *)changedManifest
569{
570    if (entry.count != 2) {
571        return RCTMakeAndLogError(
572            @"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil);
573    }
574    NSString *key = entry[0];
575    NSDictionary *errorOut = RCTErrorForKey(key);
576    if (errorOut) {
577        return errorOut;
578    }
579    NSString *value = entry[1];
580    NSString *filePath = [self _filePathForKey:key];
581    NSError *error;
582    if (value.length <= RCTInlineValueThreshold) {
583        if (_manifest[key] == (id)kCFNull) {
584            // If the value already existed but wasn't inlined, remove the old file.
585            [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
586            [RCTGetCache() removeObjectForKey:key];
587        }
588        *changedManifest = YES;
589        _manifest[key] = value;
590        return nil;
591    }
592    [value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error];
593    [RCTGetCache() setObject:value forKey:key cost:value.length];
594    if (error) {
595        errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key});
596    } else if (_manifest[key] != (id)kCFNull) {
597        *changedManifest = YES;
598        _manifest[key] = (id)kCFNull;
599    }
600    return errorOut;
601}
602
603- (void)_multiGet:(NSArray<NSString *> *)keys
604         callback:(RCTResponseSenderBlock)callback
605           getter:(NSString * (^)(NSUInteger i, NSString *key, NSDictionary **errorOut))getValue
606{
607    NSMutableArray<NSDictionary *> *errors;
608    NSMutableArray<NSArray<NSString *> *> *result = [NSMutableArray arrayWithCapacity:keys.count];
609    for (NSUInteger i = 0; i < keys.count; ++i) {
610        NSString *key = keys[i];
611        id keyError;
612        id value = getValue(i, key, &keyError);
613        [result addObject:@[key, RCTNullIfNil(value)]];
614        RCTAppendError(keyError, &errors);
615    }
616    callback(@[RCTNullIfNil(errors), result]);
617}
618
619- (BOOL)_passthroughDelegate
620{
621    return
622        [self.delegate respondsToSelector:@selector(isPassthrough)] && self.delegate.isPassthrough;
623}
624
625#pragma mark - Exported JS Functions
626
627// clang-format off
628RCT_EXPORT_METHOD(multiGet:(NSArray<NSString *> *)keys
629                  callback:(RCTResponseSenderBlock)callback)
630// clang-format on
631{
632    if (self.delegate != nil) {
633        [self.delegate
634            valuesForKeys:keys
635               completion:^(NSArray<id<NSObject>> *valuesOrErrors) {
636                 [self _multiGet:keys
637                        callback:callback
638                          getter:^NSString *(NSUInteger i, NSString *key, NSDictionary **errorOut) {
639                            id valueOrError = valuesOrErrors[i];
640                            if ([valueOrError isKindOfClass:[NSError class]]) {
641                                NSError *error = (NSError *)valueOrError;
642                                NSDictionary *extraData = @{@"key": RCTNullIfNil(key)};
643                                *errorOut =
644                                    RCTMakeError(error.localizedDescription, error, extraData);
645                                return nil;
646                            } else {
647                                return [valueOrError isKindOfClass:[NSString class]]
648                                           ? (NSString *)valueOrError
649                                           : nil;
650                            }
651                          }];
652               }];
653
654        if (![self _passthroughDelegate]) {
655            return;
656        }
657    }
658
659    NSDictionary *errorOut = [self _ensureSetup];
660    if (errorOut) {
661        callback(@[@[errorOut], (id)kCFNull]);
662        return;
663    }
664    [self _multiGet:keys
665           callback:callback
666             getter:^(NSUInteger i, NSString *key, NSDictionary **errorOut) {
667               return [self _getValueForKey:key errorOut:errorOut];
668             }];
669}
670
671// clang-format off
672RCT_EXPORT_METHOD(multiSet:(NSArray<NSArray<NSString *> *> *)kvPairs
673                  callback:(RCTResponseSenderBlock)callback)
674// clang-format on
675{
676    if (self.delegate != nil) {
677        NSMutableArray<NSString *> *keys = [NSMutableArray arrayWithCapacity:kvPairs.count];
678        NSMutableArray<NSString *> *values = [NSMutableArray arrayWithCapacity:kvPairs.count];
679        for (NSArray<NSString *> *entry in kvPairs) {
680            [keys addObject:entry[0]];
681            [values addObject:entry[1]];
682        }
683        [self.delegate setValues:values
684                         forKeys:keys
685                      completion:^(NSArray<id<NSObject>> *results) {
686                        NSArray<NSDictionary *> *errors = RCTMakeErrors(results);
687                        callback(@[RCTNullIfNil(errors)]);
688                      }];
689
690        if (![self _passthroughDelegate]) {
691            return;
692        }
693    }
694
695    NSDictionary *errorOut = [self _ensureSetup];
696    if (errorOut) {
697        callback(@[@[errorOut]]);
698        return;
699    }
700    BOOL changedManifest = NO;
701    NSMutableArray<NSDictionary *> *errors;
702    for (NSArray<NSString *> *entry in kvPairs) {
703        NSDictionary *keyError = [self _writeEntry:entry changedManifest:&changedManifest];
704        RCTAppendError(keyError, &errors);
705    }
706    if (changedManifest) {
707        [self _writeManifest:&errors];
708    }
709    callback(@[RCTNullIfNil(errors)]);
710}
711
712// clang-format off
713RCT_EXPORT_METHOD(multiMerge:(NSArray<NSArray<NSString *> *> *)kvPairs
714                    callback:(RCTResponseSenderBlock)callback)
715// clang-format on
716{
717    if (self.delegate != nil) {
718        NSMutableArray<NSString *> *keys = [NSMutableArray arrayWithCapacity:kvPairs.count];
719        NSMutableArray<NSString *> *values = [NSMutableArray arrayWithCapacity:kvPairs.count];
720        for (NSArray<NSString *> *entry in kvPairs) {
721            [keys addObject:entry[0]];
722            [values addObject:entry[1]];
723        }
724        [self.delegate mergeValues:values
725                           forKeys:keys
726                        completion:^(NSArray<id<NSObject>> *results) {
727                          NSArray<NSDictionary *> *errors = RCTMakeErrors(results);
728                          callback(@[RCTNullIfNil(errors)]);
729                        }];
730
731        if (![self _passthroughDelegate]) {
732            return;
733        }
734    }
735
736    NSDictionary *errorOut = [self _ensureSetup];
737    if (errorOut) {
738        callback(@[@[errorOut]]);
739        return;
740    }
741    BOOL changedManifest = NO;
742    NSMutableArray<NSDictionary *> *errors;
743    for (__strong NSArray<NSString *> *entry in kvPairs) {
744        NSDictionary *keyError;
745        NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError];
746        if (!keyError) {
747            if (value) {
748                NSError *jsonError;
749                NSMutableDictionary *mergedVal = RCTJSONParseMutable(value, &jsonError);
750                if (RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &jsonError))) {
751                    entry = @[entry[0], RCTNullIfNil(RCTJSONStringify(mergedVal, NULL))];
752                }
753                if (jsonError) {
754                    keyError = RCTJSErrorFromNSError(jsonError);
755                }
756            }
757            if (!keyError) {
758                keyError = [self _writeEntry:entry changedManifest:&changedManifest];
759            }
760        }
761        RCTAppendError(keyError, &errors);
762    }
763    if (changedManifest) {
764        [self _writeManifest:&errors];
765    }
766    callback(@[RCTNullIfNil(errors)]);
767}
768
769// clang-format off
770RCT_EXPORT_METHOD(multiRemove:(NSArray<NSString *> *)keys
771                     callback:(RCTResponseSenderBlock)callback)
772// clang-format on
773{
774    if (self.delegate != nil) {
775        [self.delegate removeValuesForKeys:keys
776                                completion:^(NSArray<id<NSObject>> *results) {
777                                  NSArray<NSDictionary *> *errors = RCTMakeErrors(results);
778                                  callback(@[RCTNullIfNil(errors)]);
779                                }];
780
781        if (![self _passthroughDelegate]) {
782            return;
783        }
784    }
785
786    NSDictionary *errorOut = [self _ensureSetup];
787    if (errorOut) {
788        callback(@[@[errorOut]]);
789        return;
790    }
791    NSMutableArray<NSDictionary *> *errors;
792    BOOL changedManifest = NO;
793    for (NSString *key in keys) {
794        NSDictionary *keyError = RCTErrorForKey(key);
795        if (!keyError) {
796            if (_manifest[key] == (id)kCFNull) {
797                NSString *filePath = [self _filePathForKey:key];
798                [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
799                [RCTGetCache() removeObjectForKey:key];
800            }
801            if (_manifest[key]) {
802                changedManifest = YES;
803                [_manifest removeObjectForKey:key];
804            }
805        }
806        RCTAppendError(keyError, &errors);
807    }
808    if (changedManifest) {
809        [self _writeManifest:&errors];
810    }
811    callback(@[RCTNullIfNil(errors)]);
812}
813
814// clang-format off
815RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback)
816// clang-format on
817{
818    if (self.delegate != nil) {
819        [self.delegate removeAllValues:^(NSError *error) {
820          NSDictionary *result = nil;
821          if (error != nil) {
822              result = RCTMakeError(error.localizedDescription, error, nil);
823          }
824          callback(@[RCTNullIfNil(result)]);
825        }];
826        return;
827    }
828
829    [_manifest removeAllObjects];
830    [RCTGetCache() removeAllObjects];
831    NSDictionary *error = RCTDeleteStorageDirectory();
832    callback(@[RCTNullIfNil(error)]);
833}
834
835// clang-format off
836RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback)
837// clang-format on
838{
839    if (self.delegate != nil) {
840        [self.delegate allKeys:^(NSArray<id<NSObject>> *keys) {
841          callback(@[(id)kCFNull, keys]);
842        }];
843
844        if (![self _passthroughDelegate]) {
845            return;
846        }
847    }
848
849    NSDictionary *errorOut = [self _ensureSetup];
850    if (errorOut) {
851        callback(@[errorOut, (id)kCFNull]);
852    } else {
853        callback(@[(id)kCFNull, _manifest.allKeys]);
854    }
855}
856
857@end
858