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 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 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], pub xor_hash: u64,
416}
417
418#[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#[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
651pub 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
877pub 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], }
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 pub fn tag_as_hex(&self) -> String {
918 self.header
919 .tag
920 .iter()
921 .map(|b| format!("{:02x}", b))
922 .collect()
923 }
924}