Merge branch 'next'

This commit is contained in:
NGnius (Graham) 2024-04-02 17:57:03 -04:00
commit 811aa01444
54 changed files with 3524 additions and 2176 deletions

1329
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,16 @@
[package]
name = "usdpl"
version = "0.10.0"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
edition = "2021"
license = "GPL-3.0-only"
repository = "https://github.com/NGnius/usdpl-rs"
readme = "README.md"
[workspace]
members = [
"usdpl-core",
"usdpl-front",
"usdpl-back",
"usdpl-build",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
exclude = [
"templates/decky/backend"
]
resolver = "2"
[profile.release]
# Tell `rustc` to optimize for small code size.
@ -16,14 +19,3 @@ debug = false
strip = true
lto = true
codegen-units = 4
[workspace]
members = [
"usdpl-core",
"usdpl-front",
"usdpl-back",
]
exclude = [
"templates/decky/backend"
]

View file

@ -1,29 +1,35 @@
[package]
name = "usdpl-back"
version = "0.10.1"
version = "0.11.0"
edition = "2021"
authors = ["NGnius <ngniusness@gmail.com>"]
license = "GPL-3.0-only"
repository = "https://github.com/NGnius/usdpl-rs"
readme = "README.md"
repository = "https://git.ngni.us/NG-SD-Plugins/usdpl-rs"
readme = "../README.md"
description = "Universal Steam Deck Plugin Library back-end"
[features]
default = ["blocking", "translate"]
default = ["blocking"]
decky = ["usdpl-core/decky"]
crankshaft = ["usdpl-core/crankshaft"]
blocking = ["tokio", "tokio/rt", "tokio/rt-multi-thread"] # synchronous API for async functionality, using tokio
encrypt = ["usdpl-core/encrypt", "obfstr", "hex"]
translate = ["usdpl-core/translate", "gettext-ng"]
blocking = [] # synchronous API for async functionality, using tokio
#encrypt = ["usdpl-core", "obfstr", "hex"]
[dependencies]
usdpl-core = { version = "0.10", path = "../usdpl-core"}
usdpl-core = { version = "0.11", path = "../usdpl-core"}
log = "0.4"
# gRPC/protobuf
nrpc = { version = "0.10", path = "../../nRPC/nrpc", default-features = false, features = [ "server-send" ] }
async-lock = "2.7"
prost = "0.11"
# websocket framework
ratchet_rs = { version = "0.4", features = [ "deflate" ] }
# HTTP web framework
warp = { version = "0.3" }
bytes = { version = "1.1" }
tokio = { version = "1", optional = true }
tokio = { version = "1", features = [ "full" ]}
# this is why people don't like async
async-trait = "0.1.57"
@ -34,4 +40,7 @@ obfstr = { version = "0.3", optional = true }
hex = { version = "0.4", optional = true }
# translations
gettext-ng = { version = "0.4.1", optional = true }
gettext-ng = { version = "0.4.1" }
[build-dependencies]
usdpl-build = { version = "0.11", path = "../usdpl-build" }

6
usdpl-back/build.rs Normal file
View file

@ -0,0 +1,6 @@
fn main() {
usdpl_build::back::build_with_custom_builtins(
[].into_iter(),
[].into_iter(),
)
}

View file

@ -5,14 +5,10 @@ use std::process::Command;
/// The home directory of the user currently running the Steam Deck UI (specifically: running gamescope).
pub fn home() -> Option<PathBuf> {
let who_out = Command::new("who")
.output().ok()?;
let who_out = Command::new("who").output().ok()?;
let who_str = String::from_utf8_lossy(who_out.stdout.as_slice());
for login in who_str.split("\n") {
let username = login
.split(" ")
.next()?
.trim();
let username = login.split(" ").next()?.trim();
let path = Path::new("/home").join(username);
if path.is_dir() {
return Some(path);

View file

@ -6,15 +6,11 @@ use std::path::PathBuf;
pub fn home() -> Option<PathBuf> {
#[cfg(not(any(feature = "decky", feature = "crankshaft")))]
let result = crate::api_any::dirs::home();
#[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))]
let result = None; // TODO
#[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))]
let result = crate::api_decky::home().ok()
.map(|x| PathBuf::from(x)
.join("..")
.canonicalize()
.ok()
).flatten();
let result = crate::api_decky::home()
.ok()
.map(|x| PathBuf::from(x).join("..").canonicalize().ok())
.flatten();
result
}
@ -23,8 +19,6 @@ pub fn home() -> Option<PathBuf> {
pub fn plugin() -> Option<PathBuf> {
#[cfg(not(any(feature = "decky", feature = "crankshaft")))]
let result = None; // TODO
#[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))]
let result = None; // TODO
#[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))]
let result = crate::api_decky::plugin_dir().ok().map(|x| x.into());
@ -35,8 +29,6 @@ pub fn plugin() -> Option<PathBuf> {
pub fn log() -> Option<PathBuf> {
#[cfg(not(any(feature = "decky", feature = "crankshaft")))]
let result = crate::api_any::dirs::log();
#[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))]
let result = None; // TODO
#[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))]
let result = crate::api_decky::log_dir().ok().map(|x| x.into());

View file

@ -1,46 +0,0 @@
//! Common low-level file operations
use std::fmt::Display;
use std::path::Path;
use std::fs::File;
use std::io::{Read, Write, self};
use std::str::FromStr;
/// Write something to a file.
/// Useful for kernel configuration files.
#[inline]
pub fn write_single<P: AsRef<Path>, D: Display>(path: P, display: D) -> Result<(), io::Error> {
let mut file = File::create(path)?;
write!(file, "{}", display)
}
/// read_single error
#[derive(Debug)]
pub enum ReadError<E> {
/// IO Error
Io(io::Error),
/// String parsing error
Parse(E),
}
impl<E: std::error::Error> std::fmt::Display for ReadError<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(io) => write!(f, "io: {}", io),
Self::Parse(e) => write!(f, "parse: {}", e),
}
}
}
impl<E: std::error::Error> std::error::Error for ReadError<E> {
}
/// Read something from a file.
/// Useful for kernel configuration files.
#[inline]
pub fn read_single<P: AsRef<Path>, D: FromStr<Err=E>, E>(path: P) -> Result<D, ReadError<E>> {
let mut file = File::open(path).map_err(ReadError::Io)?;
let mut string = String::new();
file.read_to_string(&mut string).map_err(ReadError::Io)?;
string.trim().parse().map_err(ReadError::Parse)
}

View file

@ -1,2 +1 @@
pub mod dirs;
pub mod files;

View file

@ -1 +0,0 @@
compile_error!("Crankshaft unsupported (project no longer maintained)");

View file

@ -1,87 +0,0 @@
use std::sync::{Arc, Mutex};
use usdpl_core::serdes::Primitive;
/// A mutable function which can be called from the front-end (remotely)
pub trait MutCallable: Send + Sync {
/// Invoke the function
fn call(&mut self, params: Vec<Primitive>) -> Vec<Primitive>;
}
impl<F: (FnMut(Vec<Primitive>) -> Vec<Primitive>) + Send + Sync> MutCallable for F {
fn call(&mut self, params: Vec<Primitive>) -> Vec<Primitive> {
(self)(params)
}
}
/// A function which can be called from the front-end (remotely)
pub trait Callable: Send + Sync {
/// Invoke the function
fn call(&self, params: Vec<Primitive>) -> Vec<Primitive>;
}
impl<F: (Fn(Vec<Primitive>) -> Vec<Primitive>) + Send + Sync> Callable for F {
fn call(&self, params: Vec<Primitive>) -> Vec<Primitive> {
(self)(params)
}
}
/// An async function which can be called from the front-end (remotely)
#[async_trait::async_trait]
pub trait AsyncCallable: Send + Sync {
/// Invoke the function
async fn call(&self, params: Vec<Primitive>) -> Vec<Primitive>;
}
#[async_trait::async_trait]
impl<F: (Fn(Vec<Primitive>) -> A) + Send + Sync, A: core::future::Future<Output=Vec<Primitive>> + Send> AsyncCallable for F {
async fn call(&self, params: Vec<Primitive>) -> Vec<Primitive> {
(self)(params).await
}
}
pub enum WrappedCallable {
Blocking(Arc<Mutex<Box<dyn MutCallable>>>),
Ref(Arc<Box<dyn Callable>>),
Async(Arc<Box<dyn AsyncCallable>>),
}
impl WrappedCallable {
pub fn new_ref<T: Callable + 'static>(callable: T) -> Self {
Self::Ref(Arc::new(Box::new(callable)))
}
pub fn new_locking<T: MutCallable + 'static>(callable: T) -> Self {
Self::Blocking(Arc::new(Mutex::new(Box::new(callable))))
}
pub fn new_async<T: AsyncCallable + 'static>(callable: T) -> Self {
Self::Async(Arc::new(Box::new(callable)))
}
}
impl Clone for WrappedCallable {
fn clone(&self) -> Self {
match self {
Self::Blocking(x) => Self::Blocking(x.clone()),
Self::Ref(x) => Self::Ref(x.clone()),
Self::Async(x) => Self::Async(x.clone()),
}
}
}
#[async_trait::async_trait]
impl AsyncCallable for WrappedCallable {
async fn call(&self, params: Vec<Primitive>) -> Vec<Primitive> {
match self {
Self::Blocking(mut_callable) => {
mut_callable
.lock()
.expect("Failed to acquire mut_callable lock")
.call(params)
},
Self::Ref(callable) => callable.call(params),
Self::Async(async_callable) => async_callable.call(params).await,
}
}
}

View file

@ -1,262 +0,0 @@
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use warp::Filter;
use usdpl_core::serdes::{Dumpable, Loadable};
use usdpl_core::{socket, RemoteCallResponse};
use super::{Callable, MutCallable, AsyncCallable, WrappedCallable};
static LAST_ID: AtomicU64 = AtomicU64::new(0);
const MAX_ID_DIFFERENCE: u64 = 32;
//type WrappedCallable = Arc<Mutex<Box<dyn Callable>>>; // thread-safe, cloneable Callable
#[cfg(feature = "encrypt")]
const NONCE: [u8; socket::NONCE_SIZE] = [0u8; socket::NONCE_SIZE];
/// Back-end instance for interacting with the front-end
pub struct Instance {
calls: HashMap<String, WrappedCallable>,
port: u16,
#[cfg(feature = "encrypt")]
encryption_key: Vec<u8>,
}
impl Instance {
/// Initialise an instance of the back-end
#[inline]
pub fn new(port_usdpl: u16) -> Self {
Instance {
calls: HashMap::new(),
port: port_usdpl,
#[cfg(feature = "encrypt")]
encryption_key: hex::decode(obfstr::obfstr!(env!("USDPL_ENCRYPTION_KEY"))).unwrap(),
}
}
/// Register a thread-safe function which can be invoked by the front-end
pub fn register<S: std::convert::Into<String>, F: Callable + 'static>(
mut self,
name: S,
f: F,
) -> Self {
self.calls
.insert(name.into(), WrappedCallable::new_ref(f));
self
}
/// Register a thread-unsafe function which can be invoked by the front-end
pub fn register_blocking<S: std::convert::Into<String>, F: MutCallable + 'static>(
mut self,
name: S,
f: F,
) -> Self {
self.calls
.insert(name.into(), WrappedCallable::new_locking(f));
self
}
/// Register a thread-unsafe function which can be invoked by the front-end
pub fn register_async<S: std::convert::Into<String>, F: AsyncCallable + 'static>(
mut self,
name: S,
f: F,
) -> Self {
self.calls
.insert(name.into(), WrappedCallable::new_async(f));
self
}
/// Run the web server instance forever, blocking this thread
#[cfg(feature = "blocking")]
pub fn run_blocking(&self) -> Result<(), ()> {
let result = self.serve_internal();
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(result)
}
/// Run the web server forever, asynchronously
pub async fn run(&self) -> Result<(), ()> {
self.serve_internal().await
}
#[async_recursion::async_recursion]
async fn handle_call(
packet: socket::Packet,
handlers: &HashMap<String, WrappedCallable>,
) -> socket::Packet {
match packet {
socket::Packet::Call(call) => {
log::debug!("Got USDPL call {} (`{}`, params: {})", call.id, call.function, call.parameters.len());
let last_id = LAST_ID.load(Ordering::SeqCst);
if last_id == 0 {
log::info!("Last id is 0, assuming resumed connection (overriding last id)");
LAST_ID.store(call.id, Ordering::SeqCst);
} else if call.id < MAX_ID_DIFFERENCE {
log::info!("Call ID is low, assuming new connection (resetting last id)");
LAST_ID.store(call.id, Ordering::SeqCst);
} else if call.id > last_id && call.id - last_id < MAX_ID_DIFFERENCE {
LAST_ID.store(call.id, Ordering::SeqCst);
} else if call.id < last_id && last_id - call.id < MAX_ID_DIFFERENCE {
// Allowed, but don't store new (lower) LAST_ID
} else {
#[cfg(not(debug_assertions))]
{
log::error!("Got USDPL call with strange ID! got:{} last id:{} (rejecting packet)", call.id, last_id);
return socket::Packet::Invalid
}
#[cfg(debug_assertions)]
log::warn!("Got USDPL call with strange ID! got:{} last id:{} (in release mode this packet will be rejected)", call.id, last_id);
}
//let handlers = CALLS.lock().expect("Failed to acquire CALLS lock");
if let Some(target) = handlers.get(&call.function) {
let result = target.call(call.parameters).await;
socket::Packet::CallResponse(RemoteCallResponse {
id: call.id,
response: result,
})
} else {
socket::Packet::Invalid
}
},
socket::Packet::Many(packets) => {
let mut result = Vec::with_capacity(packets.len());
for packet in packets {
result.push(Self::handle_call(packet, handlers).await);
}
socket::Packet::Many(result)
},
#[cfg(feature = "translate")]
socket::Packet::Language(lang) => socket::Packet::Translations(get_all_translations(lang)),
_ => socket::Packet::Invalid,
}
}
#[cfg(not(feature = "encrypt"))]
async fn process_body((data, handlers): (bytes::Bytes, HashMap<String, WrappedCallable>)) -> impl warp::Reply {
let (packet, _) = match socket::Packet::load_base64(&data) {
Ok(x) => x,
Err(e) => {
return warp::reply::with_status(
warp::http::Response::builder()
.body(format!("Failed to load packet: {}", e)),
warp::http::StatusCode::from_u16(400).unwrap(),
)
}
};
//let mut buffer = [0u8; socket::PACKET_BUFFER_SIZE];
let mut buffer = String::with_capacity(socket::PACKET_BUFFER_SIZE);
let response = Self::handle_call(packet, &handlers).await;
let _len = match response.dump_base64(&mut buffer) {
Ok(x) => x,
Err(e) => {
return warp::reply::with_status(
warp::http::Response::builder()
.body(format!("Failed to dump response packet: {}", e)),
warp::http::StatusCode::from_u16(500).unwrap(),
)
}
};
warp::reply::with_status(
warp::http::Response::builder().body(buffer),
warp::http::StatusCode::from_u16(200).unwrap(),
)
}
#[cfg(feature = "encrypt")]
async fn process_body((data, handlers, key): (bytes::Bytes, HashMap<String, WrappedCallable>, Vec<u8>)) -> impl warp::Reply {
let (packet, _) = match socket::Packet::load_encrypted(&data, &key, &NONCE) {
Ok(x) => x,
Err(_) => {
return warp::reply::with_status(
warp::http::Response::builder()
.body("Failed to load packet".to_string()),
warp::http::StatusCode::from_u16(400).unwrap(),
)
}
};
let mut buffer = Vec::with_capacity(socket::PACKET_BUFFER_SIZE);
//buffer.extend(&[0u8; socket::PACKET_BUFFER_SIZE]);
let response = Self::handle_call(packet, &handlers).await;
let len = match response.dump_encrypted(&mut buffer, &key, &NONCE) {
Ok(x) => x,
Err(_) => {
return warp::reply::with_status(
warp::http::Response::builder()
.body("Failed to dump response packet".to_string()),
warp::http::StatusCode::from_u16(500).unwrap(),
)
}
};
buffer.truncate(len);
let string: String = String::from_utf8(buffer).unwrap().into();
warp::reply::with_status(
warp::http::Response::builder().body(string),
warp::http::StatusCode::from_u16(200).unwrap(),
)
}
/// Receive and execute callbacks forever
async fn serve_internal(&self) -> Result<(), ()> {
let handlers = self.calls.clone();
#[cfg(not(feature = "encrypt"))]
let input_mapper = move |data: bytes::Bytes| { (data, handlers.clone()) };
#[cfg(feature = "encrypt")]
let key = self.encryption_key.clone();
#[cfg(feature = "encrypt")]
let input_mapper = move |data: bytes::Bytes| { (data, handlers.clone(), key.clone()) };
//self.calls = HashMap::new();
let calls = warp::post()
.and(warp::path!("usdpl" / "call"))
.and(warp::body::content_length_limit(
(socket::PACKET_BUFFER_SIZE * 2) as _,
))
.and(warp::body::bytes())
.map(input_mapper)
.then(Self::process_body)
.map(|reply| warp::reply::with_header(reply, "Access-Control-Allow-Origin", "*"));
#[cfg(debug_assertions)]
warp::serve(calls).run(([0, 0, 0, 0], self.port)).await;
#[cfg(not(debug_assertions))]
warp::serve(calls).run(([127, 0, 0, 1], self.port)).await;
Ok(())
}
}
#[cfg(feature = "translate")]
fn get_all_translations(language: String) -> Vec<(String, Vec<String>)> {
log::debug!("Loading translations for language `{}`...", language);
let result = load_locale(&language);
match result {
Ok(catalog) => {
let map = catalog.nalltext();
let mut result = Vec::with_capacity(map.len());
for (key, val) in map.iter() {
result.push((key.to_owned().into(), val.iter().map(|x| x.into()).collect()));
}
result
},
Err(e) => {
log::error!("Failed to load translations for language `{}`: {}", language, e);
vec![]
}
}
}
#[cfg(feature = "translate")]
fn load_locale(lang: &str) -> Result<gettext_ng::Catalog, gettext_ng::Error> {
let path = crate::api::dirs::plugin().unwrap_or_else(|| "".into()).join("translations").join(format!("{}.mo", lang));
let file = std::fs::File::open(path).map_err(|e| gettext_ng::Error::Io(e))?;
gettext_ng::Catalog::parse(file)
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
}

