use crate::event::EventKind;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fmt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
static PAYLOAD_REGISTRY_OPEN_CACHE: Mutex<Option<Result<(), EventPayloadRegistryError>>> =
Mutex::new(None);
static PAYLOAD_REGISTRY_WARNED: AtomicBool = AtomicBool::new(false);
pub trait EventPayload: Serialize + DeserializeOwned {
const KIND: EventKind;
const PAYLOAD_VERSION: u16 = 1;
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum EventPayloadValidation {
Warn,
#[default]
FailFast,
Silent,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EventPayloadKindCollision {
pub category: u8,
pub type_id: u16,
pub first_type_name: &'static str,
pub second_type_name: &'static str,
}
impl EventPayloadKindCollision {
fn from_support(collision: batpak_macros_support::EventKindCollision) -> Self {
let kind = EventKind::from_raw_u16(collision.kind_bits);
Self {
category: kind.category(),
type_id: kind.type_id(),
first_type_name: collision.first_type_name,
second_type_name: collision.second_type_name,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EventPayloadRegistryError {
collisions: Vec<EventPayloadKindCollision>,
}
impl EventPayloadRegistryError {
pub fn new(collisions: Vec<EventPayloadKindCollision>) -> Self {
assert!(
!collisions.is_empty(),
"EventPayloadRegistryError requires at least one collision"
);
Self { collisions }
}
pub fn collisions(&self) -> &[EventPayloadKindCollision] {
&self.collisions
}
}
impl fmt::Display for EventPayloadRegistryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let first = &self.collisions[0];
write!(
f,
"EventPayload registry contains {} duplicate kind assignment(s); first collision is category=0x{:X} type_id=0x{:03X} between `{}` and `{}`",
self.collisions.len(),
first.category,
first.type_id,
first.first_type_name,
first.second_type_name,
)
}
}
impl std::error::Error for EventPayloadRegistryError {}
pub fn validate_event_payload_registry() -> Result<(), EventPayloadRegistryError> {
let collisions = batpak_macros_support::find_kind_collisions()
.into_iter()
.map(EventPayloadKindCollision::from_support)
.collect::<Vec<_>>();
if collisions.is_empty() {
Ok(())
} else {
Err(EventPayloadRegistryError::new(collisions))
}
}
pub fn verify_registry() -> Result<(), EventPayloadRegistryError> {
validate_event_payload_registry()
}
#[cfg(feature = "startup-registry-check")]
#[ctor::ctor]
fn __batpak_verify_registry_at_startup() {
use std::io::Write;
if let Err(error) = verify_registry() {
let message = format!("batpak startup-registry-check: aborting before main: {error}\n");
let mut stderr = std::io::stderr();
let _ = stderr.write_all(message.as_bytes());
let _ = stderr.flush();
std::process::abort();
}
}
pub fn revalidate_event_payload_registry() -> Result<(), EventPayloadRegistryError> {
let result = validate_event_payload_registry();
PAYLOAD_REGISTRY_WARNED.store(false, Ordering::SeqCst);
let Ok(mut cached) = PAYLOAD_REGISTRY_OPEN_CACHE.lock() else {
return result;
};
*cached = Some(result.clone());
result
}
pub(crate) fn cached_event_payload_registry_validation() -> Result<(), EventPayloadRegistryError> {
let Ok(mut cached) = PAYLOAD_REGISTRY_OPEN_CACHE.lock() else {
return validate_event_payload_registry();
};
if let Some(result) = cached.as_ref() {
return result.clone();
}
let result = validate_event_payload_registry();
*cached = Some(result.clone());
result
}
pub(crate) fn mark_event_payload_registry_warning_emitted() -> bool {
!PAYLOAD_REGISTRY_WARNED.swap(true, Ordering::SeqCst)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn warning_marker_returns_true_once_until_revalidation_resets_it() {
revalidate_event_payload_registry().expect("test registry is clean");
assert!(
mark_event_payload_registry_warning_emitted(),
"PROPERTY: first open-time collision warning in a process must be emitted"
);
assert!(
!mark_event_payload_registry_warning_emitted(),
"PROPERTY: later open-time checks in the same process must not emit duplicate warnings"
);
revalidate_event_payload_registry().expect("test registry is clean after marker reset");
assert!(
mark_event_payload_registry_warning_emitted(),
"PROPERTY: explicit revalidation must reset the one-shot warning marker"
);
}
}