use serde::Deserialize;
use rho_sdk::hooks::HookDecision;
use super::config::HOOKS_SCHEMA_VERSION;
pub const MAX_DECISION_BYTES: usize = 8 * 1024;
const MAX_REASON_BYTES: usize = 1024;
#[derive(Debug, Deserialize)]
struct RawDecision {
version: u32,
decision: String,
#[serde(default)]
reason: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum DecisionError {
#[error("handler wrote no decision on stdout")]
Empty,
#[error("handler decision is not valid JSON: {0}")]
Malformed(String),
#[error(
"handler decision reports schema version {found}; this build speaks {HOOKS_SCHEMA_VERSION}"
)]
SchemaMismatch { found: u32 },
#[error("handler decision '{0}' is not 'continue' or 'deny'")]
UnknownDecision(String),
#[error("handler denied without a reason")]
MissingReason,
#[error("handler wrote more than {MAX_DECISION_BYTES} bytes on stdout")]
TooLarge,
}
pub fn parse_decision(stdout: &[u8]) -> Result<HookDecision, DecisionError> {
if stdout.len() > MAX_DECISION_BYTES {
return Err(DecisionError::TooLarge);
}
let text = String::from_utf8_lossy(stdout);
if text.trim().is_empty() {
return Err(DecisionError::Empty);
}
let raw: RawDecision = serde_json::from_str(text.trim())
.map_err(|error| DecisionError::Malformed(error.to_string()))?;
if raw.version != HOOKS_SCHEMA_VERSION {
return Err(DecisionError::SchemaMismatch { found: raw.version });
}
match raw.decision.as_str() {
"continue" => Ok(HookDecision::Continue),
"deny" => {
let reason = raw
.reason
.map(|reason| reason.trim().to_owned())
.filter(|reason| !reason.is_empty())
.ok_or(DecisionError::MissingReason)?;
Ok(HookDecision::Deny {
reason: truncate_on_char_boundary(reason, MAX_REASON_BYTES),
})
}
other => Err(DecisionError::UnknownDecision(other.to_owned())),
}
}
fn truncate_on_char_boundary(mut value: String, limit: usize) -> String {
if value.len() <= limit {
return value;
}
let mut boundary = limit;
while boundary > 0 && !value.is_char_boundary(boundary) {
boundary -= 1;
}
value.truncate(boundary);
value
}
#[cfg(test)]
#[path = "protocol_tests.rs"]
mod tests;