@logbrew/react-native 0.1.9 → 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 +111 -2
- 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/global-errors.cjs +4 -2
- package/global-errors.d.cts +63 -0
- package/global-errors.d.ts +63 -0
- package/global-errors.js +2 -1
- package/global-errors.native.js +63 -2
- 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 +47 -2
- package/index.native.js +70 -2
- 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/promise-rejections.cjs +128 -4
- package/src/NativeLogBrewFatalStore.ts +9 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
package co.logbrew.reactnative;
|
|
2
|
+
|
|
3
|
+
import java.io.ByteArrayInputStream;
|
|
4
|
+
import java.io.ByteArrayOutputStream;
|
|
5
|
+
import java.io.DataInputStream;
|
|
6
|
+
import java.io.DataOutputStream;
|
|
7
|
+
import java.io.File;
|
|
8
|
+
import java.io.FileInputStream;
|
|
9
|
+
import java.io.FileOutputStream;
|
|
10
|
+
import java.io.IOException;
|
|
11
|
+
import java.nio.charset.StandardCharsets;
|
|
12
|
+
import java.util.ArrayList;
|
|
13
|
+
import java.util.Collections;
|
|
14
|
+
import java.util.Comparator;
|
|
15
|
+
import java.util.List;
|
|
16
|
+
import java.util.regex.Matcher;
|
|
17
|
+
import java.util.regex.Pattern;
|
|
18
|
+
import java.util.zip.CRC32;
|
|
19
|
+
|
|
20
|
+
/** Crash-safe app-private queue backing the synchronous React Native eventStore adapter. */
|
|
21
|
+
final class EventRecordStore {
|
|
22
|
+
static final String EVENT_PREFIX = "event-";
|
|
23
|
+
static final String EVENT_SUFFIX = ".record";
|
|
24
|
+
static final String MARKER_PREFIX = "accepted-";
|
|
25
|
+
static final String MARKER_SUFFIX = ".marker";
|
|
26
|
+
|
|
27
|
+
private static final int RECORD_MAGIC = 0x4c425145;
|
|
28
|
+
private static final int MARKER_MAGIC = 0x4c42514d;
|
|
29
|
+
private static final int FILE_FORMAT_VERSION = 1;
|
|
30
|
+
private static final int MAX_EVENT_BYTES = 4 * 1024 * 1024;
|
|
31
|
+
private static final int MAX_QUEUE_BYTES = 4 * 1024 * 1024;
|
|
32
|
+
private static final int MAX_QUEUE_RECORDS = 1000;
|
|
33
|
+
private static final Pattern EVENT_PATTERN =
|
|
34
|
+
Pattern.compile("^event-([0-9]{20})\\.record$");
|
|
35
|
+
private static final Pattern MARKER_PATTERN =
|
|
36
|
+
Pattern.compile("^accepted-([0-9]{20})\\.marker$");
|
|
37
|
+
private static final FatalRecordStore.ParentDirectorySync UNSUPPORTED_PARENT_DIRECTORY_SYNC =
|
|
38
|
+
directory -> FatalRecordStore.ParentDirectorySyncResult.UNSUPPORTED;
|
|
39
|
+
|
|
40
|
+
private final File directory;
|
|
41
|
+
private final FatalRecordStore.ParentDirectorySync parentDirectorySync;
|
|
42
|
+
private boolean poisoned;
|
|
43
|
+
|
|
44
|
+
EventRecordStore(File directory) {
|
|
45
|
+
this(directory, UNSUPPORTED_PARENT_DIRECTORY_SYNC);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
EventRecordStore(
|
|
49
|
+
File directory, FatalRecordStore.ParentDirectorySync parentDirectorySync) {
|
|
50
|
+
this.directory = resolveCanonicalParent(directory);
|
|
51
|
+
this.parentDirectorySync = parentDirectorySync;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
synchronized Result load() {
|
|
55
|
+
Snapshot snapshot = snapshot();
|
|
56
|
+
return snapshot == null
|
|
57
|
+
? Result.status("storage_error")
|
|
58
|
+
: new Result("loaded", snapshot.records);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
synchronized Result append(String serializedEvent, int eventBytes) {
|
|
62
|
+
if (poisoned || !validEvent(serializedEvent, eventBytes)) {
|
|
63
|
+
return Result.status("storage_error");
|
|
64
|
+
}
|
|
65
|
+
Snapshot snapshot = snapshot();
|
|
66
|
+
if (snapshot == null
|
|
67
|
+
|| snapshot.records.size() >= MAX_QUEUE_RECORDS
|
|
68
|
+
|| snapshot.totalEventBytes + (long) eventBytes > MAX_QUEUE_BYTES) {
|
|
69
|
+
return Result.status("storage_error");
|
|
70
|
+
}
|
|
71
|
+
long sequence = Math.max(snapshot.markerSequence, snapshot.maximumRecordSequence);
|
|
72
|
+
if (sequence == Long.MAX_VALUE) {
|
|
73
|
+
return Result.status("storage_error");
|
|
74
|
+
}
|
|
75
|
+
sequence += 1;
|
|
76
|
+
File recordFile = new File(directory, eventFileName(sequence));
|
|
77
|
+
if (!atomicWrite(recordFile, encodeRecord(sequence, serializedEvent, eventBytes))) {
|
|
78
|
+
return Result.status("storage_error");
|
|
79
|
+
}
|
|
80
|
+
return Result.status("appended");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
synchronized Result acknowledge(int count) {
|
|
84
|
+
if (poisoned || count < 0) {
|
|
85
|
+
return Result.status("storage_error");
|
|
86
|
+
}
|
|
87
|
+
Snapshot snapshot = snapshot();
|
|
88
|
+
if (snapshot == null || count > snapshot.records.size()) {
|
|
89
|
+
return Result.status("storage_error");
|
|
90
|
+
}
|
|
91
|
+
if (count == 0) {
|
|
92
|
+
return Result.status("acknowledged");
|
|
93
|
+
}
|
|
94
|
+
long acceptedSequence = snapshot.records.get(count - 1).sequence;
|
|
95
|
+
if (!commitMarker(acceptedSequence)) {
|
|
96
|
+
return Result.status("storage_error");
|
|
97
|
+
}
|
|
98
|
+
removeAcceptedFiles(acceptedSequence);
|
|
99
|
+
return Result.status("acknowledged");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
synchronized Result purge() {
|
|
103
|
+
if (poisoned || !prepareDirectory() || !removeStaleTemporaryFiles()) {
|
|
104
|
+
return Result.status("storage_error");
|
|
105
|
+
}
|
|
106
|
+
File[] files = directory.listFiles();
|
|
107
|
+
if (files == null) {
|
|
108
|
+
return Result.status("storage_error");
|
|
109
|
+
}
|
|
110
|
+
long maximumSequence = 0;
|
|
111
|
+
for (File file : files) {
|
|
112
|
+
Matcher eventMatcher = EVENT_PATTERN.matcher(file.getName());
|
|
113
|
+
Matcher markerMatcher = MARKER_PATTERN.matcher(file.getName());
|
|
114
|
+
Matcher matcher = eventMatcher.matches() ? eventMatcher : markerMatcher.matches() ? markerMatcher : null;
|
|
115
|
+
if (matcher == null || !isSafeRegularFile(file)) {
|
|
116
|
+
return Result.status("storage_error");
|
|
117
|
+
}
|
|
118
|
+
Long sequence = parseSequence(matcher.group(1));
|
|
119
|
+
if (sequence == null) {
|
|
120
|
+
return Result.status("storage_error");
|
|
121
|
+
}
|
|
122
|
+
maximumSequence = Math.max(maximumSequence, sequence);
|
|
123
|
+
}
|
|
124
|
+
if (maximumSequence == 0) {
|
|
125
|
+
return Result.status("purged");
|
|
126
|
+
}
|
|
127
|
+
if (maximumSequence == Long.MAX_VALUE) {
|
|
128
|
+
return Result.status("storage_error");
|
|
129
|
+
}
|
|
130
|
+
long acceptedSequence = maximumSequence + 1;
|
|
131
|
+
if (!commitMarker(acceptedSequence)) {
|
|
132
|
+
return Result.status("storage_error");
|
|
133
|
+
}
|
|
134
|
+
removeAcceptedFiles(acceptedSequence);
|
|
135
|
+
removeAllOlderMarkers(acceptedSequence);
|
|
136
|
+
return Result.status("purged");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
synchronized Result close() {
|
|
140
|
+
return Result.status(poisoned ? "storage_error" : "closed");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private Snapshot snapshot() {
|
|
144
|
+
if (poisoned || !prepareDirectory() || !removeStaleTemporaryFiles()) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
File[] files = directory.listFiles();
|
|
148
|
+
if (files == null) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
long markerSequence = 0;
|
|
153
|
+
List<FileSequence> eventFiles = new ArrayList<>();
|
|
154
|
+
List<FileSequence> markerFiles = new ArrayList<>();
|
|
155
|
+
for (File file : files) {
|
|
156
|
+
String name = file.getName();
|
|
157
|
+
Matcher eventMatcher = EVENT_PATTERN.matcher(name);
|
|
158
|
+
Matcher markerMatcher = MARKER_PATTERN.matcher(name);
|
|
159
|
+
if (eventMatcher.matches()) {
|
|
160
|
+
Long sequence = parseSequence(eventMatcher.group(1));
|
|
161
|
+
if (sequence == null || !isSafeRegularFile(file)) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
eventFiles.add(new FileSequence(file, sequence));
|
|
165
|
+
} else if (markerMatcher.matches()) {
|
|
166
|
+
Long sequence = parseSequence(markerMatcher.group(1));
|
|
167
|
+
if (sequence == null || !isSafeRegularFile(file)) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
markerSequence = Math.max(markerSequence, sequence);
|
|
171
|
+
markerFiles.add(new FileSequence(file, sequence));
|
|
172
|
+
} else {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (markerSequence > 0) {
|
|
177
|
+
File newestMarker = new File(directory, markerFileName(markerSequence));
|
|
178
|
+
if (!isSafeRegularFile(newestMarker) || !validMarker(newestMarker, markerSequence)) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
eventFiles.sort(Comparator.comparingLong(value -> value.sequence));
|
|
184
|
+
List<Record> records = new ArrayList<>();
|
|
185
|
+
long totalEventBytes = 0;
|
|
186
|
+
long maximumRecordSequence = 0;
|
|
187
|
+
for (FileSequence value : eventFiles) {
|
|
188
|
+
maximumRecordSequence = Math.max(maximumRecordSequence, value.sequence);
|
|
189
|
+
if (value.sequence <= markerSequence) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
Record record = decodeRecord(value.file, value.sequence);
|
|
193
|
+
if (record == null) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
totalEventBytes += record.eventBytes;
|
|
197
|
+
if (records.size() >= MAX_QUEUE_RECORDS || totalEventBytes > MAX_QUEUE_BYTES) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
records.add(record);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
removeAcceptedFiles(markerSequence);
|
|
204
|
+
removeOldMarkers(markerFiles, markerSequence);
|
|
205
|
+
return new Snapshot(
|
|
206
|
+
markerSequence, maximumRecordSequence, totalEventBytes, records);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private boolean commitMarker(long sequence) {
|
|
210
|
+
File marker = new File(directory, markerFileName(sequence));
|
|
211
|
+
if (marker.exists()) {
|
|
212
|
+
return isSafeRegularFile(marker) && validMarker(marker, sequence);
|
|
213
|
+
}
|
|
214
|
+
return atomicWrite(marker, encodeMarker(sequence));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private void removeAcceptedFiles(long markerSequence) {
|
|
218
|
+
if (markerSequence <= 0) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
File[] files = directory.listFiles();
|
|
222
|
+
if (files == null) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
boolean deleted = false;
|
|
226
|
+
for (File file : files) {
|
|
227
|
+
Matcher matcher = EVENT_PATTERN.matcher(file.getName());
|
|
228
|
+
if (!matcher.matches()) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
Long sequence = parseSequence(matcher.group(1));
|
|
232
|
+
if (sequence != null
|
|
233
|
+
&& sequence <= markerSequence
|
|
234
|
+
&& isSafeRegularFile(file)
|
|
235
|
+
&& file.delete()) {
|
|
236
|
+
deleted = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (deleted) {
|
|
240
|
+
parentDirectorySync.sync(directory);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private void removeOldMarkers(List<FileSequence> markers, long markerSequence) {
|
|
245
|
+
boolean deleted = false;
|
|
246
|
+
for (FileSequence marker : markers) {
|
|
247
|
+
if (marker.sequence < markerSequence
|
|
248
|
+
&& isSafeRegularFile(marker.file)
|
|
249
|
+
&& marker.file.delete()) {
|
|
250
|
+
deleted = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (deleted) {
|
|
254
|
+
parentDirectorySync.sync(directory);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private void removeAllOlderMarkers(long markerSequence) {
|
|
259
|
+
File[] files = directory.listFiles();
|
|
260
|
+
if (files == null) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
List<FileSequence> markers = new ArrayList<>();
|
|
264
|
+
for (File file : files) {
|
|
265
|
+
Matcher matcher = MARKER_PATTERN.matcher(file.getName());
|
|
266
|
+
if (!matcher.matches()) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
Long sequence = parseSequence(matcher.group(1));
|
|
270
|
+
if (sequence != null) {
|
|
271
|
+
markers.add(new FileSequence(file, sequence));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
removeOldMarkers(markers, markerSequence);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private boolean prepareDirectory() {
|
|
278
|
+
try {
|
|
279
|
+
if (directory.exists()) {
|
|
280
|
+
if (!directory.isDirectory() || !isCanonicalPath(directory)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
} else if (!directory.mkdirs()) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
return makeDirectoryPrivate(directory) && isCanonicalPath(directory);
|
|
287
|
+
} catch (IOException | SecurityException error) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private boolean removeStaleTemporaryFiles() {
|
|
293
|
+
File[] files = directory.listFiles((unused, name) -> name.endsWith(".tmp"));
|
|
294
|
+
if (files == null) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
for (File file : files) {
|
|
298
|
+
if (!isSafeRegularFile(file) || !file.delete()) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private boolean atomicWrite(File destination, byte[] bytes) {
|
|
306
|
+
if (bytes == null || bytes.length == 0 || destination.exists()) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
File temporary = new File(directory, destination.getName() + ".tmp");
|
|
310
|
+
if (temporary.exists()
|
|
311
|
+
&& (!isSafeRegularFile(temporary) || !temporary.delete())) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
boolean committed = false;
|
|
315
|
+
boolean renamed = false;
|
|
316
|
+
try {
|
|
317
|
+
if (!temporary.createNewFile()
|
|
318
|
+
|| !isSafeRegularFile(temporary)
|
|
319
|
+
|| !makeFilePrivate(temporary)) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
try (FileOutputStream output = new FileOutputStream(temporary, false)) {
|
|
323
|
+
output.write(bytes);
|
|
324
|
+
output.flush();
|
|
325
|
+
output.getFD().sync();
|
|
326
|
+
}
|
|
327
|
+
if (!makeFilePrivate(temporary) || !temporary.renameTo(destination)) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
renamed = true;
|
|
331
|
+
if (!makeFilePrivate(destination)) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
if (parentDirectorySync.sync(directory)
|
|
335
|
+
== FatalRecordStore.ParentDirectorySyncResult.FAILED) {
|
|
336
|
+
poisoned = true;
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
committed = true;
|
|
340
|
+
return true;
|
|
341
|
+
} catch (IOException | SecurityException error) {
|
|
342
|
+
return false;
|
|
343
|
+
} finally {
|
|
344
|
+
if (!committed) {
|
|
345
|
+
if (renamed) {
|
|
346
|
+
poisoned = true;
|
|
347
|
+
} else if (temporary.exists() && isSafeRegularFile(temporary)) {
|
|
348
|
+
temporary.delete();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private static boolean validEvent(String serializedEvent, int eventBytes) {
|
|
355
|
+
if (serializedEvent == null || serializedEvent.isEmpty() || eventBytes <= 0) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
byte[] bytes = serializedEvent.getBytes(StandardCharsets.UTF_8);
|
|
359
|
+
return bytes.length == eventBytes && bytes.length <= MAX_EVENT_BYTES;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private static byte[] encodeRecord(
|
|
363
|
+
long sequence, String serializedEvent, int eventBytes) {
|
|
364
|
+
byte[] payload = serializedEvent.getBytes(StandardCharsets.UTF_8);
|
|
365
|
+
CRC32 checksum = new CRC32();
|
|
366
|
+
checksum.update(payload);
|
|
367
|
+
try {
|
|
368
|
+
ByteArrayOutputStream bytes = new ByteArrayOutputStream(payload.length + 32);
|
|
369
|
+
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
|
370
|
+
output.writeInt(RECORD_MAGIC);
|
|
371
|
+
output.writeInt(FILE_FORMAT_VERSION);
|
|
372
|
+
output.writeLong(sequence);
|
|
373
|
+
output.writeInt(eventBytes);
|
|
374
|
+
output.writeInt(payload.length);
|
|
375
|
+
output.write(payload);
|
|
376
|
+
output.writeLong(checksum.getValue());
|
|
377
|
+
}
|
|
378
|
+
return bytes.toByteArray();
|
|
379
|
+
} catch (IOException impossible) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private static Record decodeRecord(File file, long expectedSequence) {
|
|
385
|
+
if (file.length() <= 0 || file.length() > MAX_EVENT_BYTES + 32L) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
byte[] bytes = readBounded(file, MAX_EVENT_BYTES + 32);
|
|
389
|
+
if (bytes == null) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes))) {
|
|
393
|
+
if (input.readInt() != RECORD_MAGIC || input.readInt() != FILE_FORMAT_VERSION) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
long sequence = input.readLong();
|
|
397
|
+
int eventBytes = input.readInt();
|
|
398
|
+
int payloadLength = input.readInt();
|
|
399
|
+
if (sequence != expectedSequence
|
|
400
|
+
|| eventBytes <= 0
|
|
401
|
+
|| payloadLength != eventBytes
|
|
402
|
+
|| payloadLength > MAX_EVENT_BYTES) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
byte[] payload = new byte[payloadLength];
|
|
406
|
+
input.readFully(payload);
|
|
407
|
+
long expectedChecksum = input.readLong();
|
|
408
|
+
if (input.read() != -1) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
CRC32 checksum = new CRC32();
|
|
412
|
+
checksum.update(payload);
|
|
413
|
+
if (checksum.getValue() != expectedChecksum) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
String serializedEvent = new String(payload, StandardCharsets.UTF_8);
|
|
417
|
+
if (!validEvent(serializedEvent, eventBytes)) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
return new Record(sequence, serializedEvent, eventBytes);
|
|
421
|
+
} catch (IOException error) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private static byte[] encodeMarker(long sequence) {
|
|
427
|
+
CRC32 checksum = new CRC32();
|
|
428
|
+
checksum.update(longBytes(sequence));
|
|
429
|
+
try {
|
|
430
|
+
ByteArrayOutputStream bytes = new ByteArrayOutputStream(24);
|
|
431
|
+
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
|
432
|
+
output.writeInt(MARKER_MAGIC);
|
|
433
|
+
output.writeInt(FILE_FORMAT_VERSION);
|
|
434
|
+
output.writeLong(sequence);
|
|
435
|
+
output.writeLong(checksum.getValue());
|
|
436
|
+
}
|
|
437
|
+
return bytes.toByteArray();
|
|
438
|
+
} catch (IOException impossible) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private static boolean validMarker(File file, long expectedSequence) {
|
|
444
|
+
byte[] bytes = readBounded(file, 24);
|
|
445
|
+
if (bytes == null || bytes.length != 24) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes))) {
|
|
449
|
+
if (input.readInt() != MARKER_MAGIC || input.readInt() != FILE_FORMAT_VERSION) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
long sequence = input.readLong();
|
|
453
|
+
long expectedChecksum = input.readLong();
|
|
454
|
+
CRC32 checksum = new CRC32();
|
|
455
|
+
checksum.update(longBytes(sequence));
|
|
456
|
+
return sequence == expectedSequence
|
|
457
|
+
&& sequence > 0
|
|
458
|
+
&& checksum.getValue() == expectedChecksum
|
|
459
|
+
&& input.read() == -1;
|
|
460
|
+
} catch (IOException error) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private static byte[] longBytes(long value) {
|
|
466
|
+
return new byte[] {
|
|
467
|
+
(byte) (value >>> 56),
|
|
468
|
+
(byte) (value >>> 48),
|
|
469
|
+
(byte) (value >>> 40),
|
|
470
|
+
(byte) (value >>> 32),
|
|
471
|
+
(byte) (value >>> 24),
|
|
472
|
+
(byte) (value >>> 16),
|
|
473
|
+
(byte) (value >>> 8),
|
|
474
|
+
(byte) value
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private static byte[] readBounded(File file, int maximumBytes) {
|
|
479
|
+
try (FileInputStream input = new FileInputStream(file);
|
|
480
|
+
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
|
481
|
+
byte[] buffer = new byte[8192];
|
|
482
|
+
int total = 0;
|
|
483
|
+
int count;
|
|
484
|
+
while ((count = input.read(buffer)) != -1) {
|
|
485
|
+
total += count;
|
|
486
|
+
if (total > maximumBytes) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
output.write(buffer, 0, count);
|
|
490
|
+
}
|
|
491
|
+
return output.toByteArray();
|
|
492
|
+
} catch (IOException error) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private static String eventFileName(long sequence) {
|
|
498
|
+
return String.format(java.util.Locale.ROOT, "%s%020d%s", EVENT_PREFIX, sequence, EVENT_SUFFIX);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private static String markerFileName(long sequence) {
|
|
502
|
+
return String.format(
|
|
503
|
+
java.util.Locale.ROOT, "%s%020d%s", MARKER_PREFIX, sequence, MARKER_SUFFIX);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private static Long parseSequence(String value) {
|
|
507
|
+
try {
|
|
508
|
+
long sequence = Long.parseLong(value);
|
|
509
|
+
return sequence > 0 ? sequence : null;
|
|
510
|
+
} catch (NumberFormatException error) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private static File resolveCanonicalParent(File value) {
|
|
516
|
+
File absolute = value.getAbsoluteFile();
|
|
517
|
+
File parent = absolute.getParentFile();
|
|
518
|
+
if (parent == null) {
|
|
519
|
+
return absolute;
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
return new File(parent.getCanonicalFile(), absolute.getName());
|
|
523
|
+
} catch (IOException | SecurityException error) {
|
|
524
|
+
return absolute;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private static boolean isCanonicalPath(File value) throws IOException {
|
|
529
|
+
return value.getCanonicalFile().equals(value.getAbsoluteFile());
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private static boolean isSafeRegularFile(File value) {
|
|
533
|
+
try {
|
|
534
|
+
return value.isFile()
|
|
535
|
+
&& isCanonicalPath(value);
|
|
536
|
+
} catch (IOException | SecurityException error) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private static boolean makeDirectoryPrivate(File value) {
|
|
542
|
+
return value.setReadable(false, false)
|
|
543
|
+
&& value.setWritable(false, false)
|
|
544
|
+
&& value.setExecutable(false, false)
|
|
545
|
+
&& value.setReadable(true, true)
|
|
546
|
+
&& value.setWritable(true, true)
|
|
547
|
+
&& value.setExecutable(true, true);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private static boolean makeFilePrivate(File value) {
|
|
551
|
+
return value.setReadable(false, false)
|
|
552
|
+
&& value.setWritable(false, false)
|
|
553
|
+
&& value.setExecutable(false, false)
|
|
554
|
+
&& value.setReadable(true, true)
|
|
555
|
+
&& value.setWritable(true, true);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
static final class Record {
|
|
559
|
+
final long sequence;
|
|
560
|
+
final String serializedEvent;
|
|
561
|
+
final int eventBytes;
|
|
562
|
+
|
|
563
|
+
Record(long sequence, String serializedEvent, int eventBytes) {
|
|
564
|
+
this.sequence = sequence;
|
|
565
|
+
this.serializedEvent = serializedEvent;
|
|
566
|
+
this.eventBytes = eventBytes;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
static final class Result {
|
|
571
|
+
final String status;
|
|
572
|
+
final List<Record> records;
|
|
573
|
+
|
|
574
|
+
Result(String status, List<Record> records) {
|
|
575
|
+
this.status = status;
|
|
576
|
+
this.records = Collections.unmodifiableList(new ArrayList<>(records));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
static Result status(String status) {
|
|
580
|
+
return new Result(status, Collections.emptyList());
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private static final class Snapshot {
|
|
585
|
+
final long markerSequence;
|
|
586
|
+
final long maximumRecordSequence;
|
|
587
|
+
final long totalEventBytes;
|
|
588
|
+
final List<Record> records;
|
|
589
|
+
|
|
590
|
+
Snapshot(
|
|
591
|
+
long markerSequence,
|
|
592
|
+
long maximumRecordSequence,
|
|
593
|
+
long totalEventBytes,
|
|
594
|
+
List<Record> records) {
|
|
595
|
+
this.markerSequence = markerSequence;
|
|
596
|
+
this.maximumRecordSequence = maximumRecordSequence;
|
|
597
|
+
this.totalEventBytes = totalEventBytes;
|
|
598
|
+
this.records = records;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private static final class FileSequence {
|
|
603
|
+
final File file;
|
|
604
|
+
final long sequence;
|
|
605
|
+
|
|
606
|
+
FileSequence(File file, long sequence) {
|
|
607
|
+
this.file = file;
|
|
608
|
+
this.sequence = sequence;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|