wim_parser/
lib.rs

1use anyhow::{Context, Result};
2use std::fs::File;
3use std::io::{Read, Seek, SeekFrom};
4use std::path::Path;
5use tracing::{debug, info};
6
7/// WIM 文件头结构体 (WIMHEADER_V1_PACKED)
8/// 总大小:204 字节
9#[derive(Debug, Clone)]
10#[allow(dead_code)]
11pub struct WimHeader {
12    /// 文件签名 "MSWIM\x00\x00\x00"
13    pub signature: [u8; 8],
14    /// 文件头大小
15    pub header_size: u32,
16    /// 格式版本
17    pub format_version: u32,
18    /// 文件标志
19    pub file_flags: u32,
20    /// 压缩文件大小
21    pub compressed_size: u32,
22    /// 唯一标识符 (GUID)
23    pub guid: [u8; 16],
24    /// 段号
25    pub segment_number: u16,
26    /// 段总数
27    pub total_segments: u16,
28    /// 镜像数量
29    pub image_count: u32,
30    /// 偏移表文件资源
31    pub offset_table_resource: FileResourceEntry,
32    /// XML 数据文件资源
33    pub xml_data_resource: FileResourceEntry,
34    /// 引导元数据文件资源
35    pub boot_metadata_resource: FileResourceEntry,
36    /// 可引导镜像索引
37    pub bootable_image_index: u32,
38    /// 完整性数据文件资源
39    pub integrity_resource: FileResourceEntry,
40}
41
42/// 文件资源条目结构体 (_RESHDR_DISK_SHORT)
43/// 总大小:24 字节
44#[derive(Debug, Clone)]
45#[allow(dead_code)]
46pub struct FileResourceEntry {
47    /// 资源大小 (7 字节)
48    pub size: u64,
49    /// 资源标志 (1 字节)
50    pub flags: u8,
51    /// 资源偏移 (8 字节)
52    pub offset: u64,
53    /// 原始大小 (8 字节)
54    pub original_size: u64,
55}
56
57/// 文件资源条目标志
58#[derive(Debug, Clone)]
59#[allow(dead_code)]
60pub struct ResourceFlags;
61
62#[allow(dead_code)]
63impl ResourceFlags {
64    pub const FREE: u8 = 0x01; // 条目空闲
65    pub const METADATA: u8 = 0x02; // 包含元数据
66    pub const COMPRESSED: u8 = 0x04; // 已压缩
67    pub const SPANNED: u8 = 0x08; // 跨段
68}
69
70/// 文件标志
71#[derive(Debug, Clone)]
72#[allow(dead_code)]
73pub struct FileFlags;
74
75#[allow(dead_code)]
76impl FileFlags {
77    pub const COMPRESSION: u32 = 0x00000002; // 资源已压缩
78    pub const READONLY: u32 = 0x00000004; // 只读
79    pub const SPANNED: u32 = 0x00000008; // 跨段
80    pub const RESOURCE_ONLY: u32 = 0x00000010; // 仅包含文件资源
81    pub const METADATA_ONLY: u32 = 0x00000020; // 仅包含元数据
82    pub const COMPRESS_XPRESS: u32 = 0x00020000; // XPRESS 压缩
83    pub const COMPRESS_LZX: u32 = 0x00040000; // LZX 压缩
84}
85
86/// 镜像信息结构体
87#[derive(Debug, Clone)]
88#[allow(dead_code)]
89pub struct ImageInfo {
90    /// 镜像索引
91    pub index: u32,
92    /// 镜像名称
93    pub name: String,
94    /// 镜像描述
95    pub description: String,
96    /// 目录数量
97    pub dir_count: u32,
98    /// 文件数量
99    pub file_count: u32,
100    /// 总字节数
101    pub total_bytes: u64,
102    /// 创建时间
103    pub creation_time: Option<u64>,
104    /// 最后修改时间
105    pub last_modification_time: Option<u64>,
106    /// 版本信息
107    pub version: Option<String>,
108    /// 架构信息
109    pub architecture: Option<String>,
110}
111
112/// WIM 文件解析器
113pub struct WimParser {
114    file: File,
115    header: Option<WimHeader>,
116    images: Vec<ImageInfo>,
117}
118
119impl WimParser {
120    /// 创建新的 WIM 解析器
121    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    /// 创建用于测试的 WIM 解析器(不需要实际文件)
135    #[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    /// 读取并解析 WIM 文件头
146    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        // 跳转到文件开始
154        self.file.seek(SeekFrom::Start(0))?;
155
156        // 读取 204 字节的文件头
157        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        // 验证签名
165        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    /// 解析文件头缓冲区
179    fn parse_header_buffer(&self, buffer: &[u8]) -> Result<WimHeader> {
180        use std::convert::TryInto;
181
182        // 辅助函数:从缓冲区读取 little-endian 数值
183        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        // 解析文件资源条目
196        let parse_resource_entry = |offset: usize| -> FileResourceEntry {
197            // 读取 7 字节的大小 + 1 字节标志
198            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        // 解析文件头各个字段
216        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    /// 读取并解析 XML 数据
245    pub fn read_xml_data(&mut self) -> Result<()> {
246        // 确保文件头已读取
247        if self.header.is_none() {
248            self.read_header()?;
249        }
250
251        let header = self.header.as_ref().unwrap();
252
253        // 检查 XML 数据资源是否存在
254        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        // 跳转到 XML 数据位置
264        self.file
265            .seek(SeekFrom::Start(header.xml_data_resource.offset))?;
266
267        // 读取 XML 数据
268        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        // 解析 XML 数据
274        self.parse_xml_data(&xml_buffer)?;
275
276        info!("成功解析 {} 个镜像的信息", self.images.len());
277        Ok(())
278    }
279
280    /// 解析 XML 数据
281    fn parse_xml_data(&mut self, xml_buffer: &[u8]) -> Result<()> {
282        // XML 数据以 UTF-16 LE BOM 开始
283        if xml_buffer.len() < 2 {
284            return Err(anyhow::anyhow!("XML 数据太短"));
285        }
286
287        // 检查 BOM (0xFEFF)
288        if xml_buffer[0] != 0xFF || xml_buffer[1] != 0xFE {
289            return Err(anyhow::anyhow!("无效的 XML 数据 BOM"));
290        }
291
292        // 将 UTF-16 LE 转换为 UTF-8
293        let xml_utf16_data = &xml_buffer[2..]; // 跳过 BOM
294
295        // 确保数据长度为偶数(UTF-16 每个字符 2 字节)
296        if xml_utf16_data.len() % 2 != 0 {
297            return Err(anyhow::anyhow!("XML UTF-16 数据长度不是偶数"));
298        }
299
300        // 转换为 u16 数组
301        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        // 转换为 UTF-8 字符串
308        let xml_string = String::from_utf16(&utf16_chars).context("无法将 XML 数据转换为 UTF-8")?;
309
310        debug!("XML 数据长度: {} 字符", xml_string.len());
311
312        // 解析 XML 镜像信息
313        self.parse_xml_images(&xml_string)?;
314
315        Ok(())
316    }
317
318    /// 解析 XML 中的镜像信息
319    fn parse_xml_images(&mut self, xml_content: &str) -> Result<()> {
320        // 简单的 XML 解析(基于字符串匹配)
321        // 在实际生产环境中,建议使用专门的 XML 解析库
322
323        self.images.clear();
324
325        // 查找所有 <IMAGE> 标签
326        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            // 查找对应的 </IMAGE> 标签
331            if let Some(image_end) = xml_content[absolute_start..].find("</IMAGE>") {
332                let absolute_end = absolute_start + image_end + 8; // 包含 </IMAGE>
333                let image_xml = &xml_content[absolute_start..absolute_end];
334
335                // 解析单个镜像信息
336                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    /// 解析单个镜像的 XML 信息
350    pub fn parse_single_image_xml(&self, image_xml: &str) -> Result<ImageInfo> {
351        // 辅助函数:从 XML 中提取标签值
352        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        // 提取 INDEX 属性
368        let index = if let Some(index_start) = image_xml.find("INDEX=\"") {
369            let index_value_start = index_start + 7; // "INDEX=\"".len()
370            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        // 提取各种信息
381        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        // 尝试从XML中的ARCH标签解析架构信息
396        let arch_from_xml = self.parse_arch_from_xml(image_xml);
397
398        // 从名称中提取版本信息,架构信息优先使用XML中的ARCH标签
399        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,          // 可以进一步解析 CREATIONTIME
410            last_modification_time: None, // 可以进一步解析 LASTMODIFICATIONTIME
411            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    /// 从镜像名称和描述中提取版本和架构信息
424    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        // 提取版本信息
432        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        // 提取架构信息
449        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    /// 从XML中的ARCH标签解析架构信息
463    pub fn parse_arch_from_xml(&self, image_xml: &str) -> Option<String> {
464        // 辅助函数:从 XML 中提取标签值
465        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        // 提取ARCH标签值
481        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    /// 获取所有镜像信息
498    pub fn get_images(&self) -> &[ImageInfo] {
499        &self.images
500    }
501
502    /// 获取指定索引的镜像信息
503    #[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    /// 获取文件头信息
509    #[allow(dead_code)]
510    pub fn get_header(&self) -> Option<&WimHeader> {
511        self.header.as_ref()
512    }
513
514    /// 检查是否包含多个镜像
515    #[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    /// 获取镜像数量
524    #[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    /// 检查是否为压缩文件
530    #[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    /// 获取压缩类型
539    #[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    /// 完整解析 WIM 文件(头部 + XML 数据)
557    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    /// 获取所有镜像的版本摘要
603    #[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    /// 获取主要版本信息(如果有多个镜像,返回最常见的版本)
625    pub fn get_primary_version(&self) -> Option<String> {
626        if self.images.is_empty() {
627            return None;
628        }
629
630        // 统计版本出现频率
631        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        // 找到最常见的版本
639        version_counts
640            .into_iter()
641            .max_by_key(|(_, count)| *count)
642            .map(|(version, _)| version)
643    }
644
645    /// 获取主要架构信息(如果有多个镜像,返回最常见的架构)
646    pub fn get_primary_architecture(&self) -> Option<String> {
647        if self.images.is_empty() {
648            return None;
649        }
650
651        // 统计架构出现频率
652        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        // 找到最常见的架构
660        arch_counts
661            .into_iter()
662            .max_by_key(|(_, count)| *count)
663            .map(|(arch, _)| arch)
664    }
665
666    /// 检查是否包含指定版本的镜像
667    #[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    /// 检查是否包含指定架构的镜像
677    #[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    /// 获取Windows版本的详细信息
687    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        // 检查是否是Windows镜像
692        if !primary_version.to_lowercase().contains("windows") {
693            return None;
694        }
695
696        // 计算总的镜像版本(如Pro, Home, Enterprise等)
697        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/// Windows 版本信息摘要
726#[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}