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