Skip to main content

journal_core/file/
object.rs

1use crate::error::{JournalError, Result};
2use crate::file::object_compression::{
3    clear_compression_error, decompress_lz4_payload, decompress_xz_payload, decompress_zstd_payload,
4};
5use crate::file::offset_array::{Cursor, InlinedCursor, List};
6use std::num::{NonZeroU32, NonZeroU64, NonZeroUsize};
7use zerocopy::{
8    ByteSlice, ByteSliceMut, FromBytes, Immutable, IntoBytes, KnownLayout, Ref, SplitByteSlice,
9    SplitByteSliceMut,
10};
11
12pub use super::object_hash::{
13    DataHashTable, FieldHashTable, HashTable, HashTableMut, HashableObject, HashableObjectMut,
14};
15
16pub trait JournalObject<B: SplitByteSlice>: Sized {
17    /// Create a new journal object from a byte slice
18    fn from_data(data: B, is_compact: bool) -> Option<Self>;
19}
20
21pub trait JournalObjectMut<B: SplitByteSliceMut>: JournalObject<B> {
22    /// Create a new journal object from a byte slice
23    fn from_data_mut(data: B, is_compact: bool) -> Option<Self>;
24}
25
26pub enum HeaderIncompatibleFlags {
27    CompressedXz = 1 << 0,
28    CompressedLz4 = 1 << 1,
29    KeyedHash = 1 << 2,
30    CompressedZstd = 1 << 3,
31    Compact = 1 << 4,
32}
33
34pub enum HeaderCompatibleFlags {
35    Sealed = 1 << 0,
36    TailEntryBootId = 1 << 1,
37    SealedContinuous = 1 << 2,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum JournalState {
42    Offline = 0,
43    Online = 1,
44    Archived = 2,
45}
46
47impl TryFrom<u8> for JournalState {
48    type Error = JournalError;
49
50    fn try_from(value: u8) -> Result<Self> {
51        match value {
52            0 => Ok(JournalState::Offline),
53            1 => Ok(JournalState::Online),
54            2 => Ok(JournalState::Archived),
55            _ => Err(JournalError::InvalidJournalFileState),
56        }
57    }
58}
59
60impl std::fmt::Display for JournalState {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            JournalState::Offline => write!(f, "OFFLINE"),
64            JournalState::Online => write!(f, "ONLINE"),
65            JournalState::Archived => write!(f, "ARCHIVED"),
66        }
67    }
68}
69
70#[derive(Default, Debug, Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)]
71#[repr(C)]
72pub struct JournalHeader {
73    pub signature: [u8; 8],                          // "LPKSHHRH"
74    pub compatible_flags: u32,                       // Compatible extension flags
75    pub incompatible_flags: u32,                     // Incompatible extension flags
76    pub state: u8,                                   // File state (offline=0, online=1, archived=2)
77    pub reserved: [u8; 7],                           // Reserved space
78    pub file_id: [u8; 16],                           // Unique ID for this file
79    pub machine_id: [u8; 16],                        // Machine ID this belongs to
80    pub tail_entry_boot_id: [u8; 16],                // Boot ID of the last entry
81    pub seqnum_id: [u8; 16],                         // Sequence number ID
82    pub header_size: u64,                            // Size of the header (272 in v260+)
83    pub arena_size: u64,                             // Size of the data arena
84    pub data_hash_table_offset: Option<NonZeroU64>,  // Offset of the data hash table
85    pub data_hash_table_size: Option<NonZeroU64>,    // Size of the data hash table
86    pub field_hash_table_offset: Option<NonZeroU64>, // Offset of the field hash table
87    pub field_hash_table_size: Option<NonZeroU64>,   // Size of the field hash table
88    pub tail_object_offset: Option<NonZeroU64>,      // Offset of the last object
89    pub n_objects: u64,                              // Number of objects
90    pub n_entries: u64,                              // Number of entries
91    pub tail_entry_seqnum: u64,                      // Sequence number of the last entry
92    pub head_entry_seqnum: u64,                      // Sequence number of the first entry
93    pub entry_array_offset: Option<NonZeroU64>,      // Offset of the entry array
94    pub head_entry_realtime: u64,                    // Realtime timestamp of the first entry
95    pub tail_entry_realtime: u64,                    // Realtime timestamp of the last entry
96    pub tail_entry_monotonic: u64,                   // Monotonic timestamp of the last entry
97    // Added in 187
98    pub n_data: u64,   // Number of data objects
99    pub n_fields: u64, // Number of field objects
100    // Added in 189
101    pub n_tags: u64,         // Number of tag objects
102    pub n_entry_arrays: u64, // Number of entry array objects
103    // Added in 246
104    pub data_hash_chain_depth: u64, // Deepest chain in data hash table
105    pub field_hash_chain_depth: u64, // Deepest chain in field hash table
106    // Added in 252
107    pub tail_entry_array_offset: u32, // Offset to the tail entry array
108    pub tail_entry_array_n_entries: u32, // Number of entries in the tail entry array
109    // Added in 254
110    pub tail_entry_offset: u64, // Offset to the tail entry
111}
112
113impl JournalHeader {
114    fn declared_arena_end(&self) -> Result<u64> {
115        if self.header_size < 208 || self.header_size % 8 != 0 {
116            return Err(JournalError::ObjectExceedsFileBounds);
117        }
118        self.header_size
119            .checked_add(self.arena_size)
120            .ok_or(JournalError::ObjectExceedsFileBounds)
121    }
122
123    /// Bounds mappings without requiring a live writer's future allocation to exist.
124    pub(super) fn validate_reader_mappings(&self, file_size: u64) -> Result<()> {
125        let end = self.declared_arena_end()?.min(file_size);
126        if self.header_size > end {
127            return Err(JournalError::ObjectExceedsFileBounds);
128        }
129        for (offset, size) in [
130            (self.data_hash_table_offset, self.data_hash_table_size),
131            (self.field_hash_table_offset, self.field_hash_table_size),
132        ] {
133            match (offset, size) {
134                (None, None) => {}
135                (Some(offset), Some(size)) => {
136                    let offset = offset.get();
137                    let size = size.get();
138                    if offset < self.header_size
139                        || offset - self.header_size < 16
140                        || offset % 8 != 0
141                        || size < 16
142                        || size % 16 != 0
143                        || offset > end
144                        || size > end - offset
145                    {
146                        return Err(JournalError::ObjectExceedsFileBounds);
147                    }
148                }
149                _ => return Err(JournalError::InvalidObjectLocation),
150            }
151        }
152        Ok(())
153    }
154
155    /// Validates stable declared extents for writer-excluded capture and recovery.
156    #[doc(hidden)]
157    pub fn validated_arena_end(&self, file_size: u64) -> Result<u64> {
158        self.validate_reader_mappings(file_size)?;
159        let end = self.declared_arena_end()?;
160        if end > file_size {
161            return Err(JournalError::ObjectExceedsFileBounds);
162        }
163        let tail = self.tail_object_offset.map_or(0, NonZeroU64::get);
164        if tail != 0 && (tail < self.header_size || tail % 8 != 0 || tail > end || end - tail < 16)
165        {
166            return Err(JournalError::ObjectExceedsFileBounds);
167        }
168        for offset in [self.data_hash_table_offset, self.field_hash_table_offset]
169            .into_iter()
170            .flatten()
171        {
172            if offset.get() - 16 > tail {
173                return Err(JournalError::ObjectExceedsFileBounds);
174            }
175        }
176        Ok(end)
177    }
178
179    /// Empty files may inherit a sequence counter, but not per-file ENTRY state.
180    #[doc(hidden)]
181    pub fn validate_empty_entry_metadata(&self) -> Result<()> {
182        if self.n_entries != 0 {
183            return Ok(());
184        }
185        if self.head_entry_seqnum != 0
186            || self.entry_array_offset.is_some()
187            || self.head_entry_realtime != 0
188            || self.tail_entry_realtime != 0
189            || self.tail_entry_monotonic != 0
190            || (self.header_size >= 264
191                && (self.tail_entry_array_offset != 0 || self.tail_entry_array_n_entries != 0))
192            || (self.header_size >= 272 && self.tail_entry_offset != 0)
193            || (self.header_size >= 272
194                && self.has_compatible_flag(HeaderCompatibleFlags::TailEntryBootId)
195                && self.tail_entry_boot_id != [0; 16])
196        {
197            return Err(JournalError::InvalidObjectLocation);
198        }
199        Ok(())
200    }
201    pub fn has_incompatible_flag(&self, flag: HeaderIncompatibleFlags) -> bool {
202        (self.incompatible_flags & flag as u32) != 0
203    }
204
205    pub fn has_compatible_flag(&self, flag: HeaderCompatibleFlags) -> bool {
206        (self.compatible_flags & flag as u32) != 0
207    }
208}
209
210pub enum ObjectFlags {
211    CompressedXz = 1 << 0,
212    CompressedLz4 = 1 << 1,
213    CompressedZstd = 1 << 2,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217#[repr(u8)]
218pub enum ObjectType {
219    Unused = 0,
220    Data = 1,
221    Field = 2,
222    Entry = 3,
223    DataHashTable = 4,
224    FieldHashTable = 5,
225    EntryArray = 6,
226    Tag = 7,
227}
228
229impl TryFrom<u8> for ObjectType {
230    type Error = JournalError;
231
232    fn try_from(value: u8) -> Result<Self> {
233        match value {
234            0 => Ok(ObjectType::Unused),
235            1 => Ok(ObjectType::Data),
236            2 => Ok(ObjectType::Field),
237            3 => Ok(ObjectType::Entry),
238            4 => Ok(ObjectType::DataHashTable),
239            5 => Ok(ObjectType::FieldHashTable),
240            6 => Ok(ObjectType::EntryArray),
241            7 => Ok(ObjectType::Tag),
242            _ => Err(JournalError::InvalidObjectType),
243        }
244    }
245}
246
247#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
248#[repr(C)]
249pub struct ObjectHeader {
250    pub type_: u8,
251    pub flags: u8,
252    pub reserved: [u8; 6],
253    pub size: u64,
254}
255
256impl ObjectHeader {
257    pub fn xz_compressed(&self) -> bool {
258        (self.flags & ObjectFlags::CompressedXz as u8) != 0
259    }
260
261    pub fn lz4_compressed(&self) -> bool {
262        (self.flags & ObjectFlags::CompressedLz4 as u8) != 0
263    }
264
265    pub fn zstd_compressed(&self) -> bool {
266        (self.flags & ObjectFlags::CompressedZstd as u8) != 0
267    }
268
269    pub fn is_compressed(&self) -> bool {
270        self.zstd_compressed() | self.lz4_compressed() | self.xz_compressed()
271    }
272
273    pub fn aligned_size(&self) -> u64 {
274        (self.size + 7) & !7
275    }
276
277    /// Validates that the object size is sane.
278    ///
279    /// Returns the size if valid, or an error if the size is invalid.
280    /// This should be called when reading an ObjectHeader from a journal file
281    /// to protect against corrupted data.
282    pub fn validated_size(&self) -> crate::error::Result<u64> {
283        let min_size = std::mem::size_of::<ObjectHeader>() as u64;
284
285        if self.size < min_size {
286            return Err(crate::error::JournalError::InvalidObjectSize(self.size));
287        }
288
289        Ok(self.size)
290    }
291}
292
293#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
294#[repr(C)]
295pub struct FieldObjectHeader {
296    pub object_header: ObjectHeader,
297    pub hash: u64,
298    pub next_hash_offset: Option<NonZeroU64>,
299    pub head_data_offset: Option<NonZeroU64>,
300}
301
302#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
303#[repr(C)]
304pub struct OffsetArrayObjectHeader {
305    pub object_header: ObjectHeader,
306    pub next_offset_array: Option<NonZeroU64>,
307}
308
309#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
310#[repr(C)]
311pub struct HashItem {
312    pub head_hash_offset: Option<NonZeroU64>,
313    pub tail_hash_offset: Option<NonZeroU64>,
314}
315
316#[derive(Debug)]
317pub struct FieldObject<B: ByteSlice> {
318    pub header: Ref<B, FieldObjectHeader>,
319    pub payload: B,
320}
321
322impl<B: SplitByteSlice> JournalObject<B> for FieldObject<B> {
323    fn from_data(data: B, _is_compact: bool) -> Option<Self> {
324        let (header, payload) = zerocopy::Ref::from_prefix(data).ok()?;
325        Some(FieldObject { header, payload })
326    }
327}
328
329impl<B: SplitByteSliceMut> JournalObjectMut<B> for FieldObject<B> {
330    fn from_data_mut(data: B, _is_compact: bool) -> Option<Self> {
331        let (header, payload) = zerocopy::Ref::from_prefix(data).ok()?;
332        Some(FieldObject { header, payload })
333    }
334}
335
336pub enum OffsetsType<B: ByteSlice> {
337    Regular(Ref<B, [Option<NonZeroU64>]>),
338    Compact(Ref<B, [Option<NonZeroU32>]>),
339}
340
341impl<B: ByteSlice> OffsetsType<B> {
342    pub fn get(&self, index: usize) -> Option<NonZeroU64> {
343        match self {
344            OffsetsType::Regular(offsets) => offsets[index],
345            OffsetsType::Compact(offsets) => offsets[index].map(NonZeroU64::from),
346        }
347    }
348}
349
350impl<B: ByteSliceMut> OffsetsType<B> {
351    pub fn set(&mut self, index: usize, value: NonZeroU64) {
352        match self {
353            OffsetsType::Regular(offsets) => offsets[index] = Some(value),
354            OffsetsType::Compact(offsets) => {
355                assert!(value.get() <= u32::MAX as u64);
356                offsets[index] = NonZeroU32::new(value.get() as u32);
357            }
358        }
359    }
360}
361
362impl<B: ByteSlice> std::fmt::Debug for OffsetsType<B> {
363    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364        match self {
365            OffsetsType::Regular(items) => write!(f, "Regular({} items)", items.len()),
366            OffsetsType::Compact(items) => write!(f, "Compact({} items)", items.len()),
367        }
368    }
369}
370
371pub struct OffsetArrayObject<B: ByteSlice> {
372    pub header: Ref<B, OffsetArrayObjectHeader>,
373    pub items: OffsetsType<B>,
374}
375
376impl<B: ByteSlice> OffsetArrayObject<B> {
377    pub fn capacity(&self) -> usize {
378        match &self.items {
379            OffsetsType::Regular(offsets) => offsets.len(),
380            OffsetsType::Compact(offsets) => offsets.len(),
381        }
382    }
383
384    pub fn len(&self, remaining_items: usize) -> usize {
385        self.capacity().min(remaining_items)
386    }
387
388    pub fn is_empty(&self, remaining_items: usize) -> bool {
389        self.len(remaining_items) == 0
390    }
391
392    pub fn get(&self, index: usize, remaining_items: usize) -> Result<Option<NonZeroU64>> {
393        if self.is_empty(remaining_items) {
394            return Err(JournalError::EmptyOffsetArrayNode);
395        }
396
397        Ok(self.items.get(index))
398    }
399
400    pub fn collect_offsets(
401        &self,
402        start_index: usize,
403        remaining_items: usize,
404        offsets: &mut Vec<NonZeroU64>,
405    ) -> Result<()> {
406        let len = self.len(remaining_items);
407
408        if start_index >= len {
409            return Err(JournalError::InvalidOffsetArrayIndex);
410        }
411
412        match &self.items {
413            OffsetsType::Regular(s) => {
414                offsets.extend(s[start_index..len].iter().filter_map(|&opt| opt));
415            }
416            OffsetsType::Compact(s) => {
417                offsets.extend(
418                    s[start_index..len]
419                        .iter()
420                        .filter_map(|&opt| opt.map(NonZeroU64::from)),
421                );
422            }
423        }
424
425        Ok(())
426    }
427}
428
429impl<B: ByteSliceMut> OffsetArrayObject<B> {
430    pub fn set(&mut self, index: usize, offset: NonZeroU64) -> Result<()> {
431        if index >= self.capacity() {
432            return Err(JournalError::OutOfBoundsIndex);
433        }
434
435        self.items.set(index, offset);
436        Ok(())
437    }
438}
439
440impl<B: ByteSlice> std::fmt::Debug for OffsetArrayObject<B> {
441    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442        f.debug_struct("JournalHeader")
443            .field("header", &self.header)
444            .finish()
445    }
446}
447
448impl<B: SplitByteSlice> JournalObject<B> for OffsetArrayObject<B> {
449    fn from_data(data: B, is_compact: bool) -> Option<Self> {
450        let (header_data, items_data) = data
451            .split_at(std::mem::size_of::<OffsetArrayObjectHeader>())
452            .ok()?;
453
454        let header = zerocopy::Ref::from_bytes(header_data).ok()?;
455
456        let items_type = if is_compact {
457            let compact_items = zerocopy::Ref::from_bytes(items_data).ok()?;
458            OffsetsType::Compact(compact_items)
459        } else {
460            let regular_items = zerocopy::Ref::from_bytes(items_data).ok()?;
461            OffsetsType::Regular(regular_items)
462        };
463
464        Some(OffsetArrayObject {
465            header,
466            items: items_type,
467        })
468    }
469}
470
471impl<B: SplitByteSliceMut> JournalObjectMut<B> for OffsetArrayObject<B> {
472    fn from_data_mut(data: B, is_compact: bool) -> Option<Self> {
473        let (header_data, items_data) = data
474            .split_at(std::mem::size_of::<OffsetArrayObjectHeader>())
475            .ok()?;
476
477        let header = zerocopy::Ref::from_bytes(header_data).ok()?;
478
479        let items_type = if is_compact {
480            let compact_items = zerocopy::Ref::from_bytes(items_data).ok()?;
481            OffsetsType::Compact(compact_items)
482        } else {
483            let regular_items = zerocopy::Ref::from_bytes(items_data).ok()?;
484            OffsetsType::Regular(regular_items)
485        };
486
487        Some(OffsetArrayObject {
488            header,
489            items: items_type,
490        })
491    }
492}
493
494#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
495#[repr(C)]
496pub struct EntryObjectHeader {
497    pub object_header: ObjectHeader,
498    pub seqnum: u64,
499    pub realtime: u64,
500    pub monotonic: u64,
501    pub boot_id: [u8; 16], // UUID/128-bit ID
502    pub xor_hash: u64,
503}
504
505// For regular (non-compact) format - an array of these follows the header
506#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
507#[repr(C)]
508pub struct RegularEntryItem {
509    pub object_offset: u64,
510    pub hash: u64,
511}
512
513// For compact format - an array of these follows the header
514#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
515#[repr(C)]
516pub struct CompactEntryItem {
517    pub object_offset: u32,
518}
519
520pub enum EntryItemsType<B: ByteSlice> {
521    Regular(Ref<B, [RegularEntryItem]>),
522    Compact(Ref<B, [CompactEntryItem]>),
523}
524
525impl<B: ByteSliceMut> EntryItemsType<B> {
526    pub fn set(&mut self, index: usize, object_offset: NonZeroU64, hash: Option<u64>) {
527        match self {
528            EntryItemsType::Regular(entry_items) => {
529                entry_items[index].object_offset = object_offset.get();
530                entry_items[index].hash = hash.unwrap();
531            }
532            EntryItemsType::Compact(entry_items) => {
533                debug_assert!(hash.is_none());
534                assert!(object_offset.get() <= u32::MAX as u64);
535                entry_items[index].object_offset = object_offset.get() as u32;
536            }
537        }
538    }
539}
540
541impl<B: ByteSlice> EntryItemsType<B> {
542    pub fn get(&self, index: usize) -> u64 {
543        match self {
544            EntryItemsType::Regular(entry_items) => entry_items[index].object_offset,
545            EntryItemsType::Compact(entry_items) => entry_items[index].object_offset as u64,
546        }
547    }
548
549    pub fn len(&self) -> usize {
550        match self {
551            EntryItemsType::Regular(entry_items) => entry_items.len(),
552            EntryItemsType::Compact(entry_items) => entry_items.len(),
553        }
554    }
555
556    pub fn is_empty(&self) -> bool {
557        match self {
558            EntryItemsType::Regular(entry_items) => entry_items.is_empty(),
559            EntryItemsType::Compact(entry_items) => entry_items.is_empty(),
560        }
561    }
562}
563
564impl<B: ByteSlice> std::fmt::Debug for EntryItemsType<B> {
565    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566        match self {
567            EntryItemsType::Regular(items) => write!(f, "Regular({} items)", items.len()),
568            EntryItemsType::Compact(items) => write!(f, "Compact({} items)", items.len()),
569        }
570    }
571}
572
573pub struct EntryObject<B: ByteSlice> {
574    pub header: Ref<B, EntryObjectHeader>,
575    pub items: EntryItemsType<B>,
576}
577
578impl<B: ByteSlice> EntryObject<B> {
579    pub fn collect_offsets(&self, offsets: &mut Vec<NonZeroU64>) -> Result<()> {
580        match &self.items {
581            EntryItemsType::Regular(items) => {
582                offsets.reserve(items.len());
583
584                for item in items.iter() {
585                    let offset =
586                        NonZeroU64::new(item.object_offset).ok_or(JournalError::InvalidOffset)?;
587                    offsets.push(offset);
588                }
589            }
590            EntryItemsType::Compact(items) => {
591                offsets.reserve(items.len());
592
593                for item in items.iter() {
594                    let offset = NonZeroU64::new(item.object_offset as u64)
595                        .ok_or(JournalError::InvalidOffset)?;
596                    offsets.push(offset);
597                }
598            }
599        }
600
601        Ok(())
602    }
603}
604
605impl<B: ByteSlice> std::fmt::Debug for EntryObject<B> {
606    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607        f.debug_struct("EntryObject")
608            .field("header", &self.header)
609            .field("items", &self.items)
610            .finish()
611    }
612}
613
614impl<B: SplitByteSlice> JournalObject<B> for EntryObject<B> {
615    fn from_data(data: B, is_compact: bool) -> Option<Self> {
616        let (header_data, items_data) = data
617            .split_at(std::mem::size_of::<EntryObjectHeader>())
618            .ok()?;
619
620        let header = zerocopy::Ref::from_bytes(header_data).ok()?;
621
622        let items_type = if is_compact {
623            let compact_items = zerocopy::Ref::from_bytes(items_data).ok()?;
624            EntryItemsType::Compact(compact_items)
625        } else {
626            let regular_items = zerocopy::Ref::from_bytes(items_data).ok()?;
627            EntryItemsType::Regular(regular_items)
628        };
629
630        Some(EntryObject {
631            header,
632            items: items_type,
633        })
634    }
635}
636
637impl<B: SplitByteSliceMut> JournalObjectMut<B> for EntryObject<B> {
638    fn from_data_mut(data: B, is_compact: bool) -> Option<Self> {
639        let (header_data, items_data) = data
640            .split_at(std::mem::size_of::<EntryObjectHeader>())
641            .ok()?;
642
643        let header = zerocopy::Ref::from_bytes(header_data).ok()?;
644
645        let items_type = if is_compact {
646            let compact_items = zerocopy::Ref::from_bytes(items_data).ok()?;
647            EntryItemsType::Compact(compact_items)
648        } else {
649            let regular_items = zerocopy::Ref::from_bytes(items_data).ok()?;
650            EntryItemsType::Regular(regular_items)
651        };
652
653        Some(EntryObject {
654            header,
655            items: items_type,
656        })
657    }
658}
659
660#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
661#[repr(C)]
662pub struct DataObjectHeader {
663    pub object_header: ObjectHeader,
664    pub hash: u64,
665    pub next_hash_offset: Option<NonZeroU64>,
666    pub next_field_offset: Option<NonZeroU64>,
667    pub entry_offset: Option<NonZeroU64>,
668    pub entry_array_offset: Option<NonZeroU64>,
669    pub n_entries: Option<NonZeroU64>,
670}
671
672impl DataObjectHeader {
673    pub fn xz_compressed(&self) -> bool {
674        self.object_header.xz_compressed()
675    }
676
677    pub fn lz4_compressed(&self) -> bool {
678        self.object_header.lz4_compressed()
679    }
680
681    pub fn zstd_compressed(&self) -> bool {
682        self.object_header.zstd_compressed()
683    }
684
685    pub fn is_compressed(&self) -> bool {
686        self.object_header.is_compressed()
687    }
688
689    pub fn inlined_cursor(&self) -> Option<InlinedCursor> {
690        let inlined_offset = self.entry_offset?;
691        let cursor = match self.n_entries?.get() {
692            1 => None,
693            n => {
694                let total_items = NonZeroUsize::new(n as usize - 1)?;
695                Some(Cursor::at_head(List::new(
696                    self.entry_array_offset?,
697                    total_items,
698                )))
699            }
700        };
701        Some(InlinedCursor::new(inlined_offset, cursor))
702    }
703}
704
705#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable, PartialEq, Eq)]
706#[repr(C)]
707pub struct CompactDataFields {
708    pub tail_entry_array_offset: u32,
709    pub tail_entry_array_n_entries: u32,
710}
711
712#[derive(PartialEq, Eq)]
713pub enum DataPayloadType<B: ByteSlice> {
714    Regular(B),
715    Compact {
716        compact_fields: Ref<B, CompactDataFields>,
717        payload: B,
718    },
719}
720
721impl<B: ByteSlice> std::fmt::Debug for DataPayloadType<B> {
722    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
723        match self {
724            DataPayloadType::Regular(payload) => write!(f, "Regular({} bytes)", payload.len()),
725            DataPayloadType::Compact {
726                compact_fields,
727                payload,
728            } => write!(
729                f,
730                "Compact(fields: {:?}, payload: {} bytes)",
731                compact_fields,
732                payload.len()
733            ),
734        }
735    }
736}
737
738// Complete Data Object structure
739pub struct DataObject<B: ByteSlice> {
740    pub header: Ref<B, DataObjectHeader>,
741    pub payload: DataPayloadType<B>,
742}
743
744impl<B: ByteSlice> std::fmt::Debug for DataObject<B> {
745    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
746        f.debug_struct("DataObject")
747            .field("header", &self.header)
748            .field("payload", &self.payload)
749            .finish()
750    }
751}
752
753impl<B: SplitByteSlice> JournalObject<B> for DataObject<B> {
754    fn from_data(data: B, is_compact: bool) -> Option<Self> {
755        let (header_data, remaining_data) = data
756            .split_at(std::mem::size_of::<DataObjectHeader>())
757            .ok()?;
758
759        let header = zerocopy::Ref::from_bytes(header_data).ok()?;
760
761        let payload = if is_compact {
762            let (fields_data, payload_data) = remaining_data
763                .split_at(std::mem::size_of::<CompactDataFields>())
764                .ok()?;
765
766            let compact_fields = zerocopy::Ref::from_bytes(fields_data).ok()?;
767
768            DataPayloadType::Compact {
769                compact_fields,
770                payload: payload_data,
771            }
772        } else {
773            DataPayloadType::Regular(remaining_data)
774        };
775
776        Some(DataObject { header, payload })
777    }
778}
779
780impl<B: SplitByteSliceMut> JournalObjectMut<B> for DataObject<B> {
781    fn from_data_mut(data: B, is_compact: bool) -> Option<Self> {
782        let (header_data, remaining_data) = data
783            .split_at(std::mem::size_of::<DataObjectHeader>())
784            .ok()?;
785
786        let header = zerocopy::Ref::from_bytes(header_data).ok()?;
787
788        let payload = if is_compact {
789            let (fields_data, payload_data) = remaining_data
790                .split_at(std::mem::size_of::<CompactDataFields>())
791                .ok()?;
792
793            let compact_fields = zerocopy::Ref::from_bytes(fields_data).ok()?;
794
795            DataPayloadType::Compact {
796                compact_fields,
797                payload: payload_data,
798            }
799        } else {
800            DataPayloadType::Regular(remaining_data)
801        };
802
803        Some(DataObject { header, payload })
804    }
805}
806
807impl<B: ByteSlice> DataObject<B> {
808    #[doc(hidden)]
809    pub fn tail_entry_array_hint(&self) -> Option<(u32, u32)> {
810        match &self.payload {
811            DataPayloadType::Compact { compact_fields, .. } => Some((
812                compact_fields.tail_entry_array_offset,
813                compact_fields.tail_entry_array_n_entries,
814            )),
815            DataPayloadType::Regular(_) => None,
816        }
817    }
818
819    pub fn raw_payload(&self) -> &[u8] {
820        match &self.payload {
821            DataPayloadType::Regular(payload) => payload,
822            DataPayloadType::Compact { payload, .. } => payload,
823        }
824    }
825
826    pub fn inlined_cursor(&self) -> Option<InlinedCursor> {
827        self.header.inlined_cursor()
828    }
829
830    pub fn is_compressed(&self) -> bool {
831        self.header.is_compressed()
832    }
833
834    pub fn xz_compressed(&self) -> bool {
835        self.header.xz_compressed()
836    }
837
838    pub fn lz4_compressed(&self) -> bool {
839        self.header.lz4_compressed()
840    }
841
842    pub fn zstd_compressed(&self) -> bool {
843        self.header.zstd_compressed()
844    }
845
846    pub fn decompress(&self, buf: &mut Vec<u8>) -> Result<usize> {
847        debug_assert!(self.is_compressed());
848
849        if self.zstd_compressed() {
850            decompress_zstd_payload(self.raw_payload(), buf)
851        } else if self.lz4_compressed() {
852            decompress_lz4_payload(self.raw_payload(), buf)
853        } else if self.xz_compressed() {
854            decompress_xz_payload(self.raw_payload(), buf)
855        } else {
856            clear_compression_error(buf, JournalError::UnknownCompressionMethod)
857        }
858    }
859}
860
861#[cfg(test)]
862mod tests {
863    use super::*;
864    use crate::file::object_compression::{
865        MAX_UNCOMPRESSED_DATA_OBJECT_SIZE, read_limited_to_end_with_cap,
866    };
867    use std::io::Read;
868
869    fn data_object_bytes(payload: &[u8], flags: u8) -> Vec<u8> {
870        let header = DataObjectHeader {
871            object_header: ObjectHeader {
872                type_: ObjectType::Data as u8,
873                flags,
874                reserved: [0; 6],
875                size: (std::mem::size_of::<DataObjectHeader>() + payload.len()) as u64,
876            },
877            hash: 0,
878            next_hash_offset: None,
879            next_field_offset: None,
880            entry_offset: None,
881            entry_array_offset: None,
882            n_entries: None,
883        };
884
885        let mut bytes = Vec::with_capacity(header.object_header.size as usize);
886        bytes.extend_from_slice(header.as_bytes());
887        bytes.extend_from_slice(payload);
888        bytes
889    }
890
891    #[test]
892    fn lz4_decompress_clears_buffer_on_short_prefix() {
893        let bytes = data_object_bytes(b"short", ObjectFlags::CompressedLz4 as u8);
894        let object = DataObject::from_data(bytes.as_slice(), false).unwrap();
895        let mut buf = b"stale".to_vec();
896
897        assert!(matches!(
898            object.decompress(&mut buf),
899            Err(JournalError::DecompressorError)
900        ));
901        assert!(buf.is_empty());
902        assert_eq!(buf.capacity(), 0);
903    }
904
905    #[test]
906    fn lz4_decompress_rejects_oversized_payload_prefix() {
907        let mut stored_payload = Vec::new();
908        stored_payload
909            .extend_from_slice(&((MAX_UNCOMPRESSED_DATA_OBJECT_SIZE as u64) + 1).to_le_bytes());
910        stored_payload.extend_from_slice(b"invalid");
911
912        let bytes = data_object_bytes(&stored_payload, ObjectFlags::CompressedLz4 as u8);
913        let object = DataObject::from_data(bytes.as_slice(), false).unwrap();
914        let mut buf = b"stale".to_vec();
915
916        assert!(matches!(
917            object.decompress(&mut buf),
918            Err(JournalError::DecompressorError)
919        ));
920        assert!(buf.is_empty());
921        assert_eq!(buf.capacity(), 0);
922    }
923
924    #[test]
925    fn lz4_decompress_clears_buffer_on_decode_error() {
926        let uncompressed_size = 4usize;
927        let mut stored_payload = Vec::new();
928        stored_payload.extend_from_slice(&(uncompressed_size as u64).to_le_bytes());
929        stored_payload.extend_from_slice(&[0x10, b'a', 1, 0]);
930
931        let bytes = data_object_bytes(&stored_payload, ObjectFlags::CompressedLz4 as u8);
932        let object = DataObject::from_data(bytes.as_slice(), false).unwrap();
933        let mut buf = b"stale".to_vec();
934
935        assert!(matches!(
936            object.decompress(&mut buf),
937            Err(JournalError::DecompressorError)
938        ));
939        assert!(buf.is_empty());
940        assert_eq!(buf.capacity(), 0);
941    }
942
943    #[test]
944    fn lz4_decompress_rejects_size_mismatch() {
945        let uncompressed_size = 4usize;
946        let mut stored_payload = Vec::new();
947        stored_payload.extend_from_slice(&(uncompressed_size as u64).to_le_bytes());
948        stored_payload.extend_from_slice(&[0x30, b'a', b'b', b'c']);
949
950        let bytes = data_object_bytes(&stored_payload, ObjectFlags::CompressedLz4 as u8);
951        let object = DataObject::from_data(bytes.as_slice(), false).unwrap();
952        let mut buf = b"stale".to_vec();
953
954        assert!(matches!(
955            object.decompress(&mut buf),
956            Err(JournalError::DecompressorError)
957        ));
958        assert!(buf.is_empty());
959        assert_eq!(buf.capacity(), 0);
960    }
961
962    #[test]
963    fn read_limited_to_end_errors_and_clears_when_limit_is_exceeded() {
964        let mut buf = b"stale".to_vec();
965
966        assert!(matches!(
967            read_limited_to_end_with_cap(std::io::repeat(b'x').take(5), &mut buf, 4),
968            Err(JournalError::DecompressorError)
969        ));
970        assert!(buf.is_empty());
971        assert_eq!(buf.capacity(), 0);
972    }
973}
974
975// SHA-256 HMAC is 32 bytes (256 bits)
976pub const TAG_LENGTH: usize = 256 / 8;
977
978#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
979#[repr(C)]
980pub struct TagObjectHeader {
981    pub object_header: ObjectHeader,
982    pub seqnum: u64,
983    pub epoch: u64,
984    pub tag: [u8; TAG_LENGTH], // SHA-256 HMAC
985}
986
987pub struct TagObject<B: ByteSlice> {
988    pub header: Ref<B, TagObjectHeader>,
989}
990
991impl<B: ByteSlice> std::fmt::Debug for TagObject<B> {
992    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
993        f.debug_struct("TagObject")
994            .field("header", &self.header)
995            .finish()
996    }
997}
998
999impl<B: SplitByteSlice> JournalObject<B> for TagObject<B> {
1000    fn from_data(data: B, _is_compact: bool) -> Option<Self> {
1001        let header = zerocopy::Ref::from_bytes(data).ok()?;
1002        Some(TagObject { header })
1003    }
1004}
1005
1006impl<B: SplitByteSliceMut> JournalObjectMut<B> for TagObject<B> {
1007    fn from_data_mut(data: B, _is_compact: bool) -> Option<Self> {
1008        let header = zerocopy::Ref::from_bytes(data).ok()?;
1009        Some(TagObject { header })
1010    }
1011}
1012
1013impl<B: ByteSlice> TagObject<B> {
1014    // Helper function to format tag as hex string
1015    pub fn tag_as_hex(&self) -> String {
1016        self.header
1017            .tag
1018            .iter()
1019            .map(|b| format!("{:02x}", b))
1020            .collect()
1021    }
1022}