use std::fmt;
use std::io::Write as _;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::Arc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use crate::ask::Correlation;
use crate::queue::{FieldPath, Predicate};
use crate::schema::Message;
use crate::transport::QueueName;
pub const VALIDATE_QUEUE_ENV: &str = "ONEMESSAGEBUS_VALIDATE_QUEUE";
pub const VALIDATE_CORRELATION_ENV: &str = "ONEMESSAGEBUS_VALIDATE_CORRELATION";
pub const PASS_RECORD_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "verdict", rename_all = "lowercase")]
pub enum Verdict {
Pass,
Refuse {
reason: String,
},
Unjudged {
reason: String,
},
}
impl Verdict {
#[must_use]
pub fn passes(&self) -> bool {
matches!(self, Self::Pass)
}
#[must_use]
pub fn reason(&self) -> Option<&str> {
match self {
Self::Pass => None,
Self::Refuse { reason } | Self::Unjudged { reason } => Some(reason),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ValidationContext {
pub queue: QueueName,
pub correlation: Option<Correlation>,
}
impl ValidationContext {
#[must_use]
pub fn new(queue: QueueName) -> Self {
Self {
queue,
correlation: None,
}
}
#[must_use]
pub fn with_correlation(mut self, correlation: Correlation) -> Self {
self.correlation = Some(correlation);
self
}
}
pub trait Validator<M: Message>: Send + Sync {
fn validate(&self, message: &M, context: &ValidationContext) -> Verdict;
}
pub struct Validators<M> {
each: Vec<Arc<dyn Validator<M>>>,
}
impl<M> Clone for Validators<M> {
fn clone(&self) -> Self {
Self {
each: self.each.clone(),
}
}
}
impl<M> Default for Validators<M> {
fn default() -> Self {
Self { each: Vec::new() }
}
}
impl<M> fmt::Debug for Validators<M> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Validators")
.field("count", &self.each.len())
.finish()
}
}
impl<M: Message> Validators<M> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(mut self, validator: impl Validator<M> + 'static) -> Self {
self.each.push(Arc::new(validator));
self
}
pub fn push(&mut self, validator: Arc<dyn Validator<M>>) {
self.each.push(validator);
}
#[must_use]
pub fn len(&self) -> usize {
self.each.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.each.is_empty()
}
pub fn judge(&self, message: &M, context: &ValidationContext) -> Verdict {
combined(
self.each
.iter()
.map(|validator| validator.validate(message, context))
.collect::<Vec<_>>(),
)
}
}
pub(crate) fn combined(verdicts: impl IntoIterator<Item = Verdict>) -> Verdict {
let mut refused = None;
let mut unjudged = None;
for verdict in verdicts {
match verdict {
Verdict::Pass => {}
Verdict::Refuse { reason } => {
refused.get_or_insert(reason);
}
Verdict::Unjudged { reason } => {
unjudged.get_or_insert(reason);
}
}
}
match (refused, unjudged) {
(Some(reason), _) => Verdict::Refuse { reason },
(None, Some(reason)) => Verdict::Unjudged { reason },
(None, None) => Verdict::Pass,
}
}
pub struct OnRecords<M, V> {
validator: V,
of: PhantomData<fn() -> M>,
}
impl<M: Message, V: Validator<M>> OnRecords<M, V> {
#[must_use]
pub fn new(validator: V) -> Self {
Self {
validator,
of: PhantomData,
}
}
}
impl<M: Message, V: Validator<M>> Validator<Value> for OnRecords<M, V> {
fn validate(&self, message: &Value, context: &ValidationContext) -> Verdict {
match serde_json::from_value::<M>(message.clone()) {
Ok(typed) => self.validator.validate(&typed, context),
Err(failure) => Verdict::Refuse {
reason: format!("the message is not a {}: {failure}", M::SCHEMA),
},
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum When {
Carries(FieldPath),
Matches(Predicate),
}
impl When {
#[must_use]
pub fn admits(&self, message: &Value) -> bool {
match self {
Self::Carries(field) => Predicate::NonEmpty {
field: field.clone(),
non_empty: true,
}
.matches(message),
Self::Matches(predicate) => predicate.matches(message),
}
}
}
const CARRIES: &str = "carries";
impl Serialize for When {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Carries(field) => {
let mut form = Map::new();
form.insert(CARRIES.to_owned(), Value::String(field.to_string()));
form.serialize(serializer)
}
Self::Matches(predicate) => predicate.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for When {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = Value::deserialize(deserializer)?;
if let Some(form) = value.as_object().filter(|form| form.contains_key(CARRIES)) {
if form.len() != 1 {
let others: Vec<&str> = form
.keys()
.map(String::as_str)
.filter(|key| *key != CARRIES)
.collect();
return Err(serde::de::Error::custom(format!(
"`carries` stands alone in a `when`, and this also names {}",
others.join(" and ")
)));
}
let field = form
.get(CARRIES)
.and_then(Value::as_str)
.ok_or_else(|| serde::de::Error::custom("`carries` names a field path"))?
.parse::<FieldPath>()
.map_err(serde::de::Error::custom)?;
return Ok(Self::Carries(field));
}
Predicate::deserialize(value)
.map(Self::Matches)
.map_err(serde::de::Error::custom)
}
}
impl JsonSchema for When {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("When")
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let field = generator.subschema_for::<FieldPath>();
let predicate = generator.subschema_for::<Predicate>();
schemars::json_schema!({
"description": "When a validator judges a message: `{carries: <field path>}`, or any queue predicate.",
"anyOf": [
{
"type": "object",
"properties": { "carries": field },
"required": ["carries"],
"additionalProperties": false
},
predicate
]
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ValidatorError {
#[error("a command validator names no command; give its argv, the program first")]
NoCommand,
#[error("a pass cache names no bar fingerprint command; give the argv whose output fingerprints the bar, so a moved bar invalidates what was passed under the old one")]
NoBar,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandValidator {
command: Vec<String>,
cache: Option<PassCache>,
}
impl CommandValidator {
pub fn new(
command: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self, ValidatorError> {
let command: Vec<String> = command.into_iter().map(Into::into).collect();
if command.first().is_none_or(|program| program.is_empty()) {
return Err(ValidatorError::NoCommand);
}
Ok(Self {
command,
cache: None,
})
}
#[must_use]
pub fn with_cache(mut self, cache: PassCache) -> Self {
self.cache = Some(cache);
self
}
#[must_use]
pub fn command(&self) -> &[String] {
&self.command
}
#[must_use]
pub fn cache(&self) -> Option<&PassCache> {
self.cache.as_ref()
}
#[must_use]
pub fn judge(&self, content: &[u8], context: &ValidationContext) -> Verdict {
let key = self
.cache
.as_ref()
.and_then(|cache| cache.key(&self.command, content).map(|key| (cache, key)));
if let Some((cache, key)) = &key {
if cache.holds(key) {
return Verdict::Pass;
}
}
let verdict = self.run(content, context);
if verdict.passes() {
if let Some((cache, key)) = &key {
let _ = cache.record(key);
}
}
verdict
}
fn rendered(&self) -> String {
self.command.join(" ")
}
fn run(&self, content: &[u8], context: &ValidationContext) -> Verdict {
let Some((program, arguments)) = self.command.split_first() else {
return Verdict::Unjudged {
reason: ValidatorError::NoCommand.to_string(),
};
};
let mut command = Command::new(program);
command
.args(arguments)
.env(VALIDATE_QUEUE_ENV, context.queue.as_str())
.env_remove(VALIDATE_CORRELATION_ENV);
if let Some(correlation) = &context.correlation {
command.env(VALIDATE_CORRELATION_ENV, correlation.as_str());
}
let spawned = command
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn();
let mut child = match spawned {
Ok(child) => child,
Err(failure) => {
return Verdict::Unjudged {
reason: format!(
"the validator `{}` could not be run: {failure}",
self.rendered()
),
}
}
};
let stdin = child.stdin.take();
let mut offered = content.to_vec();
offered.push(b'\n');
let writing = match std::thread::Builder::new()
.name("onemessagebus-validator-stdin".to_owned())
.spawn(move || {
if let Some(mut stdin) = stdin {
let _ = stdin.write_all(&offered);
}
}) {
Ok(writing) => writing,
Err(failure) => {
let _ = child.wait();
return Verdict::Unjudged {
reason: format!(
"the validator `{}` could not receive its message: {failure}",
self.rendered()
),
};
}
};
let output = child.wait_with_output();
let _ = writing.join();
let output = match output {
Ok(output) => output,
Err(failure) => {
return Verdict::Unjudged {
reason: format!(
"the validator `{}` did not finish: {failure}",
self.rendered()
),
}
}
};
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
match output.status.code() {
Some(0) => Verdict::Pass,
Some(1) if stderr.trim().is_empty() => Verdict::Refuse {
reason: format!(
"the validator `{}` refused the message and wrote no reason on stderr",
self.rendered()
),
},
Some(1) => Verdict::Refuse { reason: stderr },
Some(code) => Verdict::Unjudged {
reason: format!(
"the validator `{}` exited {code}, which is neither a pass (0) nor a refusal (1){}",
self.rendered(),
said(&stderr)
),
},
None => Verdict::Unjudged {
reason: format!(
"the validator `{}` was ended by a signal before it answered{}",
self.rendered(),
said(&stderr)
),
},
}
}
}
fn said(stderr: &str) -> String {
let trimmed = stderr.trim();
if trimmed.is_empty() {
String::new()
} else {
format!("; it said: {trimmed}")
}
}
impl<M: Message> Validator<M> for CommandValidator {
fn validate(&self, message: &M, context: &ValidationContext) -> Verdict {
match serde_json::to_vec(message) {
Ok(content) => self.judge(&content, context),
Err(failure) => Verdict::Unjudged {
reason: format!("the message could not be written as JSON: {failure}"),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PassCache {
dir: PathBuf,
bar_fingerprint: Vec<String>,
}
struct PassKey {
content: String,
bar: String,
fingerprint: String,
command: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct PassRecord {
schema_version: u32,
content: String,
bar: String,
fingerprint: String,
command: Vec<String>,
}
impl PassCache {
pub fn new(
dir: impl Into<PathBuf>,
bar_fingerprint: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self, ValidatorError> {
let bar_fingerprint: Vec<String> = bar_fingerprint.into_iter().map(Into::into).collect();
if bar_fingerprint
.first()
.is_none_or(|program| program.is_empty())
{
return Err(ValidatorError::NoBar);
}
Ok(Self {
dir: dir.into(),
bar_fingerprint,
})
}
#[must_use]
pub fn dir(&self) -> &Path {
&self.dir
}
#[must_use]
pub fn bar_fingerprint(&self) -> &[String] {
&self.bar_fingerprint
}
#[must_use]
pub fn fingerprint(&self) -> Option<String> {
let (program, arguments) = self.bar_fingerprint.split_first()?;
let output = Command::new(program)
.args(arguments)
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()
.ok()?;
if !output.status.success() {
return None;
}
let printed = String::from_utf8_lossy(&output.stdout).trim().to_owned();
(!printed.is_empty()).then_some(printed)
}
#[must_use]
pub fn holds_pass(&self, command: &[String], content: &[u8]) -> bool {
self.key(command, content)
.is_some_and(|key| self.holds(&key))
}
fn key(&self, command: &[String], content: &[u8]) -> Option<PassKey> {
let fingerprint = self.fingerprint()?;
let mut bar = Sha256::new();
for word in command {
bar.update(word.as_bytes());
bar.update([0]);
}
bar.update([0xff]);
bar.update(fingerprint.as_bytes());
Some(PassKey {
content: hex(&Sha256::digest(content)),
bar: hex(&bar.finalize()),
fingerprint,
command: command.to_vec(),
})
}
fn path(&self, key: &PassKey) -> PathBuf {
self.dir
.join(format!("{}.{}.pass.json", key.content, key.bar))
}
fn holds(&self, key: &PassKey) -> bool {
let Ok(bytes) = std::fs::read(self.path(key)) else {
return false;
};
serde_json::from_slice::<PassRecord>(&bytes).is_ok_and(|record| {
record.schema_version == PASS_RECORD_VERSION
&& record.content == key.content
&& record.bar == key.bar
&& record.fingerprint == key.fingerprint
&& record.command == key.command
})
}
fn record(&self, key: &PassKey) -> std::io::Result<()> {
std::fs::create_dir_all(&self.dir)?;
let record = PassRecord {
schema_version: PASS_RECORD_VERSION,
content: key.content.clone(),
bar: key.bar.clone(),
fingerprint: key.fingerprint.clone(),
command: key.command.clone(),
};
let bytes = serde_json::to_vec_pretty(&record).map_err(std::io::Error::other)?;
let path = self.path(key);
let staged = path.with_extension(format!("{}.tmp", std::process::id()));
std::fs::write(&staged, bytes)?;
std::fs::rename(&staged, &path)
}
}
fn hex(bytes: &[u8]) -> String {
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut out, byte| {
use std::fmt::Write as _;
let _ = write!(out, "{byte:02x}");
out
})
}