use std::io::BufRead;
use plushie_ext::codec::Codec;
use plushie_ext::protocol::{IncomingMessage, SessionMessage};
use serde_json::Value;
pub(crate) fn detect_codec(forced: Option<Codec>, reader: &mut impl BufRead) -> Codec {
match forced {
Some(c) => {
log::info!("wire codec (forced): {c}");
c
}
None => {
let buf = match reader.fill_buf() {
Ok(buf) if !buf.is_empty() => buf,
Ok(_) => {
log::error!("stdin closed before first message");
std::process::exit(1);
}
Err(e) => {
log::error!("stdin read error during codec detection: {e}");
std::process::exit(1);
}
};
let codec = Codec::detect_from_first_byte(buf[0]);
log::info!("wire codec (detected): {codec}");
codec
}
}
}
pub(crate) struct InitialSettings {
pub session: String,
pub settings: Value,
}
impl InitialSettings {
pub fn into_parts(self) -> (String, IncomingMessage) {
(
self.session,
IncomingMessage::Settings {
settings: self.settings,
},
)
}
pub fn into_incoming_message(self) -> IncomingMessage {
IncomingMessage::Settings {
settings: self.settings,
}
}
}
pub(crate) fn read_required_settings(codec: &Codec, reader: &mut impl BufRead) -> InitialSettings {
let payload = match codec.read_message(reader) {
Ok(Some(bytes)) => bytes,
Ok(None) => {
startup_exit(codec, "stdin closed before settings received");
}
Err(e) => {
startup_exit(codec, &format!("failed to read initial settings: {e}"));
}
};
let value: Value = match codec.decode(&payload) {
Ok(v) => v,
Err(e) => {
startup_exit(codec, &format!("failed to decode initial settings: {e}"));
}
};
let sm = match SessionMessage::from_value(value) {
Ok(sm) => sm,
Err(e) => {
startup_exit(codec, &format!("failed to parse initial settings: {e}"));
}
};
match sm.message {
IncomingMessage::Settings { settings } => {
log::info!("initial settings received (session {:?})", sm.session);
InitialSettings {
session: sm.session,
settings,
}
}
ref other => {
let variant = message_variant_name(other);
startup_exit(
codec,
&format!("expected settings as first message, got {variant}"),
);
}
}
}
pub(crate) fn validate_settings(settings: &Value, expected_token: Option<&str>, codec: &Codec) {
let expected = u64::from(plushie_ext::protocol::PROTOCOL_VERSION);
if let Some(version) = settings.get("protocol_version").and_then(|v| v.as_u64()) {
if version != expected {
startup_exit(
codec,
&format!(
"protocol version mismatch: host sent {version}, renderer expects {expected}"
),
);
}
} else {
log::warn!("no protocol_version in Settings, assuming compatible (expected {expected})");
}
if let Some(expected_tok) = expected_token {
match settings.get("token").and_then(|v| v.as_str()) {
Some(tok) if constant_time_eq(tok.as_bytes(), expected_tok.as_bytes()) => {
log::info!("token verified");
}
Some(_) => {
startup_exit(codec, "token mismatch: connection rejected");
}
None => {
startup_exit(codec, "missing token in Settings: connection rejected");
}
}
}
plushie_renderer_lib::settings::apply_validate_props(settings);
}
pub(crate) fn collect_font_bytes(settings: &Value) -> Vec<Vec<u8>> {
let mut font_bytes = plushie_renderer_lib::settings::parse_inline_fonts(settings);
if let Some(fonts) = settings.get("fonts").and_then(|v| v.as_array()) {
for font_val in fonts {
if let Some(path) = font_val.as_str() {
match std::fs::read(path) {
Ok(bytes) => {
log::info!("loaded font: {path}");
font_bytes.push(bytes);
}
Err(e) => {
log::error!("failed to load font {path}: {e}");
}
}
}
}
}
font_bytes
}
fn startup_exit(codec: &Codec, message: &str) -> ! {
log::error!("{message}");
let error = serde_json::json!({"type": "error", "message": message});
if let Ok(bytes) = codec.encode(&error) {
let _ = plushie_renderer_lib::emitters::write_output(&bytes);
}
std::process::exit(1);
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
fn message_variant_name(msg: &IncomingMessage) -> &'static str {
match msg {
IncomingMessage::Snapshot { .. } => "snapshot",
IncomingMessage::Patch { .. } => "patch",
IncomingMessage::Effect { .. } => "effect",
IncomingMessage::WidgetOp { .. } => "widget_op",
IncomingMessage::Subscribe { .. } => "subscribe",
IncomingMessage::Unsubscribe { .. } => "unsubscribe",
IncomingMessage::WindowOp { .. } => "window_op",
IncomingMessage::SystemOp { .. } => "system_op",
IncomingMessage::SystemQuery { .. } => "system_query",
IncomingMessage::Settings { .. } => "settings",
IncomingMessage::Query { .. } => "query",
IncomingMessage::Interact { .. } => "interact",
IncomingMessage::TreeHash { .. } => "tree_hash",
IncomingMessage::Screenshot { .. } => "screenshot",
IncomingMessage::Reset { .. } => "reset",
IncomingMessage::ImageOp { .. } => "image_op",
IncomingMessage::ExtensionCommand { .. } => "extension_command",
IncomingMessage::ExtensionCommands { .. } => "extension_commands",
IncomingMessage::AdvanceFrame { .. } => "advance_frame",
IncomingMessage::RegisterEffectStub { .. } => "register_effect_stub",
IncomingMessage::UnregisterEffectStub { .. } => "unregister_effect_stub",
}
}