View file

@ -5,42 +5,59 @@
//!
#![warn(missing_docs)]
#[cfg(not(any(feature = "decky", feature = "crankshaft")))]
#[cfg(not(any(feature = "decky")))]
mod api_any;
mod api_common;
#[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))]
mod api_crankshaft;
#[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))]
#[cfg(all(feature = "decky", not(any(feature = "any"))))]
mod api_decky;
mod callable;
//mod errors;
mod instance;
mod rpc;
mod services_impl;
pub use callable::{Callable, MutCallable, AsyncCallable};
pub(crate) use callable::WrappedCallable;
pub use instance::Instance;
//mod errors;
mod websockets;
pub use websockets::WebsocketServer as Server;
//pub use errors::{ServerError, ServerResult};
#[allow(missing_docs)]
#[allow(dead_code)]
pub(crate) mod services {
include!(concat!(env!("OUT_DIR"), "/mod.rs"));
}
/// USDPL backend API.
/// This contains functionality used exclusively by the back-end.
pub mod api {
pub use super::api_common::*;
/// Standard interfaces not specific to a single plugin loader
#[cfg(not(any(feature = "decky", feature = "crankshaft")))]
pub mod any { pub use super::super::api_any::*; }
/// Crankshaft-specific interfaces (FIXME)
#[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))]
pub mod crankshaft { pub use super::super::api_crankshaft::*; }
#[cfg(not(any(feature = "decky")))]
pub mod any {
pub use super::super::api_any::*;
}
/// Decky-specific interfaces
#[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))]
pub mod decky { pub use super::super::api_decky::*; }
#[cfg(all(feature = "decky", not(any(feature = "any"))))]
pub mod decky {
pub use super::super::api_decky::*;
}
}
/// usdpl-core re-export
pub mod core {
pub use usdpl_core::*;
}
/// nrpc re-export
pub mod nrpc {
pub use nrpc::*;
}
/*/// nRPC-generated exports
#[allow(missing_docs)]
#[allow(dead_code)]
pub mod services {
include!(concat!(env!("OUT_DIR"), "/mod.rs"));
}*/

View file

@ -0,0 +1,5 @@
mod registry;
pub use registry::{ServiceRegistry, StaticServiceRegistry};
mod websocket_stream;
pub use websocket_stream::ws_stream;

View file

@ -0,0 +1,43 @@
use async_lock::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
use nrpc::{ServerService, ServiceError, ServiceServerStream};
pub type StaticServiceRegistry = ServiceRegistry<'static>;
#[derive(Default, Clone)]
pub struct ServiceRegistry<'a> {
entries: HashMap<String, Arc<Mutex<Box<dyn ServerService<'a> + Send + 'a>>>>,
}
impl<'a> ServiceRegistry<'a> {
pub async fn call_descriptor<'b: 'a>(
&mut self,
descriptor: &str,
method: &str,
input: ServiceServerStream<'a, bytes::Bytes>,
) -> Result<ServiceServerStream<'a, bytes::Bytes>, ServiceError> {
if let Some(service) = self.entries.get(descriptor) {
let mut service_lock = service.lock_arc().await;
let output = service_lock.call(method, input).await?;
Ok(output.into())
} else {
Err(ServiceError::ServiceNotFound)
}
}
pub fn register<S: ServerService<'a> + Send + 'a>(&mut self, service: S) -> &mut Self {
let key = service.descriptor().to_owned();
self.entries
.insert(key, Arc::new(Mutex::new(Box::new(service))));
self
}
pub fn with_builtins() -> Self {
let mut reg = Self::default();
reg.register(crate::services::usdpl::DevToolsServer::new(crate::services_impl::DevTools{}))
.register(crate::services::usdpl::TranslationsServer::new(crate::services_impl::Translations{}));
reg
}
}

View file

@ -0,0 +1,34 @@
use core::marker::Unpin;
use std::sync::Arc;
use tokio::{net::TcpStream, sync::Mutex};
use ratchet_rs::{WebSocket, Message, Error as RatchetError, Extension};
use nrpc::ServiceError;
use nrpc::_helpers::futures::Stream;
use nrpc::_helpers::bytes::{BytesMut, Bytes};
struct WsStreamState<T: Extension + Unpin>{
ws: Arc<Mutex<WebSocket<TcpStream, T>>>,
buf: BytesMut,
}
pub fn ws_stream<'a, T: Extension + Unpin + 'a>(ws: Arc<Mutex<WebSocket<TcpStream, T>>>) -> impl Stream<Item = Result<Bytes, ServiceError>> + 'a {
nrpc::_helpers::futures::stream::unfold(WsStreamState { ws, buf: BytesMut::new() }, |mut state| async move {
let mut locked_ws = state.ws.lock().await;
if locked_ws.is_closed() || !locked_ws.is_active() {
None
} else {
let result = locked_ws.read(&mut state.buf).await;
drop(locked_ws);
match result {
Ok(Message::Binary) => Some((Ok(state.buf.clone().freeze()), state)),
Ok(_) => Some((Err(ServiceError::Method(Box::new(RatchetError::with_cause(
ratchet_rs::ErrorKind::Protocol,
"Websocket text messages are not accepted",
)))), state)),
Err(e) => Some((Err(ServiceError::Method(Box::new(e))), state))
}
}
})
}

View file

@ -0,0 +1,22 @@
use crate::services::usdpl as generated;
/// Built-in dev tools service implementation
pub(crate) struct DevTools {}
#[async_trait::async_trait]
impl<'a> generated::IDevTools<'a> for DevTools {
async fn log(
&mut self,
input: generated::LogMessage,
) -> Result<generated::Empty, Box<dyn std::error::Error + Send>> {
match input.level {
lvl if lvl == generated::LogLevel::Trace as _ => log::trace!("{}", input.msg),
lvl if lvl == generated::LogLevel::Debug as _ => log::debug!("{}", input.msg),
lvl if lvl == generated::LogLevel::Info as _ => log::info!("{}", input.msg),
lvl if lvl == generated::LogLevel::Warn as _ => log::warn!("{}", input.msg),
lvl if lvl == generated::LogLevel::Error as _ => log::error!("{}", input.msg),
lvl => return Err(Box::<dyn std::error::Error + Send + Sync>::from(format!("Unexpected input log level {}", lvl)))
}
Ok(generated::Empty{ ok: true })
}
}

View file

@ -0,0 +1,5 @@
mod dev_tools;
pub(crate) use dev_tools::DevTools;
mod translations;
pub(crate) use translations::Translations;

View file

@ -0,0 +1,31 @@
use crate::services::usdpl as generated;
/// Built-in translation service implementation
pub(crate) struct Translations {}
#[async_trait::async_trait]
impl<'a> generated::ITranslations<'a> for Translations {
async fn get_language(
&mut self,
input: generated::LanguageRequest,
) -> Result<generated::TranslationsReply, Box<dyn std::error::Error + Send>> {
let catalog = load_locale(&input.lang).map_err(|e| Box::new(e) as _)?;
let catalog_map = catalog.nalltext();
let mut map = std::collections::HashMap::with_capacity(catalog_map.len());
for (key, val) in catalog_map.into_iter() {
if val.len() > 1 {
log::warn!("Translations key {} for language {} has plural entries which aren't currently supported", key, input.lang);
}
if let Some(val_0) = val.get(0) {
map.insert(key.to_owned(), val_0.to_owned());
}
}
Ok(generated::TranslationsReply { translations: map })
}
}
fn load_locale(lang: &str) -> Result<gettext_ng::Catalog, gettext_ng::Error> {
let path = crate::api::dirs::plugin().unwrap_or_else(|| "".into()).join("translations").join(format!("{}.mo", lang));
let file = std::fs::File::open(path).map_err(|e| gettext_ng::Error::Io(e))?;
gettext_ng::Catalog::parse(file)
}

View file

