1use anyhow::{format_err, Context, Result};
2use std::io::Read;
3use strum::IntoEnumIterator;
4
5mod npm;
6
7#[derive(Clone, Debug)]
8pub struct JsExtension {
9 name_: String,
10 registry_host_names_: Vec<String>,
11 root_url_: url::Url,
12 registry_human_url_template_: String,
13}
14
15impl vouch_lib::extension::FromLib for JsExtension {
16 fn new() -> Self {
17 Self {
18 name_: "js".to_string(),
19 registry_host_names_: vec!["npmjs.com".to_owned()],
20 root_url_: url::Url::parse("https://www.npmjs.com").unwrap(),
21 registry_human_url_template_:
22 "https://www.npmjs.com/package/{{package_name}}/v/{{package_version}}".to_string(),
23 }
24 }
25}
26
27impl vouch_lib::extension::Extension for JsExtension {
28 fn name(&self) -> String {
29 self.name_.clone()
30 }
31
32 fn registries(&self) -> Vec<String> {
33 self.registry_host_names_.clone()
34 }
35
36 fn identify_local_dependencies(
37 &self,
38 working_directory: &std::path::PathBuf,
39 ) -> Result<Vec<vouch_lib::extension::DependenciesSpec>> {
40 let dependency_files = match identify_dependency_files(&working_directory) {
42 Some(v) => v,
43 None => return Ok(Vec::new()),
44 };
45
46 let mut all_dependency_specs = Vec::new();
48 for dependency_file in dependency_files {
49 let (dependencies, registry_host_name) = match dependency_file.r#type {
51 DependencyFileType::Npm => (
52 npm::get_dependencies(&dependency_file.path)?,
53 npm::get_registry_host_name(),
54 ),
55 };
56 all_dependency_specs.push(vouch_lib::extension::DependenciesSpec {
57 path: dependency_file.path,
58 registry_host_name: registry_host_name,
59 dependencies: dependencies.into_iter().collect(),
60 });
61 }
62
63 Ok(all_dependency_specs)
64 }
65
66 fn registries_package_metadata(
67 &self,
68 package_name: &str,
69 package_version: &Option<&str>,
70 ) -> Result<Vec<vouch_lib::extension::RegistryPackageMetadata>> {
71 let package_version = match package_version {
72 Some(v) => Some(v.to_string()),
73 None => get_latest_version(&package_name)?,
74 }
75 .ok_or(format_err!("Failed to find package version."))?;
76
77 let human_url = get_registry_human_url(&self, &package_name, &package_version)?;
79
80 let registry_host_name = self
82 .registries()
83 .first()
84 .ok_or(format_err!(
85 "Code error: vector of registry host names is empty."
86 ))?
87 .clone();
88
89 let entry_json = get_registry_entry_json(&package_name)?;
90 let artifact_url = get_archive_url(&entry_json, &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,
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 versions = json["versions"]
106 .as_object()
107 .ok_or(format_err!("Failed to find versions JSON section."))?;
108 let latest_version = versions.keys().last();
109 Ok(latest_version.cloned())
110}
111
112fn get_registry_human_url(
113 extension: &JsExtension,
114 package_name: &str,
115 package_version: &str,
116) -> Result<url::Url> {
117 let handlebars_registry = handlebars::Handlebars::new();
119 let url = handlebars_registry.render_template(
120 &extension.registry_human_url_template_,
121 &maplit::btreemap! {
122 "package_name" => package_name,
123 "package_version" => package_version,
124 },
125 )?;
126 Ok(url::Url::parse(url.as_str())?)
127}
128
129fn get_registry_entry_json(package_name: &str) -> Result<serde_json::Value> {
130 let handlebars_registry = handlebars::Handlebars::new();
131 let json_url = handlebars_registry.render_template(
132 "https://registry.npmjs.com/{{package_name}}",
133 &maplit::btreemap! {"package_name" => package_name},
134 )?;
135
136 let mut result = reqwest::blocking::get(&json_url.to_string())?;
137 let mut body = String::new();
138 result.read_to_string(&mut body)?;
139
140 Ok(serde_json::from_str(&body).context(format!("JSON was not well-formatted:\n{}", body))?)
141}
142
143fn get_archive_url(
144 registry_entry_json: &serde_json::Value,
145 package_version: &str,
146) -> Result<url::Url> {
147 Ok(url::Url::parse(
148 registry_entry_json["versions"][package_version]["dist"]["tarball"]
149 .as_str()
150 .ok_or(format_err!("Failed to parse package archive URL."))?,
151 )?)
152}
153
154#[derive(Debug, Copy, Clone, strum_macros::EnumIter)]
156enum DependencyFileType {
157 Npm,
158}
159
160impl DependencyFileType {
161 pub fn file_name(&self) -> std::path::PathBuf {
163 match self {
164 Self::Npm => std::path::PathBuf::from("package-lock.json"),
165 }
166 }
167}
168
169#[derive(Debug, Clone)]
171struct DependencyFile {
172 r#type: DependencyFileType,
173 path: std::path::PathBuf,
174}
175
176fn identify_dependency_files(
180 working_directory: &std::path::PathBuf,
181) -> Option<Vec<DependencyFile>> {
182 assert!(working_directory.is_absolute());
183 let mut working_directory = working_directory.clone();
184
185 loop {
186 let mut found_dependency_file = false;
188
189 let mut dependency_files: Vec<DependencyFile> = Vec::new();
190 for dependency_file_type in DependencyFileType::iter() {
191 let target_absolute_path = working_directory.join(dependency_file_type.file_name());
192 if target_absolute_path.is_file() {
193 found_dependency_file = true;
194 dependency_files.push(DependencyFile {
195 r#type: dependency_file_type,
196 path: target_absolute_path,
197 })
198 }
199 }
200 if found_dependency_file {
201 return Some(dependency_files);
202 }
203
204 if working_directory == std::path::PathBuf::from("/") {
206 break;
207 }
208
209 working_directory.pop();
211 }
212 None
213}