Merge branch 'next'
This commit is contained in:
commit
811aa01444
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