Compare commits

...

23 commits

Author SHA1 Message Date
8a65313039
Fix bug dropping selected disk info
We don't need to reset disk_index_dest to None since it would be reset
before it's used anyway
2025-12-13 08:10:38 -08:00
e5f476f48d
Add more log/display info 2025-12-13 08:09:49 -08:00
a75911cb32
Add win-installer partitioning/formatting logic 2025-12-13 08:09:12 -08:00
cb7ba1a285
Invert backup/setup bool 2025-12-13 08:07:53 -08:00
87ccc80602
Track WIM scans to avoid stacking scans 2025-12-04 22:36:38 -08:00
c4c174b546
Handle missing WIM build/spbuild info
Needed for manual backup captures
2025-11-30 21:03:57 -08:00
4658624988
Add missing back keys and footer text 2025-11-30 21:03:24 -08:00
c572716ef9
Include size in WimImage 2025-11-30 20:46:55 -08:00
e873ec9602
Refactor WIM setup-image logic 2025-11-30 20:46:02 -08:00
09b204c0b0
Add option to include local backup WIMs 2025-11-29 05:41:08 -08:00
dd4733c991
Add username section 2025-11-29 03:25:22 -08:00
88dfe52b07
Add WIM Image selection section 2025-11-29 02:33:24 -08:00
89e768e3a4
Add WIM file selection section 2025-11-29 01:52:23 -08:00
70525ae6e0
Implement network WIM scan 2025-11-28 23:41:35 -08:00
a760773269
Add initial network scan code 2025-11-28 19:25:53 -08:00
cf3d71567e
Refactor scan_local_drives() 2025-11-28 19:25:18 -08:00
2aed8d130b
Make progress 2025-11-16 20:39:43 -08:00
3ce36c5a0f
Misc
???
2025-11-08 22:32:41 -08:00
e0932c7b48
Add first few screens 2025-11-08 21:46:31 -08:00
f51a4e85c4
Add more framework for workflow 2025-11-08 18:43:50 -08:00
cf87ac32af
Update Modes for win-installer 2025-11-08 17:48:17 -08:00
69c3feb838
Update WIM structs 2025-11-08 17:48:00 -08:00
94faae27ac
Add initial base for win-installer 2025-11-08 16:54:07 -08:00
23 changed files with 2292 additions and 31 deletions

63
Cargo.lock generated
View file

@ -689,7 +689,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users", "redox_users",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -2661,15 +2661,15 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.19.1" version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.3.2", "getrandom 0.3.2",
"once_cell", "once_cell",
"rustix 1.0.3", "rustix 1.0.3",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -2933,6 +2933,16 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
[[package]]
name = "tui-input"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "911e93158bf80bbc94bad533b2b16e3d711e1132d69a6a6980c3920a63422c19"
dependencies = [
"ratatui",
"unicode-width 0.2.0",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.18.0" version = "1.18.0"
@ -3126,6 +3136,30 @@ dependencies = [
"wit-bindgen-rt", "wit-bindgen-rt",
] ]
[[package]]
name = "win-installer"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"color-eyre",
"core",
"crossterm",
"futures",
"ratatui",
"serde",
"tempfile",
"tokio",
"toml",
"tracing",
"tracing-error",
"tracing-subscriber",
"tui-input",
"vergen-gix",
"windows-sys 0.61.2",
"xml",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -3176,6 +3210,12 @@ dependencies = [
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@ -3194,6 +3234,15 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.48.5" version = "0.48.5"
@ -3354,6 +3403,12 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "xml"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "838dd679b10a4180431ce7c2caa6e5585a7c8f63154c19ae99345126572e80cc"
[[package]] [[package]]
name = "yaml-rust2" name = "yaml-rust2"
version = "0.10.0" version = "0.10.0"

View file

@ -14,6 +14,9 @@
# along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. # along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
[workspace] [workspace]
members = ["core", "boot_diags", "deja_vu", "pe_menu"] members = ["core", "boot_diags", "deja_vu", "pe_menu", "win_installer"]
default-members = ["core", "boot_diags", "deja_vu", "pe_menu"] default-members = ["core", "boot_diags", "deja_vu", "pe_menu", "win_installer"]
resolver = "2" resolver = "2"
[profile.release]
lto = true

View file

@ -657,7 +657,11 @@ fn build_footer_string(cur_mode: Mode) -> String {
| Mode::PEMenu | Mode::PEMenu
| Mode::PreClone | Mode::PreClone
| Mode::PostClone | Mode::PostClone
| Mode::SelectTableType => { | Mode::ScanWinSources
| Mode::SelectTableType
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetUserName => {
panic!("This shouldn't happen?") panic!("This shouldn't happen?")
} }
} }
@ -770,7 +774,11 @@ fn build_left_items(app: &App) -> Action {
| Mode::Confirm | Mode::Confirm
| Mode::PreClone | Mode::PreClone
| Mode::Clone | Mode::Clone
| Mode::PostClone => { | Mode::PostClone
| Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetUserName => {
panic!("This shouldn't happen?") panic!("This shouldn't happen?")
} }
}; };

