1
+#![allow(unused_imports, clippy::field_reassign_with_default)]
2
+
3
+use crate::hash;
4
+use crate::object::*;
5
+use crate::offset_array;
6
+use error::{JournalError, Result};
7
+use std::cell::{RefCell, UnsafeCell};
8
+use std::fs::{File, OpenOptions};
9
+use std::marker::PhantomData;
10
+use std::num::NonZero;
11
+use std::num::NonZeroI128;
12
+use std::num::NonZeroU64;
13
+use std::path::Path;
14
+use window_manager::{MemoryMap, MemoryMapMut, WindowManager};
15
+use zerocopy::{ByteSlice, FromBytes, SplitByteSlice, SplitByteSliceMut};
16
+
17
+#[cfg(debug_assertions)]
18
+use std::backtrace::Backtrace;
19
+
20
+use crate::value_guard::ValueGuard;
21
+
22
+#[cfg(target_os = "linux")]
23
+pub fn load_machine_id() -> Result<[u8; 16]> {
24
+ let content = std::fs::read_to_string("/etc/machine-id")?;
25
+ let decoded = hex::decode(content.trim()).map_err(|_| JournalError::UuidSerde)?;
26
+ let bytes: [u8; 16] = decoded.try_into().map_err(|_| JournalError::UuidSerde)?;
27
+ Ok(bytes)
28
+}
29
+
30
+#[cfg(target_os = "macos")]
31
+pub fn load_machine_id() -> Result<[u8; 16]> {
32
+ use std::process::Command;
33
+
34
+ let output = Command::new("system_profiler")
35
+ .arg("SPHardwareDataType")
36
+ .output()
37
+ .map_err(|_| JournalError::UuidSerde)?;
38
+
39
+ if output.status.success() {
40
+ let output_str = String::from_utf8_lossy(&output.stdout);
41
+ for line in output_str.lines() {
42
+ if line.contains("Hardware UUID:") {
43
+ if let Some(uuid_str) = line.split("Hardware UUID:").nth(1) {
44
+ let uuid_str = uuid_str.trim();
45
+ let hex_str: String = uuid_str.chars().filter(|c| *c != '-').collect();
46
+
47
+ if hex_str.len() == 32 {
48
+ let mut bytes = [0u8; 16];
49
+ for i in 0..16 {
50
+ let hex_pair = &hex_str[i * 2..i * 2 + 2];
51
+ bytes[i] = u8::from_str_radix(hex_pair, 16)
52
+ .map_err(|_| JournalError::UuidSerde)?;
53
+ }
54
+ return Ok(bytes);
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ Err(JournalError::UuidSerde)
62
+}
63
+
64
+#[cfg(not(any(target_os = "linux", target_os = "macos")))]
65
+pub fn load_machine_id() -> Result<[u8; 16]> {
66
+ Err(JournalError::UuidSerde)
67
+}
68
+
69
+#[cfg(target_os = "linux")]
70
+pub fn load_boot_id() -> Result<[u8; 16]> {
71
+ let content = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")?;
72
+
73
+ let uuid_str = content.trim();
74
+ let hex_str: String = uuid_str.chars().filter(|c| *c != '-').collect();
75
+
76
+ if hex_str.len() != 32 {
77
+ return Err(JournalError::UuidSerde);
78
+ }
79
+
80
+ let mut bytes = [0u8; 16];
81
+ for i in 0..16 {
82
+ let hex_pair = &hex_str[i * 2..i * 2 + 2];
83
+ bytes[i] = u8::from_str_radix(hex_pair, 16).map_err(|_| JournalError::UuidSerde)?;
84
+ }
85
+
86
+ Ok(bytes)
87
+}
88
+
89
+#[cfg(target_os = "macos")]
90
+pub fn load_boot_id() -> Result<[u8; 16]> {
91
+ use std::process::Command;
92
+
93
+ let output = Command::new("sysctl")
94
+ .arg("-n")
95
+ .arg("kern.boottime")
96
+ .output()
97
+ .map_err(|_| JournalError::UuidSerde)?;
98
+
99
+ if output.status.success() {
100
+ let output_str = String::from_utf8_lossy(&output.stdout);
101
+ // Parse "{ sec = 1753988677, usec = 131097 } Thu Jul 31 22:04:37 2025"
102
+ // Extract sec and usec values
103
+ if let (Some(sec_start), Some(usec_start)) =
104
+ (output_str.find("sec = "), output_str.find("usec = "))
105
+ {
106
+ let sec_str = &output_str[sec_start + 6..];
107
+ let sec_end = sec_str.find(',').unwrap_or(sec_str.len());
108
+ let sec_str = &sec_str[..sec_end].trim();
109
+
110
+ let usec_str = &output_str[usec_start + 7..];
111
+ let usec_end = usec_str.find(' ').unwrap_or(usec_str.len());
112
+ let usec_str = &usec_str[..usec_end].trim();
113
+
114
+ if let (Ok(sec), Ok(usec)) = (sec_str.parse::<u64>(), usec_str.parse::<u64>()) {
115
+ // Create a deterministic UUID from boot time
116
+ // Use sec in first 8 bytes, usec in next 4 bytes, pad remaining with zeros
117
+ let mut bytes = [0u8; 16];
118
+ bytes[0..8].copy_from_slice(&sec.to_be_bytes());
119
+ bytes[8..12].copy_from_slice(&(usec as u32).to_be_bytes());
120
+ // bytes[12..16] remain zero-filled for consistency
121
+ return Ok(bytes);
122
+ }
123
+ }
124
+ }
125
+
126
+ Err(JournalError::UuidSerde)
127
+}
128
+
129
+#[cfg(not(any(target_os = "linux", target_os = "macos")))]
130
+pub fn load_boot_id() -> Result<[u8; 16]> {
131
+ Err(JournalError::UuidSerde)
132
+}
133
+
134
+// Size to pad objects to (8 bytes)
135
+const OBJECT_ALIGNMENT: u64 = 8;
136
+
137
+pub trait BucketVisitor<'a> {
138
+ type Object: JournalObject<&'a [u8]> + HashableObject;
139
+ type Output;
140
+
141
+ /// Called for each object in the bucket. Return Some(output) to stop iteration,
142
+ /// or None to continue to the next object.
143
+ fn visit(&mut self, object: &ValueGuard<'a, Self::Object>) -> Result<Option<Self::Output>>;
144
+}
145
+
146
+struct PayloadMatcher<'data, T> {
147
+ payload: &'data [u8],
148
+ hash: u64,
149
+ _phantom: PhantomData<T>,
150
+}
151
+
152
+impl<'data, B: ByteSlice> PayloadMatcher<'data, DataObject<B>> {
153
+ fn data_matcher(payload: &'data [u8], hash: u64) -> Self {
154
+ Self {
155
+ payload,
156
+ hash,
157
+ _phantom: PhantomData::<DataObject<B>>,
158
+ }
159
+ }
160
+}
161
+
162
+impl<'data, B: ByteSlice> PayloadMatcher<'data, FieldObject<B>> {
163
+ fn field_matcher(payload: &'data [u8], hash: u64) -> Self {
164
+ Self {
165
+ payload,
166
+ hash,
167
+ _phantom: PhantomData::<FieldObject<B>>,
168
+ }
169
+ }
170
+}
171
+
172
+impl<'a, T> BucketVisitor<'a> for PayloadMatcher<'_, T>
173
+where
174
+ T: JournalObject<&'a [u8]> + HashableObject,
175
+{
176
+ type Object = T;
177
+ type Output = NonZeroU64;
178
+
179
+ fn visit(&mut self, object: &ValueGuard<'a, Self::Object>) -> Result<Option<Self::Output>> {
180
+ if object.hash() == self.hash && object.get_payload() == self.payload {
181
+ Ok(Some(object.offset()))
182
+ } else {
183
+ Ok(None)
184
+ }
185
+ }
186
+}
187
+
188
+#[derive(Debug, Clone)]
189
+pub struct JournalFileOptions {
190
+ machine_id: [u8; 16],
191
+ boot_id: [u8; 16],
192
+ seqnum_id: [u8; 16],
193
+ file_id: [u8; 16],
194
+ window_size: u64,
195
+ data_hash_table_buckets: usize,
196
+ field_hash_table_buckets: usize,
197
+ enable_keyed_hash: bool,
198
+}
199
+
200
+impl JournalFileOptions {
201
+ pub fn new(
202
+ machine_id: [u8; 16],
203
+ boot_id: [u8; 16],
204
+ seqnum_id: [u8; 16],
205
+ file_id: [u8; 16],
206
+ ) -> Self {
207
+ Self {
208
+ machine_id,
209
+ boot_id,
210
+ seqnum_id,
211
+ file_id,
212
+ window_size: 64 * 1024,
213
+ data_hash_table_buckets: 4096,
214
+ field_hash_table_buckets: 512,
215
+ enable_keyed_hash: true,
216
+ }
217
+ }
218
+
219
+ pub fn with_window_size(mut self, size: u64) -> Self {
220
+ assert_eq!(size % OBJECT_ALIGNMENT, 0);
221
+ assert_eq!(size % 4096, 0, "Window size must be page-aligned");
222
+ self.window_size = size;
223
+ self
224
+ }
225
+
226
+ pub fn with_data_hash_table_buckets(mut self, buckets: usize) -> Self {
227
+ assert!(
228
+ buckets.is_power_of_two(),
229
+ "Hash table buckets should be a power of two"
230
+ );
231
+ self.data_hash_table_buckets = buckets;
232
+ self
233
+ }
234
+
235
+ pub fn with_field_hash_table_buckets(mut self, buckets: usize) -> Self {
236
+ assert!(
237
+ buckets.is_power_of_two(),
238
+ "Hash table buckets should be a power of two"
239
+ );
240
+ self.field_hash_table_buckets = buckets;
241
+ self
242
+ }
243
+
244
+ pub fn with_keyed_hash(mut self, enabled: bool) -> Self {
245
+ self.enable_keyed_hash = enabled;
246
+ self
247
+ }
248
+
249
+ pub fn create<M: MemoryMapMut>(self, path: impl AsRef<Path>) -> Result<JournalFile<M>> {
250
+ JournalFile::create(path, self)
251
+ }
252
+}
253
+
254
+/// Hash table bucket utilization statistics
255
+#[derive(Debug, Clone, Copy)]
256
+pub struct BucketUtilization {
257
+ pub data_occupied: usize,
258
+ pub data_total: usize,
259
+ pub field_occupied: usize,
260
+ pub field_total: usize,
261
+}
262
+
263
+impl BucketUtilization {
264
+ pub fn data_utilization(&self) -> f64 {
265
+ if self.data_total == 0 {
266
+ 0.0
267
+ } else {
268
+ self.data_occupied as f64 / self.data_total as f64
269
+ }
270
+ }
271
+
272
+ pub fn field_utilization(&self) -> f64 {
273
+ if self.field_total == 0 {
274
+ 0.0
275
+ } else {
276
+ self.field_occupied as f64 / self.field_total as f64
277
+ }
278
+ }
279
+}
280
+
281
+///
282
+/// A reader for systemd journal files that efficiently maps small regions of the file into memory.
283
+///
284
+/// # Memory Management
285
+///
286
+/// This implementation uses a window-based memory mapping strategy similar to systemd's original
287
+/// implementation. Instead of mapping the entire file, it maintains a small set of memory-mapped
288
+/// windows and reuses them as needed.
289
+///
290
+/// # Concurrency and Safety
291
+///
292
+/// `JournalFile` uses interior mutability to provide a safe API with the following characteristics:
293
+///
294
+/// - The window manager is wrapped in an `UnsafeCell` to allow mutation through a shared reference.
295
+/// - A single `RefCell<bool>` guards access to ensure only one object can be active at a time.
296
+/// - Methods like `data_object()` return a `ValueGuard<T>` that automatically releases the lock
297
+/// when dropped.
298
+///
299
+/// This design ensures that memory safety is maintained even though references to memory-mapped
300
+/// regions could be invalidated when new objects are created.
301
+pub struct JournalFile<M: MemoryMap> {
302
+ // Persistent memory maps for journal header and data/field hash tables
303
+ header_map: M,
304
+ data_hash_table_map: Option<M>,
305
+ field_hash_table_map: Option<M>,
306
+
307
+ // Window manager for other objects
308
+ window_manager: UnsafeCell<WindowManager<M>>,
309
+
310
+ // Flag to track if any object is in use
311
+ object_in_use: RefCell<bool>,
312
+
313
+ #[cfg(debug_assertions)]
314
+ prev_backtrace: RefCell<Backtrace>,
315
+ #[cfg(debug_assertions)]
316
+ backtrace: RefCell<Backtrace>,
317
+}
318
+
319
+fn map_hash_table<M: MemoryMap>(
320
+ file: &File,
321
+ offset: Option<NonZeroU64>,
322
+ size: Option<NonZeroU64>,
323
+) -> Result<Option<M>> {
324
+ let (Some(offset), Some(size)) = (offset, size) else {
325
+ return Ok(None);
326
+ };
327
+
328
+ if offset.get() <= std::mem::size_of::<JournalHeader>() as u64 {
329
+ return Err(JournalError::InvalidObjectLocation);
330
+ }
331
+ if size.get() <= std::mem::size_of::<ObjectHeader>() as u64 {
332
+ return Err(JournalError::InvalidObjectLocation);
333
+ }
334
+
335
+ let offset = offset.get() - std::mem::size_of::<ObjectHeader>() as u64;
336
+ let size = std::mem::size_of::<ObjectHeader>() as u64 + size.get();
337
+ M::create(file, offset, size).map(Some)
338
+}
339
+
340
+impl<M: MemoryMap> JournalFile<M> {
341
+ pub fn visit_bucket<'a, H, V>(
342
+ &'a self,
343
+ hash_table: Option<H>,
344
+ hash: u64,
345
+ mut visitor: V,
346
+ ) -> Result<Option<V::Output>>
347
+ where
348
+ H: HashTable<Object = V::Object>,
349
+ V: BucketVisitor<'a>,
350
+ {
351
+ let hash_table = hash_table.ok_or(JournalError::MissingHashTable)?;
352
+ let bucket = hash_table.hash_item_ref(hash);
353
+ let mut object_offset = bucket.head_hash_offset;
354
+
355
+ while let Some(offset) = object_offset {
356
+ let object_guard = self.journal_object_ref::<V::Object>(offset)?;
357
+
358
+ if let Some(output) = visitor.visit(&object_guard)? {
359
+ return Ok(Some(output));
360
+ }
361
+
362
+ object_offset = object_guard.next_hash_offset();
363
+ }
364
+
365
+ Ok(None)
366
+ }
367
+
368
+ pub fn open(path: impl AsRef<Path>, window_size: u64) -> Result<Self> {
369
+ debug_assert_eq!(window_size % OBJECT_ALIGNMENT, 0);
370
+
371
+ // Open file and check its size
372
+ let file = OpenOptions::new().read(true).write(false).open(&path)?;
373
+
374
+ // Create a memory map for the header
375
+ let header_size = std::mem::size_of::<JournalHeader>() as u64;
376
+ let header_map = M::create(&file, 0, header_size)?;
377
+ let header = JournalHeader::ref_from_prefix(&header_map).unwrap().0;
378
+ if header.signature != *b"LPKSHHRH" {
379
+ return Err(JournalError::InvalidMagicNumber);
380
+ }
381
+
382
+ // Initialize the hash table maps if they exist
383
+ let data_hash_table_map = map_hash_table(
384
+ &file,
385
+ header.data_hash_table_offset,
386
+ header.data_hash_table_size,
387
+ )?;
388
+ let field_hash_table_map = map_hash_table(
389
+ &file,
390
+ header.field_hash_table_offset,
391
+ header.field_hash_table_size,
392
+ )?;
393
+
394
+ // Create window manager for the rest of the objects
395
+ let window_manager = UnsafeCell::new(WindowManager::new(file, window_size, 32)?);
396
+
397
+ Ok(JournalFile {
398
+ header_map,
399
+ data_hash_table_map,
400
+ field_hash_table_map,
401
+ window_manager,
402
+ object_in_use: RefCell::new(false),
403
+
404
+ #[cfg(debug_assertions)]
405
+ prev_backtrace: RefCell::new(Backtrace::capture()),
406
+ #[cfg(debug_assertions)]
407
+ backtrace: RefCell::new(Backtrace::capture()),
408
+ })
409
+ }
410
+
411
+ pub fn hash(&self, data: &[u8]) -> u64 {
412
+ let is_keyed_hash = self
413
+ .journal_header_ref()
414
+ .has_incompatible_flag(HeaderIncompatibleFlags::KeyedHash);
415
+
416
+ hash::journal_hash_data(
417
+ data,
418
+ is_keyed_hash,
419
+ if is_keyed_hash {
420
+ Some(&self.journal_header_ref().file_id)
421
+ } else {
422
+ None
423
+ },
424
+ )
425
+ }
426
+
427
+ pub fn entry_list(&self) -> Option<offset_array::List> {
428
+ let head_offset = self.journal_header_ref().entry_array_offset?;
429
+ let total_items =
430
+ std::num::NonZeroUsize::new(self.journal_header_ref().n_entries as usize)?;
431
+ Some(offset_array::List::new(head_offset, total_items))
432
+ }
433
+
434
+ pub fn journal_header_ref(&self) -> &JournalHeader {
435
+ JournalHeader::ref_from_prefix(&self.header_map).unwrap().0
436
+ }
437
+
438
+ pub fn data_hash_table_map(&self) -> Option<&M> {
439
+ self.data_hash_table_map.as_ref()
440
+ }
441
+ pub fn field_hash_table_map(&self) -> Option<&M> {
442
+ self.field_hash_table_map.as_ref()
443
+ }
444
+
445
+ pub fn data_hash_table_ref(&self) -> Option<DataHashTable<&[u8]>> {
446
+ self.data_hash_table_map
447
+ .as_ref()
448
+ .and_then(|m| DataHashTable::<&[u8]>::from_data(m, false))
449
+ }
450
+
451
+ pub fn field_hash_table_ref(&self) -> Option<FieldHashTable<&[u8]>> {
452
+ self.field_hash_table_map
453
+ .as_ref()
454
+ .and_then(|m| FieldHashTable::<&[u8]>::from_data(m, false))
455
+ }
456
+
457
+ pub fn object_header_ref(&self, position: NonZeroU64) -> Result<&ObjectHeader> {
458
+ let size_needed = std::mem::size_of::<ObjectHeader>() as u64;
459
+ let window_manager = unsafe { &mut *self.window_manager.get() };
460
+ let header_slice = window_manager.get_slice(position.get(), size_needed)?;
461
+ Ok(ObjectHeader::ref_from_bytes(header_slice).unwrap())
462
+ }
463
+
464
+ fn object_data_ref(&self, offset: NonZeroU64, size_needed: u64) -> Result<&[u8]> {
465
+ let window_manager = unsafe { &mut *self.window_manager.get() };
466
+ let object_slice = window_manager.get_slice(offset.get(), size_needed)?;
467
+ Ok(object_slice)
468
+ }
469
+
470
+ fn journal_object_ref<'a, T>(&'a self, offset: NonZeroU64) -> Result<ValueGuard<'a, T>>
471
+ where
472
+ T: JournalObject<&'a [u8]>,
473
+ {
474
+ // Check if any object is already in use
475
+ let mut is_in_use = self.object_in_use.borrow_mut();
476
+ if *is_in_use {
477
+ #[cfg(debug_assertions)]
478
+ {
479
+ eprintln!(
480
+ "Value is in use. Current Backtrace: {:?}, Previous Backtrace: {:?}",
481
+ self.backtrace.borrow().to_string(),
482
+ self.prev_backtrace.borrow().to_string()
483
+ );
484
+ }
485
+ return Err(JournalError::ValueGuardInUse);
486
+ }
487
+
488
+ #[cfg(debug_assertions)]
489
+ {
490
+ self.backtrace.swap(&self.prev_backtrace);
491
+ let _ = self.backtrace.replace(Backtrace::force_capture());
492
+ }
493
+
494
+ let is_compact = self
495
+ .journal_header_ref()
496
+ .has_incompatible_flag(HeaderIncompatibleFlags::Compact);
497
+
498
+ let size_needed = {
499
+ let header = self.object_header_ref(offset)?;
500
+ header.size
501
+ };
502
+
503
+ let data = self.object_data_ref(offset, size_needed)?;
504
+ let Some(value) = T::from_data(data, is_compact) else {
505
+ return Err(JournalError::ZerocopyFailure);
506
+ };
507
+
508
+ // Mark as in use
509
+ *is_in_use = true;
510
+
511
+ Ok(ValueGuard::new(offset, value, &self.object_in_use))
512
+ }
513
+
514
+ pub fn offset_array_ref(
515
+ &self,
516
+ offset: NonZeroU64,
517
+ ) -> Result<ValueGuard<'_, OffsetArrayObject<&[u8]>>> {
518
+ self.journal_object_ref(offset)
519
+ }
520
+
521
+ pub fn field_ref(&self, offset: NonZeroU64) -> Result<ValueGuard<'_, FieldObject<&[u8]>>> {
522
+ self.journal_object_ref(offset)
523
+ }
524
+
525
+ pub fn entry_ref(&self, offset: NonZeroU64) -> Result<ValueGuard<'_, EntryObject<&[u8]>>> {
526
+ self.journal_object_ref(offset)
527
+ }
528
+
529
+ pub fn data_ref(&self, offset: NonZeroU64) -> Result<ValueGuard<'_, DataObject<&[u8]>>> {
530
+ self.journal_object_ref(offset)
531
+ }
532
+
533
+ pub fn tag_ref(&self, offset: NonZeroU64) -> Result<ValueGuard<'_, TagObject<&[u8]>>> {
534
+ self.journal_object_ref(offset)
535
+ }
536
+
537
+ pub fn find_data_offset(&self, hash: u64, payload: &[u8]) -> Result<Option<NonZeroU64>> {
538
+ let visitor = PayloadMatcher::data_matcher(payload, hash);
539
+ self.visit_bucket(self.data_hash_table_ref(), hash, visitor)
540
+ }
541
+
542
+ pub fn find_field_offset(&self, hash: u64, payload: &[u8]) -> Result<Option<NonZeroU64>> {
543
+ let visitor = PayloadMatcher::field_matcher(payload, hash);
544
+ self.visit_bucket(self.field_hash_table_ref(), hash, visitor)
545
+ }
546
+
547
+ /// Run a directed partition point query on a data object's entry array
548
+ ///
549
+ /// This finds the first/last entry (depending on direction) that satisfies the given predicate
550
+ /// in the entry array chain of the data object.
551
+ pub fn data_object_directed_partition_point<F>(
552
+ &self,
553
+ data_offset: NonZeroU64,
554
+ predicate: F,
555
+ direction: offset_array::Direction,
556
+ ) -> Result<Option<NonZeroU64>>
557
+ where
558
+ F: Fn(NonZeroU64) -> Result<bool>,
559
+ {
560
+ let Some(cursor) = self.data_ref(data_offset)?.inlined_cursor() else {
561
+ return Ok(None);
562
+ };
563
+
564
+ let Some(best_match) = cursor.directed_partition_point(self, predicate, direction)? else {
565
+ return Ok(None);
566
+ };
567
+
568
+ best_match.value(self)
569
+ }
570
+
571
+ /// Creates an iterator over all field objects in the field hash table
572
+ pub fn fields(&self) -> FieldIterator<'_, M> {
573
+ // Get the field hash table
574
+ let field_hash_table = self.field_hash_table_ref();
575
+
576
+ // Initialize with the first bucket
577
+ let mut iterator = FieldIterator {
578
+ journal: self,
579
+ field_hash_table,
580
+ current_bucket_index: 0,
581
+ next_field_offset: None,
582
+ };
583
+
584
+ // Find the first non-empty bucket
585
+ iterator.advance_to_next_nonempty_bucket();
586
+
587
+ iterator
588
+ }
589
+
590
+ /// Creates an iterator over all DATA objects for the specified field
591
+ pub fn field_data_objects<'a>(
592
+ &'a self,
593
+ field_name: &'a [u8],
594
+ ) -> Result<FieldDataIterator<'a, M>> {
595
+ // Find the field offset by name
596
+ let field_hash = self.hash(field_name);
597
+ let Some(field_offset) = self.find_field_offset(field_hash, field_name)? else {
598
+ return Ok(FieldDataIterator {
599
+ journal: self,
600
+ current_data_offset: None,
601
+ });
602
+ };
603
+
604
+ // Get the field object to access its head_data_offset
605
+ let field_guard = self.field_ref(field_offset)?;
606
+ let head_data_offset = field_guard.header.head_data_offset;
607
+
608
+ // Create the iterator
609
+ Ok(FieldDataIterator {
610
+ journal: self,
611
+ current_data_offset: head_data_offset,
612
+ })
613
+ }
614
+
615
+ /// Creates an iterator over all DATA objects for a specific entry
616
+ pub fn entry_data_objects(&self, entry_offset: NonZeroU64) -> Result<EntryDataIterator<'_, M>> {
617
+ // Get the entry object to determine how many data items it has
618
+ let entry_guard = self.entry_ref(entry_offset)?;
619
+
620
+ // Get the total number of items
621
+ let total_items = match &entry_guard.items {
622
+ EntryItemsType::Regular(items) => items.len(),
623
+ EntryItemsType::Compact(items) => items.len(),
624
+ };
625
+
626
+ // Create the iterator
627
+ Ok(EntryDataIterator {
628
+ journal: self,
629
+ entry_offset: Some(entry_offset),
630
+ current_index: 0,
631
+ total_items,
632
+ })
633
+ }
634
+
635
+ /// Get hash table bucket utilization statistics
636
+ pub fn bucket_utilization(&self) -> Option<BucketUtilization> {
637
+ let data_hash_table = self.data_hash_table_ref()?;
638
+ let data_total = data_hash_table.items.len();
639
+ let data_occupied = data_hash_table
640
+ .items
641
+ .iter()
642
+ .filter(|item| item.head_hash_offset.is_some())
643
+ .count();
644
+
645
+ let field_hash_table = self.field_hash_table_ref()?;
646
+ let field_total = field_hash_table.items.len();
647
+ let field_occupied = field_hash_table
648
+ .items
649
+ .iter()
650
+ .filter(|item| item.head_hash_offset.is_some())
651
+ .count();
652
+
653
+ Some(BucketUtilization {
654
+ data_occupied,
655
+ data_total,
656
+ field_occupied,
657
+ field_total,
658
+ })
659
+ }
660
+}
661
+
662
+impl<M: MemoryMapMut> JournalFile<M> {
663
+ pub fn create(path: impl AsRef<Path>, options: JournalFileOptions) -> Result<Self> {
664
+ let file = OpenOptions::new()
665
+ .create(true)
666
+ .truncate(true)
667
+ .read(true)
668
+ .write(true)
669
+ .open(&path)?;
670
+
671
+ // Calculate hash table sizes
672
+ let data_hash_table_size =
673
+ options.data_hash_table_buckets * std::mem::size_of::<HashItem>();
674
+ let field_hash_table_size =
675
+ options.field_hash_table_buckets * std::mem::size_of::<HashItem>();
676
+
677
+ // Calculate hash table offsets
678
+ let data_hash_table_offset = std::mem::size_of::<JournalHeader>() as u64
679
+ + std::mem::size_of::<ObjectHeader>() as u64;
680
+ let field_hash_table_offset = data_hash_table_offset
681
+ + data_hash_table_size as u64
682
+ + std::mem::size_of::<ObjectHeader>() as u64;
683
+
684
+ // Create header with options configuration
685
+ let mut header = JournalHeader::default();
686
+ header.signature = *b"LPKSHHRH";
687
+
688
+ // Set flags based on options configuration
689
+ if options.enable_keyed_hash {
690
+ header.incompatible_flags |= HeaderIncompatibleFlags::KeyedHash as u32;
691
+ }
692
+
693
+ // Set hash table configuration
694
+ header.data_hash_table_offset = NonZeroU64::new(data_hash_table_offset);
695
+ header.data_hash_table_size = NonZeroU64::new(data_hash_table_size as u64);
696
+ header.field_hash_table_offset = NonZeroU64::new(field_hash_table_offset);
697
+ header.field_hash_table_size = NonZeroU64::new(field_hash_table_size as u64);
698
+
699
+ // Set other header fields
700
+ header.tail_object_offset =
701
+ NonZeroU64::new(data_hash_table_offset + data_hash_table_size as u64);
702
+ header.header_size = std::mem::size_of::<JournalHeader>() as u64;
703
+ header.n_objects = 2;
704
+ header.arena_size =
705
+ field_hash_table_offset + field_hash_table_size as u64 - header.header_size;
706
+
707
+ // Set IDs from options
708
+ header.machine_id = options.machine_id;
709
+ header.tail_entry_boot_id = options.boot_id;
710
+ header.file_id = options.file_id;
711
+ header.seqnum_id = options.seqnum_id;
712
+
713
+ // Create memory maps for hash tables
714
+ let data_hash_table_map = map_hash_table(
715
+ &file,
716
+ header.data_hash_table_offset,
717
+ header.data_hash_table_size,
718
+ )?;
719
+ let field_hash_table_map = map_hash_table(
720
+ &file,
721
+ header.field_hash_table_offset,
722
+ header.field_hash_table_size,
723
+ )?;
724
+
725
+ // Create header memory map and write header
726
+ let header_size = std::mem::size_of::<JournalHeader>() as u64;
727
+ let mut header_map = M::create(&file, 0, header_size)?;
728
+ {
729
+ let header_mut = JournalHeader::mut_from_prefix(&mut header_map).unwrap().0;
730
+ *header_mut = header;
731
+ }
732
+
733
+ // Create window manager for the rest of the objects
734
+ let window_manager = UnsafeCell::new(WindowManager::new(file, options.window_size, 32)?);
735
+
736
+ let jf = JournalFile {
737
+ header_map,
738
+ data_hash_table_map,
739
+ field_hash_table_map,
740
+ window_manager,
741
+ object_in_use: RefCell::new(false),
742
+
743
+ #[cfg(debug_assertions)]
744
+ prev_backtrace: RefCell::new(Backtrace::capture()),
745
+ #[cfg(debug_assertions)]
746
+ backtrace: RefCell::new(Backtrace::capture()),
747
+ };
748
+
749
+ // write data hash table object header info
750
+ {
751
+ let offset = NonZeroU64::new(
752
+ header.data_hash_table_offset.unwrap().get()
753
+ - std::mem::size_of::<ObjectHeader>() as u64,
754
+ )
755
+ .unwrap();
756
+ let size = header.data_hash_table_size.unwrap().get()
757
+ + std::mem::size_of::<ObjectHeader>() as u64;
758
+
759
+ let object_header = jf.object_header_mut(offset)?;
760
+ object_header.type_ = ObjectType::DataHashTable as u8;
761
+ object_header.size = size
762
+ }
763
+
764
+ // write field hash table object header info
765
+ {
766
+ let offset = NonZeroU64::new(
767
+ header.field_hash_table_offset.unwrap().get()
768
+ - std::mem::size_of::<ObjectHeader>() as u64,
769
+ )
770
+ .unwrap();
771
+ let size = header.field_hash_table_size.unwrap().get()
772
+ + std::mem::size_of::<ObjectHeader>() as u64;
773
+
774
+ let object_header = jf.object_header_mut(offset)?;
775
+ object_header.type_ = ObjectType::FieldHashTable as u8;
776
+ object_header.size = size
777
+ }
778
+
779
+ Ok(jf)
780
+ }
781
+
782
+ pub fn journal_header_mut(&mut self) -> &mut JournalHeader {
783
+ JournalHeader::mut_from_prefix(&mut self.header_map)
784
+ .unwrap()
785
+ .0
786
+ }
787
+
788
+ pub fn data_hash_table_mut(&mut self) -> Option<DataHashTable<&mut [u8]>> {
789
+ self.data_hash_table_map
790
+ .as_mut()
791
+ .and_then(|m| DataHashTable::<&mut [u8]>::from_data_mut(m, false))
792
+ }
793
+
794
+ pub fn field_hash_table_mut(&mut self) -> Option<FieldHashTable<&mut [u8]>> {
795
+ self.field_hash_table_map
796
+ .as_mut()
797
+ .and_then(|m| FieldHashTable::<&mut [u8]>::from_data_mut(m, false))
798
+ }
799
+
800
+ fn object_header_mut(&self, offset: NonZeroU64) -> Result<&mut ObjectHeader> {
801
+ let size_needed = std::mem::size_of::<ObjectHeader>() as u64;
802
+ let window_manager = unsafe { &mut *self.window_manager.get() };
803
+ let header_slice = window_manager.get_slice_mut(offset.get(), size_needed)?;
804
+ Ok(ObjectHeader::mut_from_bytes(header_slice).unwrap())
805
+ }
806
+
807
+ fn object_data_mut(&self, offset: NonZeroU64, size_needed: u64) -> Result<&mut [u8]> {
808
+ let window_manager = unsafe { &mut *self.window_manager.get() };
809
+ let object_slice = window_manager.get_slice_mut(offset.get(), size_needed)?;
810
+ Ok(object_slice)
811
+ }
812
+
813
+ fn journal_object_mut<'a, T>(
814
+ &'a self,
815
+ type_: ObjectType,
816
+ offset: NonZeroU64,
817
+ size: Option<u64>,
818
+ ) -> Result<ValueGuard<'a, T>>
819
+ where
820
+ T: JournalObjectMut<&'a mut [u8]>,
821
+ {
822
+ // Check if any object is already in use
823
+ let mut is_in_use = self.object_in_use.borrow_mut();
824
+ if *is_in_use {
825
+ #[cfg(debug_assertions)]
826
+ {
827
+ eprintln!(
828
+ "Value is in use. Current Backtrace: {:?}, Previous Backtrace: {:?}",
829
+ self.backtrace.borrow().to_string(),
830
+ self.prev_backtrace.borrow().to_string()
831
+ );
832
+ }
833
+ return Err(JournalError::ValueGuardInUse);
834
+ }
835
+
836
+ #[cfg(debug_assertions)]
837
+ {
838
+ self.backtrace.swap(&self.prev_backtrace);
839
+ let _ = self.backtrace.replace(Backtrace::force_capture());
840
+ }
841
+
842
+ let is_compact = self
843
+ .journal_header_ref()
844
+ .has_incompatible_flag(HeaderIncompatibleFlags::Compact);
845
+
846
+ let size_needed = match size {
847
+ Some(size) => {
848
+ let header = self.object_header_mut(offset)?;
849
+ header.type_ = type_ as u8;
850
+ header.size = size;
851
+ size
852
+ }
853
+ None => {
854
+ let header = self.object_header_ref(offset)?;
855
+ if header.type_ != type_ as u8 {
856
+ return Err(JournalError::InvalidObjectType);
857
+ }
858
+ header.size
859
+ }
860
+ };
861
+
862
+ let data = self.object_data_mut(offset, size_needed)?;
863
+ let value = T::from_data_mut(data, is_compact).ok_or(JournalError::ZerocopyFailure)?;
864
+
865
+ // Mark as in use
866
+ *is_in_use = true;
867
+ Ok(ValueGuard::new(offset, value, &self.object_in_use))
868
+ }
869
+
870
+ pub fn offset_array_mut(
871
+ &self,
872
+ offset: NonZeroU64,
873
+ capacity: Option<NonZeroU64>,
874
+ ) -> Result<ValueGuard<'_, OffsetArrayObject<&mut [u8]>>> {
875
+ let size = capacity.map(|c| {
876
+ let mut size = std::mem::size_of::<OffsetArrayObjectHeader>() as u64;
877
+
878
+ let is_compact = self
879
+ .journal_header_ref()
880
+ .has_incompatible_flag(HeaderIncompatibleFlags::Compact);
881
+ if is_compact {
882
+ size += c.get() * std::mem::size_of::<u32>() as u64;
883
+ } else {
884
+ size += c.get() * std::mem::size_of::<u64>() as u64;
885
+ }
886
+
887
+ size
888
+ });
889
+
890
+ let offset_array = self.journal_object_mut(ObjectType::EntryArray, offset, size);
891
+ offset_array
892
+ }
893
+
894
+ pub fn field_mut(
895
+ &self,
896
+ offset: NonZeroU64,
897
+ size: Option<u64>,
898
+ ) -> Result<ValueGuard<'_, FieldObject<&mut [u8]>>> {
899
+ let size = size.map(|n| std::mem::size_of::<FieldObjectHeader>() as u64 + n);
900
+ self.journal_object_mut(ObjectType::Field, offset, size)
901
+ }
902
+
903
+ pub fn entry_mut(
904
+ &self,
905
+ offset: NonZeroU64,
906
+ size: Option<u64>,
907
+ ) -> Result<ValueGuard<'_, EntryObject<&mut [u8]>>> {
908
+ let size = size.map(|n| std::mem::size_of::<DataObjectHeader>() as u64 + n);
909
+ self.journal_object_mut(ObjectType::Entry, offset, size)
910
+ }
911
+
912
+ pub fn data_mut(
913
+ &self,
914
+ offset: NonZeroU64,
915
+ size: Option<u64>,
916
+ ) -> Result<ValueGuard<'_, DataObject<&mut [u8]>>> {
917
+ let size = size.map(|n| std::mem::size_of::<DataObjectHeader>() as u64 + n);
918
+ self.journal_object_mut(ObjectType::Data, offset, size)
919
+ }
920
+
921
+ pub fn tag_mut(
922
+ &self,
923
+ offset: NonZeroU64,
924
+ new: bool,
925
+ ) -> Result<ValueGuard<'_, TagObject<&mut [u8]>>> {
926
+ let size = if new {
927
+ Some(std::mem::size_of::<TagObjectHeader>() as u64)
928
+ } else {
929
+ None
930
+ };
931
+ self.journal_object_mut(ObjectType::Tag, offset, size)
932
+ }
933
+}
934
+
935
+macro_rules! impl_hash_table_set_tail_offset {
936
+ (
937
+ $method_name:ident,
938
+ $hash_table_ref:ident,
939
+ $hash_table_mut:ident,
940
+ $object_mut:ident
941
+ ) => {
942
+ pub fn $method_name(&mut self, hash: u64, object_offset: NonZeroU64) -> Result<()> {
943
+ let hash_item = {
944
+ let Some(ht) = self.$hash_table_ref() else {
945
+ return Err(JournalError::MissingHashTable);
946
+ };
947
+ *ht.hash_item_ref(hash)
948
+ };
949
+
950
+ if let Some(tail_hash_offset) = hash_item.tail_hash_offset {
951
+ let mut tail_object = self.$object_mut(tail_hash_offset, None)?;
952
+ tail_object.set_next_hash_offset(object_offset);
953
+ }
954
+
955
+ let Some(mut ht) = self.$hash_table_mut() else {
956
+ return Err(JournalError::MissingHashTable);
957
+ };
958
+
959
+ let hash_item = ht.hash_item_mut(hash);
960
+ if hash_item.head_hash_offset.is_none() {
961
+ hash_item.head_hash_offset = Some(object_offset);
962
+ }
963
+ hash_item.tail_hash_offset = Some(object_offset);
964
+
965
+ Ok(())
966
+ }
967
+ };
968
+}
969
+
970
+impl<M: MemoryMapMut> JournalFile<M> {
971
+ impl_hash_table_set_tail_offset!(
972
+ data_hash_table_set_tail_offset,
973
+ data_hash_table_ref,
974
+ data_hash_table_mut,
975
+ data_mut
976
+ );
977
+
978
+ impl_hash_table_set_tail_offset!(
979
+ field_hash_table_set_tail_offset,
980
+ field_hash_table_ref,
981
+ field_hash_table_mut,
982
+ field_mut
983
+ );
984
+}
985
+
986
+/// Iterator that walks through all field objects in the field hash table
987
+pub struct FieldIterator<'a, M: MemoryMap> {
988
+ journal: &'a JournalFile<M>,
989
+ field_hash_table: Option<FieldHashTable<&'a [u8]>>,
990
+ current_bucket_index: usize,
991
+ next_field_offset: Option<NonZeroU64>,
992
+}
993
+
994
+impl<M: MemoryMap> FieldIterator<'_, M> {
995
+ /// Advances to the next non-empty bucket
996
+ fn advance_to_next_nonempty_bucket(&mut self) {
997
+ // If we don't have a hash table, there's nothing to iterate
998
+ let Some(hash_table) = &self.field_hash_table else {
999
+ return;
1000
+ };
1001
+
1002
+ let items = &hash_table.items;
1003
+
1004
+ // Find the next non-empty bucket
1005
+ while self.current_bucket_index < items.len() {
1006
+ let bucket = items[self.current_bucket_index];
1007
+ if bucket.head_hash_offset.is_some() {
1008
+ self.next_field_offset = bucket.head_hash_offset;
1009
+ return;
1010
+ }
1011
+ self.current_bucket_index += 1;
1012
+ }
1013
+
1014
+ // No more non-empty buckets
1015
+ self.next_field_offset = None;
1016
+ }
1017
+}
1018
+
1019
+impl<'a, M: MemoryMap> Iterator for FieldIterator<'a, M> {
1020
+ type Item = Result<ValueGuard<'a, FieldObject<&'a [u8]>>>;
1021
+
1022
+ fn next(&mut self) -> Option<Self::Item> {
1023
+ let offset = self.next_field_offset?;
1024
+
1025
+ match self.journal.field_ref(offset) {
1026
+ Ok(field_guard) => {
1027
+ // Get the next field offset before we return the guard
1028
+ self.next_field_offset = field_guard.header.next_hash_offset;
1029
+
1030
+ // If we've reached the end of the chain, move to the next bucket
1031
+ if self.next_field_offset.is_none() {
1032
+ self.current_bucket_index += 1;
1033
+ self.advance_to_next_nonempty_bucket();
1034
+ }
1035
+
1036
+ Some(Ok(field_guard))
1037
+ }
1038
+ Err(e) => {
1039
+ self.next_field_offset = None;
1040
+ Some(Err(e))
1041
+ }
1042
+ }
1043
+ }
1044
+}
1045
+
1046
+/// Iterator that walks through all DATA objects for a specific field
1047
+pub struct FieldDataIterator<'a, M: MemoryMap> {
1048
+ journal: &'a JournalFile<M>,
1049
+ current_data_offset: Option<NonZeroU64>,
1050
+}
1051
+
1052
+impl<'a, M: MemoryMap> Iterator for FieldDataIterator<'a, M> {
1053
+ type Item = Result<ValueGuard<'a, DataObject<&'a [u8]>>>;
1054
+
1055
+ fn next(&mut self) -> Option<Self::Item> {
1056
+ let data_offset = self.current_data_offset?;
1057
+
1058
+ match self.journal.data_ref(data_offset) {
1059
+ Ok(data_guard) => {
1060
+ // Get the next data offset before we return the guard
1061
+ self.current_data_offset = data_guard.header.next_field_offset;
1062
+ Some(Ok(data_guard))
1063
+ }
1064
+ Err(e) => {
1065
+ self.current_data_offset = None;
1066
+ Some(Err(e))
1067
+ }
1068
+ }
1069
+ }
1070
+}
1071
+
1072
+/// Iterator that walks through all DATA objects for a specific entry
1073
+pub struct EntryDataIterator<'a, M: MemoryMap> {
1074
+ journal: &'a JournalFile<M>,
1075
+ entry_offset: Option<NonZeroU64>,
1076
+ current_index: usize,
1077
+ total_items: usize,
1078
+}
1079
+
1080
+impl<'a, M: MemoryMap> Iterator for EntryDataIterator<'a, M> {
1081
+ type Item = Result<ValueGuard<'a, DataObject<&'a [u8]>>>;
1082
+
1083
+ fn next(&mut self) -> Option<Self::Item> {
1084
+ let entry_offset = self.entry_offset?;
1085
+
1086
+ // If we've reached the end of the data indices, return None
1087
+ if self.current_index >= self.total_items {
1088
+ return None;
1089
+ }
1090
+
1091
+ // Get the entry object to access the data offset
1092
+ match self.journal.entry_ref(entry_offset) {
1093
+ Ok(entry_guard) => {
1094
+ let idx = self.current_index;
1095
+ self.current_index += 1;
1096
+
1097
+ let data_offset = match &entry_guard.items {
1098
+ EntryItemsType::Regular(items) => {
1099
+ if idx >= items.len() {
1100
+ return None;
1101
+ }
1102
+ items[idx].object_offset
1103
+ }
1104
+ EntryItemsType::Compact(items) => {
1105
+ if idx >= items.len() {
1106
+ return None;
1107
+ }
1108
+ items[idx].object_offset as u64
1109
+ }
1110
+ };
1111
+
1112
+ let data_offset = NonZeroU64::new(data_offset)?;
1113
+
1114
+ // Drop the entry guard before obtaining the data object
1115
+ drop(entry_guard);
1116
+
1117
+ // Try to get the data object
1118
+ match self.journal.data_ref(data_offset) {
1119
+ Ok(data_guard) => Some(Ok(data_guard)),
1120
+ Err(e) => Some(Err(e)),
1121
+ }
1122
+ }
1123
+ Err(e) => {
1124
+ // If we can't read the entry, return the error and stop iteration
1125
+ self.current_index = self.total_items;
1126
+ Some(Err(e))
1127
+ }
1128
+ }
1129
+ }
1130
+}