mirror of
https://gitlab.com/veilid/veilid.git
synced 2025-02-03 10:00:22 -05:00
test work
This commit is contained in:
parent
5b0bfcef48
commit
5f8b440d84
BIN
veilid-core/0qrl21wlR1IjCA13sIOjw7Byk8ruCfPcnVBRxMLHCrk
Normal file
BIN
veilid-core/0qrl21wlR1IjCA13sIOjw7Byk8ruCfPcnVBRxMLHCrk
Normal file
Binary file not shown.
BIN
veilid-core/BrpdAUI1bcti0_vJinSu42N9w-vNQRfWBGaR8DLGnAY
Normal file
BIN
veilid-core/BrpdAUI1bcti0_vJinSu42N9w-vNQRfWBGaR8DLGnAY
Normal file
Binary file not shown.
BIN
veilid-core/bazGk6a59NnYJyQ-s5bQu3GogeYnRpkHJss5vba1khA
Normal file
BIN
veilid-core/bazGk6a59NnYJyQ-s5bQu3GogeYnRpkHJss5vba1khA
Normal file
Binary file not shown.
@ -5,11 +5,7 @@ fn fake_routing_table() -> routing_table::RoutingTable {
|
|||||||
let block_store = BlockStore::new(veilid_config.clone());
|
let block_store = BlockStore::new(veilid_config.clone());
|
||||||
let protected_store = ProtectedStore::new(veilid_config.clone());
|
let protected_store = ProtectedStore::new(veilid_config.clone());
|
||||||
let table_store = TableStore::new(veilid_config.clone(), protected_store.clone());
|
let table_store = TableStore::new(veilid_config.clone(), protected_store.clone());
|
||||||
let crypto = Crypto::new(
|
let crypto = Crypto::new(veilid_config.clone(), table_store.clone());
|
||||||
veilid_config.clone(),
|
|
||||||
table_store.clone(),
|
|
||||||
protected_store.clone(),
|
|
||||||
);
|
|
||||||
let storage_manager = storage_manager::StorageManager::new(
|
let storage_manager = storage_manager::StorageManager::new(
|
||||||
veilid_config.clone(),
|
veilid_config.clone(),
|
||||||
crypto.clone(),
|
crypto.clone(),
|
||||||
|
@ -5,6 +5,8 @@ mod table_store;
|
|||||||
pub use table_db::*;
|
pub use table_db::*;
|
||||||
pub use table_store::*;
|
pub use table_store::*;
|
||||||
|
|
||||||
|
pub mod tests;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
mod wasm;
|
mod wasm;
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
@ -163,15 +163,11 @@ impl TableStore {
|
|||||||
self.flush().await;
|
self.flush().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_device_encryption_key(&self) -> EyreResult<Option<TypedSharedSecret>> {
|
pub fn maybe_unprotect_device_encryption_key(
|
||||||
let dek_bytes: Option<Vec<u8>> = self
|
&self,
|
||||||
.protected_store
|
dek_bytes: &[u8],
|
||||||
.load_user_secret("device_encryption_key")
|
device_encryption_key_password: &str,
|
||||||
.await?;
|
) -> EyreResult<TypedSharedSecret> {
|
||||||
let Some(dek_bytes) = dek_bytes else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure the key is at least as long as necessary if unencrypted
|
// Ensure the key is at least as long as necessary if unencrypted
|
||||||
if dek_bytes.len() < (4 + SHARED_SECRET_LENGTH) {
|
if dek_bytes.len() < (4 + SHARED_SECRET_LENGTH) {
|
||||||
bail!("device encryption key is not valid");
|
bail!("device encryption key is not valid");
|
||||||
@ -184,11 +180,6 @@ impl TableStore {
|
|||||||
bail!("unsupported cryptosystem");
|
bail!("unsupported cryptosystem");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Decrypt encryption key if we have it
|
|
||||||
let device_encryption_key_password = {
|
|
||||||
let c = self.config.get();
|
|
||||||
c.protected_store.device_encryption_key_password.clone()
|
|
||||||
};
|
|
||||||
if !device_encryption_key_password.is_empty() {
|
if !device_encryption_key_password.is_empty() {
|
||||||
if dek_bytes.len()
|
if dek_bytes.len()
|
||||||
!= (4 + SHARED_SECRET_LENGTH + vcrypto.aead_overhead() + NONCE_LENGTH)
|
!= (4 + SHARED_SECRET_LENGTH + vcrypto.aead_overhead() + NONCE_LENGTH)
|
||||||
@ -209,28 +200,126 @@ impl TableStore {
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.wrap_err("failed to decrypt device encryption key")?;
|
.wrap_err("failed to decrypt device encryption key")?;
|
||||||
return Ok(Some(TypedSharedSecret::new(
|
return Ok(TypedSharedSecret::new(
|
||||||
kind,
|
kind,
|
||||||
SharedSecret::try_from(unprotected_key.as_slice())
|
SharedSecret::try_from(unprotected_key.as_slice())
|
||||||
.wrap_err("invalid shared secret")?,
|
.wrap_err("invalid shared secret")?,
|
||||||
)));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(TypedSharedSecret::new(
|
Ok(TypedSharedSecret::new(
|
||||||
kind,
|
kind,
|
||||||
SharedSecret::try_from(&dek_bytes[4..])?,
|
SharedSecret::try_from(&dek_bytes[4..])?,
|
||||||
)))
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn maybe_protect_device_encryption_key(
|
||||||
|
&self,
|
||||||
|
dek: TypedSharedSecret,
|
||||||
|
device_encryption_key_password: &str,
|
||||||
|
) -> EyreResult<Vec<u8>> {
|
||||||
|
// Check if we are to protect the key
|
||||||
|
if device_encryption_key_password.is_empty() {
|
||||||
|
// Return the unprotected key bytes
|
||||||
|
let mut out = Vec::with_capacity(4 + SHARED_SECRET_LENGTH);
|
||||||
|
out.extend_from_slice(&dek.kind.0);
|
||||||
|
out.extend_from_slice(&dek.value.bytes);
|
||||||
|
return Ok(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cryptosystem
|
||||||
|
let crypto = self.inner.lock().crypto.as_ref().unwrap().clone();
|
||||||
|
let Some(vcrypto) = crypto.get(dek.kind) else {
|
||||||
|
bail!("unsupported cryptosystem");
|
||||||
|
};
|
||||||
|
|
||||||
|
let nonce = vcrypto.random_nonce();
|
||||||
|
let shared_secret = vcrypto
|
||||||
|
.derive_shared_secret(device_encryption_key_password.as_bytes(), &nonce.bytes)
|
||||||
|
.wrap_err("failed to derive shared secret")?;
|
||||||
|
let mut protected_key = vcrypto
|
||||||
|
.encrypt_aead(
|
||||||
|
&dek.value.bytes,
|
||||||
|
&Nonce::try_from(nonce).wrap_err("invalid nonce")?,
|
||||||
|
&shared_secret,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.wrap_err("failed to decrypt device encryption key")?;
|
||||||
|
let mut out =
|
||||||
|
Vec::with_capacity(4 + SHARED_SECRET_LENGTH + vcrypto.aead_overhead() + NONCE_LENGTH);
|
||||||
|
out.extend_from_slice(&dek.kind.0);
|
||||||
|
out.append(&mut protected_key);
|
||||||
|
out.extend_from_slice(&nonce.bytes);
|
||||||
|
assert!(out.len() == 4 + SHARED_SECRET_LENGTH + vcrypto.aead_overhead() + NONCE_LENGTH);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_device_encryption_key(&self) -> EyreResult<Option<TypedSharedSecret>> {
|
||||||
|
let dek_bytes: Option<Vec<u8>> = self
|
||||||
|
.protected_store
|
||||||
|
.load_user_secret("device_encryption_key")
|
||||||
|
.await?;
|
||||||
|
let Some(dek_bytes) = dek_bytes else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get device encryption key protection password if we have it
|
||||||
|
let device_encryption_key_password = {
|
||||||
|
let c = self.config.get();
|
||||||
|
c.protected_store.device_encryption_key_password.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(self.maybe_unprotect_device_encryption_key(
|
||||||
|
&dek_bytes,
|
||||||
|
&device_encryption_key_password,
|
||||||
|
)?))
|
||||||
}
|
}
|
||||||
async fn save_device_encryption_key(
|
async fn save_device_encryption_key(
|
||||||
&self,
|
&self,
|
||||||
device_encryption_key: Option<TypedSharedSecret>,
|
device_encryption_key: Option<TypedSharedSecret>,
|
||||||
) -> EyreResult<()> {
|
) -> EyreResult<()> {
|
||||||
// Save the new device encryption key
|
let Some(device_encryption_key) = device_encryption_key else {
|
||||||
self.protected_store
|
// Remove the device encryption key
|
||||||
.save_user_secret_json("device_encryption_key", &device_encryption_key)
|
let existed = self
|
||||||
|
.protected_store
|
||||||
|
.remove_user_secret("device_encryption_key")
|
||||||
.await?;
|
.await?;
|
||||||
|
trace!("removed device encryption key. existed: {}", existed);
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
xxxx
|
// Get new device encryption key protection password if we are changing it
|
||||||
|
let new_device_encryption_key_password = {
|
||||||
|
let c = self.config.get();
|
||||||
|
c.protected_store.new_device_encryption_key_password.clone()
|
||||||
|
};
|
||||||
|
let device_encryption_key_password =
|
||||||
|
if let Some(new_device_encryption_key_password) = new_device_encryption_key_password {
|
||||||
|
// Change password
|
||||||
|
self.config
|
||||||
|
.with_mut(|c| {
|
||||||
|
c.protected_store.device_encryption_key_password =
|
||||||
|
new_device_encryption_key_password.clone();
|
||||||
|
Ok(new_device_encryption_key_password)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
} else {
|
||||||
|
// Get device encryption key protection password if we have it
|
||||||
|
let c = self.config.get();
|
||||||
|
c.protected_store.device_encryption_key_password.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let dek_bytes = self.maybe_protect_device_encryption_key(
|
||||||
|
device_encryption_key,
|
||||||
|
&device_encryption_key_password,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Save the new device encryption key
|
||||||
|
let existed = self
|
||||||
|
.protected_store
|
||||||
|
.save_user_secret("device_encryption_key", &dek_bytes)
|
||||||
|
.await?;
|
||||||
|
trace!("saving device encryption key. existed: {}", existed);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
veilid-core/src/table_store/tests/mod.rs
Normal file
1
veilid-core/src/table_store/tests/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod test_table_store;
|
@ -1,4 +1,4 @@
|
|||||||
use super::test_veilid_config::*;
|
use crate::tests::test_veilid_config::*;
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
async fn startup() -> VeilidAPI {
|
async fn startup() -> VeilidAPI {
|
||||||
@ -208,6 +208,56 @@ pub async fn test_json(vcrypto: CryptoSystemVersion, ts: TableStore) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn test_protect_unprotect(vcrypto: CryptoSystemVersion, ts: TableStore) {
|
||||||
|
trace!("test_protect_unprotect");
|
||||||
|
|
||||||
|
let dek1 = TypedSharedSecret::new(
|
||||||
|
vcrypto.kind(),
|
||||||
|
SharedSecret::new([
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
let dek2 = TypedSharedSecret::new(
|
||||||
|
vcrypto.kind(),
|
||||||
|
SharedSecret::new([
|
||||||
|
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0xFF,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
let dek3 = TypedSharedSecret::new(
|
||||||
|
vcrypto.kind(),
|
||||||
|
SharedSecret::new([0x80u8; SHARED_SECRET_LENGTH]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let deks = [dek1, dek2, dek3];
|
||||||
|
let passwords = ["", " ", " ", "12345678", "|/\\!@#$%^&*()_+", "Ⓜ️", "🔥🔥♾️"];
|
||||||
|
|
||||||
|
for dek in deks {
|
||||||
|
for password in passwords {
|
||||||
|
let dek_bytes = ts
|
||||||
|
.maybe_protect_device_encryption_key(dek, password)
|
||||||
|
.expect(&format!("protect: dek: '{}' pw: '{}'", dek, password));
|
||||||
|
let unprotected = ts
|
||||||
|
.maybe_unprotect_device_encryption_key(&dek_bytes, password)
|
||||||
|
.expect(&format!("unprotect: dek: '{}' pw: '{}'", dek, password));
|
||||||
|
assert_eq!(unprotected, dek);
|
||||||
|
let invalid_password = format!("{}x", password);
|
||||||
|
let _ = ts
|
||||||
|
.maybe_unprotect_device_encryption_key(&dek_bytes, &invalid_password)
|
||||||
|
.expect_err(&format!(
|
||||||
|
"invalid_password: dek: '{}' pw: '{}'",
|
||||||
|
dek, &invalid_password
|
||||||
|
));
|
||||||
|
if password != "" {
|
||||||
|
let _ = ts
|
||||||
|
.maybe_unprotect_device_encryption_key(&dek_bytes, "")
|
||||||
|
.expect_err(&format!("empty_password: dek: '{}' pw: ''", dek));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn test_all() {
|
pub async fn test_all() {
|
||||||
let api = startup().await;
|
let api = startup().await;
|
||||||
let crypto = api.crypto().unwrap();
|
let crypto = api.crypto().unwrap();
|
||||||
@ -215,6 +265,7 @@ pub async fn test_all() {
|
|||||||
|
|
||||||
for ck in VALID_CRYPTO_KINDS {
|
for ck in VALID_CRYPTO_KINDS {
|
||||||
let vcrypto = crypto.get(ck).unwrap();
|
let vcrypto = crypto.get(ck).unwrap();
|
||||||
|
test_protect_unprotect(vcrypto.clone(), ts.clone()).await;
|
||||||
test_delete_open_delete(ts.clone()).await;
|
test_delete_open_delete(ts.clone()).await;
|
||||||
test_store_delete_load(ts.clone()).await;
|
test_store_delete_load(ts.clone()).await;
|
||||||
test_rkyv(vcrypto.clone(), ts.clone()).await;
|
test_rkyv(vcrypto.clone(), ts.clone()).await;
|
@ -1,5 +1,4 @@
|
|||||||
pub mod test_host_interface;
|
pub mod test_host_interface;
|
||||||
pub mod test_protected_store;
|
pub mod test_protected_store;
|
||||||
pub mod test_table_store;
|
|
||||||
pub mod test_veilid_config;
|
pub mod test_veilid_config;
|
||||||
pub mod test_veilid_core;
|
pub mod test_veilid_core;
|
||||||
|
@ -166,7 +166,7 @@ pub fn setup_veilid_core() -> (UpdateCallback, ConfigCallback) {
|
|||||||
|
|
||||||
fn config_callback(key: String) -> ConfigCallbackReturn {
|
fn config_callback(key: String) -> ConfigCallbackReturn {
|
||||||
match key.as_str() {
|
match key.as_str() {
|
||||||
"program_name" => Ok(Box::new(String::from("Veilid"))),
|
"program_name" => Ok(Box::new(String::from("VeilidCoreTests"))),
|
||||||
"namespace" => Ok(Box::new(String::from(""))),
|
"namespace" => Ok(Box::new(String::from(""))),
|
||||||
"capabilities.protocol_udp" => Ok(Box::new(true)),
|
"capabilities.protocol_udp" => Ok(Box::new(true)),
|
||||||
"capabilities.protocol_connect_tcp" => Ok(Box::new(true)),
|
"capabilities.protocol_connect_tcp" => Ok(Box::new(true)),
|
||||||
@ -176,13 +176,17 @@ fn config_callback(key: String) -> ConfigCallbackReturn {
|
|||||||
"capabilities.protocol_connect_wss" => Ok(Box::new(true)),
|
"capabilities.protocol_connect_wss" => Ok(Box::new(true)),
|
||||||
"capabilities.protocol_accept_wss" => Ok(Box::new(true)),
|
"capabilities.protocol_accept_wss" => Ok(Box::new(true)),
|
||||||
"table_store.directory" => Ok(Box::new(get_table_store_path())),
|
"table_store.directory" => Ok(Box::new(get_table_store_path())),
|
||||||
"table_store.delete" => Ok(Box::new(false)),
|
"table_store.delete" => Ok(Box::new(true)),
|
||||||
"block_store.directory" => Ok(Box::new(get_block_store_path())),
|
"block_store.directory" => Ok(Box::new(get_block_store_path())),
|
||||||
"block_store.delete" => Ok(Box::new(false)),
|
"block_store.delete" => Ok(Box::new(true)),
|
||||||
"protected_store.allow_insecure_fallback" => Ok(Box::new(true)),
|
"protected_store.allow_insecure_fallback" => Ok(Box::new(true)),
|
||||||
"protected_store.always_use_insecure_storage" => Ok(Box::new(false)),
|
"protected_store.always_use_insecure_storage" => Ok(Box::new(false)),
|
||||||
"protected_store.directory" => Ok(Box::new(get_protected_store_path())),
|
"protected_store.directory" => Ok(Box::new(get_protected_store_path())),
|
||||||
"protected_store.delete" => Ok(Box::new(false)),
|
"protected_store.delete" => Ok(Box::new(true)),
|
||||||
|
"protected_store.device_encryption_key_password" => Ok(Box::new("".to_owned())),
|
||||||
|
"protected_store.new_device_encryption_key_password" => {
|
||||||
|
Ok(Box::new(Option::<String>::None))
|
||||||
|
}
|
||||||
"network.connection_initial_timeout_ms" => Ok(Box::new(2_000u32)),
|
"network.connection_initial_timeout_ms" => Ok(Box::new(2_000u32)),
|
||||||
"network.connection_inactivity_timeout_ms" => Ok(Box::new(60_000u32)),
|
"network.connection_inactivity_timeout_ms" => Ok(Box::new(60_000u32)),
|
||||||
"network.max_connections_per_ip4" => Ok(Box::new(8u32)),
|
"network.max_connections_per_ip4" => Ok(Box::new(8u32)),
|
||||||
@ -302,13 +306,21 @@ pub async fn test_config() {
|
|||||||
assert_eq!(inner.capabilities.protocol_connect_wss, true);
|
assert_eq!(inner.capabilities.protocol_connect_wss, true);
|
||||||
assert_eq!(inner.capabilities.protocol_accept_wss, true);
|
assert_eq!(inner.capabilities.protocol_accept_wss, true);
|
||||||
assert_eq!(inner.table_store.directory, get_table_store_path());
|
assert_eq!(inner.table_store.directory, get_table_store_path());
|
||||||
assert_eq!(inner.table_store.delete, false);
|
assert_eq!(inner.table_store.delete, true);
|
||||||
assert_eq!(inner.block_store.directory, get_block_store_path());
|
assert_eq!(inner.block_store.directory, get_block_store_path());
|
||||||
assert_eq!(inner.block_store.delete, false);
|
assert_eq!(inner.block_store.delete, true);
|
||||||
assert_eq!(inner.protected_store.allow_insecure_fallback, true);
|
assert_eq!(inner.protected_store.allow_insecure_fallback, true);
|
||||||
assert_eq!(inner.protected_store.always_use_insecure_storage, false);
|
assert_eq!(inner.protected_store.always_use_insecure_storage, false);
|
||||||
assert_eq!(inner.protected_store.directory, get_protected_store_path());
|
assert_eq!(inner.protected_store.directory, get_protected_store_path());
|
||||||
assert_eq!(inner.protected_store.delete, false);
|
assert_eq!(inner.protected_store.delete, true);
|
||||||
|
assert_eq!(
|
||||||
|
inner.protected_store.device_encryption_key_password,
|
||||||
|
"".to_owned()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
inner.protected_store.new_device_encryption_key_password,
|
||||||
|
Option::<String>::None
|
||||||
|
);
|
||||||
assert_eq!(inner.network.connection_initial_timeout_ms, 2_000u32);
|
assert_eq!(inner.network.connection_initial_timeout_ms, 2_000u32);
|
||||||
assert_eq!(inner.network.connection_inactivity_timeout_ms, 60_000u32);
|
assert_eq!(inner.network.connection_inactivity_timeout_ms, 60_000u32);
|
||||||
assert_eq!(inner.network.max_connections_per_ip4, 8u32);
|
assert_eq!(inner.network.max_connections_per_ip4, 8u32);
|
||||||
|
@ -13,4 +13,5 @@ pub use common::*;
|
|||||||
pub use crypto::tests::*;
|
pub use crypto::tests::*;
|
||||||
pub use network_manager::tests::*;
|
pub use network_manager::tests::*;
|
||||||
pub use routing_table::tests::*;
|
pub use routing_table::tests::*;
|
||||||
|
pub use table_store::tests::*;
|
||||||
pub use veilid_api::tests::*;
|
pub use veilid_api::tests::*;
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
use crate::crypto::tests::*;
|
use crate::crypto::tests::*;
|
||||||
use crate::network_manager::tests::*;
|
use crate::network_manager::tests::*;
|
||||||
use crate::routing_table;
|
use crate::routing_table;
|
||||||
|
use crate::table_store::tests::*;
|
||||||
use crate::tests::common::*;
|
use crate::tests::common::*;
|
||||||
use crate::veilid_api;
|
use crate::veilid_api;
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
@ -734,6 +734,9 @@ impl VeilidConfig {
|
|||||||
|
|
||||||
// Remove secrets
|
// Remove secrets
|
||||||
safe_cfg.network.routing_table.node_id_secret = TypedSecretSet::new();
|
safe_cfg.network.routing_table.node_id_secret = TypedSecretSet::new();
|
||||||
|
safe_cfg.protected_store.device_encryption_key_password = "".to_owned();
|
||||||
|
safe_cfg.protected_store.new_device_encryption_key_password = None;
|
||||||
|
|
||||||
|
|
||||||
safe_cfg
|
safe_cfg
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
//! Test suite for the Web and headless browsers.
|
//! Test suite for the Web and headless browsers.
|
||||||
|
|
||||||
|
//XXXXXXXXXXXXXXX
|
||||||
|
//XXX DOES NOT WORK.
|
||||||
|
|
||||||
#![cfg(target_arch = "wasm32")]
|
#![cfg(target_arch = "wasm32")]
|
||||||
|
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user