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