View file

@ -1,2 +1,17 @@
// 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 <https://www.gnu.org/licenses/>.
//
pub mod logview; pub mod logview;
pub mod progress; pub mod progress;

View file

@ -1,3 +1,18 @@
// 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 <https://www.gnu.org/licenses/>.
//
use color_eyre::Result; use color_eyre::Result;
use core::system::disk::PartitionTableType; use core::system::disk::PartitionTableType;
use core::tasks::Tasks; use core::tasks::Tasks;

View file

@ -188,5 +188,44 @@
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend"
}, },
"ScanWinSources": {
"<Enter>": "Process",
"<b>": "FindWimBackups",
"<n>": "FindWimNetwork",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"SelectWinSource": {
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<b>": "PrevScreen",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"SelectWinImage": {
"<Enter>": "Process",
"<Up>": "KeyUp",
"<Down>": "KeyDown",
"<b>": "PrevScreen",
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
"SetUserName": {
"<Esc>": "PrevScreen",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend"
},
}, },
"network_server": "SERVER",
"network_share": "SHARE",
"network_user": "USER",
"network_pass": "PASS"
} }

View file

@ -55,6 +55,10 @@ pub enum Action {
OpenTerminal, OpenTerminal,
Restart, Restart,
Shutdown, Shutdown,
// App (Win-Installer)
FindWimBackups,
FindWimNetwork,
SetUserName(String),
// Screens // Screens
DismissPopup, DismissPopup,
DisplayPopup(PopupType, String), DisplayPopup(PopupType, String),

View file

@ -22,6 +22,7 @@ use ratatui::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::Display; use strum::Display;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use tracing::info;
use super::Component; use super::Component;
use crate::{action::Action, config::Config}; use crate::{action::Action, config::Config};
@ -64,6 +65,7 @@ impl Component for Popup {
match action { match action {
Action::DismissPopup => self.popup_text.clear(), Action::DismissPopup => self.popup_text.clear(),
Action::DisplayPopup(new_type, new_text) => { Action::DisplayPopup(new_type, new_text) => {
info!("Show Popup ({new_type}): {new_text}");
self.popup_type = new_type; self.popup_type = new_type;
self.popup_text = format!("\n{new_text}"); self.popup_text = format!("\n{new_text}");
} }

View file

@ -54,6 +54,14 @@ pub struct Config {
pub keybindings: KeyBindings, pub keybindings: KeyBindings,
#[serde(default)] #[serde(default)]
pub styles: Styles, pub styles: Styles,
#[serde(default)]
pub network_server: String,
#[serde(default)]
pub network_share: String,
#[serde(default)]
pub network_user: String,
#[serde(default)]
pub network_pass: String,
} }
pub static PROJECT_NAME: &str = "DEJA-VU"; pub static PROJECT_NAME: &str = "DEJA-VU";
@ -76,16 +84,14 @@ impl Config {
let config_dir = get_config_dir(); let config_dir = get_config_dir();
let mut builder = config::Config::builder() let mut builder = config::Config::builder()
.set_default("app_title", default_config.app_title.as_str())? .set_default("app_title", default_config.app_title.as_str())?
.set_default( .set_default("clone_app_path", default_config.app_title.as_str())?
"clone_app_path", .set_default("conemu_path", default_config.app_title.as_str())?
String::from("C:\\Program Files\\Some Clone Tool\\app.exe"),
)?
.set_default(
"conemu_path",
String::from("C:\\Program Files\\ConEmu\\ConEmu64.exe"),
)?
.set_default("config_dir", config_dir.to_str().unwrap())? .set_default("config_dir", config_dir.to_str().unwrap())?
.set_default("data_dir", data_dir.to_str().unwrap())?; .set_default("data_dir", data_dir.to_str().unwrap())?
.set_default("network_server", default_config.app_title.as_str())?
.set_default("network_share", default_config.app_title.as_str())?
.set_default("network_user", default_config.app_title.as_str())?
.set_default("network_pass", default_config.app_title.as_str())?;
let config_files = [ let config_files = [
("config.json5", config::FileFormat::Json5), ("config.json5", config::FileFormat::Json5),

View file

@ -96,6 +96,12 @@ pub fn get_disk_description_right(
line_colors, line_colors,
}); });
}); });
if disk.parts_description.is_empty() {
description.push(DVLine {
line_parts: vec![String::from("-None-")],
line_colors: vec![Color::Reset],
});
}
description description
} }

View file

