mirror of
https://gitlab.com/fabinfra/fabaccess/bffh.git
synced 2024-11-24 23:57:57 +01:00
Commit current state
This commit is contained in:
parent
d7a66e2149
commit
4778c7a8d3
44
Cargo.lock
generated
44
Cargo.lock
generated
@ -836,7 +836,6 @@ dependencies = [
|
|||||||
"rsasl",
|
"rsasl",
|
||||||
"rust-argon2",
|
"rust-argon2",
|
||||||
"rustls",
|
"rustls",
|
||||||
"sdk",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_dhall",
|
"serde_dhall",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -878,6 +877,13 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dummy"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"sdk",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
@ -2104,8 +2110,19 @@ dependencies = [
|
|||||||
name = "sdk"
|
name = "sdk"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"diflouroborane",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"sdk-proc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sdk-proc"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"trybuild",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2410,6 +2427,15 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.29"
|
version = "0.1.29"
|
||||||
@ -2499,6 +2525,20 @@ dependencies = [
|
|||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trybuild"
|
||||||
|
version = "1.0.53"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d664de8ea7e531ad4c0f5a834f20b8cb2b8e6dfe88d05796ee7887518ed67b9"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"lazy_static",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"termcolor",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typed-arena"
|
name = "typed-arena"
|
||||||
version = "1.7.0"
|
version = "1.7.0"
|
||||||
|
@ -25,8 +25,6 @@ path = "bin/bffhd/main.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libc = "0.2.101"
|
libc = "0.2.101"
|
||||||
|
|
||||||
sdk = { path = "modules/sdk", default-features = false }
|
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
uuid = { version = "0.8.2", features = ["serde", "v4"] }
|
uuid = { version = "0.8.2", features = ["serde", "v4"] }
|
||||||
async-trait = "0.1.51"
|
async-trait = "0.1.51"
|
||||||
|
@ -6,18 +6,48 @@ use async_oneshot as oneshot;
|
|||||||
use futures_signals::signal::Signal;
|
use futures_signals::signal::Signal;
|
||||||
use futures_util::future::BoxFuture;
|
use futures_util::future::BoxFuture;
|
||||||
use smol::future::FutureExt;
|
use smol::future::FutureExt;
|
||||||
use sdk::initiators::{Initiator, InitiatorError, UpdateError, UpdateSink, UserID, ResourceID};
|
|
||||||
use crate::resource::{Error, Update};
|
use crate::resource::{Error, Update};
|
||||||
|
use crate::resource::claim::{ResourceID, UserID};
|
||||||
|
use crate::resource::state::State;
|
||||||
|
|
||||||
|
pub enum UpdateError {
|
||||||
|
/// We're not connected to anything anymore. You can't do anything about this error and the
|
||||||
|
/// only reason why you even get it is because your future was called a last time before
|
||||||
|
/// being shelved so best way to handle this error is to just return from your loop entirely,
|
||||||
|
/// cleaning up any state that doesn't survive a freeze.
|
||||||
|
Closed,
|
||||||
|
|
||||||
|
Denied,
|
||||||
|
|
||||||
|
Other(Box<dyn std::error::Error + Send>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait InitiatorError: std::error::Error + Send {
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Initiator {
|
||||||
|
fn start_for(&mut self, machine: ResourceID)
|
||||||
|
-> BoxFuture<'static, Result<(), Box<dyn InitiatorError>>>;
|
||||||
|
|
||||||
|
fn run(&mut self, request: &mut UpdateSink)
|
||||||
|
-> BoxFuture<'static, Result<(), Box<dyn InitiatorError>>>;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct BffhUpdateSink {
|
pub struct UpdateSink {
|
||||||
tx: channel::Sender<(Option<UserID>, sdk::initiators::State)>,
|
tx: channel::Sender<(Option<UserID>, State)>,
|
||||||
rx: channel::Receiver<Result<(), Error>>,
|
rx: channel::Receiver<Result<(), Error>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
impl UpdateSink {
|
||||||
impl UpdateSink for BffhUpdateSink {
|
fn new(tx: channel::Sender<(Option<UserID>, State)>,
|
||||||
async fn send(&mut self, userid: Option<UserID>, state: sdk::initiators::State)
|
rx: channel::Receiver<Result<(), Error>>)
|
||||||
|
-> Self
|
||||||
|
{
|
||||||
|
Self { tx, rx }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send(&mut self, userid: Option<UserID>, state: State)
|
||||||
-> Result<(), UpdateError>
|
-> Result<(), UpdateError>
|
||||||
{
|
{
|
||||||
if let Err(_e) = self.tx.send((userid, state)).await {
|
if let Err(_e) = self.tx.send((userid, state)).await {
|
||||||
@ -34,15 +64,6 @@ impl UpdateSink for BffhUpdateSink {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BffhUpdateSink {
|
|
||||||
fn new(tx: channel::Sender<(Option<UserID>, sdk::initiators::State)>,
|
|
||||||
rx: channel::Receiver<Result<(), Error>>)
|
|
||||||
-> Self
|
|
||||||
{
|
|
||||||
Self { tx, rx }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Resource;
|
struct Resource;
|
||||||
pub struct InitiatorDriver<S, I: Initiator> {
|
pub struct InitiatorDriver<S, I: Initiator> {
|
||||||
resource_signal: S,
|
resource_signal: S,
|
||||||
@ -51,8 +72,8 @@ pub struct InitiatorDriver<S, I: Initiator> {
|
|||||||
|
|
||||||
initiator: I,
|
initiator: I,
|
||||||
initiator_future: Option<BoxFuture<'static, Result<(), Box<dyn InitiatorError>>>>,
|
initiator_future: Option<BoxFuture<'static, Result<(), Box<dyn InitiatorError>>>>,
|
||||||
update_sink: BffhUpdateSink,
|
update_sink: UpdateSink,
|
||||||
initiator_req_rx: channel::Receiver<(Option<UserID>, sdk::initiators::State)>,
|
initiator_req_rx: channel::Receiver<(Option<UserID>, State)>,
|
||||||
initiator_reply_tx: channel::Sender<Result<(), Error>>,
|
initiator_reply_tx: channel::Sender<Result<(), Error>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +86,7 @@ impl<S: Signal<Item=ResourceSink>, I: Initiator> InitiatorDriver<S, I> {
|
|||||||
pub fn new(resource_signal: S, initiator: I) -> Self {
|
pub fn new(resource_signal: S, initiator: I) -> Self {
|
||||||
let (initiator_reply_tx, initiator_reply_rx) = channel::bounded(1);
|
let (initiator_reply_tx, initiator_reply_rx) = channel::bounded(1);
|
||||||
let (initiator_req_tx, initiator_req_rx) = async_channel::bounded(1);
|
let (initiator_req_tx, initiator_req_rx) = async_channel::bounded(1);
|
||||||
let update_sink = BffhUpdateSink::new(initiator_req_tx, initiator_reply_rx);
|
let update_sink = UpdateSink::new(initiator_req_tx, initiator_reply_rx);
|
||||||
Self {
|
Self {
|
||||||
resource: None,
|
resource: None,
|
||||||
resource_signal,
|
resource_signal,
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use lmdb::{RwTransaction, Transaction};
|
||||||
use crate::db::{RawDB, DB, AllocAdapter, Environment, Result};
|
use crate::db::{RawDB, DB, AllocAdapter, Environment, Result};
|
||||||
use crate::db::{DatabaseFlags, LMDBorrow, RoTransaction, WriteFlags, };
|
use crate::db::{DatabaseFlags, LMDBorrow, RoTransaction, WriteFlags, };
|
||||||
use super::User;
|
use super::User;
|
||||||
@ -62,3 +64,59 @@ impl UserDB {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct UserIndex {
|
||||||
|
env: Arc<Environment>,
|
||||||
|
usernames: RawDB,
|
||||||
|
roles: RawDB,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserIndex {
|
||||||
|
pub fn update(&self, old: &User, new: &User) -> Result<()> {
|
||||||
|
assert_eq!(old.id, new.id);
|
||||||
|
let mut txn = self.env.begin_rw_txn()?;
|
||||||
|
if old.username != new.username {
|
||||||
|
self.update_username(&mut txn, new.id, &old.username, &new.username)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut to_remove: HashSet<&String> = old.roles.iter().collect();
|
||||||
|
let mut to_add: HashSet<&String> = HashSet::new();
|
||||||
|
for role in new.roles.iter() {
|
||||||
|
// If a role wasn't found in the old ones it's a new one that's being added
|
||||||
|
if !to_remove.remove(role) {
|
||||||
|
to_add.insert(role);
|
||||||
|
}
|
||||||
|
// Otherwise it's in both sets so we just ignore it.
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_roles(&mut txn, new.id, to_remove, to_add)?;
|
||||||
|
txn.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_username(&self, txn: &mut RwTransaction, uid: u128, old: &String, new: &String)
|
||||||
|
-> Result<()>
|
||||||
|
{
|
||||||
|
let flags = WriteFlags::empty();
|
||||||
|
self.usernames.del(txn, &old.as_bytes(), Some(&uid.to_ne_bytes()))?;
|
||||||
|
self.usernames.put(txn, &new.as_bytes(), &uid.to_ne_bytes(), flags)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_roles(&self,
|
||||||
|
txn: &mut RwTransaction,
|
||||||
|
uid: u128,
|
||||||
|
remove: HashSet<&String>,
|
||||||
|
add: HashSet<&String>
|
||||||
|
) -> Result<()>
|
||||||
|
{
|
||||||
|
let flags = WriteFlags::empty();
|
||||||
|
for role in remove.iter() {
|
||||||
|
self.roles.del(txn, &role.as_bytes(), Some(&uid.to_ne_bytes()))?;
|
||||||
|
}
|
||||||
|
for role in add.iter() {
|
||||||
|
self.roles.put(txn, &role.as_bytes(), &uid.to_ne_bytes(), flags)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||||
|
* Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan.
|
||||||
|
* Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna.
|
||||||
|
* Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus.
|
||||||
|
* Vestibulum commodo. Ut rhoncus gravida arcu.
|
||||||
|
*/
|
||||||
|
|
||||||
use rkyv::{Archive, Serialize, Deserialize};
|
use rkyv::{Archive, Serialize, Deserialize};
|
||||||
|
|
||||||
use capnp::capability::Promise;
|
use capnp::capability::Promise;
|
||||||
@ -26,7 +34,9 @@ pub struct User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
|
pub fn new(id: u128, username: String, roles: Vec<String>) -> Self {
|
||||||
|
User { id, username, roles }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl info::Server for User {
|
impl info::Server for User {
|
||||||
|
@ -6,5 +6,6 @@ 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]
|
||||||
async-trait = "0.1.51"
|
sdk-proc = { path = "sdk_proc" }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
diflouroborane = { path = "../.." }
|
87
modules/sdk/sdk_proc/src/lib.rs
Normal file
87
modules/sdk/sdk_proc/src/lib.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{braced, parse_macro_input, Field, Ident, Token, Visibility, Type};
|
||||||
|
use syn::parse::{Parse, ParseStream};
|
||||||
|
use syn::punctuated::Punctuated;
|
||||||
|
use syn::token::Brace;
|
||||||
|
|
||||||
|
mod keywords {
|
||||||
|
syn::custom_keyword!(initiator);
|
||||||
|
syn::custom_keyword!(actor);
|
||||||
|
syn::custom_keyword!(sensor);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ModuleAttrs {
|
||||||
|
Nothing,
|
||||||
|
Initiator,
|
||||||
|
Actor,
|
||||||
|
Sensor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for ModuleAttrs {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
if input.is_empty() {
|
||||||
|
Ok(ModuleAttrs::Nothing)
|
||||||
|
} else {
|
||||||
|
let lookahead = input.lookahead1();
|
||||||
|
if lookahead.peek(keywords::initiator) {
|
||||||
|
Ok(ModuleAttrs::Initiator)
|
||||||
|
} else if lookahead.peek(keywords::actor) {
|
||||||
|
Ok(ModuleAttrs::Actor)
|
||||||
|
} else if lookahead.peek(keywords::sensor) {
|
||||||
|
Ok(ModuleAttrs::Sensor)
|
||||||
|
} else {
|
||||||
|
Err(input.error("Module type must be empty or one of \"initiator\", \"actor\", or \
|
||||||
|
\"sensor\""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ModuleInput {
|
||||||
|
pub ident: Ident,
|
||||||
|
pub fields: Punctuated<Field, Token![,]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for ModuleInput {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
let lookahead = input.lookahead1();
|
||||||
|
if lookahead.peek(Token![pub]) {
|
||||||
|
let _vis: Visibility = input.parse()?;
|
||||||
|
}
|
||||||
|
if input.parse::<Token![struct]>().is_err() {
|
||||||
|
return Err(input.error("Modules must be structs"));
|
||||||
|
}
|
||||||
|
let ident = input.parse::<Ident>()?;
|
||||||
|
|
||||||
|
let lookahead = input.lookahead1();
|
||||||
|
if !lookahead.peek(Brace) {
|
||||||
|
return Err(input.error("Modules can't be unit structs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let content;
|
||||||
|
braced!(content in input);
|
||||||
|
Ok(Self {
|
||||||
|
ident,
|
||||||
|
fields: content.parse_terminated(Field::parse_named)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn module(attr: TokenStream, tokens: TokenStream) -> TokenStream {
|
||||||
|
let attrs = parse_macro_input!(attr as ModuleAttrs);
|
||||||
|
let item = parse_macro_input!(tokens as ModuleInput);
|
||||||
|
|
||||||
|
let output = {
|
||||||
|
let ident = item.ident;
|
||||||
|
let fields = item.fields.iter();
|
||||||
|
quote! {
|
||||||
|
pub struct #ident {
|
||||||
|
#(#fields),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
output.into()
|
||||||
|
}
|
10
modules/sdk/sdk_proc/tests/01-parse-struct.rs
Normal file
10
modules/sdk/sdk_proc/tests/01-parse-struct.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Check if the proc macro for modules exists and is correctly imported from top level
|
||||||
|
|
||||||
|
use sdk_proc::module;
|
||||||
|
|
||||||
|
#[module]
|
||||||
|
pub struct Module {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn main() {}
|
11
modules/sdk/sdk_proc/tests/02-not-or-bad-struct.rs
Normal file
11
modules/sdk/sdk_proc/tests/02-not-or-bad-struct.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#[sdk_proc::module]
|
||||||
|
enum EnumModule {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sdk_proc::module]
|
||||||
|
struct UnitStructModule;
|
||||||
|
|
||||||
|
fn main() {}
|
@ -1,35 +1,10 @@
|
|||||||
use async_trait::async_trait;
|
pub use diflouroborane::{
|
||||||
use futures_util::future::BoxFuture;
|
initiators::{
|
||||||
|
UpdateSink,
|
||||||
|
UpdateError,
|
||||||
|
|
||||||
pub struct State;
|
Initiator,
|
||||||
pub struct UserID;
|
InitiatorError,
|
||||||
pub struct ResourceID;
|
},
|
||||||
pub struct Error;
|
resource::claim::ResourceID,
|
||||||
|
};
|
||||||
pub enum UpdateError {
|
|
||||||
/// We're not connected to anything anymore. You can't do anything about this error and the
|
|
||||||
/// only reason why you even get it is because your future was called a last time before
|
|
||||||
/// being shelved so best way to handle this error is to just return from your loop entirely,
|
|
||||||
/// cleaning up any state that doesn't survive a freeze.
|
|
||||||
Closed,
|
|
||||||
|
|
||||||
Denied,
|
|
||||||
|
|
||||||
Other(Box<dyn std::error::Error + Send>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait UpdateSink: Send {
|
|
||||||
async fn send(&mut self, userid: Option<UserID>, state: State) -> Result<(), UpdateError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait InitiatorError: std::error::Error + Send {
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Initiator {
|
|
||||||
fn start_for(&mut self, machine: ResourceID)
|
|
||||||
-> BoxFuture<'static, Result<(), Box<dyn InitiatorError>>>;
|
|
||||||
|
|
||||||
fn run(&mut self, request: &mut impl UpdateSink)
|
|
||||||
-> BoxFuture<'static, Result<(), Box<dyn InitiatorError>>>;
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,20 @@
|
|||||||
#[forbid(private_in_public)]
|
#[forbid(private_in_public)]
|
||||||
|
|
||||||
|
pub use sdk_proc::module;
|
||||||
|
|
||||||
|
pub use futures_util::future::BoxFuture;
|
||||||
pub mod initiators;
|
pub mod initiators;
|
||||||
|
|
||||||
|
pub const VERSION_STRING: &'static str = env!("CARGO_PKG_VERSION");
|
||||||
|
pub const VERSION_STRING_PARTS: (&'static str, &'static str, &'static str, &'static str) = (
|
||||||
|
env!("CARGO_PKG_VERSION_MAJOR"),
|
||||||
|
env!("CARGO_PKG_VERSION_MINOR"),
|
||||||
|
env!("CARGO_PKG_VERSION_PATCH"),
|
||||||
|
env!("CARGO_PKG_VERSION_PRE"),
|
||||||
|
);
|
||||||
|
|
||||||
|
pub static VERSION_MAJOR: u32 = 0;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
|
Loading…
Reference in New Issue
Block a user