302 lines
8.1 KiB
Rust
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)
|
|
}
|