provenant/parsers/
publiccode.rs1use std::fs;
2use std::path::Path;
3
4use crate::models::{DatasourceId, PackageData, PackageType, Party};
5use crate::parser_warn as warn;
6
7use super::PackageParser;
8use super::license_normalization::normalize_spdx_declared_license;
9
10pub struct PubliccodeParser;
11
12impl PackageParser for PubliccodeParser {
13 const PACKAGE_TYPE: PackageType = PackageType::Publiccode;
14
15 fn is_match(path: &Path) -> bool {
16 matches!(
17 path.file_name().and_then(|name| name.to_str()),
18 Some("publiccode.yml" | "publiccode.yaml")
19 )
20 }
21
22 fn extract_packages(path: &Path) -> Vec<PackageData> {
23 let content = match fs::read_to_string(path) {
24 Ok(content) => content,
25 Err(error) => {
26 warn!(
27 "Failed to read publiccode metadata at {:?}: {}",
28 path, error
29 );
30 return vec![default_package_data()];
31 }
32 };
33
34 let yaml: yaml_serde::Value = match yaml_serde::from_str(&content) {
35 Ok(yaml) => yaml,
36 Err(error) => {
37 warn!(
38 "Failed to parse publiccode metadata at {:?}: {}",
39 path, error
40 );
41 return vec![default_package_data()];
42 }
43 };
44
45 vec![parse_publiccode(&yaml)]
46 }
47}
48
49fn default_package_data() -> PackageData {
50 PackageData {
51 package_type: Some(PubliccodeParser::PACKAGE_TYPE),
52 datasource_id: Some(DatasourceId::PubliccodeYaml),
53 ..Default::default()
54 }
55}
56
57fn parse_publiccode(yaml: &yaml_serde::Value) -> PackageData {
58 if yaml
59 .get("publiccodeYmlVersion")
60 .and_then(yaml_value_as_string)
61 .is_none()
62 {
63 return default_package_data();
64 }
65
66 let mut package = default_package_data();
67 package.name = yaml
68 .get("name")
69 .and_then(extract_localized_string)
70 .map(str::to_string);
71 package.version = yaml
72 .get("softwareVersion")
73 .and_then(yaml_value_as_string)
74 .map(str::to_string);
75 package.vcs_url = yaml
76 .get("url")
77 .and_then(yaml_value_as_string)
78 .map(str::to_string);
79 package.homepage_url = yaml
80 .get("landingURL")
81 .and_then(yaml_value_as_string)
82 .map(str::to_string);
83 package.description = yaml
84 .get("longDescription")
85 .and_then(extract_localized_string)
86 .or_else(|| {
87 yaml.get("shortDescription")
88 .and_then(extract_localized_string)
89 })
90 .map(str::to_string);
91 package.copyright = yaml
92 .get("legal")
93 .and_then(|legal| legal.get("mainCopyrightOwner"))
94 .and_then(yaml_value_as_string)
95 .or_else(|| yaml.get("repoOwner").and_then(yaml_value_as_string))
96 .map(str::to_string);
97 package.parties = extract_contact_parties(yaml.get("maintenance"));
98
99 if let Some(license) = yaml
100 .get("legal")
101 .and_then(|legal| legal.get("license"))
102 .and_then(yaml_value_as_string)
103 {
104 let license = license.to_string();
105 package.extracted_license_statement = Some(license.clone());
106 let (declared, declared_spdx, detections) = normalize_spdx_declared_license(Some(&license));
107 package.declared_license_expression = declared;
108 package.declared_license_expression_spdx = declared_spdx;
109 package.license_detections = detections;
110 }
111
112 package
113}
114
115fn extract_localized_string(value: &yaml_serde::Value) -> Option<&str> {
116 if let Some(string) = value.as_str() {
117 return Some(string);
118 }
119
120 if let Some(english) = value.get("en").and_then(yaml_value_as_string) {
121 return Some(english);
122 }
123
124 value
125 .as_mapping()
126 .and_then(|mapping| mapping.values().find_map(yaml_serde::Value::as_str))
127}
128
129fn extract_contact_parties(maintenance: Option<&yaml_serde::Value>) -> Vec<Party> {
130 maintenance
131 .and_then(|maintenance| maintenance.get("contacts"))
132 .and_then(yaml_serde::Value::as_sequence)
133 .into_iter()
134 .flatten()
135 .filter_map(|contact| {
136 let name = contact
137 .get("name")
138 .and_then(yaml_value_as_string)
139 .map(str::to_string);
140 let email = contact
141 .get("email")
142 .and_then(yaml_value_as_string)
143 .map(str::to_string);
144 let url = contact
145 .get("url")
146 .and_then(yaml_value_as_string)
147 .map(str::to_string);
148
149 if name.is_none() && email.is_none() && url.is_none() {
150 return None;
151 }
152
153 Some(Party {
154 r#type: Some("person".to_string()),
155 role: Some("maintainer".to_string()),
156 name,
157 email,
158 url,
159 organization: None,
160 organization_url: None,
161 timezone: None,
162 })
163 })
164 .collect()
165}
166
167fn yaml_value_as_string(value: &yaml_serde::Value) -> Option<&str> {
168 value.as_str()
169}
170
171crate::register_parser!(
172 "publiccode metadata",
173 &["**/publiccode.yml", "**/publiccode.yaml"],
174 "publiccode",
175 "YAML",
176 Some("https://yml.publiccode.tools/"),
177);