@ -0,0 +1,175 @@
use ratchet_rs::deflate::DeflateExtProvider;
use ratchet_rs::{Error as RatchetError, ProtocolRegistry, WebSocketConfig};
use tokio::net::{TcpListener, TcpStream};
use nrpc::_helpers::futures::StreamExt;
use crate::rpc::StaticServiceRegistry;
struct MethodDescriptor<'a> {
service: &'a str,
method: &'a str,
}
/// Handler for communication to and from the front-end
pub struct WebsocketServer {
services: StaticServiceRegistry,
port: u16,
}
impl WebsocketServer {
/// Initialise an instance of the back-end websocket server
pub fn new(port_usdpl: u16) -> Self {
Self {
services: StaticServiceRegistry::with_builtins(),
port: port_usdpl,
}
}
/// Get the service registry that the server handles
pub fn registry(&mut self) -> &'_ mut StaticServiceRegistry {
&mut self.services
}
/// Register a nRPC service for this server to handle
pub fn register<S: nrpc::ServerService<'static> + Send + 'static>(mut self, service: S) -> Self {
self.services.register(service);
self
}
/// Run the web server forever, asynchronously
pub async fn run(&self) -> std::io::Result<()> {
#[cfg(debug_assertions)]
let addr = (std::net::Ipv4Addr::UNSPECIFIED, self.port);
#[cfg(not(debug_assertions))]
let addr = (std::net::Ipv4Addr::LOCALHOST, self.port);
let tcp = TcpListener::bind(addr).await?;
while let Ok((stream, _addr_do_not_use)) = tcp.accept().await {
tokio::spawn(error_logger("USDPL websocket server error", Self::connection_handler(self.services.clone(), stream)));
}
Ok(())
}
#[cfg(feature = "blocking")]
/// Run the server forever, blocking the current thread
pub fn run_blocking(self) -> std::io::Result<()> {
let runner = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
runner.block_on(self.run())
}
async fn connection_handler(
mut services: StaticServiceRegistry,
stream: TcpStream,
) -> Result<(), RatchetError> {
log::debug!("connection_handler invoked!");
let upgraded = ratchet_rs::accept_with(
stream,
WebSocketConfig::default(),
DeflateExtProvider::default(),
ProtocolRegistry::new(["usdpl-nrpc"])?,
)
.await?
.upgrade()
.await?;
let request_path = upgraded.request.uri().path();
log::debug!("accepted new connection on uri {}", request_path);
let websocket = std::sync::Arc::new(tokio::sync::Mutex::new(upgraded.websocket));
let descriptor = Self::parse_uri_path(request_path)
.map_err(|e| RatchetError::with_cause(ratchet_rs::ErrorKind::Protocol, e))?;
let input_stream = Box::new(nrpc::_helpers::futures::stream::StreamExt::boxed(crate::rpc::ws_stream(websocket.clone())));
let output_stream = services
.call_descriptor(
descriptor.service,
descriptor.method,
input_stream,
)
.await
.map_err(|e| {
RatchetError::with_cause(ratchet_rs::ErrorKind::Protocol, e.to_string())
})?;
output_stream.for_each(|result| async {
match result {
Ok(msg) => {
let mut ws_lock = websocket.lock().await;
if let Err(e) = ws_lock.write_binary(msg).await {
log::error!("websocket error while writing response on uri {}: {}", request_path, e);
}
},
Err(e) => {
log::error!("service error while writing response on uri {}: {}", request_path, e);
}
}
}).await;
websocket.lock().await.close(ratchet_rs::CloseReason {
code: ratchet_rs::CloseCode::Normal,
description: None,
}).await?;
/*let mut buf = BytesMut::new();
loop {
match websocket.read(&mut buf).await? {
Message::Text => {
return Err(RatchetError::with_cause(
ratchet_rs::ErrorKind::Protocol,
"Websocket text messages are not accepted",
))
}
Message::Binary => {
log::debug!("got binary ws message on uri {}", request_path);
let response = services
.call_descriptor(
descriptor.service,
descriptor.method,
buf.clone().freeze(),
)
.await
.map_err(|e| {
RatchetError::with_cause(ratchet_rs::ErrorKind::Protocol, e.to_string())
})?;
log::debug!("service completed response on uri {}", request_path);
websocket.write_binary(response).await?;
}
Message::Ping(x) => websocket.write_pong(x).await?,
Message::Pong(_) => {}
Message::Close(_) => break,
}
}*/
log::debug!("ws connection {} closed", request_path);
Ok(())
}
fn parse_uri_path<'a>(path: &'a str) -> Result<MethodDescriptor<'a>, &'static str> {
let mut iter = path.trim_matches('/').split('/');
if let Some(service) = iter.next() {
if let Some(method) = iter.next() {
if iter.next().is_none() {
return Ok(MethodDescriptor { service, method });
} else {
Err("URL path has too many separators")
}
} else {
Err("URL path has no method")
}
} else {
Err("URL path has no service")
}
}
}
async fn error_logger<E: std::error::Error>(msg: &'static str, f: impl core::future::Future<Output=Result<(), E>>) {
if let Err(e) = f.await {
log::error!("{}: {}", msg, e);
}
}

23
usdpl-build/Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "usdpl-build"
version = "0.11.0"
edition = "2021"
authors = ["NGnius <ngniusness@gmail.com>"]
license = "GPL-3.0-only"
repository = "https://git.ngni.us/NG-SD-Plugins/usdpl-rs"
readme = "../README.md"
description = "Universal Steam Deck Plugin Library core"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nrpc-build = { version = "0.10", path = "../../nRPC/nrpc-build" }
prost-build = "0.11"
prost-types = "0.11"
# code gen
prettyplease = "0.2"
quote = "1.0"
syn = "2.0"
proc-macro2 = "1.0"

View file

@ -0,0 +1,27 @@
syntax = "proto3";
package usdpl;
// The translation service
service DevTools {
// Retrieves all translations for the provided 4-letter code
rpc Log (LogMessage) returns (Empty);
}
enum LogLevel {
Trace = 0;
Debug = 1;
Info = 2;
Warn = 3;
Error = 4;
}
// The request message containing the log message
message LogMessage {
LogLevel level = 1;
string msg = 2;
}
message Empty {
bool ok = 1;
}

View file

@ -0,0 +1,19 @@
syntax = "proto3";
package usdpl;
// The translation service
service Translations {
// Retrieves all translations for the provided 4-letter code
rpc GetLanguage (LanguageRequest) returns (TranslationsReply) {}
}
// The request message containing the language code
message LanguageRequest {
string lang = 1;
}
// The response message containing all translations for the language
message TranslationsReply {
map<string, string> translations = 1;
}

View file

@ -0,0 +1,20 @@
pub fn build(
custom_protos: impl Iterator<Item = String>,
custom_dirs: impl Iterator<Item = String>,
) {
nrpc_build::compile_servers(
custom_protos,
crate::proto_out_paths(custom_dirs),
)
}
pub fn build_with_custom_builtins(
custom_protos: impl Iterator<Item = String>,
custom_dirs: impl Iterator<Item = String>,
) {
crate::dump_protos_out().unwrap();
nrpc_build::compile_servers(
crate::all_proto_filenames(crate::proto_builtins_out_path(), custom_protos),
crate::proto_out_paths(custom_dirs),
)
}

View file

@ -0,0 +1,45 @@
mod preprocessor;
pub use preprocessor::WasmProtoPreprocessor;
mod service_generator;
pub use service_generator::WasmServiceGenerator;
mod shared_state;
pub(crate) use shared_state::SharedState;
pub fn build(
custom_protos: impl Iterator<Item = String>,
custom_dirs: impl Iterator<Item = String>,
) {
let shared_state = SharedState::new();
crate::dump_protos_out().unwrap();
nrpc_build::Transpiler::new(
crate::all_proto_filenames(crate::proto_builtins_out_path(), custom_protos),
crate::proto_out_paths(custom_dirs),
)
.unwrap()
.generate_client()
.with_preprocessor(nrpc_build::AbstractImpl::outer(
WasmProtoPreprocessor::with_state(&shared_state),
))
.with_service_generator(nrpc_build::AbstractImpl::outer(
WasmServiceGenerator::with_state(&shared_state),
))
.transpile()
.unwrap()
}
pub fn build_min(
custom_protos: impl Iterator<Item = String>,
custom_dirs: impl Iterator<Item = String>,
) {
crate::dump_protos_out().unwrap();
nrpc_build::Transpiler::new(
crate::all_proto_filenames(crate::proto_builtins_out_path(), custom_protos),
crate::proto_out_paths(custom_dirs),
)
.unwrap()
.generate_client()
.transpile()
.unwrap()
}

View file

