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    pub fn has_incompatible_flag(&self, flag: HeaderIncompatibleFlags) -> bool {
115        (self.incompatible_flags & flag as u32) != 0
116    }
117
118    pub fn has_compatible_flag(&self, flag: HeaderCompatibleFlags) -> bool {
119        (self.compatible_flags & flag as u32) != 0
120    }
121}
122
123pub enum ObjectFlags {
124    CompressedXz = 1 << 0,
125    CompressedLz4 = 1 << 1,
126    CompressedZstd = 1 << 2,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130#[repr(u8)]
131pub enum ObjectType {
132    Unused = 0,
133    Data = 1,
134    Field = 2,
135    Entry = 3,
136    DataHashTable = 4,
137    FieldHashTable = 5,
138    EntryArray = 6,
139    Tag = 7,
140}
141
142impl TryFrom<u8> for ObjectType {
143    type Error = JournalError;
144
145    fn try_from(value: u8) -> Result<Self> {
146        match value {
147            0 => Ok(ObjectType::Unused),
148            1 => Ok(ObjectType::Data),
149            2 => Ok(ObjectType::Field),
150            3 => Ok(ObjectType::Entry),
151            4 => Ok(ObjectType::DataHashTable),
152            5 => Ok(ObjectType::FieldHashTable),
153            6 => Ok(ObjectType::EntryArray),
154            7 => Ok(ObjectType::Tag),
155            _ => Err(JournalError::InvalidObjectType),
156        }
157    }
158}
159
160#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
161#[repr(C)]
162pub struct ObjectHeader {
163    pub type_: u8,
164    pub flags: u8,
165    pub reserved: [u8; 6],
166    pub size: u64,
167}
168
169impl ObjectHeader {
170    pub fn xz_compressed(&self) -> bool {
171        (self.flags & ObjectFlags::CompressedXz as u8) != 0
172    }
173
174    pub fn lz4_compressed(&self) -> bool {
175        (self.flags & ObjectFlags::CompressedLz4 as u8) != 0
176    }
177
178    pub fn zstd_compressed(&self) -> bool {
179        (self.flags & ObjectFlags::CompressedZstd as u8) != 0
180    }
181
182    pub fn is_compressed(&self) -> bool {
183        self.zstd_compressed() | self.lz4_compressed() | self.xz_compressed()
184    }
185
186    pub fn aligned_size(&self) -> u64 {
187        (self.size + 7) & !7
188    }
189
190    /// Validates that the object size is sane.
191    ///
192    /// Returns the size if valid, or an error if the size is invalid.
193    /// This should be called when reading an ObjectHeader from a journal file
194    /// to protect against corrupted data.
195    pub fn validated_size(&self) -> crate::error::Result<u64> {
196        let min_size = std::mem::size_of::<ObjectHeader>() as u64;
197
198        if self.size < min_size {
199            return Err(crate::error::JournalError::InvalidObjectSize(self.size));
200        }
201
202        Ok(self.size)
203    }
204}
205
206#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
207#[repr(C)]
208pub struct FieldObjectHeader {
209    pub object_header: ObjectHeader,
210    pub hash: u64,
211    pub next_hash_offset: Option<NonZeroU64>,
212    pub head_data_offset: Option<NonZeroU64>,
213}
214
215#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
216#[repr(C)]
217pub struct OffsetArrayObjectHeader {
218    pub object_header: ObjectHeader,
219    pub next_offset_array: Option<NonZeroU64>,
220}
221
222#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
223#[repr(C)]
224pub struct HashItem {
225    pub head_hash_offset: Option<NonZeroU64>,
226    pub tail_hash_offset: Option<NonZeroU64>,
227}
228
229#[derive(Debug)]
230pub struct FieldObject<B: ByteSlice> {
231    pub header: Ref<B, FieldObjectHeader>,
232    pub payload: B,
233}
234
235impl<B: SplitByteSlice> JournalObject<B> for FieldObject<B> {
236    fn from_data(data: B, _is_compact: bool) -> Option<Self> {
237        let (header, payload) = zerocopy::Ref::from_prefix(data).ok()?;
238        Some(FieldObject { header, payload })
239    }
240}
241
242impl<B: SplitByteSliceMut> JournalObjectMut<B> for FieldObject<B> {
243    fn from_data_mut(data: B, _is_compact: bool) -> Option<Self> {
244        let (header, payload) = zerocopy::Ref::from_prefix(data).ok()?;
245        Some(FieldObject { header, payload })
246    }
247}
248
249pub enum OffsetsType<B: ByteSlice> {
250    Regular(Ref<B, [Option<NonZeroU64>]>),
251    Compact(Ref<B, [Option<NonZeroU32>]>),
252}
253
254impl<B: ByteSlice> OffsetsType<B> {
255    pub fn get(&self, index: usize) -> Option<NonZeroU64> {
256        match self {
257            OffsetsType::Regular(offsets) => offsets[index],
258            OffsetsType::Compact(offsets) => offsets[index].map(NonZeroU64::from),
259        }
260    }
261}
262
263impl<B: ByteSliceMut> OffsetsType<B> {
264    pub fn set(&mut self, index: usize, value: NonZeroU64) {
265        match self {
266            OffsetsType::Regular(offsets) => offsets[index] = Some(value),
267            OffsetsType::Compact(offsets) => {
268                assert!(value.get() <= u32::MAX as u64);
269                offsets[index] = NonZeroU32::new(value.get() as u32);
270            }
271        }
272    }
273}
274
275impl<B: ByteSlice> std::fmt::Debug for OffsetsType<B> {
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        match self {
278            OffsetsType::Regular(items) => write!(f, "Regular({} items)", items.len()),
279            OffsetsType::Compact(items) => write!(f, "Compact({} items)", items.len()),
280        }
281    }
282}
283
284pub struct OffsetArrayObject<B: ByteSlice> {
285    pub header: Ref<B, OffsetArrayObjectHeader>,
286    pub items: OffsetsType<B>,
287}
288
289impl<B: ByteSlice> OffsetArrayObject<B> {
290    pub fn capacity(&self) -> usize {
291        match &self.items {
292            OffsetsType::Regular(offsets) => offsets.len(),
293            OffsetsType::Compact(offsets) => offsets.len(),
294        }
295    }
296
297    pub fn len(&self, remaining_items: usize) -> usize {
298        self.capacity().min(remaining_items)
299    }
300
301    pub fn is_empty(&self, remaining_items: usize) -> bool {
302        self.len(remaining_items) == 0
303    }
304
305    pub fn get(&self, index: usize, remaining_items: usize) -> Result<Option<NonZeroU64>> {
306        if self.is_empty(remaining_items) {
307            return Err(JournalError::EmptyOffsetArrayNode);
308        }
309
310        Ok(self.items.get(index))
311    }
312
313    pub fn collect_offsets(
314        &self,
315        start_index: usize,
316        remaining_items: usize,
317        offsets: &mut Vec<NonZeroU64>,
318    ) -> Result<()> {
319        let len = self.len(remaining_items);
320
321        if start_index >= len {
322            return Err(JournalError::InvalidOffsetArrayIndex);
323        }
324
325        match &self.items {
326            OffsetsType::Regular(s) => {
327                offsets.extend(s[start_index..len].iter().filter_map(|&opt| opt));
328            }
329            OffsetsType::Compact(s) => {
330                offsets.extend(
331                    s[start_index..len]
332                        .iter()
333                        .filter_map(|&opt| opt.map(NonZeroU64::from)),
334                );
335            }
336        }
337
338        Ok(())
339    }
340}
341
342impl<B: ByteSliceMut> OffsetArrayObject<B> {
343    pub fn set(&mut self, index: usize, offset: NonZeroU64) -> Result<()> {
344        if index >= self.capacity() {
345            return Err(JournalError::OutOfBoundsIndex);
346        }
347
348        self.items.set(index, offset);
349        Ok(())
350    }
351}
352
353impl<B: ByteSlice> std::fmt::Debug for OffsetArrayObject<B> {
354    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355        f.debug_struct("JournalHeader")
356            .field("header", &self.header)
357            .finish()
358    }
359}
360
361impl<B: SplitByteSlice> JournalObject<B> for OffsetArrayObject<B> {
362    fn from_data(data: B, is_compact: bool) -> Option<Self> {
363        let (header_data, items_data) = data
364            .split_at(std::mem::size_of::<OffsetArrayObjectHeader>())
365            .ok()?;
366
367        let header = zerocopy::Ref::from_bytes(header_data).ok()?;
368
369        let items_type = if is_compact {
370            let compact_items = zerocopy::Ref::from_bytes(items_data).ok()?;
371            OffsetsType::Compact(compact_items)
372        } else {
373            let regular_items = zerocopy::Ref::from_bytes(items_data).ok()?;
374            OffsetsType::Regular(regular_items)
375        };
376
377        Some(OffsetArrayObject {
378            header,
379            items: items_type,
380        })
381    }
382}
383
384impl<B: SplitByteSliceMut> JournalObjectMut<B> for OffsetArrayObject<B> {
385    fn from_data_mut(data: B, is_compact: bool) -> Option<Self> {
386        let (header_data, items_data) = data
387            .split_at(std::mem::size_of::<OffsetArrayObjectHeader>())
388            .ok()?;
389
390        let header = zerocopy::Ref::from_bytes(header_data).ok()?;
391
392        let items_type = if is_compact {
393            let compact_items = zerocopy::Ref::from_bytes(items_data).ok()?;
394            OffsetsType::Compact(compact_items)
395        } else {
396            let regular_items = zerocopy::Ref::from_bytes(items_data).ok()?;
397            OffsetsType::Regular(regular_items)
398        };
399
400        Some(OffsetArrayObject {
401            header,
402            items: items_type,
403        })
404    }
405}
406
407#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
408#[repr(C)]
409pub struct EntryObjectHeader {
410    pub object_header: ObjectHeader,
411    pub seqnum: u64,
412    pub realtime: u64,
413    pub monotonic: u64,
414    pub boot_id: [u8; 16], // UUID/128-bit ID
415    pub xor_hash: u64,
416}
417
418// For regular (non-compact) format - an array of these follows the header
419#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
420#[repr(C)]
421pub struct RegularEntryItem {
422    pub object_offset: u64,
423    pub hash: u64,
424}
425
426// For compact format - an array of these follows the header
427#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
428#[repr(C)]
429pub struct CompactEntryItem {
430    pub object_offset: u32,
431}
432
433pub enum EntryItemsType<B: ByteSlice> {
434    Regular(Ref<B, [RegularEntryItem]>),
435    Compact(Ref<B, [CompactEntryItem]>),
436}
437
438impl<B: ByteSliceMut> EntryItemsType<B> {
439    pub fn set(&mut self, index: usize, object_offset: NonZeroU64, hash: Option<u64>) {
440        match self {
441            EntryItemsType::Regular(entry_items) => {
442                entry_items[index].object_offset = object_offset.get();
443                entry_items[index].hash = hash.unwrap();
444            }
445            EntryItemsType::Compact(entry_items) => {
446                debug_assert!(hash.is_none());
447                assert!(object_offset.get() <= u32::MAX as u64);
448                entry_items[index].object_offset = object_offset.get() as u32;
449            }
450        }
451    }
452}
453
454impl<B: ByteSlice> EntryItemsType<B> {
455    pub fn get(&self, index: usize) -> u64 {
456        match self {
457            EntryItemsType::Regular(entry_items) => entry_items[index].object_offset,
458            EntryItemsType::Compact(entry_items) => entry_items[index].object_offset as u64,
459        }
460    }
461
462    pub fn len(&self) -> usize {
463        match self {
464            EntryItemsType::Regular(entry_items) => entry_items.len(),
465            EntryItemsType::Compact(entry_items) => entry_items.len(),
466        }
467    }
468
469    pub fn is_empty(&self) -> bool {
470        match self {
471            EntryItemsType::Regular(entry_items) => entry_items.is_empty(),
472            EntryItemsType::Compact(entry_items) => entry_items.is_empty(),
473        }
474    }
475}
476
477impl<B: ByteSlice> std::fmt::Debug for EntryItemsType<B> {
478    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
479        match self {
480            EntryItemsType::Regular(items) => write!(f, "Regular({} items)", items.len()),
481            EntryItemsType::Compact(items) => write!(f, "Compact({} items)", items.len()),
482        }
483    }
484}
485
486pub struct EntryObject<B: ByteSlice> {
487    pub header: Ref<B, EntryObjectHeader>,
488    pub items: EntryItemsType<B>,
489}
490
491impl<B: ByteSlice> EntryObject<B> {
492    pub fn collect_offsets(&self, offsets: &mut Vec<NonZeroU64>) -> Result<()> {
493        match &self.items {
494            EntryItemsType::Regular(items) => {
495                offsets.reserve(items.len());
496
497                for item in items.iter() {
498                    let offset =
499                        NonZeroU64::new(item.object_offset).ok_or(JournalError::InvalidOffset)?;
500                    offsets.push(offset);
501                }
502            }
503            EntryItemsType::Compact(items) => {
504                offsets.reserve(items.len());
505
506                for item in items.iter() {
507                    let offset = NonZeroU64::new(item.object_offset as u64)
508                        .ok_or(JournalError::InvalidOffset)?;
509                    offsets.push(offset);
510                }
511            }
512        }
513
514        Ok(())
515    }
516}
517
518impl<B: ByteSlice> std::fmt::Debug for EntryObject<B> {
519    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520        f.debug_struct("EntryObject")
521            .field("header", &self.header)
522            .field("items", &self.items)
523            .finish()
524    }
525}
526
527impl<B: SplitByteSlice> JournalObject<B> for EntryObject<B> {
528    fn from_data(data: B, is_compact: bool) -> Option<Self> {
529        let (header_data, items_data) = data
530            .split_at(std::mem::size_of::<EntryObjectHeader>())
531            .ok()?;
532
533        let header = zerocopy::Ref::from_bytes(header_data).ok()?;
534
535        let items_type = if is_compact {
536            let compact_items = zerocopy::Ref::from_bytes(items_data).ok()?;
537            EntryItemsType::Compact(compact_items)
538        } else {
539            let regular_items = zerocopy::Ref::from_bytes(items_data).ok()?;
540            EntryItemsType::Regular(regular_items)
541        };
542
543        Some(EntryObject {
544            header,
545            items: items_type,
546        })
547    }
548}
549
550impl<B: SplitByteSliceMut> JournalObjectMut<B> for EntryObject<B> {
551    fn from_data_mut(data: B, is_compact: bool) -> Option<Self> {
552        let (header_data, items_data) = data
553            .split_at(std::mem::size_of::<EntryObjectHeader>())
554            .ok()?;
555
556        let header = zerocopy::Ref::from_bytes(header_data).ok()?;
557
558        let items_type = if is_compact {
559            let compact_items = zerocopy::Ref::from_bytes(items_data).ok()?;
560            EntryItemsType::Compact(compact_items)
561        } else {
562            let regular_items = zerocopy::Ref::from_bytes(items_data).ok()?;
563            EntryItemsType::Regular(regular_items)
564        };
565
566        Some(EntryObject {
567            header,
568            items: items_type,
569        })
570    }
571}
572
573#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
574#[repr(C)]
575pub struct DataObjectHeader {
576    pub object_header: ObjectHeader,
577    pub hash: u64,
578    pub next_hash_offset: Option<NonZeroU64>,
579    pub next_field_offset: Option<NonZeroU64>,
580    pub entry_offset: Option<NonZeroU64>,
581    pub entry_array_offset: Option<NonZeroU64>,
582    pub n_entries: Option<NonZeroU64>,
583}
584
585impl DataObjectHeader {
586    pub fn xz_compressed(&self) -> bool {
587        self.object_header.xz_compressed()
588    }
589
590    pub fn lz4_compressed(&self) -> bool {
591        self.object_header.lz4_compressed()
592    }
593
594    pub fn zstd_compressed(&self) -> bool {
595        self.object_header.zstd_compressed()
596    }
597
598    pub fn is_compressed(&self) -> bool {
599        self.object_header.is_compressed()
600    }
601
602    pub fn inlined_cursor(&self) -> Option<InlinedCursor> {
603        let inlined_offset = self.entry_offset?;
604        let cursor = match self.n_entries?.get() {
605            1 => None,
606            n => {
607                let total_items = NonZeroUsize::new(n as usize - 1)?;
608                Some(Cursor::at_head(List::new(
609                    self.entry_array_offset?,
610                    total_items,
611                )))
612            }
613        };
614        Some(InlinedCursor::new(inlined_offset, cursor))
615    }
616}
617
618#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable, PartialEq, Eq)]
619#[repr(C)]
620pub struct CompactDataFields {
621    pub tail_entry_array_offset: u32,
622    pub tail_entry_array_n_entries: u32,
623}
624
625#[derive(PartialEq, Eq)]
626pub enum DataPayloadType<B: ByteSlice> {
627    Regular(B),
628    Compact {
629        compact_fields: Ref<B, CompactDataFields>,
630        payload: B,
631    },
632}
633
634impl<B: ByteSlice> std::fmt::Debug for DataPayloadType<B> {
635    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636        match self {
637            DataPayloadType::Regular(payload) => write!(f, "Regular({} bytes)", payload.len()),
638            DataPayloadType::Compact {
639                compact_fields,
640                payload,
641            } => write!(
642                f,
643                "Compact(fields: {:?}, payload: {} bytes)",
644                compact_fields,
645                payload.len()
646            ),
647        }
648    }
649}
650
651// Complete Data Object structure
652pub struct DataObject<B: ByteSlice> {
653    pub header: Ref<B, DataObjectHeader>,
654    pub payload: DataPayloadType<B>,
655}
656
657impl<B: ByteSlice> std::fmt::Debug for DataObject<B> {
658    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
659        f.debug_struct("DataObject")
660            .field("header", &self.header)
661            .field("payload", &self.payload)
662            .finish()
663    }
664}
665
666impl<B: SplitByteSlice> JournalObject<B> for DataObject<B> {
667    fn from_data(data: B, is_compact: bool) -> Option<Self> {
668        let (header_data, remaining_data) = data
669            .split_at(std::mem::size_of::<DataObjectHeader>())
670            .ok()?;
671
672        let header = zerocopy::Ref::from_bytes(header_data).ok()?;
673
674        let payload = if is_compact {
675            let (fields_data, payload_data) = remaining_data
676                .split_at(std::mem::size_of::<CompactDataFields>())
677                .ok()?;
678
679            let compact_fields = zerocopy::Ref::from_bytes(fields_data).ok()?;
680
681            DataPayloadType::Compact {
682                compact_fields,
683                payload: payload_data,
684            }
685        } else {
686            DataPayloadType::Regular(remaining_data)
687        };
688
689        Some(DataObject { header, payload })
690    }
691}
692
693impl<B: SplitByteSliceMut> JournalObjectMut<B> for DataObject<B> {
694    fn from_data_mut(data: B, is_compact: bool) -> Option<Self> {
695        let (header_data, remaining_data) = data
696            .split_at(std::mem::size_of::<DataObjectHeader>())
697            .ok()?;
698
699        let header = zerocopy::Ref::from_bytes(header_data).ok()?;
700
701        let payload = if is_compact {
702            let (fields_data, payload_data) = remaining_data
703                .split_at(std::mem::size_of::<CompactDataFields>())
704                .ok()?;
705
706            let compact_fields = zerocopy::Ref::from_bytes(fields_data).ok()?;
707
708            DataPayloadType::Compact {
709                compact_fields,
710                payload: payload_data,
711            }
712        } else {
713            DataPayloadType::Regular(remaining_data)
714        };
715
716        Some(DataObject { header, payload })
717    }
718}
719
720impl<B: ByteSlice> DataObject<B> {
721    pub fn raw_payload(&self) -> &[u8] {
722        match &self.payload {
723            DataPayloadType::Regular(payload) => payload,
724            DataPayloadType::Compact { payload, .. } => payload,
725        }
726    }
727
728    pub fn inlined_cursor(&self) -> Option<InlinedCursor> {
729        self.header.inlined_cursor()
730    }
731
732    pub fn is_compressed(&self) -> bool {
733        self.header.is_compressed()
734    }
735
736    pub fn xz_compressed(&self) -> bool {
737        self.header.xz_compressed()
738    }
739
740    pub fn lz4_compressed(&self) -> bool {
741        self.header.lz4_compressed()
742    }
743
744    pub fn zstd_compressed(&self) -> bool {
745        self.header.zstd_compressed()
746    }
747
748    pub fn decompress(&self, buf: &mut Vec<u8>) -> Result<usize> {
749        debug_assert!(self.is_compressed());
750
751        if self.zstd_compressed() {
752            decompress_zstd_payload(self.raw_payload(), buf)
753        } else if self.lz4_compressed() {
754            decompress_lz4_payload(self.raw_payload(), buf)
755        } else if self.xz_compressed() {
756            decompress_xz_payload(self.raw_payload(), buf)
757        } else {
758            clear_compression_error(buf, JournalError::UnknownCompressionMethod)
759        }
760    }
761}
762
763#[cfg(test)]
764mod tests {
765    use super::*;
766    use crate::file::object_compression::{
767        MAX_UNCOMPRESSED_DATA_OBJECT_SIZE, read_limited_to_end_with_cap,
768    };
769    use std::io::Read;
770
771    fn data_object_bytes(payload: &[u8], flags: u8) -> Vec<u8> {
772        let header = DataObjectHeader {
773            object_header: ObjectHeader {
774                type_: ObjectType::Data as u8,
775                flags,
776                reserved: [0; 6],
777                size: (std::mem::size_of::<DataObjectHeader>() + payload.len()) as u64,
778            },
779            hash: 0,
780            next_hash_offset: None,
781            next_field_offset: None,
782            entry_offset: None,
783            entry_array_offset: None,
784            n_entries: None,
785        };
786
787        let mut bytes = Vec::with_capacity(header.object_header.size as usize);
788        bytes.extend_from_slice(header.as_bytes());
789        bytes.extend_from_slice(payload);
790        bytes
791    }
792
793    #[test]
794    fn lz4_decompress_clears_buffer_on_short_prefix() {
795        let bytes = data_object_bytes(b"short", ObjectFlags::CompressedLz4 as u8);
796        let object = DataObject::from_data(bytes.as_slice(), false).unwrap();
797        let mut buf = b"stale".to_vec();
798
799        assert!(matches!(
800            object.decompress(&mut buf),
801            Err(JournalError::DecompressorError)
802        ));
803        assert!(buf.is_empty());
804        assert_eq!(buf.capacity(), 0);
805    }
806
807    #[test]
808    fn lz4_decompress_rejects_oversized_payload_prefix() {
809        let mut stored_payload = Vec::new();
810        stored_payload
811            .extend_from_slice(&((MAX_UNCOMPRESSED_DATA_OBJECT_SIZE as u64) + 1).to_le_bytes());
812        stored_payload.extend_from_slice(b"invalid");
813
814        let bytes = data_object_bytes(&stored_payload, ObjectFlags::CompressedLz4 as u8);
815        let object = DataObject::from_data(bytes.as_slice(), false).unwrap();
816        let mut buf = b"stale".to_vec();
817
818        assert!(matches!(
819            object.decompress(&mut buf),
820            Err(JournalError::DecompressorError)
821        ));
822        assert!(buf.is_empty());
823        assert_eq!(buf.capacity(), 0);
824    }
825
826    #[test]
827    fn lz4_decompress_clears_buffer_on_decode_error() {
828        let uncompressed_size = 4usize;
829        let mut stored_payload = Vec::new();
830        stored_payload.extend_from_slice(&(uncompressed_size as u64).to_le_bytes());
831        stored_payload.extend_from_slice(&[0x10, b'a', 1, 0]);
832
833        let bytes = data_object_bytes(&stored_payload, ObjectFlags::CompressedLz4 as u8);
834        let object = DataObject::from_data(bytes.as_slice(), false).unwrap();
835        let mut buf = b"stale".to_vec();
836
837        assert!(matches!(
838            object.decompress(&mut buf),
839            Err(JournalError::DecompressorError)
840        ));
841        assert!(buf.is_empty());
842        assert_eq!(buf.capacity(), 0);
843    }
844
845    #[test]
846    fn lz4_decompress_rejects_size_mismatch() {
847        let uncompressed_size = 4usize;
848        let mut stored_payload = Vec::new();
849        stored_payload.extend_from_slice(&(uncompressed_size as u64).to_le_bytes());
850        stored_payload.extend_from_slice(&[0x30, b'a', b'b', b'c']);
851
852        let bytes = data_object_bytes(&stored_payload, ObjectFlags::CompressedLz4 as u8);
853        let object = DataObject::from_data(bytes.as_slice(), false).unwrap();
854        let mut buf = b"stale".to_vec();
855
856        assert!(matches!(
857            object.decompress(&mut buf),
858            Err(JournalError::DecompressorError)
859        ));
860        assert!(buf.is_empty());
861        assert_eq!(buf.capacity(), 0);
862    }
863
864    #[test]
865    fn read_limited_to_end_errors_and_clears_when_limit_is_exceeded() {
866        let mut buf = b"stale".to_vec();
867
868        assert!(matches!(
869            read_limited_to_end_with_cap(std::io::repeat(b'x').take(5), &mut buf, 4),
870            Err(JournalError::DecompressorError)
871        ));
872        assert!(buf.is_empty());
873        assert_eq!(buf.capacity(), 0);
874    }
875}
876
877// SHA-256 HMAC is 32 bytes (256 bits)
878pub const TAG_LENGTH: usize = 256 / 8;
879
880#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)]
881#[repr(C)]
882pub struct TagObjectHeader {
883    pub object_header: ObjectHeader,
884    pub seqnum: u64,
885    pub epoch: u64,
886    pub tag: [u8; TAG_LENGTH], // SHA-256 HMAC
887}
888
889pub struct TagObject<B: ByteSlice> {
890    pub header: Ref<B, TagObjectHeader>,
891}
892
893impl<B: ByteSlice> std::fmt::Debug for TagObject<B> {
894    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
895        f.debug_struct("TagObject")
896            .field("header", &self.header)
897            .finish()
898    }
899}
900
901impl<B: SplitByteSlice> JournalObject<B> for TagObject<B> {
902    fn from_data(data: B, _is_compact: bool) -> Option<Self> {
903        let header = zerocopy::Ref::from_bytes(data).ok()?;
904        Some(TagObject { header })
905    }
906}
907
908impl<B: SplitByteSliceMut> JournalObjectMut<B> for TagObject<B> {
909    fn from_data_mut(data: B, _is_compact: bool) -> Option<Self> {
910        let header = zerocopy::Ref::from_bytes(data).ok()?;
911        Some(TagObject { header })
912    }
913}
914
915impl<B: ByteSlice> TagObject<B> {
916    // Helper function to format tag as hex string
917    pub fn tag_as_hex(&self) -> String {
918        self.header
919            .tag
920            .iter()
921            .map(|b| format!("{:02x}", b))
922            .collect()
923    }
924}