sdjournal 0.1.15

Pure Rust systemd journal reader and query engine
Documentation
use crate::error::{LimitKind, Result, SdJournalError};

pub(crate) fn checked_add_u64(a: u64, b: u64, context: &'static str) -> Result<u64> {
    a.checked_add(b).ok_or_else(|| SdJournalError::Corrupt {
        path: None,
        offset: None,
        reason: format!("{context}: overflow when adding {a} + {b}"),
    })
}

pub(crate) fn take<const N: usize>(buf: &[u8], offset: usize) -> Option<[u8; N]> {
    let end = offset.checked_add(N)?;
    let slice = buf.get(offset..end)?;
    let mut out = [0u8; N];
    out.copy_from_slice(slice);
    Some(out)
}

pub(crate) fn read_u8(buf: &[u8], offset: usize) -> Option<u8> {
    buf.get(offset).copied()
}

pub(crate) fn read_u32_le(buf: &[u8], offset: usize) -> Option<u32> {
    let bytes = take::<4>(buf, offset)?;
    Some(u32::from_le_bytes(bytes))
}

pub(crate) fn read_u64_le(buf: &[u8], offset: usize) -> Option<u64> {
    let bytes = take::<8>(buf, offset)?;
    Some(u64::from_le_bytes(bytes))
}

pub(crate) fn read_id128(buf: &[u8], offset: usize) -> Option<[u8; 16]> {
    take::<16>(buf, offset)
}

pub(crate) fn is_ascii_field_name(name: &[u8]) -> bool {
    name.iter().all(|&b| b.is_ascii() && b != b'=' && b != 0)
}

pub(crate) fn hex_encode(bytes: &[u8]) -> String {
    const LUT: &[u8; 16] = b"0123456789abcdef";
    let mut out = Vec::with_capacity(bytes.len().saturating_mul(2));
    for &b in bytes {
        out.push(LUT[(b >> 4) as usize]);
        out.push(LUT[(b & 0x0f) as usize]);
    }
    String::from_utf8_lossy(&out).into_owned()
}

pub(crate) fn hex_decode(s: &str) -> Result<Vec<u8>> {
    let s = s.trim();
    if !s.len().is_multiple_of(2) {
        return Err(SdJournalError::InvalidQuery {
            reason: "hex string must have even length".to_string(),
        });
    }

    fn val(c: u8) -> Option<u8> {
        match c {
            b'0'..=b'9' => Some(c - b'0'),
            b'a'..=b'f' => Some(c - b'a' + 10),
            b'A'..=b'F' => Some(c - b'A' + 10),
            _ => None,
        }
    }

    let bytes = s.as_bytes();
    let mut out = Vec::with_capacity(bytes.len() / 2);
    let mut i = 0;
    while i < bytes.len() {
        let hi = val(bytes[i]).ok_or_else(|| SdJournalError::InvalidQuery {
            reason: "invalid hex digit".to_string(),
        })?;
        let lo = val(bytes[i + 1]).ok_or_else(|| SdJournalError::InvalidQuery {
            reason: "invalid hex digit".to_string(),
        })?;
        out.push((hi << 4) | lo);
        i += 2;
    }
    Ok(out)
}

pub(crate) fn ensure_limit_usize(kind: LimitKind, limit: usize, value: usize) -> Result<()> {
    if value > limit {
        return Err(SdJournalError::LimitExceeded {
            kind,
            limit: u64::try_from(limit).unwrap_or(u64::MAX),
        });
    }
    Ok(())
}

pub(crate) mod hash {
    use siphasher::sip::SipHasher24;
    use std::hash::Hasher;

    pub(crate) fn siphash24(key: &[u8; 16], data: &[u8]) -> u64 {
        let mut hasher = SipHasher24::new_with_key(key);
        hasher.write(data);
        hasher.finish()
    }

    pub(crate) fn jenkins_hash64(data: &[u8]) -> u64 {
        let (pc, pb) = jenkins_hashlittle2(data, 0, 0);
        ((u64::from(pc)) << 32) | u64::from(pb)
    }

