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