Fix closure panics

This commit is contained in:
Edward Shen 2021-10-24 16:16:02 -07:00
parent a509ff08b4
commit beda106f6a
Signed by: edward
GPG key ID: 19182661E818369F
7 changed files with 219 additions and 152 deletions

2
Cargo.lock generated
View file

@ -1051,12 +1051,14 @@ dependencies = [
"chacha20poly1305", "chacha20poly1305",
"chrono", "chrono",
"headers", "headers",
"http",
"lazy_static", "lazy_static",
"rand", "rand",
"serde", "serde",
"sha2", "sha2",
"thiserror", "thiserror",
"url", "url",
"web-sys",
] ]
[[package]] [[package]]

View file

@ -17,3 +17,9 @@ serde = { version = "1", features = ["derive"] }
sha2 = "0.9" sha2 = "0.9"
thiserror = "1" thiserror = "1"
url = "2" url = "2"
web-sys = { version = "0.3", features = ["Headers"], optional = true }
http = { version = "0.2", optional = true }
[features]
wasm = ["web-sys", "http"]

View file

@ -293,6 +293,21 @@ impl From<Expiration> for HeaderValue {
} }
} }
#[cfg(feature = "wasm")]
impl TryFrom<web_sys::Headers> for Expiration {
type Error = ParseHeaderValueError;
fn try_from(headers: web_sys::Headers) -> Result<Self, Self::Error> {
headers
.get(http::header::EXPIRES.as_str())
.ok()
.flatten()
.as_deref()
.and_then(|v| Expiration::try_from(v).ok())
.ok_or(ParseHeaderValueError)
}
}
pub struct ParseHeaderValueError; pub struct ParseHeaderValueError;
impl TryFrom<&HeaderValue> for Expiration { impl TryFrom<&HeaderValue> for Expiration {

View file

@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
omegaupload-common = { path = "../common" } omegaupload-common = { path = "../common", features = ["wasm"] }
# Enables wasm support # Enables wasm support
getrandom = { version = "*", features = ["js"] } getrandom = { version = "*", features = ["js"] }

89
web/src/idb_object.rs Normal file
View file

@ -0,0 +1,89 @@
use std::{hint::unreachable_unchecked, marker::PhantomData};
use js_sys::{Array, JsString, Object};
use wasm_bindgen::JsValue;
pub struct IdbObject<State>(Array, PhantomData<State>);
impl<State: IdbObjectState> IdbObject<State> {
fn add_tuple<NextState>(self, key: &str, value: &JsValue) -> IdbObject<NextState> {
let array = Array::new();
array.push(&JsString::from(key));
array.push(value);
self.0.push(&array);
IdbObject(self.0, PhantomData)
}
}
impl From<IdbObject<Ready>> for Object {
fn from(db_object: IdbObject<Ready>) -> Self {
match Object::from_entries(db_object.as_ref()) {
Ok(o) => o,
// SAFETY: IdbObject maintains the invariant that it can eventually
// be constructed into a JS object.
_ => unsafe { panic!() },
}
}
}
impl IdbObject<NeedsType> {
pub fn new() -> Self {
Self(Array::new(), PhantomData)
}
pub fn video(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("video"))
}
pub fn audio(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("audio"))
}
pub fn image(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("image"))
}
pub fn blob(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("blob"))
}
pub fn string(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("string"))
}
}
impl IdbObject<NeedsExpiration> {
pub fn expiration_text(self, expires: &str) -> IdbObject<NeedsData> {
self.add_tuple("expiration", &JsString::from(expires))
}
}
impl IdbObject<NeedsData> {
pub fn data(self, value: &JsValue) -> IdbObject<Ready> {
self.add_tuple("data", value)
}
}
impl IdbObject<Ready> {
pub fn extra(self, key: &str, value: impl Into<JsValue>) -> Self {
self.add_tuple(key, &value.into())
}
}
impl AsRef<JsValue> for IdbObject<Ready> {
fn as_ref(&self) -> &JsValue {
self.0.as_ref()
}
}
macro_rules! impl_idb_object_state {
($($ident:ident),*) => {
pub trait IdbObjectState {}
$(
pub enum $ident {}
impl IdbObjectState for $ident {}
)*
};
}
impl_idb_object_state!(NeedsType, NeedsExpiration, NeedsData, Ready);

