1use std::collections::HashMap;
22use std::path::Path;
23
24use log::warn;
25use serde::Deserialize;
26
27use crate::models::{DatasourceId, PackageData, PackageType, Party};
28use crate::parsers::utils::read_file_to_string;
29
30use super::PackageParser;
31
32const PACKAGE_TYPE: PackageType = PackageType::Freebsd;
33
34fn default_package_data() -> PackageData {
35 PackageData {
36 package_type: Some(PACKAGE_TYPE),
37 datasource_id: Some(DatasourceId::FreebsdCompactManifest),
38 ..Default::default()
39 }
40}
41
42pub struct FreebsdCompactManifestParser;
44
45impl PackageParser for FreebsdCompactManifestParser {
46 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
47
48 fn is_match(path: &Path) -> bool {
49 path.file_name()
50 .and_then(|name| name.to_str())
51 .map(|name| name == "+COMPACT_MANIFEST")
52 .unwrap_or(false)
53 }
54
55 fn extract_packages(path: &Path) -> Vec<PackageData> {
56 let content = match read_file_to_string(path) {
57 Ok(c) => c,
58 Err(e) => {
59 warn!("Failed to read FreeBSD manifest {:?}: {}", path, e);
60 return vec![default_package_data()];
61 }
62 };
63
64 vec![parse_freebsd_manifest(&content)]
65 }
66}
67
68#[derive(Debug, Deserialize)]
69struct FreebsdManifest {
70 name: Option<String>,
71 version: Option<String>,
72 #[serde(rename = "desc")]
73 description: Option<String>,
74 categories: Option<Vec<String>>,
75 www: Option<String>,
76 maintainer: Option<String>,
77 origin: Option<String>,
78 arch: Option<String>,
79 licenses: Option<Vec<String>>,
80 licenselogic: Option<String>,
81}
82
83pub(crate) fn parse_freebsd_manifest(content: &str) -> PackageData {
84 let manifest: FreebsdManifest = match serde_yaml::from_str(content) {
85 Ok(m) => m,
86 Err(e) => {
87 warn!("Failed to parse FreeBSD manifest: {}", e);
88 return default_package_data();
89 }
90 };
91
92 let name = manifest.name.clone();
93 let version = manifest.version.clone();
94 let description = manifest.description;
95 let homepage_url = manifest.www;
96 let keywords = manifest.categories.unwrap_or_default();
97
98 let mut qualifiers = HashMap::new();
100 if let Some(ref arch) = manifest.arch {
101 qualifiers.insert("arch".to_string(), arch.clone());
102 }
103 if let Some(ref origin) = manifest.origin {
104 qualifiers.insert("origin".to_string(), origin.clone());
105 }
106
107 let mut parties = Vec::new();
109 if let Some(maintainer_email) = manifest.maintainer {
110 parties.push(Party {
111 r#type: Some("person".to_string()),
112 role: Some("maintainer".to_string()),
113 name: None,
114 email: Some(maintainer_email),
115 url: None,
116 organization: None,
117 organization_url: None,
118 timezone: None,
119 });
120 }
121
122 let extracted_license_statement =
124 build_license_statement(&manifest.licenses, &manifest.licenselogic);
125
126 let code_view_url = manifest
128 .origin
129 .as_ref()
130 .map(|origin| format!("https://svnweb.freebsd.org/ports/head/{}", origin));
131
132 let download_url = if let (Some(arch), Some(pkg_name), Some(pkg_version)) =
134 (&manifest.arch, &name, &version)
135 {
136 Some(format!(
137 "https://pkg.freebsd.org/{}/latest/All/{}-{}.txz",
138 arch, pkg_name, pkg_version
139 ))
140 } else {
141 None
142 };
143
144 PackageData {
145 datasource_id: Some(DatasourceId::FreebsdCompactManifest),
146 package_type: Some(PACKAGE_TYPE),
147 name,
148 version,
149 description,
150 homepage_url,
151 keywords,
152 parties,
153 qualifiers: if qualifiers.is_empty() {
154 None
155 } else {
156 Some(qualifiers)
157 },
158 extracted_license_statement,
159 code_view_url,
160 download_url,
161 ..Default::default()
162 }
163}
164
165pub(crate) fn build_license_statement(
173 licenses: &Option<Vec<String>>,
174 licenselogic: &Option<String>,
175) -> Option<String> {
176 let license_list = licenses.as_ref()?;
177
178 if license_list.is_empty() {
179 return None;
180 }
181
182 let filtered_licenses: Vec<String> = license_list
184 .iter()
185 .filter_map(|lic| {
186 let trimmed = lic.trim();
187 if trimmed.is_empty() {
188 None
189 } else {
190 Some(trimmed.to_string())
191 }
192 })
193 .collect();
194
195 if filtered_licenses.is_empty() {
196 return None;
197 }
198
199 let logic = licenselogic.as_deref().unwrap_or("and");
200
201 match logic {
202 "single" => Some(filtered_licenses[0].clone()),
203 "or" | "dual" => Some(filtered_licenses.join(" OR ")),
204 _ => Some(filtered_licenses.join(" AND ")), }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use std::path::PathBuf;
212
213 #[test]
214 fn test_is_match() {
215 assert!(FreebsdCompactManifestParser::is_match(&PathBuf::from(
216 "/path/to/+COMPACT_MANIFEST"
217 )));
218 assert!(FreebsdCompactManifestParser::is_match(&PathBuf::from(
219 "+COMPACT_MANIFEST"
220 )));
221 assert!(!FreebsdCompactManifestParser::is_match(&PathBuf::from(
222 "+MANIFEST"
223 )));
224 assert!(!FreebsdCompactManifestParser::is_match(&PathBuf::from(
225 "COMPACT_MANIFEST"
226 )));
227 assert!(!FreebsdCompactManifestParser::is_match(&PathBuf::from(
228 "package.json"
229 )));
230 }
231
232 #[test]
233 fn test_build_license_statement_single() {
234 let licenses = Some(vec!["GPLv2".to_string()]);
235 let logic = Some("single".to_string());
236 let result = build_license_statement(&licenses, &logic);
237 assert_eq!(result, Some("GPLv2".to_string()));
238 }
239
240 #[test]
241 fn test_build_license_statement_and() {
242 let licenses = Some(vec!["MIT".to_string(), "BSD-2-Clause".to_string()]);
243 let logic = Some("and".to_string());
244 let result = build_license_statement(&licenses, &logic);
245 assert_eq!(result, Some("MIT AND BSD-2-Clause".to_string()));
246 }
247
248 #[test]
249 fn test_build_license_statement_or() {
250 let licenses = Some(vec!["MIT".to_string(), "Apache-2.0".to_string()]);
251 let logic = Some("or".to_string());
252 let result = build_license_statement(&licenses, &logic);
253 assert_eq!(result, Some("MIT OR Apache-2.0".to_string()));
254 }
255
256 #[test]
257 fn test_build_license_statement_dual() {
258 let licenses = Some(vec!["MIT".to_string(), "Apache-2.0".to_string()]);
259 let logic = Some("dual".to_string());
260 let result = build_license_statement(&licenses, &logic);
261 assert_eq!(result, Some("MIT OR Apache-2.0".to_string()));
262 }
263
264 #[test]
265 fn test_build_license_statement_default_and() {
266 let licenses = Some(vec!["MIT".to_string(), "BSD".to_string()]);
267 let logic = None;
268 let result = build_license_statement(&licenses, &logic);
269 assert_eq!(result, Some("MIT AND BSD".to_string()));
270 }
271
272 #[test]
273 fn test_build_license_statement_unknown_defaults_to_and() {
274 let licenses = Some(vec!["MIT".to_string(), "BSD".to_string()]);
275 let logic = Some("unknown".to_string());
276 let result = build_license_statement(&licenses, &logic);
277 assert_eq!(result, Some("MIT AND BSD".to_string()));
278 }
279
280 #[test]
281 fn test_build_license_statement_empty_licenses() {
282 let licenses = Some(vec![]);
283 let logic = Some("and".to_string());
284 let result = build_license_statement(&licenses, &logic);
285 assert_eq!(result, None);
286 }
287
288 #[test]
289 fn test_build_license_statement_no_licenses() {
290 let licenses = None;
291 let logic = Some("and".to_string());
292 let result = build_license_statement(&licenses, &logic);
293 assert_eq!(result, None);
294 }
295
296 #[test]
297 fn test_build_license_statement_filters_empty() {
298 let licenses = Some(vec!["MIT".to_string(), "".to_string(), " ".to_string()]);
299 let logic = Some("and".to_string());
300 let result = build_license_statement(&licenses, &logic);
301 assert_eq!(result, Some("MIT".to_string()));
302 }
303
304 #[test]
305 fn test_build_license_statement_trims_whitespace() {
306 let licenses = Some(vec![" MIT ".to_string(), " Apache-2.0 ".to_string()]);
307 let logic = Some("or".to_string());
308 let result = build_license_statement(&licenses, &logic);
309 assert_eq!(result, Some("MIT OR Apache-2.0".to_string()));
310 }
311}
312
313crate::register_parser!(
314 "FreeBSD +COMPACT_MANIFEST package manifest",
315 &["**/*COMPACT_MANIFEST"],
316 "freebsd",
317 "",
318 Some("https://man.freebsd.org/cgi/man.cgi?query=pkg-create"),
319);