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 fn from_data(data: B, is_compact: bool) -> Option<Self>;
19}
20
21pub trait JournalObjectMut<B: SplitByteSliceMut>: JournalObject<B> {
22 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], pub compatible_flags: u32, pub incompatible_flags: u32, pub state: u8, pub reserved: [u8; 7], pub file_id: [u8; 16], pub machine_id: [u8; 16], pub tail_entry_boot_id: [u8; 16], pub seqnum_id: [u8; 16], pub header_size: u64, pub arena_size: u64, pub data_hash_table_offset: Option<NonZeroU64>, pub data_hash_table_size: Option<NonZeroU64>, pub field_hash_table_offset: Option<NonZeroU64>, pub field_hash_table_size: Option<NonZeroU64>, pub tail_object_offset: Option<NonZeroU64>, pub n_objects: u64, pub n_entries: u64, pub tail_entry_seqnum: u64, pub head_entry_seqnum: u64, pub entry_array_offset: Option<NonZeroU64>, pub head_entry_realtime: u64, pub tail_entry_realtime: u64, pub tail_entry_monotonic: u64, pub n_data: u64, pub n_fields: u64, pub n_tags: u64, pub n_entry_arrays: u64, pub data_hash_chain_depth: u64, pub field_hash_chain_depth: u64, pub tail_entry_array_offset: u32, pub tail_entry_array_n_entries: u32, pub tail_entry_offset: u64, }
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 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 #[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 #[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 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], pub xor_hash: u64,
503}
504
505#[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#[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
738pub 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
975pub 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], }
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 pub fn tag_as_hex(&self) -> String {
1016 self.header
1017 .tag
1018 .iter()
1019 .map(|b| format!("{:02x}", b))
1020 .collect()
1021 }
1022}