@ -0,0 +1,24 @@
use nrpc_build::IPreprocessor;
//use prost_build::{Service, ServiceGenerator};
use prost_types::FileDescriptorSet;
use super::SharedState;
pub struct WasmProtoPreprocessor {
shared: SharedState,
}
impl WasmProtoPreprocessor {
pub fn with_state(state: &SharedState) -> Self {
Self {
shared: state.clone(),
}
}
}
impl IPreprocessor for WasmProtoPreprocessor {
fn process(&mut self, fds: &mut FileDescriptorSet) -> proc_macro2::TokenStream {
self.shared.lock().expect("Cannot lock shared state").fds = Some(fds.clone());
quote::quote! {}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
use std::sync::{Arc, Mutex};
use prost_types::FileDescriptorSet;
#[derive(Clone)]
pub struct SharedState(Arc<Mutex<SharedProtoData>>);
impl SharedState {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(SharedProtoData { fds: None })))
}
}
impl std::ops::Deref for SharedState {
type Target = Arc<Mutex<SharedProtoData>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct SharedProtoData {
pub fds: Option<FileDescriptorSet>,
}

7
usdpl-build/src/lib.rs Normal file
View file

@ -0,0 +1,7 @@
pub mod back;
pub mod front;
mod proto_files;
pub use proto_files::{
all_proto_filenames, dump_protos, dump_protos_out, proto_builtins_out_path, proto_out_paths,
};

View file

@ -0,0 +1,86 @@
use std::path::{Path, PathBuf};
struct IncludedFileStr<'a> {
filename: &'a str,
contents: &'a str,
}
const ADDITIONAL_PROTOBUFS_ENV_VAR: &'static str = "USDPL_PROTOS_PATH";
const DEBUG_PROTO: IncludedFileStr<'static> = IncludedFileStr {
filename: "debug.proto",
contents: include_str!("../protos/debug.proto"),
};
const TRANSLATIONS_PROTO: IncludedFileStr<'static> = IncludedFileStr {
filename: "translations.proto",
contents: include_str!("../protos/translations.proto"),
};
const ALL_PROTOS: [IncludedFileStr<'static>; 2] = [DEBUG_PROTO, TRANSLATIONS_PROTO];
pub fn proto_builtins_out_path() -> PathBuf {
PathBuf::from(std::env::var("OUT_DIR").expect("Not in a build.rs context (missing $OUT_DIR)"))
.join("protos")
}
pub fn proto_out_paths(additionals: impl Iterator<Item = String>) -> impl Iterator<Item = String> {
std::iter::once(proto_builtins_out_path())
.map(|x| x.to_str().unwrap().to_owned())
.chain(custom_protos_dirs(additionals).into_iter())
}
fn custom_protos_dirs(additionals: impl Iterator<Item = String>) -> Vec<String> {
let dirs = std::env::var(ADDITIONAL_PROTOBUFS_ENV_VAR).unwrap_or_else(|_| "".to_owned());
dirs.split(':')
.filter(|x| std::fs::read_dir(x).is_ok())
.map(|x| x.to_owned())
.chain(additionals)
.collect()
}
fn custom_protos_filenames() -> Vec<String> {
let dirs = std::env::var(ADDITIONAL_PROTOBUFS_ENV_VAR).unwrap_or_else(|_| "".to_owned());
dirs.split(':')
.map(std::fs::read_dir)
.filter(|x| x.is_ok())
.flat_map(|x| x.unwrap())
.filter(|x| x.is_ok())
.map(|x| x.unwrap().path())
.filter(|x| {
if let Some(ext) = x.extension() {
ext.to_ascii_lowercase() == "proto" && x.is_file()
} else {
false
}
})
.filter_map(|x| x.to_str().map(|x| x.to_owned()))
.collect()
}
pub fn all_proto_filenames(
p: impl AsRef<Path> + 'static,
additionals: impl Iterator<Item = String>,
) -> impl Iterator<Item = String> {
//let p = p.as_ref();
ALL_PROTOS
.iter()
.map(move |x| p.as_ref().join(x.filename).to_str().unwrap().to_owned())
.chain(custom_protos_filenames())
.chain(additionals)
}
pub fn dump_protos(p: impl AsRef<Path>) -> std::io::Result<()> {
let p = p.as_ref();
for f in ALL_PROTOS {
let fullpath = p.join(f.filename);
std::fs::write(fullpath, f.contents)?;
}
Ok(())
}
pub fn dump_protos_out() -> std::io::Result<()> {
let path = proto_builtins_out_path();
std::fs::create_dir_all(&path)?;
dump_protos(&path)
}

View file

@ -1,22 +1,22 @@
[package]
name = "usdpl-core"
version = "0.10.0"
version = "0.11.0"
authors = ["NGnius <ngniusness@gmail.com>"]
edition = "2021"
license = "GPL-3.0-only"
repository = "https://github.com/NGnius/usdpl-rs"
readme = "README.md"
description = "Universal Steam Deck Plugin Library core"
repository = "https://git.ngni.us/NG-SD-Plugins/usdpl-rs"
readme = "../README.md"
description = "Universal Steam Deck Plugin Library core designed for all architectures"
[features]
default = []
decky = []
crankshaft = []
encrypt = ["aes-gcm-siv"]
translate = []
[dependencies]
base64 = "0.13"
aes-gcm-siv = { version = "0.10", optional = true, default-features = false, features = ["alloc", "aes"] }
# nrpc = "0.2"
[dev-dependencies]
hex-literal = "0.3.4"

View file

@ -4,8 +4,6 @@ pub enum Platform {
Any,
/// Decky aka PluginLoader platform
Decky,
/// Crankshaft platform
Crankshaft,
}
impl Platform {
@ -16,10 +14,6 @@ impl Platform {
{
Self::Decky
}
#[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))]
{
Self::Crankshaft
}
#[cfg(not(any(feature = "decky", feature = "crankshaft")))]
{
Self::Any
@ -32,7 +26,6 @@ impl std::fmt::Display for Platform {
match self {
Self::Any => write!(f, "any"),
Self::Decky => write!(f, "decky"),
Self::Crankshaft => write!(f, "crankshaft"),
}
}
}

View file

@ -1 +0,0 @@

View file

@ -2,29 +2,18 @@
//! This contains serialization functionality and networking datatypes.
#![warn(missing_docs)]
mod remote_call;
#[cfg(not(any(feature = "decky", feature = "crankshaft")))]
#[cfg(not(any(feature = "decky")))]
mod api_any;
mod api_common;
#[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))]
mod api_crankshaft;
#[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))]
#[cfg(all(feature = "decky", not(any(feature = "any"))))]
mod api_decky;
pub mod serdes;
pub mod socket;
pub use remote_call::{RemoteCall, RemoteCallResponse};
/// USDPL core API.
/// This contains functionality used in both the back-end and front-end.
pub mod api {
#[cfg(not(any(feature = "decky", feature = "crankshaft")))]
#[cfg(not(any(feature = "decky")))]
pub use super::api_any::*;
pub use super::api_common::*;
#[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))]
pub use super::api_crankshaft::*;
#[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))]
#[cfg(all(feature = "decky", not(any(feature = "any"))))]
pub use super::api_decky::*;
}

View file

@ -1,111 +0,0 @@
use std::io::{Read, Write};
use crate::serdes::{DumpError, Dumpable, LoadError, Loadable, Primitive};
/// Remote call packet representing a function to call on the back-end, sent from the front-end
pub struct RemoteCall {
/// The call id assigned by the front-end
pub id: u64,
/// The function's name
pub function: String,
/// The function's input parameters
pub parameters: Vec<Primitive>,
}
impl Loadable for RemoteCall {
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let (id_num, len0) = u64::load(buffer)?;
let (function_name, len1) = String::load(buffer)?;
let (params, len2) = Vec::<Primitive>::load(buffer)?;
Ok((
Self {
id: id_num,
function: function_name,
parameters: params,
},
len0 + len1 + len2,
))
}
}
impl Dumpable for RemoteCall {
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError> {
let len0 = self.id.dump(buffer)?;
let len1 = self.function.dump(buffer)?;
let len2 = self.parameters.dump(buffer)?;
Ok(len0 + len1 + len2)
}
}
/// Remote call response packet representing the response from a remote call after the back-end has executed it.
pub struct RemoteCallResponse {
/// The call id from the RemoteCall
pub id: u64,
/// The function's result
pub response: Vec<Primitive>,
}
impl Loadable for RemoteCallResponse {
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let (id_num, len0) = u64::load(buffer)?;
let (response_var, len1) = Vec::<Primitive>::load(buffer)?;
Ok((
Self {
id: id_num,
response: response_var,
},
len0 + len1,
))
}
}
impl Dumpable for RemoteCallResponse {
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError> {
let len0 = self.id.dump(buffer)?;
let len1 = self.response.dump(buffer)?;
Ok(len0 + len1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn remote_call_idempotence_test() {
let call = RemoteCall{
id: 42,
function: "something very long just in case this causes unexpected issues".into(),
parameters: vec!["param1".into(), 42f64.into()],
};
let mut buffer = String::with_capacity(crate::socket::PACKET_BUFFER_SIZE);
let len = call.dump_base64(&mut buffer).unwrap();
println!("base64 dumped: `{}` (len: {})", buffer, len);
let (loaded_call, loaded_len) = RemoteCall::load_base64(buffer.as_bytes()).unwrap();
assert_eq!(len, loaded_len, "Expected load and dump lengths to match");
assert_eq!(loaded_call.id, call.id, "RemoteCall.id does not match");
assert_eq!(loaded_call.function, call.function, "RemoteCall.function does not match");
if let Primitive::String(loaded) = &loaded_call.parameters[0] {
if let Primitive::String(original) = &call.parameters[0] {
assert_eq!(loaded, original, "RemoteCall.parameters[0] does not match");
} else {
panic!("Original call parameter 0 is not String")
}
} else {
panic!("Loaded call parameter 0 is not String")
}
if let Primitive::F64(loaded) = &loaded_call.parameters[1] {
if let Primitive::F64(original) = &call.parameters[1] {
assert_eq!(loaded, original, "RemoteCall.parameters[1] does not match");
} else {
panic!("Original call parameter 1 is not f64")
}
} else {
panic!("Loaded call parameter 1 is not f64")
}
}
}

View file

@ -1,166 +0,0 @@
use std::io::Write;
use super::{DumpError, Dumpable};
impl Dumpable for String {
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError> {
let str_bytes = self.as_bytes();
let len_bytes = (str_bytes.len() as u32).to_le_bytes();
let size1 = buffer.write(&len_bytes).map_err(DumpError::Io)?;
let size2 = buffer.write(&str_bytes).map_err(DumpError::Io)?;
Ok(size1 + size2)
}
}
impl<T: Dumpable> Dumpable for Vec<T> {
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError> {
let len_bytes = (self.len() as u32).to_le_bytes();
let mut total = buffer.write(&len_bytes).map_err(DumpError::Io)?;
for obj in self.iter() {
let len = obj.dump(buffer)?;
total += len;
}
Ok(total)
}
}
impl<T0: Dumpable, T1: Dumpable> Dumpable for (T0, T1) {
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError> {
Ok(
self.0.dump(buffer)?
+ self.1.dump(buffer)?
)
}
}
impl<T0: Dumpable, T1: Dumpable, T2: Dumpable> Dumpable for (T0, T1, T2) {
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError> {
Ok(
self.0.dump(buffer)?
+ self.1.dump(buffer)?
+ self.2.dump(buffer)?
)
}
}
impl<T0: Dumpable, T1: Dumpable, T2: Dumpable, T3: Dumpable> Dumpable for (T0, T1, T2, T3) {
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError> {
Ok(
self.0.dump(buffer)?
+ self.1.dump(buffer)?
+ self.2.dump(buffer)?
+ self.3.dump(buffer)?
)
}
}
impl<T0: Dumpable, T1: Dumpable, T2: Dumpable, T3: Dumpable, T4: Dumpable> Dumpable for (T0, T1, T2, T3, T4) {
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError> {
Ok(
self.0.dump(buffer)?
+ self.1.dump(buffer)?
+ self.2.dump(buffer)?
+ self.3.dump(buffer)?
+ self.4.dump(buffer)?
)
}
}
impl Dumpable for bool {
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError> {
buffer.write(&[*self as u8]).map_err(DumpError::Io)
}
}
impl Dumpable for u8 {
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError> {
buffer.write(&[*self]).map_err(DumpError::Io)
}
}
/*impl Dumpable for i8 {
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError> {
buffer.write(&self.to_le_bytes()).map_err(DumpError::Io)
}
}*/
macro_rules! int_impl {
($type:ty) => {
impl Dumpable for $type {
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError> {
buffer.write(&self.to_le_bytes()).map_err(DumpError::Io)
}
}
};
}
int_impl! {u16}
int_impl! {u32}
int_impl! {u64}
int_impl! {u128}
int_impl! {i8}
int_impl! {i16}
int_impl! {i32}
int_impl! {i64}
int_impl! {i128}
int_impl! {f32}
int_impl! {f64}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test_impl {
($fn_name:ident, $data:expr, $expected_len:literal, $expected_dump:expr) => {
#[test]
fn $fn_name() {
let data = $data;
let mut buffer = Vec::with_capacity(128);
let write_len = data.dump(&mut buffer).expect("Dump not ok");
assert_eq!(write_len, $expected_len, "Wrong amount written");
assert_eq!(&buffer[..write_len], $expected_dump);
println!("Dumped {:?}", buffer.as_slice());
}
};
}
test_impl! {string_dump_test, "test".to_string(), 8, &[4, 0, 0, 0, 116, 101, 115, 116]}
test_impl! {
vec_dump_test,
vec![
"".to_string(),
"test1".to_string(),
"test2".to_string()
],
26,
&[3, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 49, 5, 0, 0, 0, 116, 101, 115, 116, 50]
}
test_impl! {tuple2_dump_test, (0u8, 1u8), 2, &[0, 1]}
test_impl! {tuple3_dump_test, (0u8, 1u8, 2u8), 3, &[0, 1, 2]}
test_impl! {tuple4_dump_test, (0u8, 1u8, 2u8, 3u8), 4, &[0, 1, 2, 3]}
test_impl! {tuple5_dump_test, (0u8, 1u8, 2u8, 3u8, 4u8), 5, &[0, 1, 2, 3, 4]}
test_impl! {bool_true_dump_test, true, 1, &[1]}
test_impl! {bool_false_dump_test, false, 1, &[0]}
// testing macro-generated code isn't particularly useful, but do it anyway
test_impl! {u8_dump_test, 42u8, 1, &[42]}
test_impl! {u16_dump_test, 42u16, 2, &[42, 0]}
test_impl! {u32_dump_test, 42u32, 4, &[42, 0, 0, 0]}
test_impl! {u64_dump_test, 42u64, 8, &[42, 0, 0, 0, 0, 0, 0, 0]}
test_impl! {u128_dump_test, 42u128, 16, &[42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}
test_impl! {i8_dump_test, 42i8, 1, &[42]}
test_impl! {i16_dump_test, 42i16, 2, &[42, 0]}
test_impl! {i32_dump_test, 42i32, 4, &[42, 0, 0, 0]}
test_impl! {i64_dump_test, 42i64, 8, &[42, 0, 0, 0, 0, 0, 0, 0]}
test_impl! {i128_dump_test, 42i128, 16, &[42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}
test_impl! {f32_dump_test, 42f32, 4, &[0, 0, 40, 66]}
test_impl! {f64_dump_test, 42f64, 8, &[0, 0, 0, 0, 0, 0, 69, 64]}
}

View file

@ -1,195 +0,0 @@
use std::io::Read;
use super::{LoadError, Loadable};
impl Loadable for String {
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let mut u32_bytes: [u8; 4] = [u8::MAX; 4];
buffer.read_exact(&mut u32_bytes).map_err(LoadError::Io)?;
let str_size = u32::from_le_bytes(u32_bytes) as usize;
//let mut str_buf = String::with_capacity(str_size);
let mut str_buf = Vec::with_capacity(str_size);
let mut byte_buf = [u8::MAX; 1];
for _ in 0..str_size {
buffer.read_exact(&mut byte_buf).map_err(LoadError::Io)?;
str_buf.push(byte_buf[0]);
}
//let size2 = buffer.read_to_string(&mut str_buf).map_err(LoadError::Io)?;
Ok((
String::from_utf8(str_buf).map_err(|_| LoadError::InvalidData)?,
str_size + 4,
))
}
}
impl<T: Loadable> Loadable for Vec<T> {
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let mut u32_bytes: [u8; 4] = [u8::MAX; 4];
buffer.read_exact(&mut u32_bytes).map_err(LoadError::Io)?;
let count = u32::from_le_bytes(u32_bytes) as usize;
let mut cursor = 4;
let mut items = Vec::with_capacity(count);
for _ in 0..count {
let (obj, len) = T::load(buffer)?;
cursor += len;
items.push(obj);
}
Ok((items, cursor))
}
}
impl<T0: Loadable, T1: Loadable> Loadable for (T0, T1) {
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let (t0, len0) = T0::load(buffer)?;
let (t1, len1) = T1::load(buffer)?;
Ok((
(t0, t1),
len0 + len1
))
}
}
impl<T0: Loadable, T1: Loadable, T2: Loadable> Loadable for (T0, T1, T2) {
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let (t0, len0) = T0::load(buffer)?;
let (t1, len1) = T1::load(buffer)?;
let (t2, len2) = T2::load(buffer)?;
Ok((
(t0, t1, t2),
len0 + len1 + len2
))
}
}
impl<T0: Loadable, T1: Loadable, T2: Loadable, T3: Loadable> Loadable for (T0, T1, T2, T3) {
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let (t0, len0) = T0::load(buffer)?;
let (t1, len1) = T1::load(buffer)?;
let (t2, len2) = T2::load(buffer)?;
let (t3, len3) = T3::load(buffer)?;
Ok((
(t0, t1, t2, t3),
len0 + len1 + len2 + len3
))
}
}
impl<T0: Loadable, T1: Loadable, T2: Loadable, T3: Loadable, T4: Loadable> Loadable for (T0, T1, T2, T3, T4) {
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let (t0, len0) = T0::load(buffer)?;
let (t1, len1) = T1::load(buffer)?;
let (t2, len2) = T2::load(buffer)?;
let (t3, len3) = T3::load(buffer)?;
let (t4, len4) = T4::load(buffer)?;
Ok((
(t0, t1, t2, t3, t4),
len0 + len1 + len2 + len3 + len4
))
}
}
impl Loadable for bool {
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let mut byte = [u8::MAX; 1];
buffer.read_exact(&mut byte).map_err(LoadError::Io)?;
Ok((byte[0] != 0, 1))
}
}
impl Loadable for u8 {
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let mut byte = [u8::MAX; 1];
buffer.read_exact(&mut byte).map_err(LoadError::Io)?;
Ok((byte[0], 1))
}
}
impl Loadable for i8 {
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let mut byte = [u8::MAX; 1];
buffer.read_exact(&mut byte).map_err(LoadError::Io)?;
Ok((i8::from_le_bytes(byte), 1))
}
}
macro_rules! int_impl {
($type:ty, $size:literal) => {
impl Loadable for $type {
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let mut bytes: [u8; $size] = [u8::MAX; $size];
buffer.read_exact(&mut bytes).map_err(LoadError::Io)?;
let i = <$type>::from_le_bytes(bytes);
Ok((i, $size))
}
}
};
}
int_impl! {u16, 2}
int_impl! {u32, 4}
int_impl! {u64, 8}
int_impl! {u128, 16}
int_impl! {i16, 2}
int_impl! {i32, 4}
int_impl! {i64, 8}
int_impl! {i128, 16}
int_impl! {f32, 4}
int_impl! {f64, 8}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
macro_rules! test_impl {
($fn_name:ident, $data:expr, $type:ty, $expected_len:literal, $expected_load:expr) => {
#[test]
fn $fn_name() {
let buffer_data = $data;
let mut buffer = Vec::with_capacity(buffer_data.len());
buffer.extend_from_slice(&buffer_data);
let (obj, read_len) = <$type>::load(&mut Cursor::new(buffer)).expect("Load not ok");
assert_eq!(read_len, $expected_len, "Wrong amount read");
assert_eq!(obj, $expected_load, "Loaded value not as expected");
println!("Loaded {:?}", obj);
}
};
}
test_impl! {string_load_test, [4u8, 0, 0, 0, 116, 101, 115, 116, 0, 128], String, 8, "test"}
test_impl! {
vec_load_test,
[3u8, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 49, 5, 0, 0, 0, 116, 101, 115, 116, 50],
Vec<String>,
26,
vec![
"".to_string(),
"test1".to_string(),
"test2".to_string()
]
}
test_impl! {tuple2_load_test, [0, 1], (u8, u8), 2, (0, 1)}
test_impl! {bool_true_load_test, [1], bool, 1, true}
test_impl! {bool_false_load_test, [0], bool, 1, false}
// testing macro-generated code isn't particularly useful, but do it anyway
test_impl! {u8_load_test, [42], u8, 1, 42u8}
test_impl! {u16_load_test, [42, 0], u16, 2, 42u16}
test_impl! {u32_load_test, [42, 0, 0, 0], u32, 4, 42u32}
test_impl! {u64_load_test, [42, 0, 0, 0, 0, 0, 0, 0], u64, 8, 42u64}
test_impl! {u128_load_test, [42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], u128, 16, 42u128}
test_impl! {i8_load_test, [42], i8, 1, 42i8}
test_impl! {i16_load_test, [42, 0], i16, 2, 42i16}
test_impl! {i32_load_test, [42, 0, 0, 0], i32, 4, 42i32}
test_impl! {i64_load_test, [42, 0, 0, 0, 0, 0, 0, 0], i64, 8, 42i64}
test_impl! {i128_load_test, [42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], i128, 16, 42i128}
test_impl! {f32_load_test, [0, 0, 40, 66], f32, 4, 42f32}
test_impl! {f64_load_test, [0, 0, 0, 0, 0, 0, 69, 64], f64, 8, 42f64}
}

