provenant/parsers/
publiccode.rs1use std::path::Path;
2
3use crate::models::{DatasourceId, PackageData, PackageType, Party};
4use crate::parser_warn as warn;
5
6use super::PackageParser;
7use super::license_normalization::normalize_spdx_declared_license;
8use super::utils::{MAX_ITERATION_COUNT, read_file_to_string, truncate_field};
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 read_file_to_string(path, None) {
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(|s| truncate_field(s.to_string()));
71 package.version = yaml
72 .get("softwareVersion")
73 .and_then(yaml_value_as_string)
74 .map(|s| truncate_field(s.to_string()));
75 package.vcs_url = yaml
76 .get("url")
77 .and_then(yaml_value_as_string)
78 .map(|s| truncate_field(s.to_string()));
79 package.homepage_url = yaml
80 .get("landingURL")
81 .and_then(yaml_value_as_string)
82 .map(|s| truncate_field(s.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(|s| truncate_field(s.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(|s| truncate_field(s.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 = truncate_field(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 .take(MAX_ITERATION_COUNT)
136 .filter_map(|contact| {
137 let name = contact
138 .get("name")
139 .and_then(yaml_value_as_string)
140 .map(|s| truncate_field(s.to_string()));
141 let email = contact
142 .get("email")
143 .and_then(yaml_value_as_string)
144 .map(|s| truncate_field(s.to_string()));
145 let url = contact
146 .get("url")
147 .and_then(yaml_value_as_string)
148 .map(|s| truncate_field(s.to_string()));
149
150 if name.is_none() && email.is_none() && url.is_none() {
151 return None;
152 }
153
154 Some(Party {
155 r#type: Some("person".to_string()),
156 role: Some("maintainer".to_string()),
157 name,
158 email,
159 url,
160 organization: None,
161 organization_url: None,
162 timezone: None,
163 })
164 })
165 .collect()
166}
167
168fn yaml_value_as_string(value: &yaml_serde::Value) -> Option<&str> {
169 value.as_str()
170}
171
172crate::register_parser!(
173 "publiccode metadata",
174 &["**/publiccode.yml", "**/publiccode.yaml"],
175 "publiccode",
176 "YAML",
177 Some("https://yml.publiccode.tools/"),
178);