#[derive(Debug, Clone, Copy)]
pub enum CompleteOutcome {
Reaped,
Killed,
ReapFailed,
}
impl CompleteOutcome {
pub(super) fn as_str(self) -> &'static str {
match self {
Self::Reaped => "reaped",
Self::Killed => "killed",
Self::ReapFailed => "reap_failed",
}
}
}
#[derive(Debug)]
pub struct SpawnRecord<'a> {
pub wallclock_ms: u64,
pub observer_ns: u64,
pub agent_pid: u32,
pub child_pid: u32,
pub mode: &'a str,
pub program: &'a str,
pub source: &'a str,
pub template_len: u32,
}
#[derive(Debug)]
pub struct CompleteRecord {
pub wallclock_ms: u64,
pub observer_ns: u64,
pub agent_pid: u32,
pub child_pid: u32,
pub outcome: CompleteOutcome,
pub exit_code: Option<i32>,
pub signal: Option<i32>,
pub duration_ns: u64,
pub stdout_len: u32,
pub stderr_len: u32,
pub truncated: bool,
}
#[derive(Debug)]
pub struct RefusedRecord<'a> {
pub wallclock_ms: u64,
pub observer_ns: u64,
pub agent_pid: u32,
pub reason: &'a str,
}
#[derive(Debug, Clone, Copy)]
pub(super) enum BootReason {
Fresh,
Resume,
LegacyV1,
CorruptTail,
SchemaDrift,
Rotation,
}
impl BootReason {
pub(super) fn as_str(self) -> &'static str {
match self {
Self::Fresh => "fresh",
Self::Resume => "resume",
Self::LegacyV1 => "legacy_v1",
Self::CorruptTail => "corrupt_tail",
Self::SchemaDrift => "schema_drift",
Self::Rotation => "rotation",
}
}
}
#[derive(Debug, Clone, Copy)]
pub(super) enum AuditKind {
Boot,
Spawn,
Complete,
Refused,
}
impl AuditKind {
#[cfg_attr(not(feature = "audit-chain"), allow(dead_code))]
pub(super) fn as_bytes(self) -> &'static [u8] {
match self {
Self::Boot => b"boot",
Self::Spawn => b"spawn",
Self::Complete => b"complete",
Self::Refused => b"refused",
}
}
}
pub(super) const AUDIT_HEADER_V2: &str = "# varta-watch recovery audit v2\n";
pub(super) const AUDIT_HEADER_V1_PREFIX: &str = "# varta-watch recovery audit v1";
#[inline]
pub fn chain_enabled() -> bool {
cfg!(feature = "audit-chain")
}
pub(super) fn parse_record(line: &[u8]) -> Option<(u64, [u8; 32])> {
let s = core::str::from_utf8(line).ok()?;
if s.starts_with('#') {
return None;
}
let mut cols = s.split('\t');
let seq_str = cols.next()?;
let seq: u64 = seq_str.parse().ok()?;
let chain_str = s.rsplit('\t').next()?;
if chain_str == "-" {
return Some((seq, [0u8; 32]));
}
if chain_str.len() != 64 {
return None;
}
let raw = varta_vlp::util::decode_hex_32(chain_str.as_bytes()).ok()?;
Some((seq, raw))
}
pub(super) fn sanitize(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'\t' | '\n' | '\r' => out.push(' '),
_ => out.push(ch),
}
}
out
}
pub(super) fn hex_encode_32_string(bytes: &[u8; 32]) -> String {
let hex = varta_vlp::util::encode_hex_32(bytes);
String::from_utf8(hex.to_vec()).expect("hex output is ASCII")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_strips_tabs_and_newlines() {
assert_eq!(sanitize("a\tb"), "a b");
assert_eq!(sanitize("a\nb"), "a b");
assert_eq!(sanitize("/usr/bin/x"), "/usr/bin/x");
}
}