Skip to main content

package_parser/pkgs/java/
maven.rs

1use maplit::hashset;
2use packageurl::PackageUrl;
3use quick_xml::Reader;
4use crate::types::LockMavenParam;
5use crate::types::Relation;
6use serde::{Deserialize, Serialize};
7
8use crate::error::SourcePkgError;
9use crate::pkgs::common::model::{DependentPackage, Package, PackageManifest};
10
11use std::collections::HashMap;
12use std::io::BufRead;
13use std::path::Path;
14
15
16#[derive(Clone, Debug, Serialize, Deserialize)]
17#[serde(rename = "metadata")]
18pub struct Metadata {
19    #[serde(rename = "groupId")]
20    group_id: String,
21    #[serde(rename = "artifactId")]
22    artifact_id: String,
23    versioning: Versioning,
24}
25
26#[derive(Default, Debug, Clone, Serialize, Deserialize)]
27#[serde(rename = "versioning")]
28pub struct Versioning {
29    latest: String,
30    release: String,
31    versions: Versions,
32    #[serde(rename = "lastUpdated")]
33    last_updated: String,
34}
35
36#[derive(Default, Debug, Clone, Serialize, Deserialize)]
37#[serde(rename = "versions")]
38pub struct Versions {
39    #[serde(rename = "version", default)]
40    versions: Vec<String>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, Default)]
44#[serde(rename = "dependencies")]
45pub struct Dependencies {
46    #[serde(default)]
47    pub dependency: Vec<Dependency>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct Dependency {
53    pub group_id: String,
54    pub artifact_id: String,
55    pub version: Option<String>,
56    pub scope: Option<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct Parent {
62    pub group_id: Option<String>,
63    pub artifact_id: Option<String>,
64    pub version: Option<String>,
65    pub relative_path: Option<String>,
66}
67
68// namespace = "http://maven.apache.org/POM/4.0.0"
69#[derive(Default, Debug, Serialize, Deserialize)]
70#[serde(rename = "project", rename_all = "camelCase")]
71pub struct MavenPom {
72    // Coordinates
73    group_id: Option<String>,
74    artifact_id: Option<String>,
75    version: Option<String>,
76
77    parent: Option<Parent>,
78    name: Option<String>,
79    #[serde(default)]
80    dependencies: Dependencies,
81}
82
83#[allow(dead_code)]
84#[derive(Debug, Deserialize)]
85struct ToolError {
86    problems: Vec<String>,
87}
88
89fn parse_properties<R: BufRead>(
90    reader: &mut Reader<R>,
91) -> Result<HashMap<String, String>, SourcePkgError> {
92    use quick_xml::events::Event;
93
94    let mut buffer = Vec::new();
95
96    let mut properties = HashMap::new();
97
98    let mut current_key = None;
99    let mut current_value = String::new();
100
101    loop {
102        match reader.read_event_into(&mut buffer)? {
103            Event::Start(element) => {
104                current_key = Some(String::from_utf8(element.name().as_ref().to_vec())?);
105            }
106            Event::Text(text) => {
107                let text = text.unescape()?;
108                current_value.push_str(&text);
109            }
110            Event::End(element) => {
111                let name = element.name();
112                match current_key.take() {
113                    Some(key) => {
114                        if key.as_bytes() == name.as_ref() {
115                            properties
116                                .insert(format!("${{{key}}}"), current_value.trim().to_string());
117                            current_value.clear();
118                        } else {
119                            return Err(SourcePkgError::GenericsError(
120                                "Failed to parse properties, tags mismatch",
121                            ));
122                        }
123                    }
124                    None => {
125                        if name.as_ref() == b"properties" {
126                            return Ok(properties);
127                        } else {
128                            return Err(SourcePkgError::GenericsError(
129                                "Failed to parse properties, tags mismatch",
130                            ));
131                        }
132                    }
133                }
134            }
135            _ => {}
136        }
137        // Clear the buffer to keep memory usage low
138        buffer.clear();
139    }
140}
141
142#[allow(dead_code)]
143pub struct JavaMavenPom {
144}
145
146impl JavaMavenPom {
147    pub fn new() -> Self {
148        Self {}
149    }
150
151    async fn parse_fallback(
152        &self,
153        path: impl AsRef<Path>,
154    ) -> Result<Vec<LockMavenParam>, SourcePkgError> {
155        use quick_xml::events::Event;
156
157        let path = path.as_ref();
158        let file = tokio::fs::read(path).await?;
159        let file_str = crate::pkgs::common::decode_string(&file)?;
160
161        let mut reader = quick_xml::Reader::from_str(&file_str);
162        let mut buffer = Vec::new();
163
164        let mut properties = loop {
165            match reader.read_event_into(&mut buffer)? {
166                Event::Eof => break HashMap::new(),
167                Event::Start(e) => {
168                    if e.name().as_ref() == b"properties" {
169                        break parse_properties(&mut reader)?;
170                    }
171                }
172                _ => {}
173            }
174            // Clear the buffer to keep memory usage low
175            buffer.clear();
176        };
177
178        let root = quick_xml::de::from_str::<MavenPom>(&file_str)?;
179
180        if let Some(ref s) = root.artifact_id {
181            properties.insert("${project.artifactId}".to_string(), s.clone());
182        }
183        if let Some(ref s) = root.group_id {
184            properties.insert("${project.groupId}".to_string(), s.clone());
185        }
186
187        let process_properties = |version: &str| -> String {
188            if version.contains("${") {
189                let mut version = version.to_string();
190                for (key, value) in &properties {
191                    version = version.replace(key, value);
192                }
193                version
194            } else {
195                version.to_string()
196            }
197        };
198
199        let requirements = root
200            .dependencies
201            .dependency
202            .into_iter()
203            .map(|dep| LockMavenParam {
204                group_id: process_properties(&dep.group_id),
205                artifact_id: process_properties(&dep.artifact_id),
206                version: process_properties(&dep.version.unwrap_or_default()),
207                scope: dep.scope,
208                is_optional: false,
209            })
210            .collect();
211
212        Ok(requirements)
213    }
214
215    async fn parse(&self, path: impl AsRef<Path>) -> Result<Package, SourcePkgError> {
216        let path = path.as_ref();
217
218        let dependencies = self.parse_fallback(path).await?;
219
220        fn convert_to_package(dep: LockMavenParam) -> DependentPackage {
221            let scope = dep.scope.unwrap_or_else(|| "compile".into());
222            let is_runtime = matches!(scope.as_str(), "compile" | "runtime" | "provided");
223
224            DependentPackage {
225                purl: PackageUrl::new("maven", dep.artifact_id)
226                    .unwrap()
227                    .with_namespace(dep.group_id)
228                    .to_string(),
229                requirement: dep.version,
230                scope,
231                is_runtime,
232                is_optional: false,
233                is_resolved: false,
234                relation: hashset! {Relation::Direct},
235                ..Default::default()
236            }
237        }
238
239        let dependent_packages = dependencies
240            .into_iter()
241            .map(convert_to_package)
242            .collect();
243
244        let package = Package {
245            dependencies: dependent_packages,
246            ..Default::default()
247        };
248
249        Ok(package)
250    }
251}
252
253#[async_trait::async_trait]
254impl PackageManifest for JavaMavenPom {
255    fn get_name(&self) -> String {
256        "maven".into()
257    }
258
259    async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
260        self.parse(path).await
261    }
262
263    fn file_name_patterns(&self) -> &'static [&'static str] {
264        &["pom.xml", "pom.xml", "*.pom"]
265    }
266}