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