@logbrew/react-native 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,542 @@
1
+ #import "LBRNFatalRecordStore.h"
2
+
3
+ #import <errno.h>
4
+ #import <fcntl.h>
5
+ #import <math.h>
6
+ #import <stdint.h>
7
+ #import <sys/stat.h>
8
+ #import <unistd.h>
9
+
10
+ NSString *const LBRNFatalRecordFileName = @"fatal-js-v1.record";
11
+ NSString *const LBRNFatalRecordTemporaryFileName = @"fatal-js-v1.tmp";
12
+
13
+ static const NSUInteger LBRNMaximumRecordBytes = 16 * 1024;
14
+ static const NSUInteger LBRNMaximumFrames = 24;
15
+ static const NSUInteger LBRNMaximumFilenameBytes = 512;
16
+ static const NSUInteger LBRNMaximumIdentifierBytes = 96;
17
+ static const int32_t LBRNMaximumCounter = INT32_MAX;
18
+
19
+ @interface LBRNFatalRecordStore ()
20
+ @property (nonatomic, readonly) NSURL *directoryURL;
21
+ @property (nonatomic, readonly, copy)
22
+ LBRNFatalDirectoryPreparation directoryPreparation;
23
+ @end
24
+
25
+ @implementation LBRNFatalRecordStore
26
+
27
+ - (instancetype)initWithDirectoryURL:(NSURL *)directoryURL
28
+ {
29
+ return [self initWithDirectoryURL:directoryURL
30
+ directoryPreparation:^BOOL(__unused NSURL *preparedURL) {
31
+ return NO;
32
+ }];
33
+ }
34
+
35
+ - (instancetype)initWithDirectoryURL:(NSURL *)directoryURL
36
+ directoryPreparation:(LBRNFatalDirectoryPreparation)directoryPreparation
37
+ {
38
+ self = [super init];
39
+ if (self != nil) {
40
+ _directoryURL = directoryURL;
41
+ _directoryPreparation = [directoryPreparation copy];
42
+ }
43
+ return self;
44
+ }
45
+
46
+ - (NSDictionary *)writeRecord:(NSDictionary *)record
47
+ {
48
+ @synchronized(self) {
49
+ NSDictionary *validated = [self validatedRecord:record requireZeroCounters:YES];
50
+ if (validated == nil) {
51
+ return @{ @"status" : @"invalid_record" };
52
+ }
53
+
54
+ int directoryFD = [self openPrivateDirectory];
55
+ if (directoryFD < 0) {
56
+ return @{ @"status" : @"storage_error" };
57
+ }
58
+ @try {
59
+ BOOL directoryPrepared = NO;
60
+ @try {
61
+ directoryPrepared = self.directoryPreparation(self.directoryURL);
62
+ } @catch (__unused NSException *exception) {
63
+ directoryPrepared = NO;
64
+ }
65
+ if (!directoryPrepared || ![self directoryFDMatchesDirectoryURL:directoryFD]) {
66
+ return @{ @"status" : @"storage_error" };
67
+ }
68
+
69
+ NSDictionary *existing = [self readRecordFromDirectory:directoryFD];
70
+ NSString *status = existing[@"status"];
71
+ if ([status isEqualToString:@"storage_error"]) {
72
+ return existing;
73
+ }
74
+ if ([status isEqualToString:@"pending"]) {
75
+ NSMutableDictionary *preserved = [existing[@"record"] mutableCopy];
76
+ int32_t dropped = [preserved[@"droppedRecords"] intValue];
77
+ if (dropped < LBRNMaximumCounter) {
78
+ dropped += 1;
79
+ }
80
+ preserved[@"droppedRecords"] = @(dropped);
81
+ if (![self atomicallyWriteRecord:preserved directoryFD:directoryFD]) {
82
+ return @{ @"status" : @"storage_error" };
83
+ }
84
+ return @{
85
+ @"status" : @"dropped_pending",
86
+ @"recordId" : preserved[@"id"],
87
+ @"droppedRecords" : @(dropped),
88
+ };
89
+ }
90
+
91
+ NSMutableDictionary *stored = [validated mutableCopy];
92
+ BOOL recoveredCorruption = [status isEqualToString:@"corrupt_discarded"];
93
+ if (recoveredCorruption) {
94
+ stored[@"corruptRecords"] = @1;
95
+ }
96
+ if (![self atomicallyWriteRecord:stored directoryFD:directoryFD]) {
97
+ return @{ @"status" : @"storage_error" };
98
+ }
99
+ return @{
100
+ @"status" : recoveredCorruption ? @"stored_after_corruption" : @"stored",
101
+ @"recordId" : stored[@"id"],
102
+ @"corruptRecords" : stored[@"corruptRecords"],
103
+ };
104
+ } @finally {
105
+ close(directoryFD);
106
+ }
107
+ }
108
+ }
109
+
110
+ - (NSDictionary *)readRecord
111
+ {
112
+ @synchronized(self) {
113
+ int directoryFD = [self openPrivateDirectory];
114
+ if (directoryFD < 0) {
115
+ return @{ @"status" : @"storage_error" };
116
+ }
117
+ @try {
118
+ return [self readRecordFromDirectory:directoryFD];
119
+ } @finally {
120
+ close(directoryFD);
121
+ }
122
+ }
123
+ }
124
+
125
+ - (NSDictionary *)acknowledgeRecordId:(NSString *)recordId
126
+ {
127
+ @synchronized(self) {
128
+ if (![self validIdentifier:recordId]) {
129
+ return @{ @"status" : @"id_mismatch" };
130
+ }
131
+ int directoryFD = [self openPrivateDirectory];
132
+ if (directoryFD < 0) {
133
+ return @{ @"status" : @"storage_error" };
134
+ }
135
+ @try {
136
+ NSDictionary *existing = [self readRecordFromDirectory:directoryFD];
137
+ if (![existing[@"status"] isEqualToString:@"pending"]) {
138
+ return existing;
139
+ }
140
+ NSDictionary *record = existing[@"record"];
141
+ if (![record[@"id"] isEqualToString:recordId]) {
142
+ return @{
143
+ @"status" : @"id_mismatch",
144
+ @"recordId" : record[@"id"],
145
+ };
146
+ }
147
+ if (unlinkat(directoryFD, LBRNFatalRecordFileName.UTF8String, 0) != 0
148
+ || fsync(directoryFD) != 0) {
149
+ return @{ @"status" : @"storage_error" };
150
+ }
151
+ return @{
152
+ @"status" : @"acknowledged",
153
+ @"recordId" : recordId,
154
+ };
155
+ } @finally {
156
+ close(directoryFD);
157
+ }
158
+ }
159
+ }
160
+
161
+ - (NSDictionary *)discardRecord
162
+ {
163
+ @synchronized(self) {
164
+ int directoryFD = [self openPrivateDirectory];
165
+ if (directoryFD < 0) {
166
+ return @{ @"status" : @"storage_error" };
167
+ }
168
+ @try {
169
+ NSDictionary *existing = [self readRecordFromDirectory:directoryFD];
170
+ if (![existing[@"status"] isEqualToString:@"pending"]) {
171
+ return existing;
172
+ }
173
+ NSDictionary *record = existing[@"record"];
174
+ if (unlinkat(directoryFD, LBRNFatalRecordFileName.UTF8String, 0) != 0
175
+ || fsync(directoryFD) != 0) {
176
+ return @{ @"status" : @"storage_error" };
177
+ }
178
+ return @{
179
+ @"status" : @"discarded",
180
+ @"recordId" : record[@"id"],
181
+ };
182
+ } @finally {
183
+ close(directoryFD);
184
+ }
185
+ }
186
+ }
187
+
188
+ - (int)openPrivateDirectory
189
+ {
190
+ if (!self.directoryURL.isFileURL) {
191
+ return -1;
192
+ }
193
+ const char *path = self.directoryURL.fileSystemRepresentation;
194
+ struct stat info;
195
+ if (lstat(path, &info) != 0) {
196
+ if (errno != ENOENT || mkdir(path, 0700) != 0) {
197
+ return -1;
198
+ }
199
+ } else if (!S_ISDIR(info.st_mode) || S_ISLNK(info.st_mode)) {
200
+ return -1;
201
+ }
202
+
203
+ int directoryFD = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
204
+ if (directoryFD < 0) {
205
+ return -1;
206
+ }
207
+ if (fstat(directoryFD, &info) != 0 || !S_ISDIR(info.st_mode)
208
+ || fchmod(directoryFD, 0700) != 0) {
209
+ close(directoryFD);
210
+ return -1;
211
+ }
212
+ return directoryFD;
213
+ }
214
+
215
+ - (BOOL)directoryFDMatchesDirectoryURL:(int)directoryFD
216
+ {
217
+ struct stat openedInfo;
218
+ struct stat pathInfo;
219
+ if (fstat(directoryFD, &openedInfo) != 0
220
+ || lstat(self.directoryURL.fileSystemRepresentation, &pathInfo) != 0) {
221
+ return NO;
222
+ }
223
+ return S_ISDIR(openedInfo.st_mode) && S_ISDIR(pathInfo.st_mode)
224
+ && !S_ISLNK(pathInfo.st_mode) && openedInfo.st_dev == pathInfo.st_dev
225
+ && openedInfo.st_ino == pathInfo.st_ino;
226
+ }
227
+
228
+ - (NSDictionary *)readRecordFromDirectory:(int)directoryFD
229
+ {
230
+ if (![self removeStaleTemporaryFile:directoryFD]) {
231
+ return @{ @"status" : @"storage_error" };
232
+ }
233
+
234
+ struct stat pathInfo;
235
+ if (fstatat(
236
+ directoryFD,
237
+ LBRNFatalRecordFileName.UTF8String,
238
+ &pathInfo,
239
+ AT_SYMLINK_NOFOLLOW)
240
+ != 0) {
241
+ return errno == ENOENT ? @{ @"status" : @"empty" }
242
+ : @{ @"status" : @"storage_error" };
243
+ }
244
+ if (!S_ISREG(pathInfo.st_mode) || S_ISLNK(pathInfo.st_mode)) {
245
+ return @{ @"status" : @"storage_error" };
246
+ }
247
+ if (pathInfo.st_size < 0 || (uint64_t)pathInfo.st_size > LBRNMaximumRecordBytes) {
248
+ return [self discardCorruptRecord:directoryFD];
249
+ }
250
+
251
+ int fileFD = openat(
252
+ directoryFD,
253
+ LBRNFatalRecordFileName.UTF8String,
254
+ O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
255
+ if (fileFD < 0) {
256
+ return @{ @"status" : @"storage_error" };
257
+ }
258
+ NSMutableData *data = [NSMutableData dataWithLength:(NSUInteger)pathInfo.st_size];
259
+ BOOL readSucceeded = YES;
260
+ NSUInteger offset = 0;
261
+ while (offset < data.length) {
262
+ ssize_t count =
263
+ read(fileFD, (uint8_t *)data.mutableBytes + offset, data.length - offset);
264
+ if (count <= 0) {
265
+ readSucceeded = NO;
266
+ break;
267
+ }
268
+ offset += (NSUInteger)count;
269
+ }
270
+ struct stat openedInfo;
271
+ if (fstat(fileFD, &openedInfo) != 0 || openedInfo.st_dev != pathInfo.st_dev
272
+ || openedInfo.st_ino != pathInfo.st_ino || !S_ISREG(openedInfo.st_mode)) {
273
+ readSucceeded = NO;
274
+ }
275
+ close(fileFD);
276
+ if (!readSucceeded) {
277
+ return @{ @"status" : @"storage_error" };
278
+ }
279
+
280
+ NSError *error = nil;
281
+ id object = [NSPropertyListSerialization propertyListWithData:data
282
+ options:NSPropertyListImmutable
283
+ format:nil
284
+ error:&error];
285
+ NSDictionary *record =
286
+ [object isKindOfClass:[NSDictionary class]]
287
+ ? [self validatedRecord:(NSDictionary *)object requireZeroCounters:NO]
288
+ : nil;
289
+ if (record == nil) {
290
+ return [self discardCorruptRecord:directoryFD];
291
+ }
292
+ return @{
293
+ @"status" : @"pending",
294
+ @"record" : record,
295
+ };
296
+ }
297
+
298
+ - (NSDictionary *)discardCorruptRecord:(int)directoryFD
299
+ {
300
+ if (unlinkat(directoryFD, LBRNFatalRecordFileName.UTF8String, 0) != 0
301
+ || fsync(directoryFD) != 0) {
302
+ return @{ @"status" : @"storage_error" };
303
+ }
304
+ return @{
305
+ @"status" : @"corrupt_discarded",
306
+ @"corruptRecords" : @1,
307
+ };
308
+ }
309
+
310
+ - (BOOL)removeStaleTemporaryFile:(int)directoryFD
311
+ {
312
+ struct stat info;
313
+ if (fstatat(
314
+ directoryFD,
315
+ LBRNFatalRecordTemporaryFileName.UTF8String,
316
+ &info,
317
+ AT_SYMLINK_NOFOLLOW)
318
+ != 0) {
319
+ return errno == ENOENT;
320
+ }
321
+ if (!S_ISREG(info.st_mode) || S_ISLNK(info.st_mode)) {
322
+ return NO;
323
+ }
324
+ return unlinkat(directoryFD, LBRNFatalRecordTemporaryFileName.UTF8String, 0) == 0;
325
+ }
326
+
327
+ - (BOOL)atomicallyWriteRecord:(NSDictionary *)record directoryFD:(int)directoryFD
328
+ {
329
+ NSError *serializationError = nil;
330
+ NSData *data =
331
+ [NSPropertyListSerialization dataWithPropertyList:record
332
+ format:NSPropertyListBinaryFormat_v1_0
333
+ options:0
334
+ error:&serializationError];
335
+ if (data == nil || data.length == 0 || data.length > LBRNMaximumRecordBytes) {
336
+ return NO;
337
+ }
338
+ if (![self removeStaleTemporaryFile:directoryFD]) {
339
+ return NO;
340
+ }
341
+
342
+ int fileFD = openat(
343
+ directoryFD,
344
+ LBRNFatalRecordTemporaryFileName.UTF8String,
345
+ O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW,
346
+ 0600);
347
+ if (fileFD < 0) {
348
+ return NO;
349
+ }
350
+ BOOL succeeded = fchmod(fileFD, 0600) == 0;
351
+ NSUInteger offset = 0;
352
+ while (succeeded && offset < data.length) {
353
+ ssize_t count =
354
+ write(fileFD, (const uint8_t *)data.bytes + offset, data.length - offset);
355
+ if (count <= 0) {
356
+ succeeded = NO;
357
+ } else {
358
+ offset += (NSUInteger)count;
359
+ }
360
+ }
361
+ if (succeeded) {
362
+ succeeded = fsync(fileFD) == 0;
363
+ }
364
+ if (close(fileFD) != 0) {
365
+ succeeded = NO;
366
+ }
367
+ if (succeeded) {
368
+ succeeded = renameat(
369
+ directoryFD,
370
+ LBRNFatalRecordTemporaryFileName.UTF8String,
371
+ directoryFD,
372
+ LBRNFatalRecordFileName.UTF8String)
373
+ == 0;
374
+ }
375
+ if (succeeded) {
376
+ succeeded = fsync(directoryFD) == 0;
377
+ }
378
+ if (!succeeded) {
379
+ unlinkat(directoryFD, LBRNFatalRecordTemporaryFileName.UTF8String, 0);
380
+ }
381
+ return succeeded;
382
+ }
383
+
384
+ - (nullable NSDictionary *)validatedRecord:(NSDictionary *)record
385
+ requireZeroCounters:(BOOL)requireZeroCounters
386
+ {
387
+ NSSet *expectedKeys = [NSSet setWithArray:@[
388
+ @"schemaVersion",
389
+ @"id",
390
+ @"timestamp",
391
+ @"errorName",
392
+ @"stackFrames",
393
+ @"droppedRecords",
394
+ @"corruptRecords",
395
+ ]];
396
+ if (![expectedKeys isEqualToSet:[NSSet setWithArray:record.allKeys]]
397
+ || ![record[@"schemaVersion"] isEqual:@1]
398
+ || ![self validIdentifier:record[@"id"]]
399
+ || ![self validTimestamp:record[@"timestamp"]]
400
+ || ![self validErrorName:record[@"errorName"]]
401
+ || ![record[@"stackFrames"] isKindOfClass:[NSArray class]]) {
402
+ return nil;
403
+ }
404
+
405
+ NSArray *frames = record[@"stackFrames"];
406
+ if (frames.count > LBRNMaximumFrames) {
407
+ return nil;
408
+ }
409
+ NSMutableArray *validatedFrames = [NSMutableArray arrayWithCapacity:frames.count];
410
+ for (id candidate in frames) {
411
+ if (![candidate isKindOfClass:[NSDictionary class]]) {
412
+ return nil;
413
+ }
414
+ NSDictionary *frame = candidate;
415
+ NSSet *frameKeys =
416
+ [NSSet setWithArray:@[ @"filename", @"line", @"column" ]];
417
+ if (![frameKeys isEqualToSet:[NSSet setWithArray:frame.allKeys]]
418
+ || ![self validFilename:frame[@"filename"]]
419
+ || ![self validPositiveInteger:frame[@"line"]]
420
+ || ![self validPositiveInteger:frame[@"column"]]) {
421
+ return nil;
422
+ }
423
+ [validatedFrames addObject:@{
424
+ @"filename" : frame[@"filename"],
425
+ @"line" : frame[@"line"],
426
+ @"column" : frame[@"column"],
427
+ }];
428
+ }
429
+
430
+ NSNumber *dropped = record[@"droppedRecords"];
431
+ NSNumber *corrupt = record[@"corruptRecords"];
432
+ if (![self validCounter:dropped] || ![self validCounter:corrupt]
433
+ || (requireZeroCounters && (dropped.intValue != 0 || corrupt.intValue != 0))) {
434
+ return nil;
435
+ }
436
+ return @{
437
+ @"schemaVersion" : @1,
438
+ @"id" : record[@"id"],
439
+ @"timestamp" : record[@"timestamp"],
440
+ @"errorName" : record[@"errorName"],
441
+ @"stackFrames" : validatedFrames,
442
+ @"droppedRecords" : @(dropped.intValue),
443
+ @"corruptRecords" : @(corrupt.intValue),
444
+ };
445
+ }
446
+
447
+ - (BOOL)validIdentifier:(id)value
448
+ {
449
+ if (![value isKindOfClass:[NSString class]]
450
+ || [(NSString *)value lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > LBRNMaximumIdentifierBytes) {
451
+ return NO;
452
+ }
453
+ NSRegularExpression *pattern =
454
+ [NSRegularExpression regularExpressionWithPattern:@"^evt_rn_fatal_[a-z0-9]+(?:_[a-z0-9]+)*$"
455
+ options:0
456
+ error:nil];
457
+ NSString *identifier = value;
458
+ return [pattern firstMatchInString:identifier
459
+ options:0
460
+ range:NSMakeRange(0, identifier.length)]
461
+ != nil;
462
+ }
463
+
464
+ - (BOOL)validTimestamp:(id)value
465
+ {
466
+ if (![value isKindOfClass:[NSString class]]) {
467
+ return NO;
468
+ }
469
+ NSString *timestamp = value;
470
+ if (timestamp.length < 20 || timestamp.length > 35) {
471
+ return NO;
472
+ }
473
+ NSRegularExpression *pattern =
474
+ [NSRegularExpression regularExpressionWithPattern:
475
+ @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{1,9})?Z$"
476
+ options:0
477
+ error:nil];
478
+ return [pattern firstMatchInString:timestamp
479
+ options:0
480
+ range:NSMakeRange(0, timestamp.length)]
481
+ != nil;
482
+ }
483
+
484
+ - (BOOL)validErrorName:(id)value
485
+ {
486
+ static NSSet *allowedNames;
487
+ static dispatch_once_t onceGuard;
488
+ dispatch_once(&onceGuard, ^{
489
+ allowedNames = [NSSet setWithArray:@[
490
+ @"Error",
491
+ @"EvalError",
492
+ @"RangeError",
493
+ @"ReferenceError",
494
+ @"SyntaxError",
495
+ @"TypeError",
496
+ @"URIError",
497
+ ]];
498
+ });
499
+ return [value isKindOfClass:[NSString class]] && [allowedNames containsObject:value];
500
+ }
501
+
502
+ - (BOOL)validFilename:(id)value
503
+ {
504
+ if (![value isKindOfClass:[NSString class]]) {
505
+ return NO;
506
+ }
507
+ NSString *filename = value;
508
+ if (filename.length == 0
509
+ || [filename lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > LBRNMaximumFilenameBytes
510
+ || [filename hasPrefix:@"/"] || [filename containsString:@"\\"]
511
+ || [filename containsString:@"://"] || [filename containsString:@"?"]
512
+ || [filename containsString:@"#"]) {
513
+ return NO;
514
+ }
515
+ for (NSString *component in [filename componentsSeparatedByString:@"/"]) {
516
+ if ([component isEqualToString:@".."]) {
517
+ return NO;
518
+ }
519
+ }
520
+ NSCharacterSet *control = [NSCharacterSet controlCharacterSet];
521
+ return [filename rangeOfCharacterFromSet:control].location == NSNotFound;
522
+ }
523
+
524
+ - (BOOL)validPositiveInteger:(id)value
525
+ {
526
+ if (![value isKindOfClass:[NSNumber class]]) {
527
+ return NO;
528
+ }
529
+ double number = [value doubleValue];
530
+ return isfinite(number) && number >= 1 && number <= INT32_MAX && floor(number) == number;
531
+ }
532
+
533
+ - (BOOL)validCounter:(id)value
534
+ {
535
+ if (![value isKindOfClass:[NSNumber class]]) {
536
+ return NO;
537
+ }
538
+ double number = [value doubleValue];
539
+ return isfinite(number) && number >= 0 && number <= LBRNMaximumCounter && floor(number) == number;
540
+ }
541
+
542
+ @end
@@ -0,0 +1,4 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface LBRNFatalStoreModule : NSObject <RCTBridgeModule>
4
+ @end
@@ -0,0 +1,147 @@
1
+ #import "LBRNFatalStoreModule.h"
2
+
3
+ #import "LBRNFatalRecordStore.h"
4
+
5
+ #ifdef RCT_NEW_ARCH_ENABLED
6
+ #import <LogBrewReactNativeSpec/LogBrewReactNativeSpec.h>
7
+ #endif
8
+
9
+ static NSDictionary *LBRNStorageError(void)
10
+ {
11
+ return @{ @"status" : @"storage_error" };
12
+ }
13
+
14
+ static NSDictionary *LBRNNormalizeRecord(NSDictionary *record)
15
+ {
16
+ if (![record isKindOfClass:[NSDictionary class]]) {
17
+ return @{};
18
+ }
19
+ id framesValue = record[@"stackFrames"];
20
+ if (![framesValue isKindOfClass:[NSArray class]]) {
21
+ return record;
22
+ }
23
+ NSMutableArray *frames = [NSMutableArray arrayWithCapacity:[framesValue count]];
24
+ for (id value in framesValue) {
25
+ if (![value isKindOfClass:[NSDictionary class]]) {
26
+ return record;
27
+ }
28
+ NSMutableDictionary *frame = [value mutableCopy];
29
+ NSString *filename = frame[@"filename"];
30
+ if ([filename isKindOfClass:[NSString class]]
31
+ && [filename hasPrefix:@"/"]
32
+ && [[filename substringFromIndex:1] rangeOfString:@"/"].location == NSNotFound) {
33
+ frame[@"filename"] = [filename substringFromIndex:1];
34
+ }
35
+ [frames addObject:frame];
36
+ }
37
+ NSMutableDictionary *normalized = [record mutableCopy];
38
+ normalized[@"stackFrames"] = frames;
39
+ return normalized;
40
+ }
41
+
42
+ @interface LBRNFatalStoreModule ()
43
+ #ifdef RCT_NEW_ARCH_ENABLED
44
+ <NativeLogBrewFatalStoreSpec>
45
+ #endif
46
+ @property (nonatomic, nullable) LBRNFatalRecordStore *store;
47
+ @end
48
+
49
+ @implementation LBRNFatalStoreModule
50
+
51
+ RCT_EXPORT_MODULE(LogBrewFatalStore)
52
+
53
+ + (BOOL)requiresMainQueueSetup
54
+ {
55
+ return NO;
56
+ }
57
+
58
+ - (instancetype)init
59
+ {
60
+ self = [super init];
61
+ if (self != nil) {
62
+ NSURL *baseURL =
63
+ [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory
64
+ inDomains:NSUserDomainMask].firstObject;
65
+ if (baseURL != nil) {
66
+ NSURL *directoryURL = [baseURL URLByAppendingPathComponent:@"LogBrewFatalJS"
67
+ isDirectory:YES];
68
+ LBRNFatalDirectoryPreparation directoryPreparation = ^BOOL(NSURL *preparedURL) {
69
+ NSError *writeError = nil;
70
+ if (![preparedURL setResourceValue:@YES
71
+ forKey:NSURLIsExcludedFromBackupKey
72
+ error:&writeError]
73
+ || writeError != nil) {
74
+ return NO;
75
+ }
76
+ NSNumber *excluded = nil;
77
+ NSError *readError = nil;
78
+ return [preparedURL getResourceValue:&excluded
79
+ forKey:NSURLIsExcludedFromBackupKey
80
+ error:&readError]
81
+ && readError == nil && excluded.boolValue;
82
+ };
83
+ _store = [[LBRNFatalRecordStore alloc]
84
+ initWithDirectoryURL:directoryURL
85
+ directoryPreparation:directoryPreparation];
86
+ }
87
+ }
88
+ return self;
89
+ }
90
+
91
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(writeFatalRecord:(NSDictionary *)record)
92
+ {
93
+ if (self.store == nil) {
94
+ return LBRNStorageError();
95
+ }
96
+ @try {
97
+ return [self.store writeRecord:LBRNNormalizeRecord(record)];
98
+ } @catch (__unused NSException *exception) {
99
+ return LBRNStorageError();
100
+ }
101
+ }
102
+
103
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(readFatalRecord)
104
+ {
105
+ if (self.store == nil) {
106
+ return LBRNStorageError();
107
+ }
108
+ @try {
109
+ return [self.store readRecord];
110
+ } @catch (__unused NSException *exception) {
111
+ return LBRNStorageError();
112
+ }
113
+ }
114
+
115
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(acknowledgeFatalRecord:(NSString *)recordId)
116
+ {
117
+ if (self.store == nil || ![recordId isKindOfClass:[NSString class]]) {
118
+ return LBRNStorageError();
119
+ }
120
+ @try {
121
+ return [self.store acknowledgeRecordId:recordId];
122
+ } @catch (__unused NSException *exception) {
123
+ return LBRNStorageError();
124
+ }
125
+ }
126
+
127
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(discardFatalRecord)
128
+ {
129
+ if (self.store == nil) {
130
+ return LBRNStorageError();
131
+ }
132
+ @try {
133
+ return [self.store discardRecord];
134
+ } @catch (__unused NSException *exception) {
135
+ return LBRNStorageError();
136
+ }
137
+ }
138
+
139
+ #ifdef RCT_NEW_ARCH_ENABLED
140
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
141
+ (const facebook::react::ObjCTurboModule::InitParams &)params
142
+ {
143
+ return std::make_shared<facebook::react::NativeLogBrewFatalStoreSpecJSI>(params);
144
+ }
145
+ #endif
146
+
147
+ @end