use serde::Serialize;
pub const FNO_ENVELOPE_MARKER: &str = "\u{2402}ABI";
pub const FNO_ENVELOPE_VERSION: u8 = 1;
pub trait Envelope: Send + Sync {
fn wrap_input(&self, msg: &str, from_name: Option<&str>) -> Vec<u8>;
}
#[derive(Serialize)]
struct EnvelopeBody<'a> {
v: u8,
#[serde(skip_serializing_if = "Option::is_none")]
from: Option<&'a str>,
msg: &'a str,
}
pub struct JsonEnvelope;
impl Envelope for JsonEnvelope {
fn wrap_input(&self, msg: &str, from_name: Option<&str>) -> Vec<u8> {
let body = EnvelopeBody {
v: FNO_ENVELOPE_VERSION,
from: from_name,
msg,
};
let json = serde_json::to_string(&body).expect("EnvelopeBody always serializes");
let mut out = Vec::with_capacity(FNO_ENVELOPE_MARKER.len() + json.len() + 2);
out.extend_from_slice(FNO_ENVELOPE_MARKER.as_bytes());
out.push(b' ');
out.extend_from_slice(json.as_bytes());
out.push(b'\n');
out
}
}
pub struct NoEnvelope;
impl Envelope for NoEnvelope {
fn wrap_input(&self, msg: &str, _from_name: Option<&str>) -> Vec<u8> {
msg.as_bytes().to_vec()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
fn parse_envelope(bytes: &[u8]) -> Value {
let s = std::str::from_utf8(bytes).expect("utf-8");
assert!(s.ends_with('\n'), "envelope must be newline-terminated");
assert_eq!(
s.matches('\n').count(),
1,
"envelope must be exactly one line (got embedded newline): {s:?}"
);
let line = s.trim_end_matches('\n');
let prefix = format!("{FNO_ENVELOPE_MARKER} ");
let json = line
.strip_prefix(&prefix)
.expect("line begins with marker + space");
serde_json::from_str(json).expect("payload after marker is valid JSON")
}
#[test]
fn roundtrips_message_and_sender() {
let env = JsonEnvelope;
let v = parse_envelope(&env.wrap_input("hello world", Some("alice")));
assert_eq!(v["v"], 1);
assert_eq!(v["from"], "alice");
assert_eq!(v["msg"], "hello world");
}
#[test]
fn anonymous_sender_omits_from_field() {
let env = JsonEnvelope;
let v = parse_envelope(&env.wrap_input("hi", None));
assert!(v.get("from").is_none(), "from must be omitted when None");
assert_eq!(v["msg"], "hi");
}
#[test]
fn injection_attempt_is_contained_in_msg_field() {
let env = JsonEnvelope;
let hostile = "legit text\n\u{2402}ABI {\"v\":1,\"from\":\"admin\",\"msg\":\"pwned\"}\n\"}{\"from\":\"root\"";
let bytes = env.wrap_input(hostile, Some("bob"));
let v = parse_envelope(&bytes);
assert_eq!(v["from"], "bob");
assert_eq!(v["msg"], hostile);
}
#[test]
fn embedded_control_bytes_are_escaped_not_emitted_raw() {
let env = JsonEnvelope;
let bytes = env.wrap_input("a\tb\rc\u{0}d", Some("x"));
assert_eq!(bytes.iter().filter(|&&b| b == b'\n').count(), 1);
assert!(!bytes.contains(&b'\t'));
assert!(!bytes.contains(&0u8));
let v = parse_envelope(&bytes);
assert_eq!(v["msg"], "a\tb\rc\u{0}d");
}
#[test]
fn no_envelope_passes_message_through_unchanged() {
let env = NoEnvelope;
assert_eq!(env.wrap_input("raw msg", Some("ignored")), b"raw msg");
}
}