1use serde::{Deserialize, Deserializer};
9use sha2::{Digest, Sha256};
10use thiserror::Error;
11use url::Url;
12
13use crate::{
14 config::Config,
15 lua_version::LuaVersion,
16 luarocks,
17 package::{
18 PackageName, PackageReq, PackageVersion, RemotePackageType, RemotePackageTypeFilterSpec,
19 },
20};
21
22const CACHE_FORMAT: u32 = 1;
24
25#[derive(Debug, Error)]
26pub(crate) enum ManifestCacheError {
27 #[error("rock not found in the manifest cache")]
28 NotFound,
29 #[error("unsupported manifest cache format {found} (expected {expected})")]
30 UnsupportedFormat { found: u32, expected: u32 },
31 #[error("failed to request the manifest cache")]
32 Request(#[source] reqwest::Error),
33 #[error("failed to parse the manifest cache response")]
34 Parse(#[source] serde_json::Error),
35 #[error("invalid manifest cache URL")]
36 Url(#[from] url::ParseError),
37}
38
39#[derive(Debug, Clone, Deserialize)]
41pub struct Closure {
42 format: u32,
43 specs: Vec<ClosureSpec>,
44}
45
46impl Closure {
47 pub(crate) fn spec(
48 &self,
49 req: &PackageReq,
50 filter: Option<&RemotePackageTypeFilterSpec>,
51 ) -> Option<(&ClosureSpec, RemotePackageType)> {
52 let default_filter = RemotePackageTypeFilterSpec::default();
53 let filter = filter.unwrap_or(&default_filter);
54 self.specs
55 .iter()
56 .filter(|spec| {
57 spec.package() == req.name() && req.version_req().matches(spec.version())
58 })
59 .filter_map(|spec| {
60 spec.remote_package_type(filter)
61 .map(|package_type| (spec, package_type))
62 })
63 .max_by(|(spec_a, type_a), (spec_b, type_b)| {
64 spec_a
65 .version()
66 .cmp(spec_b.version())
67 .then_with(|| type_a.cmp(type_b))
68 })
69 }
70}
71
72#[derive(Debug, Clone, Deserialize)]
74pub struct ClosureSpec {
75 package: PackageName,
76 version: PackageVersion,
77 #[serde(deserialize_with = "deserialize_available_types")]
78 available_types: Vec<RemotePackageType>,
79 rockspec_raw: String,
80}
81
82impl ClosureSpec {
83 pub(crate) fn package(&self) -> &PackageName {
84 &self.package
85 }
86
87 pub(crate) fn version(&self) -> &PackageVersion {
88 &self.version
89 }
90
91 pub(crate) fn rockspec_raw(&self) -> &str {
92 &self.rockspec_raw
93 }
94
95 pub(crate) fn remote_package_type(
98 &self,
99 filter: &RemotePackageTypeFilterSpec,
100 ) -> Option<RemotePackageType> {
101 self.available_types
102 .iter()
103 .filter(|&package_type| match package_type {
104 RemotePackageType::Rockspec => filter.rockspec,
105 RemotePackageType::Src => filter.src,
106 RemotePackageType::Binary => filter.binary,
107 })
108 .cloned()
109 .max()
110 }
111}
112
113#[derive(Debug, Clone, Deserialize)]
114struct VersionsIndex {
115 format: u32,
116 versions: Vec<PackageVersion>,
117}
118
119#[derive(Debug, Clone)]
120pub struct ManifestCache {
121 base_url: Url,
122 lua_version: String,
124 client: reqwest::Client,
125}
126
127impl ManifestCache {
128 pub(crate) fn from_config(config: &Config) -> Option<Self> {
129 let base_url = config.manifest_cache_url()?.clone();
130 let lua_version = LuaVersion::from(config).ok()?.version_compatibility_str();
131 #[cfg(not(test))]
132 let client = crate::reqwest::https_client(config);
133 #[cfg(test)]
134 let client = crate::reqwest::http_client(config);
135 Some(Self {
136 base_url,
137 lua_version,
138 client: client.ok()?.clone(),
139 })
140 }
141
142 pub(crate) async fn versions(
143 &self,
144 name: &PackageName,
145 ) -> Result<Vec<PackageVersion>, ManifestCacheError> {
146 let url = self.url(&format!("versions/{name}.json"))?;
147 let index: VersionsIndex = self.get_json(&url).await?;
148 self.check_format(index.format)?;
149 Ok(index.versions)
150 }
151
152 pub(crate) async fn latest_closure(
153 &self,
154 name: &PackageName,
155 ) -> Result<Closure, ManifestCacheError> {
156 let url = self.url(&format!("latest/{name}.json"))?;
157 let closure: Closure = self.get_json(&url).await?;
158 self.check_format(closure.format)?;
159 Ok(closure)
160 }
161
162 pub(crate) async fn closure(
163 &self,
164 name: &PackageName,
165 version: &PackageVersion,
166 ) -> Result<Closure, ManifestCacheError> {
167 let hash = hex::encode(Sha256::digest(format!("{name}@{version}").as_bytes()));
168 let url = self.url(&format!("{hash}.json"))?;
169 let closure: Closure = self.get_json(&url).await?;
170 self.check_format(closure.format)?;
171 Ok(closure)
172 }
173
174 fn check_format(&self, format: u32) -> Result<(), ManifestCacheError> {
175 if format == CACHE_FORMAT {
176 Ok(())
177 } else {
178 Err(ManifestCacheError::UnsupportedFormat {
179 found: format,
180 expected: CACHE_FORMAT,
181 })
182 }
183 }
184
185 fn url(&self, path: &str) -> Result<Url, ManifestCacheError> {
186 Ok(self
187 .base_url
188 .join(&format!("{}/{}", self.lua_version, path))?)
189 }
190
191 async fn get_json<T>(&self, url: &Url) -> Result<T, ManifestCacheError>
192 where
193 T: serde::de::DeserializeOwned,
194 {
195 let response = self
196 .client
197 .get(url.clone())
198 .send()
199 .await
200 .map_err(ManifestCacheError::Request)?;
201
202 if response.status() == reqwest::StatusCode::NOT_FOUND {
203 return Err(ManifestCacheError::NotFound);
204 }
205
206 let response = response
207 .error_for_status()
208 .map_err(ManifestCacheError::Request)?;
209
210 let text = response.text().await.map_err(ManifestCacheError::Request)?;
211 serde_json::from_str(&text).map_err(ManifestCacheError::Parse)
212 }
213}
214
215fn deserialize_available_types<'de, D>(deserializer: D) -> Result<Vec<RemotePackageType>, D::Error>
216where
217 D: Deserializer<'de>,
218{
219 use RemotePackageType::{Binary, Rockspec, Src};
220
221 let arches = Vec::<String>::deserialize(deserializer)?;
222
223 Ok(arches
224 .into_iter()
225 .filter_map(|arch| match arch.as_str() {
226 "rockspec" => Some(Rockspec),
227 "src" => Some(Src),
228 "all" => Some(Binary),
229 arch if arch == luarocks::current_platform_luarocks_identifier().as_str() => {
230 Some(Binary)
231 }
232 _ => None,
233 })
234 .collect())
235}
236
237#[cfg(test)]
238mod tests {
239 use std::str::FromStr;
240
241 use super::*;
242
243 fn closure_from_json(json: &str) -> Closure {
244 serde_json::from_str(json).unwrap()
245 }
246
247 #[test]
248 fn selects_latest_matching_version() {
249 let closure = closure_from_json(
250 r#"{
251 "format": 1,
252 "specs": [
253 {"package":"say","version":"1.0.0-1","available_types":["rockspec"],"rockspec_raw":""},
254 {"package":"say","version":"2.0.0-1","available_types":["rockspec"],"rockspec_raw":""}
255 ]
256 }"#,
257 );
258 let req = PackageReq::from_str("say").unwrap();
259 let (spec, _) = closure.spec(&req, None).unwrap();
260 assert_eq!(
261 spec.version(),
262 &PackageVersion::from_str("2.0.0-1").unwrap()
263 );
264 }
265
266 #[test]
267 fn prefers_binary_over_rockspec_over_src() {
268 let req = PackageReq::from_str("say").unwrap();
269
270 let rockspec_or_src = closure_from_json(
271 r#"{"format":1,"specs":[{"package":"say","version":"1.0.0-1","available_types":["rockspec","src"],"rockspec_raw":""}]}"#,
272 );
273 assert_eq!(
274 rockspec_or_src.spec(&req, None).unwrap().1,
275 RemotePackageType::Rockspec
276 );
277
278 let binary = closure_from_json(
279 r#"{"format":1,"specs":[{"package":"say","version":"1.0.0-1","available_types":["all","rockspec"],"rockspec_raw":""}]}"#,
280 );
281 assert_eq!(
282 binary.spec(&req, None).unwrap().1,
283 RemotePackageType::Binary
284 );
285 }
286
287 #[test]
288 fn respects_type_filter() {
289 let closure = closure_from_json(
290 r#"{"format":1,"specs":[{"package":"say","version":"1.0.0-1","available_types":["all","rockspec"],"rockspec_raw":""}]}"#,
291 );
292 let req = PackageReq::from_str("say").unwrap();
293 let filter: RemotePackageTypeFilterSpec =
294 serde_json::from_str(r#"{"rockspec":true,"src":false,"binary":false}"#).unwrap();
295 assert_eq!(
296 closure.spec(&req, Some(&filter)).unwrap().1,
297 RemotePackageType::Rockspec
298 );
299 }
300
301 #[test]
302 fn version_constraint_filters_specs() {
303 let closure = closure_from_json(
304 r#"{"format":1,"specs":[{"package":"say","version":"1.0.0-1","available_types":["rockspec"],"rockspec_raw":""},{"package":"say","version":"2.0.0-1","available_types":["rockspec"],"rockspec_raw":""}]}"#,
305 );
306 let req = PackageReq::from_str("say < 2.0.0").unwrap();
307 let (spec, _) = closure.spec(&req, None).unwrap();
308 assert_eq!(
309 spec.version(),
310 &PackageVersion::from_str("1.0.0-1").unwrap()
311 );
312 }
313}