Compare commits

...

3 commits

Author SHA1 Message Date
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
11 changed files with 796 additions and 10 deletions

32
Cargo.lock generated
View file

@ -2661,9 +2661,9 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.19.1"
version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom 0.3.2",
@ -3126,6 +3126,28 @@ dependencies = [
"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",
"vergen-gix",
"xml",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -3354,6 +3376,12 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "xml"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "838dd679b10a4180431ce7c2caa6e5585a7c8f63154c19ae99345126572e80cc"
[[package]]
name = "yaml-rust2"
version = "0.10.0"

View file

@ -14,6 +14,6 @@
# along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
[workspace]
members = ["core", "boot_diags", "deja_vu", "pe_menu"]
default-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", "win_installer"]
resolver = "2"

View file

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

View file

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

View file

@ -129,7 +129,10 @@ impl App {
| Mode::LogView
| Mode::PEMenu
| Mode::Process
| Mode::SetBootMode => panic!("This shouldn't happen?"),
| Mode::ScanWinImages
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
}
}
@ -154,7 +157,10 @@ impl App {
| Mode::LogView
| Mode::PEMenu
| Mode::Process
| Mode::SetBootMode => panic!("This shouldn't happen?"),
| Mode::ScanWinImages
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
};
if new_mode == self.cur_mode {
@ -634,7 +640,10 @@ fn build_footer_string(cur_mode: Mode) -> String {
| Mode::LogView
| Mode::PEMenu
| Mode::Process
| Mode::SetBootMode => panic!("This shouldn't happen?"),
| Mode::ScanWinImages
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
}
}
@ -707,7 +716,10 @@ fn build_left_items(app: &App, cur_mode: Mode) -> Action {
| Mode::LogView
| Mode::PEMenu
| Mode::Process
| Mode::SetBootMode => panic!("This shouldn't happen?"),
| Mode::ScanWinImages
| Mode::SelectWinImage
| Mode::SetBootMode
| Mode::SetUserName => panic!("This shouldn't happen?"),
};
Action::UpdateLeft(title, labels, items, select_type)
}

48
win_installer/Cargo.toml Normal file
View file

@ -0,0 +1,48 @@
# 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"
xml = "1.1.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()
}

346
win_installer/src/app.rs Normal file
View file

