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::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}