View file

@ -1,10 +0,0 @@
//! Serialization and deserialization functionality.
//! Little endian is preferred.
mod dump_impl;
mod load_impl;
mod primitive;
mod traits;
pub use primitive::Primitive;
pub use traits::{DumpError, Dumpable, LoadError, Loadable};

View file

@ -1,162 +0,0 @@
use std::io::{Read, Write};
use super::{DumpError, Dumpable, LoadError, Loadable};
/// Primitive types supported for communication between the USDPL back- and front-end.
/// These are used for sending over the TCP connection.
pub enum Primitive {
/// Null or unsupported object
Empty,
/// String-like
String(String),
/// f32
F32(f32),
/// f64
F64(f64),
/// u32
U32(u32),
/// u64
U64(u64),
/// i32
I32(i32),
/// i64
I64(i64),
/// boolean
Bool(bool),
/// Non-primitive in Json format
Json(String),
}
impl Primitive {
/// Discriminant -- first byte of a dumped primitive
const fn discriminant(&self) -> u8 {
match self {
Self::Empty => 1,
Self::String(_) => 2,
Self::F32(_) => 3,
Self::F64(_) => 4,
Self::U32(_) => 5,
Self::U64(_) => 6,
Self::I32(_) => 7,
Self::I64(_) => 8,
Self::Bool(_) => 9,
Self::Json(_) => 10,
}
}
}
impl Loadable for Primitive {
fn load(buf: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let mut discriminant_buf = [u8::MAX; 1];
buf.read_exact(&mut discriminant_buf).map_err(LoadError::Io)?;
let mut result: (Self, usize) = match discriminant_buf[0] {
//0 => (None, 0),
1 => (Self::Empty, 0),
2 => String::load(buf).map(|(obj, len)| (Self::String(obj), len))?,
3 => f32::load(buf).map(|(obj, len)| (Self::F32(obj), len))?,
4 => f64::load(buf).map(|(obj, len)| (Self::F64(obj), len))?,
5 => u32::load(buf).map(|(obj, len)| (Self::U32(obj), len))?,
6 => u64::load(buf).map(|(obj, len)| (Self::U64(obj), len))?,
7 => i32::load(buf).map(|(obj, len)| (Self::I32(obj), len))?,
8 => i64::load(buf).map(|(obj, len)| (Self::I64(obj), len))?,
9 => bool::load(buf).map(|(obj, len)| (Self::Bool(obj), len))?,
10 => String::load(buf).map(|(obj, len)| (Self::Json(obj), len))?,
_ => return Err(LoadError::InvalidData),
};
result.1 += 1;
Ok(result)
}
}
impl Dumpable for Primitive {
fn dump(&self, buf: &mut dyn Write) -> Result<usize, DumpError> {
let size1 = buf.write(&[self.discriminant()]).map_err(DumpError::Io)?;
let result = match self {
Self::Empty => Ok(0),
Self::String(s) => s.dump(buf),
Self::F32(x) => x.dump(buf),
Self::F64(x) => x.dump(buf),
Self::U32(x) => x.dump(buf),
Self::U64(x) => x.dump(buf),
Self::I32(x) => x.dump(buf),
Self::I64(x) => x.dump(buf),
Self::Bool(x) => x.dump(buf),
Self::Json(x) => x.dump(buf),
}?;
Ok(size1 + result)
}
}
impl std::convert::Into<Primitive> for &str {
fn into(self) -> Primitive {
Primitive::String(self.to_string())
}
}
impl std::convert::Into<Primitive> for () {
fn into(self) -> Primitive {
Primitive::Empty
}
}
macro_rules! into_impl {
($type:ty, $variant:ident) => {
impl std::convert::Into<Primitive> for $type {
fn into(self) -> Primitive {
Primitive::$variant(self)
}
}
}
}
into_impl! {String, String}
into_impl! {bool, Bool}
into_impl! {u32, U32}
into_impl! {u64, U64}
into_impl! {i32, I32}
into_impl! {i64, I64}
into_impl! {f32, F32}
into_impl! {f64, F64}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn string_idempotence_test() {
let data = "Test";
let primitive = Primitive::String(data.to_string());
let mut buffer = Vec::with_capacity(128);
let write_len = primitive.dump(&mut buffer).expect("Dump not ok");
let (obj, read_len) = Primitive::load(&mut Cursor::new(buffer)).expect("Load not ok");
assert_eq!(
write_len, read_len,
"Amount written and amount read do not match"
);
if let Primitive::String(result) = obj {
assert_eq!(data, result, "Data written and read does not match");
} else {
panic!("Read non-string primitive");
}
}
#[test]
fn empty_idempotence_test() {
let primitive = Primitive::Empty;
let mut buffer = Vec::with_capacity(128);
let write_len = primitive.dump(&mut buffer).expect("Dump not ok");
let (obj, read_len) = Primitive::load(&mut Cursor::new(buffer)).expect("Load not ok");
assert_eq!(
write_len, read_len,
"Amount written and amount read do not match"
);
if let Primitive::Empty = obj {
//assert_eq!(data, result, "Data written and read does not match");
} else {
panic!("Read non-string primitive");
}
}
}

View file

@ -1,142 +0,0 @@
use std::io::{Read, Write, Cursor};
use base64::{decode_config_buf, encode_config_buf, Config};
const B64_CONF: Config = Config::new(base64::CharacterSet::Standard, true);
#[cfg(feature = "encrypt")]
const ASSOCIATED_DATA: &[u8] = b"usdpl-core-data";
#[cfg(feature = "encrypt")]
use aes_gcm_siv::aead::{AeadInPlace, NewAead};
/// Errors from Loadable::load
#[derive(Debug)]
pub enum LoadError {
/// Buffer smaller than expected
TooSmallBuffer,
/// Unexpected/corrupted data encountered
InvalidData,
/// Encrypted data cannot be decrypted
#[cfg(feature = "encrypt")]
DecryptionError,
/// Read error
Io(std::io::Error),
/// Unimplemented
#[cfg(debug_assertions)]
Todo,
}
impl std::fmt::Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::TooSmallBuffer => write!(f, "LoadError: TooSmallBuffer"),
Self::InvalidData => write!(f, "LoadError: InvalidData"),
#[cfg(feature = "encrypt")]
Self::DecryptionError => write!(f, "LoadError: DecryptionError"),
Self::Io(err) => write!(f, "LoadError: Io({})", err),
#[cfg(debug_assertions)]
Self::Todo => write!(f, "LoadError: TODO!"),
}
}
}
/// Load an object from the buffer
pub trait Loadable: Sized {
/// Read the buffer, building the object and returning the amount of bytes read.
/// If anything is wrong with the buffer, Err should be returned.
fn load(buffer: &mut dyn Read) -> Result<(Self, usize), LoadError>;
/// Load data from a base64-encoded buffer
fn load_base64(buffer: &[u8]) -> Result<(Self, usize), LoadError> {
let mut buffer2 = Vec::with_capacity(crate::socket::PACKET_BUFFER_SIZE);
decode_config_buf(buffer, B64_CONF, &mut buffer2)
.map_err(|_| LoadError::InvalidData)?;
let mut cursor = Cursor::new(buffer2);
Self::load(&mut cursor)
}
/// Load data from an encrypted base64-encoded buffer
#[cfg(feature = "encrypt")]
fn load_encrypted(buffer: &[u8], key: &[u8], nonce: &[u8]) -> Result<(Self, usize), LoadError> {
//println!("encrypted buffer: {}", String::from_utf8(buffer.to_vec()).unwrap());
let key = aes_gcm_siv::Key::from_slice(key);
let cipher = aes_gcm_siv::Aes256GcmSiv::new(key);
let nonce = aes_gcm_siv::Nonce::from_slice(nonce);
let mut decoded_buf = Vec::with_capacity(crate::socket::PACKET_BUFFER_SIZE);
base64::decode_config_buf(buffer, B64_CONF, &mut decoded_buf)
.map_err(|_| LoadError::InvalidData)?;
//println!("Decoded buf: {:?}", decoded_buf);
cipher.decrypt_in_place(nonce, ASSOCIATED_DATA, &mut decoded_buf).map_err(|_| LoadError::DecryptionError)?;
//println!("Decrypted buf: {:?}", decoded_buf);
let mut cursor = Cursor::new(decoded_buf);
Self::load(&mut cursor)
}
}
/// Errors from Dumpable::dump
#[derive(Debug)]
pub enum DumpError {
/// Buffer not big enough to dump data into
TooSmallBuffer,
/// Data cannot be dumped
Unsupported,
/// Data cannot be encrypted
#[cfg(feature = "encrypt")]
EncryptionError,
/// Write error
Io(std::io::Error),
/// Unimplemented
#[cfg(debug_assertions)]
Todo,
}
impl std::fmt::Display for DumpError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::TooSmallBuffer => write!(f, "DumpError: TooSmallBuffer"),
Self::Unsupported => write!(f, "DumpError: Unsupported"),
#[cfg(feature = "encrypt")]
Self::EncryptionError => write!(f, "DumpError: EncryptionError"),
Self::Io(err) => write!(f, "DumpError: Io({})", err),
#[cfg(debug_assertions)]
Self::Todo => write!(f, "DumpError: TODO!"),
}
}
}
/// Dump an object into the buffer
pub trait Dumpable {
/// Write the object to the buffer, returning the amount of bytes written.
/// If anything is wrong, false should be returned.
fn dump(&self, buffer: &mut dyn Write) -> Result<usize, DumpError>;
/// Dump data as base64-encoded.
/// Useful for transmitting data as text.
fn dump_base64(&self, buffer: &mut String) -> Result<usize, DumpError> {
let mut buffer2 = Vec::with_capacity(crate::socket::PACKET_BUFFER_SIZE);
let len = self.dump(&mut buffer2)?;
encode_config_buf(&buffer2[..len], B64_CONF, buffer);
Ok(len)
}
/// Dump data as an encrypted base64-encoded buffer
#[cfg(feature = "encrypt")]
fn dump_encrypted(&self, buffer: &mut Vec<u8>, key: &[u8], nonce: &[u8]) -> Result<usize, DumpError> {
let mut buffer2 = Vec::with_capacity(crate::socket::PACKET_BUFFER_SIZE);
let size = self.dump(&mut buffer2)?;
buffer2.truncate(size);
//println!("Buf: {:?}", buffer2);
let key = aes_gcm_siv::Key::from_slice(key);
let cipher = aes_gcm_siv::Aes256GcmSiv::new(key);
let nonce = aes_gcm_siv::Nonce::from_slice(nonce);
cipher.encrypt_in_place(nonce, ASSOCIATED_DATA, &mut buffer2).map_err(|_| DumpError::EncryptionError)?;
//println!("Encrypted slice: {:?}", &buffer2);
let mut base64_buf = String::with_capacity(crate::socket::PACKET_BUFFER_SIZE);
encode_config_buf(buffer2.as_slice(), B64_CONF, &mut base64_buf);
//println!("base64 len: {}", base64_buf.as_bytes().len());
buffer.extend_from_slice(base64_buf.as_bytes());
//let string = String::from_utf8(buffer.as_slice().to_vec()).unwrap();
//println!("Encoded slice: {}", string);
Ok(base64_buf.len())
}
}

View file

