syd 3.54.1

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/log/map.rs: Ordered map for log entries
//
// Copyright (c) 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    fmt,
    hash::{Hash, Hasher},
    io,
    io::Write,
};

use nix::errno::Errno;

use super::WriteBuf;

/// Log value with fully fallible allocation.
#[derive(Clone, Debug)]
pub enum LogValue {
    /// JSON null.
    Null,
    /// JSON boolean.
    Bool(bool),
    /// Signed 64-bit integer.
    I64(i64),
    /// Unsigned 64-bit integer.
    U64(u64),
    /// 64-bit float.
    F64(f64),
    /// Borrowed static string.
    Borrowed(&'static str),
    /// Serialized raw JSON.
    Raw(Vec<u8>),
}

impl LogValue {
    /// Fallibly serialize any `Serialize` type into a `LogValue`.
    pub fn try_serialize<T: serde::Serialize>(val: &T) -> Result<Self, Errno> {
        let mut buf = WriteBuf::new();

        serde_json::to_writer(&mut buf, val).or(Err(Errno::ENOMEM))?;

        match buf.0.first() {
            // Number: Try to parse as i64, then u64, then f64.
            Some(b'-' | b'0'..=b'9') => {
                // serde_json produces valid UTF-8 for numbers.
                if let Ok(s) = std::str::from_utf8(&buf.0) {
                    if let Ok(n) = s.parse::<i64>() {
                        return Ok(Self::I64(n));
                    }
                    if let Ok(n) = s.parse::<u64>() {
                        return Ok(Self::U64(n));
                    }
                    if let Ok(n) = s.parse::<f64>() {
                        return Ok(Self::F64(n));
                    }
                }

                // Fallback: Store as raw.
                Ok(Self::Raw(buf.0))
            }
            // Boolean true
            Some(b't') => Ok(Self::Bool(true)),
            // Boolean false
            Some(b'f') => Ok(Self::Bool(false)),
            // Null
            Some(b'n') => Ok(Self::Null),
            // Store anything else as raw JSON.
            Some(_) => Ok(Self::Raw(buf.0)),
            // Empty output should not happen.
            None => Err(Errno::EINVAL),
        }
    }

    /// Returns true if this is a null value.
    pub fn is_null(&self) -> bool {
        matches!(self, Self::Null)
    }

    /// Try to extract as i64.
    pub fn as_i64(&self) -> Option<i64> {
        match self {
            Self::I64(n) => Some(*n),
            Self::U64(n) => i64::try_from(*n).ok(),
            _ => None,
        }
    }

    /// Try to extract as u64.
    pub fn as_u64(&self) -> Option<u64> {
        match self {
            Self::U64(n) => Some(*n),
            Self::I64(n) => u64::try_from(*n).ok(),
            _ => None,
        }
    }

    /// Write this value as JSON into a writer.
    fn write_json<W: Write>(&self, writer: &mut W) -> io::Result<()> {
        match self {
            Self::Null => writer.write_all(b"null"),
            Self::Bool(true) => writer.write_all(b"true"),
            Self::Bool(false) => writer.write_all(b"false"),
            Self::I64(n) => {
                let mut buf = itoa::Buffer::new();
                writer.write_all(buf.format(*n).as_bytes())
            }
            Self::U64(n) => {
                let mut buf = itoa::Buffer::new();
                writer.write_all(buf.format(*n).as_bytes())
            }
            Self::F64(n) => write!(writer, "{n}"),
            Self::Borrowed(s) => write_json_string(writer, s),
            Self::Raw(bytes) => writer.write_all(bytes),
        }
    }
}

impl TryFrom<String> for LogValue {
    type Error = Errno;

    fn try_from(s: String) -> Result<Self, Errno> {
        Self::try_serialize(&s)
    }
}

impl fmt::Display for LogValue {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Null => f.write_str("null"),
            Self::Bool(true) => f.write_str("true"),
            Self::Bool(false) => f.write_str("false"),
            Self::I64(n) => write!(f, "{n}"),
            Self::U64(n) => write!(f, "{n}"),
            Self::F64(n) => write!(f, "{n}"),
            Self::Borrowed(s) => write!(f, "\"{s}\""),
            Self::Raw(bytes) => {
                // serde_json always produces valid UTF-8.
                let s = std::str::from_utf8(bytes).unwrap_or("?");
                f.write_str(s)
            }
        }
    }
}

/// Write a str as a JSON-quoted string, escaping as needed.
fn write_json_string<W: Write>(writer: &mut W, s: &str) -> io::Result<()> {
    writer.write_all(b"\"")?;
    for byte in s.as_bytes() {
        match byte {
            b'"' => writer.write_all(b"\\\"")?,
            b'\\' => writer.write_all(b"\\\\")?,
            b'\n' => writer.write_all(b"\\n")?,
            b'\r' => writer.write_all(b"\\r")?,
            b'\t' => writer.write_all(b"\\t")?,
            0x00..=0x1f => {
                let hi = b"0123456789abcdef"[(byte >> 4) as usize];
                let lo = b"0123456789abcdef"[(byte & 0x0f) as usize];
                writer.write_all(&[b'\\', b'u', b'0', b'0', hi, lo])?;
            }
            _ => writer.write_all(std::slice::from_ref(byte))?,
        }
    }
    writer.write_all(b"\"")
}

