use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Ord, PartialOrd)]
#[cfg_attr(feature = "allocative", derive(allocative::Allocative))]
pub struct FieldName(String);
impl FieldName {
pub fn new_unchecked(name: impl Into<String>) -> Self {
Self(name.into())
}
pub fn new(name: impl Into<String>) -> Option<Self> {
let name = name.into();
if name.is_empty() || name.contains('=') {
None
} else {
Some(Self(name))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
pub fn into_inner(self) -> String {
self.0
}
pub fn with_value(&self, value: impl AsRef<str>) -> FieldValuePair {
FieldValuePair::new_unchecked(self.clone(), value.as_ref().to_string())
}
}
impl fmt::Display for FieldName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for FieldName {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Ord, PartialOrd)]
#[cfg_attr(feature = "allocative", derive(allocative::Allocative))]
pub struct FieldValuePair {
key: String,
split_pos: usize,
}
impl FieldValuePair {
pub fn new_unchecked(field: FieldName, value: String) -> Self {
let split_pos = field.as_str().len();
let key = format!("{}={}", field.as_str(), value);
Self { key, split_pos }
}
pub fn parse(s: impl AsRef<str>) -> Option<Self> {
let s = s.as_ref();
let split_pos = s.find('=')?;
if split_pos == 0 {
return None;
}
Some(Self {
key: s.to_string(),
split_pos,
})
}
pub fn parse_bytes(payload: &[u8]) -> Option<Self> {
let split_pos = payload.iter().position(|byte| *byte == b'=')?;
if split_pos == 0 {
return None;
}
let key = std::str::from_utf8(payload).ok()?.to_string();
Some(Self { key, split_pos })
}
pub fn field(&self) -> &str {
&self.key[..self.split_pos]
}
pub fn value(&self) -> &str {
&self.key[self.split_pos + 1..]
}
pub fn as_str(&self) -> &str {
&self.key
}
pub fn as_bytes(&self) -> &[u8] {
self.key.as_bytes()
}
pub fn into_inner(self) -> String {
self.key
}
pub fn extract_field(&self) -> FieldName {
FieldName::new_unchecked(self.field())
}
pub fn decompose(self) -> (FieldName, String) {
let field = FieldName::new_unchecked(self.field());
let value = self.value().to_string();
(field, value)
}
pub fn strip_field_prefix<'a>(field_name: &[u8], payload: &'a [u8]) -> Option<&'a [u8]> {
if !payload.starts_with(field_name) {
return None;
}
let offset = field_name.len();
if payload.len() <= offset || payload[offset] != b'=' {
return None;
}
Some(&payload[offset + 1..])
}
}
impl fmt::Display for FieldValuePair {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.key)
}
}
impl AsRef<str> for FieldValuePair {
fn as_ref(&self) -> &str {
&self.key
}
}
impl From<FieldValuePair> for String {
fn from(pair: FieldValuePair) -> String {
pair.into_inner()
}
}
impl From<&FieldValuePair> for String {
fn from(pair: &FieldValuePair) -> String {
pair.to_string()
}
}
impl From<FieldName> for String {
fn from(name: FieldName) -> String {
name.into_inner()
}
}
impl From<&FieldName> for String {
fn from(name: &FieldName) -> String {
name.to_string()
}
}
pub fn parse_timestamp(
field_name: &[u8],
data_object: &journal_core::file::DataObject<&[u8]>,
) -> crate::Result<u64> {
parse_timestamp_payload(field_name, data_object.raw_payload())
}
pub fn parse_timestamp_payload(field_name: &[u8], payload: &[u8]) -> crate::Result<u64> {
let value_bytes = FieldValuePair::strip_field_prefix(field_name, payload)
.ok_or_else(|| crate::IndexError::InvalidFieldPrefix)?;
let timestamp_str =
std::str::from_utf8(value_bytes).map_err(|_| crate::IndexError::NonUtf8Payload)?;
timestamp_str
.parse::<u64>()
.map_err(|_| crate::IndexError::NonIntegerPayload)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_field_name_creation() {
assert!(FieldName::new("PRIORITY").is_some());
assert!(FieldName::new("SYSLOG_IDENTIFIER").is_some());
assert!(FieldName::new("").is_none());
assert!(FieldName::new("PRIORITY=error").is_none());
}
#[test]
fn test_field_name_as_bytes() {
let field = FieldName::new("PRIORITY").unwrap();
assert_eq!(field.as_bytes(), b"PRIORITY");
assert_eq!(field.as_str(), "PRIORITY");
}
#[test]
fn test_field_value_pair_parsing() {
let pair = FieldValuePair::parse("PRIORITY=error").unwrap();
assert_eq!(pair.field(), "PRIORITY");
assert_eq!(pair.value(), "error");
assert_eq!(pair.as_str(), "PRIORITY=error");
assert_eq!(pair.as_bytes(), b"PRIORITY=error");
let pair = FieldValuePair::parse("MESSAGE=IN=eth0 OUT= MAC=aa:bb:cc").unwrap();
assert_eq!(pair.field(), "MESSAGE");
assert_eq!(pair.value(), "IN=eth0 OUT= MAC=aa:bb:cc");
assert!(FieldValuePair::parse("PRIORITY").is_none());
assert!(FieldValuePair::parse("=error").is_none());
}
#[test]
fn test_field_value_pair_parse_bytes() {
let pair = FieldValuePair::parse_bytes(b"PRIORITY=error").unwrap();
assert_eq!(pair.field(), "PRIORITY");
assert_eq!(pair.value(), "error");
assert_eq!(pair.as_bytes(), b"PRIORITY=error");
assert!(FieldValuePair::parse_bytes(b"PRIORITY").is_none());
assert!(FieldValuePair::parse_bytes(b"=error").is_none());
assert!(FieldValuePair::parse_bytes(b"FIELD=\xff").is_none());
}
#[test]
fn test_parse_timestamp_payload() {
let timestamp = parse_timestamp_payload(
b"_SOURCE_REALTIME_TIMESTAMP",
b"_SOURCE_REALTIME_TIMESTAMP=1704067200000000",
)
.unwrap();
assert_eq!(timestamp, 1_704_067_200_000_000);
assert!(matches!(
parse_timestamp_payload(b"_SOURCE_REALTIME_TIMESTAMP", b"MESSAGE=hello"),
Err(crate::IndexError::InvalidFieldPrefix)
));
}
#[test]
fn test_field_with_value() {
let field = FieldName::new("PRIORITY").unwrap();
let pair = field.with_value("error");
assert_eq!(pair.field(), "PRIORITY");
assert_eq!(pair.value(), "error");
assert_eq!(pair.as_str(), "PRIORITY=error");
}
#[test]
fn test_field_name_ordering() {
let mut fields = vec![
FieldName::new("PRIORITY").unwrap(),
FieldName::new("_HOSTNAME").unwrap(),
FieldName::new("SYSLOG_IDENTIFIER").unwrap(),
FieldName::new("ERRNO").unwrap(),
];
fields.sort();
assert_eq!(fields[0].as_str(), "ERRNO");
assert_eq!(fields[1].as_str(), "PRIORITY");
assert_eq!(fields[2].as_str(), "SYSLOG_IDENTIFIER");
assert_eq!(fields[3].as_str(), "_HOSTNAME");
}
#[test]
fn test_field_value_pair_ordering() {
let mut pairs = vec![
FieldValuePair::parse("PRIORITY=error").unwrap(),
FieldValuePair::parse("PRIORITY=debug").unwrap(),
FieldValuePair::parse("_HOSTNAME=server2").unwrap(),
FieldValuePair::parse("_HOSTNAME=server1").unwrap(),
];
pairs.sort();
assert_eq!(pairs[0].as_str(), "PRIORITY=debug");
assert_eq!(pairs[1].as_str(), "PRIORITY=error");
assert_eq!(pairs[2].as_str(), "_HOSTNAME=server1");
assert_eq!(pairs[3].as_str(), "_HOSTNAME=server2");
}
#[test]
fn test_strip_field_prefix() {
let payload = b"PRIORITY=6";
let value = FieldValuePair::strip_field_prefix(b"PRIORITY", payload);
assert_eq!(value, Some(&b"6"[..]));
let payload = b"MESSAGE=error: connection=failed";
let value = FieldValuePair::strip_field_prefix(b"MESSAGE", payload);
assert_eq!(value, Some(&b"error: connection=failed"[..]));
let payload = b"FIELD=";
let value = FieldValuePair::strip_field_prefix(b"FIELD", payload);
assert_eq!(value, Some(&b""[..]));
let payload = b"PRIORITY=6";
let value = FieldValuePair::strip_field_prefix(b"MESSAGE", payload);
assert_eq!(value, None);
let payload = b"PRIORITY6";
let value = FieldValuePair::strip_field_prefix(b"PRIORITY", payload);
assert_eq!(value, None);
let payload = b"PRIORITYX=6";
let value = FieldValuePair::strip_field_prefix(b"PRIORITY", payload);
assert_eq!(value, None);
let value = FieldValuePair::strip_field_prefix(b"PRIORITY", b"");
assert_eq!(value, None);
let value = FieldValuePair::strip_field_prefix(b"PRIORITY", b"PRI");
assert_eq!(value, None);
}
}