@ -0,0 +1,346 @@
// 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, footer::Footer, left::Left, popup, right::Right, title::Title},
config::Config,
state::Mode,
tasks::{TaskType, Tasks},
tui::{Event, Tui},
};
use std::{
iter::zip,
path::PathBuf,
sync::{Arc, Mutex},
};
use color_eyre::Result;
use ratatui::{
crossterm::event::KeyEvent,
layout::{Constraint, Direction, Layout},
prelude::Rect,
};
use tokio::sync::mpsc;
use tracing::{debug, info};
use crate::state::State;
pub struct App<'a> {
// TUI
action_rx: mpsc::UnboundedReceiver<Action>,
action_tx: mpsc::UnboundedSender<Action>,
components: Vec<Box<dyn Component>>,
config: Config,
frame_rate: f64,
last_tick_key_events: Vec<KeyEvent>,
should_quit: bool,
should_suspend: bool,
tick_rate: f64,
// App
state: State<'a>,
mode: Mode,
tasks: Tasks,
}
impl App<'_> {
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
let (action_tx, action_rx) = mpsc::unbounded_channel();
let disk_list_arc = Arc::new(Mutex::new(Vec::new()));
let tasks = Tasks::new(action_tx.clone(), disk_list_arc.clone());
Ok(Self {
// TUI
action_rx,
action_tx,
components: vec![
Box::new(Title::new("Windows Install Tool")),
Box::new(Left::new()),
Box::new(Right::new()),
Box::new(Footer::new()),
Box::new(popup::Popup::new()),
],
config: Config::new()?,
frame_rate,
last_tick_key_events: Vec::new(),
should_quit: false,
should_suspend: false,
tick_rate,
// App
mode: Mode::default(),
state: State::new(disk_list_arc),
tasks,
})
}
pub fn next_mode(&mut self) -> Mode {
match self.mode {
Mode::Home | Mode::InstallDrivers => Mode::ScanDisks,
Mode::ScanDisks => Mode::SelectDisks,
Mode::SelectDisks => Mode::SelectTableType,
Mode::SelectTableType => Mode::ScanWinImages,
Mode::ScanWinImages => Mode::SelectWinImage,
Mode::SelectWinImage => Mode::SetUserName,
Mode::SetUserName => Mode::Confirm,
Mode::Confirm => Mode::Process, // i.e. format, apply, etc
Mode::Process | Mode::Done => Mode::Done,
Mode::Failed => Mode::Failed,
// Invalid States
Mode::BootDiags
| Mode::BootScan
| Mode::BootSetup
| Mode::Clone
| Mode::DiagMenu
| Mode::InjectDrivers
| Mode::LogView
| Mode::PEMenu
| Mode::PreClone
| Mode::PostClone
| Mode::SelectParts
| Mode::SetBootMode => panic!("This shouldn't happen?"),
}
}
pub async fn run(&mut self) -> Result<()> {
let mut tui = Tui::new()?
// .mouse(true) // uncomment this line to enable mouse support
.tick_rate(self.tick_rate)
.frame_rate(self.frame_rate);
tui.enter()?;
for component in &mut self.components {
component.register_action_handler(self.action_tx.clone())?;
}
for component in &mut self.components {
component.register_config_handler(self.config.clone())?;
}
for component in &mut self.components {
component.init(tui.size()?)?;
}
let action_tx = self.action_tx.clone();
action_tx.send(Action::SetMode(Mode::ScanDisks))?;
loop {
self.handle_events(&mut tui).await?;
self.handle_actions(&mut tui)?;
if self.should_suspend {
tui.suspend()?;
action_tx.send(Action::Resume)?;
action_tx.send(Action::ClearScreen)?;
// tui.mouse(true);
tui.enter()?;
} else if self.should_quit {
tui.stop()?;
break;
}
}
tui.exit()?;
Ok(())
}
async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> {
let Some(event) = tui.next_event().await else {
return Ok(());
};
let action_tx = self.action_tx.clone();
match event {
Event::Quit => action_tx.send(Action::Quit)?,
Event::Tick => action_tx.send(Action::Tick)?,
Event::Render => action_tx.send(Action::Render)?,
Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?,
Event::Key(key) => self.handle_key_event(key)?,
_ => {}
}
for component in &mut self.components {
if let Some(action) = component.handle_events(Some(event.clone()))? {
action_tx.send(action)?;
}
}
Ok(())
}
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 {
return Ok(());
};
if let Some(action) = keymap.get(&vec![key]) {
info!("Got action: {action:?}");
action_tx.send(action.clone())?;
} else {
// If the key was not handled as a single key action,
// then consider it for multi-key combinations.
self.last_tick_key_events.push(key);
// Check for multi-key combinations
if let Some(action) = keymap.get(&self.last_tick_key_events) {
info!("Got action: {action:?}");
action_tx.send(action.clone())?;
}
}
Ok(())
}
fn handle_actions(&mut self, tui: &mut Tui) -> Result<()> {
while let Ok(action) = self.action_rx.try_recv() {
if action != Action::Tick && action != Action::Render {
debug!("{action:?}");
}
match action {
Action::Tick => {
self.last_tick_key_events.drain(..);
// Check background task(s)
self.tasks.poll()?;
}
Action::Quit => self.should_quit = true,
Action::Suspend => self.should_suspend = true,
Action::Resume => self.should_suspend = false,
Action::ClearScreen => tui.terminal.clear()?,
// Action::KeyUp => {
// self.list.previous();
// if let Some(tool) = self.list.get_selected()
// && tool.separator
// {
// // Skip over separator
// self.list.previous();
// if let Some(index) = self.list.selected() {
// self.action_tx.send(Action::Highlight(index))?;
// }
// }
// }
// Action::KeyDown => {
// self.list.next();
// if let Some(tool) = self.list.get_selected()
// && tool.separator
// {
// // Skip over separator
// self.list.next();
// if let Some(index) = self.list.selected() {
// self.action_tx.send(Action::Highlight(index))?;
// }
// }
// }
Action::Error(ref msg) => {
self.action_tx
.send(Action::DisplayPopup(popup::Type::Error, msg.clone()))?;
self.action_tx.send(Action::SetMode(Mode::Failed))?;
}
// Action::Process => {
// // Run selected tool
// if let Some(tool) = self.list.get_selected() {
// info!("Run tool: {:?}", &tool);
// self.tasks.add(build_tool_command(self, &tool));
// }
// }
Action::Resize(w, h) => self.handle_resize(tui, w, h)?,
Action::Render => self.render(tui)?,
Action::SetMode(mode) => {
self.mode = mode;
if self.mode == Mode::ScanDisks {
self.state.reset();
}
self.action_tx.send(Action::UpdateFooter(String::from(
"(Enter) to select / (t) for terminal / (p) to power off / (r) to restart",
)))?;
//self.action_tx.send(build_left_items(self))?;
//self.action_tx.send(build_right_items(self))?;
self.action_tx.send(Action::Select(None, None))?;
}
_ => {}
}
for component in &mut self.components {
if let Some(action) = component.update(action.clone())? {
self.action_tx.send(action)?;
};
}
}
Ok(())
}
fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> Result<()> {
tui.resize(Rect::new(0, 0, w, h))?;
self.render(tui)?;
Ok(())
}
fn render(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| {
if let [header, _body, footer, left, right, popup] = get_chunks(frame.area())[..] {
let component_areas = vec![header, left, right, footer, popup];
for (component, area) in zip(self.components.iter_mut(), component_areas) {
if let Err(err) = component.draw(frame, area) {
let _ = self
.action_tx
.send(Action::Error(format!("Failed to draw: {err:?}")));
}
}
};
})?;
Ok(())
}
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
// Cut the given rectangle into three vertical pieces
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
// Then cut the middle vertical piece into three width-wise pieces
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1] // Return the middle chunk
}
fn get_chunks(r: Rect) -> Vec<Rect> {
let mut chunks: Vec<Rect> = Vec::with_capacity(6);
// Main sections
chunks.extend(
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(1),
Constraint::Length(3),
])
.split(r)
.to_vec(),
);
// Left/Right
chunks.extend(
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(centered_rect(90, 90, chunks[1]))
.to_vec(),
);
// Popup
chunks.push(centered_rect(60, 25, r));
// Done
chunks
}

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

