From 7d4f28f950451fdd3c04c0f28921bfa8f9feaca8 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sun, 3 Nov 2024 17:48:52 -0800 Subject: [PATCH] Move fast, break things Data structures and logic was changing very fast. Don't have the time to break downt the individual sections ATM. Sorry future reader (me). --- .config/config.json5 | 18 +- Cargo.lock | 18 +- Cargo.toml | 5 + src/action.rs | 12 +- src/app.rs | 107 ++++++----- src/components.rs | 1 + src/components/footer.rs | 6 +- src/components/left.rs | 160 ++++++++++++++-- src/components/popup.rs | 36 +++- src/components/right.rs | 28 ++- src/components/state.rs | 113 ++++++++++++ src/config.rs | 2 +- src/main.rs | 1 + src/system.rs | 19 ++ src/system/cpu.rs | 28 +++ src/system/disk.rs | 364 +++++++++++++++++++++++++++++++++++++ src/system/diskpart.rs | 382 +++++++++++++++++++++++++++++++++++++++ src/system/drivers.rs | 103 +++++++++++ 18 files changed, 1331 insertions(+), 72 deletions(-) create mode 100644 src/system.rs create mode 100644 src/system/cpu.rs create mode 100644 src/system/disk.rs create mode 100644 src/system/diskpart.rs create mode 100644 src/system/drivers.rs diff --git a/.config/config.json5 b/.config/config.json5 index 1c953c3..7535430 100644 --- a/.config/config.json5 +++ b/.config/config.json5 @@ -1,7 +1,6 @@ { "keybindings": { - "Home": { - "": "PrevScreen", + "ScanDisks": { "": "Process", "": "Quit", // Quit the application "": "Quit", // Another way to quit @@ -9,14 +8,26 @@ "": "Suspend" // Suspend the application }, "SelectDisks": { - "": "PrevScreen", + "": "ScanDisks", "": "Process", + "": "KeyUp", + "": "KeyDown", "": "Quit", // Quit the application "": "Quit", // Another way to quit "": "Quit", // Yet another way to quit "": "Suspend" // Suspend the application }, "SelectParts": { + "": "PrevScreen", + "": "Process", + "": "KeyUp", + "": "KeyDown", + "": "Quit", // Quit the application + "": "Quit", // Another way to quit + "": "Quit", // Yet another way to quit + "": "Suspend" // Suspend the application + }, + "Confirm": { "": "PrevScreen", "": "Process", "": "Quit", // Quit the application @@ -29,7 +40,6 @@ "": "Quit", // Quit the application "": "Quit", // Another way to quit "": "Quit", // Yet another way to quit - "": "Suspend" // Suspend the application }, } } diff --git a/Cargo.lock b/Cargo.lock index bd5dc00..ee8bcdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -527,19 +527,24 @@ dependencies = [ "json5", "lazy_static", "libc", + "once_cell", "pretty_assertions", "ratatui", + "raw-cpuid", + "regex", "serde", "serde_json", "signal-hook", "strip-ansi-escapes", "strum", + "tempfile", "tokio", "tokio-util", "tracing", "tracing-error", "tracing-subscriber", "vergen-gix", + "walkdir", ] [[package]] @@ -1881,6 +1886,15 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "raw-cpuid" +version = "11.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_syscall" version = "0.5.7" @@ -1903,9 +1917,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 81bb47b..7e4c451 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,18 +43,23 @@ human-panic = "2.0.1" json5 = "0.4.1" lazy_static = "1.5.0" libc = "0.2.158" +once_cell = "1.20.2" pretty_assertions = "1.4.0" ratatui = { version = "0.28.1", features = ["serde", "macros"] } +raw-cpuid = "11.2.0" +regex = "1.11.1" serde = { version = "1.0.208", features = ["derive"] } serde_json = "1.0.125" signal-hook = "0.3.17" strip-ansi-escapes = "0.2.0" strum = { version = "0.26.3", features = ["derive"] } +tempfile = "3.13.0" tokio = { version = "1.39.3", features = ["full"] } tokio-util = "0.7.11" tracing = "0.1.40" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] } +walkdir = "2.5.0" [build-dependencies] anyhow = "1.0.86" diff --git a/src/action.rs b/src/action.rs index a8615c9..b582068 100644 --- a/src/action.rs +++ b/src/action.rs @@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize}; use strum::Display; -use crate::app::Mode; +use crate::{app::Mode, system::disk::Disk}; #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] pub enum Action { @@ -27,11 +27,17 @@ pub enum Action { Resume, Quit, ClearScreen, + DismissPopup, + DisplayPopup(String), Error(String), Help, + KeyUp, + KeyDown, SetMode(Mode), PrevScreen, + NextScreen, Process, - SelectDisks(Option, Option), // indicies for (source, dest) - SelectParts(Option, Option), // indicies for (boot, os) + ScanDisks, + Select(Option, Option), // indicies for (source, dest) or (boot, os) + UpdateDiskList(Vec), } diff --git a/src/app.rs b/src/app.rs index daf28eb..3309712 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,7 +13,7 @@ // You should have received a copy of the GNU General Public License // along with Deja-vu. If not, see . // -use std::{collections::HashMap, iter::zip}; +use std::iter::zip; use color_eyre::Result; use crossterm::event::KeyEvent; @@ -40,10 +40,11 @@ pub struct App { tick_rate: f64, frame_rate: f64, components: Vec>, - component_indices: HashMap, should_quit: bool, should_suspend: bool, - mode: Mode, + cur_mode: Mode, + prev_mode: Mode, + selections: Vec>, last_tick_key_events: Vec, action_tx: mpsc::UnboundedSender, action_rx: mpsc::UnboundedReceiver, @@ -52,32 +53,13 @@ pub struct App { #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Mode { #[default] - Home, + ScanDisks, SelectDisks, SelectParts, + Confirm, Done, } -impl Mode { - pub fn next_screen(cur_mode: Mode) -> Mode { - match cur_mode { - Mode::Home => Mode::SelectDisks, - Mode::SelectDisks => Mode::SelectParts, - Mode::SelectParts => Mode::Done, - Mode::Done => Mode::Done, - } - } - - pub fn prev_screen(cur_mode: Mode) -> Mode { - match cur_mode { - Mode::Home => Mode::Home, - Mode::SelectDisks => Mode::Home, - Mode::SelectParts => Mode::SelectDisks, - Mode::Done => Mode::SelectParts, - } - } -} - impl App { pub fn new(tick_rate: f64, frame_rate: f64) -> Result { let (action_tx, action_rx) = mpsc::unbounded_channel(); @@ -92,24 +74,45 @@ impl App { Box::new(Footer::new()), Box::new(Popup::new()), ], - component_indices: HashMap::from([ - (String::from("title"), 0), - (String::from("fps"), 1), - (String::from("left"), 2), - (String::from("right"), 3), - (String::from("footer"), 4), - (String::from("popup"), 5), - ]), should_quit: false, should_suspend: false, config: Config::new()?, - mode: Mode::Home, + cur_mode: Mode::ScanDisks, + prev_mode: Mode::ScanDisks, + selections: vec![None, None], last_tick_key_events: Vec::new(), action_tx, action_rx, }) } + pub fn next_mode(&mut self) -> Option { + let new_mode = match self.cur_mode { + Mode::ScanDisks => Mode::SelectDisks, + Mode::SelectDisks | Mode::SelectParts => { + if self.selections[1].is_some() { + Mode::Confirm + } else { + self.cur_mode + } + } + Mode::Done => Mode::Done, + Mode::Confirm => match self.prev_mode { + Mode::SelectDisks => Mode::SelectParts, + Mode::SelectParts => Mode::Done, + _ => self.cur_mode, + }, + }; + if new_mode != self.cur_mode { + if self.cur_mode != Mode::Confirm { + self.prev_mode = self.cur_mode; + } + Some(new_mode) + } else { + None + } + } + pub async fn run(&mut self) -> Result<()> { let mut tui = Tui::new()? // .mouse(true) // uncomment this line to enable mouse support @@ -169,7 +172,7 @@ impl App { fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> { let action_tx = self.action_tx.clone(); - let Some(keymap) = self.config.keybindings.get(&self.mode) else { + let Some(keymap) = self.config.keybindings.get(&self.cur_mode) else { return Ok(()); }; match keymap.get(&vec![key]) { @@ -207,13 +210,35 @@ impl App { Action::ClearScreen => tui.terminal.clear()?, Action::Resize(w, h) => self.handle_resize(tui, w, h)?, Action::Render => self.render(tui)?, - Action::PrevScreen => self - .action_tx - .send(Action::SetMode(Mode::prev_screen(self.mode)))?, - Action::Process => self - .action_tx - .send(Action::SetMode(Mode::next_screen(self.mode)))?, - Action::SetMode(new_mode) => self.mode = new_mode, + Action::PrevScreen => { + self.action_tx.send(Action::SetMode(self.prev_mode))?; + self.action_tx.send(Action::Select(None, None))?; + } + Action::NextScreen => { + if let Some(mode) = self.next_mode() { + self.action_tx.send(Action::SetMode(mode))?; + } + } + Action::Select(one, two) => { + self.selections[0] = one; + self.selections[1] = two; + } + Action::SetMode(new_mode) => { + self.cur_mode = new_mode; + match new_mode { + Mode::ScanDisks => { + self.prev_mode = self.cur_mode; + self.action_tx.send(Action::Select(None, None))?; + self.action_tx.send(Action::ScanDisks)?; + } + Mode::Done => { + self.action_tx.send(Action::DisplayPopup(String::from( + "COMPLETE\n\nThank you for using this tool!", + )))?; + } + _ => {} + } + } _ => {} } for component in self.components.iter_mut() { diff --git a/src/components.rs b/src/components.rs index 8b90898..6d1fd97 100644 --- a/src/components.rs +++ b/src/components.rs @@ -28,6 +28,7 @@ pub mod fps; pub mod left; pub mod popup; pub mod right; +pub mod state; pub mod title; /// `Component` is a trait that represents a visual and interactive element of the user interface. diff --git a/src/components/footer.rs b/src/components/footer.rs index ff39728..6b1b3d9 100644 --- a/src/components/footer.rs +++ b/src/components/footer.rs @@ -57,9 +57,13 @@ impl Component for Footer { } Action::SetMode(new_mode) => { self.footer_text = match new_mode { + Mode::ScanDisks => String::from("(Enter) to start / (q) to quit"), Mode::SelectDisks => String::from( - "(Enter) to select / (b) to go back / (i) to install driver / (q) to quit", + "(Enter) to select / / (i) to install driver / (r) to rescan / (q) to quit", ), + Mode::Confirm => { + String::from("(Enter) to confirm / (b) to go back / (q) to quit") + } Mode::Done => String::from("(Enter) or (q) to quit"), _ => String::from("(Enter) to select / (b) to go back / (q) to quit"), } diff --git a/src/components/left.rs b/src/components/left.rs index 589cb1e..e12403d 100644 --- a/src/components/left.rs +++ b/src/components/left.rs @@ -18,19 +18,23 @@ use crossterm::event::KeyEvent; use ratatui::{prelude::*, widgets::*}; use tokio::sync::mpsc::UnboundedSender; -use super::Component; -use crate::{action::Action, app::Mode, config::Config}; +use super::{state::StatefulList, Component}; +use crate::{action::Action, app::Mode, config::Config, system::disk::Disk}; #[derive(Default)] pub struct Left { command_tx: Option>, config: Config, title_text: String, + item_list: StatefulList, + mode: Mode, + selections: Vec>, } impl Left { pub fn new() -> Self { Self { + selections: vec![None, None], title_text: String::from("Home"), ..Default::default() } @@ -62,12 +66,92 @@ impl Component for Left { Action::Render => { // add any logic here that should run on every render } - Action::SetMode(new_mode) => match new_mode { - Mode::Home => self.title_text = String::from("Home"), - Mode::SelectDisks => self.title_text = String::from("Select Source Disk"), - Mode::SelectParts => self.title_text = String::from("Select Boot Partition"), - Mode::Done => self.title_text = String::from("Done"), + Action::KeyUp => self.item_list.previous(), + Action::KeyDown => self.item_list.next(), + Action::Process => match self.mode { + Mode::ScanDisks => { + if let Some(command_tx) = self.command_tx.clone() { + command_tx.send(Action::ScanDisks)?; + } + } + Mode::Confirm => { + if let Some(command_tx) = self.command_tx.clone() { + command_tx.send(Action::NextScreen)?; + } + } + Mode::SelectDisks | Mode::SelectParts => { + if let Some(index) = self.item_list.state.selected() { + if let Some(command_tx) = self.command_tx.clone() { + let mut selection_one: Option = None; + let mut selection_two: Option = None; + + // Get selection(s) + if self.selections[0].is_none() { + // First selection + selection_one = Some(index); + selection_two = None; + } else { + // Second selection + if let Some(source_index) = self.selections[0] { + if index != source_index { + selection_one = self.selections[0]; + selection_two = Some(index); + } + } + } + + // Send selection(s) + // NOTE: This is needed to keep the app and all components in sync + match self.mode { + Mode::SelectDisks => { + command_tx + .send(Action::Select(selection_one, selection_two))?; + } + Mode::SelectParts => { + command_tx + .send(Action::Select(selection_one, selection_two))?; + } + _ => {} + }; + + // Advance screen if both selections made + if selection_two.is_some() { + command_tx.send(Action::NextScreen)?; + } + } + } + } + _ => {} }, + Action::Select(None, None) => self.selections = vec![None, None], + Action::Select(Some(index), None) => self.selections[0] = Some(index), + Action::SetMode(new_mode) => { + let prev_mode = self.mode; + self.mode = new_mode; + match new_mode { + Mode::ScanDisks => self.title_text = String::from(""), + Mode::SelectDisks => { + self.selections[0] = None; + self.selections[1] = None; + self.title_text = String::from("Select Source and Destination Disks") + } + Mode::SelectParts => { + self.selections[0] = None; + self.selections[1] = None; + self.title_text = String::from("Select Boot and OS Partitions") + } + Mode::Confirm => match prev_mode { + Mode::SelectDisks => self.title_text = String::from("Confirm Selections"), + Mode::SelectParts => { + self.title_text = String::from("Confirm Selections (Again)") + } + _ => {} + }, + + Mode::Done => self.title_text = String::from("Done"), + } + } + Action::UpdateDiskList(disks) => self.item_list.set_items(disks), _ => {} } Ok(None) @@ -85,13 +169,61 @@ impl Component for Left { .block(Block::default().borders(Borders::NONE)); frame.render_widget(title, title_area); - // Body - let paragraph = Paragraph::new("Body text...").block( - Block::default() - .borders(Borders::ALL) - .padding(Padding::new(1, 1, 1, 1)), - ); - frame.render_widget(paragraph, body_area); + // Body (scan disks) + if self.item_list.items.is_empty() { + let paragraph = Paragraph::new(String::from("Press Enter to start!")).block( + Block::default() + .borders(Borders::ALL) + .padding(Padding::new(1, 1, 1, 1)), + ); + frame.render_widget(paragraph, body_area); + + // Bail early + return Ok(()); + } + + // Body (confirm) + if self.mode == Mode::Confirm { + let paragraph = Paragraph::new(String::from("Are the listed selections correct?")) + .block( + Block::default() + .borders(Borders::ALL) + .padding(Padding::new(1, 1, 1, 1)), + ); + frame.render_widget(paragraph, body_area); + + // Bail early + return Ok(()); + } + + // Body (list) + let mut list_items = Vec::::new(); + if !self.item_list.items.is_empty() { + for (index, item) in self.item_list.items.iter().enumerate() { + let mut item_style = Style::default(); + let mut item_text_tail = ""; + if let Some(selection_one) = self.selections[0] { + if selection_one == index { + item_style = Style::new().yellow(); + item_text_tail = " ~already selected~"; + } + } + list_items.push( + ListItem::new(format!(" {item}\n{item_text_tail}\n\n")).style(item_style), + ); + } + } + let list = List::new(list_items) + .block( + Block::default() + .borders(Borders::ALL) + .padding(Padding::new(1, 1, 1, 1)), + ) + .highlight_spacing(HighlightSpacing::Always) + .highlight_style(Style::new().green().bold()) + .highlight_symbol(" --> ") + .repeat_highlight_symbol(false); + frame.render_stateful_widget(list, body_area, &mut self.item_list.state); // Done Ok(()) diff --git a/src/components/popup.rs b/src/components/popup.rs index 06a14f2..9e073d5 100644 --- a/src/components/popup.rs +++ b/src/components/popup.rs @@ -1,3 +1,4 @@ +use clap::command; // This file is part of Deja-vu. // // Deja-vu is free software: you can redistribute it and/or modify it @@ -18,17 +19,27 @@ use ratatui::{prelude::*, widgets::*}; use tokio::sync::mpsc::UnboundedSender; use super::Component; -use crate::{action::Action, config::Config}; +use crate::{ + action::Action, + app::Mode, + config::Config, + system::disk::{get_disks, Disk}, +}; #[derive(Default)] pub struct Popup { command_tx: Option>, config: Config, + disks: Vec, + popup_text: String, } impl Popup { pub fn new() -> Self { - Self::default() + Self { + popup_text: String::from("Scanning Disks..."), + ..Default::default() + } } } @@ -51,16 +62,35 @@ impl Component for Popup { Action::Render => { // add any logic here that should run on every render } + Action::DismissPopup => self.popup_text.clear(), + Action::DisplayPopup(new_text) => self.popup_text = String::from(new_text), + Action::ScanDisks => { + if let Some(command_tx) = self.command_tx.clone() { + self.disks = get_disks(); + command_tx.send(Action::NextScreen)?; + } + } + Action::SetMode(Mode::SelectDisks) => { + self.popup_text.clear(); + if let Some(command_tx) = self.command_tx.clone() { + command_tx.send(Action::UpdateDiskList(self.disks.clone()))?; + } + } _ => {} } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + if self.popup_text.is_empty() { + // Only show popup if popup_text is set + return Ok(()); + } + let popup_block = Block::default() .borders(Borders::ALL) .style(Style::default().cyan().bold()); - let popup = Paragraph::new(vec![Line::from(Span::raw("Popup text..."))]) + let popup = Paragraph::new(vec![Line::from(Span::raw(&self.popup_text))]) .block(popup_block) .centered() .wrap(Wrap { trim: false }); diff --git a/src/components/right.rs b/src/components/right.rs index 4567e38..15aa6fb 100644 --- a/src/components/right.rs +++ b/src/components/right.rs @@ -19,17 +19,24 @@ use ratatui::{prelude::*, widgets::*}; use tokio::sync::mpsc::UnboundedSender; use super::Component; -use crate::{action::Action, config::Config}; +use crate::{action::Action, app::Mode, config::Config}; #[derive(Default)] pub struct Right { command_tx: Option>, config: Config, + cur_mode: Mode, + prev_mode: Mode, + all_modes: Vec, + selections: Vec>, } impl Right { pub fn new() -> Self { - Self::default() + Self { + selections: vec![None, None], + ..Default::default() + } } } @@ -58,6 +65,17 @@ impl Component for Right { Action::Render => { // add any logic here that should run on every render } + Action::Select(one, two) => { + self.selections[0] = one; + self.selections[1] = two; + } + Action::SetMode(new_mode) => { + if self.cur_mode != Mode::Confirm { + self.prev_mode = self.cur_mode; + } + self.cur_mode = new_mode; + self.all_modes.push(new_mode); + } _ => {} } Ok(None) @@ -76,7 +94,11 @@ impl Component for Right { frame.render_widget(title, title_area); // Body - let paragraph = Paragraph::new("Some info...").block( + let paragraph = Paragraph::new(format!( + "Prev Mode: {:?} // Cur Mode: {:?}\n{:?}\n{:?}", + self.prev_mode, self.cur_mode, self.all_modes, self.selections, + )) + .block( Block::default() .borders(Borders::ALL) .padding(Padding::new(1, 1, 1, 1)), diff --git a/src/components/state.rs b/src/components/state.rs index ffbe662..95a303a 100644 --- a/src/components/state.rs +++ b/src/components/state.rs @@ -13,3 +13,116 @@ // You should have received a copy of the GNU General Public License // along with Deja-vu. If not, see . // +use std::collections::{hash_map, HashMap}; + +use ratatui::widgets::ListState; + +#[derive(Default, Debug, Clone, PartialEq)] +pub struct StatefulList { + pub state: ListState, + pub items: Vec, + pub last_selected: Option, + pub selected: HashMap, +} + +impl StatefulList { + #[must_use] + pub fn new() -> StatefulList { + StatefulList { + state: ListState::default(), + items: Vec::new(), + last_selected: None, + selected: HashMap::new(), + } + } + + pub fn clear_items(&mut self) { + // Clear list and rebuild with provided items + self.items.clear(); + } + + #[must_use] + pub fn get_selected(&self) -> Option<&T> { + if let Some(i) = self.state.selected() { + self.items.get(i) + } else { + None + } + } + + pub fn pop_selected(&mut self) -> Option { + if let Some(i) = self.state.selected() { + Some(self.items[i].clone()) + } else { + None + } + } + + fn select_first_item(&mut self) { + if self.items.is_empty() { + self.state.select(None); + } else { + self.state.select(Some(0)); + } + self.last_selected = None; + } + + pub fn set_items(&mut self, items: Vec) { + // Clear list and rebuild with provided items + self.clear_items(); + for item in items { + self.items.push(item); + } + + // Reset state and select first item (if available) + self.state = ListState::default(); + self.select_first_item(); + } + + pub fn next(&mut self) { + if self.items.is_empty() { + return; + } + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => self.last_selected.unwrap_or(0), + }; + self.state.select(Some(i)); + } + + pub fn previous(&mut self) { + if self.items.is_empty() { + return; + } + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => self.last_selected.unwrap_or(0), + }; + self.state.select(Some(i)); + } + pub fn toggle_selected(&mut self) { + if let Some(i) = self.state.selected() { + if let hash_map::Entry::Vacant(e) = self.selected.entry(i) { + // Was NOT selected, WILL be shortly + self.last_selected = Some(i); + e.insert(true); + } else { + // WAS selected, will NOT be shortly + self.last_selected = None; + self.selected.remove(&i); + } + } + } +} diff --git a/src/config.rs b/src/config.rs index 0099158..cf2e033 100644 --- a/src/config.rs +++ b/src/config.rs @@ -520,7 +520,7 @@ mod tests { let c = Config::new()?; assert_eq!( c.keybindings - .get(&Mode::Home) + .get(&Mode::ScanDisks) // i.e. Home .unwrap() .get(&parse_key_sequence("").unwrap_or_default()) .unwrap(), diff --git a/src/main.rs b/src/main.rs index 4024bec..8d440bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,7 @@ mod components; mod config; mod errors; mod logging; +mod system; mod tui; #[tokio::main] diff --git a/src/system.rs b/src/system.rs new file mode 100644 index 0000000..a923b3e --- /dev/null +++ b/src/system.rs @@ -0,0 +1,19 @@ +// This file is part of Deja-vu. +// +// Deja-vu is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Deja-vu is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Deja-vu. If not, see . +// +pub mod cpu; +pub mod disk; +pub mod diskpart; +pub mod drivers; diff --git a/src/system/cpu.rs b/src/system/cpu.rs new file mode 100644 index 0000000..bdca0b2 --- /dev/null +++ b/src/system/cpu.rs @@ -0,0 +1,28 @@ +// This file is part of Deja-vu. +// +// Deja-vu is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Deja-vu is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Deja-vu. If not, see . +// + +/// CPU Functions +#[must_use] +pub fn get_cpu_name() -> String { + let cpuid = raw_cpuid::CpuId::new(); + if let Some(name) = cpuid.get_processor_brand_string() { + String::from(name.as_str()) + } else if let Some(vendor) = cpuid.get_vendor_info() { + format!("{vendor}: Unknown Model") + } else { + String::from("Unknown") + } +} diff --git a/src/system/disk.rs b/src/system/disk.rs new file mode 100644 index 0000000..f6ca11b --- /dev/null +++ b/src/system/disk.rs @@ -0,0 +1,364 @@ +// This file is part of Deja-vu. +// +// Deja-vu is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Deja-vu is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Deja-vu. If not, see . +// +use serde::{Deserialize, Serialize}; +use std::{ + fmt, + process::{Command, Stdio}, + thread::sleep, + time::Duration, +}; + +use once_cell::sync::Lazy; +use regex::Regex; + +use crate::system::diskpart; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Disk { + pub conn_type: String, + pub id: String, + pub model: String, + pub part_type: PartitionTableType, + pub parts: Vec, + pub parts_description: Vec, + pub sector_size: usize, + pub serial: String, + pub size: u64, // In bytes +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Partition { + pub id: String, + pub is_selected: bool, + pub fs_type: Option, + pub label: Option, + pub letter: Option, + pub part_type: String, + pub size: u64, // In bytes +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PartitionTableType { + #[default] + Guid, + Legacy, +} + +impl Disk { + pub fn generate_descriptions(&mut self) { + for part in &self.parts { + self.parts_description.push(format!("{part}")); + } + } + #[must_use] + pub fn get_id(&self) -> &str { + self.id.as_str() + } +} + +impl fmt::Display for Disk { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if cfg!(windows) { + write!( + f, + "Disk {:<3} {:>11} {:<4} {} ({})", + self.id, + bytes_to_string(&self.size), + self.conn_type, + self.model, + self.serial, + ) + } else { + write!( + f, + "{:<14} {:>11} {:<4} {} ({})", + self.id, + bytes_to_string(&self.size), + self.conn_type, + self.model, + self.serial, + ) + } + } +} + +impl fmt::Display for Partition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut s: String; + let fs = if let Some(fs_type) = &self.fs_type { + format!("({fs_type})") + } else { + String::from("(?)") + }; + if cfg!(windows) { + s = format!( + "{:<8} {:>11} {:<7}", + self.id, + bytes_to_string(&self.size), + fs + ); + } else { + s = format!( + "{:<14} {:>11} {:<7}", + self.id, + bytes_to_string(&self.size), + fs + ); + } + if let Some(l) = &self.label { + s = format!("{s} \"{l}\""); + } + write!(f, "{s}") + } +} + +impl fmt::Display for PartitionTableType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + PartitionTableType::Guid => write!(f, "GPT / UEFI"), + PartitionTableType::Legacy => write!(f, "MBR / Legacy"), + } + } +} + +pub fn get_disks() -> Vec { + let disks; + if cfg!(windows) { + disks = diskpart::get_disks(); + } else { + disks = get_fake_disks(); + sleep(Duration::from_secs(2)); + } + disks +} + +#[allow(clippy::too_many_lines)] +#[must_use] +pub fn get_fake_disks() -> Vec { + let mut disks = vec![ + Disk { + conn_type: "SATA".to_string(), + id: "/dev/sda".to_string(), + model: "Samsung Evo 870".to_string(), + part_type: PartitionTableType::Legacy, + parts: vec![ + Partition { + id: String::from("1"), + fs_type: Some(String::from("NTFS")), + label: Some(String::from("System Reserved")), + part_type: String::from("7"), + size: 104_857_600, + ..Default::default() + }, + Partition { + id: String::from("2"), + fs_type: None, + label: None, + part_type: String::from("5"), + size: 267_806_310_400, + ..Default::default() + }, + Partition { + id: String::from("5"), + fs_type: Some(String::from("NTFS")), + label: Some(String::from("Win7")), + part_type: String::from("7"), + size: 267_701_452_800, + ..Default::default() + }, + Partition { + id: String::from("6"), + fs_type: Some(String::from("NTFS")), + label: Some(String::from("Tools")), + part_type: String::from("7"), + size: 524_288_000, + ..Default::default() + }, + ], + serial: "MDZ1243".to_string(), + size: 268_435_456_000, + ..Default::default() + }, + Disk { + conn_type: "SATA".to_string(), + id: "Disk 2".to_string(), + model: "ADATA Garbage".to_string(), + part_type: PartitionTableType::Legacy, + parts: vec![Partition { + id: String::from("1"), + fs_type: Some(String::from("NTFS")), + label: Some(String::from("Scratch")), + part_type: String::from("7"), + size: 249_998_951_424, + ..Default::default() + }], + serial: "000010000".to_string(), + size: 250_000_000_000, + ..Default::default() + }, + Disk { + conn_type: "NVMe".to_string(), + id: "/dev/nvme0n1".to_string(), + model: "Crucial P3 Plus".to_string(), + part_type: PartitionTableType::Guid, + parts: vec![ + Partition { + id: String::from("1"), + fs_type: Some(String::from("FAT32")), + label: Some(String::from("ESP")), + part_type: String::from("EFI"), + size: 272_629_760, + ..Default::default() + }, + Partition { + id: String::from("2"), + fs_type: None, + label: None, + part_type: String::from("MSR"), + size: 16_777_216, + ..Default::default() + }, + Partition { + id: String::from("3"), + fs_type: Some(String::from("NTFS")), + label: Some(String::from("Win10")), + part_type: String::from("MS Basic Data"), + size: 824_340_119_552, + ..Default::default() + }, + ], + serial: "80085".to_string(), + size: 268_435_456_000, + ..Default::default() + }, + Disk { + conn_type: "IDE".to_string(), + id: "/dev/hda".to_string(), + model: "Fireball".to_string(), + part_type: PartitionTableType::Guid, + parts: vec![ + Partition { + id: String::from("1"), + fs_type: Some(String::from("FAT32")), + label: Some(String::from("EFI Boot")), + part_type: String::from("EFI"), + size: 209_715_200, + ..Default::default() + }, + Partition { + id: String::from("2"), + fs_type: None, + label: None, + part_type: String::from("{48465300-0000-11AA-AA11-00306543ECAC}"), + size: 171_586_879_488, + ..Default::default() + }, + ], + serial: "0xfff".to_string(), + size: 171_798_691_840, + ..Default::default() + }, + Disk { + conn_type: "MISC".to_string(), + id: "A:\\".to_string(), + part_type: PartitionTableType::Legacy, + parts: Vec::new(), + model: "Iomega".to_string(), + serial: "000".to_string(), + size: 14, + ..Default::default() + }, + Disk::default(), + ]; + for disk in &mut disks { + disk.generate_descriptions(); + } + disks +} + +#[must_use] +pub fn get_disk_serial_number(id: &str) -> String { + let mut serial = String::new(); + if cfg!(windows) { + let output = Command::new("wmic") + .args([ + "diskdrive", + "where", + format!("index='{id}'").as_str(), + "get", + "serialNumber", + "/value", + ]) + .stdout(Stdio::piped()) + .output(); + if let Ok(result) = output { + let s = String::from_utf8_lossy(&result.stdout).trim().to_string(); + if s.len() >= 15 { + serial = String::from(&s[14..]); + } + } + } + serial +} + +/// Misc +/// +/// Clippy exception is fine because this supports sizes up to 2 EiB +#[allow(clippy::cast_precision_loss)] +#[must_use] +pub fn bytes_to_string(size: &u64) -> String { + let units = "KMGTPEZY".chars(); + let scale = 1024.0; + let mut size = *size as f64; + let mut suffix: Option = None; + for u in units { + if size < scale { + break; + } + size /= scale; + suffix = Some(u); + } + if let Some(s) = suffix { + format!("{size:4.2} {s}iB") + } else { + format!("{size:4.2} B") + } +} + +/// # Panics +/// +/// Will panic if s is not simliar to 32B, 64MB, etc... +pub fn string_to_bytes(s: &str) -> u64 { + static RE: Lazy = Lazy::new(|| Regex::new(r"(\d+)\s+(\w+)B").unwrap()); + let base: u64 = 1024; + let mut size: u64 = 0; + for (_, [size_str, suffix]) in RE.captures_iter(s).map(|c| c.extract()) { + let x: u64 = size_str.parse().unwrap(); + size += x; + match suffix { + "K" => size *= base, + "M" => size *= base.pow(2), + "G" => size *= base.pow(3), + "T" => size *= base.pow(4), + "P" => size *= base.pow(5), + "E" => size *= base.pow(6), + "Z" => size *= base.pow(7), + "Y" => size *= base.pow(8), + _ => (), + } + } + size +} diff --git a/src/system/diskpart.rs b/src/system/diskpart.rs new file mode 100644 index 0000000..7e0cfec --- /dev/null +++ b/src/system/diskpart.rs @@ -0,0 +1,382 @@ +// This file is part of Deja-vu. +// +// Deja-vu is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Deja-vu is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Deja-vu. If not, see . +// +use std::{ + collections::HashMap, + fs::File, + io::Write, + process::{Command, Output, Stdio}, +}; + +use once_cell::sync::Lazy; +use regex::Regex; +use tempfile::tempdir; +use tracing::{info, warn}; + +use crate::system::disk::{ + get_disk_serial_number, string_to_bytes, Disk, Partition, PartitionTableType, +}; + +static DEFAULT_MAX_DISKS: usize = 8; + +pub fn add_disk_details(disk: &mut Disk, disk_details: Option<&str>) { + static RE_DETAILS: Lazy = Lazy::new(|| { + Regex::new(r"(.*?)\r?\nDisk ID\s*:\s+(.*?)\r?\nType\s*:\s+(.*?)\r?\n").unwrap() + }); + static RE_UUID: Lazy = Lazy::new(|| { + Regex::new(r"^\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}$").unwrap() + }); + + // Get details + let details: String; + if let Some(details_str) = disk_details { + details = String::from(details_str); + } else { + let script = format!("select disk {}\r\ndetail disk", disk.id); + details = run_script(&script); + }; + + // Parse details + for (_, [model, part_type, conn_type]) in + RE_DETAILS.captures_iter(&details).map(|c| c.extract()) + { + disk.model = String::from(model); + disk.conn_type = String::from(conn_type); + if RE_UUID.is_match(part_type) { + disk.part_type = PartitionTableType::Guid; + } else { + disk.part_type = PartitionTableType::Legacy; + } + disk.serial = get_disk_serial_number(&disk.id); + } +} + +pub fn add_partition_details( + disk: &mut Disk, + disk_details: Option<&str>, + part_details: Option<&str>, +) { + // TODO: Convert to get_partition_details(disk_id: &str, disk_details [..]){} + // Drops need to have mutable access to disk + static RE_LIS: Lazy = + Lazy::new(|| Regex::new(r"Partition\s+(\d+)\s+\w+\s+(\d+\s+\w+B)").unwrap()); + + // List partition + let contents: String; + if let Some(details) = disk_details { + contents = String::from(details); + } else { + let script = format!("select disk {}\r\nlist partition", disk.id); + contents = run_script(&script); + }; + for (_, [number, size]) in RE_LIS.captures_iter(&contents).map(|c| c.extract()) { + let part = Partition { + id: String::from(number), + size: string_to_bytes(size), + ..Default::default() + }; + // pub fs_type: Option, + // pub label: Option, + // pub part_type: String, + disk.parts.push(part); + } + + // Detail parititon + let mut script = vec![format!("select disk {}", disk.id)]; + for part in &disk.parts { + if part_details.is_some() { + // Currently only used by tests + break; + } + script.push(format!( + // Remove/Assign included to ensure all (accessible) volumes have letters + "\r\nselect partition {}\r\nremove noerr\r\nassign noerr\r\ndetail partition", + part.id + )); + } + let part_contents: String; + if let Some(details) = part_details { + part_contents = String::from(details); + } else { + part_contents = run_script(script.join("\r\n").as_str()); + }; + parse_partition_details(disk, &part_contents); +} + +#[must_use] +pub fn build_dest_format_script(disk_id: &str, part_type: &PartitionTableType) -> String { + let mut script = vec!["select disk {disk_id}", "clean"]; + match part_type { + PartitionTableType::Guid => { + script.push("convert gpt"); + script.push("create partition efi size=260"); + script.push("format fs=fat32 quick label=ESP"); + script.push("create partition msr size=16"); + } + PartitionTableType::Legacy => { + script.push("create partition primary size=100"); + script.push("active"); + script.push("format fs=ntfs quick label=System"); + } + } + script.join("\r\n").replace("{disk_id}", disk_id) +} + +#[must_use] +pub fn build_get_disk_script(disk_nums: Option>) -> String { + let capacity = DEFAULT_MAX_DISKS * 3 + 1; + let script: String; + + // Get disk and partition details + if let Some(disks) = disk_nums { + // (Slower case) + let mut script_parts = Vec::with_capacity(capacity); + + // Get list of disks + script_parts.push(String::from("list disk")); + + // Add disks from provided list + for num in disks { + script_parts.push(format!("select disk {num}")); + script_parts.push(String::from("detail disk")); + script_parts.push(String::from("list partition")); + } + + // Done + script = script_parts.join("\n"); + } else { + // (Best case) + let mut script_parts = Vec::with_capacity(capacity); + + // Get list of disks + script_parts.push("list disk"); + + // Assuming first disk number is zero + script_parts.push("select disk 0"); + script_parts.push("detail disk"); + script_parts.push("list partition"); + + // Limit to 8 disks (if there's more the manual "worst" case will be used) + let mut i = 0; + while i < 8 { + script_parts.push("select disk next"); + script_parts.push("detail disk"); + script_parts.push("list partition"); + i += 1; + } + + // Done + script = script_parts.join("\n"); + } + + script +} + +pub fn get_disks() -> Vec { + static RE_DIS_DET: Lazy = Lazy::new(|| { + Regex::new(r"(?s)Disk (\d+) is now the selected disk.*?\r?\n\s*\r?\n(.*)").unwrap() + }); + static RE_DIS_LIS: Lazy = + Lazy::new(|| Regex::new(r"Disk\s+(\d+)\s+(\w+)\s+(\d+\s+\w+B)").unwrap()); + let mut contents: String; + let mut output; + let mut script: String; + + // Run diskpart and check result + script = build_get_disk_script(None); + output = run_script_raw(&script); + contents = String::from_utf8_lossy(&output.stdout).to_string(); + if let Some(return_code) = output.status.code() { + let disk_nums = parse_disk_numbers(&contents); + if return_code != 0 && !disk_nums.is_empty() { + // The base assumptions were correct! skipping fallback method + // + // Since the return_code was not zero, and at least one disk was detected, that + // means that the number of disks was less than DEFAULT_MAX_DISKS + // + // If the number of disks is exactly DEFAULT_MAX_DISKS then we may be omitting some + // disk(s) from the list so a manual check is needed + // This only adds one additional diskpart call, but avoiding it is preferred + // + // If zero disks were detected then either the first disk number wasn't zero or no + // disks were actually connected + // (this could be due to missing drivers but we're leaving that issue to the user) + info!("Disk assumptions correct!"); + } else { + // The base assumptions were wrong so we need to check which disk numbers are present + warn!("Disk assumptions incorrect, falling back to manual method."); + script = build_get_disk_script(Some(disk_nums)); + output = run_script_raw(&script); + contents = String::from_utf8_lossy(&output.stdout).to_string(); + } + } + + // Split Diskpart output contents into sections + let mut dp_sections = split_diskpart_disk_output(&contents); + + // Build Disk structs + // NOTE: A HashMap is used because it's possible to have gaps in the list of disks + // i.e. 0, 1, 3, 4 + // For instance, this can happen if a drive is disconnected after startup + let mut disks_map: HashMap<&str, Disk> = HashMap::with_capacity(DEFAULT_MAX_DISKS); + for (_, [number, _status, size]) in RE_DIS_LIS + .captures_iter(dp_sections.remove(0)) // This is the "list disk" section + .map(|c| c.extract()) + { + disks_map.insert( + number, + Disk { + id: String::from(number), + size: string_to_bytes(size), + ..Default::default() + }, + ); + } + + // Add Disk details + let mut disks_raw: Vec = Vec::with_capacity(DEFAULT_MAX_DISKS); + for section in dp_sections { + for (_, [id, details]) in RE_DIS_DET.captures_iter(section).map(|c| c.extract()) { + if let Some(mut disk) = disks_map.remove(id) { + // We remove the disk from the HashMap because we're moving it to the Vec + add_disk_details(&mut disk, Some(details)); + add_partition_details(&mut disk, Some(details), None); + disk.generate_descriptions(); + disks_raw.push(disk); + } + } + } + + // Return list of Disks + disks_raw +} + +pub fn parse_disk_numbers(contents: &str) -> Vec<&str> { + //Disk 0 is now the selected disk. + // + //Red Hat VirtIO SCSI Disk Device + //Disk ID: {E9CE8DFA-46B2-43C1-99BB-850C661CEE6B} + static RE: Lazy = + Lazy::new(|| Regex::new(r"\s+Disk\s+(\d+).*\n.*\n.*\nDisk ID:").unwrap()); + + let mut disk_nums = Vec::new(); + for (_, [number]) in RE.captures_iter(contents).map(|c| c.extract()) { + disk_nums.push(number); + } + disk_nums +} + +pub fn parse_partition_details(disk: &mut Disk, contents: &str) { + // TODO: Update multiple fields at once? + // https://stackoverflow.com/a/52905826 + static RE_PAR: Lazy = Lazy::new(|| { + Regex::new( + r"Partition (\d+)\r?\nType\s*: (\S+)(\r?\n.*){5}\s*(Volume.*\r?\n.*\r?\n|There is no volume)(.*)", + ) + .unwrap() + }); + static RE_VOL: Lazy = Lazy::new(|| { + // Volume ### Ltr Label Fs Type Size Status Info + // ---------- --- ----------- ----- ---------- ------- --------- -------- + // * Volume 1 S ESP FAT32 Partition 100 MB Healthy Hidden + Regex::new(r"..Volume (\d.{2}) (.{3}) (.{11}) (.{5})").unwrap() + }); + + for (part_index, (_, [_part_id, part_type, _, _vol_header, vol_line])) in RE_PAR + .captures_iter(contents) + .map(|c| c.extract()) + .enumerate() + { + let part = &mut disk.parts[part_index]; + + // Partition info + if !part_type.trim().is_empty() { + part.part_type = String::from(part_type.trim()); + } + + // Volume info + for (_, [_id, letter, label, fs_type]) in + RE_VOL.captures_iter(vol_line).map(|c| c.extract()) + { + if !label.trim().is_empty() { + part.label = Some(String::from(label.trim())); + } + if !letter.trim().is_empty() { + part.letter = Some(String::from(letter.trim())); + } + if !fs_type.trim().is_empty() { + part.fs_type = Some(String::from(fs_type.trim())); + } + } + } +} + +pub fn refresh_disk_info(disk: &mut Disk) { + // TODO: Needs refactor - assuming add_ functions are replaced with get_ variants + disk.parts.clear(); + disk.parts_description.clear(); + add_disk_details(disk, None); + add_partition_details(disk, None, None); + disk.generate_descriptions(); +} + +/// # Panics +/// +/// Will panic if diskpart fails to run the script or if the script if malformed +pub fn run_script_raw(script: &str) -> Output { + info!("Running Diskpart: {:?}", &script); + let temp_dir = tempdir().expect("Failed to create temp dir"); + let script_path = temp_dir.path().join("diskpart.script"); + let mut script_file = File::create(&script_path).expect("Failed to create temp file"); + script_file + .write_all(script.as_bytes()) + .expect("Failed to write script to disk"); + let output = Command::new("diskpart") + .args(["/s", format!("{}", script_path.display()).as_str()]) + .stdout(Stdio::piped()) + .output() + .expect("Failed to execute command"); + output +} + +#[must_use] +pub fn run_script(script: &str) -> String { + let output = run_script_raw(script); + String::from_utf8_lossy(&output.stdout).to_string() +} + +pub fn split_diskpart_disk_output(contents: &str) -> Vec<&str> { + // NOTE: A simple split isn't helpful since we want to include the matching lines + static RE: Lazy = + Lazy::new(|| Regex::new(r"Disk \d+ is now the selected disk").unwrap()); + let mut sections = Vec::new(); + let mut starts: Vec = vec![0]; + let mut ends: Vec = Vec::new(); + let _: Vec<_> = RE + .find_iter(contents) + .map(|m| { + ends.push(m.start() - 1); + starts.push(m.start()); + }) + .collect(); + ends.push(contents.len()); + let ranges: Vec<(&usize, &usize)> = starts.iter().zip(ends.iter()).collect(); + for range in ranges { + let start = *range.0; + let end = *range.1; + sections.push(&contents[start..end]); + } + sections +} diff --git a/src/system/drivers.rs b/src/system/drivers.rs new file mode 100644 index 0000000..d1f2d0b --- /dev/null +++ b/src/system/drivers.rs @@ -0,0 +1,103 @@ +// This file is part of Deja-vu. +// +// Deja-vu is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Deja-vu is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Deja-vu. If not, see . +// +use std::{env, fmt, fs::read_dir, path::PathBuf, process::Command}; + +use tracing::info; +use walkdir::WalkDir; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Driver { + pub name: String, + pub path: PathBuf, + pub inf_paths: Vec, +} + +impl Driver { + fn new() -> Driver { + Driver { + name: String::new(), + path: PathBuf::new(), + inf_paths: Vec::new(), + } + } +} + +impl Default for Driver { + fn default() -> Driver { + Driver::new() + } +} + +impl fmt::Display for Driver { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +/// # Panics +/// +/// Will panic if a driver fails to load +pub fn load(inf_paths: &Vec) { + // Load drivers into live environment + for inf in inf_paths { + let inf = inf.clone(); + if let Ok(path) = inf.into_os_string().into_string() { + info!("Installing driver: {}", &path); + Command::new("drvload") + .arg(path) + .output() + .expect("Failed to load driver"); + } + } +} + +pub fn scan() -> Vec { + let mut drivers: Vec = Vec::new(); + if let Ok(exe_path) = env::current_exe() { + let driver_path = exe_path.with_file_name("drivers"); + if let Ok(dir_entry) = read_dir(driver_path) { + for entry in dir_entry.flatten() { + if entry.path().is_dir() { + if let Ok(name) = entry.file_name().into_string() { + drivers.push(Driver { + name, + path: entry.path(), + inf_paths: Vec::new(), + }); + } + } + } + } + } + drivers.sort(); + drivers.reverse(); + for driver in &mut drivers { + if &driver.name[..1] == "0" { + driver.name = String::from(&driver.name[1..]); + } + for entry in WalkDir::new(&driver.path) + .into_iter() + .filter_map(Result::ok) + { + if let Some(ext) = entry.path().extension() { + if ext == "inf" { + driver.inf_paths.push(entry.into_path()); + } + } + } + } + drivers +}