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