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