@ -1,166 +0,0 @@
//! Web messaging
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::io::{Read, Write};
use crate::serdes::{DumpError, Dumpable, LoadError, Loadable};
use crate::{RemoteCall, RemoteCallResponse};
/// Host IP address for web browsers
pub const HOST_STR: &str = "localhost";
/// Host IP address
pub const HOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
/// Standard max packet size
pub const PACKET_BUFFER_SIZE: usize = 1024;
/// Encryption nonce size
pub const NONCE_SIZE: usize = 12;
/// Address and port
#[inline]
pub fn socket_addr(port: u16) -> SocketAddr {
SocketAddr::V4(SocketAddrV4::new(HOST, port))
}
/// Accepted Packet types and the data they contain
pub enum Packet {
/// A remote call
Call(RemoteCall),
/// A reponse to a remote call
CallResponse(RemoteCallResponse),
/// Unused
KeepAlive,
/// Invalid
Invalid,
/// General message
Message(String),
/// Response to an unsupported packet
Unsupported,
/// Broken packet type, useful for testing
Bad,
/// Many packets merged into one
Many(Vec<Packet>),
/// Translation data dump
#[cfg(feature = "translate")]
Translations(Vec<(String, Vec<String>)>),
/// Request translations for language
#[cfg(feature = "translate")]
Language(String),
}
impl Packet {
/// Byte representing the packet type -- the first byte of any packet in USDPL
const fn discriminant(&self) -> u8 {
match self {
Self::Call(_) => 1,
Self::CallResponse(_) => 2,
Self::KeepAlive => 3,
Self::Invalid => 4,
Self::Message(_) => 5,
Self::Unsupported => 6,
Self::Bad => 7,
Self::Many(_) => 8,
#[cfg(feature = "translate")]
Self::Translations(_) => 9,
#[cfg(feature = "translate")]
Self::Language(_) => 10,
}
}
}
impl Loadable for Packet {
fn load(buf: &mut dyn Read) -> Result<(Self, usize), LoadError> {
let mut discriminant_buf = [u8::MAX; 1];
buf.read_exact(&mut discriminant_buf).map_err(LoadError::Io)?;
let mut result: (Self, usize) = match discriminant_buf[0] {
//0 => (None, 0),
1 => {
let (obj, len) = RemoteCall::load(buf)?;
(Self::Call(obj), len)
}
2 => {
let (obj, len) = RemoteCallResponse::load(buf)?;
(Self::CallResponse(obj), len)
}
3 => (Self::KeepAlive, 0),
4 => (Self::Invalid, 0),
5 => {
let (obj, len) = String::load(buf)?;
(Self::Message(obj), len)
}
6 => (Self::Unsupported, 0),
7 => return Err(LoadError::InvalidData),
8 => {
let (obj, len) = <_>::load(buf)?;
(Self::Many(obj), len)
},
#[cfg(feature = "translate")]
9 => {
let (obj, len) = <_>::load(buf)?;
(Self::Translations(obj), len)
},
#[cfg(feature = "translate")]
10 => {
let (obj, len) = <_>::load(buf)?;
(Self::Language(obj), len)
},
_ => return Err(LoadError::InvalidData),
};
result.1 += 1;
Ok(result)
}
}
impl Dumpable for Packet {
fn dump(&self, buf: &mut dyn Write) -> Result<usize, DumpError> {
let size1 = buf.write(&[self.discriminant()]).map_err(DumpError::Io)?;
let result = match self {
Self::Call(c) => c.dump(buf),
Self::CallResponse(c) => c.dump(buf),
Self::KeepAlive => Ok(0),
Self::Invalid => Ok(0),
Self::Message(s) => s.dump(buf),
Self::Unsupported => Ok(0),
Self::Bad => return Err(DumpError::Unsupported),
Self::Many(v) => v.dump(buf),
#[cfg(feature = "translate")]
Self::Translations(tr) => tr.dump(buf),
#[cfg(feature = "translate")]
Self::Language(l) => l.dump(buf),
}?;
Ok(size1 + result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "encrypt")]
#[test]
fn encryption_integration_test() {
let key = hex_literal::hex!("59C4E408F27250B3147E7724511824F1D28ED7BEF43CF7103ACE747F77A2B265");
let nonce = [0u8; NONCE_SIZE];
let packet = Packet::Call(RemoteCall{
id: 42,
function: "test".into(),
parameters: Vec::new(),
});
let mut buffer = Vec::with_capacity(PACKET_BUFFER_SIZE);
let len = packet.dump_encrypted(&mut buffer, &key, &nonce).unwrap();
println!("buffer: {}", String::from_utf8(buffer.as_slice()[..len].to_vec()).unwrap());
let (packet_out, _len) = Packet::load_encrypted(&buffer.as_slice()[..len], &key, &nonce).unwrap();
if let Packet::Call(call_out) = packet_out {
if let Packet::Call(call_in) = packet {
assert_eq!(call_in.id, call_out.id, "Input and output packets do not match");
assert_eq!(call_in.function, call_out.function, "Input and output packets do not match");
assert_eq!(call_in.parameters.len(), call_out.parameters.len(), "Input and output packets do not match");
} else {
panic!("Packet in not a Call");
}
} else {
panic!("Packet out not a Call!");
}
}
}

View file

@ -1,27 +1,28 @@
[package]
name = "usdpl-front"
version = "0.10.1"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
version = "0.11.0"
authors = ["NGnius <ngniusness@gmail.com>"]
edition = "2021"
license = "GPL-3.0-only"
repository = "https://github.com/NGnius/usdpl-rs"
readme = "README.md"
repository = "https://git.ngni.us/NG-SD-Plugins/usdpl-rs"
readme = "../README.md"
description = "Universal Steam Deck Plugin Library front-end designed for WASM"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["translate"]
default = []
decky = ["usdpl-core/decky"]
crankshaft = ["usdpl-core/crankshaft"]
debug = ["console_error_panic_hook"]
encrypt = ["usdpl-core/encrypt", "obfstr", "hex"]
translate = ["usdpl-core/translate"]
#encrypt = ["usdpl-core/encrypt", "obfstr", "hex"]
[dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
gloo-net = { version = "0.4", features = ["websocket"] }
futures = "0.3"
futures-channel = "0.3"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
@ -36,13 +37,17 @@ web-sys = { version = "0.3", features = [
'RequestMode',
'Response',
'Window',
'console',
]}
js-sys = { version = "0.3" }
obfstr = { version = "0.3", optional = true }
hex = { version = "0.4", optional = true }
usdpl-core = { version = "0.10", path = "../usdpl-core" }
nrpc = { version = "0.10", path = "../../nRPC/nrpc", default-features = false}
usdpl-core = { version = "0.11", path = "../usdpl-core" }
prost = "0.11"
log = "0.4"
[dev-dependencies]
wasm-bindgen-test = { version = "0.3.13" }

View file

@ -0,0 +1,168 @@
use std::sync::atomic::{AtomicU64, Ordering};
use futures::{SinkExt, StreamExt, future::{select, Either}};
use gloo_net::websocket::{futures::WebSocket, Message};
use nrpc::{ClientHandler, ServiceError, ServiceClientStream, _helpers::async_trait, _helpers::bytes};
use wasm_bindgen_futures::spawn_local;
static LAST_ID: AtomicU64 = AtomicU64::new(0);
/// Websocket client.
/// In most cases, this shouldn't be used directly, but generated code will use this.
pub struct WebSocketHandler {
port: u16,
}
#[inline]
fn ws_is_alive(ws_state: &gloo_net::websocket::State) -> bool {
match ws_state {
gloo_net::websocket::State::Connecting | gloo_net::websocket::State::Open => true,
gloo_net::websocket::State::Closing | gloo_net::websocket::State::Closed => false,
}
}
async fn send_recv_ws<'a>(mut tx: futures_channel::mpsc::Sender<Result<bytes::Bytes, String>>, url: String, mut input: ServiceClientStream<'a, bytes::Bytes>) {
let ws = match WebSocket::open_with_protocol(&url, "usdpl-nrpc").map_err(|e| e.to_string()) {
Ok(x) => x,
Err(e) => {
log::error!("ws open error: {}", e);
tx.send(Err(e.to_string())).await.unwrap_or(());
return;
}
};
log::debug!("ws opened successfully with url `{}`", url);
let (mut input_done, mut output_done) = (false, false);
let mut last_ws_state = ws.state();
log::debug!("ws with url `{}` initial state: {:?}", url, last_ws_state);
let (mut ws_sink, mut ws_stream) = ws.split();
let (mut left, mut right) = (input.next(), ws_stream.next());
while ws_is_alive(&last_ws_state) {
if !input_done && !output_done {
log::debug!("Input and output streams are both alive");
match select(left, right).await {
Either::Left((next, outstanding)) => {
log::debug!("Got message to send over websocket");
if let Some(next) = next {
match next {
Ok(next) => {
if let Err(e) = ws_sink.send(Message::Bytes(next.into())).await {
tx.send(Err(e.to_string())).await.unwrap_or(());
}
},
Err(e) => tx.send(Err(e.to_string())).await.unwrap_or(())
}
} else {
input_done = true;
}
right = outstanding;
left = input.next();
},
Either::Right((response, outstanding)) => {
log::debug!("Received message from websocket");
if let Some(next) = response {
match next {
Ok(Message::Bytes(b)) => tx.send(Ok(b.into())).await.unwrap_or(()),
Ok(_) => tx.send(Err("Message::Text not allowed".into())).await.unwrap_or(()),
Err(e) => tx.send(Err(e.to_string())).await.unwrap_or(()),
}
} else {
output_done = true;
}
left = outstanding;
let ws = ws_stream.reunite(ws_sink).unwrap();
last_ws_state = ws.state();
(ws_sink, ws_stream) = ws.split();
right = ws_stream.next();
}
}
} else if input_done {
log::debug!("Input stream is complete");
if let Some(next) = right.await {
log::debug!("Received message from websocket");
match next {
Ok(Message::Bytes(b)) => tx.send(Ok(b.into())).await.unwrap_or(()),
Ok(_) => tx.send(Err("Message::Text not allowed".into())).await.unwrap_or(()),
Err(e) => tx.send(Err(e.to_string())).await.unwrap_or(()),
}
} else {
output_done = true;
}
//left = outstanding;
let ws = ws_stream.reunite(ws_sink).unwrap();
last_ws_state = ws.state();
(ws_sink, ws_stream) = ws.split();
right = ws_stream.next();
} else {
// output_done is true
log::debug!("Output stream is complete");
if let Some(next) = left.await {
log::debug!("Got message to send over websocket");
match next {
Ok(next) => {
if let Err(e) = ws_sink.send(Message::Bytes(next.into())).await {
tx.send(Err(e.to_string())).await.unwrap_or(());
}
},
Err(e) => tx.send(Err(e.to_string())).await.unwrap_or(())
}
} else {
input_done = true;
}
//right = outstanding;
let ws = ws_stream.reunite(ws_sink).unwrap();
last_ws_state = ws.state();
(ws_sink, ws_stream) = ws.split();
left = input.next();
right = ws_stream.next(); // this should always resolve to None (but compiler is unhappy without this)
}
}
log::debug!("ws with url `{}` has closed", url);
}
#[derive(Debug)]
struct ErrorStr(String);
impl std::fmt::Display for ErrorStr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error message: {}", self.0)
}
}
impl std::error::Error for ErrorStr {}
const CHANNEL_BOUND: usize = 4;
impl WebSocketHandler {
/// Instantiate the web socket client for connecting on the specified port
pub fn new(port: u16) -> Self {
Self { port }
}
}
#[async_trait::async_trait(?Send)]
impl ClientHandler<'static> for WebSocketHandler {
async fn call<'a: 'static>(
&self,
package: &str,
service: &str,
method: &str,
input: ServiceClientStream<'a, bytes::Bytes>,
) -> Result<ServiceClientStream<'a, bytes::Bytes>, ServiceError> {
let id = LAST_ID.fetch_add(1, Ordering::SeqCst);
let url = format!(
"ws://usdpl-ws-{}.localhost:{}/{}.{}/{}",
id, self.port, package, service, method,
);
log::debug!("doing send/receive on ws url `{}`", url);
let (tx, rx) = futures_channel::mpsc::channel(CHANNEL_BOUND);
spawn_local(send_recv_ws(tx, url, input));
Ok(Box::new(rx.map(|buf_result: Result<bytes::Bytes, String>| buf_result
.map(|buf| bytes::Bytes::from(buf))
.map_err(|e| ServiceError::Method(Box::new(ErrorStr(e)))))))
}
}

View file