View file

@ -1,36 +1,56 @@
#![warn(clippy::nursery, clippy::pedantic)] #![warn(clippy::nursery, clippy::pedantic)]
use std::marker::PhantomData;
use std::str::FromStr; use std::str::FromStr;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Context, Result};
use byte_unit::Byte; use byte_unit::Byte;
use decrypt::DecryptedData; use decrypt::DecryptedData;
use gloo_console::log; use gloo_console::log;
use http::header::EXPIRES;
use http::uri::PathAndQuery; use http::uri::PathAndQuery;
use http::{StatusCode, Uri}; use http::{StatusCode, Uri};
use js_sys::{Array, JsString, Object, Uint8Array}; use js_sys::{JsString, Object, Uint8Array};
use omegaupload_common::{Expiration, PartialParsedUrl}; use omegaupload_common::{Expiration, PartialParsedUrl};
use reqwasm::http::Request; use reqwasm::http::Request;
use wasm_bindgen::prelude::{wasm_bindgen, Closure}; use wasm_bindgen::prelude::{wasm_bindgen, Closure};
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::{spawn_local, JsFuture}; use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{window, Event, IdbDatabase, IdbObjectStore, IdbOpenDbRequest, IdbTransactionMode}; use web_sys::{Event, IdbObjectStore, IdbOpenDbRequest, IdbTransactionMode, Location, Window};
use crate::decrypt::decrypt; use crate::decrypt::decrypt;
use crate::idb_object::IdbObject;
use crate::util::as_idb_db;
mod decrypt; mod decrypt;
mod idb_object;
mod util;
#[wasm_bindgen] #[wasm_bindgen]
extern "C" { extern "C" {
fn loadFromDb(); #[wasm_bindgen(js_name = loadFromDb)]
fn createNotFoundUi(); fn load_from_db();
#[wasm_bindgen(js_name = createNotFoundUi)]
fn create_not_found_ui();
}
fn window() -> Window {
web_sys::window().expect("Failed to get a reference of the window")
}
fn location() -> Location {
window().location()
}
fn open_idb() -> Result<IdbOpenDbRequest> {
window()
.indexed_db()
.unwrap()
.context("Missing browser idb impl")?
.open("omegaupload")
.map_err(|_| anyhow!("Failed to open idb"))
} }
fn main() { fn main() {
let window = window().unwrap(); let url = String::from(location().to_string());
let url = String::from(window.location().to_string());
let request_uri = { let request_uri = {
let mut uri_parts = url.parse::<Uri>().unwrap().into_parts(); let mut uri_parts = url.parse::<Uri>().unwrap().into_parts();
if let Some(parts) = uri_parts.path_and_query.as_mut() { if let Some(parts) = uri_parts.path_and_query.as_mut() {
@ -39,30 +59,27 @@ fn main() {
Uri::from_parts(uri_parts).unwrap() Uri::from_parts(uri_parts).unwrap()
}; };
if window.location().pathname().unwrap() == "/" { log!(&url);
log!(&request_uri.to_string());
log!(&location().pathname().unwrap());
if location().pathname().unwrap() == "/" {
} else { } else {
spawn_local(async { spawn_local(async {
a(request_uri, url).await; if let Err(e) = fetch_resources(request_uri, url).await {
log!(e.to_string());
}
}); });
} }
} }
#[allow(clippy::future_not_send)] #[allow(clippy::future_not_send)]
async fn a(request_uri: Uri, url: String) -> Result<()> { async fn fetch_resources(request_uri: Uri, url: String) -> Result<()> {
match Request::get(&request_uri.to_string()).send().await { match Request::get(&request_uri.to_string()).send().await {
Ok(resp) if resp.status() == StatusCode::OK => { Ok(resp) if resp.status() == StatusCode::OK => {
let expires = resp let expires = Expiration::try_from(resp.headers()).map_or_else(
.headers() |_| "This item does not expire.".to_string(),
.get(EXPIRES.as_str()) |expires| expires.to_string(),
.ok() );
.flatten()
.as_deref()
.and_then(|v| Expiration::try_from(v).ok())
.as_ref()
.map_or_else(
|| "This item does not expire.".to_string(),
Expiration::to_string,
);
let data = { let data = {
let data_fut = resp let data_fut = resp
@ -75,44 +92,33 @@ async fn a(request_uri: Uri, url: String) -> Result<()> {
Uint8Array::new(&data).to_vec() Uint8Array::new(&data).to_vec()
}; };
let info = url let (key, nonce) = {
.split_once('#') let partial_parsed_url = url
.map(|(_, fragment)| PartialParsedUrl::from(fragment)) .split_once('#')
.unwrap_or_default(); .map(|(_, fragment)| PartialParsedUrl::from(fragment))
let key = info .unwrap_or_default();
.decryption_key let key = partial_parsed_url
.expect("missing key should be handled in the future"); .decryption_key
let nonce = info.nonce.expect("missing nonce be handled in the future"); .context("missing key should be handled in the future")?;
let nonce = partial_parsed_url
let result = decrypt(data, key, nonce, None); .nonce
.context("missing nonce be handled in the future")?;
let decrypted = match result { (key, nonce)
Ok(decrypted) => decrypted,
Err(err) => {
// log!("decryption error: {}", err);
// return Box::new(PasteError(err));
unimplemented!()
}
}; };
let db_open_req = window() let decrypted = decrypt(data, key, nonce, None)?;
.unwrap() let db_open_req = open_idb()?;
.indexed_db()
.unwrap()
.unwrap()
.open("omegaupload")
.unwrap();
// On success callback // On success callback
let on_success = Closure::once(Box::new(move |event: Event| { let on_success = Closure::once(Box::new(move |event: Event| {
let target: IdbOpenDbRequest = event.target().unwrap().dyn_into().unwrap(); let transaction: IdbObjectStore = as_idb_db(&event)
let db: IdbDatabase = target.result().unwrap().dyn_into().unwrap();
let transaction: IdbObjectStore = db
.transaction_with_str_and_mode("decrypted data", IdbTransactionMode::Readwrite) .transaction_with_str_and_mode("decrypted data", IdbTransactionMode::Readwrite)
.unwrap() .unwrap()
.object_store("decrypted data") .object_store("decrypted data")
.unwrap(); .unwrap();
log!(line!());
let decrypted_object = match &decrypted { let decrypted_object = match &decrypted {
DecryptedData::String(s) => IdbObject::new() DecryptedData::String(s) => IdbObject::new()
.string() .string()
@ -146,37 +152,49 @@ async fn a(request_uri: Uri, url: String) -> Result<()> {
.data(blob), .data(blob),
}; };
let db_entry = Object::from_entries(decrypted_object.as_ref()).unwrap(); log!(line!());
transaction let put_action = transaction
.put_with_key( .put_with_key(
&db_entry, &Object::from(decrypted_object),
&JsString::from(window().unwrap().location().pathname().unwrap()), &JsString::from(location().pathname().unwrap()),
) )
.unwrap() .unwrap();
.set_onsuccess(Some( put_action.set_onsuccess(Some(
Closure::once(Box::new(|| { Closure::wrap(Box::new(|| {
log!("success"); log!("success");
loadFromDb(); load_from_db();
}) as Box<dyn FnOnce()>) }) as Box<dyn Fn()>)
.into_js_value() .into_js_value()
.dyn_ref() .unchecked_ref(),
.unwrap(), ));
)); put_action.set_onerror(Some(
Closure::wrap(Box::new(|e| {
log!(line!());
log!(e);
}) as Box<dyn Fn(Event)>)
.into_js_value()
.unchecked_ref(),
));
}) as Box<dyn FnOnce(Event)>); }) as Box<dyn FnOnce(Event)>);
db_open_req.set_onsuccess(Some(on_success.into_js_value().dyn_ref().unwrap())); db_open_req.set_onsuccess(Some(on_success.into_js_value().unchecked_ref()));
db_open_req.set_onerror(Some(
// On upgrade callback Closure::wrap(Box::new(|e| {
log!(line!());
log!(e);
}) as Box<dyn Fn(Event)>)
.into_js_value()
.unchecked_ref(),
));
let on_upgrade = Closure::wrap(Box::new(move |event: Event| { let on_upgrade = Closure::wrap(Box::new(move |event: Event| {
let target: IdbOpenDbRequest = event.target().unwrap().dyn_into().unwrap(); let db = as_idb_db(&event);
let db: IdbDatabase = target.result().unwrap().dyn_into().unwrap(); let _ = db.create_object_store("decrypted data").unwrap();
let _obj_store = db.create_object_store("decrypted data").unwrap();
}) as Box<dyn FnMut(Event)>); }) as Box<dyn FnMut(Event)>);
db_open_req.set_onupgradeneeded(Some(on_upgrade.into_js_value().unchecked_ref()));
db_open_req.set_onupgradeneeded(Some(on_upgrade.into_js_value().dyn_ref().unwrap())); log!(&db_open_req);
} }
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => { Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
createNotFoundUi(); create_not_found_ui();
} }
Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => {} Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => {}
Ok(err) => {} Ok(err) => {}
@ -185,79 +203,3 @@ async fn a(request_uri: Uri, url: String) -> Result<()> {
Ok(()) Ok(())
} }
struct IdbObject<State>(Array, PhantomData<State>);
impl<State: IdbObjectState> IdbObject<State> {
fn add_tuple<NextState>(self, key: &str, value: &JsValue) -> IdbObject<NextState> {
let array = Array::new();
array.push(&JsString::from(key));
array.push(value);
self.0.push(&array);
IdbObject(self.0, PhantomData)
}
}
impl IdbObject<NeedsType> {
fn new() -> Self {
Self(Array::new(), PhantomData)
}
fn video(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("video"))
}
fn audio(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("audio"))
}
fn image(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("image"))
}
fn blob(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("blob"))
}
fn string(self) -> IdbObject<NeedsExpiration> {
self.add_tuple("type", &JsString::from("string"))
}
}
impl IdbObject<NeedsExpiration> {
fn expiration_text(self, expires: &str) -> IdbObject<NeedsData> {
self.add_tuple("expiration", &JsString::from(expires))
}
}
impl IdbObject<NeedsData> {
fn data(self, value: &JsValue) -> IdbObject<Ready> {
self.add_tuple("data", value)
}
}
impl IdbObject<Ready> {
fn extra(self, key: &str, value: impl Into<JsValue>) -> Self {
self.add_tuple(key, &value.into())
}
}
impl AsRef<JsValue> for IdbObject<Ready> {
fn as_ref(&self) -> &JsValue {
self.0.as_ref()
}
}
trait IdbObjectState {}
enum NeedsType {}
impl IdbObjectState for NeedsType {}
enum NeedsExpiration {}
impl IdbObjectState for NeedsExpiration {}
enum NeedsData {}
impl IdbObjectState for NeedsData {}
enum Ready {}
impl IdbObjectState for Ready {}

13
web/src/util.rs Normal file
View file

@ -0,0 +1,13 @@
use js_sys::Function;
use wasm_bindgen::closure::WasmClosure;
use wasm_bindgen::prelude::Closure;
use wasm_bindgen::JsCast;
use web_sys::{Event, IdbDatabase, IdbOpenDbRequest};
/// # Panics
///
/// This will panic if event is not an event from the IDB API.
pub fn as_idb_db(event: &Event) -> IdbDatabase {
let target: IdbOpenDbRequest = event.target().map(JsCast::unchecked_into).unwrap();
target.result().map(JsCast::unchecked_into).unwrap()
}