@ -42,6 +42,11 @@ pub enum Mode {
Clone, Clone,
SelectParts, SelectParts,
PostClone, PostClone,
// Windows Installer
ScanWinSources,
SelectWinSource,
SelectWinImage,
SetUserName,
// WinPE // WinPE
PEMenu, PEMenu,
} }

View file

@ -32,6 +32,12 @@ use crate::system::disk::{
static DEFAULT_MAX_DISKS: usize = 8; static DEFAULT_MAX_DISKS: usize = 8;
#[derive(Debug, PartialEq)]
pub enum FormatUseCase {
ApplyWimImage,
Clone,
}
pub struct RegexList { pub struct RegexList {
detail_all_disks: OnceLock<Regex>, detail_all_disks: OnceLock<Regex>,
detail_disk: OnceLock<Regex>, detail_disk: OnceLock<Regex>,
@ -205,7 +211,11 @@ pub fn get_partitions(
} }
#[must_use] #[must_use]
pub fn build_dest_format_script(disk_id: usize, part_type: &PartitionTableType) -> String { pub fn build_dest_format_script(
disk_id: usize,
part_type: &PartitionTableType,
format_use_case: FormatUseCase,
) -> String {
let disk_id = format!("{disk_id}"); let disk_id = format!("{disk_id}");
let mut script = vec!["automount enable noerr", "select disk {disk_id}", "clean"]; let mut script = vec!["automount enable noerr", "select disk {disk_id}", "clean"];
match part_type { match part_type {
@ -221,6 +231,10 @@ pub fn build_dest_format_script(disk_id: usize, part_type: &PartitionTableType)
script.push("format fs=ntfs quick label=System"); script.push("format fs=ntfs quick label=System");
} }
} }
if format_use_case == FormatUseCase::ApplyWimImage {
script.push("create partition primary");
script.push("format fs=ntfs quick label=Windows");
}
script.join("\r\n").replace("{disk_id}", &disk_id) script.join("\r\n").replace("{disk_id}", &disk_id)
} }

View file

