1use std::path::Path;
12
13use quick_xml::Reader;
14use quick_xml::XmlVersion;
15use quick_xml::events::{BytesStart, Event};
16
17use super::PackageParser;
18use super::metadata::ParserMetadata;
19use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
20use crate::parser_warn as warn;
21use crate::parsers::utils::{MAX_ITERATION_COUNT, read_file_to_string, truncate_field};
22
23pub struct IvyXmlParser;
24
25impl PackageParser for IvyXmlParser {
26 const PACKAGE_TYPE: PackageType = PackageType::Ivy;
27
28 fn is_match(path: &Path) -> bool {
29 path.to_str().is_some_and(|p| p.ends_with("/ivy.xml"))
30 }
31
32 fn extract_packages(path: &Path) -> Vec<PackageData> {
33 let content = match read_file_to_string(path, None) {
34 Ok(content) => content,
35 Err(e) => {
36 warn!("Failed to read ivy.xml at {:?}: {}", path, e);
37 return vec![default_ivy_package_data()];
38 }
39 };
40
41 vec![interpret_ivy_xml(&content, path)]
42 }
43
44 fn metadata() -> Vec<ParserMetadata> {
45 vec![ParserMetadata {
46 description: "Apache Ant/Ivy dependency manifest",
47 file_patterns: &["**/ivy.xml"],
48 package_type: "ivy",
49 primary_language: "Java",
50 documentation_url: Some(
51 "https://ant.apache.org/ivy/history/latest-milestone/ivyfile.html",
52 ),
53 }]
54 }
55}
56
57fn default_ivy_package_data() -> PackageData {
58 PackageData {
59 package_type: Some(PackageType::Ivy),
60 primary_language: Some("Java".to_string()),
61 datasource_id: Some(DatasourceId::AntIvyXml),
62 ..Default::default()
63 }
64}
65
66fn attr_value(element: &BytesStart, key: &[u8]) -> Option<String> {
67 element
68 .attributes()
69 .filter_map(|attr| attr.ok())
70 .find(|attr| attr.key.as_ref() == key)
71 .and_then(|attr| {
73 attr.normalized_value(XmlVersion::Implicit1_0)
74 .ok()
75 .map(|value| value.into_owned())
76 })
77 .map(|value| value.trim().to_string())
78 .filter(|value| !value.is_empty())
79}
80
81fn interpret_ivy_xml(content: &str, path: &Path) -> PackageData {
82 let mut package_data = default_ivy_package_data();
83
84 let mut reader = Reader::from_str(content);
85 reader.config_mut().trim_text(true);
86
87 let mut namespace: Option<String> = None;
88 let mut name: Option<String> = None;
89 let mut version: Option<String> = None;
90 let mut dependencies: Vec<Dependency> = Vec::new();
91
92 let mut buf = Vec::new();
93 let mut in_dependencies = false;
94 let mut iteration_count: usize = 0;
95
96 loop {
97 iteration_count += 1;
98 if iteration_count > MAX_ITERATION_COUNT {
99 warn!(
100 "Iteration limit exceeded in ivy.xml at {:?}; stopping at {} items",
101 path, MAX_ITERATION_COUNT
102 );
103 break;
104 }
105
106 match reader.read_event_into(&mut buf) {
107 Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
108 let tag = e.name();
109 match tag.as_ref() {
110 b"info" => {
111 namespace = attr_value(&e, b"organisation");
112 name = attr_value(&e, b"module");
113 version = attr_value(&e, b"revision");
114 }
115 b"dependencies" => in_dependencies = true,
116 b"dependency" if in_dependencies => {
117 if let Some(dep) = parse_ivy_dependency(&e) {
118 dependencies.push(dep);
119 }
120 }
121 _ => {}
122 }
123 }
124 Ok(Event::End(e)) if e.name().as_ref() == b"dependencies" => {
125 in_dependencies = false;
126 }
127 Ok(Event::Eof) => break,
128 Err(e) => {
129 warn!("Error parsing ivy.xml at {:?}: {}", path, e);
130 break;
131 }
132 _ => {}
133 }
134 buf.clear();
135 }
136
137 package_data.namespace = namespace.map(truncate_field);
138 package_data.name = name.map(truncate_field);
139 package_data.version = version.map(truncate_field);
140 package_data.dependencies = dependencies;
141
142 if let (Some(namespace), Some(name)) = (&package_data.namespace, &package_data.name) {
143 package_data.purl = Some(truncate_field(build_ivy_purl(
144 namespace,
145 name,
146 package_data.version.as_deref(),
147 )));
148 }
149
150 package_data
151}
152
153fn parse_ivy_dependency(element: &BytesStart) -> Option<Dependency> {
154 let org = attr_value(element, b"org");
155 let name = attr_value(element, b"name")?;
156 let rev = attr_value(element, b"rev");
157 let conf = attr_value(element, b"conf");
158
159 let purl = match &org {
160 Some(org) => build_ivy_purl(org, &name, None),
161 None => format!("pkg:ivy/{}", name),
162 };
163
164 Some(Dependency {
165 purl: Some(truncate_field(purl)),
166 extracted_requirement: rev.map(truncate_field),
167 scope: conf.map(truncate_field),
168 is_runtime: None,
169 is_optional: None,
170 is_pinned: None,
171 is_direct: Some(true),
172 resolved_package: None,
173 extra_data: None,
174 })
175}
176
177fn build_ivy_purl(organisation: &str, module: &str, revision: Option<&str>) -> String {
178 let mut purl = format!("pkg:ivy/{}/{}", organisation, module);
179 if let Some(revision) = revision.filter(|value| !value.trim().is_empty()) {
180 purl.push('@');
181 purl.push_str(revision);
182 }
183 purl
184}