@ -1,104 +0,0 @@
//use std::net::TcpStream;
//use std::io::{Read, Write};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
//use web_sys::{WebSocket, MessageEvent, ErrorEvent};
use js_sys::JsString;
use web_sys::{Request, RequestInit, RequestMode, Response};
//use wasm_rs_shared_channel::{Expects, spsc::{Receiver, Sender}};
use usdpl_core::serdes::{Dumpable, Loadable, Primitive};
use usdpl_core::socket;
#[cfg(feature = "encrypt")]
const NONCE: [u8; socket::NONCE_SIZE]= [0u8; socket::NONCE_SIZE];
pub async fn send_recv_packet(
id: u64,
packet: socket::Packet,
port: u16,
#[cfg(feature = "encrypt")]
key: Vec<u8>,
) -> Result<socket::Packet, JsValue> {
let mut opts = RequestInit::new();
opts.method("POST");
opts.mode(RequestMode::Cors);
let url = format!("http://usdpl{}.{}:{}/usdpl/call", id, socket::HOST_STR, port);
#[allow(unused_variables)]
let (buffer, len) = dump_to_buffer(packet, #[cfg(feature = "encrypt")] key.as_slice())?;
let string: String = String::from_utf8_lossy(buffer.as_slice()).into();
#[cfg(feature="debug")]
crate::imports::console_log(&format!("Dumped base64 `{}` len:{}", string, len));
opts.body(Some(&string.into()));
let request = Request::new_with_str_and_init(&url, &opts)?;
//request.headers().set("Accept", "text/base64")?;
//.set("Authorization", "wasm TODO_KEY")?;
let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into()?;
let text = JsFuture::from(resp.text()?).await?;
let string: JsString = text.dyn_into()?;
let rust_str = string.as_string().unwrap();
#[cfg(feature="debug")]
crate::imports::console_log(&format!("Received base64 `{}` len:{}", rust_str, rust_str.len()));
#[cfg(not(feature = "encrypt"))]
{Ok(socket::Packet::load_base64(rust_str.as_bytes())
.map_err(super::convert::str_to_js)?
.0)}
#[cfg(feature = "encrypt")]
{Ok(socket::Packet::load_encrypted(rust_str.as_bytes(), key.as_slice(), &NONCE)
.map_err(super::convert::str_to_js)?
.0)}
}
pub async fn send_call(
id: u64,
packet: socket::Packet,
port: u16,
#[cfg(feature = "encrypt")]
key: Vec<u8>,
) -> Result<Vec<Primitive>, JsValue> {
let packet = send_recv_packet(id, packet, port, #[cfg(feature = "encrypt")] key).await?;
match packet
{
socket::Packet::CallResponse(resp) => Ok(resp.response),
_ => {
//imports::console_warn(&format!("USDPL warning: Got non-call-response message from {}", resp.url()));
Err("Expected call response message, got something else".into())
}
}
}
#[cfg(feature = "encrypt")]
fn dump_to_buffer(packet: socket::Packet, key: &[u8]) -> Result<(Vec<u8>, usize), JsValue> {
let mut buffer = Vec::with_capacity(socket::PACKET_BUFFER_SIZE);
//buffer.extend_from_slice(&[0u8; socket::PACKET_BUFFER_SIZE]);
let len = packet
.dump_encrypted(&mut buffer, key, &NONCE)
.map_err(super::convert::str_to_js)?;
Ok((buffer, len))
}
#[cfg(not(feature = "encrypt"))]
fn dump_to_buffer(packet: socket::Packet) -> Result<(Vec<u8>, usize), JsValue> {
let mut buffer = String::with_capacity(socket::PACKET_BUFFER_SIZE);
//buffer.extend_from_slice(&[0u8; socket::PACKET_BUFFER_SIZE]);
let len = packet
.dump_base64(&mut buffer)
.map_err(super::convert::str_to_js)?;
Ok((buffer.as_bytes().to_vec(), len))
}

View file

@ -0,0 +1,52 @@
pub(crate) struct BuiltInLogger {
min_level: log::Level,
}
impl BuiltInLogger {
pub const fn new(min: log::Level) -> Self {
Self {
min_level: min,
}
}
}
impl log::Log for BuiltInLogger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= self.min_level
}
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
match record.level() {
log::Level::Error => web_sys::console::error_1(&fmt_msg(record).into()),
log::Level::Warn => web_sys::console::warn_1(&fmt_msg(record).into()),
log::Level::Info => web_sys::console::log_1(&fmt_msg(record).into()),
log::Level::Debug => web_sys::console::debug_1(&fmt_msg(record).into()),
log::Level::Trace => web_sys::console::debug_1(&fmt_msg(record).into()),
}
}
}
fn flush(&self) {}
}
fn fmt_msg(record: &log::Record) -> String {
#[cfg(feature = "debug")]
{ format!("[{}]({}) {}", record.level(), file_line_info(record), record.args()) }
#[cfg(not(feature = "debug"))]
{ format!("[{}]({}) {}", record.level(), module_line_info(record), record.args()) }
}
#[cfg(feature = "debug")]
fn file_line_info(record: &log::Record) -> String {
let filepath = record.file().unwrap_or("<unknown file>");
let line = record.line().map(|l| l.to_string()).unwrap_or_else(|| "line?".to_string());
format!("{}:{}", filepath, line)
}
#[cfg(not(feature = "debug"))]
fn module_line_info(record: &log::Record) -> String {
let target = record.target();
let line = record.line().map(|l| l.to_string()).unwrap_or_else(|| "line?".to_string());
format!("{}:{}", target, line)
}

View file