impl From<i32> for LogValue {
    fn from(v: i32) -> Self {
        Self::I64(i64::from(v))
    }
}

impl From<u32> for LogValue {
    fn from(v: u32) -> Self {
        Self::U64(u64::from(v))
    }
}

impl From<i64> for LogValue {
    fn from(v: i64) -> Self {
        Self::I64(v)
    }
}

impl From<u64> for LogValue {
    fn from(v: u64) -> Self {
        Self::U64(v)
    }
}

impl From<bool> for LogValue {
    fn from(v: bool) -> Self {
        Self::Bool(v)
    }
}

impl From<&'static str> for LogValue {
    fn from(s: &'static str) -> Self {
        Self::Borrowed(s)
    }
}

impl Hash for LogValue {
    fn hash<H: Hasher>(&self, state: &mut H) {
        match self {
            Self::Null => 0u8.hash(state),
            Self::Bool(b) => {
                1u8.hash(state);
                b.hash(state);
            }
            Self::I64(n) => {
                2u8.hash(state);
                n.hash(state);
            }
            Self::U64(n) => {
                2u8.hash(state);
                n.hash(state);
            }
            Self::F64(n) => {
                2u8.hash(state);
                n.to_bits().hash(state);
            }
            Self::Borrowed(s) => {
                3u8.hash(state);
                s.hash(state);
            }
            Self::Raw(bytes) => {
                3u8.hash(state);
                bytes.hash(state);
            }
        }
    }
}

/// Ordered map for log entries.
#[derive(Clone)]
pub struct LogMap {
    entries: Vec<(&'static str, LogValue)>,
}

impl Default for LogMap {
    fn default() -> Self {
        Self::new()
    }
}

impl fmt::Debug for LogMap {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_map()
            .entries(self.entries.iter().map(|(k, v)| (k, v)))
            .finish()
    }
}

impl LogMap {
    /// Create an empty map.
    pub fn new() -> Self {
        LogMap {
            entries: Vec::new(),
        }
    }

    /// Returns the number of entries.
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Returns true if empty.
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    /// Insert or replace.
    pub fn try_insert(&mut self, key: &'static str, value: LogValue) -> Result<(), Errno> {
        if let Some(entry) = self.entries.iter_mut().find(|(k, _)| *k == key) {
            entry.1 = value;
            return Ok(());
        }

        self.entries.try_reserve(1).or(Err(Errno::ENOMEM))?;
        self.entries.push((key, value));

        Ok(())
    }

    /// Insert at index for field ordering. Moves existing key if present.
    pub fn try_shift_insert(
        &mut self,
        index: usize,
        key: &'static str,
        value: LogValue,
    ) -> Result<(), Errno> {
        let existed = if let Some(pos) = self.entries.iter().position(|(k, _)| *k == key) {
            self.entries.remove(pos);
            true
        } else {
            false
        };

        let index = index.min(self.entries.len());

        if !existed {
            self.entries.try_reserve(1).or(Err(Errno::ENOMEM))?;
        }

        self.entries.insert(index, (key, value));

        Ok(())
    }

    /// Remove a key, returning its value.
    pub fn remove(&mut self, key: &str) -> Option<LogValue> {
        if let Some(pos) = self.entries.iter().position(|(k, _)| *k == key) {
            Some(self.entries.remove(pos).1)
        } else {
            None
        }
    }

    /// Retain entries matching the predicate.
    pub fn retain<F>(&mut self, mut f: F)
    where
        F: FnMut(&str, &LogValue) -> bool,
    {
        self.entries.retain(|(k, v)| f(k, v));
    }

    /// Write this map as compact JSON into a writer.
    pub fn write_json<W: Write>(&self, writer: &mut W) -> io::Result<()> {
        writer.write_all(b"{")?;

        for (idx, (key, val)) in self.entries.iter().enumerate() {
            if idx > 0 {
                writer.write_all(b",")?;
            }

            writer.write_all(b"\"")?;
            writer.write_all(key.as_bytes())?;
            writer.write_all(b"\":")?;

            val.write_json(writer)?;
        }

        writer.write_all(b"}")
    }

    /// Write this map as pretty-printed JSON into a writer.
    pub fn write_json_pretty<W: Write>(&self, writer: &mut W) -> io::Result<()> {
        writer.write_all(b"{\n")?;

        let last = self.entries.len().saturating_sub(1);
        for (idx, (key, val)) in self.entries.iter().enumerate() {
            writer.write_all(b"  \"")?;
            writer.write_all(key.as_bytes())?;
            writer.write_all(b"\": ")?;

            val.write_json(writer)?;

            if idx < last {
                writer.write_all(b",")?;
            }

            writer.write_all(b"\n")?;
        }

        writer.write_all(b"}")
    }
}

impl Hash for LogMap {
    fn hash<H: Hasher>(&self, state: &mut H) {
        const LOG_KEYS: &[&str] = &[
            "act", "addr", "cap", "ctx", "err", "exe", "lib", "mnt", "mode", "msg", "op", "path",
            "sig", "sys", "tip", "type", "unix",
        ];

        for (key, val) in &self.entries {
            if LOG_KEYS.binary_search(key).is_ok() {
                key.hash(state);
                val.hash(state);
            }
        }
    }
}