1use std::fmt::Display;
4use std::fmt::Write;
5use std::str;
6use std::str::FromStr;
7
8use crate::MetadataError;
9use crate::metadata::Headers;
10
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct Metadata23 {
15 pub metadata_version: String,
18 pub name: String,
20 pub version: String,
22 pub platforms: Vec<String>,
25 pub supported_platforms: Vec<String>,
28 pub summary: Option<String>,
30 pub description: Option<String>,
32 pub description_content_type: Option<String>,
37 pub keywords: Option<String>,
40 pub home_page: Option<String>,
44 pub download_url: Option<String>,
48 pub author: Option<String>,
51 pub author_email: Option<String>,
54 pub maintainer: Option<String>,
61 pub maintainer_email: Option<String>,
67 pub license: Option<String>,
70 pub license_expression: Option<String>,
74 pub license_files: Vec<String>,
78 pub classifiers: Vec<String>,
80 pub requires_dist: Vec<String>,
83 pub provides_dist: Vec<String>,
86 pub obsoletes_dist: Vec<String>,
90 pub requires_python: Option<String>,
93 pub requires_external: Vec<String>,
96 pub project_urls: Vec<String>,
99 pub provides_extra: Vec<String>,
103 pub dynamic: Vec<String>,
105}
106
107impl Metadata23 {
108 pub fn parse(content: &[u8]) -> Result<Self, MetadataError> {
110 let headers = Headers::parse(content)?;
111
112 let metadata_version = headers
113 .get_first_value("Metadata-Version")
114 .ok_or(MetadataError::FieldNotFound("Metadata-Version"))?;
115 let name = headers
116 .get_first_value("Name")
117 .ok_or(MetadataError::FieldNotFound("Name"))?;
118 let version = headers
119 .get_first_value("Version")
120 .ok_or(MetadataError::FieldNotFound("Version"))?;
121 let platforms = headers.get_all_values("Platform").collect();
122 let supported_platforms = headers.get_all_values("Supported-Platform").collect();
123 let summary = headers.get_first_value("Summary");
124 let body = str::from_utf8(&content[headers.body_start..])
125 .map_err(MetadataError::DescriptionEncoding)?;
126 let description = if body.trim().is_empty() {
127 headers.get_first_value("Description")
128 } else {
129 Some(body.to_string())
130 };
131 let keywords = headers.get_first_value("Keywords");
132 let home_page = headers.get_first_value("Home-Page");
133 let download_url = headers.get_first_value("Download-URL");
134 let author = headers.get_first_value("Author");
135 let author_email = headers.get_first_value("Author-email");
136 let license = headers.get_first_value("License");
137 let license_expression = headers.get_first_value("License-Expression");
138 let license_files = headers.get_all_values("License-File").collect();
139 let classifiers = headers.get_all_values("Classifier").collect();
140 let requires_dist = headers.get_all_values("Requires-Dist").collect();
141 let provides_dist = headers.get_all_values("Provides-Dist").collect();
142 let obsoletes_dist = headers.get_all_values("Obsoletes-Dist").collect();
143 let maintainer = headers.get_first_value("Maintainer");
144 let maintainer_email = headers.get_first_value("Maintainer-email");
145 let requires_python = headers.get_first_value("Requires-Python");
146 let requires_external = headers.get_all_values("Requires-External").collect();
147 let project_urls = headers.get_all_values("Project-URL").collect();
148 let provides_extra = headers.get_all_values("Provides-Extra").collect();
149 let description_content_type = headers.get_first_value("Description-Content-Type");
150 let dynamic = headers.get_all_values("Dynamic").collect();
151 Ok(Self {
152 metadata_version,
153 name,
154 version,
155 platforms,
156 supported_platforms,
157 summary,
158 description,
159 description_content_type,
160 keywords,
161 home_page,
162 download_url,
163 author,
164 author_email,
165 maintainer,
166 maintainer_email,
167 license,
168 license_expression,
169 license_files,
170 classifiers,
171 requires_dist,
172 provides_dist,
173 obsoletes_dist,
174 requires_python,
175 requires_external,
176 project_urls,
177 provides_extra,
178 dynamic,
179 })
180 }
181
182 pub fn core_metadata_format(&self) -> String {
201 fn write_str(writer: &mut String, key: &str, value: impl Display) {
202 let value = value.to_string();
203 let mut lines = value.lines();
204 if let Some(line) = lines.next() {
205 let _ = writeln!(writer, "{key}: {line}");
206 } else {
207 let _ = writeln!(writer, "{key}: ");
209 }
210 for line in lines {
211 let _ = writeln!(writer, "{}{}", " ".repeat(key.len() + 2), line);
214 }
215 }
216 fn write_opt_str(writer: &mut String, key: &str, value: Option<&impl Display>) {
217 if let Some(value) = value {
218 write_str(writer, key, value);
219 }
220 }
221 fn write_all(
222 writer: &mut String,
223 key: &str,
224 values: impl IntoIterator<Item = impl Display>,
225 ) {
226 for value in values {
227 write_str(writer, key, value);
228 }
229 }
230
231 let mut writer = String::new();
232 write_str(&mut writer, "Metadata-Version", &self.metadata_version);
233 write_str(&mut writer, "Name", &self.name);
234 write_str(&mut writer, "Version", &self.version);
235 write_all(&mut writer, "Platform", &self.platforms);
236 write_all(&mut writer, "Supported-Platform", &self.supported_platforms);
237 write_all(&mut writer, "Summary", &self.summary);
238 write_opt_str(&mut writer, "Keywords", self.keywords.as_ref());
239 write_opt_str(&mut writer, "Home-Page", self.home_page.as_ref());
240 write_opt_str(&mut writer, "Download-URL", self.download_url.as_ref());
241 write_opt_str(&mut writer, "Author", self.author.as_ref());
242 write_opt_str(&mut writer, "Author-email", self.author_email.as_ref());
243 write_opt_str(&mut writer, "License", self.license.as_ref());
244 write_opt_str(
245 &mut writer,
246 "License-Expression",
247 self.license_expression.as_ref(),
248 );
249 write_all(&mut writer, "License-File", &self.license_files);
250 write_all(&mut writer, "Classifier", &self.classifiers);
251 write_all(&mut writer, "Requires-Dist", &self.requires_dist);
252 write_all(&mut writer, "Provides-Dist", &self.provides_dist);
253 write_all(&mut writer, "Obsoletes-Dist", &self.obsoletes_dist);
254 write_opt_str(&mut writer, "Maintainer", self.maintainer.as_ref());
255 write_opt_str(
256 &mut writer,
257 "Maintainer-email",
258 self.maintainer_email.as_ref(),
259 );
260 write_opt_str(
261 &mut writer,
262 "Requires-Python",
263 self.requires_python.as_ref(),
264 );
265 write_all(&mut writer, "Requires-External", &self.requires_external);
266 write_all(&mut writer, "Project-URL", &self.project_urls);
267 write_all(&mut writer, "Provides-Extra", &self.provides_extra);
268 write_opt_str(
269 &mut writer,
270 "Description-Content-Type",
271 self.description_content_type.as_ref(),
272 );
273 write_all(&mut writer, "Dynamic", &self.dynamic);
274
275 if let Some(description) = &self.description {
276 writer.push('\n');
277 writer.push_str(description);
278 }
279 writer
280 }
281}
282
283impl FromStr for Metadata23 {
284 type Err = MetadataError;
285
286 fn from_str(s: &str) -> Result<Self, Self::Err> {
287 Self::parse(s.as_bytes())
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use crate::MetadataError;
295
296 #[test]
297 fn test_parse_from_str() {
298 let s = "Metadata-Version: 1.0";
299 let meta: Result<Metadata23, MetadataError> = s.parse();
300 assert!(matches!(meta, Err(MetadataError::FieldNotFound("Name"))));
301
302 let s = "Metadata-Version: 1.0\nName: asdf";
303 let meta = Metadata23::parse(s.as_bytes());
304 assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version"))));
305
306 let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0";
307 let meta = Metadata23::parse(s.as_bytes()).unwrap();
308 assert_eq!(meta.metadata_version, "1.0");
309 assert_eq!(meta.name, "asdf");
310 assert_eq!(meta.version, "1.0");
311
312 let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nDescription: a Python package";
313 let meta: Metadata23 = s.parse().unwrap();
314 assert_eq!(meta.description.as_deref(), Some("a Python package"));
315
316 let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\n\na Python package";
317 let meta: Metadata23 = s.parse().unwrap();
318 assert_eq!(meta.description.as_deref(), Some("a Python package"));
319
320 let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nAuthor: 中文\n\n一个 Python 包";
321 let meta: Metadata23 = s.parse().unwrap();
322 assert_eq!(meta.author.as_deref(), Some("中文"));
323 assert_eq!(meta.description.as_deref(), Some("一个 Python 包"));
324 }
325}