@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 CHANGED
@@ -118,6 +118,73 @@ client key before creating a new client. A `429` preserves the queue and
118
118
  reports `pausedReason: "rate_limit"` plus the bounded retry signal exposed by
119
119
  the failed flush.
120
120
 
121
+ ## Offline And Restart Delivery
122
+
123
+ The React Native entry uses an app-private native queue by default when the
124
+ current app binary contains the linked LogBrew module. Each accepted event is
125
+ written before it enters memory. After a JavaScript runtime or app restart,
126
+ the client loads pending events oldest first with their original IDs. A
127
+ successful intake response commits which records were accepted before removing
128
+ them, so an interrupted removal can cause a duplicate but cannot silently lose
129
+ an unaccepted event. Replayed events keep their stable IDs. Delivery is at
130
+ least once, so apps must tolerate duplicates, including when retries regroup
131
+ events into different batches.
132
+
133
+ Inspect the delivery health snapshot to observe the active behavior:
134
+
135
+ ```js
136
+ const health = client.deliveryHealth();
137
+ if (health.storage !== "persistent") {
138
+ // The app is running without the linked native queue.
139
+ }
140
+ ```
141
+
142
+ The default `persistentQueue: "auto"` mode uses memory when the native module
143
+ is absent, including Expo Go and an older app binary after a JavaScript-only
144
+ update. For a production build that must not start without restart recovery,
145
+ set `persistentQueue: "required"`. Use `persistentQueue: "disabled"` only
146
+ when the app intentionally accepts a memory-only queue.
147
+
148
+ ```js
149
+ const client = createLogBrewReactNativeClient({
150
+ clientKey,
151
+ persistentQueue: "required",
152
+ transport: createReactNativeFetchTransport()
153
+ });
154
+ ```
155
+
156
+ The native queue stores one compact event per atomic record, is limited to
157
+ 1,000 events and 4 MiB of compact event data, and uses the same smaller limits
158
+ when you configure them on the client. Both platforms use app-private storage.
159
+ The client key is not written to disk; its SHA-256 digest separates queues
160
+ after key rotation. The queue does not provide mathematically exactly-once
161
+ delivery.
162
+
163
+ A successful `shutdown()` drains and closes the queue. A failed flush or
164
+ shutdown leaves the exact remainder available to the same client and the next
165
+ app start. Call `client.purgePendingEvents()` only when no flush or shutdown is
166
+ active. To retire an active key without sending its remainder, purge that
167
+ client first and then close it:
168
+
169
+ ```js
170
+ client.purgePendingEvents();
171
+ await client.shutdown();
172
+ ```
173
+
174
+ When no client owns the key, remove any remaining records explicitly with:
175
+
176
+ ```js
177
+ import { purgeLogBrewReactNativePersistentQueue } from "@logbrew/react-native";
178
+
179
+ purgeLogBrewReactNativePersistentQueue({ clientKey: previousClientKey });
180
+ ```
181
+
182
+ Use only one active persistent client for a given key across an app process.
183
+ The SDK rejects duplicates within one JavaScript runtime; apps with multiple
184
+ React Native runtimes or platform processes must coordinate that ownership.
185
+ Different client keys use separate native queues. The Node ESM and CommonJS
186
+ entries remain platform-neutral and never load React Native.
187
+
121
188
  ## Product Actions And API Milestones
122
189
 
123
190
  Use explicit action and network helpers for important mobile funnel steps your app already understands. These events are designed for timelines and agent analysis without enabling broad automatic replay:
@@ -202,7 +269,7 @@ Installation is idempotent for the active React Native `ErrorUtils` object. The
202
269
 
203
270
  The React Native conditional export obtains LogBrew's synchronous native fatal store through the supported TurboModule or `NativeModules` seam. Before chaining a fatal report, it writes one bounded record to app-private storage that is excluded from operating-system archives. On a later installation it performs stable-ID at-least-once replay, and acknowledgement happens only after local queue admission is observable through the SDK queue counters. Filtered, dropped, unknown-admission, persistence-failed, and acknowledgement-failed records are retained. A failed acknowledgement is retried without admitting the same ID twice in one JavaScript runtime. Use `fatalHealth()` for frozen bounded counters and status, or `discardPendingFatalRecord()` for an explicit rollback discard. The Node ESM and CommonJS entries never import React Native; non-React-Native callers must inject `fatalStore` explicitly.
204
271
 
205
- Automatic events exclude the original error message, raw stack, arbitrary metadata, full URLs, hosts, query strings, local absolute paths, payloads, and native error text. `onDiagnostic` receives only a fixed code. This integration does not claim mathematically exactly-once delivery, backend-visible deduplication, native crash capture, ANR or hang detection, general offline queueing, or symbolication.
272
+ Automatic events exclude the original error message, raw stack, arbitrary metadata, full URLs, hosts, query strings, local absolute paths, payloads, and native error text. `onDiagnostic` receives only a fixed code. This error-handler integration does not claim mathematically exactly-once delivery, native crash capture, ANR or hang detection, general offline queueing by the fatal-record slot, or symbolication. The client-level persistent queue above owns normal event restart delivery.
206
273
 
207
274
  ### Opt-in Hermes Promise rejection tracking
208
275
 
@@ -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
+ }