provenant/parsers/
microsoft_update_manifest.rs1use 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}