deja-vu/win_installer/src/wim.rs
2Shirt c4c174b546
Handle missing WIM build/spbuild info
Needed for manual backup captures
2025-11-30 21:03:57 -08:00

302 lines
8.1 KiB
Rust

use core::system::disk::bytes_to_string;
// 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 xml::reader::{EventReader, XmlEvent};
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_setup: 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>,
}
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 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_setup: 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_setup,
};
Ok(wim_file)
}