Skip to main content

provenant/parsers/
microsoft_update_manifest.rs

1// SPDX-FileCopyrightText: nexB Inc. and others
2// SPDX-FileCopyrightText: Provenant contributors
3// SPDX-License-Identifier: Apache-2.0
4// Derived from ScanCode Toolkit (Apache-2.0); modified. See NOTICE.
5
6//! Parser for Microsoft Update Manifest (.mum) files.
7//!
8//! Extracts Windows Update package metadata from .mum XML manifest files.
9//!
10//! # Supported Formats
11//! - `*.mum` - Microsoft Update Manifest XML files
12//!
13//! # Implementation Notes
14//! - Format: XML with assembly and package metadata
15//! - Spec: Windows Update manifests
16
17use crate::models::{DatasourceId, PackageType, Party, PartyType};
18use std::path::Path;
19
20use crate::parser_warn as warn;
21use quick_xml::events::Event;
22use quick_xml::reader::Reader;
23
24use crate::models::PackageData;
25use crate::parsers::utils::{MAX_ITERATION_COUNT, read_file_to_string, truncate_field};
26
27use super::PackageParser;
28use super::metadata::ParserMetadata;
29
30const PACKAGE_TYPE: PackageType = PackageType::WindowsUpdate;
31
32pub struct MicrosoftUpdateManifestParser;
33
34impl PackageParser for MicrosoftUpdateManifestParser {
35    const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
36
37    fn metadata() -> Vec<ParserMetadata> {
38        vec![ParserMetadata {
39            description: "Microsoft Update Manifest .mum file",
40            file_patterns: &["*.mum"],
41            package_type: "windows-update",
42            primary_language: "",
43            documentation_url: None,
44        }]
45    }
46
47    fn is_match(path: &Path) -> bool {
48        path.extension().is_some_and(|ext| ext == "mum")
49    }
50
51    fn extract_packages(path: &Path) -> Vec<PackageData> {
52        let content = match read_file_to_string(path, None) {
53            Ok(c) => c,
54            Err(e) => {
55                warn!("Failed to read .mum file {:?}: {}", path, e);
56                return vec![PackageData {
57                    package_type: Some(PACKAGE_TYPE),
58                    datasource_id: Some(DatasourceId::MicrosoftUpdateManifestMum),
59                    ..Default::default()
60                }];
61            }
62        };
63
64        vec![parse_mum_xml(&content)]
65    }
66}
67
68pub(crate) fn parse_mum_xml(content: &str) -> PackageData {
69    let mut reader = Reader::from_str(content);
70    reader.config_mut().trim_text(true);
71
72    let mut company = None;
73    let mut name = None;
74    let mut version = None;
75    let mut description = None;
76    let mut copyright = None;
77    let mut homepage_url = None;
78
79    let mut buf = Vec::new();
80    let mut iteration_count: usize = 0;
81
82    loop {
83        iteration_count += 1;
84        if iteration_count > MAX_ITERATION_COUNT {
85            warn!(
86                "Exceeded MAX_ITERATION_COUNT ({}) parsing .mum XML, stopping",
87                MAX_ITERATION_COUNT
88            );
89            break;
90        }
91        match reader.read_event_into(&mut buf) {
92            Ok(Event::Empty(e)) if e.name().as_ref() == b"assemblyIdentity" => {
93                for attr in e.attributes().filter_map(|a| a.ok()) {
94                    match attr.key.as_ref() {
95                        b"name" => {
96                            let raw = attr.value.to_vec();
97                            let has_invalid = String::from_utf8(raw.clone()).is_err();
98                            let val = String::from_utf8_lossy(&raw).into_owned();
99                            if has_invalid {
100                                warn!("Invalid UTF-8 in 'name' attribute, using lossy conversion");
101                            }
102                            if name.is_none() {
103                                name = Some(truncate_field(val));
104                            }
105                        }
106                        b"version" => {
107                            let raw = attr.value.to_vec();
108                            let has_invalid = String::from_utf8(raw.clone()).is_err();
109                            let val = String::from_utf8_lossy(&raw).into_owned();
110                            if has_invalid {
111                                warn!(
112                                    "Invalid UTF-8 in 'version' attribute, using lossy conversion"
113                                );
114                            }
115                            if version.is_none() {
116                                version = Some(truncate_field(val));
117                            }
118                        }
119                        _ => {}
120                    }
121                }
122            }
123            Ok(Event::Start(e)) if e.name().as_ref() == b"assembly" => {
124                for attr in e.attributes().filter_map(|a| a.ok()) {
125                    match attr.key.as_ref() {
126                        b"description" => {
127                            let raw = attr.value.to_vec();
128                            let has_invalid = String::from_utf8(raw.clone()).is_err();
129                            let val = String::from_utf8_lossy(&raw).into_owned();
130                            if has_invalid {
131                                warn!(
132                                    "Invalid UTF-8 in 'description' attribute, using lossy conversion"
133                                );
134                            }
135                            description = Some(truncate_field(val));
136                        }
137                        b"company" => {
138                            let raw = attr.value.to_vec();
139                            let has_invalid = String::from_utf8(raw.clone()).is_err();
140                            let val = String::from_utf8_lossy(&raw).into_owned();
141                            if has_invalid {
142                                warn!(
143                                    "Invalid UTF-8 in 'company' attribute, using lossy conversion"
144                                );
145                            }
146                            company = Some(truncate_field(val));
147                        }
148                        b"copyright" => {
149                            let raw = attr.value.to_vec();
150                            let has_invalid = String::from_utf8(raw.clone()).is_err();
151                            let val = String::from_utf8_lossy(&raw).into_owned();
152                            if has_invalid {
153                                warn!(
154                                    "Invalid UTF-8 in 'copyright' attribute, using lossy conversion"
155                                );
156                            }
157                            copyright = Some(truncate_field(val));
158                        }
159                        b"supportInformation" => {
160                            let raw = attr.value.to_vec();
161                            let has_invalid = String::from_utf8(raw.clone()).is_err();
162                            let val = String::from_utf8_lossy(&raw).into_owned();
163                            if has_invalid {
164                                warn!(
165                                    "Invalid UTF-8 in 'supportInformation' attribute, using lossy conversion"
166                                );
167                            }
168                            homepage_url = Some(truncate_field(val));
169                        }
170                        _ => {}
171                    }
172                }
173            }
174            Ok(Event::Eof) => break,
175            Err(e) => {
176                warn!(
177                    "Error parsing XML at position {}: {}",
178                    reader.buffer_position(),
179                    e
180                );
181                break;
182            }
183            _ => {}
184        }
185        buf.clear();
186    }
187
188    let parties = company.clone().map_or_else(Vec::new, |company_name| {
189        vec![Party {
190            r#type: Some(PartyType::Organization),
191            role: Some("owner".to_string()),
192            name: Some(company_name),
193            email: None,
194            url: None,
195            organization: None,
196            organization_url: None,
197            timezone: None,
198        }]
199    });
200
201    PackageData {
202        package_type: Some(PACKAGE_TYPE),
203        name,
204        version,
205        description,
206        parties,
207        homepage_url,
208        copyright,
209        holder: company,
210        datasource_id: Some(DatasourceId::MicrosoftUpdateManifestMum),
211        ..Default::default()
212    }
213}