Compare commits
13 commits
61db1be355
...
811aa01444
Author | SHA1 | Date | |
---|---|---|---|
811aa01444 | |||
4aa33971b4 | |||
1ad6205067 | |||
44298f660f | |||
b7b42a8c6d | |||
72c7f111e8 | |||
84cae5af7d | |||
68b7455c9e | |||
4fed12d6d9 | |||
0b44ebc12b | |||
febaafe50c | |||
6a525fa384 | |||
570c194e82 |
54 changed files with 3524 additions and 2176 deletions
1329
Cargo.lock
generated
1329
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
32
Cargo.toml
32
Cargo.toml
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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
6
usdpl-back/build.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
fn main() {
|
||||
usdpl_build::back::build_with_custom_builtins(
|
||||
[].into_iter(),
|
||||
[].into_iter(),
|
||||
)
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -1,2 +1 @@
|
|||
pub mod dirs;
|
||||
pub mod files;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
compile_error!("Crankshaft unsupported (project no longer maintained)");
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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::*;
|
||||
}
|
|
@ -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"));
|
||||
}*/
|
||||
|
|
5
usdpl-back/src/rpc/mod.rs
Normal file
5
usdpl-back/src/rpc/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod registry;
|
||||
pub use registry::{ServiceRegistry, StaticServiceRegistry};
|
||||
|
||||
mod websocket_stream;
|
||||
pub use websocket_stream::ws_stream;
|
43
usdpl-back/src/rpc/registry.rs
Normal file
43
usdpl-back/src/rpc/registry.rs
Normal 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
|
||||
}
|
||||
}
|
34
usdpl-back/src/rpc/websocket_stream.rs
Normal file
34
usdpl-back/src/rpc/websocket_stream.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
22
usdpl-back/src/services_impl/dev_tools.rs
Normal file
22
usdpl-back/src/services_impl/dev_tools.rs
Normal 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 })
|
||||
}
|
||||
}
|
5
usdpl-back/src/services_impl/mod.rs
Normal file
5
usdpl-back/src/services_impl/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod dev_tools;
|
||||
pub(crate) use dev_tools::DevTools;
|
||||
|
||||
mod translations;
|
||||
pub(crate) use translations::Translations;
|
31
usdpl-back/src/services_impl/translations.rs
Normal file
31
usdpl-back/src/services_impl/translations.rs
Normal 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)
|
||||
}
|
175
usdpl-back/src/websockets.rs
Normal file
175
usdpl-back/src/websockets.rs
Normal 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
23
usdpl-build/Cargo.toml
Normal 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"
|
||||
|
27
usdpl-build/protos/debug.proto
Normal file
27
usdpl-build/protos/debug.proto
Normal 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;
|
||||
}
|
19
usdpl-build/protos/translations.proto
Normal file
19
usdpl-build/protos/translations.proto
Normal 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;
|
||||
}
|
20
usdpl-build/src/back/mod.rs
Normal file
20
usdpl-build/src/back/mod.rs
Normal 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),
|
||||
)
|
||||
}
|
45
usdpl-build/src/front/mod.rs
Normal file
45
usdpl-build/src/front/mod.rs
Normal 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()
|
||||
}
|
24
usdpl-build/src/front/preprocessor.rs
Normal file
24
usdpl-build/src/front/preprocessor.rs
Normal 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! {}
|
||||
}
|
||||
}
|
1117
usdpl-build/src/front/service_generator.rs
Normal file
1117
usdpl-build/src/front/service_generator.rs
Normal file
File diff suppressed because it is too large
Load diff
24
usdpl-build/src/front/shared_state.rs
Normal file
24
usdpl-build/src/front/shared_state.rs
Normal 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
7
usdpl-build/src/lib.rs
Normal 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,
|
||||
};
|
86
usdpl-build/src/proto_files.rs
Normal file
86
usdpl-build/src/proto_files.rs
Normal 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)
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -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::*;
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]}
|
||||
}
|
|
@ -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}
|
||||
}
|
|
@ -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};
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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" }
|
||||
|
|
168
usdpl-front/src/client_handler.rs
Normal file
168
usdpl-front/src/client_handler.rs
Normal 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)))))))
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
52
usdpl-front/src/console_logs.rs
Normal file
52
usdpl-front/src/console_logs.rs
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
94
usdpl-front/src/wasm/arrays.rs
Normal file
94
usdpl-front/src/wasm/arrays.rs
Normal 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
|
||||
}
|
||||
}
|
74
usdpl-front/src/wasm/js_function_stream.rs
Normal file
74
usdpl-front/src/wasm/js_function_stream.rs
Normal 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);
|
||||
}
|
99
usdpl-front/src/wasm/maps.rs
Normal file
99
usdpl-front/src/wasm/maps.rs
Normal 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
|
||||
}
|
||||
}
|
11
usdpl-front/src/wasm/mod.rs
Normal file
11
usdpl-front/src/wasm/mod.rs
Normal 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::*;
|
192
usdpl-front/src/wasm/streaming.rs
Normal file
192
usdpl-front/src/wasm/streaming.rs
Normal 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()
|
||||
}
|
||||
}
|
40
usdpl-front/src/wasm/trivials.rs
Normal file
40
usdpl-front/src/wasm/trivials.rs
Normal 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! { () }
|
40
usdpl-front/src/wasm/wasm_traits.rs
Normal file
40
usdpl-front/src/wasm/wasm_traits.rs
Normal 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 {}
|
Loading…
Reference in a new issue