General Upgrade, NEW: WebServers, Better Docs
Some checks failed
CI / checks (push) Failing after 5m18s

This commit is contained in:
Alex Emmet 2026-07-18 03:08:03 +02:00
commit 6e5c985719
122 changed files with 10309 additions and 5206 deletions

View file

@ -1,9 +1,6 @@
use wasm_bindgen::{JsCast, prelude::*};
use mtp_codec::{
CommunicationType, CommunicationValue, DataType, DataValue, communication_type_name,
data_type_name,
};
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, PROTOCOL_VERSION};
use mtp_type_map::TypeMap;
use crate::error::js_error;
@ -25,40 +22,40 @@ fn set_prop(obj: &js_sys::Object, key: &str, value: &JsValue) -> Result<(), JsVa
}
fn integer_value(value: &str) -> JsValue {
if let Ok(number) = value.parse::<f64>() {
if number.fract() == 0.0 && number.abs() <= 9_007_199_254_740_991.0 {
return JsValue::from_f64(number);
}
if let Ok(number) = value.parse::<f64>()
&& number.fract() == 0.0
&& number.abs() <= 9_007_199_254_740_991.0
{
return JsValue::from_f64(number);
}
JsValue::from_str(value)
}
fn data_value_to_js(value: &DataValue) -> Result<JsValue, JsValue> {
fn data_value_to_js(value: &DataValue, tm: &TypeMap) -> Result<JsValue, JsValue> {
match value {
DataValue::BoolTrue => Ok(JsValue::TRUE),
DataValue::BoolFalse => Ok(JsValue::FALSE),
DataValue::Bool(v) => Ok(JsValue::from_bool(*v)),
DataValue::SignedNumber(n) => Ok(integer_value(&n.to_string())),
DataValue::UnsignedNumber(n) => Ok(integer_value(&n.to_string())),
DataValue::Float(exp, mant) => {
Ok(JsValue::from_f64((*mant as f64) * 10f64.powi(*exp as i32)))
}
DataValue::Float(value) => Ok(JsValue::from_f64(*value)),
DataValue::Str(s) => Ok(JsValue::from_str(s)),
DataValue::Bytes(bytes) => Ok(js_sys::Uint8Array::from(&bytes[..]).into()),
DataValue::Array(values) => {
let arr = js_sys::Array::new();
for value in values {
arr.push(&data_value_to_js(value)?);
arr.push(&data_value_to_js(value, tm)?);
}
Ok(arr.into())
}
DataValue::Container(entries) => {
let obj = js_sys::Object::new();
for (key, value) in entries {
let name = data_type_name(key.0)
let name = tm
.data_type_name(key.0)
.map(str::to_string)
.unwrap_or_else(|| key.0.to_string());
set_prop(&obj, &name, &data_value_to_js(value)?)?;
set_prop(&obj, &name, &data_value_to_js(value, tm)?)?;
}
Ok(obj.into())
}
@ -71,7 +68,8 @@ fn data_value_to_js(value: &DataValue) -> Result<JsValue, JsValue> {
}
}
fn js_to_data_value(value: &JsValue) -> Result<DataValue, JsValue> {
const MAX_SAFE_INT: f64 = 9007199254740991.0; // 2^53 - 1
fn js_to_data_value(value: &JsValue, tm: &TypeMap) -> Result<DataValue, JsValue> {
if value.is_null() || value.is_undefined() {
return Ok(DataValue::Null);
}
@ -88,19 +86,25 @@ fn js_to_data_value(value: &JsValue) -> Result<DataValue, JsValue> {
let array = js_sys::Array::from(value);
let mut values = Vec::with_capacity(array.length() as usize);
for item in array.iter() {
values.push(js_to_data_value(&item)?);
values.push(js_to_data_value(&item, tm)?);
}
return Ok(DataValue::Array(values));
}
if let Some(v) = value.as_f64() {
if v.fract() == 0.0 {
if v >= 0.0 {
return Ok(DataValue::UnsignedNumber(v as u128));
if let Some(v) = value.as_f64() {
if v.is_finite()
&& v.fract() == 0.0
&& (-(MAX_SAFE_INT + 1.0)..=MAX_SAFE_INT).contains(&v)
{
if v >= 0.0 {
return Ok(DataValue::UnsignedNumber(v as u128));
} else {
return Ok(DataValue::SignedNumber(v as i128));
}
}
return Ok(DataValue::SignedNumber(v as i128));
return Ok(DataValue::Float(v));
}
let mantissa = (v * 1_000_000.0).round() as u32;
return Ok(DataValue::Float(246, mantissa));
return Ok(DataValue::Float(v));
}
let type_name = value.js_typeof().as_string().unwrap_or_default();
@ -131,12 +135,15 @@ fn js_to_data_value(value: &JsValue) -> Result<DataValue, JsValue> {
.as_string()
.ok_or_else(|| js_error("object key must be a string"))?;
let data_type = DataType::from_name(&key)
.ok_or_else(|| js_error(&format!("unknown data type: {key}")))?;
.ok_or_else(|| js_error(format!("unknown data type: {key}")))?;
let value = js_sys::Reflect::get(&object, &JsValue::from_str(&key))?;
entries.push((
data_type.to_id(&TypeMap::latest()),
js_to_data_value(&value)?,
));
let id = data_type.try_to_id(tm).ok_or_else(|| {
js_error(format!(
"data type {key} is not available in protocol version {}",
tm.version
))
})?;
entries.push((id, js_to_data_value(&value, tm)?));
}
return Ok(DataValue::Container(entries));
}
@ -150,7 +157,7 @@ fn option_u32(options: &JsValue, key: &str) -> Result<Option<u32>, JsValue> {
return Ok(None);
}
let Some(n) = value.as_f64() else {
return Err(js_error(&format!("{key} must be a number")));
return Err(js_error(format!("{key} must be a number")));
};
Ok(Some(n as u32))
}
@ -173,14 +180,18 @@ fn option_u64(options: &JsValue, key: &str) -> Result<Option<u64>, JsValue> {
return as_string
.parse::<u64>()
.map(Some)
.map_err(|_| js_error(&format!("{key} out of range")));
.map_err(|_| js_error(format!("{key} out of range")));
}
Err(js_error(&format!("{key} must be a number or bigint")))
Err(js_error(format!("{key} must be a number or bigint")))
}
pub(crate) fn parse_frame_value(frame: &[u8]) -> Result<JsValue, JsValue> {
let comm = CommunicationValue::from_bytes(frame)
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
.map_err(|e| js_error(format!("parse failed: {}", e)))?;
let tm = comm
.type_map()
.cloned()
.unwrap_or_else(|| TypeMap::new(PROTOCOL_VERSION));
let obj = js_sys::Object::new();
let data = js_sys::Object::new();
@ -188,7 +199,8 @@ pub(crate) fn parse_frame_value(frame: &[u8]) -> Result<JsValue, JsValue> {
set_prop(&obj, "id", &JsValue::from_f64(comm.get_id() as f64))?;
}
let frame_type = communication_type_name(comm.get_type().0)
let frame_type = tm
.communication_type_name(comm.get_type().0)
.map(str::to_string)
.unwrap_or_else(|| comm.get_type().0.to_string());
set_prop(&obj, "type", &JsValue::from_str(&frame_type))?;
@ -209,10 +221,11 @@ pub(crate) fn parse_frame_value(frame: &[u8]) -> Result<JsValue, JsValue> {
}
for (key, value) in comm.data() {
let name = data_type_name(key.0)
let name = tm
.data_type_name(key.0)
.map(str::to_string)
.unwrap_or_else(|| key.0.to_string());
set_prop(&data, &name, &data_value_to_js(value)?)?;
set_prop(&data, &name, &data_value_to_js(value, &tm)?)?;
}
set_prop(&obj, "data", &data.into())?;
set_prop(&obj, "raw", &js_sys::Uint8Array::from(frame).into())?;
@ -244,14 +257,14 @@ pub fn build_ping_frame(
}
msg.to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))
.map_err(|e| js_error(format!("encode failed: {}", e)))
}
/// Parse an auth response frame into a JS object.
#[wasm_bindgen]
#[wasm_bindgen(unchecked_return_type = "AuthResponse")]
pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
let comm = CommunicationValue::from_bytes(response)
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
.map_err(|e| js_error(format!("parse failed: {}", e)))?;
let connected = matches!(comm.get_data(DataType::Connected), DataValue::BoolTrue);
@ -282,10 +295,20 @@ pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
js_sys::Reflect::set(&obj, &"clientNonce".into(), &arr).ok();
}
if let Some(id) = assigned_id {
js_sys::Reflect::set(&obj, &"assignedId".into(), &JsValue::from(id as f64)).ok();
js_sys::Reflect::set(
&obj,
&"assignedId".into(),
&JsValue::bigint_from_str(&id.to_string()),
)
.ok();
}
if let Some(ts) = timestamp {
js_sys::Reflect::set(&obj, &"timestamp".into(), &JsValue::from(ts as f64)).ok();
js_sys::Reflect::set(
&obj,
&"timestamp".into(),
&JsValue::bigint_from_str(&ts.to_string()),
)
.ok();
}
if let Some(sig) = signature {
let arr = js_sys::Uint8Array::from(&sig[..]);
@ -299,7 +322,7 @@ pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
#[wasm_bindgen]
pub fn format_frame(frame: &[u8]) -> Result<String, JsValue> {
let comm = CommunicationValue::from_bytes(frame)
.map_err(|e| js_error(&format!("parse failed: {}", e)))?;
.map_err(|e| js_error(format!("parse failed: {}", e)))?;
Ok(comm.to_string())
}
@ -317,7 +340,8 @@ pub fn build_frame(
options: JsValue,
) -> Result<Vec<u8>, JsValue> {
let comm_type = CommunicationType::from_name(message_type)
.ok_or_else(|| js_error(&format!("unknown communication type: {message_type}")))?;
.ok_or_else(|| js_error(format!("unknown communication type: {message_type}")))?;
let tm = TypeMap::new(PROTOCOL_VERSION);
let mut msg = CommunicationValue::new(comm_type);
if !options.is_null() && !options.is_undefined() {
@ -341,12 +365,15 @@ pub fn build_frame(
.as_string()
.ok_or_else(|| js_error("object key must be a string"))?;
let data_type = DataType::from_name(&key)
.ok_or_else(|| js_error(&format!("unknown data type: {key}")))?;
.ok_or_else(|| js_error(format!("unknown data type: {key}")))?;
let value = js_sys::Reflect::get(&object, &JsValue::from_str(&key))?;
msg = msg.add_data(
data_type.to_id(&TypeMap::latest()),
js_to_data_value(&value)?,
);
let id = data_type.try_to_id(&tm).ok_or_else(|| {
js_error(format!(
"data type {key} is not available in protocol version {}",
tm.version
))
})?;
msg = msg.add_data(id, js_to_data_value(&value, &tm)?);
}
} else if !data.is_null() && !data.is_undefined() {
return Err(js_error(
@ -355,7 +382,7 @@ pub fn build_frame(
}
msg.to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))
.map_err(|e| js_error(format!("encode failed: {}", e)))
}
#[cfg(test)]
@ -370,7 +397,10 @@ mod tests {
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
let tm = TypeMap::latest();
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
assert_eq!(
cv.get_type(),
CommunicationType::Ping.try_to_id(&tm).unwrap()
);
assert_eq!(cv.get_sender(), 42);
assert_eq!(
cv.get_data(DataType::Description),
@ -389,7 +419,10 @@ mod tests {
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
let tm = TypeMap::latest();
assert_eq!(cv.get_type(), CommunicationType::Ping.to_id(&tm));
assert_eq!(
cv.get_type(),
CommunicationType::Ping.try_to_id(&tm).unwrap()
);
assert_eq!(cv.get_sender(), 99);
assert_eq!(
cv.get_data(DataType::Description),
@ -412,13 +445,27 @@ mod tests {
assert_eq!(cv.get_sender(), 0);
}
#[wasm_bindgen_test]
fn fractional_float_roundtrips_without_corruption() {
let tm = TypeMap::new(PROTOCOL_VERSION);
for expected in [-12.5, 0.125, 1.5e200] {
let encoded = js_to_data_value(&JsValue::from_f64(expected), &tm)
.expect("JS float should encode");
assert_eq!(encoded, DataValue::Float(expected));
let decoded = data_value_to_js(&encoded, &tm).expect("float should decode");
assert_eq!(decoded.as_f64(), Some(expected));
}
}
#[wasm_bindgen_test]
fn parse_auth_response_success() {
const ASSIGNED_ID: u128 = 9_007_199_254_740_993;
const TIMESTAMP: u128 = 9_007_199_254_740_995;
let resp = CommunicationValue::new(CommunicationType::IdentificationResponse)
.add_typed_default(DataType::Connected, DataValue::BoolTrue)
.add_typed_default(DataType::ClientNonce, DataValue::UnsignedNumber(999))
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(42))
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(12345))
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(ASSIGNED_ID))
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(TIMESTAMP))
.to_bytes()
.expect("encode failed");
@ -430,9 +477,20 @@ mod tests {
assert_eq!(connected, Some(true));
let id = js_sys::Reflect::get(&result, &"assignedId".into())
.ok()
.and_then(|v| v.as_f64());
assert_eq!(id, Some(42.0));
.expect("assignedId should be present")
.unchecked_into::<js_sys::BigInt>()
.to_string(10)
.expect("assignedId should stringify")
.as_string();
assert_eq!(id.as_deref(), Some("9007199254740993"));
let timestamp = js_sys::Reflect::get(&result, &"timestamp".into())
.expect("timestamp should be present")
.unchecked_into::<js_sys::BigInt>()
.to_string(10)
.expect("timestamp should stringify")
.as_string();
assert_eq!(timestamp.as_deref(), Some("9007199254740995"));
}
#[wasm_bindgen_test]