1use anyhow::{Context, Result};
2use std::fs::File;
3use std::io::{Read, Seek, SeekFrom};
4use std::path::Path;
5use tracing::{debug, info};
6
7#[derive(Debug, Clone)]
10#[allow(dead_code)]
11pub struct WimHeader {
12 pub signature: [u8; 8],
14 pub header_size: u32,
16 pub format_version: u32,
18 pub file_flags: u32,
20 pub compressed_size: u32,
22 pub guid: [u8; 16],
24 pub segment_number: u16,
26 pub total_segments: u16,
28 pub image_count: u32,
30 pub offset_table_resource: FileResourceEntry,
32 pub xml_data_resource: FileResourceEntry,
34 pub boot_metadata_resource: FileResourceEntry,
36 pub bootable_image_index: u32,
38 pub integrity_resource: FileResourceEntry,
40}
41
42#[derive(Debug, Clone)]
45#[allow(dead_code)]
46pub struct FileResourceEntry {
47 pub size: u64,
49 pub flags: u8,
51 pub offset: u64,
53 pub original_size: u64,
55}
56
57#[derive(Debug, Clone)]
59#[allow(dead_code)]
60pub struct ResourceFlags;
61
62#[allow(dead_code)]
63impl ResourceFlags {
64 pub const FREE: u8 = 0x01; pub const METADATA: u8 = 0x02; pub const COMPRESSED: u8 = 0x04; pub const SPANNED: u8 = 0x08; }
69
70#[derive(Debug, Clone)]
72#[allow(dead_code)]
73pub struct FileFlags;
74
75#[allow(dead_code)]
76impl FileFlags {
77 pub const COMPRESSION: u32 = 0x00000002; pub const READONLY: u32 = 0x00000004; pub const SPANNED: u32 = 0x00000008; pub const RESOURCE_ONLY: u32 = 0x00000010; pub const METADATA_ONLY: u32 = 0x00000020; pub const COMPRESS_XPRESS: u32 = 0x00020000; pub const COMPRESS_LZX: u32 = 0x00040000; }
85
86#[derive(Debug, Clone)]
88#[allow(dead_code)]
89pub struct ImageInfo {
90 pub index: u32,
92 pub name: String,
94 pub description: String,
96 pub dir_count: u32,
98 pub file_count: u32,
100 pub total_bytes: u64,
102 pub creation_time: Option<u64>,
104 pub last_modification_time: Option<u64>,
106 pub version: Option<String>,
108 pub architecture: Option<String>,
110}
111
112pub struct WimParser {
114 file: File,
115 header: Option<WimHeader>,
116 images: Vec<ImageInfo>,
117}
118
119impl WimParser {
120 pub fn new<P: AsRef<Path>>(wim_path: P) -> Result<Self> {
122 let file = File::open(wim_path.as_ref())
123 .with_context(|| format!("无法打开 WIM 文件: {}", wim_path.as_ref().display()))?;
124
125 debug!("创建 WIM 解析器: {}", wim_path.as_ref().display());
126
127 Ok(Self {
128 file,
129 header: None,
130 images: Vec::new(),
131 })
132 }
133
134 #[doc(hidden)]
136 #[allow(dead_code)]
137 pub fn new_for_test(file: File) -> Self {
138 Self {
139 file,
140 header: None,
141 images: Vec::new(),
142 }
143 }
144
145 pub fn read_header(&mut self) -> Result<&WimHeader> {
147 if self.header.is_some() {
148 return Ok(self.header.as_ref().unwrap());
149 }
150
151 debug!("开始读取 WIM 文件头");
152
153 self.file.seek(SeekFrom::Start(0))?;
155
156 let mut header_buffer = vec![0u8; 204];
158 self.file
159 .read_exact(&mut header_buffer)
160 .context("读取 WIM 文件头失败")?;
161
162 let header = self.parse_header_buffer(&header_buffer)?;
163
164 if &header.signature != b"MSWIM\x00\x00\x00" {
166 return Err(anyhow::anyhow!("无效的 WIM 文件签名"));
167 }
168
169 info!(
170 "成功读取 WIM 文件头 - 版本: {}, 镜像数: {}",
171 header.format_version, header.image_count
172 );
173
174 self.header = Some(header);
175 Ok(self.header.as_ref().unwrap())
176 }
177
178 fn parse_header_buffer(&self, buffer: &[u8]) -> Result<WimHeader> {
180 use std::convert::TryInto;
181
182 let read_u32_le = |offset: usize| -> u32 {
184 u32::from_le_bytes(buffer[offset..offset + 4].try_into().unwrap())
185 };
186
187 let read_u16_le = |offset: usize| -> u16 {
188 u16::from_le_bytes(buffer[offset..offset + 2].try_into().unwrap())
189 };
190
191 let read_u64_le = |offset: usize| -> u64 {
192 u64::from_le_bytes(buffer[offset..offset + 8].try_into().unwrap())
193 };
194
195 let parse_resource_entry = |offset: usize| -> FileResourceEntry {
197 let size_bytes = &buffer[offset..offset + 7];
199 let mut size_array = [0u8; 8];
200 size_array[..7].copy_from_slice(size_bytes);
201 let size = u64::from_le_bytes(size_array);
202
203 let flags = buffer[offset + 7];
204 let offset_val = read_u64_le(offset + 8);
205 let original_size = read_u64_le(offset + 16);
206
207 FileResourceEntry {
208 size,
209 flags,
210 offset: offset_val,
211 original_size,
212 }
213 };
214
215 let mut signature = [0u8; 8];
217 signature.copy_from_slice(&buffer[0..8]);
218
219 let header = WimHeader {
220 signature,
221 header_size: read_u32_le(8),
222 format_version: read_u32_le(12),
223 file_flags: read_u32_le(16),
224 compressed_size: read_u32_le(20),
225 guid: buffer[24..40].try_into().unwrap(),
226 segment_number: read_u16_le(40),
227 total_segments: read_u16_le(42),
228 image_count: read_u32_le(44),
229 offset_table_resource: parse_resource_entry(48),
230 xml_data_resource: parse_resource_entry(72),
231 boot_metadata_resource: parse_resource_entry(96),
232 bootable_image_index: read_u32_le(120),
233 integrity_resource: parse_resource_entry(124),
234 };
235
236 debug!(
237 "解析 WIM 头部完成 - 镜像数: {}, 文件标志: 0x{:08X}",
238 header.image_count, header.file_flags
239 );
240
241 Ok(header)
242 }
243
244 pub fn read_xml_data(&mut self) -> Result<()> {
246 if self.header.is_none() {
248 self.read_header()?;
249 }
250
251 let header = self.header.as_ref().unwrap();
252
253 if header.xml_data_resource.size == 0 {
255 return Err(anyhow::anyhow!("WIM 文件中没有 XML 数据资源"));
256 }
257
258 debug!(
259 "开始读取 XML 数据,偏移: {}, 大小: {}",
260 header.xml_data_resource.offset, header.xml_data_resource.size
261 );
262
263 self.file
265 .seek(SeekFrom::Start(header.xml_data_resource.offset))?;
266
267 let mut xml_buffer = vec![0u8; header.xml_data_resource.size as usize];
269 self.file
270 .read_exact(&mut xml_buffer)
271 .context("读取 XML 数据失败")?;
272
273 self.parse_xml_data(&xml_buffer)?;
275
276 info!("成功解析 {} 个镜像的信息", self.images.len());
277 Ok(())
278 }
279
280 fn parse_xml_data(&mut self, xml_buffer: &[u8]) -> Result<()> {
282 if xml_buffer.len() < 2 {
284 return Err(anyhow::anyhow!("XML 数据太短"));
285 }
286
287 if xml_buffer[0] != 0xFF || xml_buffer[1] != 0xFE {
289 return Err(anyhow::anyhow!("无效的 XML 数据 BOM"));
290 }
291
292 let xml_utf16_data = &xml_buffer[2..]; if xml_utf16_data.len() % 2 != 0 {
297 return Err(anyhow::anyhow!("XML UTF-16 数据长度不是偶数"));
298 }
299
300 let mut utf16_chars = Vec::new();
302 for chunk in xml_utf16_data.chunks_exact(2) {
303 let char_val = u16::from_le_bytes([chunk[0], chunk[1]]);
304 utf16_chars.push(char_val);
305 }
306
307 let xml_string = String::from_utf16(&utf16_chars).context("无法将 XML 数据转换为 UTF-8")?;
309
310 debug!("XML 数据长度: {} 字符", xml_string.len());
311
312 self.parse_xml_images(&xml_string)?;
314
315 Ok(())
316 }
317
318 fn parse_xml_images(&mut self, xml_content: &str) -> Result<()> {
320 self.images.clear();
324
325 let mut start_pos = 0;
327 while let Some(image_start) = xml_content[start_pos..].find("<IMAGE") {
328 let absolute_start = start_pos + image_start;
329
330 if let Some(image_end) = xml_content[absolute_start..].find("</IMAGE>") {
332 let absolute_end = absolute_start + image_end + 8; let image_xml = &xml_content[absolute_start..absolute_end];
334
335 if let Ok(image_info) = self.parse_single_image_xml(image_xml) {
337 self.images.push(image_info);
338 }
339
340 start_pos = absolute_end;
341 } else {
342 break;
343 }
344 }
345
346 Ok(())
347 }
348
349 pub fn parse_single_image_xml(&self, image_xml: &str) -> Result<ImageInfo> {
351 let extract_tag_value = |xml: &str, tag: &str| -> Option<String> {
353 let start_tag = format!("<{tag}>");
354 let end_tag = format!("</{tag}>");
355
356 if let Some(start) = xml.find(&start_tag) {
357 if let Some(end) = xml.find(&end_tag) {
358 let value_start = start + start_tag.len();
359 if value_start < end {
360 return Some(xml[value_start..end].trim().to_string());
361 }
362 }
363 }
364 None
365 };
366
367 let index = if let Some(index_start) = image_xml.find("INDEX=\"") {
369 let index_value_start = index_start + 7; if let Some(index_end) = image_xml[index_value_start..].find("\"") {
371 let index_str = &image_xml[index_value_start..index_value_start + index_end];
372 index_str.parse().unwrap_or(0)
373 } else {
374 0
375 }
376 } else {
377 0
378 };
379
380 let name =
382 extract_tag_value(image_xml, "DISPLAYNAME").unwrap_or_else(|| format!("Image {index}"));
383 let description = extract_tag_value(image_xml, "DISPLAYDESCRIPTION")
384 .unwrap_or_else(|| "Unknown".to_string());
385 let dir_count = extract_tag_value(image_xml, "DIRCOUNT")
386 .and_then(|s| s.parse().ok())
387 .unwrap_or(0);
388 let file_count = extract_tag_value(image_xml, "FILECOUNT")
389 .and_then(|s| s.parse().ok())
390 .unwrap_or(0);
391 let total_bytes = extract_tag_value(image_xml, "TOTALBYTES")
392 .and_then(|s| s.parse().ok())
393 .unwrap_or(0);
394
395 let arch_from_xml = self.parse_arch_from_xml(image_xml);
397
398 let (version, arch_from_name) = self.extract_version_and_arch(&name, &description);
400 let architecture = arch_from_xml.or(arch_from_name);
401
402 let image_info = ImageInfo {
403 index,
404 name,
405 description,
406 dir_count,
407 file_count,
408 total_bytes,
409 creation_time: None, last_modification_time: None, version,
412 architecture,
413 };
414
415 debug!(
416 "解析镜像信息: {} - {} - {} - {:#?}",
417 image_info.index, image_info.name, image_info.description, image_info.architecture
418 );
419
420 Ok(image_info)
421 }
422
423 fn extract_version_and_arch(
425 &self,
426 name: &str,
427 description: &str,
428 ) -> (Option<String>, Option<String>) {
429 let combined_text = format!("{name} {description}").to_lowercase();
430
431 let version = if combined_text.contains("windows 11") {
433 Some("Windows 11".to_string())
434 } else if combined_text.contains("windows 10") {
435 Some("Windows 10".to_string())
436 } else if combined_text.contains("windows server 2022") {
437 Some("Windows Server 2022".to_string())
438 } else if combined_text.contains("windows server 2019") {
439 Some("Windows Server 2019".to_string())
440 } else if combined_text.contains("windows server") {
441 Some("Windows Server".to_string())
442 } else if combined_text.contains("windows") {
443 Some("Windows".to_string())
444 } else {
445 None
446 };
447
448 let architecture = if combined_text.contains("x64") || combined_text.contains("amd64") {
450 Some("x64".to_string())
451 } else if combined_text.contains("x86") {
452 Some("x86".to_string())
453 } else if combined_text.contains("arm64") {
454 Some("ARM64".to_string())
455 } else {
456 None
457 };
458
459 (version, architecture)
460 }
461
462 pub fn parse_arch_from_xml(&self, image_xml: &str) -> Option<String> {
464 let extract_tag_value = |xml: &str, tag: &str| -> Option<String> {
466 let start_tag = format!("<{tag}>");
467 let end_tag = format!("</{tag}>");
468
469 if let Some(start) = xml.find(&start_tag) {
470 if let Some(end) = xml.find(&end_tag) {
471 let value_start = start + start_tag.len();
472 if value_start < end {
473 return Some(xml[value_start..end].trim().to_string());
474 }
475 }
476 }
477 None
478 };
479
480 if let Some(arch_value) = extract_tag_value(image_xml, "ARCH") {
482 match arch_value.as_str() {
483 "0" => Some("x86".to_string()),
484 "9" => Some("x64".to_string()),
485 "5" => Some("ARM".to_string()),
486 "12" => Some("ARM64".to_string()),
487 _ => {
488 debug!("未知的架构值: {}", arch_value);
489 None
490 }
491 }
492 } else {
493 None
494 }
495 }
496
497 pub fn get_images(&self) -> &[ImageInfo] {
499 &self.images
500 }
501
502 #[allow(dead_code)]
504 pub fn get_image(&self, index: u32) -> Option<&ImageInfo> {
505 self.images.iter().find(|img| img.index == index)
506 }
507
508 #[allow(dead_code)]
510 pub fn get_header(&self) -> Option<&WimHeader> {
511 self.header.as_ref()
512 }
513
514 #[allow(dead_code)]
516 pub fn has_multiple_images(&self) -> bool {
517 self.header
518 .as_ref()
519 .map(|h| h.image_count > 1)
520 .unwrap_or(false)
521 }
522
523 #[allow(dead_code)]
525 pub fn get_image_count(&self) -> u32 {
526 self.header.as_ref().map(|h| h.image_count).unwrap_or(0)
527 }
528
529 #[allow(dead_code)]
531 pub fn is_compressed(&self) -> bool {
532 self.header
533 .as_ref()
534 .map(|h| h.file_flags & FileFlags::COMPRESSION != 0)
535 .unwrap_or(false)
536 }
537
538 #[allow(dead_code)]
540 pub fn get_compression_type(&self) -> Option<&'static str> {
541 if let Some(header) = &self.header {
542 if header.file_flags & FileFlags::COMPRESS_XPRESS != 0 {
543 Some("XPRESS")
544 } else if header.file_flags & FileFlags::COMPRESS_LZX != 0 {
545 Some("LZX")
546 } else if header.file_flags & FileFlags::COMPRESSION != 0 {
547 Some("Unknown")
548 } else {
549 None
550 }
551 } else {
552 None
553 }
554 }
555
556 pub fn parse_full(&mut self) -> Result<()> {
558 self.read_header()?;
559 self.read_xml_data()?;
560 Ok(())
561 }
562}
563
564impl std::fmt::Display for ImageInfo {
565 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566 write!(f, "镜像 {} - {}", self.index, self.name)?;
567 if let Some(ref version) = self.version {
568 write!(f, " [{version}]")?;
569 }
570 if let Some(ref arch) = self.architecture {
571 write!(f, " [{arch}]")?;
572 }
573 write!(f, " | 描述: {}", self.description)?;
574 write!(
575 f,
576 " | 文件数: {}, 目录数: {}",
577 self.file_count, self.dir_count
578 )?;
579 write!(f, " | 总大小: {} MB", self.total_bytes / (1024 * 1024))?;
580 Ok(())
581 }
582}
583
584impl std::fmt::Display for WimHeader {
585 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
586 writeln!(f, "WIM Header:")?;
587 writeln!(f, " Format Version: {}", self.format_version)?;
588 writeln!(f, " File Flags: 0x{:08X}", self.file_flags)?;
589 writeln!(f, " Image Count: {}", self.image_count)?;
590 writeln!(
591 f,
592 " Segment: {}/{}",
593 self.segment_number, self.total_segments
594 )?;
595 writeln!(f, " Bootable Image Index: {}", self.bootable_image_index)?;
596 Ok(())
597 }
598}
599
600#[allow(dead_code)]
601impl WimParser {
602 #[allow(dead_code)]
604 pub fn get_version_summary(&self) -> Vec<String> {
605 let mut summaries = Vec::new();
606
607 for image in &self.images {
608 let mut summary = format!("镜像 {}: {}", image.index, image.name);
609
610 if let Some(ref version) = image.version {
611 summary.push_str(&format!(" ({version})"));
612 }
613
614 if let Some(ref arch) = image.architecture {
615 summary.push_str(&format!(" [{arch}]"));
616 }
617
618 summaries.push(summary);
619 }
620
621 summaries
622 }
623
624 pub fn get_primary_version(&self) -> Option<String> {
626 if self.images.is_empty() {
627 return None;
628 }
629
630 let mut version_counts = std::collections::HashMap::new();
632 for image in &self.images {
633 if let Some(ref version) = image.version {
634 *version_counts.entry(version.clone()).or_insert(0) += 1;
635 }
636 }
637
638 version_counts
640 .into_iter()
641 .max_by_key(|(_, count)| *count)
642 .map(|(version, _)| version)
643 }
644
645 pub fn get_primary_architecture(&self) -> Option<String> {
647 if self.images.is_empty() {
648 return None;
649 }
650
651 let mut arch_counts = std::collections::HashMap::new();
653 for image in &self.images {
654 if let Some(ref arch) = image.architecture {
655 *arch_counts.entry(arch.clone()).or_insert(0) += 1;
656 }
657 }
658
659 arch_counts
661 .into_iter()
662 .max_by_key(|(_, count)| *count)
663 .map(|(arch, _)| arch)
664 }
665
666 #[allow(dead_code)]
668 pub fn has_version(&self, version: &str) -> bool {
669 self.images.iter().any(|img| {
670 img.version
671 .as_ref()
672 .is_some_and(|v| v.to_lowercase().contains(&version.to_lowercase()))
673 })
674 }
675
676 #[allow(dead_code)]
678 pub fn has_architecture(&self, arch: &str) -> bool {
679 self.images.iter().any(|img| {
680 img.architecture
681 .as_ref()
682 .is_some_and(|a| a.to_lowercase().contains(&arch.to_lowercase()))
683 })
684 }
685
686 pub fn get_windows_info(&self) -> Option<WindowsInfo> {
688 let primary_version = self.get_primary_version()?;
689 let primary_arch = self.get_primary_architecture()?;
690
691 if !primary_version.to_lowercase().contains("windows") {
693 return None;
694 }
695
696 let mut editions = Vec::new();
698 for image in &self.images {
699 let name_lower = image.name.to_lowercase();
700 if name_lower.contains("pro") && !editions.contains(&"Pro".to_string()) {
701 editions.push("Pro".to_string());
702 } else if name_lower.contains("home") && !editions.contains(&"Home".to_string()) {
703 editions.push("Home".to_string());
704 } else if name_lower.contains("enterprise")
705 && !editions.contains(&"Enterprise".to_string())
706 {
707 editions.push("Enterprise".to_string());
708 } else if name_lower.contains("education")
709 && !editions.contains(&"Education".to_string())
710 {
711 editions.push("Education".to_string());
712 }
713 }
714
715 Some(WindowsInfo {
716 version: primary_version,
717 architecture: primary_arch,
718 editions,
719 image_count: self.images.len() as u32,
720 total_size: self.images.iter().map(|img| img.total_bytes).sum(),
721 })
722 }
723}
724
725#[derive(Debug, Clone)]
727pub struct WindowsInfo {
728 pub version: String,
729 pub architecture: String,
730 pub editions: Vec<String>,
731 pub image_count: u32,
732 pub total_size: u64,
733}
734
735impl std::fmt::Display for WindowsInfo {
736 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
737 write!(f, "{} ({})", self.version, self.architecture)?;
738 if !self.editions.is_empty() {
739 write!(f, " - 版本: {}", self.editions.join(", "))?;
740 }
741 write!(f, " | 镜像数量: {}", self.image_count)?;
742 write!(f, " | 总大小: {} MB", self.total_size / (1024 * 1024))?;
743 Ok(())
744 }
745}