mod client_events;
#[cfg(all(any(unix, windows), feature = "net"))]
mod regular;
#[cfg(all(any(unix, windows), feature = "net"))]
pub use regular::*;
#[cfg(all(target_family = "wasm", feature = "net"))]
mod browser;
#[cfg(all(target_family = "wasm", feature = "net"))]
pub use browser::*;
#[cfg(feature = "net")]
pub mod streaming;
pub use client_events::*;
#[cfg(feature = "net")]
type HostResult = Result<HostResponse, ClientError>;
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Deserialization(#[from] bincode::Error),
#[error("channel closed")]
ChannelClosed,
#[cfg(all(any(unix, windows), feature = "net"))]
#[error(transparent)]
ConnectionError(#[from] tokio_tungstenite::tungstenite::Error),
#[cfg(all(target_family = "wasm", feature = "net"))]
#[error("request error: {0}")]
ConnectionError(serde_json::Value),
#[error("connection closed")]
ConnectionClosed,
#[error("unhandled error: {0}")]
OtherError(Box<dyn std::error::Error + Send + Sync>),
}
pub trait TryFromFbs<T>: Sized {
fn try_decode_fbs(value: T) -> Result<Self, WsApiError>;
}
pub(crate) fn fixed_size_field<const N: usize>(
field: &str,
data: &[u8],
) -> Result<[u8; N], WsApiError> {
data.try_into().map_err(|_| {
WsApiError::deserialization(format!(
"{field} must be exactly {N} bytes; got {} bytes",
data.len()
))
})
}
pub(crate) fn unknown_union_discriminant(union: &str, discriminant: u8) -> WsApiError {
WsApiError::deserialization(format!("unknown {union} discriminant: {discriminant}"))
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum WsApiError {
#[error("Unsupported contract version")]
UnsupportedContractVersion,
#[error("Failed unpacking contract container")]
UnpackingContractContainerError(Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("Failed decoding message from client request: {cause}")]
DeserError { cause: String },
}
impl WsApiError {
pub fn deserialization(cause: String) -> Self {
Self::DeserError { cause }
}
pub fn into_fbs_bytes(self) -> Vec<u8> {
use crate::generated::host_response::{
finish_host_response_buffer, Error, ErrorArgs, HostResponse, HostResponseArgs,
HostResponseType,
};
let mut builder = flatbuffers::FlatBufferBuilder::new();
let as_msg = format!("{self}");
let msg_offset = builder.create_string(&as_msg);
let err_offset = Error::create(
&mut builder,
&ErrorArgs {
msg: Some(msg_offset),
},
);
let res = HostResponse::create(
&mut builder,
&HostResponseArgs {
response_type: HostResponseType::Error,
response: Some(err_offset.as_union_value()),
},
);
finish_host_response_buffer(&mut builder, res);
builder.finished_data().to_vec()
}
}
#[cfg(test)]
mod decode_boundary_conventions {
const DECODER_SOURCES: [(&str, &str); 5] = [
(
"client_api/client_events.rs",
include_str!("client_api/client_events.rs"),
),
(
"contract_interface/update.rs",
include_str!("contract_interface/update.rs"),
),
(
"contract_interface/key.rs",
include_str!("contract_interface/key.rs"),
),
(
"delegate_interface.rs",
include_str!("delegate_interface.rs"),
),
("versioning.rs", include_str!("versioning.rs")),
];
fn code_lines(src: &str) -> impl Iterator<Item = (usize, &str)> {
src.lines()
.enumerate()
.map(|(i, l)| (i + 1, l.split("//").next().unwrap_or("")))
.filter(|(_, l)| !l.trim().is_empty())
}
#[test]
fn no_wire_field_is_read_with_a_panicking_conversion() {
let wire = concat!(".by", "tes()");
let bad = [concat!("unwr", "ap()"), concat!("copy_from_", "slice")];
let mut hits = vec![];
for (name, src) in DECODER_SOURCES {
for (line_no, line) in code_lines(src) {
if line.contains(wire) && bad.iter().any(|b| line.contains(b)) {
hits.push(format!("{name}:{line_no}: {}", line.trim()));
}
}
}
assert!(
hits.is_empty(),
"a wire field is read with a conversion that panics on the wrong length. \
The flatbuffers verifier checks that a `(required)` field is PRESENT, not \
that it is the right LENGTH, so this is reachable from any client and kills \
the connection task. Use `client_api::fixed_size_field` instead.\n{}",
hits.join("\n")
);
}
#[test]
fn union_matches_never_use_unreachable() {
let needle = concat!("unreach", "able!");
let mut hits = vec![];
for (name, src) in DECODER_SOURCES {
for (line_no, line) in code_lines(src) {
if line.contains(needle) {
hits.push(format!("{name}:{line_no}: {}", line.trim()));
}
}
}
assert!(
hits.is_empty(),
"a decoder treats an unrecognized value as impossible. Every generated \
flatbuffers union verifier ends in `_ => Ok(())`, so any discriminant a \
client sets reaches the match and this panics the connection task. Return \
`client_api::unknown_union_discriminant` instead.\n{}",
hits.join("\n")
);
}
}