b9874d0941
Add redbear-usb-storage-check in-guest binary that validates USB mass storage read and write I/O: discovers /scheme/disk/ devices, writes a test pattern to sector 2048, reads it back, verifies match, restores original content. Updates test-usb-storage-qemu.sh with write-proof verification step. Includes all accumulated Red Bear OS work: kernel patches, relibc patches, driver infrastructure, DRM/GPU, KDE recipes, firmware, validation tooling, build system hardening, and documentation.
587 lines
18 KiB
Rust
587 lines
18 KiB
Rust
use termion::event::Key;
|
|
|
|
use crate::backend::{ConsoleBackend, IpMode, Profile, ScanResult, SecurityKind, WifiRuntimeState};
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Focus {
|
|
Profiles,
|
|
Scan,
|
|
Fields,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Field {
|
|
Name,
|
|
Description,
|
|
Interface,
|
|
Ssid,
|
|
Security,
|
|
Key,
|
|
IpMode,
|
|
Address,
|
|
Gateway,
|
|
Dns,
|
|
}
|
|
|
|
impl Field {
|
|
pub fn label(&self) -> &'static str {
|
|
match self {
|
|
Self::Name => "Profile",
|
|
Self::Description => "Description",
|
|
Self::Interface => "Interface",
|
|
Self::Ssid => "SSID",
|
|
Self::Security => "Security",
|
|
Self::Key => "Key",
|
|
Self::IpMode => "IP",
|
|
Self::Address => "Address",
|
|
Self::Gateway => "Gateway",
|
|
Self::Dns => "DNS",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct EditorState {
|
|
pub field: Field,
|
|
pub buffer: String,
|
|
}
|
|
|
|
pub struct App<B: ConsoleBackend> {
|
|
backend: B,
|
|
pub profiles: Vec<String>,
|
|
pub selected_profile: usize,
|
|
pub draft: Profile,
|
|
pub scans: Vec<ScanResult>,
|
|
pub selected_scan: usize,
|
|
pub selected_field: usize,
|
|
pub focus: Focus,
|
|
pub editor: Option<EditorState>,
|
|
pub active_profile: Option<String>,
|
|
pub status: WifiRuntimeState,
|
|
pub message: String,
|
|
pub dirty: bool,
|
|
pub should_quit: bool,
|
|
}
|
|
|
|
impl<B: ConsoleBackend> App<B> {
|
|
pub fn new(backend: B) -> Result<Self, String> {
|
|
let mut app = Self {
|
|
backend,
|
|
profiles: Vec::new(),
|
|
selected_profile: 0,
|
|
draft: Profile::default(),
|
|
scans: Vec::new(),
|
|
selected_scan: 0,
|
|
selected_field: 0,
|
|
focus: Focus::Profiles,
|
|
editor: None,
|
|
active_profile: None,
|
|
status: WifiRuntimeState::default(),
|
|
message: "Tab switches panes. r scans. s saves. c connects. d disconnects.".to_string(),
|
|
dirty: false,
|
|
should_quit: false,
|
|
};
|
|
app.reload_profiles()?;
|
|
app.refresh_status();
|
|
Ok(app)
|
|
}
|
|
|
|
pub fn visible_fields(&self) -> Vec<Field> {
|
|
let mut fields = vec![
|
|
Field::Name,
|
|
Field::Description,
|
|
Field::Interface,
|
|
Field::Ssid,
|
|
Field::Security,
|
|
];
|
|
|
|
if matches!(self.draft.security, SecurityKind::Wpa2Psk) {
|
|
fields.push(Field::Key);
|
|
}
|
|
|
|
fields.push(Field::IpMode);
|
|
|
|
if matches!(self.draft.ip_mode, IpMode::Static) {
|
|
fields.extend([Field::Address, Field::Gateway, Field::Dns]);
|
|
}
|
|
|
|
fields
|
|
}
|
|
|
|
pub fn selected_field(&self) -> Field {
|
|
let fields = self.visible_fields();
|
|
fields[self.selected_field.min(fields.len().saturating_sub(1))]
|
|
}
|
|
|
|
pub fn field_value(&self, field: Field) -> String {
|
|
match field {
|
|
Field::Name => self.draft.name.clone(),
|
|
Field::Description => self.draft.description.clone(),
|
|
Field::Interface => self.draft.interface.clone(),
|
|
Field::Ssid => self.draft.ssid.clone(),
|
|
Field::Security => self.draft.security.as_str().to_string(),
|
|
Field::Key => mask_secret(&self.draft.key),
|
|
Field::IpMode => self.draft.ip_mode.as_str().to_string(),
|
|
Field::Address => self.draft.address.clone(),
|
|
Field::Gateway => self.draft.gateway.clone(),
|
|
Field::Dns => self.draft.dns.clone(),
|
|
}
|
|
}
|
|
|
|
pub fn handle_key(&mut self, key: Key) {
|
|
if self.editor.is_some() {
|
|
self.handle_editor_key(key);
|
|
return;
|
|
}
|
|
|
|
match key {
|
|
Key::Char('q') => self.should_quit = true,
|
|
Key::Char('\t') => self.cycle_focus(true),
|
|
Key::BackTab => self.cycle_focus(false),
|
|
Key::Char('r') => self.scan(),
|
|
Key::Char('s') => self.save_current_profile(),
|
|
Key::Char('a') => self.activate_current_profile(),
|
|
Key::Char('c') => self.connect_current_profile(),
|
|
Key::Char('d') => self.disconnect_current_profile(),
|
|
Key::Char('n') => self.start_new_profile(),
|
|
Key::Up | Key::Char('k') => self.move_selection(-1),
|
|
Key::Down | Key::Char('j') => self.move_selection(1),
|
|
Key::Left | Key::Char('h') => self.adjust_field_enum(false),
|
|
Key::Right | Key::Char('l') => self.adjust_field_enum(true),
|
|
Key::Char('\n') => self.activate_current_focus(),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub fn reload_profiles(&mut self) -> Result<(), String> {
|
|
self.active_profile = self.backend.active_profile_name()?;
|
|
self.profiles = self.backend.list_wifi_profiles()?;
|
|
|
|
if let Some(active) = &self.active_profile
|
|
&& let Some(index) = self.profiles.iter().position(|name| name == active)
|
|
{
|
|
self.selected_profile = index;
|
|
self.load_selected_profile()?;
|
|
return Ok(());
|
|
}
|
|
|
|
if let Some(index) = self
|
|
.profiles
|
|
.iter()
|
|
.position(|name| name == &self.draft.name)
|
|
{
|
|
self.selected_profile = index;
|
|
return Ok(());
|
|
}
|
|
|
|
if !self.profiles.is_empty() {
|
|
self.selected_profile = 0;
|
|
self.load_selected_profile()?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn refresh_status(&mut self) {
|
|
self.status = self.backend.read_status(&self.draft.interface);
|
|
}
|
|
|
|
fn cycle_focus(&mut self, forward: bool) {
|
|
self.focus = match (self.focus, forward) {
|
|
(Focus::Profiles, true) => Focus::Scan,
|
|
(Focus::Scan, true) => Focus::Fields,
|
|
(Focus::Fields, true) => Focus::Profiles,
|
|
(Focus::Profiles, false) => Focus::Fields,
|
|
(Focus::Scan, false) => Focus::Profiles,
|
|
(Focus::Fields, false) => Focus::Scan,
|
|
};
|
|
}
|
|
|
|
fn move_selection(&mut self, delta: isize) {
|
|
match self.focus {
|
|
Focus::Profiles => adjust_index(&mut self.selected_profile, self.profiles.len(), delta),
|
|
Focus::Scan => adjust_index(&mut self.selected_scan, self.scans.len(), delta),
|
|
Focus::Fields => {
|
|
let field_count = self.visible_fields().len();
|
|
adjust_index(&mut self.selected_field, field_count, delta)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn activate_current_focus(&mut self) {
|
|
match self.focus {
|
|
Focus::Profiles => {
|
|
if let Err(err) = self.load_selected_profile() {
|
|
self.message = format!("Error: {err}");
|
|
}
|
|
}
|
|
Focus::Scan => self.apply_selected_scan(),
|
|
Focus::Fields => self.activate_selected_field(),
|
|
}
|
|
}
|
|
|
|
fn activate_selected_field(&mut self) {
|
|
match self.selected_field() {
|
|
Field::Security => {
|
|
self.draft.security = self.draft.security.next();
|
|
self.selected_field = self
|
|
.selected_field
|
|
.min(self.visible_fields().len().saturating_sub(1));
|
|
self.dirty = true;
|
|
}
|
|
Field::IpMode => {
|
|
self.draft.ip_mode = self.draft.ip_mode.next();
|
|
self.selected_field = self
|
|
.selected_field
|
|
.min(self.visible_fields().len().saturating_sub(1));
|
|
self.dirty = true;
|
|
}
|
|
field => {
|
|
self.editor = Some(EditorState {
|
|
field,
|
|
buffer: self.raw_field_value(field),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn adjust_field_enum(&mut self, forward: bool) {
|
|
if self.focus != Focus::Fields {
|
|
return;
|
|
}
|
|
|
|
match self.selected_field() {
|
|
Field::Security => {
|
|
self.draft.security = if forward {
|
|
self.draft.security.next()
|
|
} else {
|
|
self.draft.security.previous()
|
|
};
|
|
self.selected_field = self
|
|
.selected_field
|
|
.min(self.visible_fields().len().saturating_sub(1));
|
|
self.dirty = true;
|
|
}
|
|
Field::IpMode => {
|
|
self.draft.ip_mode = if forward {
|
|
self.draft.ip_mode.next()
|
|
} else {
|
|
self.draft.ip_mode.previous()
|
|
};
|
|
self.selected_field = self
|
|
.selected_field
|
|
.min(self.visible_fields().len().saturating_sub(1));
|
|
self.dirty = true;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn handle_editor_key(&mut self, key: Key) {
|
|
let Some(editor) = self.editor.as_mut() else {
|
|
return;
|
|
};
|
|
|
|
match key {
|
|
Key::Esc => self.editor = None,
|
|
Key::Backspace => {
|
|
editor.buffer.pop();
|
|
}
|
|
Key::Char('\n') => {
|
|
let field = editor.field;
|
|
let buffer = editor.buffer.clone();
|
|
self.editor = None;
|
|
self.commit_field(field, buffer);
|
|
}
|
|
Key::Char(ch) if !ch.is_control() => editor.buffer.push(ch),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn commit_field(&mut self, field: Field, value: String) {
|
|
match field {
|
|
Field::Name => self.draft.name = value,
|
|
Field::Description => self.draft.description = value,
|
|
Field::Interface => {
|
|
self.draft.interface = value;
|
|
self.refresh_status();
|
|
}
|
|
Field::Ssid => self.draft.ssid = value,
|
|
Field::Key => self.draft.key = value,
|
|
Field::Address => self.draft.address = value,
|
|
Field::Gateway => self.draft.gateway = value,
|
|
Field::Dns => self.draft.dns = value,
|
|
Field::Security | Field::IpMode => {}
|
|
}
|
|
|
|
self.dirty = true;
|
|
}
|
|
|
|
fn load_selected_profile(&mut self) -> Result<(), String> {
|
|
let Some(name) = self.profiles.get(self.selected_profile).cloned() else {
|
|
return Ok(());
|
|
};
|
|
self.draft = self.backend.load_profile(&name)?;
|
|
self.editor = None;
|
|
self.selected_field = 0;
|
|
self.selected_scan = 0;
|
|
self.scans.clear();
|
|
self.refresh_status();
|
|
self.message = format!("Loaded {name}");
|
|
self.dirty = false;
|
|
Ok(())
|
|
}
|
|
|
|
fn apply_selected_scan(&mut self) {
|
|
let Some(scan) = self.scans.get(self.selected_scan).cloned() else {
|
|
self.message = "No scan result selected".to_string();
|
|
return;
|
|
};
|
|
self.draft.ssid = scan.ssid.clone();
|
|
if let Some(security_hint) = scan.security_hint {
|
|
self.draft.security = security_hint;
|
|
}
|
|
self.dirty = true;
|
|
self.message = format!("Selected SSID {}", scan.ssid);
|
|
}
|
|
|
|
fn save_current_profile(&mut self) {
|
|
match self.backend.save_profile(&self.draft) {
|
|
Ok(()) => {
|
|
self.dirty = false;
|
|
self.message = format!("Saved {}", self.draft.name);
|
|
if let Err(err) = self.reload_profiles() {
|
|
self.message = format!("Saved {}, but reload failed: {err}", self.draft.name);
|
|
}
|
|
}
|
|
Err(err) => self.message = format!("Error: {err}"),
|
|
}
|
|
}
|
|
|
|
fn activate_current_profile(&mut self) {
|
|
match self.backend.save_profile(&self.draft) {
|
|
Ok(()) => match self.backend.set_active_profile(&self.draft.name) {
|
|
Ok(()) => {
|
|
self.active_profile = Some(self.draft.name.clone());
|
|
self.dirty = false;
|
|
self.message = format!("Activated {} for boot/profile reuse", self.draft.name);
|
|
let _ = self.reload_profiles();
|
|
}
|
|
Err(err) => self.message = format!("Error: {err}"),
|
|
},
|
|
Err(err) => self.message = format!("Error: {err}"),
|
|
}
|
|
}
|
|
|
|
fn connect_current_profile(&mut self) {
|
|
match self.backend.connect(&self.draft) {
|
|
Ok(message) => {
|
|
self.active_profile = Some(self.draft.name.clone());
|
|
self.dirty = false;
|
|
self.message = message;
|
|
let _ = self.reload_profiles();
|
|
self.refresh_status();
|
|
}
|
|
Err(err) => {
|
|
self.refresh_status();
|
|
self.message = format!("Error: {err}");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn disconnect_current_profile(&mut self) {
|
|
match self
|
|
.backend
|
|
.disconnect(Some(&self.draft.name), &self.draft.interface)
|
|
{
|
|
Ok(message) => {
|
|
if self.active_profile.as_deref() == Some(self.draft.name.as_str()) {
|
|
self.active_profile = None;
|
|
}
|
|
self.message = message;
|
|
let _ = self.reload_profiles();
|
|
self.refresh_status();
|
|
}
|
|
Err(err) => {
|
|
self.refresh_status();
|
|
self.message = format!("Error: {err}");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn scan(&mut self) {
|
|
match self.backend.scan(&self.draft.interface) {
|
|
Ok(scans) => {
|
|
self.scans = scans;
|
|
self.selected_scan = 0;
|
|
self.refresh_status();
|
|
self.message = if self.scans.is_empty() {
|
|
format!(
|
|
"Scan completed for {} with no visible results",
|
|
self.draft.interface
|
|
)
|
|
} else {
|
|
format!(
|
|
"Scan completed for {} with {} result(s)",
|
|
self.draft.interface,
|
|
self.scans.len()
|
|
)
|
|
};
|
|
}
|
|
Err(err) => {
|
|
self.refresh_status();
|
|
self.message = format!("Error: {err}");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn start_new_profile(&mut self) {
|
|
self.draft = Profile::default();
|
|
self.scans.clear();
|
|
self.selected_scan = 0;
|
|
self.selected_field = 0;
|
|
self.focus = Focus::Fields;
|
|
self.editor = None;
|
|
self.dirty = true;
|
|
self.refresh_status();
|
|
self.message = "Started a new Wi-Fi profile draft".to_string();
|
|
}
|
|
|
|
fn raw_field_value(&self, field: Field) -> String {
|
|
match field {
|
|
Field::Name => self.draft.name.clone(),
|
|
Field::Description => self.draft.description.clone(),
|
|
Field::Interface => self.draft.interface.clone(),
|
|
Field::Ssid => self.draft.ssid.clone(),
|
|
Field::Key => self.draft.key.clone(),
|
|
Field::Address => self.draft.address.clone(),
|
|
Field::Gateway => self.draft.gateway.clone(),
|
|
Field::Dns => self.draft.dns.clone(),
|
|
Field::Security => self.draft.security.as_str().to_string(),
|
|
Field::IpMode => self.draft.ip_mode.as_str().to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn adjust_index(index: &mut usize, len: usize, delta: isize) {
|
|
if len == 0 {
|
|
*index = 0;
|
|
return;
|
|
}
|
|
|
|
if delta < 0 {
|
|
*index = index.saturating_sub(delta.unsigned_abs());
|
|
} else {
|
|
*index = (*index + delta as usize).min(len.saturating_sub(1));
|
|
}
|
|
}
|
|
|
|
fn mask_secret(secret: &str) -> String {
|
|
if secret.is_empty() {
|
|
"<empty>".to_string()
|
|
} else {
|
|
"•".repeat(secret.chars().count())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::backend::ScanResult;
|
|
|
|
#[derive(Default)]
|
|
struct MockBackend {
|
|
profiles: Vec<String>,
|
|
active_profile: Option<String>,
|
|
loaded_profile: Profile,
|
|
scans: Vec<ScanResult>,
|
|
}
|
|
|
|
impl ConsoleBackend for MockBackend {
|
|
fn list_wifi_profiles(&self) -> Result<Vec<String>, String> {
|
|
Ok(self.profiles.clone())
|
|
}
|
|
|
|
fn active_profile_name(&self) -> Result<Option<String>, String> {
|
|
Ok(self.active_profile.clone())
|
|
}
|
|
|
|
fn load_profile(&self, _name: &str) -> Result<Profile, String> {
|
|
Ok(self.loaded_profile.clone())
|
|
}
|
|
|
|
fn save_profile(&self, _profile: &Profile) -> Result<(), String> {
|
|
Ok(())
|
|
}
|
|
|
|
fn set_active_profile(&self, _profile_name: &str) -> Result<(), String> {
|
|
Ok(())
|
|
}
|
|
|
|
fn clear_active_profile(&self) -> Result<(), String> {
|
|
Ok(())
|
|
}
|
|
|
|
fn read_status(&self, interface: &str) -> WifiRuntimeState {
|
|
WifiRuntimeState {
|
|
interface: interface.to_string(),
|
|
status: "unknown".to_string(),
|
|
address: "unconfigured".to_string(),
|
|
..WifiRuntimeState::default()
|
|
}
|
|
}
|
|
|
|
fn scan(&self, _interface: &str) -> Result<Vec<ScanResult>, String> {
|
|
Ok(self.scans.clone())
|
|
}
|
|
|
|
fn connect(&self, _profile: &Profile) -> Result<String, String> {
|
|
Ok("applied wifi-profile via wlan0".to_string())
|
|
}
|
|
|
|
fn disconnect(
|
|
&self,
|
|
_profile_name: Option<&str>,
|
|
_interface: &str,
|
|
) -> Result<String, String> {
|
|
Ok("disconnected wlan0".to_string())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn visible_fields_follow_security_and_ip_mode() {
|
|
let backend = MockBackend::default();
|
|
let mut app = App::new(backend).unwrap();
|
|
|
|
assert!(!app.visible_fields().contains(&Field::Key));
|
|
assert!(!app.visible_fields().contains(&Field::Address));
|
|
|
|
app.draft.security = SecurityKind::Wpa2Psk;
|
|
app.draft.ip_mode = IpMode::Static;
|
|
|
|
assert!(app.visible_fields().contains(&Field::Key));
|
|
assert!(app.visible_fields().contains(&Field::Address));
|
|
assert!(app.visible_fields().contains(&Field::Gateway));
|
|
assert!(app.visible_fields().contains(&Field::Dns));
|
|
}
|
|
|
|
#[test]
|
|
fn selecting_scan_applies_ssid_and_security_hint() {
|
|
let backend = MockBackend {
|
|
scans: vec![ScanResult {
|
|
raw: "ssid=demo-open security=open".to_string(),
|
|
ssid: "demo-open".to_string(),
|
|
security_hint: Some(SecurityKind::Open),
|
|
}],
|
|
..MockBackend::default()
|
|
};
|
|
let mut app = App::new(backend).unwrap();
|
|
app.handle_key(Key::Char('r'));
|
|
app.focus = Focus::Scan;
|
|
app.handle_key(Key::Char('\n'));
|
|
|
|
assert_eq!(app.draft.ssid, "demo-open");
|
|
assert_eq!(app.draft.security, SecurityKind::Open);
|
|
}
|
|
}
|