use std::fmt::Arguments;
use std::io::Write;
use log::kv::Value;
pub enum FieldName<'a> {
WellFormed(&'a str),
WriteEscaped(&'a str),
}
fn is_valid_key_char(c: char) -> bool {
matches!(c, 'A'..='Z' | '0'..='9' | '_')
}
pub fn escape_journal_key(key: &str) -> Vec<u8> {
let mut escaped = key
.to_ascii_uppercase()
.replace(|c| !is_valid_key_char(c), "_");
if escaped.starts_with(|c: char| matches!(c, '_' | '0'..='9')) {
escaped = format!("ESCAPED_{}", escaped);
}
let mut payload = escaped.into_bytes();
payload.truncate(64);
payload
}
fn put_field_name(buffer: &mut Vec<u8>, name: FieldName<'_>) {
match name {
FieldName::WellFormed(name) => buffer.extend_from_slice(name.as_bytes()),
FieldName::WriteEscaped("") => buffer.extend_from_slice(b"EMPTY"),
FieldName::WriteEscaped(name) => buffer.extend_from_slice(&escape_journal_key(name)),
}
}
pub trait PutAsFieldValue {
fn put_field_value(self, buffer: &mut Vec<u8>);
}
impl PutAsFieldValue for &[u8] {
fn put_field_value(self, buffer: &mut Vec<u8>) {
buffer.extend_from_slice(self)
}
}
impl PutAsFieldValue for &Arguments<'_> {
fn put_field_value(self, buffer: &mut Vec<u8>) {
match self.as_str() {
Some(s) => buffer.extend_from_slice(s.as_bytes()),
None => write!(buffer, "{}", self).unwrap(),
}
}
}
impl PutAsFieldValue for Value<'_> {
fn put_field_value(self, buffer: &mut Vec<u8>) {
write!(buffer, "{}", self).unwrap()
}
}
pub fn put_field_length_encoded<V: PutAsFieldValue>(
buffer: &mut Vec<u8>,
name: FieldName<'_>,
value: V,
) {
put_field_name(buffer, name);
buffer.push(b'\n');
buffer.extend_from_slice(&[0; 8]);
let value_start = buffer.len();
value.put_field_value(buffer);
let value_end = buffer.len();
let length_bytes = ((value_end - value_start) as u64).to_le_bytes();
buffer[value_start - 8..value_start].copy_from_slice(&length_bytes);
buffer.push(b'\n');
}
pub fn put_field_bytes(buffer: &mut Vec<u8>, name: FieldName<'_>, value: &[u8]) {
if value.contains(&b'\n') {
put_field_length_encoded(buffer, name, value);
} else {
put_field_name(buffer, name);
buffer.push(b'=');
buffer.extend_from_slice(value);
buffer.push(b'\n');
}
}
#[cfg(test)]
mod tests {
use super::*;
use similar_asserts::assert_eq;
use FieldName::*;
#[test]
fn escape_journal_key() {
for case in &["FOO", "FOO_123"] {
assert_eq!(
&String::from_utf8_lossy(&super::escape_journal_key(case)),
case
);
}
let cases = vec![
("foo", "FOO"),
("_foo", "ESCAPED__FOO"),
("1foo", "ESCAPED_1FOO"),
("Hallöchen", "HALL_CHEN"),
];
for (key, expected) in cases {
assert_eq!(
&String::from_utf8_lossy(&super::escape_journal_key(key)),
expected
);
}
}
#[test]
fn put_field_length_encoded() {
let mut buffer = Vec::new();
super::put_field_length_encoded(&mut buffer, WellFormed("FOO"), "BAR".as_bytes());
assert_eq!(&buffer, b"FOO\n\x03\0\0\0\0\0\0\0BAR\n");
}
#[test]
fn put_field_bytes_no_newline() {
let mut buffer = Vec::new();
super::put_field_bytes(&mut buffer, WellFormed("FOO"), "BAR".as_bytes());
assert_eq!(&buffer, b"FOO=BAR\n");
}
#[test]
fn put_field_bytes_newline() {
let mut buffer = Vec::new();
super::put_field_bytes(
&mut buffer,
WellFormed("FOO"),
"BAR\nSPAM_WITH_EGGS".as_bytes(),
);
assert_eq!(&buffer, b"FOO\n\x12\0\0\0\0\0\0\0BAR\nSPAM_WITH_EGGS\n");
}
}