Skip to main content

provenant/parsers/
publiccode.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use 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);