Skip to main content

provenant/parsers/
microsoft_update_manifest.rs

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