use crate::error::{CompressionAlgo, LimitKind, Result, SdJournalError};
use crate::util::{read_id128, read_u8, read_u32_le, read_u64_le};
use std::path::Path;
pub(crate) type FileOffset = u64;
pub(crate) const HEADER_SIGNATURE: &[u8; 8] = b"LPKSHHRH";
pub(crate) const HEADER_INCOMPATIBLE_COMPRESSED_XZ: u32 = 1 << 0;
pub(crate) const HEADER_INCOMPATIBLE_COMPRESSED_LZ4: u32 = 1 << 1;
pub(crate) const HEADER_INCOMPATIBLE_KEYED_HASH: u32 = 1 << 2;
pub(crate) const HEADER_INCOMPATIBLE_COMPRESSED_ZSTD: u32 = 1 << 3;
pub(crate) const HEADER_INCOMPATIBLE_COMPACT: u32 = 1 << 4;
#[cfg(any(feature = "tracing", feature = "verify-seal"))]
pub(crate) const HEADER_COMPATIBLE_SEALED: u32 = 1 << 0;
#[cfg(any(feature = "tracing", feature = "verify-seal"))]
pub(crate) const HEADER_COMPATIBLE_SEALED_CONTINUOUS: u32 = 1 << 2;
const KNOWN_INCOMPATIBLE_FLAGS: u32 = HEADER_INCOMPATIBLE_COMPRESSED_XZ
| HEADER_INCOMPATIBLE_COMPRESSED_LZ4
| HEADER_INCOMPATIBLE_KEYED_HASH
| HEADER_INCOMPATIBLE_COMPRESSED_ZSTD
| HEADER_INCOMPATIBLE_COMPACT;
#[allow(dead_code)]
pub(crate) const STATE_OFFLINE: u8 = 0;
#[allow(dead_code)]
pub(crate) const STATE_ONLINE: u8 = 1;
pub(crate) const STATE_ARCHIVED: u8 = 2;
pub(crate) const OBJECT_DATA: u8 = 1;
#[cfg(feature = "verify-seal")]
pub(crate) const OBJECT_FIELD: u8 = 2;
pub(crate) const OBJECT_ENTRY: u8 = 3;
#[cfg(feature = "verify-seal")]
pub(crate) const OBJECT_DATA_HASH_TABLE: u8 = 4;
#[cfg(feature = "verify-seal")]
pub(crate) const OBJECT_FIELD_HASH_TABLE: u8 = 5;
pub(crate) const OBJECT_ENTRY_ARRAY: u8 = 6;
#[cfg(feature = "verify-seal")]
pub(crate) const OBJECT_TAG: u8 = 7;
pub(crate) const OBJECT_COMPRESSED_XZ: u8 = 1 << 0;
pub(crate) const OBJECT_COMPRESSED_LZ4: u8 = 1 << 1;
pub(crate) const OBJECT_COMPRESSED_ZSTD: u8 = 1 << 2;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) struct Header {
pub(crate) compatible_flags: u32,
pub(crate) incompatible_flags: u32,
pub(crate) state: u8,
pub(crate) file_id: [u8; 16],
pub(crate) machine_id: [u8; 16],
pub(crate) boot_id: [u8; 16],
pub(crate) seqnum_id: [u8; 16],
pub(crate) header_size: u64,
pub(crate) arena_size: u64,
pub(crate) data_hash_table_offset: u64,
pub(crate) data_hash_table_size: u64,
pub(crate) field_hash_table_offset: u64,
pub(crate) field_hash_table_size: u64,
pub(crate) tail_object_offset: u64,
pub(crate) n_entries: u64,
pub(crate) entry_array_offset: u64,
}
impl Header {
pub(crate) fn parse(buf: &[u8], path: &Path) -> Result<Self> {
if buf.len() < 184 {
return Err(SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(0),
reason: "file too small for journal header".to_string(),
});
}
let sig = buf.get(0..8).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(0),
reason: "missing signature".to_string(),
})?;
if sig != HEADER_SIGNATURE {
return Err(SdJournalError::Unsupported {
reason: "not a systemd journal file (bad signature)".to_string(),
});
}
let compatible_flags = read_u32_le(buf, 8).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(8),
reason: "missing compatible_flags".to_string(),
})?;
let incompatible_flags = read_u32_le(buf, 12).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(12),
reason: "missing incompatible_flags".to_string(),
})?;
let state = read_u8(buf, 16).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(16),
reason: "missing state".to_string(),
})?;
if incompatible_flags & !KNOWN_INCOMPATIBLE_FLAGS != 0 {
return Err(SdJournalError::Unsupported {
reason: format!("unknown incompatible flags: 0x{incompatible_flags:08x}"),
});
}
let file_id = read_id128(buf, 24).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(24),
reason: "missing file_id".to_string(),
})?;
let machine_id = read_id128(buf, 40).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(40),
reason: "missing machine_id".to_string(),
})?;
let boot_id = read_id128(buf, 56).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(56),
reason: "missing boot_id".to_string(),
})?;
let seqnum_id = read_id128(buf, 72).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(72),
reason: "missing seqnum_id".to_string(),
})?;
let header_size = read_u64_le(buf, 88).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(88),
reason: "missing header_size".to_string(),
})?;
let arena_size = read_u64_le(buf, 96).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(96),
reason: "missing arena_size".to_string(),
})?;
let data_hash_table_offset =
read_u64_le(buf, 104).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(104),
reason: "missing data_hash_table_offset".to_string(),
})?;
let data_hash_table_size =
read_u64_le(buf, 112).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(112),
reason: "missing data_hash_table_size".to_string(),
})?;
let field_hash_table_offset =
read_u64_le(buf, 120).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(120),
reason: "missing field_hash_table_offset".to_string(),
})?;
let field_hash_table_size =
read_u64_le(buf, 128).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(128),
reason: "missing field_hash_table_size".to_string(),
})?;
let tail_object_offset = read_u64_le(buf, 136).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(136),
reason: "missing tail_object_offset".to_string(),
})?;
let n_entries = read_u64_le(buf, 152).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(152),
reason: "missing n_entries".to_string(),
})?;
let entry_array_offset = read_u64_le(buf, 176).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(176),
reason: "missing entry_array_offset".to_string(),
})?;
Ok(Self {
compatible_flags,
incompatible_flags,
state,
file_id,
machine_id,
boot_id,
seqnum_id,
header_size,
arena_size,
data_hash_table_offset,
data_hash_table_size,
field_hash_table_offset,
field_hash_table_size,
tail_object_offset,
n_entries,
entry_array_offset,
})
}
pub(crate) fn is_compact(&self) -> bool {
self.incompatible_flags & HEADER_INCOMPATIBLE_COMPACT != 0
}
#[allow(dead_code)]
pub(crate) fn is_keyed_hash(&self) -> bool {
self.incompatible_flags & HEADER_INCOMPATIBLE_KEYED_HASH != 0
}
#[cfg(any(feature = "tracing", feature = "verify-seal"))]
pub(crate) fn is_sealed(&self) -> bool {
self.compatible_flags & HEADER_COMPATIBLE_SEALED != 0
}
#[cfg(any(feature = "tracing", feature = "verify-seal"))]
pub(crate) fn is_sealed_continuous(&self) -> bool {
self.compatible_flags & HEADER_COMPATIBLE_SEALED_CONTINUOUS != 0
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ObjectHeader {
pub(crate) object_type: u8,
pub(crate) flags: u8,
pub(crate) size: u64,
}
impl ObjectHeader {
pub(crate) fn parse(buf: &[u8], path: &Path, offset: FileOffset) -> Result<Self> {
if buf.len() < 16 {
return Err(SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(offset),
reason: "object header truncated".to_string(),
});
}
let object_type = buf[0];
let flags = buf[1];
let size = read_u64_le(buf, 8).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(offset),
reason: "object size missing".to_string(),
})?;
Ok(Self {
object_type,
flags,
size,
})
}
}
pub(crate) fn parse_entry_data_offsets_bytes(
items_bytes: &[u8],
compact: bool,
max_fields_per_entry: usize,
path: &Path,
entry_offset: FileOffset,
) -> Result<Vec<FileOffset>> {
let mut data_offsets = Vec::new();
if compact {
if !items_bytes.len().is_multiple_of(4) {
return Err(SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(entry_offset),
reason: "ENTRY compact items not aligned".to_string(),
});
}
crate::util::ensure_limit_usize(
LimitKind::FieldsPerEntry,
max_fields_per_entry,
items_bytes.len() / 4,
)?;
let mut i = 0;
while i < items_bytes.len() {
let off = read_u32_le(items_bytes, i).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(entry_offset),
reason: "ENTRY compact item truncated".to_string(),
})?;
if off == 0 {
break;
}
data_offsets.push(u64::from(off));
i += 4;
}
} else {
if !items_bytes.len().is_multiple_of(16) {
return Err(SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(entry_offset),
reason: "ENTRY regular items not aligned".to_string(),
});
}
crate::util::ensure_limit_usize(
LimitKind::FieldsPerEntry,
max_fields_per_entry,
items_bytes.len() / 16,
)?;
let mut i = 0;
while i < items_bytes.len() {
let object_offset =
read_u64_le(items_bytes, i).ok_or_else(|| SdJournalError::Corrupt {
path: Some(path.to_path_buf()),
offset: Some(entry_offset),
reason: "ENTRY item truncated".to_string(),
})?;
if object_offset == 0 {
break;
}
data_offsets.push(object_offset);
i += 16;
}
}
Ok(data_offsets)
}
pub(crate) fn compression_from_object_flags(flags: u8) -> Result<Option<CompressionAlgo>> {
let algo = if flags & OBJECT_COMPRESSED_XZ != 0 {
Some(CompressionAlgo::Xz)
} else if flags & OBJECT_COMPRESSED_LZ4 != 0 {
Some(CompressionAlgo::Lz4)
} else if flags & OBJECT_COMPRESSED_ZSTD != 0 {
Some(CompressionAlgo::Zstd)
} else {
None
};
let mut count = 0;
for bit in [
OBJECT_COMPRESSED_XZ,
OBJECT_COMPRESSED_LZ4,
OBJECT_COMPRESSED_ZSTD,
] {
if flags & bit != 0 {
count += 1;
}
}
if count > 1 {
return Err(SdJournalError::Corrupt {
path: None,
offset: None,
reason: "multiple compression flags set".to_string(),
});
}
Ok(algo)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::LimitKind;
use std::path::Path;
fn sample_header_bytes(compatible_flags: u32, incompatible_flags: u32) -> Vec<u8> {
let mut buf = vec![0u8; 184];
buf[0..8].copy_from_slice(HEADER_SIGNATURE);
buf[8..12].copy_from_slice(&compatible_flags.to_le_bytes());
buf[12..16].copy_from_slice(&incompatible_flags.to_le_bytes());
buf[16] = STATE_ARCHIVED;
buf[24..40].copy_from_slice(&[0x11; 16]);
buf[40..56].copy_from_slice(&[0x22; 16]);
buf[56..72].copy_from_slice(&[0x33; 16]);
buf[72..88].copy_from_slice(&[0x44; 16]);
buf[88..96].copy_from_slice(&184u64.to_le_bytes());
buf[96..104].copy_from_slice(&4096u64.to_le_bytes());
buf[104..112].copy_from_slice(&256u64.to_le_bytes());
buf[112..120].copy_from_slice(&64u64.to_le_bytes());
buf[120..128].copy_from_slice(&320u64.to_le_bytes());
buf[128..136].copy_from_slice(&64u64.to_le_bytes());
buf[136..144].copy_from_slice(&512u64.to_le_bytes());
buf[152..160].copy_from_slice(&7u64.to_le_bytes());
buf[176..184].copy_from_slice(&768u64.to_le_bytes());
buf
}
#[test]
fn header_parse_accepts_minimal_header_and_reports_flags() {
#[cfg(any(feature = "tracing", feature = "verify-seal"))]
let compatible_flags = HEADER_COMPATIBLE_SEALED | HEADER_COMPATIBLE_SEALED_CONTINUOUS;
#[cfg(not(any(feature = "tracing", feature = "verify-seal")))]
let compatible_flags = 0u32;
let header = Header::parse(
&sample_header_bytes(
compatible_flags,
HEADER_INCOMPATIBLE_COMPACT | HEADER_INCOMPATIBLE_KEYED_HASH,
),
Path::new("dummy"),
)
.unwrap();
assert_eq!(header.state, STATE_ARCHIVED);
assert_eq!(header.file_id, [0x11; 16]);
assert_eq!(header.machine_id, [0x22; 16]);
assert_eq!(header.boot_id, [0x33; 16]);
assert_eq!(header.seqnum_id, [0x44; 16]);
assert_eq!(header.header_size, 184);
assert_eq!(header.n_entries, 7);
assert_eq!(header.entry_array_offset, 768);
assert!(header.is_compact());
assert!(header.is_keyed_hash());
#[cfg(any(feature = "tracing", feature = "verify-seal"))]
{
assert!(header.is_sealed());
assert!(header.is_sealed_continuous());
}
}
#[test]
fn header_parse_rejects_unknown_incompatible_flags() {
match Header::parse(
&sample_header_bytes(0, HEADER_INCOMPATIBLE_COMPACT | (1 << 31)),
Path::new("dummy"),
) {
Err(SdJournalError::Unsupported { reason }) => {
assert_eq!(reason, "unknown incompatible flags: 0x80000010");
}
other => panic!("unexpected result: {other:?}"),
}
}
#[test]
fn object_header_parse_rejects_truncated_input() {
match ObjectHeader::parse(&[OBJECT_ENTRY, 0], Path::new("dummy"), 64) {
Err(SdJournalError::Corrupt {
path,
offset,
reason,
}) => {
assert_eq!(path, Some(Path::new("dummy").to_path_buf()));
assert_eq!(offset, Some(64));
assert_eq!(reason, "object header truncated");
}
other => panic!("unexpected result: {other:?}"),
}
}
#[test]
fn compression_flags_accept_single_algorithm_and_reject_conflicts() {
assert_eq!(compression_from_object_flags(0).unwrap(), None);
assert_eq!(
compression_from_object_flags(OBJECT_COMPRESSED_LZ4).unwrap(),
Some(CompressionAlgo::Lz4)
);
assert_eq!(
compression_from_object_flags(OBJECT_COMPRESSED_XZ).unwrap(),
Some(CompressionAlgo::Xz)
);
assert_eq!(
compression_from_object_flags(OBJECT_COMPRESSED_ZSTD).unwrap(),
Some(CompressionAlgo::Zstd)
);
match compression_from_object_flags(OBJECT_COMPRESSED_LZ4 | OBJECT_COMPRESSED_ZSTD) {
Err(SdJournalError::Corrupt { reason, .. }) => {
assert_eq!(reason, "multiple compression flags set");
}
other => panic!("unexpected result: {other:?}"),
}
}
#[test]
fn compact_entry_items_offsets_only() {
let mut items = Vec::new();
items.extend_from_slice(&100u32.to_le_bytes());
items.extend_from_slice(&200u32.to_le_bytes());
items.extend_from_slice(&0u32.to_le_bytes());
let got = parse_entry_data_offsets_bytes(&items, true, 1024, Path::new("dummy"), 0)
.expect("parse compact entry items");
assert_eq!(got, vec![100, 200]);
}
#[test]
fn regular_entry_items_skip_hashes() {
let mut items = Vec::new();
items.extend_from_slice(&0x1111_2222_3333_4444u64.to_le_bytes());
items.extend_from_slice(&0u64.to_le_bytes());
items.extend_from_slice(&0u64.to_le_bytes());
items.extend_from_slice(&0xffff_eeee_dddd_ccccu64.to_le_bytes());
let got = parse_entry_data_offsets_bytes(&items, false, 1024, Path::new("dummy"), 0)
.expect("parse regular entry items");
assert_eq!(got, vec![0x1111_2222_3333_4444]);
}
#[test]
fn entry_items_reject_misalignment_and_field_limit() {
match parse_entry_data_offsets_bytes(&[1, 2, 3], true, 1024, Path::new("dummy"), 9) {
Err(SdJournalError::Corrupt { reason, offset, .. }) => {
assert_eq!(offset, Some(9));
assert_eq!(reason, "ENTRY compact items not aligned");
}
other => panic!("unexpected result: {other:?}"),
}
let mut regular = Vec::new();
regular.extend_from_slice(&100u64.to_le_bytes());
regular.extend_from_slice(&0u64.to_le_bytes());
regular.extend_from_slice(&200u64.to_le_bytes());
regular.extend_from_slice(&0u64.to_le_bytes());
match parse_entry_data_offsets_bytes(®ular, false, 1, Path::new("dummy"), 0) {
Err(SdJournalError::LimitExceeded { kind, limit }) => {
assert_eq!(kind, LimitKind::FieldsPerEntry);
assert_eq!(limit, 1);
}
other => panic!("unexpected result: {other:?}"),
}
}
}