@ -1,10 +1,10 @@
use js_sys::JsString;
use js_sys::JSON::{parse, stringify};
//use js_sys::JsString;
//use js_sys::JSON::{parse, stringify};
use wasm_bindgen::prelude::JsValue;
use usdpl_core::serdes::Primitive;
//use usdpl_core::serdes::Primitive;
pub(crate) fn primitive_to_js(primitive: Primitive) -> JsValue {
/*pub(crate) fn primitive_to_js(primitive: Primitive) -> JsValue {
match primitive {
Primitive::Empty => JsValue::null(),
Primitive::String(s) => JsValue::from_str(&s),
@ -33,8 +33,16 @@ pub(crate) fn js_to_primitive(val: JsValue) -> Primitive {
} else {
Primitive::Empty
}
}
}*/
pub(crate) fn str_to_js<S: std::string::ToString>(s: S) -> JsString {
/*pub(crate) fn str_to_js<S: std::string::ToString>(s: S) -> JsString {
s.to_string().into()
}*/
pub(crate) fn js_to_str(js: JsValue) -> String {
if let Some(s) = js.as_string() {
s
} else {
format!("{:?}", js)
}
}

View file

@ -1,16 +0,0 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[cfg(feature = "debug")]
#[wasm_bindgen(js_namespace = console, js_name = log)]
pub fn console_log(s: &str);
#[cfg(feature = "debug")]
#[wasm_bindgen(js_namespace = console, js_name = warn)]
pub fn console_warn(s: &str);
#[cfg(feature = "debug")]
#[wasm_bindgen(js_namespace = console, js_name = error)]
pub fn console_error(s: &str);
}

View file

@ -5,29 +5,33 @@
//!
#![warn(missing_docs)]
mod connection;
mod client_handler;
pub use client_handler::WebSocketHandler;
mod console_logs;
mod convert;
mod imports;
pub mod wasm;
use std::sync::atomic::{AtomicU64, Ordering};
#[allow(missing_docs)]
pub mod _helpers {
pub use js_sys;
pub use wasm_bindgen;
pub use wasm_bindgen_futures;
pub use log;
pub use futures;
pub use nrpc;
}
use js_sys::Array;
use wasm_bindgen::prelude::*;
use usdpl_core::{socket::Packet, RemoteCall};
//const REMOTE_CALL_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
//const REMOTE_PORT: std::sync::atomic::AtomicU16 = std::sync::atomic::AtomicU16::new(31337);
#[cfg(feature = "debug")]
const DEFAULT_MIN_LEVEL: log::Level = log::Level::Trace;
#[cfg(not(feature = "debug"))]
const DEFAULT_MIN_LEVEL: log::Level = log::Level::Info;
static mut CTX: UsdplContext = UsdplContext {
port: 31337,
id: AtomicU64::new(0),
#[cfg(feature = "encrypt")]
key: Vec::new(),
};
const DEFAULT_LOGGER: console_logs::BuiltInLogger = console_logs::BuiltInLogger::new(DEFAULT_MIN_LEVEL);
static mut CACHE: Option<std::collections::HashMap<String, JsValue>> = None;
#[cfg(feature = "translate")]
static mut TRANSLATIONS: Option<std::collections::HashMap<String, Vec<String>>> = None;
#[cfg(feature = "encrypt")]
@ -35,49 +39,42 @@ fn encryption_key() -> Vec<u8> {
hex::decode(obfstr::obfstr!(env!("USDPL_ENCRYPTION_KEY"))).unwrap()
}
//#[wasm_bindgen]
#[derive(Debug)]
struct UsdplContext {
port: u16,
id: AtomicU64,
#[cfg(feature = "encrypt")]
key: Vec<u8>,
}
fn get_port() -> u16 {
unsafe { CTX.port }
}
#[cfg(feature = "encrypt")]
fn get_key() -> Vec<u8> {
unsafe { CTX.key.clone() }
}
fn increment_id() -> u64 {
let atomic = unsafe { &CTX.id };
atomic.fetch_add(1, Ordering::SeqCst)
}
static INIT_DONE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// Initialize the front-end library
#[wasm_bindgen]
pub fn init_usdpl(port: u16) {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
//REMOTE_PORT.store(port, std::sync::atomic::Ordering::SeqCst);
unsafe {
CTX = UsdplContext {
port: port,
id: AtomicU64::new(0),
#[cfg(feature = "encrypt")]
key: encryption_key(),
};
}
//#[wasm_bindgen]
pub fn init_usdpl() {
if !INIT_DONE.swap(true, std::sync::atomic::Ordering::SeqCst) {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
unsafe {
CACHE = Some(std::collections::HashMap::new());
log::set_logger(&DEFAULT_LOGGER)
.map_err(|e| web_sys::console::error_1(&format!("Failed to setup USDPL logger: {}", e).into()))
.unwrap_or(());
log::set_max_level(log::LevelFilter::Trace);
log::debug!("init_usdpl() log configured");
unsafe {
CACHE = Some(std::collections::HashMap::new());
}
log::info!("USDPL init succeeded: {}", build_info());
} else {
log::info!("USDPL init was re-attempted");
}
}
fn build_info() -> String {
format!("{} v{} ({}) for {} by {}, more: {}",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
env!("CARGO_PKG_LICENSE"),
target_usdpl(),
env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_REPOSITORY"),
)
}
/// Get the targeted plugin framework, or "any" if unknown
#[wasm_bindgen]
pub fn target_usdpl() -> String {
@ -94,7 +91,11 @@ pub fn version_usdpl() -> String {
#[wasm_bindgen]
pub fn set_value(key: String, value: JsValue) -> JsValue {
unsafe {
CACHE.as_mut().unwrap().insert(key, value).unwrap_or(JsValue::NULL)
CACHE
.as_mut()
.unwrap()
.insert(key, value)
.unwrap_or(JsValue::NULL)
}
}
@ -102,90 +103,12 @@ pub fn set_value(key: String, value: JsValue) -> JsValue {
#[wasm_bindgen]
pub fn get_value(key: String) -> JsValue {
unsafe {
CACHE.as_ref().unwrap().get(&key).map(|x| x.to_owned()).unwrap_or(JsValue::UNDEFINED)
}
}
/// Call a function on the back-end.
/// Returns null (None) if this fails for any reason.
#[wasm_bindgen]
pub async fn call_backend(name: String, parameters: Vec<JsValue>) -> JsValue {
#[cfg(feature = "debug")]
imports::console_log(&format!(
"call_backend({}, [params; {}])",
name,
parameters.len()
));
let next_id = increment_id();
let mut params = Vec::with_capacity(parameters.len());
for val in parameters {
params.push(convert::js_to_primitive(val));
}
let port = get_port();
#[cfg(feature = "debug")]
imports::console_log(&format!("USDPL: Got port {}", port));
let results = connection::send_call(
next_id,
Packet::Call(RemoteCall {
id: next_id,
function: name.clone(),
parameters: params,
}),
port,
#[cfg(feature = "encrypt")]
get_key()
)
.await;
let results = match results {
Ok(x) => x,
#[allow(unused_variables)]
Err(e) => {
#[cfg(feature = "debug")]
imports::console_error(&format!("USDPL: Got error while calling {}: {:?}", name, e));
return JsValue::NULL;
}
};
let results_js = Array::new_with_length(results.len() as _);
let mut i = 0;
for item in results {
results_js.set(i as _, convert::primitive_to_js(item));
i += 1;
}
results_js.into()
}
/// Initialize translation strings for the front-end
#[wasm_bindgen]
pub async fn init_tr(locale: String) {
let next_id = increment_id();
match connection::send_recv_packet(
next_id,
Packet::Language(locale.clone()),
get_port(),
#[cfg(feature = "encrypt")]
get_key()
).await {
Ok(Packet::Translations(translations)) => {
#[cfg(feature = "debug")]
imports::console_log(&format!("USDPL: Got translations for {}", locale));
// convert translations into map
let mut tr_map = std::collections::HashMap::with_capacity(translations.len());
for (key, val) in translations {
tr_map.insert(key, val);
}
unsafe { TRANSLATIONS = Some(tr_map) }
},
Ok(_) => {
#[cfg(feature = "debug")]
imports::console_error(&format!("USDPL: Got wrong packet response for init_tr"));
unsafe { TRANSLATIONS = None }
},
#[allow(unused_variables)]
Err(e) => {
#[cfg(feature = "debug")]
imports::console_error(&format!("USDPL: Got wrong error for init_tr: {:#?}", e));
unsafe { TRANSLATIONS = None }
}
CACHE
.as_ref()
.unwrap()
.get(&key)
.map(|x| x.to_owned())
.unwrap_or(JsValue::UNDEFINED)
}
}

View file

@ -0,0 +1,94 @@
use js_sys::Array;
use super::{FromWasmable, IntoWasmable};
macro_rules! numbers_array {
($num_ty: ident) => {
impl FromWasmable<Array> for Vec<$num_ty> {
fn from_wasm(js: Array) -> Self {
let mut result = Vec::with_capacity(js.length() as usize);
js.for_each(&mut |val, _index, _arr| {
// according to MDN, this is guaranteed to be in order so index can be ignored
if let Some(val) = val.as_f64() {
result.push(val as $num_ty);
}
});
result
}
}
impl IntoWasmable<Array> for Vec<$num_ty> {
fn into_wasm(self) -> Array {
let result = Array::new();
for val in self {
result.push(&val.into());
}
result
}
}
};
}
numbers_array! { f64 }
numbers_array! { f32 }
numbers_array! { isize }
numbers_array! { usize }
numbers_array! { i8 }
numbers_array! { i16 }
numbers_array! { i32 }
numbers_array! { i64 }
numbers_array! { i128 }
numbers_array! { u8 }
numbers_array! { u16 }
numbers_array! { u32 }
numbers_array! { u64 }
numbers_array! { u128 }
impl FromWasmable<Array> for Vec<String> {
fn from_wasm(js: Array) -> Self {
let mut result = Vec::with_capacity(js.length() as usize);
js.for_each(&mut |val, _index, _arr| {
// according to MDN, this is guaranteed to be in order so index can be ignored
if let Some(val) = val.as_string() {
result.push(val);
}
});
result
}
}
impl IntoWasmable<Array> for Vec<String> {
fn into_wasm(self) -> Array {
let result = Array::new();
for val in self {
result.push(&val.into());
}
result
}
}
impl FromWasmable<Array> for Vec<bool> {
fn from_wasm(js: Array) -> Self {
let mut result = Vec::with_capacity(js.length() as usize);
js.for_each(&mut |val, _index, _arr| {
// according to MDN, this is guaranteed to be in order so index can be ignored
if let Some(val) = val.as_bool() {
result.push(val);
}
});
result
}
}
impl IntoWasmable<Array> for Vec<bool> {
fn into_wasm(self) -> Array {
let result = Array::new();
for val in self {
result.push(&val.into());
}
result
}
}

View file

@ -0,0 +1,74 @@
use core::pin::Pin;
use core::future::Future;
use futures::{Stream, task::{Poll, Context}};
use wasm_bindgen_futures::JsFuture;
use wasm_bindgen::JsValue;
use js_sys::{Function, Promise};
use nrpc::ServiceError;
use super::FromWasmStreamableType;
use crate::convert::js_to_str;
/// futures::Stream wrapper for a JS async function that generates a new T-like value every call
pub struct JsFunctionStream<T: FromWasmStreamableType + Unpin + 'static> {
function: Function,
promise: Option<JsFuture>,
_idc: std::marker::PhantomData<T>,
}
impl <T: FromWasmStreamableType + Unpin + 'static> JsFunctionStream<T> {
/// Construct the function stream wrapper
pub fn from_function(f: Function) -> Self {
Self {
function: f,
promise: None,
_idc: std::marker::PhantomData::default(),
}
}
}
impl <T: FromWasmStreamableType + Unpin + 'static> Stream for JsFunctionStream<T> {
type Item = Result<T, ServiceError>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>
) -> Poll<Option<Self::Item>> {
// this is horrible, I'm sorry
let js_poll = if let Some(mut promise) = self.promise.take() {
let mut pin = Pin::new(&mut promise);
JsFuture::poll(pin.as_mut(), cx)
} else {
let function_result = match self.function.call0(&JsValue::undefined()) {
Ok(x) => x,
Err(e) => return Poll::Ready(Some(Err(ServiceError::Method(s_to_err(format!("JS function call error: {}", js_to_str(e)))))))
};
let js_promise = Promise::from(function_result);
let mut js_future = JsFuture::from(js_promise);
let mut pin = Pin::new(&mut js_future);
let poll = JsFuture::poll(pin.as_mut(), cx);
self.promise = Some(js_future);
poll
};
js_poll.map(|t| match t {
Ok(t) => {
if t.is_null() || t.is_undefined() {
None
} else {
Some(T::from_wasm_streamable(t).map_err(|e| ServiceError::Method(s_to_err(format!("JS type conversion error: {}", e)))))
}
},
Err(e) => Some(Err(ServiceError::Method(s_to_err(format!("JS function promise error: {}", js_to_str(e))))))
})
}
}
fn s_to_err(s: String) -> Box<(dyn std::error::Error + Send + Sync + 'static)> {
s.into()
}
fn _check_service_stream<T: FromWasmStreamableType + Unpin + 'static>(js_stream: JsFunctionStream<T>) {
let _: nrpc::ServiceClientStream<'static, T> = Box::new(js_stream);
}

View file

@ -0,0 +1,99 @@
use std::collections::HashMap;
use js_sys::Map;
use super::{FromWasmable, IntoWasmable};
macro_rules! numbers_map {
($num_ty: ident) => {
impl FromWasmable<Map> for HashMap<String, $num_ty> {
fn from_wasm(js: Map) -> Self {
let mut result = HashMap::with_capacity(js.size() as usize);
js.for_each(&mut |key, val| {
if let Some(key) = key.as_string() {
if let Some(val) = val.as_f64() {
result.insert(key, val as $num_ty);
}
}
});
result
}
}
impl IntoWasmable<Map> for HashMap<String, $num_ty> {
fn into_wasm(self) -> Map {
let result = Map::new();
for (key, val) in self {
result.set(&key.into(), &val.into());
}
result
}
}
};
}
numbers_map! { f64 }
numbers_map! { f32 }
numbers_map! { isize }
numbers_map! { usize }
numbers_map! { i8 }
numbers_map! { i16 }
numbers_map! { i32 }
numbers_map! { i64 }
numbers_map! { i128 }
numbers_map! { u8 }
numbers_map! { u16 }
numbers_map! { u32 }
numbers_map! { u64 }
numbers_map! { u128 }
impl FromWasmable<Map> for HashMap<String, String> {
fn from_wasm(js: Map) -> Self {
let mut result = HashMap::with_capacity(js.size() as usize);
js.for_each(&mut |key, val| {
if let Some(key) = key.as_string() {
if let Some(val) = val.as_string() {
result.insert(key, val);
}
}
});
result
}
}
impl IntoWasmable<Map> for HashMap<String, String> {
fn into_wasm(self) -> Map {
let result = Map::new();
for (key, val) in self {
result.set(&key.into(), &val.into());
}
result
}
}
impl FromWasmable<Map> for HashMap<String, bool> {
fn from_wasm(js: Map) -> Self {
let mut result = HashMap::with_capacity(js.size() as usize);
js.for_each(&mut |key, val| {
if let Some(key) = key.as_string() {
if let Some(val) = val.as_bool() {
result.insert(key, val);
}
}
});
result
}
}
impl IntoWasmable<Map> for HashMap<String, bool> {
fn into_wasm(self) -> Map {
let result = Map::new();
for (key, val) in self {
result.set(&key.into(), &val.into());
}
result
}
}

View file

@ -0,0 +1,11 @@
//! WASM <-> Rust interop utilities
mod arrays;
mod js_function_stream;
mod maps;
mod streaming;
mod trivials;
mod wasm_traits;
pub use js_function_stream::JsFunctionStream;
pub use wasm_traits::*;
pub use streaming::*;

View file

@ -0,0 +1,192 @@
use wasm_bindgen::JsValue;
/// Convert Rust type to WASM-compatible type involved in nRPC streaming
pub trait IntoWasmStreamableType {
/// Required method
fn into_wasm_streamable(self) -> JsValue;
}
#[derive(Debug)]
/// Conversion error from FromWasmStreamableType
pub enum WasmStreamableConversionError {
/// JSValue underlying type is incorrect
UnexpectedType {
/// Expected Javascript type
expected: JsType,
/// Actual Javascript type
got: JsType,
},
}
impl core::fmt::Display for WasmStreamableConversionError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::UnexpectedType { expected, got } => write!(f, "Unexpected type {}, expected {}", expected, got),
}
}
}
impl std::error::Error for WasmStreamableConversionError {}
/// Approximation of all possible JS types detectable through Wasm
#[allow(missing_docs)]
#[derive(Debug)]
pub enum JsType {
Number,
String,
Bool,
Array,
BigInt,
Function,
Symbol,
Undefined,
Null,
Object,
Unknown,
}
impl core::fmt::Display for JsType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Number => write!(f, "number"),
Self::String => write!(f, "string"),
Self::Bool => write!(f, "boolean"),
Self::Array => write!(f, "array"),
Self::BigInt => write!(f, "bigint"),
Self::Function => write!(f, "function"),
Self::Symbol => write!(f, "symbol"),
Self::Undefined => write!(f, "undefined"),
Self::Null => write!(f, "null"),
Self::Object => write!(f, "object"),
Self::Unknown => write!(f, "<unknown>"),
}
}
}
impl JsType {
/// Guess the JS type of the parameter.
/// This is not guaranteed to be correct, but is intended to give more information
/// in debug and error messages
pub fn guess(js: &JsValue) -> JsType {
if js.as_f64().is_some() {
Self::Number
} else if js.as_string().is_some() {
Self::String
} else if js.as_bool().is_some() {
Self::Bool
} else if js.is_array() {
Self::Array
} else if js.is_bigint() {
Self::BigInt
} else if js.is_function() {
Self::Function
} else if js.is_symbol() {
Self::Symbol
} else if js.is_undefined() {
Self::Undefined
} else if js.is_null() {
Self::Null
} else if js.is_object() {
Self::Object
} else {
Self::Unknown
}
}
}
/// Convert WASM-compatible type involved in nRPC streaming to Rust-centric type
pub trait FromWasmStreamableType: Sized {
/// Required method
fn from_wasm_streamable(js: JsValue) -> Result<Self, WasmStreamableConversionError>;
}
macro_rules! trivial_convert_number {
($ty: ty) => {
impl FromWasmStreamableType for $ty {
fn from_wasm_streamable(js: JsValue) -> Result<Self, WasmStreamableConversionError> {
if let Some(num) = js.as_f64() {
Ok(num as $ty)
} else {
Err(WasmStreamableConversionError::UnexpectedType {
expected: JsType::Number,
got: JsType::guess(&js),
})
}
}
}
impl IntoWasmStreamableType for $ty {
fn into_wasm_streamable(self) -> JsValue {
self.into()
}
}
};
}
trivial_convert_number! { f64 }
trivial_convert_number! { f32 }
trivial_convert_number! { isize }
trivial_convert_number! { usize }
trivial_convert_number! { i8 }
trivial_convert_number! { i16 }
trivial_convert_number! { i32 }
trivial_convert_number! { i64 }
trivial_convert_number! { i128 }
trivial_convert_number! { u8 }
trivial_convert_number! { u16 }
trivial_convert_number! { u32 }
trivial_convert_number! { u64 }
trivial_convert_number! { u128 }
impl FromWasmStreamableType for String {
fn from_wasm_streamable(js: JsValue) -> Result<Self, WasmStreamableConversionError> {
if let Some(s) = js.as_string() {
Ok(s)
} else {
Err(WasmStreamableConversionError::UnexpectedType {
expected: JsType::String,
got: JsType::guess(&js),
})
}
}
}
impl IntoWasmStreamableType for String {
fn into_wasm_streamable(self) -> JsValue {
self.into()
}
}
impl FromWasmStreamableType for bool {
fn from_wasm_streamable(js: JsValue) -> Result<Self, WasmStreamableConversionError> {
if let Some(b) = js.as_bool() {
Ok(b)
} else {
Err(WasmStreamableConversionError::UnexpectedType {
expected: JsType::Bool,
got: JsType::guess(&js),
})
}
}
}
impl IntoWasmStreamableType for bool {
fn into_wasm_streamable(self) -> JsValue {
self.into()
}
}
impl FromWasmStreamableType for () {
fn from_wasm_streamable(_js: JsValue) -> Result<Self, WasmStreamableConversionError> {
Ok(())
}
}
impl IntoWasmStreamableType for () {
fn into_wasm_streamable(self) -> JsValue {
JsValue::undefined()
}
}

View file

@ -0,0 +1,40 @@
use super::{FromWasmable, IntoWasmable};
macro_rules! trivial_convert {
($ty: ty) => {
impl FromWasmable<$ty> for $ty {
fn from_wasm(js: $ty) -> Self {
js
}
}
impl IntoWasmable<$ty> for $ty {
fn into_wasm(self) -> $ty {
self
}
}
};
}
trivial_convert! { f64 }
trivial_convert! { f32 }
trivial_convert! { isize }
trivial_convert! { usize }
trivial_convert! { i8 }
trivial_convert! { i16 }
trivial_convert! { i32 }
trivial_convert! { i64 }
trivial_convert! { i128 }
trivial_convert! { u8 }
trivial_convert! { u16 }
trivial_convert! { u32 }
trivial_convert! { u64 }
trivial_convert! { u128 }
trivial_convert! { bool }
trivial_convert! { String }
trivial_convert! { () }

View file

@ -0,0 +1,40 @@
/// A Rust type which supports Into/FromWasmAbi or WasmDescribe
pub trait KnownWasmCompatible {}
/// Convert Rust type to WASM-compatible type
pub trait IntoWasmable<T: KnownWasmCompatible> {
/// Required method
fn into_wasm(self) -> T;
}
/// Convert WASM-compatible type to Rust-centric type
pub trait FromWasmable<T: KnownWasmCompatible> {
/// Required method
fn from_wasm(js: T) -> Self;
}
impl KnownWasmCompatible for f64 {}
impl KnownWasmCompatible for f32 {}
impl KnownWasmCompatible for isize {}
impl KnownWasmCompatible for usize {}
impl KnownWasmCompatible for i8 {}
impl KnownWasmCompatible for i16 {}
impl KnownWasmCompatible for i32 {}
impl KnownWasmCompatible for i64 {}
impl KnownWasmCompatible for i128 {}
impl KnownWasmCompatible for u8 {}
impl KnownWasmCompatible for u16 {}
impl KnownWasmCompatible for u32 {}
impl KnownWasmCompatible for u64 {}
impl KnownWasmCompatible for u128 {}
impl KnownWasmCompatible for bool {}
impl KnownWasmCompatible for String {}
impl KnownWasmCompatible for () {}
impl KnownWasmCompatible for js_sys::Map {}
impl KnownWasmCompatible for js_sys::Array {}