Skip to main content

provenant/parsers/
publiccode.rs

1use 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);