// Copyright Sebastian Wiesner // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! Write well-formated journal fields to buffers. use std::fmt::Arguments; use std::io::Write; use log::kv::Value; pub enum FieldName<'a> { WellFormed(&'a str), WriteEscaped(&'a str), } /// Whether `c` is a valid character in the key of a journal field. /// /// Journal field keys may only contain ASCII uppercase letters A to Z, /// numbers 0 to 9 and the underscore. fn is_valid_key_char(c: char) -> bool { matches!(c, 'A'..='Z' | '0'..='9' | '_') } /// Escape a `key` for use in a systemd journal field. /// /// See [`crate::JournalLog`] for these rules. pub fn escape_journal_key(key: &str) -> Vec { 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, name: FieldName<'_>) { match name { FieldName::WellFormed(name) => buffer.extend_from_slice(name.as_bytes()), FieldName::WriteEscaped("") => buffer.extend_from_slice(b"EMPTY"), // FIXME: We should try to find a way to do this with less allocations. FieldName::WriteEscaped(name) => buffer.extend_from_slice(&escape_journal_key(name)), } } pub trait PutAsFieldValue { fn put_field_value(self, buffer: &mut Vec); } impl PutAsFieldValue for &[u8] { fn put_field_value(self, buffer: &mut Vec) { buffer.extend_from_slice(self) } } impl PutAsFieldValue for &Arguments<'_> { fn put_field_value(self, buffer: &mut Vec) { match self.as_str() { Some(s) => buffer.extend_from_slice(s.as_bytes()), None => write!(buffer, "{}", self).unwrap(), } } } impl<'a> PutAsFieldValue for Value<'a> { fn put_field_value(self, buffer: &mut Vec) { // TODO: We can probably write the value more efficiently by visiting it? write!(buffer, "{}", self).unwrap() } } pub fn put_field_length_encoded( buffer: &mut Vec, name: FieldName<'_>, value: V, ) { put_field_name(buffer, name); buffer.push(b'\n'); // Reserve the length tag buffer.extend_from_slice(&[0; 8]); let value_start = buffer.len(); value.put_field_value(buffer); let value_end = buffer.len(); // Fill the length tag 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, name: FieldName<'_>, value: &[u8]) { if value.contains(&b'\n') { // Write as length encoded field 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(); // See "Data Format" in https://systemd.io/JOURNAL_NATIVE_PROTOCOL/ for this example 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"); } }