Update src/ to the new libtock.
This commit is contained in:
14
Cargo.toml
14
Cargo.toml
@@ -10,7 +10,9 @@ license = "Apache-2.0"
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libtock = { path = "third_party/libtock-rs" }
|
libtock_core = { path = "third_party/libtock-rs/core" }
|
||||||
|
libtock_drivers = { path = "third_party/libtock-drivers" }
|
||||||
|
lang_items = { path = "third_party/lang-items" }
|
||||||
cbor = { path = "libraries/cbor" }
|
cbor = { path = "libraries/cbor" }
|
||||||
crypto = { path = "libraries/crypto" }
|
crypto = { path = "libraries/crypto" }
|
||||||
byteorder = { version = "1", default-features = false }
|
byteorder = { version = "1", default-features = false }
|
||||||
@@ -18,12 +20,12 @@ arrayref = "0.3.6"
|
|||||||
subtle = { version = "2.2", default-features = false, features = ["nightly"] }
|
subtle = { version = "2.2", default-features = false, features = ["nightly"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
debug_allocations = ["libtock/debug_allocations"]
|
debug_allocations = ["lang_items/debug_allocations"]
|
||||||
debug_ctap = ["crypto/derive_debug"]
|
debug_ctap = ["crypto/derive_debug", "libtock_drivers/debug_ctap"]
|
||||||
panic_console = ["libtock/panic_console"]
|
panic_console = ["lang_items/panic_console"]
|
||||||
std = ["cbor/std", "crypto/std", "crypto/derive_debug"]
|
std = ["cbor/std", "crypto/std", "crypto/derive_debug", "lang_items/std"]
|
||||||
ram_storage = []
|
ram_storage = []
|
||||||
verbose = ["debug_ctap"]
|
verbose = ["debug_ctap", "libtock_drivers/verbose_usb"]
|
||||||
with_ctap1 = ["crypto/with_ctap1"]
|
with_ctap1 = ["crypto/with_ctap1"]
|
||||||
with_ctap2_1 = []
|
with_ctap2_1 = []
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use alloc::vec::Vec;
|
|||||||
use core::fmt::Write;
|
use core::fmt::Write;
|
||||||
use crypto::rng256::Rng256;
|
use crypto::rng256::Rng256;
|
||||||
#[cfg(feature = "debug_ctap")]
|
#[cfg(feature = "debug_ctap")]
|
||||||
use libtock::console::Console;
|
use libtock_drivers::console::Console;
|
||||||
|
|
||||||
// CTAP specification (version 20190130) section 8.1
|
// CTAP specification (version 20190130) section 8.1
|
||||||
// TODO: Channel allocation, section 8.1.3?
|
// TODO: Channel allocation, section 8.1.3?
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ use crypto::rng256::Rng256;
|
|||||||
use crypto::sha256::Sha256;
|
use crypto::sha256::Sha256;
|
||||||
use crypto::Hash256;
|
use crypto::Hash256;
|
||||||
#[cfg(feature = "debug_ctap")]
|
#[cfg(feature = "debug_ctap")]
|
||||||
use libtock::console::Console;
|
use libtock_drivers::console::Console;
|
||||||
use libtock::timer::{Duration, Timestamp};
|
use libtock_drivers::timer::{Duration, Timestamp};
|
||||||
use subtle::ConstantTimeEq;
|
use subtle::ConstantTimeEq;
|
||||||
|
|
||||||
// This flag enables or disables basic attestation for FIDO2. U2F is unaffected by
|
// This flag enables or disables basic attestation for FIDO2. U2F is unaffected by
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
use super::{Index, Storage, StorageError, StorageResult};
|
use super::{Index, Storage, StorageError, StorageResult};
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use libtock::syscalls;
|
use libtock_core::syscalls;
|
||||||
|
|
||||||
const DRIVER_NUMBER: usize = 0x50003;
|
const DRIVER_NUMBER: usize = 0x50003;
|
||||||
|
|
||||||
@@ -41,16 +41,14 @@ mod memop_nr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_info(nr: usize, arg: usize) -> StorageResult<usize> {
|
fn get_info(nr: usize, arg: usize) -> StorageResult<usize> {
|
||||||
let code = unsafe { syscalls::command(DRIVER_NUMBER, command_nr::GET_INFO, nr, arg) };
|
let code = syscalls::command(DRIVER_NUMBER, command_nr::GET_INFO, nr, arg);
|
||||||
if code < 0 {
|
code.map_err(|e| StorageError::KernelError {
|
||||||
Err(StorageError::KernelError { code })
|
code: e.return_code,
|
||||||
} else {
|
})
|
||||||
Ok(code as usize)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn memop(nr: u32, arg: usize) -> StorageResult<usize> {
|
fn memop(nr: u32, arg: usize) -> StorageResult<usize> {
|
||||||
let code = unsafe { syscalls::memop(nr, arg) };
|
let code = unsafe { syscalls::raw::memop(nr, arg) };
|
||||||
if code < 0 {
|
if code < 0 {
|
||||||
Err(StorageError::KernelError { code })
|
Err(StorageError::KernelError { code })
|
||||||
} else {
|
} else {
|
||||||
@@ -153,8 +151,9 @@ impl Storage for SyscallStorage {
|
|||||||
return Err(StorageError::NotAligned);
|
return Err(StorageError::NotAligned);
|
||||||
}
|
}
|
||||||
let ptr = self.read_slice(index, value.len())?.as_ptr() as usize;
|
let ptr = self.read_slice(index, value.len())?.as_ptr() as usize;
|
||||||
|
|
||||||
let code = unsafe {
|
let code = unsafe {
|
||||||
syscalls::allow_ptr(
|
syscalls::raw::allow(
|
||||||
DRIVER_NUMBER,
|
DRIVER_NUMBER,
|
||||||
allow_nr::WRITE_SLICE,
|
allow_nr::WRITE_SLICE,
|
||||||
// We rely on the driver not writing to the slice. This should use read-only allow
|
// We rely on the driver not writing to the slice. This should use read-only allow
|
||||||
@@ -166,11 +165,14 @@ impl Storage for SyscallStorage {
|
|||||||
if code < 0 {
|
if code < 0 {
|
||||||
return Err(StorageError::KernelError { code });
|
return Err(StorageError::KernelError { code });
|
||||||
}
|
}
|
||||||
let code =
|
|
||||||
unsafe { syscalls::command(DRIVER_NUMBER, command_nr::WRITE_SLICE, ptr, value.len()) };
|
let code = syscalls::command(DRIVER_NUMBER, command_nr::WRITE_SLICE, ptr, value.len());
|
||||||
if code < 0 {
|
if let Err(e) = code {
|
||||||
return Err(StorageError::KernelError { code });
|
return Err(StorageError::KernelError {
|
||||||
|
code: e.return_code,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,9 +180,11 @@ impl Storage for SyscallStorage {
|
|||||||
let index = Index { page, byte: 0 };
|
let index = Index { page, byte: 0 };
|
||||||
let length = self.page_size();
|
let length = self.page_size();
|
||||||
let ptr = self.read_slice(index, length)?.as_ptr() as usize;
|
let ptr = self.read_slice(index, length)?.as_ptr() as usize;
|
||||||
let code = unsafe { syscalls::command(DRIVER_NUMBER, command_nr::ERASE_PAGE, ptr, length) };
|
let code = syscalls::command(DRIVER_NUMBER, command_nr::ERASE_PAGE, ptr, length);
|
||||||
if code < 0 {
|
if let Err(e) = code {
|
||||||
return Err(StorageError::KernelError { code });
|
return Err(StorageError::KernelError {
|
||||||
|
code: e.return_code,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
extern crate libtock;
|
extern crate lang_items;
|
||||||
|
extern crate libtock_core;
|
||||||
|
extern crate libtock_drivers;
|
||||||
|
|
||||||
pub mod embedded_flash;
|
pub mod embedded_flash;
|
||||||
|
|||||||
85
src/main.rs
85
src/main.rs
@@ -22,14 +22,12 @@ extern crate byteorder;
|
|||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
extern crate core;
|
extern crate core;
|
||||||
extern crate ctap2;
|
extern crate ctap2;
|
||||||
extern crate libtock;
|
|
||||||
extern crate subtle;
|
extern crate subtle;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate cbor;
|
extern crate cbor;
|
||||||
extern crate crypto;
|
extern crate crypto;
|
||||||
|
|
||||||
mod ctap;
|
mod ctap;
|
||||||
mod usb_ctap_hid;
|
|
||||||
|
|
||||||
use core::cell::Cell;
|
use core::cell::Cell;
|
||||||
#[cfg(feature = "debug_ctap")]
|
#[cfg(feature = "debug_ctap")]
|
||||||
@@ -38,17 +36,18 @@ use crypto::rng256::TockRng256;
|
|||||||
use ctap::hid::{ChannelID, CtapHid, KeepaliveStatus, ProcessedPacket};
|
use ctap::hid::{ChannelID, CtapHid, KeepaliveStatus, ProcessedPacket};
|
||||||
use ctap::status_code::Ctap2StatusCode;
|
use ctap::status_code::Ctap2StatusCode;
|
||||||
use ctap::CtapState;
|
use ctap::CtapState;
|
||||||
use libtock::buttons;
|
use libtock_core::result::{CommandError, EALREADY};
|
||||||
use libtock::buttons::ButtonState;
|
use libtock_drivers::buttons;
|
||||||
|
use libtock_drivers::buttons::ButtonState;
|
||||||
#[cfg(feature = "debug_ctap")]
|
#[cfg(feature = "debug_ctap")]
|
||||||
use libtock::console::Console;
|
use libtock_drivers::console::Console;
|
||||||
use libtock::led;
|
use libtock_drivers::led;
|
||||||
use libtock::result::TockValue;
|
use libtock_drivers::result::{FlexUnwrap, TockError};
|
||||||
use libtock::syscalls;
|
use libtock_drivers::timer;
|
||||||
use libtock::timer;
|
|
||||||
#[cfg(feature = "debug_ctap")]
|
#[cfg(feature = "debug_ctap")]
|
||||||
use libtock::timer::Timer;
|
use libtock_drivers::timer::Timer;
|
||||||
use libtock::timer::{Duration, StopAlarmError, Timestamp};
|
use libtock_drivers::timer::{Duration, Timestamp};
|
||||||
|
use libtock_drivers::usb_ctap_hid;
|
||||||
|
|
||||||
const KEEPALIVE_DELAY_MS: isize = 100;
|
const KEEPALIVE_DELAY_MS: isize = 100;
|
||||||
const KEEPALIVE_DELAY: Duration<isize> = Duration::from_ms(KEEPALIVE_DELAY_MS);
|
const KEEPALIVE_DELAY: Duration<isize> = Duration::from_ms(KEEPALIVE_DELAY_MS);
|
||||||
@@ -58,7 +57,7 @@ fn main() {
|
|||||||
// Setup the timer with a dummy callback (we only care about reading the current time, but the
|
// Setup the timer with a dummy callback (we only care about reading the current time, but the
|
||||||
// API forces us to set an alarm callback too).
|
// API forces us to set an alarm callback too).
|
||||||
let mut with_callback = timer::with_callback(|_, _| {});
|
let mut with_callback = timer::with_callback(|_, _| {});
|
||||||
let timer = with_callback.init().unwrap();
|
let timer = with_callback.init().flex_unwrap();
|
||||||
|
|
||||||
// Setup USB driver.
|
// Setup USB driver.
|
||||||
if !usb_ctap_hid::setup() {
|
if !usb_ctap_hid::setup() {
|
||||||
@@ -70,7 +69,7 @@ fn main() {
|
|||||||
let mut ctap_hid = CtapHid::new();
|
let mut ctap_hid = CtapHid::new();
|
||||||
|
|
||||||
let mut led_counter = 0;
|
let mut led_counter = 0;
|
||||||
let mut last_led_increment = timer.get_current_clock();
|
let mut last_led_increment = timer.get_current_clock().flex_unwrap();
|
||||||
|
|
||||||
// Main loop. If CTAP1 is used, we register button presses for U2F while receiving and waiting.
|
// Main loop. If CTAP1 is used, we register button presses for U2F while receiving and waiting.
|
||||||
// The way TockOS and apps currently interact, callbacks need a yield syscall to execute,
|
// The way TockOS and apps currently interact, callbacks need a yield syscall to execute,
|
||||||
@@ -87,11 +86,11 @@ fn main() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
#[cfg(feature = "with_ctap1")]
|
#[cfg(feature = "with_ctap1")]
|
||||||
let mut buttons = buttons_callback.init().unwrap();
|
let mut buttons = buttons_callback.init().flex_unwrap();
|
||||||
#[cfg(feature = "with_ctap1")]
|
#[cfg(feature = "with_ctap1")]
|
||||||
// At the moment, all buttons are accepted. You can customize your setup here.
|
// At the moment, all buttons are accepted. You can customize your setup here.
|
||||||
for mut button in &mut buttons {
|
for mut button in &mut buttons {
|
||||||
button.enable().unwrap();
|
button.enable().flex_unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut pkt_request = [0; 64];
|
let mut pkt_request = [0; 64];
|
||||||
@@ -105,7 +104,7 @@ fn main() {
|
|||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let now = timer.get_current_clock();
|
let now = timer.get_current_clock().flex_unwrap();
|
||||||
#[cfg(feature = "with_ctap1")]
|
#[cfg(feature = "with_ctap1")]
|
||||||
{
|
{
|
||||||
if button_touched.get() {
|
if button_touched.get() {
|
||||||
@@ -115,7 +114,7 @@ fn main() {
|
|||||||
// Heavy computation mostly follows a registered touch luckily. Unregistering
|
// Heavy computation mostly follows a registered touch luckily. Unregistering
|
||||||
// callbacks is important to not clash with those from check_user_presence.
|
// callbacks is important to not clash with those from check_user_presence.
|
||||||
for mut button in &mut buttons {
|
for mut button in &mut buttons {
|
||||||
button.disable().unwrap();
|
button.disable().flex_unwrap();
|
||||||
}
|
}
|
||||||
drop(buttons);
|
drop(buttons);
|
||||||
drop(buttons_callback);
|
drop(buttons_callback);
|
||||||
@@ -153,7 +152,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = timer.get_current_clock();
|
let now = timer.get_current_clock().flex_unwrap();
|
||||||
if let Some(wait_duration) = now.wrapping_sub(last_led_increment) {
|
if let Some(wait_duration) = now.wrapping_sub(last_led_increment) {
|
||||||
if wait_duration > KEEPALIVE_DELAY {
|
if wait_duration > KEEPALIVE_DELAY {
|
||||||
// Loops quickly when waiting for U2F user presence, so the next LED blink
|
// Loops quickly when waiting for U2F user presence, so the next LED blink
|
||||||
@@ -188,8 +187,8 @@ fn main() {
|
|||||||
|
|
||||||
#[cfg(feature = "debug_ctap")]
|
#[cfg(feature = "debug_ctap")]
|
||||||
fn print_packet_notice(notice_text: &str, timer: &Timer) {
|
fn print_packet_notice(notice_text: &str, timer: &Timer) {
|
||||||
let now_us =
|
let now = timer.get_current_clock().flex_unwrap();
|
||||||
(Timestamp::<f64>::from_clock_value(timer.get_current_clock()).ms() * 1000.0) as u64;
|
let now_us = (Timestamp::<f64>::from_clock_value(now).ms() * 1000.0) as u64;
|
||||||
writeln!(
|
writeln!(
|
||||||
Console::new(),
|
Console::new(),
|
||||||
"{} at {}.{:06} s",
|
"{} at {}.{:06} s",
|
||||||
@@ -264,17 +263,17 @@ fn send_keepalive_up_needed(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn blink_leds(pattern_seed: isize) {
|
fn blink_leds(pattern_seed: usize) {
|
||||||
for l in 0..led::count() {
|
for l in 0..led::count().flex_unwrap() {
|
||||||
if (pattern_seed ^ l).count_ones() & 1 != 0 {
|
if (pattern_seed ^ l).count_ones() & 1 != 0 {
|
||||||
led::get(l).unwrap().on();
|
led::get(l).flex_unwrap().on().flex_unwrap();
|
||||||
} else {
|
} else {
|
||||||
led::get(l).unwrap().off();
|
led::get(l).flex_unwrap().off().flex_unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wink_leds(pattern_seed: isize) {
|
fn wink_leds(pattern_seed: usize) {
|
||||||
// This generates a "snake" pattern circling through the LEDs.
|
// This generates a "snake" pattern circling through the LEDs.
|
||||||
// Fox example with 4 LEDs the sequence of lit LEDs will be the following.
|
// Fox example with 4 LEDs the sequence of lit LEDs will be the following.
|
||||||
// 0 1 2 3
|
// 0 1 2 3
|
||||||
@@ -287,7 +286,7 @@ fn wink_leds(pattern_seed: isize) {
|
|||||||
// * *
|
// * *
|
||||||
// * * *
|
// * * *
|
||||||
// * *
|
// * *
|
||||||
let count = led::count();
|
let count = led::count().flex_unwrap();
|
||||||
let a = (pattern_seed / 2) % count;
|
let a = (pattern_seed / 2) % count;
|
||||||
let b = ((pattern_seed + 1) / 2) % count;
|
let b = ((pattern_seed + 1) / 2) % count;
|
||||||
let c = ((pattern_seed + 3) / 2) % count;
|
let c = ((pattern_seed + 3) / 2) % count;
|
||||||
@@ -300,22 +299,22 @@ fn wink_leds(pattern_seed: isize) {
|
|||||||
_ => l,
|
_ => l,
|
||||||
};
|
};
|
||||||
if k == a || k == b || k == c {
|
if k == a || k == b || k == c {
|
||||||
led::get(l).unwrap().on();
|
led::get(l).flex_unwrap().on().flex_unwrap();
|
||||||
} else {
|
} else {
|
||||||
led::get(l).unwrap().off();
|
led::get(l).flex_unwrap().off().flex_unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch_off_leds() {
|
fn switch_off_leds() {
|
||||||
for l in 0..led::count() {
|
for l in 0..led::count().flex_unwrap() {
|
||||||
led::get(l).unwrap().off();
|
led::get(l).flex_unwrap().off().flex_unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_user_presence(cid: ChannelID) -> Result<(), Ctap2StatusCode> {
|
fn check_user_presence(cid: ChannelID) -> Result<(), Ctap2StatusCode> {
|
||||||
// The timeout is N times the keepalive delay.
|
// The timeout is N times the keepalive delay.
|
||||||
const TIMEOUT_ITERATIONS: isize = ctap::TOUCH_TIMEOUT_MS / KEEPALIVE_DELAY_MS;
|
const TIMEOUT_ITERATIONS: usize = ctap::TOUCH_TIMEOUT_MS as usize / KEEPALIVE_DELAY_MS as usize;
|
||||||
|
|
||||||
// First, send a keep-alive packet to notify that the keep-alive status has changed.
|
// First, send a keep-alive packet to notify that the keep-alive status has changed.
|
||||||
send_keepalive_up_needed(cid, KEEPALIVE_DELAY)?;
|
send_keepalive_up_needed(cid, KEEPALIVE_DELAY)?;
|
||||||
@@ -328,10 +327,10 @@ fn check_user_presence(cid: ChannelID) -> Result<(), Ctap2StatusCode> {
|
|||||||
ButtonState::Released => (),
|
ButtonState::Released => (),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
let mut buttons = buttons_callback.init().unwrap();
|
let mut buttons = buttons_callback.init().flex_unwrap();
|
||||||
// At the moment, all buttons are accepted. You can customize your setup here.
|
// At the moment, all buttons are accepted. You can customize your setup here.
|
||||||
for mut button in &mut buttons {
|
for mut button in &mut buttons {
|
||||||
button.enable().unwrap();
|
button.enable().flex_unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut keepalive_response = Ok(());
|
let mut keepalive_response = Ok(());
|
||||||
@@ -343,19 +342,25 @@ fn check_user_presence(cid: ChannelID) -> Result<(), Ctap2StatusCode> {
|
|||||||
let mut keepalive_callback = timer::with_callback(|_, _| {
|
let mut keepalive_callback = timer::with_callback(|_, _| {
|
||||||
keepalive_expired.set(true);
|
keepalive_expired.set(true);
|
||||||
});
|
});
|
||||||
let mut keepalive = keepalive_callback.init().unwrap();
|
let mut keepalive = keepalive_callback.init().flex_unwrap();
|
||||||
let keepalive_alarm = keepalive.set_alarm(KEEPALIVE_DELAY).unwrap();
|
let keepalive_alarm = keepalive.set_alarm(KEEPALIVE_DELAY).flex_unwrap();
|
||||||
|
|
||||||
// Wait for a button touch or an alarm.
|
// Wait for a button touch or an alarm.
|
||||||
syscalls::yieldk_for(|| button_touched.get() || keepalive_expired.get());
|
libtock_drivers::util::yieldk_for(|| button_touched.get() || keepalive_expired.get());
|
||||||
|
|
||||||
// Cleanup alarm callback.
|
// Cleanup alarm callback.
|
||||||
match keepalive.stop_alarm(keepalive_alarm) {
|
match keepalive.stop_alarm(keepalive_alarm) {
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
Err(TockValue::Expected(StopAlarmError::AlreadyDisabled)) => {
|
Err(TockError::Command(CommandError {
|
||||||
assert!(keepalive_expired.get())
|
return_code: EALREADY,
|
||||||
|
..
|
||||||
|
})) => assert!(keepalive_expired.get()),
|
||||||
|
Err(_e) => {
|
||||||
|
#[cfg(feature = "debug_ctap")]
|
||||||
|
panic!("Unexpected error when stopping alarm: {:?}", _e);
|
||||||
|
#[cfg(not(feature = "debug_ctap"))]
|
||||||
|
panic!("Unexpected error when stopping alarm: <error is only visible with the debug_ctap feature>");
|
||||||
}
|
}
|
||||||
Err(e) => panic!("Unexpected error when stopping alarm: {:?}", e),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this may take arbitrary time. The keepalive_delay should be adjusted accordingly,
|
// TODO: this may take arbitrary time. The keepalive_delay should be adjusted accordingly,
|
||||||
@@ -374,7 +379,7 @@ fn check_user_presence(cid: ChannelID) -> Result<(), Ctap2StatusCode> {
|
|||||||
|
|
||||||
// Cleanup button callbacks.
|
// Cleanup button callbacks.
|
||||||
for mut button in &mut buttons {
|
for mut button in &mut buttons {
|
||||||
button.disable().unwrap();
|
button.disable().flex_unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns whether the user was present.
|
// Returns whether the user was present.
|
||||||
|
|||||||
Reference in New Issue
Block a user