@logbrew/react-native 0.1.10 → 0.1.11
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.
- package/README.md +68 -1
- package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +611 -0
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +129 -0
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
- package/index.cjs +2 -0
- package/index.d.cts +3 -0
- package/index.d.ts +3 -0
- package/index.js +2 -0
- package/index.native.d.ts +43 -1
- package/index.native.js +68 -1
- package/ios/LBRNEventRecordStore.h +29 -0
- package/ios/LBRNEventRecordStore.m +746 -0
- package/ios/LBRNFatalStoreModule.mm +159 -14
- package/ios/LBRNPrivateStorage.h +7 -0
- package/ios/LBRNPrivateStorage.m +38 -0
- package/package.json +7 -2
- package/persistent-delivery.native.js +282 -0
- package/src/NativeLogBrewFatalStore.ts +9 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
#import "LBRNEventRecordStore.h"
|
|
2
|
+
|
|
3
|
+
#import <dirent.h>
|
|
4
|
+
#import <errno.h>
|
|
5
|
+
#import <fcntl.h>
|
|
6
|
+
#import <limits.h>
|
|
7
|
+
#import <math.h>
|
|
8
|
+
#import <stdint.h>
|
|
9
|
+
#import <stdlib.h>
|
|
10
|
+
#import <string.h>
|
|
11
|
+
#import <sys/stat.h>
|
|
12
|
+
#import <unistd.h>
|
|
13
|
+
|
|
14
|
+
NSString *const LBRNEventRecordPrefix = @"event-";
|
|
15
|
+
NSString *const LBRNEventRecordSuffix = @".record";
|
|
16
|
+
NSString *const LBRNEventMarkerPrefix = @"accepted-";
|
|
17
|
+
NSString *const LBRNEventMarkerSuffix = @".marker";
|
|
18
|
+
|
|
19
|
+
static const NSUInteger LBRNEventMaximumBytes = 4 * 1024 * 1024;
|
|
20
|
+
static const NSUInteger LBRNEventMaximumQueueBytes = 4 * 1024 * 1024;
|
|
21
|
+
static const NSUInteger LBRNEventMaximumQueueRecords = 1000;
|
|
22
|
+
static const NSInteger LBRNEventFileSchemaVersion = 1;
|
|
23
|
+
|
|
24
|
+
@interface LBRNEventRecordStore ()
|
|
25
|
+
@property (nonatomic, readonly) NSURL *directoryURL;
|
|
26
|
+
@property (nonatomic, readonly, copy)
|
|
27
|
+
LBRNEventDirectoryPreparation directoryPreparation;
|
|
28
|
+
@property (nonatomic) BOOL poisoned;
|
|
29
|
+
@end
|
|
30
|
+
|
|
31
|
+
@implementation LBRNEventRecordStore
|
|
32
|
+
|
|
33
|
+
- (instancetype)initWithDirectoryURL:(NSURL *)directoryURL
|
|
34
|
+
{
|
|
35
|
+
return [self initWithDirectoryURL:directoryURL
|
|
36
|
+
directoryPreparation:^BOOL(__unused NSURL *preparedURL) {
|
|
37
|
+
return NO;
|
|
38
|
+
}];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
- (instancetype)initWithDirectoryURL:(NSURL *)directoryURL
|
|
42
|
+
directoryPreparation:(LBRNEventDirectoryPreparation)directoryPreparation
|
|
43
|
+
{
|
|
44
|
+
self = [super init];
|
|
45
|
+
if (self != nil) {
|
|
46
|
+
_directoryURL = directoryURL;
|
|
47
|
+
_directoryPreparation = [directoryPreparation copy];
|
|
48
|
+
_poisoned = NO;
|
|
49
|
+
}
|
|
50
|
+
return self;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (NSDictionary *)loadRecords
|
|
54
|
+
{
|
|
55
|
+
@synchronized(self) {
|
|
56
|
+
int directoryFD = [self openPreparedDirectory];
|
|
57
|
+
if (directoryFD < 0) {
|
|
58
|
+
return [self storageError];
|
|
59
|
+
}
|
|
60
|
+
@try {
|
|
61
|
+
NSDictionary *snapshot = [self snapshotFromDirectory:directoryFD];
|
|
62
|
+
return snapshot == nil
|
|
63
|
+
? [self storageError]
|
|
64
|
+
: @{ @"status" : @"loaded", @"records" : snapshot[@"records"] };
|
|
65
|
+
} @finally {
|
|
66
|
+
close(directoryFD);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
- (NSDictionary *)appendSerializedEvent:(NSString *)serializedEvent
|
|
72
|
+
eventBytes:(NSNumber *)eventBytes
|
|
73
|
+
{
|
|
74
|
+
@synchronized(self) {
|
|
75
|
+
NSData *eventData = [self validatedEventData:serializedEvent eventBytes:eventBytes];
|
|
76
|
+
if (self.poisoned || eventData == nil) {
|
|
77
|
+
return [self storageError];
|
|
78
|
+
}
|
|
79
|
+
int directoryFD = [self openPreparedDirectory];
|
|
80
|
+
if (directoryFD < 0) {
|
|
81
|
+
return [self storageError];
|
|
82
|
+
}
|
|
83
|
+
@try {
|
|
84
|
+
NSDictionary *snapshot = [self snapshotFromDirectory:directoryFD];
|
|
85
|
+
if (snapshot == nil
|
|
86
|
+
|| [snapshot[@"records"] count] >= LBRNEventMaximumQueueRecords
|
|
87
|
+
|| [snapshot[@"totalEventBytes"] unsignedLongLongValue] + eventData.length
|
|
88
|
+
> LBRNEventMaximumQueueBytes) {
|
|
89
|
+
return [self storageError];
|
|
90
|
+
}
|
|
91
|
+
unsigned long long sequence = MAX(
|
|
92
|
+
[snapshot[@"markerSequence"] unsignedLongLongValue],
|
|
93
|
+
[snapshot[@"maximumRecordSequence"] unsignedLongLongValue]);
|
|
94
|
+
if (sequence >= LLONG_MAX) {
|
|
95
|
+
return [self storageError];
|
|
96
|
+
}
|
|
97
|
+
sequence += 1;
|
|
98
|
+
NSDictionary *record = @{
|
|
99
|
+
@"schemaVersion" : @(LBRNEventFileSchemaVersion),
|
|
100
|
+
@"sequence" : @(sequence),
|
|
101
|
+
@"serializedEvent" : serializedEvent,
|
|
102
|
+
@"eventBytes" : eventBytes,
|
|
103
|
+
};
|
|
104
|
+
NSData *data = [self propertyListData:record];
|
|
105
|
+
NSString *filename = [self recordFilename:sequence];
|
|
106
|
+
if (data == nil
|
|
107
|
+
|| data.length > LBRNEventMaximumBytes + 4096
|
|
108
|
+
|| ![self atomicallyWriteData:data filename:filename directoryFD:directoryFD]) {
|
|
109
|
+
return [self storageError];
|
|
110
|
+
}
|
|
111
|
+
return @{ @"status" : @"appended" };
|
|
112
|
+
} @finally {
|
|
113
|
+
close(directoryFD);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
- (NSDictionary *)acknowledgeRecordCount:(NSNumber *)count
|
|
119
|
+
{
|
|
120
|
+
@synchronized(self) {
|
|
121
|
+
NSUInteger recordCount = 0;
|
|
122
|
+
if (self.poisoned || ![self nonnegativeInteger:count output:&recordCount]) {
|
|
123
|
+
return [self storageError];
|
|
124
|
+
}
|
|
125
|
+
int directoryFD = [self openPreparedDirectory];
|
|
126
|
+
if (directoryFD < 0) {
|
|
127
|
+
return [self storageError];
|
|
128
|
+
}
|
|
129
|
+
@try {
|
|
130
|
+
NSDictionary *snapshot = [self snapshotFromDirectory:directoryFD];
|
|
131
|
+
NSArray *records = snapshot[@"records"];
|
|
132
|
+
if (snapshot == nil || recordCount > records.count) {
|
|
133
|
+
return [self storageError];
|
|
134
|
+
}
|
|
135
|
+
if (recordCount == 0) {
|
|
136
|
+
return @{ @"status" : @"acknowledged" };
|
|
137
|
+
}
|
|
138
|
+
unsigned long long acceptedSequence =
|
|
139
|
+
[records[recordCount - 1][@"sequence"] unsignedLongLongValue];
|
|
140
|
+
if (![self commitMarker:acceptedSequence directoryFD:directoryFD]) {
|
|
141
|
+
return [self storageError];
|
|
142
|
+
}
|
|
143
|
+
[self removeAcceptedRecords:acceptedSequence directoryFD:directoryFD];
|
|
144
|
+
return @{ @"status" : @"acknowledged" };
|
|
145
|
+
} @finally {
|
|
146
|
+
close(directoryFD);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
- (NSDictionary *)purgeRecords
|
|
152
|
+
{
|
|
153
|
+
@synchronized(self) {
|
|
154
|
+
if (self.poisoned) {
|
|
155
|
+
return [self storageError];
|
|
156
|
+
}
|
|
157
|
+
int directoryFD = [self openPreparedDirectory];
|
|
158
|
+
if (directoryFD < 0) {
|
|
159
|
+
return [self storageError];
|
|
160
|
+
}
|
|
161
|
+
@try {
|
|
162
|
+
if (![self removeStaleTemporaryFiles:directoryFD]) {
|
|
163
|
+
return [self storageError];
|
|
164
|
+
}
|
|
165
|
+
NSArray *entries = [self queueEntries:directoryFD];
|
|
166
|
+
if (entries == nil) {
|
|
167
|
+
return [self storageError];
|
|
168
|
+
}
|
|
169
|
+
unsigned long long maximumSequence = 0;
|
|
170
|
+
for (NSDictionary *entry in entries) {
|
|
171
|
+
if (![self safeRegularFilename:entry[@"name"] directoryFD:directoryFD]) {
|
|
172
|
+
return [self storageError];
|
|
173
|
+
}
|
|
174
|
+
maximumSequence = MAX(
|
|
175
|
+
maximumSequence, [entry[@"sequence"] unsignedLongLongValue]);
|
|
176
|
+
}
|
|
177
|
+
if (maximumSequence == 0) {
|
|
178
|
+
return @{ @"status" : @"purged" };
|
|
179
|
+
}
|
|
180
|
+
if (maximumSequence >= LLONG_MAX) {
|
|
181
|
+
return [self storageError];
|
|
182
|
+
}
|
|
183
|
+
unsigned long long acceptedSequence = maximumSequence + 1;
|
|
184
|
+
if (![self commitMarker:acceptedSequence directoryFD:directoryFD]) {
|
|
185
|
+
return [self storageError];
|
|
186
|
+
}
|
|
187
|
+
[self removeAcceptedRecords:acceptedSequence directoryFD:directoryFD];
|
|
188
|
+
[self removeMarkersBefore:acceptedSequence directoryFD:directoryFD];
|
|
189
|
+
return @{ @"status" : @"purged" };
|
|
190
|
+
} @finally {
|
|
191
|
+
close(directoryFD);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
- (NSDictionary *)closeStore
|
|
197
|
+
{
|
|
198
|
+
@synchronized(self) {
|
|
199
|
+
return @{ @"status" : self.poisoned ? @"storage_error" : @"closed" };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
- (NSDictionary *)snapshotFromDirectory:(int)directoryFD
|
|
204
|
+
{
|
|
205
|
+
if (self.poisoned || ![self removeStaleTemporaryFiles:directoryFD]) {
|
|
206
|
+
return nil;
|
|
207
|
+
}
|
|
208
|
+
NSArray<NSDictionary *> *entries = [self queueEntries:directoryFD];
|
|
209
|
+
if (entries == nil) {
|
|
210
|
+
return nil;
|
|
211
|
+
}
|
|
212
|
+
NSMutableArray<NSDictionary *> *records = [NSMutableArray array];
|
|
213
|
+
NSMutableArray<NSDictionary *> *markers = [NSMutableArray array];
|
|
214
|
+
unsigned long long markerSequence = 0;
|
|
215
|
+
unsigned long long maximumRecordSequence = 0;
|
|
216
|
+
for (NSDictionary *entry in entries) {
|
|
217
|
+
NSString *name = entry[@"name"];
|
|
218
|
+
if (![self safeRegularFilename:name directoryFD:directoryFD]) {
|
|
219
|
+
return nil;
|
|
220
|
+
}
|
|
221
|
+
unsigned long long sequence = [entry[@"sequence"] unsignedLongLongValue];
|
|
222
|
+
if ([entry[@"kind"] isEqualToString:@"marker"]) {
|
|
223
|
+
markerSequence = MAX(markerSequence, sequence);
|
|
224
|
+
[markers addObject:entry];
|
|
225
|
+
} else {
|
|
226
|
+
maximumRecordSequence = MAX(maximumRecordSequence, sequence);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (markerSequence > 0
|
|
230
|
+
&& ![self validMarkerFilename:[self markerFilename:markerSequence]
|
|
231
|
+
sequence:markerSequence
|
|
232
|
+
directoryFD:directoryFD]) {
|
|
233
|
+
return nil;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
unsigned long long totalEventBytes = 0;
|
|
237
|
+
for (NSDictionary *entry in entries) {
|
|
238
|
+
if (![entry[@"kind"] isEqualToString:@"record"]) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
unsigned long long sequence = [entry[@"sequence"] unsignedLongLongValue];
|
|
242
|
+
if (sequence <= markerSequence) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
NSDictionary *record = [self readRecordFilename:entry[@"name"]
|
|
246
|
+
sequence:sequence
|
|
247
|
+
directoryFD:directoryFD];
|
|
248
|
+
if (record == nil) {
|
|
249
|
+
return nil;
|
|
250
|
+
}
|
|
251
|
+
totalEventBytes += [record[@"eventBytes"] unsignedLongLongValue];
|
|
252
|
+
if (records.count >= LBRNEventMaximumQueueRecords
|
|
253
|
+
|| totalEventBytes > LBRNEventMaximumQueueBytes) {
|
|
254
|
+
return nil;
|
|
255
|
+
}
|
|
256
|
+
[records addObject:record];
|
|
257
|
+
}
|
|
258
|
+
[records sortUsingComparator:^NSComparisonResult(NSDictionary *left, NSDictionary *right) {
|
|
259
|
+
return [left[@"sequence"] compare:right[@"sequence"]];
|
|
260
|
+
}];
|
|
261
|
+
[self removeAcceptedRecords:markerSequence directoryFD:directoryFD];
|
|
262
|
+
[self removeMarkerEntries:markers newestSequence:markerSequence directoryFD:directoryFD];
|
|
263
|
+
return @{
|
|
264
|
+
@"markerSequence" : @(markerSequence),
|
|
265
|
+
@"maximumRecordSequence" : @(maximumRecordSequence),
|
|
266
|
+
@"totalEventBytes" : @(totalEventBytes),
|
|
267
|
+
@"records" : records,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
- (NSArray<NSDictionary *> *)queueEntries:(int)directoryFD
|
|
272
|
+
{
|
|
273
|
+
int duplicateFD = openat(
|
|
274
|
+
directoryFD, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
|
|
275
|
+
if (duplicateFD < 0) {
|
|
276
|
+
return nil;
|
|
277
|
+
}
|
|
278
|
+
DIR *stream = fdopendir(duplicateFD);
|
|
279
|
+
if (stream == NULL) {
|
|
280
|
+
close(duplicateFD);
|
|
281
|
+
return nil;
|
|
282
|
+
}
|
|
283
|
+
NSMutableArray<NSDictionary *> *entries = [NSMutableArray array];
|
|
284
|
+
BOOL valid = YES;
|
|
285
|
+
errno = 0;
|
|
286
|
+
struct dirent *entry;
|
|
287
|
+
while ((entry = readdir(stream)) != NULL) {
|
|
288
|
+
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
NSString *name = [NSString stringWithUTF8String:entry->d_name];
|
|
292
|
+
NSString *kind = nil;
|
|
293
|
+
unsigned long long sequence = 0;
|
|
294
|
+
if (name == nil
|
|
295
|
+
|| ![self parseQueueFilename:name kind:&kind sequence:&sequence]) {
|
|
296
|
+
valid = NO;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
[entries addObject:@{ @"name" : name, @"kind" : kind, @"sequence" : @(sequence) }];
|
|
300
|
+
}
|
|
301
|
+
if (entry == NULL && errno != 0) {
|
|
302
|
+
valid = NO;
|
|
303
|
+
}
|
|
304
|
+
closedir(stream);
|
|
305
|
+
if (!valid) {
|
|
306
|
+
return nil;
|
|
307
|
+
}
|
|
308
|
+
[entries sortUsingComparator:^NSComparisonResult(NSDictionary *left, NSDictionary *right) {
|
|
309
|
+
NSComparisonResult sequenceOrder = [left[@"sequence"] compare:right[@"sequence"]];
|
|
310
|
+
return sequenceOrder == NSOrderedSame
|
|
311
|
+
? [left[@"kind"] compare:right[@"kind"]]
|
|
312
|
+
: sequenceOrder;
|
|
313
|
+
}];
|
|
314
|
+
return entries;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
- (BOOL)parseQueueFilename:(NSString *)name
|
|
318
|
+
kind:(NSString *_Nullable *_Nonnull)kind
|
|
319
|
+
sequence:(unsigned long long *)sequence
|
|
320
|
+
{
|
|
321
|
+
NSString *prefix = nil;
|
|
322
|
+
NSString *suffix = nil;
|
|
323
|
+
NSString *resolvedKind = nil;
|
|
324
|
+
if ([name hasPrefix:LBRNEventRecordPrefix] && [name hasSuffix:LBRNEventRecordSuffix]) {
|
|
325
|
+
prefix = LBRNEventRecordPrefix;
|
|
326
|
+
suffix = LBRNEventRecordSuffix;
|
|
327
|
+
resolvedKind = @"record";
|
|
328
|
+
} else if ([name hasPrefix:LBRNEventMarkerPrefix]
|
|
329
|
+
&& [name hasSuffix:LBRNEventMarkerSuffix]) {
|
|
330
|
+
prefix = LBRNEventMarkerPrefix;
|
|
331
|
+
suffix = LBRNEventMarkerSuffix;
|
|
332
|
+
resolvedKind = @"marker";
|
|
333
|
+
} else {
|
|
334
|
+
return NO;
|
|
335
|
+
}
|
|
336
|
+
NSUInteger digitsLength = name.length - prefix.length - suffix.length;
|
|
337
|
+
if (digitsLength != 20) {
|
|
338
|
+
return NO;
|
|
339
|
+
}
|
|
340
|
+
NSString *digits = [name substringWithRange:NSMakeRange(prefix.length, digitsLength)];
|
|
341
|
+
for (NSUInteger index = 0; index < digits.length; index += 1) {
|
|
342
|
+
unichar value = [digits characterAtIndex:index];
|
|
343
|
+
if (value < '0' || value > '9') {
|
|
344
|
+
return NO;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
errno = 0;
|
|
348
|
+
char *end = NULL;
|
|
349
|
+
unsigned long long value = strtoull(digits.UTF8String, &end, 10);
|
|
350
|
+
if (errno != 0 || end == NULL || *end != '\0' || value == 0 || value > LLONG_MAX) {
|
|
351
|
+
return NO;
|
|
352
|
+
}
|
|
353
|
+
*kind = resolvedKind;
|
|
354
|
+
*sequence = value;
|
|
355
|
+
return YES;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
- (NSDictionary *)readRecordFilename:(NSString *)filename
|
|
359
|
+
sequence:(unsigned long long)sequence
|
|
360
|
+
directoryFD:(int)directoryFD
|
|
361
|
+
{
|
|
362
|
+
NSData *data = [self readFilename:filename
|
|
363
|
+
maximumBytes:LBRNEventMaximumBytes + 4096
|
|
364
|
+
directoryFD:directoryFD];
|
|
365
|
+
NSDictionary *record = [self propertyListDictionary:data];
|
|
366
|
+
if (record == nil || record.count != 4
|
|
367
|
+
|| [record[@"schemaVersion"] integerValue] != LBRNEventFileSchemaVersion
|
|
368
|
+
|| [record[@"sequence"] unsignedLongLongValue] != sequence
|
|
369
|
+
|| ![record[@"serializedEvent"] isKindOfClass:[NSString class]]) {
|
|
370
|
+
return nil;
|
|
371
|
+
}
|
|
372
|
+
NSData *eventData = [self validatedEventData:record[@"serializedEvent"]
|
|
373
|
+
eventBytes:record[@"eventBytes"]];
|
|
374
|
+
if (eventData == nil) {
|
|
375
|
+
return nil;
|
|
376
|
+
}
|
|
377
|
+
return @{
|
|
378
|
+
@"sequence" : @(sequence),
|
|
379
|
+
@"serializedEvent" : record[@"serializedEvent"],
|
|
380
|
+
@"eventBytes" : record[@"eventBytes"],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
- (BOOL)validMarkerFilename:(NSString *)filename
|
|
385
|
+
sequence:(unsigned long long)sequence
|
|
386
|
+
directoryFD:(int)directoryFD
|
|
387
|
+
{
|
|
388
|
+
NSDictionary *marker = [self propertyListDictionary:[self readFilename:filename
|
|
389
|
+
maximumBytes:1024
|
|
390
|
+
directoryFD:directoryFD]];
|
|
391
|
+
return marker != nil && marker.count == 2
|
|
392
|
+
&& [marker[@"schemaVersion"] integerValue] == LBRNEventFileSchemaVersion
|
|
393
|
+
&& [marker[@"sequence"] unsignedLongLongValue] == sequence;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
- (BOOL)commitMarker:(unsigned long long)sequence directoryFD:(int)directoryFD
|
|
397
|
+
{
|
|
398
|
+
NSString *filename = [self markerFilename:sequence];
|
|
399
|
+
struct stat info;
|
|
400
|
+
if (fstatat(directoryFD, filename.UTF8String, &info, AT_SYMLINK_NOFOLLOW) == 0) {
|
|
401
|
+
return S_ISREG(info.st_mode)
|
|
402
|
+
&& [self validMarkerFilename:filename sequence:sequence directoryFD:directoryFD];
|
|
403
|
+
}
|
|
404
|
+
if (errno != ENOENT) {
|
|
405
|
+
return NO;
|
|
406
|
+
}
|
|
407
|
+
NSData *data = [self propertyListData:@{
|
|
408
|
+
@"schemaVersion" : @(LBRNEventFileSchemaVersion),
|
|
409
|
+
@"sequence" : @(sequence),
|
|
410
|
+
}];
|
|
411
|
+
return data != nil
|
|
412
|
+
&& [self atomicallyWriteData:data filename:filename directoryFD:directoryFD];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
- (void)removeAcceptedRecords:(unsigned long long)markerSequence
|
|
416
|
+
directoryFD:(int)directoryFD
|
|
417
|
+
{
|
|
418
|
+
if (markerSequence == 0) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
NSArray<NSDictionary *> *entries = [self queueEntries:directoryFD];
|
|
422
|
+
BOOL deleted = NO;
|
|
423
|
+
for (NSDictionary *entry in entries ?: @[]) {
|
|
424
|
+
if ([entry[@"kind"] isEqualToString:@"record"]
|
|
425
|
+
&& [entry[@"sequence"] unsignedLongLongValue] <= markerSequence
|
|
426
|
+
&& [self safeRegularFilename:entry[@"name"] directoryFD:directoryFD]
|
|
427
|
+
&& unlinkat(directoryFD, [entry[@"name"] UTF8String], 0) == 0) {
|
|
428
|
+
deleted = YES;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (deleted) {
|
|
432
|
+
[self synchronizeDirectory:directoryFD];
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
- (void)removeMarkersBefore:(unsigned long long)markerSequence
|
|
437
|
+
directoryFD:(int)directoryFD
|
|
438
|
+
{
|
|
439
|
+
NSArray<NSDictionary *> *entries = [self queueEntries:directoryFD];
|
|
440
|
+
NSMutableArray<NSDictionary *> *markers = [NSMutableArray array];
|
|
441
|
+
for (NSDictionary *entry in entries ?: @[]) {
|
|
442
|
+
if ([entry[@"kind"] isEqualToString:@"marker"]) {
|
|
443
|
+
[markers addObject:entry];
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
[self removeMarkerEntries:markers
|
|
447
|
+
newestSequence:markerSequence
|
|
448
|
+
directoryFD:directoryFD];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
- (void)removeMarkerEntries:(NSArray<NSDictionary *> *)markers
|
|
452
|
+
newestSequence:(unsigned long long)newestSequence
|
|
453
|
+
directoryFD:(int)directoryFD
|
|
454
|
+
{
|
|
455
|
+
BOOL deleted = NO;
|
|
456
|
+
for (NSDictionary *entry in markers) {
|
|
457
|
+
if ([entry[@"sequence"] unsignedLongLongValue] < newestSequence
|
|
458
|
+
&& [self safeRegularFilename:entry[@"name"] directoryFD:directoryFD]
|
|
459
|
+
&& unlinkat(directoryFD, [entry[@"name"] UTF8String], 0) == 0) {
|
|
460
|
+
deleted = YES;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (deleted) {
|
|
464
|
+
[self synchronizeDirectory:directoryFD];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
- (int)openPreparedDirectory
|
|
469
|
+
{
|
|
470
|
+
if (self.poisoned || !self.directoryURL.isFileURL) {
|
|
471
|
+
return -1;
|
|
472
|
+
}
|
|
473
|
+
const char *path = self.directoryURL.fileSystemRepresentation;
|
|
474
|
+
struct stat info;
|
|
475
|
+
if (lstat(path, &info) != 0) {
|
|
476
|
+
if (errno != ENOENT || mkdir(path, 0700) != 0) {
|
|
477
|
+
return -1;
|
|
478
|
+
}
|
|
479
|
+
} else if (!S_ISDIR(info.st_mode) || S_ISLNK(info.st_mode)) {
|
|
480
|
+
return -1;
|
|
481
|
+
}
|
|
482
|
+
int directoryFD = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
|
|
483
|
+
if (directoryFD < 0) {
|
|
484
|
+
return -1;
|
|
485
|
+
}
|
|
486
|
+
BOOL prepared = fstat(directoryFD, &info) == 0
|
|
487
|
+
&& S_ISDIR(info.st_mode)
|
|
488
|
+
&& fchmod(directoryFD, 0700) == 0;
|
|
489
|
+
@try {
|
|
490
|
+
prepared = prepared && self.directoryPreparation(self.directoryURL);
|
|
491
|
+
} @catch (__unused NSException *exception) {
|
|
492
|
+
prepared = NO;
|
|
493
|
+
}
|
|
494
|
+
if (!prepared || ![self directoryFDMatchesURL:directoryFD]) {
|
|
495
|
+
close(directoryFD);
|
|
496
|
+
return -1;
|
|
497
|
+
}
|
|
498
|
+
return directoryFD;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
- (BOOL)directoryFDMatchesURL:(int)directoryFD
|
|
502
|
+
{
|
|
503
|
+
struct stat openedInfo;
|
|
504
|
+
struct stat pathInfo;
|
|
505
|
+
if (fstat(directoryFD, &openedInfo) != 0
|
|
506
|
+
|| lstat(self.directoryURL.fileSystemRepresentation, &pathInfo) != 0) {
|
|
507
|
+
return NO;
|
|
508
|
+
}
|
|
509
|
+
return S_ISDIR(openedInfo.st_mode) && S_ISDIR(pathInfo.st_mode)
|
|
510
|
+
&& !S_ISLNK(pathInfo.st_mode) && openedInfo.st_dev == pathInfo.st_dev
|
|
511
|
+
&& openedInfo.st_ino == pathInfo.st_ino;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
- (BOOL)removeStaleTemporaryFiles:(int)directoryFD
|
|
515
|
+
{
|
|
516
|
+
int duplicateFD = openat(
|
|
517
|
+
directoryFD, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
|
|
518
|
+
if (duplicateFD < 0) {
|
|
519
|
+
return NO;
|
|
520
|
+
}
|
|
521
|
+
DIR *stream = fdopendir(duplicateFD);
|
|
522
|
+
if (stream == NULL) {
|
|
523
|
+
close(duplicateFD);
|
|
524
|
+
return NO;
|
|
525
|
+
}
|
|
526
|
+
BOOL succeeded = YES;
|
|
527
|
+
errno = 0;
|
|
528
|
+
struct dirent *entry;
|
|
529
|
+
while ((entry = readdir(stream)) != NULL) {
|
|
530
|
+
NSString *name = [NSString stringWithUTF8String:entry->d_name];
|
|
531
|
+
if (name == nil || ![name hasSuffix:@".tmp"]) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (![self safeRegularFilename:name directoryFD:directoryFD]
|
|
535
|
+
|| unlinkat(directoryFD, name.UTF8String, 0) != 0) {
|
|
536
|
+
succeeded = NO;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (entry == NULL && errno != 0) {
|
|
541
|
+
succeeded = NO;
|
|
542
|
+
}
|
|
543
|
+
closedir(stream);
|
|
544
|
+
return succeeded;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
- (BOOL)atomicallyWriteData:(NSData *)data
|
|
548
|
+
filename:(NSString *)filename
|
|
549
|
+
directoryFD:(int)directoryFD
|
|
550
|
+
{
|
|
551
|
+
NSString *temporary = [filename stringByAppendingString:@".tmp"];
|
|
552
|
+
struct stat info;
|
|
553
|
+
if (fstatat(directoryFD, filename.UTF8String, &info, AT_SYMLINK_NOFOLLOW) == 0
|
|
554
|
+
|| errno != ENOENT) {
|
|
555
|
+
return NO;
|
|
556
|
+
}
|
|
557
|
+
if (fstatat(directoryFD, temporary.UTF8String, &info, AT_SYMLINK_NOFOLLOW) == 0) {
|
|
558
|
+
if (!S_ISREG(info.st_mode)
|
|
559
|
+
|| unlinkat(directoryFD, temporary.UTF8String, 0) != 0) {
|
|
560
|
+
return NO;
|
|
561
|
+
}
|
|
562
|
+
} else if (errno != ENOENT) {
|
|
563
|
+
return NO;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
int fileFD = openat(
|
|
567
|
+
directoryFD,
|
|
568
|
+
temporary.UTF8String,
|
|
569
|
+
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW,
|
|
570
|
+
0600);
|
|
571
|
+
if (fileFD < 0) {
|
|
572
|
+
return NO;
|
|
573
|
+
}
|
|
574
|
+
BOOL succeeded = fchmod(fileFD, 0600) == 0;
|
|
575
|
+
NSUInteger offset = 0;
|
|
576
|
+
while (succeeded && offset < data.length) {
|
|
577
|
+
ssize_t count = write(
|
|
578
|
+
fileFD, (const uint8_t *)data.bytes + offset, data.length - offset);
|
|
579
|
+
if (count <= 0) {
|
|
580
|
+
succeeded = NO;
|
|
581
|
+
} else {
|
|
582
|
+
offset += (NSUInteger)count;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (succeeded && fsync(fileFD) != 0) {
|
|
586
|
+
succeeded = NO;
|
|
587
|
+
}
|
|
588
|
+
if (close(fileFD) != 0) {
|
|
589
|
+
succeeded = NO;
|
|
590
|
+
}
|
|
591
|
+
if (succeeded
|
|
592
|
+
&& renameat(
|
|
593
|
+
directoryFD,
|
|
594
|
+
temporary.UTF8String,
|
|
595
|
+
directoryFD,
|
|
596
|
+
filename.UTF8String)
|
|
597
|
+
!= 0) {
|
|
598
|
+
succeeded = NO;
|
|
599
|
+
}
|
|
600
|
+
if (succeeded && ![self synchronizeDirectory:directoryFD]) {
|
|
601
|
+
self.poisoned = YES;
|
|
602
|
+
succeeded = NO;
|
|
603
|
+
}
|
|
604
|
+
if (!succeeded) {
|
|
605
|
+
unlinkat(directoryFD, temporary.UTF8String, 0);
|
|
606
|
+
}
|
|
607
|
+
return succeeded;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
- (NSData *)readFilename:(NSString *)filename
|
|
611
|
+
maximumBytes:(NSUInteger)maximumBytes
|
|
612
|
+
directoryFD:(int)directoryFD
|
|
613
|
+
{
|
|
614
|
+
struct stat pathInfo;
|
|
615
|
+
if (fstatat(directoryFD, filename.UTF8String, &pathInfo, AT_SYMLINK_NOFOLLOW) != 0
|
|
616
|
+
|| !S_ISREG(pathInfo.st_mode) || S_ISLNK(pathInfo.st_mode)
|
|
617
|
+
|| pathInfo.st_size <= 0 || (uint64_t)pathInfo.st_size > maximumBytes) {
|
|
618
|
+
return nil;
|
|
619
|
+
}
|
|
620
|
+
int fileFD = openat(directoryFD, filename.UTF8String, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
|
|
621
|
+
if (fileFD < 0) {
|
|
622
|
+
return nil;
|
|
623
|
+
}
|
|
624
|
+
NSMutableData *data = [NSMutableData dataWithLength:(NSUInteger)pathInfo.st_size];
|
|
625
|
+
BOOL succeeded = YES;
|
|
626
|
+
NSUInteger offset = 0;
|
|
627
|
+
while (offset < data.length) {
|
|
628
|
+
ssize_t count = read(fileFD, (uint8_t *)data.mutableBytes + offset, data.length - offset);
|
|
629
|
+
if (count <= 0) {
|
|
630
|
+
succeeded = NO;
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
offset += (NSUInteger)count;
|
|
634
|
+
}
|
|
635
|
+
struct stat openedInfo;
|
|
636
|
+
if (fstat(fileFD, &openedInfo) != 0 || openedInfo.st_dev != pathInfo.st_dev
|
|
637
|
+
|| openedInfo.st_ino != pathInfo.st_ino || !S_ISREG(openedInfo.st_mode)
|
|
638
|
+
|| openedInfo.st_size != pathInfo.st_size) {
|
|
639
|
+
succeeded = NO;
|
|
640
|
+
}
|
|
641
|
+
close(fileFD);
|
|
642
|
+
return succeeded ? data : nil;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
- (BOOL)safeRegularFilename:(NSString *)filename directoryFD:(int)directoryFD
|
|
646
|
+
{
|
|
647
|
+
struct stat info;
|
|
648
|
+
return fstatat(directoryFD, filename.UTF8String, &info, AT_SYMLINK_NOFOLLOW) == 0
|
|
649
|
+
&& S_ISREG(info.st_mode) && !S_ISLNK(info.st_mode);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
- (BOOL)synchronizeDirectory:(int)directoryFD
|
|
653
|
+
{
|
|
654
|
+
if (fsync(directoryFD) == 0) {
|
|
655
|
+
return YES;
|
|
656
|
+
}
|
|
657
|
+
return errno == EINVAL || errno == ENOTSUP;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
- (NSData *)validatedEventData:(id)serializedEvent eventBytes:(id)eventBytes
|
|
661
|
+
{
|
|
662
|
+
if (![serializedEvent isKindOfClass:[NSString class]]
|
|
663
|
+
|| ![eventBytes isKindOfClass:[NSNumber class]]) {
|
|
664
|
+
return nil;
|
|
665
|
+
}
|
|
666
|
+
NSUInteger byteCount = 0;
|
|
667
|
+
if (![self positiveInteger:eventBytes output:&byteCount]
|
|
668
|
+
|| byteCount > LBRNEventMaximumBytes) {
|
|
669
|
+
return nil;
|
|
670
|
+
}
|
|
671
|
+
NSData *data = [serializedEvent dataUsingEncoding:NSUTF8StringEncoding];
|
|
672
|
+
return data.length == byteCount ? data : nil;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
- (BOOL)positiveInteger:(NSNumber *)value output:(NSUInteger *)output
|
|
676
|
+
{
|
|
677
|
+
if (strcmp(value.objCType, @encode(BOOL)) == 0) {
|
|
678
|
+
return NO;
|
|
679
|
+
}
|
|
680
|
+
double number = value.doubleValue;
|
|
681
|
+
if (!isfinite(number) || number <= 0 || number > INT_MAX || floor(number) != number) {
|
|
682
|
+
return NO;
|
|
683
|
+
}
|
|
684
|
+
*output = (NSUInteger)number;
|
|
685
|
+
return YES;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
- (BOOL)nonnegativeInteger:(NSNumber *)value output:(NSUInteger *)output
|
|
689
|
+
{
|
|
690
|
+
if (![value isKindOfClass:[NSNumber class]]
|
|
691
|
+
|| strcmp(value.objCType, @encode(BOOL)) == 0) {
|
|
692
|
+
return NO;
|
|
693
|
+
}
|
|
694
|
+
double number = value.doubleValue;
|
|
695
|
+
if (!isfinite(number) || number < 0 || number > INT_MAX || floor(number) != number) {
|
|
696
|
+
return NO;
|
|
697
|
+
}
|
|
698
|
+
*output = (NSUInteger)number;
|
|
699
|
+
return YES;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
- (NSData *)propertyListData:(NSDictionary *)value
|
|
703
|
+
{
|
|
704
|
+
NSError *error = nil;
|
|
705
|
+
NSData *data = [NSPropertyListSerialization dataWithPropertyList:value
|
|
706
|
+
format:NSPropertyListBinaryFormat_v1_0
|
|
707
|
+
options:0
|
|
708
|
+
error:&error];
|
|
709
|
+
return error == nil ? data : nil;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
- (NSDictionary *)propertyListDictionary:(NSData *)data
|
|
713
|
+
{
|
|
714
|
+
if (data == nil) {
|
|
715
|
+
return nil;
|
|
716
|
+
}
|
|
717
|
+
NSError *error = nil;
|
|
718
|
+
id value = [NSPropertyListSerialization propertyListWithData:data
|
|
719
|
+
options:NSPropertyListImmutable
|
|
720
|
+
format:nil
|
|
721
|
+
error:&error];
|
|
722
|
+
return error == nil && [value isKindOfClass:[NSDictionary class]] ? value : nil;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
- (NSString *)recordFilename:(unsigned long long)sequence
|
|
726
|
+
{
|
|
727
|
+
return [NSString stringWithFormat:@"%@%020llu%@",
|
|
728
|
+
LBRNEventRecordPrefix,
|
|
729
|
+
sequence,
|
|
730
|
+
LBRNEventRecordSuffix];
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
- (NSString *)markerFilename:(unsigned long long)sequence
|
|
734
|
+
{
|
|
735
|
+
return [NSString stringWithFormat:@"%@%020llu%@",
|
|
736
|
+
LBRNEventMarkerPrefix,
|
|
737
|
+
sequence,
|
|
738
|
+
LBRNEventMarkerSuffix];
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
- (NSDictionary *)storageError
|
|
742
|
+
{
|
|
743
|
+
return @{ @"status" : @"storage_error" };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
@end
|