@ -0,0 +1,34 @@
// 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 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(())
}

View file

@ -0,0 +1,56 @@
// 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::sync::{Arc, Mutex};
use core::system::{disk::Disk, drivers};
use crate::wim::WimSources;
#[derive(Debug, Default)]
pub struct State<'a> {
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 part_index_boot: Option<usize>,
pub wim_file_index: Option<usize>,
pub wim_image_index: Option<usize>,
pub wim_sources: Arc<Mutex<WimSources<'a>>>,
}
impl State<'_> {
pub fn new(disk_list: Arc<Mutex<Vec<Disk>>>) -> Self {
State {
disk_list,
..Default::default()
}
}
pub fn reset(&mut self) {
self.disk_index_dest = None;
self.part_index_boot = None;
self.wim_file_index = None;
self.wim_image_index = None;
if let Ok(mut sources) = self.wim_sources.lock() {
sources.reset();
}
}
pub fn scan_drivers(&mut self) {
self.driver_list = drivers::scan();
}
}

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

@ -0,0 +1,224 @@
// 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::{
collections::HashMap, fmt, fs::File, io::BufReader, path::Path, process::Command,
sync::LazyLock,
};
use tempfile::NamedTempFile;
use xml::reader::{EventReader, XmlEvent};
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(Debug)]
pub struct WimFile<'a> {
path: &'a Path,
images: Vec<WimImage>,
}
#[derive(Clone, Debug, Default)]
pub struct WimImage {
build: String,
index: String,
name: String,
spbuild: String,
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)
}
}
#[derive(Debug, Default)]
pub struct WimSources<'a> {
pub local: Vec<WimFile<'a>>,
pub network: Vec<WimFile<'a>>,
}
impl WimSources<'_> {
pub fn new() -> Self {
Default::default()
}
pub fn reset(&mut self) {
self.local.clear();
self.network.clear();
}
}
fn get_wim_xml(wim_file: &str) -> std::io::Result<File> {
let tmp_file = NamedTempFile::new()?;
let _ = Command::new("wiminfo")
.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)
}
fn parse_wim_file(wim_file: &str) -> std::io::Result<WimFile<'_>> {
let mut wim_images: Vec<WimImage> = Vec::new();
let wim_path = Path::new(wim_file);
if !wim_path.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);
let mut depth = 0;
for e in parser {
match e {
Ok(XmlEvent::StartElement {
name, attributes, ..
}) => {
println!("{:spaces$}+{name}", "", spaces = depth * 2);
depth += 1;
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();
println!("{:spaces$} {char_data}", "", spaces = (depth + 1) * 2);
}
if current_element == "NAME" {
image.name = char_data.trim().to_string();
println!("{:spaces$} {char_data}", "", spaces = (depth + 1) * 2);
}
if current_element == "SPBUILD" {
image.spbuild = char_data.trim().to_string();
println!("{:spaces$} {char_data}", "", spaces = (depth + 1) * 2);
}
}
Ok(XmlEvent::EndElement { name }) => {
depth -= 1;
println!("{:spaces$}-{name}", "", spaces = depth * 2);
if name.local_name.to_uppercase() == "IMAGE" {
// Append image to list
if !image.build.is_empty() && !image.name.is_empty() && !image.index.is_empty()
{
wim_images.push(image.clone());
}
// Reset image
image.reset()
}
}
Err(e) => {
eprintln!("Error: {e}");
break;
}
_ => {}
}
}
let wim_file = WimFile {
path: wim_path,
images: wim_images,
};
Ok(wim_file)
}
// fn main() -> std::io::Result<()> {
// let mut sources = WimSources::new();
// if let Ok(wim_file) = parse_wim_file("./23H2.wim")
// && !wim_file.images.is_empty()
// {
// sources.local.push(wim_file);
// }
// if let Ok(wim_file) = parse_wim_file("./24H2.wim") {
// sources.local.push(wim_file);
// }
// dbg!(&sources);
// sources.local.iter().for_each(|f| {
// println!("-- {} --", f.path.to_string_lossy());
// f.images.iter().for_each(|i| {
// println!("* {i}");
// });
// println!();
// });
//
// Ok(())
// }