1use crate::models::{DatasourceId, Dependency, PackageData, PackageType, Party};
20use crate::parser_warn as warn;
21use packageurl::PackageUrl;
22use serde::{Deserialize, Serialize};
23use std::collections::HashMap;
24use std::fs::File;
25use std::io::Read;
26use std::path::Path;
27
28use super::PackageParser;
29use super::license_normalization::{
30 DeclaredLicenseMatchMetadata, build_declared_license_data_from_pair,
31 empty_declared_license_data,
32};
33
34pub struct HaxeParser;
39
40impl PackageParser for HaxeParser {
41 const PACKAGE_TYPE: PackageType = PackageType::Haxe;
42
43 fn is_match(path: &Path) -> bool {
44 path.file_name().is_some_and(|name| name == "haxelib.json")
45 }
46
47 fn extract_packages(path: &Path) -> Vec<PackageData> {
48 let json_content = match read_haxelib_json(path) {
49 Ok(content) => content,
50 Err(e) => {
51 warn!("Failed to read or parse haxelib.json at {:?}: {}", path, e);
52 return vec![default_package_data()];
53 }
54 };
55
56 let name = json_content.name;
57 let version = json_content.version;
58
59 let purl = create_package_url(&name, &version);
61 let extracted_license_statement = json_content.license.clone();
62 let (declared_license_expression, declared_license_expression_spdx, license_detections) =
63 normalize_haxe_declared_license(extracted_license_statement.as_deref());
64
65 let (repository_homepage_url, download_url, repository_download_url) =
67 if let Some(ref n) = name {
68 let home = format!("https://lib.haxe.org/p/{}", n);
69 if let Some(ref v) = version {
70 let dl = format!("https://lib.haxe.org/p/{}/{}/download/", n, v);
71 (Some(home), Some(dl.clone()), Some(dl))
72 } else {
73 (Some(home), None, None)
74 }
75 } else {
76 (None, None, None)
77 };
78
79 let mut dependencies = Vec::new();
81 let mut deps_list: Vec<_> = json_content.dependencies.into_iter().collect();
82 deps_list.sort_by(|a, b| a.0.cmp(&b.0));
83
84 for (dep_name, dep_version) in deps_list {
85 let is_pinned = !dep_version.is_empty();
86 let dep_purl = create_dep_package_url(&dep_name, &dep_version, is_pinned);
87
88 dependencies.push(Dependency {
89 purl: dep_purl,
90 extracted_requirement: None,
91 scope: None,
92 is_runtime: Some(true),
93 is_optional: Some(false),
94 is_pinned: Some(is_pinned),
95 is_direct: Some(true),
96 resolved_package: None,
97 extra_data: None,
98 });
99 }
100
101 let mut parties = Vec::new();
103 for contrib in json_content.contributors {
104 parties.push(Party {
105 r#type: Some("person".to_string()),
106 role: Some("contributor".to_string()),
107 name: Some(contrib.clone()),
108 email: None,
109 url: Some(format!("https://lib.haxe.org/u/{}", contrib)),
110 organization: None,
111 organization_url: None,
112 timezone: None,
113 });
114 }
115
116 vec![PackageData {
117 package_type: Some(Self::PACKAGE_TYPE),
118 namespace: None,
119 name,
120 version,
121 qualifiers: None,
122 subpath: None,
123 primary_language: Some("Haxe".to_string()),
124 description: json_content.description,
125 release_date: None,
126 parties,
127 keywords: json_content.tags,
128 homepage_url: json_content.url,
129 download_url,
130 size: None,
131 sha1: None,
132 md5: None,
133 sha256: None,
134 sha512: None,
135 bug_tracking_url: None,
136 code_view_url: None,
137 vcs_url: None,
138 copyright: None,
139 holder: None,
140 declared_license_expression,
141 declared_license_expression_spdx,
142 license_detections,
143 other_license_expression: None,
144 other_license_expression_spdx: None,
145 other_license_detections: Vec::new(),
146 extracted_license_statement,
147 notice_text: None,
148 source_packages: Vec::new(),
149 file_references: Vec::new(),
150 is_private: false,
151 is_virtual: false,
152 extra_data: None,
153 dependencies,
154 repository_homepage_url,
155 repository_download_url,
156 api_data_url: None,
157 datasource_id: Some(DatasourceId::HaxelibJson),
158 purl,
159 }]
160 }
161}
162
163#[derive(Debug, Deserialize, Serialize)]
165struct HaxelibJson {
166 #[serde(default)]
167 name: Option<String>,
168 #[serde(default)]
169 version: Option<String>,
170 #[serde(default)]
171 license: Option<String>,
172 #[serde(default)]
173 url: Option<String>,
174 #[serde(default)]
175 description: Option<String>,
176 #[serde(default)]
177 tags: Vec<String>,
178 #[serde(default)]
179 contributors: Vec<String>,
180 #[serde(default)]
181 dependencies: HashMap<String, String>,
182}
183
184fn read_haxelib_json(path: &Path) -> Result<HaxelibJson, String> {
186 let mut file = File::open(path).map_err(|e| format!("Failed to open file: {}", e))?;
187
188 let mut content = String::new();
189 file.read_to_string(&mut content)
190 .map_err(|e| format!("Failed to read file: {}", e))?;
191
192 serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {}", e))
193}
194
195fn create_package_url(name: &Option<String>, version: &Option<String>) -> Option<String> {
197 name.as_ref().and_then(|name| {
198 let mut package_url = match PackageUrl::new("haxe", name) {
199 Ok(p) => p,
200 Err(e) => {
201 warn!(
202 "Failed to create PackageUrl for haxe package '{}': {}",
203 name, e
204 );
205 return None;
206 }
207 };
208
209 if let Some(v) = version
210 && let Err(e) = package_url.with_version(v)
211 {
212 warn!(
213 "Failed to set version '{}' for haxe package '{}': {}",
214 v, name, e
215 );
216 return None;
217 }
218
219 Some(package_url.to_string())
220 })
221}
222
223fn create_dep_package_url(name: &str, version: &str, is_pinned: bool) -> Option<String> {
225 let mut package_url = match PackageUrl::new("haxe", name) {
226 Ok(p) => p,
227 Err(e) => {
228 warn!(
229 "Failed to create PackageUrl for haxe dependency '{}': {}",
230 name, e
231 );
232 return None;
233 }
234 };
235
236 if is_pinned && let Err(e) = package_url.with_version(version) {
237 warn!(
238 "Failed to set version '{}' for haxe dependency '{}': {}",
239 version, name, e
240 );
241 return None;
242 }
243
244 Some(package_url.to_string())
245}
246
247fn default_package_data() -> PackageData {
248 PackageData {
249 package_type: Some(HaxeParser::PACKAGE_TYPE),
250 primary_language: Some("Haxe".to_string()),
251 datasource_id: Some(DatasourceId::HaxelibJson),
252 ..Default::default()
253 }
254}
255
256fn normalize_haxe_declared_license(
257 statement: Option<&str>,
258) -> (
259 Option<String>,
260 Option<String>,
261 Vec<crate::models::LicenseDetection>,
262) {
263 match statement.map(str::trim).filter(|value| !value.is_empty()) {
264 Some("MIT") => build_declared_license_data_from_pair(
265 "mit",
266 "MIT",
267 DeclaredLicenseMatchMetadata::single_line("MIT"),
268 ),
269 _ => empty_declared_license_data(),
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::models::DatasourceId;
277 use std::path::PathBuf;
278
279 #[test]
280 fn test_is_match() {
281 let valid_path = PathBuf::from("/some/path/haxelib.json");
282 let invalid_path = PathBuf::from("/some/path/not_haxelib.json");
283
284 assert!(HaxeParser::is_match(&valid_path));
285 assert!(!HaxeParser::is_match(&invalid_path));
286 }
287
288 #[test]
289 fn test_extract_from_testdata_basic() {
290 let haxelib_path = PathBuf::from("testdata/haxe/basic/haxelib.json");
291 let package_data = HaxeParser::extract_first_package(&haxelib_path);
292
293 assert_eq!(package_data.package_type, Some(PackageType::Haxe));
294 assert_eq!(package_data.name, Some("haxelib".to_string()));
295 assert_eq!(package_data.version, Some("3.4.0".to_string()));
296 assert_eq!(
297 package_data.homepage_url,
298 Some("https://lib.haxe.org/documentation/".to_string())
299 );
300 assert_eq!(
301 package_data.download_url,
302 Some("https://lib.haxe.org/p/haxelib/3.4.0/download/".to_string())
303 );
304 assert_eq!(
305 package_data.repository_homepage_url,
306 Some("https://lib.haxe.org/p/haxelib".to_string())
307 );
308 assert_eq!(
309 package_data.extracted_license_statement,
310 Some("GPL".to_string())
311 );
312
313 assert_eq!(
315 package_data.purl,
316 Some("pkg:haxe/haxelib@3.4.0".to_string())
317 );
318
319 assert_eq!(package_data.parties.len(), 6);
321 let names: Vec<&str> = package_data
322 .parties
323 .iter()
324 .filter_map(|p| p.name.as_deref())
325 .collect();
326 assert!(names.contains(&"back2dos"));
327 assert!(names.contains(&"ncannasse"));
328 }
329
330 #[test]
331 fn test_extract_with_dependencies() {
332 let haxelib_path = PathBuf::from("testdata/haxe/deps/haxelib.json");
333 let package_data = HaxeParser::extract_first_package(&haxelib_path);
334
335 assert_eq!(package_data.name, Some("selecthxml".to_string()));
336 assert_eq!(package_data.version, Some("0.5.1".to_string()));
337
338 assert_eq!(package_data.dependencies.len(), 2);
340
341 let pinned_deps: Vec<_> = package_data
342 .dependencies
343 .iter()
344 .filter(|d| d.is_pinned == Some(true))
345 .collect();
346 assert_eq!(pinned_deps.len(), 1);
347 assert!(pinned_deps[0].purl.as_ref().unwrap().contains("@3.23"));
348
349 let unpinned_deps: Vec<_> = package_data
350 .dependencies
351 .iter()
352 .filter(|d| d.is_pinned == Some(false))
353 .collect();
354 assert_eq!(unpinned_deps.len(), 1);
355 }
356
357 #[test]
358 fn test_extract_with_tags() {
359 let haxelib_path = PathBuf::from("testdata/haxe/tags/haxelib.json");
360 let package_data = HaxeParser::extract_first_package(&haxelib_path);
361
362 assert_eq!(package_data.name, Some("tink_core".to_string()));
363 assert_eq!(package_data.version, Some("1.18.0".to_string()));
364 assert_eq!(
365 package_data.extracted_license_statement,
366 Some("MIT".to_string())
367 );
368
369 assert_eq!(
371 package_data.keywords,
372 vec![
373 "tink".to_string(),
374 "cross".to_string(),
375 "utility".to_string(),
376 "reactive".to_string(),
377 "functional".to_string(),
378 "async".to_string(),
379 "lazy".to_string(),
380 "signal".to_string(),
381 "event".to_string(),
382 ]
383 );
384 }
385
386 #[test]
387 fn test_invalid_file() {
388 let nonexistent_path = PathBuf::from("testdata/haxe/nonexistent/haxelib.json");
389 let package_data = HaxeParser::extract_first_package(&nonexistent_path);
390
391 assert_eq!(package_data.package_type, Some(PackageType::Haxe));
393 assert_eq!(package_data.datasource_id, Some(DatasourceId::HaxelibJson));
394 assert!(package_data.name.is_none());
395 }
396}
397
398crate::register_parser!(
399 "Haxe haxelib.json package manifest",
400 &["**/haxelib.json"],
401 "haxe",
402 "Haxe",
403 Some("https://lib.haxe.org/documentation/creating-a-haxelib-package/"),
404);