1use std::collections::HashMap;
12use std::path::Path;
13
14use packageurl::PackageUrl;
15use quick_xml::Reader;
16use quick_xml::XmlVersion;
17use quick_xml::events::{BytesStart, Event};
18use serde_json::Value as JsonValue;
19
20use super::PackageParser;
21use super::metadata::ParserMetadata;
22use crate::models::{DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage};
23use crate::parser_warn as warn;
24use crate::parsers::utils::{
25 CappedIterExt, MAX_ITERATION_COUNT, read_file_to_string, truncate_field,
26};
27
28pub struct IvyXmlParser;
29pub struct IvyDependenciesPropertiesParser;
30
31impl PackageParser for IvyXmlParser {
32 const PACKAGE_TYPE: PackageType = PackageType::Ivy;
33
34 fn is_match(path: &Path) -> bool {
35 path.to_str().is_some_and(|p| p.ends_with("/ivy.xml"))
36 }
37
38 fn extract_packages(path: &Path) -> Vec<PackageData> {
39 let content = match read_file_to_string(path, None) {
40 Ok(content) => content,
41 Err(e) => {
42 warn!("Failed to read ivy.xml at {:?}: {}", path, e);
43 return vec![default_ivy_package_data()];
44 }
45 };
46
47 vec![interpret_ivy_xml(&content, path)]
48 }
49
50 fn metadata() -> Vec<ParserMetadata> {
51 vec![ParserMetadata {
52 description: "Apache Ant/Ivy dependency manifest",
53 file_patterns: &["**/ivy.xml"],
54 package_type: "ivy",
55 primary_language: "Java",
56 documentation_url: Some(
57 "https://ant.apache.org/ivy/history/latest-milestone/ivyfile.html",
58 ),
59 }]
60 }
61}
62
63impl PackageParser for IvyDependenciesPropertiesParser {
64 const PACKAGE_TYPE: PackageType = PackageType::Maven;
65
66 fn is_match(path: &Path) -> bool {
67 path.file_name()
68 .and_then(|name| name.to_str())
69 .is_some_and(is_ivy_dependencies_properties_filename)
70 }
71
72 fn extract_packages(path: &Path) -> Vec<PackageData> {
73 let content = match read_file_to_string(path, None) {
74 Ok(content) => content,
75 Err(e) => {
76 warn!(
77 "Failed to read dependencies.properties at {:?}: {}",
78 path, e
79 );
80 return vec![default_ivy_dependencies_properties_data(Vec::new())];
81 }
82 };
83
84 let scope = dependency_scope_from_filename(path);
85 let dependencies = extract_ivy_dependencies_properties(&content, scope.as_deref());
86
87 vec![default_ivy_dependencies_properties_data(dependencies)]
88 }
89
90 fn metadata() -> Vec<ParserMetadata> {
91 vec![ParserMetadata {
92 description: "Ivy-style dependencies.properties Maven dependency list",
93 file_patterns: &["**/dependencies.properties", "**/*-dependencies.properties"],
94 package_type: "maven",
95 primary_language: "Java",
96 documentation_url: Some(
97 "https://github.com/aboutcode-org/scancode-toolkit/issues/3468",
98 ),
99 }]
100 }
101}
102
103fn default_ivy_package_data() -> PackageData {
104 PackageData {
105 package_type: Some(PackageType::Ivy),
106 primary_language: Some("Java".to_string()),
107 datasource_id: Some(DatasourceId::AntIvyXml),
108 ..Default::default()
109 }
110}
111
112fn default_ivy_dependencies_properties_data(dependencies: Vec<Dependency>) -> PackageData {
113 PackageData {
114 package_type: Some(IvyDependenciesPropertiesParser::PACKAGE_TYPE),
115 primary_language: Some("Java".to_string()),
116 dependencies,
117 datasource_id: Some(DatasourceId::AntIvyDependenciesProperties),
118 ..Default::default()
119 }
120}
121
122fn attr_value(element: &BytesStart, key: &[u8]) -> Option<String> {
123 element
124 .attributes()
125 .filter_map(|attr| attr.ok())
126 .find(|attr| attr.key.as_ref() == key)
127 .and_then(|attr| {
129 attr.normalized_value(XmlVersion::Implicit1_0)
130 .ok()
131 .map(|value| value.into_owned())
132 })
133 .map(|value| value.trim().to_string())
134 .filter(|value| !value.is_empty())
135}
136
137fn interpret_ivy_xml(content: &str, path: &Path) -> PackageData {
138 let mut package_data = default_ivy_package_data();
139
140 let mut reader = Reader::from_str(content);
141 reader.config_mut().trim_text(true);
142
143 let mut namespace: Option<String> = None;
144 let mut name: Option<String> = None;
145 let mut version: Option<String> = None;
146 let mut dependencies: Vec<Dependency> = Vec::new();
147
148 let mut buf = Vec::new();
149 let mut in_dependencies = false;
150 let mut iteration_count: usize = 0;
151
152 loop {
153 iteration_count += 1;
154 if iteration_count > MAX_ITERATION_COUNT {
155 warn!(
156 "Iteration limit exceeded in ivy.xml at {:?}; stopping at {} items",
157 path, MAX_ITERATION_COUNT
158 );
159 break;
160 }
161
162 match reader.read_event_into(&mut buf) {
163 Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
164 let tag = e.name();
165 match tag.as_ref() {
166 b"info" => {
167 namespace = attr_value(&e, b"organisation");
168 name = attr_value(&e, b"module");
169 version = attr_value(&e, b"revision");
170 }
171 b"dependencies" => in_dependencies = true,
172 b"dependency" if in_dependencies => {
173 if let Some(dep) = parse_ivy_dependency(&e) {
174 dependencies.push(dep);
175 }
176 }
177 _ => {}
178 }
179 }
180 Ok(Event::End(e)) if e.name().as_ref() == b"dependencies" => {
181 in_dependencies = false;
182 }
183 Ok(Event::Eof) => break,
184 Err(e) => {
185 warn!("Error parsing ivy.xml at {:?}: {}", path, e);
186 break;
187 }
188 _ => {}
189 }
190 buf.clear();
191 }
192
193 package_data.namespace = namespace.map(truncate_field);
194 package_data.name = name.map(truncate_field);
195 package_data.version = version.map(truncate_field);
196 package_data.dependencies = dependencies;
197
198 if let (Some(namespace), Some(name)) = (&package_data.namespace, &package_data.name) {
199 package_data.purl = Some(truncate_field(build_ivy_purl(
200 namespace,
201 name,
202 package_data.version.as_deref(),
203 )));
204 }
205
206 package_data
207}
208
209fn is_ivy_dependencies_properties_filename(name: &str) -> bool {
210 name == "dependencies.properties" || name.ends_with("-dependencies.properties")
211}
212
213fn dependency_scope_from_filename(path: &Path) -> Option<String> {
214 path.file_name()
215 .and_then(|name| name.to_str())
216 .and_then(|name| name.strip_suffix("-dependencies.properties"))
217 .map(str::trim)
218 .filter(|scope| !scope.is_empty())
219 .map(|scope| truncate_field(scope.to_string()))
220}
221
222fn extract_ivy_dependencies_properties(content: &str, scope: Option<&str>) -> Vec<Dependency> {
223 parse_properties_entries(content)
224 .into_iter()
225 .filter_map(|(key, value)| parse_ivy_dependencies_entry(&key, &value, scope))
226 .collect()
227}
228
229fn parse_properties_entries(content: &str) -> Vec<(String, String)> {
230 let mut entries = Vec::new();
231 let mut current = String::new();
232
233 for raw_line in content
234 .lines()
235 .capped("Ivy dependencies.properties raw lines")
236 {
237 let line = raw_line.trim_end_matches('\r');
238 let trimmed_end = line.trim_end();
239 if current.is_empty() && line.trim_start().starts_with(['#', '!']) {
240 continue;
241 }
242
243 let is_continuation = trimmed_end.ends_with('\\');
244 let line_without_slash = if is_continuation {
245 trimmed_end.trim_end_matches('\\')
246 } else {
247 line
248 };
249
250 if !current.is_empty() {
251 current.push_str(line_without_slash.trim_start());
252 } else {
253 current.push_str(line_without_slash);
254 }
255
256 if is_continuation {
257 continue;
258 }
259
260 let logical_line = current.trim();
261 if !logical_line.is_empty()
262 && !logical_line.starts_with('#')
263 && !logical_line.starts_with('!')
264 && let Some((key, value)) = logical_line.split_once('=')
265 {
266 let key = key.trim();
267 let value = strip_inline_comment(value.trim());
268 if !key.is_empty() && !value.is_empty() {
269 entries.push((key.to_string(), value.to_string()));
270 }
271 }
272
273 current.clear();
274 }
275
276 entries
277}
278
279fn strip_inline_comment(value: &str) -> &str {
287 match value.find(" #").or_else(|| value.find("\t#")) {
288 Some(index) => value[..index].trim_end(),
289 None => value,
290 }
291}
292
293fn parse_ivy_dependencies_entry(key: &str, value: &str, scope: Option<&str>) -> Option<Dependency> {
294 if let Some((group, artifact, version)) = parse_maven_gav(value) {
295 return Some(build_maven_dependency(
296 group,
297 artifact,
298 version,
299 Some(key),
300 "value_gav",
301 scope,
302 ));
303 }
304
305 let (group, artifact) = parse_maven_ga(key)?;
306 if value.contains(':') {
307 return None;
308 }
309
310 Some(build_maven_dependency(
311 group,
312 artifact,
313 value,
314 Some(key),
315 "key_ga",
316 scope,
317 ))
318}
319
320fn parse_maven_gav(value: &str) -> Option<(&str, &str, &str)> {
321 let mut parts = value.split(':').map(str::trim);
322 let group = parts.next()?;
323 let artifact = parts.next()?;
324 let version = parts.next()?;
325 if parts.next().is_some() || group.is_empty() || artifact.is_empty() || version.is_empty() {
326 return None;
327 }
328
329 Some((group, artifact, version))
330}
331
332fn parse_maven_ga(value: &str) -> Option<(&str, &str)> {
333 let mut parts = value.split(':').map(str::trim);
334 let group = parts.next()?;
335 let artifact = parts.next()?;
336 if parts.next().is_some() || group.is_empty() || artifact.is_empty() {
337 return None;
338 }
339
340 Some((group, artifact))
341}
342
343fn build_maven_dependency(
344 group: &str,
345 artifact: &str,
346 version: &str,
347 property_name: Option<&str>,
348 coordinate_format: &str,
349 scope: Option<&str>,
350) -> Dependency {
351 let group = truncate_field(group.to_string());
352 let artifact = truncate_field(artifact.to_string());
353 let version = truncate_field(version.to_string());
354
355 let purl = PackageUrl::new("maven", &artifact)
356 .ok()
357 .and_then(|mut purl| {
358 purl.with_namespace(&group).ok()?;
359 purl.with_version(&version).ok()?;
360 Some(truncate_field(purl.to_string()))
361 });
362
363 let mut extra_data = HashMap::from([
364 (
365 "coordinate_format".to_string(),
366 JsonValue::String(coordinate_format.to_string()),
367 ),
368 ("group".to_string(), JsonValue::String(group.clone())),
369 ("artifact".to_string(), JsonValue::String(artifact.clone())),
370 ]);
371 if let Some(property_name) = property_name.filter(|name| !name.trim().is_empty()) {
372 extra_data.insert(
373 "property_name".to_string(),
374 JsonValue::String(truncate_field(property_name.to_string())),
375 );
376 }
377
378 let resolved_package = ResolvedPackage {
379 primary_language: Some("Java".to_string()),
380 datasource_id: Some(DatasourceId::AntIvyDependenciesProperties),
381 purl: purl.clone(),
382 ..ResolvedPackage::new(PackageType::Maven, group, artifact, version.clone())
383 };
384
385 Dependency {
386 purl,
387 extracted_requirement: Some(version),
388 scope: scope.map(|scope| truncate_field(scope.to_string())),
389 is_runtime: None,
390 is_optional: None,
391 is_pinned: Some(true),
392 is_direct: Some(true),
393 resolved_package: Some(Box::new(resolved_package)),
394 extra_data: Some(extra_data),
395 }
396}
397
398fn parse_ivy_dependency(element: &BytesStart) -> Option<Dependency> {
399 let org = attr_value(element, b"org");
400 let name = attr_value(element, b"name")?;
401 let rev = attr_value(element, b"rev");
402 let conf = attr_value(element, b"conf");
403
404 let purl = match &org {
405 Some(org) => build_ivy_purl(org, &name, None),
406 None => format!("pkg:ivy/{}", name),
407 };
408
409 Some(Dependency {
410 purl: Some(truncate_field(purl)),
411 extracted_requirement: rev.map(truncate_field),
412 scope: conf.map(truncate_field),
413 is_runtime: None,
414 is_optional: None,
415 is_pinned: None,
416 is_direct: Some(true),
417 resolved_package: None,
418 extra_data: None,
419 })
420}
421
422fn build_ivy_purl(organisation: &str, module: &str, revision: Option<&str>) -> String {
423 let mut purl = format!("pkg:ivy/{}/{}", organisation, module);
424 if let Some(revision) = revision.filter(|value| !value.trim().is_empty()) {
425 purl.push('@');
426 purl.push_str(revision);
427 }
428 purl
429}