@ -28,7 +28,10 @@ use core::{
line::{DVLine, get_disk_description_right, get_part_description}, line::{DVLine, get_disk_description_right, get_part_description},
state::Mode, state::Mode,
system::{ system::{
boot, cpu::get_cpu_name, disk::PartitionTableType, diskpart::build_dest_format_script, boot,
cpu::get_cpu_name,
disk::PartitionTableType,
diskpart::{FormatUseCase, build_dest_format_script},
drivers, drivers,
}, },
tasks::{Task, TaskResult, TaskType, Tasks}, tasks::{Task, TaskResult, TaskType, Tasks},
@ -129,7 +132,11 @@ impl App {
| Mode::LogView | Mode::LogView
| Mode::PEMenu | Mode::PEMenu
| Mode::Process | Mode::Process
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
} }
} }
@ -154,7 +161,11 @@ impl App {
| Mode::LogView | Mode::LogView
| Mode::PEMenu | Mode::PEMenu
| Mode::Process | Mode::Process
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
}; };
if new_mode == self.cur_mode { if new_mode == self.cur_mode {
@ -201,7 +212,8 @@ impl App {
&& let Some(disk) = disk_list.get(disk_index) && let Some(disk) = disk_list.get(disk_index)
{ {
let table_type = self.state.table_type.clone().unwrap(); let table_type = self.state.table_type.clone().unwrap();
let diskpart_script = build_dest_format_script(disk.id, &table_type); let diskpart_script =
build_dest_format_script(disk.id, &table_type, FormatUseCase::Clone);
self.tasks.add(TaskType::Diskpart(diskpart_script)); self.tasks.add(TaskType::Diskpart(diskpart_script));
} }
} }
@ -461,9 +473,8 @@ impl App {
self.set_mode(new_mode)?; self.set_mode(new_mode)?;
self.action_tx self.action_tx
.send(Action::UpdateFooter(build_footer_string(self.cur_mode)))?; .send(Action::UpdateFooter(build_footer_string(self.cur_mode)))?;
self.action_tx.send(build_left_items(self, self.cur_mode))?; self.action_tx.send(build_left_items(self))?;
self.action_tx self.action_tx.send(build_right_items(self))?;
.send(build_right_items(self, self.cur_mode))?;
match new_mode { match new_mode {
Mode::SelectTableType | Mode::Confirm => { Mode::SelectTableType | Mode::Confirm => {
// Select source/dest disks // Select source/dest disks
@ -634,16 +645,20 @@ fn build_footer_string(cur_mode: Mode) -> String {
| Mode::LogView | Mode::LogView
| Mode::PEMenu | Mode::PEMenu
| Mode::Process | Mode::Process
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
} }
} }
fn build_left_items(app: &App, cur_mode: Mode) -> Action { fn build_left_items(app: &App) -> Action {
let select_type: SelectionType; let select_type: SelectionType;
let title: String; let title: String;
let mut items = Vec::new(); let mut items = Vec::new();
let mut labels: Vec<String> = Vec::new(); let mut labels: Vec<String> = Vec::new();
match cur_mode { match app.cur_mode {
Mode::Home => { Mode::Home => {
select_type = SelectionType::Loop; select_type = SelectionType::Loop;
title = String::from("Home"); title = String::from("Home");
@ -707,16 +722,20 @@ fn build_left_items(app: &App, cur_mode: Mode) -> Action {
| Mode::LogView | Mode::LogView
| Mode::PEMenu | Mode::PEMenu
| Mode::Process | Mode::Process
| Mode::SetBootMode => panic!("This shouldn't happen?"), | Mode::ScanWinSources
| Mode::SelectWinSource
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
}; };
Action::UpdateLeft(title, labels, items, select_type) Action::UpdateLeft(title, labels, items, select_type)
} }
fn build_right_items(app: &App, cur_mode: Mode) -> Action { fn build_right_items(app: &App) -> Action {
let mut items = Vec::new(); let mut items = Vec::new();
let mut labels: Vec<Vec<DVLine>> = Vec::new(); let mut labels: Vec<Vec<DVLine>> = Vec::new();
let mut start_index = 0; let mut start_index = 0;
match cur_mode { match app.cur_mode {
Mode::InstallDrivers => { Mode::InstallDrivers => {
items.push(vec![DVLine { items.push(vec![DVLine {
line_parts: vec![String::from("CPU")], line_parts: vec![String::from("CPU")],

50
win_installer/Cargo.toml Normal file
View file

@ -0,0 +1,50 @@
# 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 <https://www.gnu.org/licenses/>.
[package]
name = "win-installer"
authors = ["2Shirt <2xShirt@gmail.com>"]
edition = "2024"
license = "GPL"
version = "0.1.0"
[dependencies]
core = { path = "../core" }
clap = { version = "4.4.5", features = [
"derive",
"cargo",
"wrap_help",
"unicode",
"string",
"unstable-styles",
] }
color-eyre = "0.6.3"
crossterm = { version = "0.28.1", features = ["event-stream"] }
futures = "0.3.30"
ratatui = "0.29.0"
serde = { version = "1.0.217", features = ["derive"] }
tokio = { version = "1.43.0", features = ["full"] }
toml = "0.8.13"
tracing = "0.1.41"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
tempfile = "3.23.0"
windows-sys = { version = "0.61.1", features = ["Win32_NetworkManagement_WNet"] }
xml = "1.1.0"
tui-input = "0.14.0"
[build-dependencies]
anyhow = "1.0.86"
vergen-gix = { version = "1.0.0", features = ["build", "cargo"] }

28
win_installer/build.rs Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
//
use anyhow::Result;
use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder};
fn main() -> Result<()> {
let build = BuildBuilder::all_build()?;
let gix = GixBuilder::all_git()?;
let cargo = CargoBuilder::all_cargo()?;
Emitter::default()
.add_instructions(&build)?
.add_instructions(&gix)?
.add_instructions(&cargo)?
.emit()
}

1044
win_installer/src/app.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
// 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 <https://www.gnu.org/licenses/>.
//
pub mod set_username;
pub mod wim_scan;

View file

@ -0,0 +1,135 @@
// 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 <https://www.gnu.org/licenses/>.
//
use color_eyre::Result;
use core::{action::Action, components::Component, config::Config, state::Mode, tui::Event};
use crossterm::event::KeyCode;
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, Paragraph},
};
use tokio::sync::mpsc::UnboundedSender;
use tui_input::{Input, InputRequest};
#[derive(Default)]
pub struct InputUsername {
command_tx: Option<UnboundedSender<Action>>,
config: Config,
input: Input,
mode: Mode,
}
impl InputUsername {
#[must_use]
pub fn new() -> Self {
Self {
input: Input::new(String::from("")),
..Default::default()
}
}
}
impl Component for InputUsername {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.command_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
self.config = config;
Ok(())
}
fn handle_events(&mut self, event: Option<Event>) -> Result<Option<Action>> {
if self.mode != Mode::SetUserName {
return Ok(None);
}
let action = match event {
Some(Event::Key(key_event)) => match key_event.code {
KeyCode::Backspace => {
self.input.handle(InputRequest::DeletePrevChar);
None
}
KeyCode::Char(c) => {
let ok_chars: Vec<char> = vec![' ', '-', '_'];
if c.is_ascii_alphanumeric() || ok_chars.contains(&c) {
self.input.handle(InputRequest::InsertChar(c));
}
None
}
KeyCode::Enter => {
let username = self.input.value();
Some(Action::SetUserName(String::from(username)))
}
KeyCode::Esc => Some(Action::SetMode(Mode::Home)),
_ => None,
},
Some(Event::Mouse(_)) => None,
_ => None,
};
Ok(action)
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::SetMode(mode) => self.mode = mode.clone(),
_ => {}
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
if self.mode != Mode::SetUserName {
// Bail early
return Ok(());
}
// Set areas
let [_, center_area, _] = Layout::horizontal([
Constraint::Length(1),
Constraint::Min(1),
Constraint::Length(1),
])
.areas(area);
let [_, input_area, _] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(3),
Constraint::Length(1),
])
.areas(center_area);
frame.render_widget(Clear, area);
let outer_block = Block::bordered().cyan().bold();
frame.render_widget(outer_block, area);
// Input Box
let width = input_area.width.max(3) - 3; // keep 2 for borders and 1 for cursor
let scroll = self.input.visual_scroll(width as usize);
let input = Paragraph::new(self.input.value())
.scroll((0, scroll as u16))
.white()
.block(Block::new().borders(Borders::ALL).title("Enter Username"));
frame.render_widget(input, input_area);
// Ratatui hides the cursor unless it's explicitly set. Position the cursor past the
// end of the input text and one line down from the border to the input line
let x = self.input.visual_cursor().max(scroll) - scroll + 1;
frame.set_cursor_position((input_area.x + x as u16, input_area.y + 1));
// Done
Ok(())
}
}

View file

@ -0,0 +1,154 @@
// 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 <https://www.gnu.org/licenses/>.
//
use core::{action::Action, components::Component, config::Config, state::Mode};
use std::{
iter::zip,
sync::{Arc, Mutex},
};
use color_eyre::Result;
use crossterm::event::KeyEvent;
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, List, ListItem, Padding, Paragraph},
};
use tokio::sync::mpsc::UnboundedSender;
use crate::wim::WimSources;
#[derive(Default)]
pub struct WimScan {
command_tx: Option<UnboundedSender<Action>>,
config: Config,
mode: Mode,
scan_network: bool,
wim_sources: Arc<Mutex<WimSources>>,
}
impl WimScan {
#[must_use]
pub fn new(wim_sources: Arc<Mutex<WimSources>>) -> Self {
let wim_sources = wim_sources.clone();
Self {
wim_sources,
..Default::default()
}
}
}
impl Component for WimScan {
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
let _ = key; // to appease clippy
Ok(None)
}
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.command_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
self.config = config;
Ok(())
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::FindWimNetwork => self.scan_network = true,
Action::SetMode(new_mode) => {
self.mode = new_mode;
}
_ => {}
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
if self.mode != Mode::ScanWinSources {
return Ok(());
}
frame.render_widget(Clear, area);
// Prep
let [left, right] = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(area);
let [left_title, left_body] = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.areas(left);
let [right_title, right_body] = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.areas(right);
// Titles
let titles = vec![
Paragraph::new(Line::from("Local").centered())
.block(Block::default().borders(Borders::NONE)),
Paragraph::new(Line::from("Network").centered())
.block(Block::default().borders(Borders::NONE)),
];
for (title, area) in zip(titles, [left_title, right_title]) {
frame.render_widget(title, area);
}
// WIM Info
if let Ok(wim_sources) = self.wim_sources.lock() {
// Local
let mut left_list = Vec::new();
if wim_sources.thread_local.is_some() {
left_list.push(ListItem::new("Scanning..."));
} else {
left_list.extend(
wim_sources
.local
.iter()
.map(|wimfile| ListItem::new(format!("{}\n\n", wimfile.path))),
);
}
let left_list = List::new(left_list).block(
Block::default()
.borders(Borders::ALL)
.padding(Padding::new(1, 1, 1, 1)),
);
frame.render_widget(left_list, left_body);
// Network
let mut right_list = Vec::new();
if wim_sources.thread_network.is_some() {
right_list.push(ListItem::new("Scanning..."));
} else {
right_list.extend(wim_sources.network.iter().map(|wimfile| {
ListItem::new(format!(
"{}\n\n",
wimfile.path.split("\\").last().unwrap_or(&wimfile.path)
))
}));
}
let right_list = List::new(right_list).block(
Block::default()
.borders(Borders::ALL)
.padding(Padding::new(1, 1, 1, 1)),
);
frame.render_widget(right_list, right_body);
}
// Done
Ok(())
}
}

36
win_installer/src/main.rs Normal file
View file

@ -0,0 +1,36 @@
// 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 <https://www.gnu.org/licenses/>.
//
use clap::Parser;
use color_eyre::Result;
use crate::app::App;
mod app;
mod components;
mod net;
mod state;
mod wim;
#[tokio::main]
async fn main() -> Result<()> {
core::errors::init()?;
core::logging::init()?;
let args = core::cli::Cli::parse();
let mut app = App::new(args.tick_rate, args.frame_rate)?;
app.run().await?;
Ok(())
}

68
win_installer/src/net.rs Normal file
View file

@ -0,0 +1,68 @@
// 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 <https://www.gnu.org/licenses/>.
//
//#![windows_subsystem = "windows"]
use std::ffi::CString;
use windows_sys::Win32::Foundation::NO_ERROR;
use windows_sys::Win32::NetworkManagement::WNet;
fn to_cstr(s: &str) -> CString {
CString::new(s).unwrap()
}
pub fn connect_network_share(
server: &str,
share: &str,
username: &str,
password: &str,
) -> Result<(), u32> {
let remote_name = to_cstr(&format!("\\\\{server}\\{share}"));
// init resources
let mut resources = WNet::NETRESOURCEA {
dwDisplayType: WNet::RESOURCEDISPLAYTYPE_SHAREADMIN,
dwScope: WNet::RESOURCE_GLOBALNET,
dwType: WNet::RESOURCETYPE_DISK,
dwUsage: WNet::RESOURCEUSAGE_ALL,
lpComment: std::ptr::null_mut(),
lpLocalName: std::ptr::null_mut(), // PUT a volume here if you want to mount as a windows volume
lpProvider: std::ptr::null_mut(),
lpRemoteName: remote_name.as_c_str().as_ptr() as *mut u8,
};
let username = to_cstr(username);
let password = to_cstr(password);
// mount
let result = unsafe {
let username_ptr = username.as_ptr();
let password_ptr = password.as_ptr();
WNet::WNetAddConnection2A(
&mut resources as *mut WNet::NETRESOURCEA,
password_ptr as *const u8,
username_ptr as *const u8,
//WNet::CONNECT_INTERACTIVE, // Interactive will show a system dialog in case credentials are wrong to retry with the password. Put 0 if you don't want it
0,
)
};
if result == NO_ERROR {
Ok(())
} else {
Err(result)
}
}

215
win_installer/src/state.rs Normal file
View file

@ -0,0 +1,215 @@
// 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 <https://www.gnu.org/licenses/>.
//
use std::{
fs::read_dir,
sync::{Arc, Mutex},
};
use core::{
config::Config,
system::{
disk::{Disk, PartitionTableType},
drivers,
},
};
use crate::{
net::connect_network_share,
wim::{WimFile, WimSources, parse_wim_file},
};
pub enum ScanType {
GeneralWimFiles, // Includes Windows installer WIMs
WindowsInstallers,
}
#[derive(Debug, Default)]
pub struct State {
pub config: Config,
pub disk_index_dest: Option<usize>,
pub disk_list: Arc<Mutex<Vec<Disk>>>,
pub driver: Option<drivers::Driver>,
pub driver_list: Vec<drivers::Driver>,
pub table_type: Option<PartitionTableType>,
pub username: Option<String>,
pub wim_file_index: Option<usize>,
pub wim_image_index: Option<usize>,
pub wim_sources: Arc<Mutex<WimSources>>,
}
impl State {
pub fn new(config: Config, disk_list: Arc<Mutex<Vec<Disk>>>) -> Self {
let wim_sources = Arc::new(Mutex::new(WimSources::new()));
State {
config,
disk_list,
wim_sources,
..Default::default()
}
}
pub fn reset_all(&mut self) {
self.wim_file_index = None;
self.wim_image_index = None;
if let Ok(mut sources) = self.wim_sources.lock() {
sources.reset_all();
}
}
pub fn reset_local(&mut self) {
if let Ok(mut sources) = self.wim_sources.lock() {
sources.reset_local();
}
}
pub fn reset_network(&mut self) {
if let Ok(mut sources) = self.wim_sources.lock() {
sources.reset_network();
}
}
pub fn scan_drivers(&mut self) {
self.driver_list = drivers::scan();
}
pub fn scan_wim_local(&mut self, scan_type: ScanType) {
let disk_list_arc = self.disk_list.clone();
let wim_sources_arc = self.wim_sources.clone();
let wim_sources_arc_inner = self.wim_sources.clone();
if let Ok(mut wim_sources) = wim_sources_arc.lock()
&& wim_sources.thread_local.is_none()
{
wim_sources.thread_local = Some(tokio::task::spawn(async move {
scan_local_drives(disk_list_arc, wim_sources_arc_inner, scan_type);
}));
}
}
pub fn scan_wim_network(&mut self) {
let wim_sources_arc = self.wim_sources.clone();
let wim_sources_arc_inner = self.wim_sources.clone();
if let Ok(mut wim_sources) = wim_sources_arc.lock()
&& wim_sources.thread_network.is_none()
{
let config = self.config.clone();
wim_sources.thread_network = Some(tokio::task::spawn(async move {
scan_network_share(config, wim_sources_arc_inner);
}));
}
}
}
fn get_subfolders(path_str: &str) -> Vec<String> {
if let Ok(read_dir) = read_dir(path_str) {
read_dir
.filter_map(|item| item.ok())
.map(|item| item.path().to_string_lossy().into_owned())
.collect()
} else {
// TODO: Use better error handling here?
Vec::new()
}
}
pub fn scan_local_drives(
disk_list_arc: Arc<Mutex<Vec<Disk>>>,
wim_sources_arc: Arc<Mutex<WimSources>>,
scan_type: ScanType,
) {
let mut to_check: Vec<String> = Vec::new();
let mut wim_files: Vec<WimFile> = Vec::new();
// Get drive letters
if let Ok(disk_list) = disk_list_arc.lock() {
disk_list.iter().for_each(|d| {
d.parts.iter().for_each(|p| {
if !p.letter.is_empty() {
match scan_type {
ScanType::GeneralWimFiles => {
to_check.append(&mut get_subfolders(&format!("{}:\\", &p.letter)));
}
ScanType::WindowsInstallers => {
to_check.push(format!("{}:\\Images", &p.letter));
}
}
}
});
})
}
// Scan drives
to_check.iter().for_each(|scan_path| {
let is_backup = !scan_path.ends_with("\\Images");
if let Ok(read_dir) = read_dir(scan_path) {
read_dir.for_each(|item| {
if let Ok(item) = item
&& item.file_name().to_string_lossy().ends_with(".wim")
&& let Some(path_str) = item.path().to_str()
&& let Ok(new_source) = parse_wim_file(path_str, is_backup)
{
wim_files.push(new_source);
}
});
}
});
// Done
wim_files.sort();
if let Ok(mut wim_sources) = wim_sources_arc.lock() {
wim_files
.into_iter()
.for_each(|file| wim_sources.add_local(file));
}
}
pub fn scan_network_share(config: Config, wim_sources_arc: Arc<Mutex<WimSources>>) {
let result = connect_network_share(
&config.network_server,
&config.network_share,
&config.network_user,
&config.network_pass,
);
let mut wim_files: Vec<WimFile> = Vec::new();
// Connect to share
if result.is_err() {
return;
}
// Scan share
let share_dir = format!("\\\\{}\\{}", &config.network_server, &config.network_share);
if let Ok(read_dir) = read_dir(share_dir) {
read_dir.for_each(|item| {
if let Ok(item) = item
&& item.file_name().to_string_lossy().ends_with(".wim")
&& let Some(path_str) = item.path().to_str()
&& let Ok(new_source) = parse_wim_file(path_str, true)
// Assuming all network sources are installers
{
wim_files.push(new_source);
}
});
}
// Done
wim_files.sort();
if let Ok(mut wim_sources) = wim_sources_arc.lock() {
wim_files
.into_iter()
.for_each(|file| wim_sources.add_network(file));
}
}

323
win_installer/src/wim.rs Normal file
View file

@ -0,0 +1,323 @@
// 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 <https://www.gnu.org/licenses/>.
//
use std::{
cmp::Ordering,
collections::HashMap,
env, fmt,
fs::File,
io::BufReader,
path::{Path, PathBuf},
process::Command,
sync::LazyLock,
};
use tempfile::NamedTempFile;
use tokio::task::JoinHandle;
use xml::reader::{EventReader, XmlEvent};
use core::system::disk::bytes_to_string;
static WIMINFO_EXE: LazyLock<String> = LazyLock::new(|| {
let program_files =
PathBuf::from(env::var("PROGRAMFILES").expect("Failed to resolve %PROGRAMFILES%"));
program_files
.join("wimlib/wiminfo.cmd")
.to_string_lossy()
.into_owned()
});
static WIN_BUILDS: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from([
// Windows 10
("10240", "1507 \"Threshold 1\""),
("10586", "1511 \"Threshold 2\""),
("14393", "1607 \"Redstone 1\""),
("15063", "1703 \"Redstone 2\""),
("16299", "1709 \"Redstone 3\""),
("17134", "1803 \"Redstone 4\""),
("17763", "1809 \"Redstone 5\""),
("18362", "1903 / 19H1"),
("18363", "1909 / 19H2"),
("19041", "2004 / 20H1"),
("19042", "20H2"),
("19043", "21H1"),
("19044", "21H2"),
("19045", "22H2"),
// Windows 11
("22000", "21H2"),
("22621", "22H2"),
("22631", "23H2"),
("26100", "24H2"),
("26200", "25H2"),
])
});
#[derive(Clone, Debug)]
pub struct WimFile {
pub path: String,
pub images: Vec<WimImage>,
pub is_backup: bool,
}
impl WimFile {
pub fn summary(&self) -> String {
let mut s = format!("{self}");
self.images.iter().for_each(|image| {
let image = format!("\n\t\t{image}");
s.push_str(&image);
});
s
}
}
impl fmt::Display for WimFile {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.path.split("\\").last().unwrap_or(&self.path))
}
}
impl PartialEq for WimFile {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
}
}
impl Eq for WimFile {}
impl Ord for WimFile {
fn cmp(&self, other: &Self) -> Ordering {
self.path.cmp(&other.path)
}
}
impl PartialOrd for WimFile {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Debug, Default)]
pub struct WimImage {
pub build: String,
pub index: String,
pub name: String,
pub size: u64,
pub spbuild: String,
pub version: String,
}
impl WimImage {
pub fn new() -> Self {
Default::default()
}
pub fn reset(&mut self) {
self.build.clear();
self.index.clear();
self.name.clear();
self.spbuild.clear();
self.version.clear();
}
}
impl fmt::Display for WimImage {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Windows 11 Home (24H2, 26100.xxxx)
let s = if self.version.is_empty() {
String::new()
} else {
format!("{}, ", self.version)
};
write!(
f,
"{} ({}{}.{}) [{}]",
self.name,
s,
self.build,
self.spbuild,
bytes_to_string(self.size)
)
}
}
#[derive(Debug, Default)]
pub struct WimSources {
pub local: Vec<WimFile>,
pub network: Vec<WimFile>,
pub thread_local: Option<JoinHandle<()>>,
pub thread_network: Option<JoinHandle<()>>,
}
impl WimSources {
pub fn new() -> Self {
Default::default()
}
pub fn add_local(&mut self, wim_file: WimFile) {
self.local.push(wim_file);
}
pub fn add_network(&mut self, wim_file: WimFile) {
self.network.push(wim_file);
}
pub fn get_file(&self, index: usize) -> WimFile {
let num_local = self.local.len();
let index = if index < num_local {
index
} else {
index - num_local
};
self.local.get(index).unwrap().clone()
}
pub fn get_file_list(&self) -> Vec<WimFile> {
let mut list = self.local.clone();
list.append(&mut self.network.clone());
list
}
pub fn poll(&mut self) {
let thread = self.thread_local.take();
if let Some(local) = thread
&& !local.is_finished()
{
// Task still going, keep tracking
self.thread_local = Some(local);
}
let thread = self.thread_network.take();
if let Some(network) = thread
&& !network.is_finished()
{
// Task still going, keep tracking
self.thread_network = Some(network);
}
}
pub fn reset_all(&mut self) {
self.local.clear();
self.network.clear();
}
pub fn reset_local(&mut self) {
self.local.clear();
}
pub fn reset_network(&mut self) {
self.network.clear();
}
}
fn get_wim_xml(wim_file: &str) -> std::io::Result<File> {
let tmp_file = NamedTempFile::new()?;
let _ = Command::new(&*WIMINFO_EXE)
.args([
wim_file,
"--extract-xml",
tmp_file.path().as_os_str().to_str().unwrap(),
])
.output()
.expect("Failed to extract XML data");
let file = File::open(tmp_file.path())?;
Ok(file)
}
pub fn parse_wim_file(wim_file: &str, is_backup: bool) -> std::io::Result<WimFile> {
let mut wim_images: Vec<WimImage> = Vec::new();
if !Path::new(wim_file).exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Failed to read WIM file",
));
};
let xml_file = get_wim_xml(wim_file).expect("Failed to open XML file");
let file = BufReader::new(xml_file);
let mut current_element = String::new();
let mut image = WimImage::new();
let parser = EventReader::new(file);
for e in parser {
match e {
Ok(XmlEvent::StartElement {
name, attributes, ..
}) => {
current_element = name.local_name.to_uppercase();
if current_element == "IMAGE" {
// Update index
if let Some(attr) = attributes.first()
&& attr.name.to_string().to_lowercase() == "index"
{
image.index = attr.value.clone();
}
}
}
Ok(XmlEvent::Characters(char_data)) => {
if current_element == "BUILD" {
let build = char_data.trim();
image.build = build.to_string();
image.version = WIN_BUILDS.get(build).map_or("", |v| v).to_string();
}
if current_element == "NAME" {
image.name = char_data.trim().to_string();
}
if current_element == "SPBUILD" {
image.spbuild = char_data.trim().to_string();
}
if current_element == "TOTALBYTES" {
let result = char_data.trim().parse::<u64>();
if let Ok(size) = result {
image.size = size;
}
}
}
Ok(XmlEvent::EndElement { name }) => {
if name.local_name.to_uppercase() == "IMAGE" {
if image.size == 0 {
break;
}
// Append image to list
if image.build.is_empty() {
image.build.push('?');
}
if image.spbuild.is_empty() {
image.spbuild.push('?');
}
if !image.name.is_empty() && !image.index.is_empty() {
wim_images.push(image.clone());
}
// Reset image
image.reset()
}
}
Err(_) => {
break;
}
_ => {}
}
}
let wim_file = WimFile {
path: wim_file.to_string(),
images: wim_images,
is_backup,
};
Ok(wim_file)
}