Skip to main content

shellcanvas_adapter_sdk/
wire.rs

1// SPDX-License-Identifier: MPL-2.0
2//! Adapter protocol v1: big-endian u32 byte length followed by UTF-8 JSON.
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::io;
6use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
7
8/// A frame limit, not a file/tree/stream size limit. Bulk services must page/chunk.
9pub const MAX_FRAME: usize = 4 * 1024 * 1024;
10#[derive(Debug, Serialize, Deserialize)]
11#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
12pub enum Envelope {
13    Request {
14        v: u8,
15        id: u64,
16        method: String,
17        params: Value,
18    },
19    Cancel {
20        v: u8,
21        id: u64,
22    },
23    Result {
24        v: u8,
25        id: u64,
26        value: Value,
27    },
28    Error {
29        v: u8,
30        id: u64,
31        code: String,
32        message: String,
33    },
34}
35pub fn encode(message: &Envelope) -> io::Result<Vec<u8>> {
36    struct Capped(Vec<u8>);
37    impl io::Write for Capped {
38        fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
39            if bytes.len() > MAX_FRAME - self.0.len() {
40                return Err(io::Error::other(
41                    "Adapter frame exceeds the stream envelope limit",
42                ));
43            }
44            self.0.extend_from_slice(bytes);
45            Ok(bytes.len())
46        }
47        fn flush(&mut self) -> io::Result<()> {
48            Ok(())
49        }
50    }
51    let mut output = Capped(Vec::new());
52    serde_json::to_writer(&mut output, message).map_err(io::Error::other)?;
53    Ok(output.0)
54}
55pub async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> io::Result<Envelope> {
56    read_frame_or_eof(reader)
57        .await?
58        .ok_or_else(|| io::Error::from(io::ErrorKind::UnexpectedEof))
59}
60/// Distinguishes a clean pipe close from a truncated header or body.
61pub async fn read_frame_or_eof(
62    reader: &mut (impl AsyncRead + Unpin),
63) -> io::Result<Option<Envelope>> {
64    let first = match reader.read_u8().await {
65        Ok(first) => first,
66        Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
67        Err(error) => return Err(error),
68    };
69    let mut prefix = [first, 0, 0, 0];
70    reader.read_exact(&mut prefix[1..]).await?;
71    let length = u32::from_be_bytes(prefix) as usize;
72    if length == 0 || length > MAX_FRAME {
73        return Err(io::Error::other("Invalid adapter frame length"));
74    }
75    let mut data = vec![0; length];
76    reader.read_exact(&mut data).await?;
77    serde_json::from_slice(&data)
78        .map(Some)
79        .map_err(io::Error::other)
80}
81pub async fn write_frame(writer: &mut (impl AsyncWrite + Unpin), data: &[u8]) -> io::Result<()> {
82    if data.is_empty() || data.len() > MAX_FRAME {
83        return Err(io::Error::other("Invalid adapter frame length"));
84    }
85    writer.write_u32(data.len() as u32).await?;
86    writer.write_all(data).await?;
87    writer.flush().await
88}
89
90#[derive(Clone, Debug, Deserialize, Serialize)]
91#[serde(rename_all = "camelCase", deny_unknown_fields)]
92pub struct ServiceDescriptor {
93    pub id: String,
94    pub version: u32,
95    pub methods: Vec<String>,
96}
97#[derive(Clone, Debug, Deserialize, Serialize)]
98#[serde(deny_unknown_fields)]
99pub struct Initialized {
100    pub protocol: u8,
101    pub services: Vec<ServiceDescriptor>,
102}
103/// Validate a protocol identifier shared by service and package contracts.
104pub fn name(value: &str) -> bool {
105    !value.is_empty()
106        && value.len() <= 200
107        && value.split('.').all(|part| {
108            part.as_bytes().first().is_some_and(u8::is_ascii_alphabetic)
109                && part.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-')
110        })
111}
112pub fn validate(info: &Initialized) -> bool {
113    let mut ids = std::collections::HashSet::new();
114    let mut methods = std::collections::HashSet::new();
115    info.protocol == 1
116        && info.services.iter().all(|service| {
117            name(&service.id)
118                && service.id != "system"
119                && !service.id.starts_with("system.")
120                && service.version > 0
121                && ids.insert(&service.id)
122                && !service.methods.is_empty()
123                && service.methods.iter().all(|method| {
124                    name(method)
125                        && method.starts_with(&format!("{}.", service.id))
126                        && methods.insert(method)
127                })
128        })
129}