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