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 = build_ivy_purl(namespace, name, package_data.version.as_deref());
200 }
201
202 package_data
203}
204
205fn is_ivy_dependencies_properties_filename(name: &str) -> bool {
206 name == "dependencies.properties" || name.ends_with("-dependencies.properties")
207}
208
209fn dependency_scope_from_filename(path: &Path) -> Option<String> {
210 path.file_name()
211 .and_then(|name| name.to_str())
212 .and_then(|name| name.strip_suffix("-dependencies.properties"))
213 .map(str::trim)
214 .filter(|scope| !scope.is_empty())
215 .map(|scope| truncate_field(scope.to_string()))
216}
217
218fn extract_ivy_dependencies_properties(content: &str, scope: Option<&str>) -> Vec<Dependency> {
219 parse_properties_entries(content)
220 .into_iter()
221 .filter_map(|(key, value)| parse_ivy_dependencies_entry(&key, &value, scope))
222 .collect()
223}
224
225fn parse_properties_entries(content: &str) -> Vec<(String, String)> {
226 let mut entries = Vec::new();
227 let mut current = String::new();
228
229 for raw_line in content
230 .lines()
231 .capped("Ivy dependencies.properties raw lines")
232 {
233 let line = raw_line.trim_end_matches('\r');
234 let trimmed_end = line.trim_end();
235 if current.is_empty() && line.trim_start().starts_with(['#', '!']) {
236 continue;
237 }
238
239 let is_continuation = trimmed_end.ends_with('\\');
240 let line_without_slash = if is_continuation {
241 trimmed_end.trim_end_matches('\\')
242 } else {
243 line
244 };
245
246 if !current.is_empty() {
247 current.push_str(line_without_slash.trim_start());
248 } else {
249 current.push_str(line_without_slash);
250 }
251
252 if is_continuation {
253 continue;
254 }
255
256 let logical_line = current.trim();
257 if !logical_line.is_empty()
258 && !logical_line.starts_with('#')
259 && !logical_line.starts_with('!')
260 && let Some((key, value)) = logical_line.split_once('=')
261 {
262 let key = key.trim();
263 let value = strip_inline_comment(value.trim());
264 if !key.is_empty() && !value.is_empty() {
265 entries.push((key.to_string(), value.to_string()));
266 }
267 }
268
269 current.clear();
270 }
271
272 entries
273}
274
275fn strip_inline_comment(value: &str) -> &str {
283 match value.find(" #").or_else(|| value.find("\t#")) {
284 Some(index) => value[..index].trim_end(),
285 None => value,
286 }
287}
288
289fn parse_ivy_dependencies_entry(key: &str, value: &str, scope: Option<&str>) -> Option<Dependency> {
290 if let Some((group, artifact, version)) = parse_maven_gav(value) {
291 return Some(build_maven_dependency(
292 group,
293 artifact,
294 version,
295 Some(key),
296 "value_gav",
297 scope,
298 ));
299 }
300
301 let (group, artifact) = parse_maven_ga(key)?;
302 if value.contains(':') {
303 return None;
304 }
305
306 Some(build_maven_dependency(
307 group,
308 artifact,
309 value,
310 Some(key),
311 "key_ga",
312 scope,
313 ))
314}
315
316fn parse_maven_gav(value: &str) -> Option<(&str, &str, &str)> {
317 let mut parts = value.split(':').map(str::trim);
318 let group = parts.next()?;
319 let artifact = parts.next()?;
320 let version = parts.next()?;
321 if parts.next().is_some() || group.is_empty() || artifact.is_empty() || version.is_empty() {
322 return None;
323 }
324
325 Some((group, artifact, version))
326}
327
328fn parse_maven_ga(value: &str) -> Option<(&str, &str)> {
329 let mut parts = value.split(':').map(str::trim);
330 let group = parts.next()?;
331 let artifact = parts.next()?;
332 if parts.next().is_some() || group.is_empty() || artifact.is_empty() {
333 return None;
334 }
335
336 Some((group, artifact))
337}
338
339fn build_maven_dependency(
340 group: &str,
341 artifact: &str,
342 version: &str,
343 property_name: Option<&str>,
344 coordinate_format: &str,
345 scope: Option<&str>,
346) -> Dependency {
347 let group = truncate_field(group.to_string());
348 let artifact = truncate_field(artifact.to_string());
349 let version = truncate_field(version.to_string());
350
351 let purl = PackageUrl::new("maven", &artifact)
352 .ok()
353 .and_then(|mut purl| {
354 purl.with_namespace(&group).ok()?;
355 purl.with_version(&version).ok()?;
356 Some(truncate_field(purl.to_string()))
357 });
358
359 let mut extra_data = HashMap::from([
360 (
361 "coordinate_format".to_string(),
362 JsonValue::String(coordinate_format.to_string()),
363 ),
364 ("group".to_string(), JsonValue::String(group.clone())),
365 ("artifact".to_string(), JsonValue::String(artifact.clone())),
366 ]);
367 if let Some(property_name) = property_name.filter(|name| !name.trim().is_empty()) {
368 extra_data.insert(
369 "property_name".to_string(),
370 JsonValue::String(truncate_field(property_name.to_string())),
371 );
372 }
373
374 let resolved_package = ResolvedPackage {
375 primary_language: Some("Java".to_string()),
376 datasource_id: Some(DatasourceId::AntIvyDependenciesProperties),
377 purl: purl.clone(),
378 ..ResolvedPackage::new(PackageType::Maven, group, artifact, version.clone())
379 };
380
381 Dependency {
382 purl,
383 extracted_requirement: Some(version),
384 scope: scope.map(|scope| truncate_field(scope.to_string())),
385 is_runtime: None,
386 is_optional: None,
387 is_pinned: Some(true),
388 is_direct: Some(true),
389 resolved_package: Some(Box::new(resolved_package)),
390 extra_data: Some(extra_data),
391 }
392}
393
394fn parse_ivy_dependency(element: &BytesStart) -> Option<Dependency> {
395 let org = attr_value(element, b"org");
396 let name = attr_value(element, b"name")?;
397 let rev = attr_value(element, b"rev");
398 let conf = attr_value(element, b"conf");
399
400 let purl = match &org {
403 Some(org) => build_ivy_purl(org, &name, None),
404 None => crate::parsers::utils::simple_purl("ivy", &name, None),
405 };
406
407 Some(Dependency {
408 purl,
409 extracted_requirement: rev.map(truncate_field),
410 scope: conf.map(truncate_field),
411 is_runtime: None,
412 is_optional: None,
413 is_pinned: None,
414 is_direct: Some(true),
415 resolved_package: None,
416 extra_data: None,
417 })
418}
419
420fn build_ivy_purl(organisation: &str, module: &str, revision: Option<&str>) -> Option<String> {
427 crate::parsers::utils::namespaced_purl("ivy", organisation, module, revision)
428}