syslog_fmt 0.5.0

Zero-allocation RFC 5424 syslog message formatter
Documentation
//! Formats log records as [RFC 5424](https://datatracker.ietf.org/doc/html/rfc5424) syslog
//! messages.
//!
//! Transport is intentionally out of scope. Separating formatting from transport lets callers
//! choose their own delivery mechanism (UDP, Unix socket, TCP) and keeps this crate's dependency
//! footprint minimal.

use core::{fmt, marker::PhantomData};
pub mod v5424;

/// Encoded priority value. [spec](https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1)
type Priority = u8;

/// Variant values are pre-shifted by 3 bits because the RFC encodes priority as `(facility << 3) |
/// severity`, reserving the lower 3 bits for severity.
#[derive(Copy, Clone, Debug)]
#[repr(u8)]
#[derive(Default)]
pub enum Facility {
    /// Kernel messages.
    Kern = 0 << 3,
    /// User-level messages. A reasonable default when no more specific facility applies.
    User = 1 << 3,
    /// Mail system.
    Mail = 2 << 3,
    /// System daemons without a dedicated facility.
    Daemon = 3 << 3,
    /// Security and authentication messages.
    Auth = 4 << 3,
    /// Messages generated by the syslog daemon itself.
    Syslog = 5 << 3,
    /// Line printer subsystem.
    Lpr = 6 << 3,
    /// Network news subsystem.
    News = 7 << 3,
    /// UUCP subsystem.
    Uucp = 8 << 3,
    /// Clock daemon (cron/at).
    Cron = 9 << 3,
    /// Security messages that must not be readable by unprivileged users, for systems where `Auth` logs
    /// are world-readable.
    Authpriv = 10 << 3,
    /// FTP daemon.
    Ftp = 11 << 3,
    /// Available for local use; no IANA-defined semantics.
    #[default]
    Local0 = 16 << 3,
    /// Available for local use; no IANA-defined semantics.
    Local1 = 17 << 3,
    /// Available for local use; no IANA-defined semantics.
    Local2 = 18 << 3,
    /// Available for local use; no IANA-defined semantics.
    Local3 = 19 << 3,
    /// Available for local use; no IANA-defined semantics.
    Local4 = 20 << 3,
    /// Available for local use; no IANA-defined semantics.
    Local5 = 21 << 3,
    /// Available for local use; no IANA-defined semantics.
    Local6 = 22 << 3,
    /// Available for local use; no IANA-defined semantics.
    Local7 = 23 << 3,
}

impl fmt::Display for Facility {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Facility::Kern => "Kern",
            Facility::User => "User",
            Facility::Mail => "Mail",
            Facility::Daemon => "Daemon",
            Facility::Auth => "Auth",
            Facility::Syslog => "Syslog",
            Facility::Lpr => "Lpr",
            Facility::News => "News",
            Facility::Uucp => "Uucp",
            Facility::Cron => "Cron",
            Facility::Authpriv => "Authpriv",
            Facility::Ftp => "Ftp",
            Facility::Local0 => "Local0",
            Facility::Local1 => "Local1",
            Facility::Local2 => "Local2",
            Facility::Local3 => "Local3",
            Facility::Local4 => "Local4",
            Facility::Local5 => "Local5",
            Facility::Local6 => "Local6",
            Facility::Local7 => "Local7",
        };

        f.write_str(s)
    }
}

impl<T> fmt::Display for IntToEnumError<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let enum_name: &'static str = std::any::type_name::<T>();
        write!(f, "Failed to convert {} to {}", self.value, enum_name)
    }
}

/// Delegates to the `i32` implementation to keep conversion logic in one place.
impl TryFrom<u8> for Facility {
    type Error = IntToEnumError<Self>;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        Into::<i32>::into(value).try_into()
    }
}

/// Accepts `libc::c_int` to support bridging from C syslog APIs that represent facilities as plain
/// integers.
impl TryFrom<i32> for Facility {
    type Error = IntToEnumError<Self>;

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        let variant = match value {
            0 => Self::Kern,
            1 => Self::User,
            2 => Self::Mail,
            3 => Self::Daemon,
            4 => Self::Auth,
            5 => Self::Syslog,
            6 => Self::Lpr,
            7 => Self::News,
            8 => Self::Uucp,
            9 => Self::Cron,
            10 => Self::Authpriv,
            11 => Self::Ftp,
            16 => Self::Local0,
            17 => Self::Local1,
            18 => Self::Local2,
            19 => Self::Local3,
            20 => Self::Local4,
            21 => Self::Local5,
            22 => Self::Local6,
            23 => Self::Local7,
            _ => {
                return Err(IntToEnumError {
                    value,
                    target: PhantomData,
                });
            }
        };

        Ok(variant)
    }
}

/// Indicates how urgent a log message is.
///
/// Ordered from most critical (`Emerg`) to least (`Debug`).
#[derive(Copy, Clone, Debug)]
#[repr(u8)]
pub enum Severity {
    /// System is unusable; immediate human intervention required.
    Emerg,
    /// A condition that must be corrected immediately to prevent further damage.
    Alert,
    /// A serious failure that may still allow the system to keep running.
    Crit,
    /// An error that prevented an operation from completing.
    Err,
    /// Something unexpected happened, but the system can continue.
    Warning,
    /// A normal but significant event worth tracking.
    Notice,
    /// Routine operational information confirming expected behaviour.
    Info,
    /// Detailed information useful only when diagnosing problems.
    Debug,
}

impl fmt::Display for Severity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Severity::Emerg => "Emerg",
            Severity::Alert => "Alert",
            Severity::Crit => "Crit",
            Severity::Err => "Err",
            Severity::Warning => "Warning",
            Severity::Notice => "Notice",
            Severity::Info => "Info",
            Severity::Debug => "Debug",
        };

        f.write_str(s)
    }
}

/// Delegates to the `i32` implementation to keep conversion logic in one place.
impl TryFrom<u8> for Severity {
    type Error = IntToEnumError<Self>;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        Into::<i32>::into(value).try_into()
    }
}

/// Accepts `libc::c_int` to support bridging from C syslog APIs that represent severities as plain
/// integers.
impl TryFrom<i32> for Severity {
    type Error = IntToEnumError<Self>;

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        let variant = match value {
            0 => Self::Emerg,
            1 => Self::Alert,
            2 => Self::Crit,
            3 => Self::Err,
            4 => Self::Warning,
            5 => Self::Notice,
            6 => Self::Info,
            7 => Self::Debug,
            _ => {
                return Err(IntToEnumError {
                    value,
                    target: PhantomData,
                });
            }
        };

        Ok(variant)
    }
}

/// Error produced when an integer does not map to a known [`Facility`] or [`Severity`] variant.
///
/// `PhantomData<T>` carries the target type name into the error message without adding to the
/// struct's memory footprint.
pub struct IntToEnumError<T> {
    value: i32,
    target: PhantomData<T>,
}

impl<T> fmt::Debug for IntToEnumError<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("IntToEnumError")
            .field("value", &self.value)
            .field("target", &std::any::type_name::<T>())
            .finish()
    }
}