python_pkginfo/
metadata.rs

1use std::str::FromStr;
2
3use mailparse::MailHeaderMap;
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7use crate::Error;
8
9/// Python package metadata
10#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct Metadata {
13    /// Version of the file format; legal values are `1.0`, `1.1`, `1.2`, `2.1` and `2.2`.
14    pub metadata_version: String,
15    /// The name of the distribution.
16    pub name: String,
17    /// A string containing the distribution’s version number.
18    pub version: String,
19    /// A Platform specification describing an operating system supported by the distribution
20    /// which is not listed in the “Operating System” Trove classifiers.
21    #[cfg_attr(feature = "serde", serde(default))]
22    pub platforms: Vec<String>,
23    /// Binary distributions containing a PKG-INFO file will use the Supported-Platform field
24    /// in their metadata to specify the OS and CPU for which the binary distribution was compiled.
25    #[cfg_attr(feature = "serde", serde(default))]
26    pub supported_platforms: Vec<String>,
27    /// A one-line summary of what the distribution does.
28    #[cfg_attr(feature = "serde", serde(default))]
29    pub summary: Option<String>,
30    /// A longer description of the distribution that can run to several paragraphs.
31    #[cfg_attr(feature = "serde", serde(default))]
32    pub description: Option<String>,
33    /// A list of additional keywords, separated by commas, to be used to
34    /// assist searching for the distribution in a larger catalog.
35    #[cfg_attr(feature = "serde", serde(default))]
36    pub keywords: Option<String>,
37    /// A string containing the URL for the distribution’s home page.
38    #[cfg_attr(feature = "serde", serde(default))]
39    pub home_page: Option<String>,
40    /// A string containing the URL from which this version of the distribution can be downloaded.
41    #[cfg_attr(feature = "serde", serde(default))]
42    pub download_url: Option<String>,
43    /// A string containing the author’s name at a minimum; additional contact information may be provided.
44    #[cfg_attr(feature = "serde", serde(default))]
45    pub author: Option<String>,
46    /// A string containing the author’s e-mail address. It can contain a name and e-mail address in the legal forms for a RFC-822 `From:` header.
47    #[cfg_attr(feature = "serde", serde(default))]
48    pub author_email: Option<String>,
49    /// Text indicating the license covering the distribution where the license is not a selection from the `License` Trove classifiers or an SPDX license expression.
50    #[cfg_attr(feature = "serde", serde(default))]
51    pub license: Option<String>,
52    /// An SPDX expression indicating the license covering the distribution.
53    #[cfg_attr(feature = "serde", serde(default))]
54    pub license_expression: Option<String>,
55    /// Paths to files containing the text of the licenses covering the distribution.
56    #[cfg_attr(feature = "serde", serde(default))]
57    pub license_files: Vec<String>,
58    /// Each entry is a string giving a single classification value for the distribution.
59    #[cfg_attr(feature = "serde", serde(default))]
60    pub classifiers: Vec<String>,
61    /// Each entry contains a string naming some other distutils project required by this distribution.
62    #[cfg_attr(feature = "serde", serde(default))]
63    pub requires_dist: Vec<String>,
64    /// Each entry contains a string naming a Distutils project which is contained within this distribution.
65    #[cfg_attr(feature = "serde", serde(default))]
66    pub provides_dist: Vec<String>,
67    /// Each entry contains a string describing a distutils project’s distribution which this distribution renders obsolete,
68    /// meaning that the two projects should not be installed at the same time.
69    #[cfg_attr(feature = "serde", serde(default))]
70    pub obsoletes_dist: Vec<String>,
71    /// A string containing the maintainer’s name at a minimum; additional contact information may be provided.
72    ///
73    /// Note that this field is intended for use when a project is being maintained by someone other than the original author:
74    /// it should be omitted if it is identical to `author`.
75    #[cfg_attr(feature = "serde", serde(default))]
76    pub maintainer: Option<String>,
77    /// A string containing the maintainer’s e-mail address.
78    /// It can contain a name and e-mail address in the legal forms for a RFC-822 `From:` header.
79    ///
80    /// Note that this field is intended for use when a project is being maintained by someone other than the original author:
81    /// it should be omitted if it is identical to `author_email`.
82    #[cfg_attr(feature = "serde", serde(default))]
83    pub maintainer_email: Option<String>,
84    /// This field specifies the Python version(s) that the distribution is guaranteed to be compatible with.
85    #[cfg_attr(feature = "serde", serde(default))]
86    pub requires_python: Option<String>,
87    /// Each entry contains a string describing some dependency in the system that the distribution is to be used.
88    #[cfg_attr(feature = "serde", serde(default))]
89    pub requires_external: Vec<String>,
90    /// A string containing a browsable URL for the project and a label for it, separated by a comma.
91    #[cfg_attr(feature = "serde", serde(default))]
92    pub project_urls: Vec<String>,
93    /// A string containing the name of an optional feature. Must be a valid Python identifier.
94    /// May be used to make a dependency conditional on whether the optional feature has been requested.
95    #[cfg_attr(feature = "serde", serde(default))]
96    pub provides_extras: Vec<String>,
97    /// A string stating the markup syntax (if any) used in the distribution’s description,
98    /// so that tools can intelligently render the description.
99    #[cfg_attr(feature = "serde", serde(default))]
100    pub description_content_type: Option<String>,
101    /// A string containing the name of another core metadata field.
102    #[cfg_attr(feature = "serde", serde(default))]
103    pub dynamic: Vec<String>,
104}
105
106impl Metadata {
107    /// Parse distribution metadata from metadata bytes
108    pub fn parse(content: &[u8]) -> Result<Self, Error> {
109        // HACK: trick mailparse to parse as UTF-8 instead of ASCII
110        let mut mail = b"Content-Type: text/plain; charset=utf-8\n".to_vec();
111        mail.extend_from_slice(content);
112
113        let msg = mailparse::parse_mail(&mail)?;
114        let headers = msg.get_headers();
115        let get_first_value = |name| {
116            headers.get_first_header(name).and_then(|header| {
117                match rfc2047_decoder::decode(header.get_value_raw()) {
118                    Ok(value) => {
119                        if value == "UNKNOWN" {
120                            None
121                        } else {
122                            Some(value)
123                        }
124                    }
125                    Err(_) => None,
126                }
127            })
128        };
129        let get_all_values = |name| {
130            let values: Vec<String> = headers
131                .get_all_values(name)
132                .into_iter()
133                .filter(|value| value != "UNKNOWN")
134                .collect();
135            values
136        };
137        let metadata_version = headers
138            .get_first_value("Metadata-Version")
139            .ok_or(Error::FieldNotFound("Metadata-Version"))?;
140        let name = headers
141            .get_first_value("Name")
142            .ok_or(Error::FieldNotFound("Name"))?;
143        let version = headers
144            .get_first_value("Version")
145            .ok_or(Error::FieldNotFound("Version"))?;
146        let platforms = get_all_values("Platform");
147        let supported_platforms = get_all_values("Supported-Platform");
148        let summary = get_first_value("Summary");
149        let body = msg.get_body()?;
150        let description = if !body.trim().is_empty() {
151            Some(body)
152        } else {
153            get_first_value("Description")
154        };
155        let keywords = get_first_value("Keywords");
156        let home_page = get_first_value("Home-Page");
157        let download_url = get_first_value("Download-URL");
158        let author = get_first_value("Author");
159        let author_email = get_first_value("Author-email");
160        let license = get_first_value("License");
161        let license_expression = get_first_value("License-Expression");
162        let license_files = get_all_values("License-File");
163        let classifiers = get_all_values("Classifier");
164        let requires_dist = get_all_values("Requires-Dist");
165        let provides_dist = get_all_values("Provides-Dist");
166        let obsoletes_dist = get_all_values("Obsoletes-Dist");
167        let maintainer = get_first_value("Maintainer");
168        let maintainer_email = get_first_value("Maintainer-email");
169        let requires_python = get_first_value("Requires-Python");
170        let requires_external = get_all_values("Requires-External");
171        let project_urls = get_all_values("Project-URL");
172        let provides_extras = get_all_values("Provides-Extra");
173        let description_content_type = get_first_value("Description-Content-Type");
174        let dynamic = get_all_values("Dynamic");
175        Ok(Metadata {
176            metadata_version,
177            name,
178            version,
179            platforms,
180            supported_platforms,
181            summary,
182            description,
183            keywords,
184            home_page,
185            download_url,
186            author,
187            author_email,
188            license,
189            license_expression,
190            license_files,
191            classifiers,
192            requires_dist,
193            provides_dist,
194            obsoletes_dist,
195            maintainer,
196            maintainer_email,
197            requires_python,
198            requires_external,
199            project_urls,
200            provides_extras,
201            description_content_type,
202            dynamic,
203        })
204    }
205}
206
207impl FromStr for Metadata {
208    type Err = Error;
209
210    fn from_str(s: &str) -> Result<Self, Self::Err> {
211        Metadata::parse(s.as_bytes())
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::Metadata;
218    use crate::Error;
219
220    #[test]
221    fn test_parse_from_str() {
222        let s = "Metadata-Version: 1.0";
223        let meta: Result<Metadata, Error> = s.parse();
224        assert!(matches!(meta, Err(Error::FieldNotFound("Name"))));
225
226        let s = "Metadata-Version: 1.0\nName: asdf";
227        let meta = Metadata::parse(s.as_bytes());
228        assert!(matches!(meta, Err(Error::FieldNotFound("Version"))));
229
230        let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0";
231        let meta = Metadata::parse(s.as_bytes()).unwrap();
232        assert_eq!(meta.metadata_version, "1.0");
233        assert_eq!(meta.name, "asdf");
234        assert_eq!(meta.version, "1.0");
235
236        let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nDescription: a Python package";
237        let meta: Metadata = s.parse().unwrap();
238        assert_eq!(meta.description.as_deref(), Some("a Python package"));
239
240        let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\n\na Python package";
241        let meta: Metadata = s.parse().unwrap();
242        assert_eq!(meta.description.as_deref(), Some("a Python package"));
243
244        let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nAuthor: 中文\n\n一个 Python 包";
245        let meta: Metadata = s.parse().unwrap();
246        assert_eq!(meta.author.as_deref(), Some("中文"));
247        assert_eq!(meta.description.as_deref(), Some("一个 Python 包"));
248    }
249
250    #[cfg(feature = "serde")]
251    #[test]
252    fn test_serde_deserialize() {
253        let input = r#"{"metadata_version": "2.3", "name": "example", "version": "1.0.0"}"#;
254        let _metadata: Metadata = serde_json::from_str(input).unwrap();
255    }
256}