1use anyhow::{format_err, Context, Result};
2use std::io::Read;
3use strum::IntoEnumIterator;
4
5mod pipfile;
6
7#[derive(Clone, Debug)]
8pub struct PyExtension {
9 name_: String,
10 registry_host_names_: Vec<String>,
11 root_url_: url::Url,
12 package_url_template_: String,
13 registry_human_url_template_: String,
14}
15
16impl vouch_lib::extension::FromLib for PyExtension {
17 fn new() -> Self {
18 Self {
19 name_: "py".to_string(),
20 registry_host_names_: vec!["pypi.org".to_owned()],
21 root_url_: url::Url::parse("https://pypi.org/pypi").unwrap(),
22 package_url_template_: "https://pypi.org/pypi/{{package_name}}/".to_string(),
23 registry_human_url_template_:
24 "https://pypi.org/pypi/{{package_name}}/{{package_version}}/".to_string(),
25 }
26 }
27}
28
29impl vouch_lib::extension::Extension for PyExtension {
30 fn name(&self) -> String {
31 self.name_.clone()
32 }
33
34 fn registries(&self) -> Vec<String> {
35 self.registry_host_names_.clone()
36 }
37
38 fn identify_local_dependencies(
39 &self,
40 working_directory: &std::path::PathBuf,
41 ) -> Result<Vec<vouch_lib::extension::DependenciesSpec>> {
42 let dependency_files = match identify_dependency_files(&working_directory) {
44 Some(v) => v,
45 None => return Ok(Vec::new()),
46 };
47
48 let mut all_dependency_specs = Vec::new();
50 for dependency_file in dependency_files {
51 let (dependencies, registry_host_name) = match dependency_file.r#type {
53 DependencyFileType::PipfileLock => (
54 pipfile::get_dependencies(&dependency_file.path)?,
55 pipfile::get_registry_host_name(),
56 ),
57 };
58 all_dependency_specs.push(vouch_lib::extension::DependenciesSpec {
59 path: dependency_file.path,
60 registry_host_name: registry_host_name,
61 dependencies: dependencies.into_iter().collect(),
62 });
63 }
64
65 Ok(all_dependency_specs)
66 }
67
68 fn registries_package_metadata(
69 &self,
70 package_name: &str,
71 package_version: &Option<&str>,
72 ) -> Result<Vec<vouch_lib::extension::RegistryPackageMetadata>> {
73 let package_version = match package_version {
74 Some(v) => Some(v.to_string()),
75 None => get_latest_version(&package_name)?,
76 }
77 .ok_or(format_err!("Failed to find package version."))?;
78
79 let registry_host_name = self
81 .registries()
82 .first()
83 .ok_or(format_err!(
84 "Code error: vector of registry host names is empty."
85 ))?
86 .clone();
87
88 let entry_json = get_registry_entry_json(&package_name)?;
89 let artifact_url = get_archive_url(&entry_json, &package_version)?;
90 let human_url = get_registry_human_url(&self, &package_name, &package_version)?;
91
92 Ok(vec![vouch_lib::extension::RegistryPackageMetadata {
93 registry_host_name: registry_host_name,
94 human_url: human_url.to_string(),
95 artifact_url: artifact_url.to_string(),
96 is_primary: true,
97 package_version: package_version.to_string(),
98 }])
99 }
100}
101
102fn get_latest_version(package_name: &str) -> Result<Option<String>> {
104 let json = get_registry_entry_json(&package_name)?;
105 let releases = json["releases"]
106 .as_object()
107 .ok_or(format_err!("Failed to find releases JSON section."))?;
108 let mut versions: Vec<semver::Version> = releases
109 .keys()
110 .filter(|v| v.chars().all(|c| c.is_numeric() || c == '.'))
111 .map(|v| semver::Version::parse(v))
112 .filter(|v| v.is_ok())
113 .map(|v| v.unwrap())
114 .collect();
115 versions.sort();
116
117 let latest_version = versions.last().map(|v| v.to_string());
118 Ok(latest_version)
119}
120
121fn get_registry_human_url(
122 extension: &PyExtension,
123 package_name: &str,
124 package_version: &str,
125) -> Result<url::Url> {
126 let handlebars_registry = handlebars::Handlebars::new();
128 let human_url = handlebars_registry.render_template(
129 &extension.registry_human_url_template_,
130 &maplit::btreemap! {
131 "package_name" => package_name,
132 "package_version" => package_version,
133 },
134 )?;
135 Ok(url::Url::parse(human_url.as_str())?)
136}
137
138fn get_registry_entry_json(package_name: &str) -> Result<serde_json::Value> {
139 let handlebars_registry = handlebars::Handlebars::new();
140 let url = handlebars_registry.render_template(
141 "https://pypi.org/pypi/{{package_name}}/json",
142 &maplit::btreemap! {
143 "package_name" => package_name,
144 },
145 )?;
146 let mut result = reqwest::blocking::get(&url.to_string())?;
147 let mut body = String::new();
148 result.read_to_string(&mut body)?;
149
150 Ok(serde_json::from_str(&body).context(format!("JSON was not well-formatted:\n{}", body))?)
151}
152
153fn get_archive_url(
154 registry_entry_json: &serde_json::Value,
155 package_version: &str,
156) -> Result<url::Url> {
157 let releases = registry_entry_json["releases"][package_version]
158 .as_array()
159 .ok_or(format_err!("Failed to parse releases array."))?;
160 for release in releases {
161 let python_version = release["python_version"]
162 .as_str()
163 .ok_or(format_err!("Failed to parse package version."))?;
164 if python_version == "source" {
165 return Ok(url::Url::parse(
166 release["url"]
167 .as_str()
168 .ok_or(format_err!("Failed to parse package archive URL."))?,
169 )?);
170 }
171 }
172 Err(format_err!("Failed to identify package archive URL."))
173}
174
175#[derive(Debug, Copy, Clone, strum_macros::EnumIter)]
177enum DependencyFileType {
178 PipfileLock,
179}
180
181impl DependencyFileType {
182 pub fn file_name(&self) -> std::path::PathBuf {
184 match self {
185 Self::PipfileLock => std::path::PathBuf::from("Pipfile.lock"),
186 }
187 }
188}
189
190#[derive(Debug, Clone)]
192struct DependencyFile {
193 r#type: DependencyFileType,
194 path: std::path::PathBuf,
195}
196
197fn identify_dependency_files(
201 working_directory: &std::path::PathBuf,
202) -> Option<Vec<DependencyFile>> {
203 assert!(working_directory.is_absolute());
204 let mut working_directory = working_directory.clone();
205
206 loop {
207 let mut found_dependency_file = false;
209
210 let mut dependency_files: Vec<DependencyFile> = Vec::new();
211 for dependency_file_type in DependencyFileType::iter() {
212 let target_absolute_path = working_directory.join(dependency_file_type.file_name());
213 if target_absolute_path.is_file() {
214 found_dependency_file = true;
215 dependency_files.push(DependencyFile {
216 r#type: dependency_file_type,
217 path: target_absolute_path,
218 })
219 }
220 }
221 if found_dependency_file {
222 return Some(dependency_files);
223 }
224
225 if working_directory == std::path::PathBuf::from("/") {
227 break;
228 }
229
230 working_directory.pop();
232 }
233 None
234}