1use std::str::FromStr;
2
3use mailparse::MailHeaderMap;
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7use crate::Error;
8
9#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct Metadata {
13 pub metadata_version: String,
15 pub name: String,
17 pub version: String,
19 #[cfg_attr(feature = "serde", serde(default))]
22 pub platforms: Vec<String>,
23 #[cfg_attr(feature = "serde", serde(default))]
26 pub supported_platforms: Vec<String>,
27 #[cfg_attr(feature = "serde", serde(default))]
29 pub summary: Option<String>,
30 #[cfg_attr(feature = "serde", serde(default))]
32 pub description: Option<String>,
33 #[cfg_attr(feature = "serde", serde(default))]
36 pub keywords: Option<String>,
37 #[cfg_attr(feature = "serde", serde(default))]
39 pub home_page: Option<String>,
40 #[cfg_attr(feature = "serde", serde(default))]
42 pub download_url: Option<String>,
43 #[cfg_attr(feature = "serde", serde(default))]
45 pub author: Option<String>,
46 #[cfg_attr(feature = "serde", serde(default))]
48 pub author_email: Option<String>,
49 #[cfg_attr(feature = "serde", serde(default))]
51 pub license: Option<String>,
52 #[cfg_attr(feature = "serde", serde(default))]
54 pub license_expression: Option<String>,
55 #[cfg_attr(feature = "serde", serde(default))]
57 pub license_files: Vec<String>,
58 #[cfg_attr(feature = "serde", serde(default))]
60 pub classifiers: Vec<String>,
61 #[cfg_attr(feature = "serde", serde(default))]
63 pub requires_dist: Vec<String>,
64 #[cfg_attr(feature = "serde", serde(default))]
66 pub provides_dist: Vec<String>,
67 #[cfg_attr(feature = "serde", serde(default))]
70 pub obsoletes_dist: Vec<String>,
71 #[cfg_attr(feature = "serde", serde(default))]
76 pub maintainer: Option<String>,
77 #[cfg_attr(feature = "serde", serde(default))]
83 pub maintainer_email: Option<String>,
84 #[cfg_attr(feature = "serde", serde(default))]
86 pub requires_python: Option<String>,
87 #[cfg_attr(feature = "serde", serde(default))]
89 pub requires_external: Vec<String>,
90 #[cfg_attr(feature = "serde", serde(default))]
92 pub project_urls: Vec<String>,
93 #[cfg_attr(feature = "serde", serde(default))]
96 pub provides_extras: Vec<String>,
97 #[cfg_attr(feature = "serde", serde(default))]
100 pub description_content_type: Option<String>,
101 #[cfg_attr(feature = "serde", serde(default))]
103 pub dynamic: Vec<String>,
104}
105
106impl Metadata {
107 pub fn parse(content: &[u8]) -> Result<Self, Error> {
109 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}