    fn jenkins_hashlittle2(key: &[u8], init_pc: u32, init_pb: u32) -> (u32, u32) {
        let mut a = 0xdeadbeefu32
            .wrapping_add(u32::try_from(key.len()).unwrap_or(u32::MAX))
            .wrapping_add(init_pc);
        let mut b = a;
        let mut c = a.wrapping_add(init_pb);

        fn mix(a: &mut u32, b: &mut u32, c: &mut u32) {
            *a = a.wrapping_sub(*c);
            *a ^= c.rotate_left(4);
            *c = c.wrapping_add(*b);

            *b = b.wrapping_sub(*a);
            *b ^= a.rotate_left(6);
            *a = a.wrapping_add(*c);

            *c = c.wrapping_sub(*b);
            *c ^= b.rotate_left(8);
            *b = b.wrapping_add(*a);

            *a = a.wrapping_sub(*c);
            *a ^= c.rotate_left(16);
            *c = c.wrapping_add(*b);

            *b = b.wrapping_sub(*a);
            *b ^= a.rotate_left(19);
            *a = a.wrapping_add(*c);

            *c = c.wrapping_sub(*b);
            *c ^= b.rotate_left(4);
            *b = b.wrapping_add(*a);
        }

        fn final_(a: &mut u32, b: &mut u32, c: &mut u32) {
            *c ^= *b;
            *c = c.wrapping_sub(b.rotate_left(14));

            *a ^= *c;
            *a = a.wrapping_sub(c.rotate_left(11));

            *b ^= *a;
            *b = b.wrapping_sub(a.rotate_left(25));

            *c ^= *b;
            *c = c.wrapping_sub(b.rotate_left(16));

            *a ^= *c;
            *a = a.wrapping_sub(c.rotate_left(4));

            *b ^= *a;
            *b = b.wrapping_sub(a.rotate_left(14));

            *c ^= *b;
            *c = c.wrapping_sub(b.rotate_left(24));
        }

        let mut i = 0usize;
        while key.len().saturating_sub(i) > 12 {
            let a_part = u32::from_le_bytes([key[i], key[i + 1], key[i + 2], key[i + 3]]);
            let b_part = u32::from_le_bytes([key[i + 4], key[i + 5], key[i + 6], key[i + 7]]);
            let c_part = u32::from_le_bytes([key[i + 8], key[i + 9], key[i + 10], key[i + 11]]);

            a = a.wrapping_add(a_part);
            b = b.wrapping_add(b_part);
            c = c.wrapping_add(c_part);
            mix(&mut a, &mut b, &mut c);

            i += 12;
        }

        let tail = &key[i..];
        if tail.is_empty() {
            return (c, b);
        }

        let n = tail.len();
        if n >= 12 {
            c = c.wrapping_add((u32::from(tail[11])) << 24);
        }
        if n >= 11 {
            c = c.wrapping_add((u32::from(tail[10])) << 16);
        }
        if n >= 10 {
            c = c.wrapping_add((u32::from(tail[9])) << 8);
        }
        if n >= 9 {
            c = c.wrapping_add(u32::from(tail[8]));
        }
        if n >= 8 {
            b = b.wrapping_add((u32::from(tail[7])) << 24);
        }
        if n >= 7 {
            b = b.wrapping_add((u32::from(tail[6])) << 16);
        }
        if n >= 6 {
            b = b.wrapping_add((u32::from(tail[5])) << 8);
        }
        if n >= 5 {
            b = b.wrapping_add(u32::from(tail[4]));
        }
        if n >= 4 {
            a = a.wrapping_add((u32::from(tail[3])) << 24);
        }
        if n >= 3 {
            a = a.wrapping_add((u32::from(tail[2])) << 16);
        }
        if n >= 2 {
            a = a.wrapping_add((u32::from(tail[1])) << 8);
        }
        if n >= 1 {
            a = a.wrapping_add(u32::from(tail[0]));
        }

        final_(&mut a, &mut b, &mut c);
        (c, b)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn checked_add_u64_reports_overflow_with_context() {
        let err = checked_add_u64(u64::MAX, 1, "read range").unwrap_err();
        match err {
            SdJournalError::Corrupt {
                path,
                offset,
                reason,
            } => {
                assert!(path.is_none());
                assert!(offset.is_none());
                assert_eq!(
                    reason,
                    "read range: overflow when adding 18446744073709551615 + 1"
                );
            }
            other => panic!("unexpected error: {other:?}"),
        }
    }

    #[test]
    fn take_and_read_helpers_handle_bounds() {
        let buf = [1u8, 2, 3, 4, 5, 6, 7, 8, 9];

        assert_eq!(take::<4>(&buf, 1), Some([2, 3, 4, 5]));
        assert_eq!(take::<4>(&buf, 6), None);

        assert_eq!(read_u8(&buf, 0), Some(1));
        assert_eq!(read_u8(&buf, 99), None);

        assert_eq!(read_u32_le(&buf, 1), Some(u32::from_le_bytes([2, 3, 4, 5])));
        assert_eq!(
            read_u64_le(&buf, 1),
            Some(u64::from_le_bytes([2, 3, 4, 5, 6, 7, 8, 9]))
        );
        assert_eq!(read_u64_le(&buf, 2), None);
        assert_eq!(read_id128(&buf, 0), None);
    }

    #[test]
    fn ascii_field_name_validation_rejects_forbidden_bytes() {
        assert!(is_ascii_field_name(b"MESSAGE"));
        assert!(is_ascii_field_name(b"_SYSTEMD_UNIT"));
        assert!(!is_ascii_field_name(b"BAD=FIELD"));
        assert!(!is_ascii_field_name(b"BAD\0FIELD"));
        assert!(!is_ascii_field_name("字段".as_bytes()));
    }

    #[test]
    fn hex_roundtrip_and_decode_rejects_invalid_input() {
        let bytes = [0x00, 0x7f, 0xa5, 0xff];
        assert_eq!(hex_encode(&bytes), "007fa5ff");
        assert_eq!(hex_decode("007FA5ff").unwrap(), bytes);

        match hex_decode("abc") {
            Err(SdJournalError::InvalidQuery { reason }) => {
                assert_eq!(reason, "hex string must have even length");
            }
            other => panic!("unexpected result: {other:?}"),
        }

        match hex_decode("zz") {
            Err(SdJournalError::InvalidQuery { reason }) => {
                assert_eq!(reason, "invalid hex digit");
            }
            other => panic!("unexpected result: {other:?}"),
        }
    }

    #[test]
    fn ensure_limit_usize_allows_equal_and_rejects_greater() {
        ensure_limit_usize(LimitKind::FieldsPerEntry, 3, 3).unwrap();

        match ensure_limit_usize(LimitKind::FieldsPerEntry, 3, 4) {
            Err(SdJournalError::LimitExceeded { kind, limit }) => {
                assert_eq!(kind, LimitKind::FieldsPerEntry);
                assert_eq!(limit, 3);
            }
            other => panic!("unexpected result: {other:?}"),
        }
    }
}