1use itertools::Itertools;
2use mlua::{Lua, LuaSerdeExt};
3use reqwest::{header::ToStrError, Client};
4use std::path::{Path, PathBuf};
5use std::time::SystemTime;
6use std::{cmp::Ordering, collections::HashMap};
7use thiserror::Error;
8use tokio::fs::{File, OpenOptions};
9use tokio::io::{AsyncReadExt, AsyncSeekExt};
10use tokio::{fs, io};
11use url::Url;
12use zip::ZipArchive;
13
14use crate::config::LuaVersionUnset;
15use crate::package::{RemotePackageType, RemotePackageTypeFilterSpec};
16use crate::progress::{Progress, ProgressBar};
17use crate::{
18 config::{Config, LuaVersion},
19 package::{PackageName, PackageReq, PackageSpec, PackageVersion, RemotePackage},
20 remote_package_source::RemotePackageSource,
21};
22
23#[derive(Error, Debug)]
24pub enum ManifestFromServerError {
25 #[error(transparent)]
26 Io(#[from] io::Error),
27 #[error("failed to pull manifest: {0}")]
28 Request(#[from] reqwest::Error),
29 #[error("invalidate date received from server: {0}")]
30 InvalidDate(#[from] httpdate::Error),
31 #[error("non-ASCII characters returned in response header: {0}")]
32 InvalidHeader(#[from] ToStrError),
33 #[error("error parsing manifest URL: {0}")]
34 Url(#[from] url::ParseError),
35 #[error("failed to read manifest archive {0}:\n{1}")]
36 ZipRead(Url, zip::result::ZipError),
37 #[error("failed to unzip manifest file {0}:\n{1}")]
38 ZipExtract(Url, zip::result::ZipError),
39 #[error(transparent)]
40 LuaVersion(#[from] LuaVersionUnset),
41}
42
43async fn get_manifest(
44 url: Url,
45 manifest_version: String,
46 target: &Path,
47 client: &Client,
48) -> Result<String, ManifestFromServerError> {
49 let manifest_bytes = client
50 .get(url.clone())
51 .send()
52 .await?
53 .error_for_status()?
54 .bytes()
55 .await?;
56 let mut archive = ZipArchive::new(std::io::Cursor::new(manifest_bytes))
57 .map_err(|err| ManifestFromServerError::ZipRead(url.clone(), err))?;
58
59 let temp = tempdir::TempDir::new("lux-manifest")?;
60
61 archive
62 .extract_unwrapped_root_dir(&temp, zip::read::root_dir_common_filter)
63 .map_err(|err| ManifestFromServerError::ZipExtract(url.clone(), err))?;
64
65 let mut extracted_manifest =
66 File::open(temp.path().join(format!("manifest-{}", manifest_version))).await?;
67 let mut target = OpenOptions::new()
68 .read(true)
69 .write(true)
70 .create(true)
71 .truncate(true)
72 .open(target)
73 .await?;
74
75 io::copy(&mut extracted_manifest, &mut target).await?;
76
77 let mut manifest = String::new();
78
79 target.seek(io::SeekFrom::Start(0)).await?;
80 target.read_to_string(&mut manifest).await?;
81
82 Ok(manifest)
83}
84
85async fn manifest_from_cache_or_server(
88 server_url: &Url,
89 config: &Config,
90 bar: &Progress<ProgressBar>,
91) -> Result<String, ManifestFromServerError> {
92 let manifest_version = LuaVersion::from(config)?.version_compatibility_str();
93 let url = mk_manifest_url(server_url, &manifest_version, config)?;
94
95 let cache = mk_manifest_cache(&url, config).await?;
98
99 let client = Client::new();
100
101 if let Ok(metadata) = fs::metadata(&cache).await {
103 let last_modified_local: SystemTime = metadata.modified()?;
104
105 let response = client.head(url.clone()).send().await?.error_for_status()?;
107
108 if let Some(last_modified_header) = response.headers().get("Last-Modified") {
109 let server_last_modified = httpdate::parse_http_date(last_modified_header.to_str()?)?;
110
111 if server_last_modified > last_modified_local {
113 bar.map(|bar| {
116 bar.set_message(format!("📥 Downloading updated manifest from {}", &url))
117 });
118
119 return get_manifest(url, manifest_version.clone(), &cache, &client).await;
120 }
121
122 return Ok(fs::read_to_string(&cache).await?);
124 }
125 }
126
127 bar.map(|bar| bar.set_message(format!("📥 Downloading manifest from {}", &url)));
130
131 get_manifest(url, manifest_version.clone(), &cache, &client).await
132}
133
134pub(crate) async fn manifest_from_server_only(
137 server_url: &Url,
138 config: &Config,
139 bar: &Progress<ProgressBar>,
140) -> Result<String, ManifestFromServerError> {
141 let manifest_version = LuaVersion::from(config)?.version_compatibility_str();
142 let url = mk_manifest_url(server_url, &manifest_version, config)?;
143 let cache = mk_manifest_cache(&url, config).await?;
144 let client = Client::new();
145 bar.map(|bar| bar.set_message(format!("📥 Downloading manifest from {}", &url)));
146 get_manifest(url, manifest_version.clone(), &cache, &client).await
147}
148
149fn mk_manifest_url(
150 server_url: &Url,
151 manifest_version: &str,
152 config: &Config,
153) -> Result<Url, ManifestFromServerError> {
154 let manifest_filename = format!("manifest-{}.zip", manifest_version);
155 let url = match config.namespace() {
156 Some(ns) => server_url
157 .join(&format!("manifests/{}/", ns))?
158 .join(&manifest_filename)?,
159 None => server_url.join(&manifest_filename)?,
160 };
161 Ok(url)
162}
163
164async fn mk_manifest_cache(url: &Url, config: &Config) -> io::Result<PathBuf> {
165 let cache = config.cache_dir().join(
166 url.to_string()
168 .replace(&[':', '*', '?', '"', '<', '>', '|', '/', '\\'][..], "_")
169 .trim_end_matches(".zip"),
170 );
171 fs::create_dir_all(cache.parent().unwrap()).await?;
173 Ok(cache)
174}
175
176#[derive(Clone, Debug)]
177pub(crate) struct ManifestMetadata {
178 pub repository: HashMap<PackageName, HashMap<PackageVersion, Vec<RemotePackageType>>>,
179}
180
181impl<'de> serde::Deserialize<'de> for ManifestMetadata {
182 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
183 where
184 D: serde::Deserializer<'de>,
185 {
186 let intermediate = IntermediateManifest::deserialize(deserializer)?;
187 Ok(Self::from_intermediate(intermediate))
188 }
189}
190
191#[derive(Error, Debug)]
192#[error("failed to parse manifest: {0}")]
193pub struct ManifestLuaError(#[from] mlua::Error);
194
195#[derive(Error, Debug)]
196#[error("failed to parse manifest from configuration: {0}")]
197pub enum ManifestError {
198 Lua(#[from] ManifestLuaError),
199 Server(#[from] ManifestFromServerError),
200}
201
202impl ManifestMetadata {
203 pub fn new(manifest: &String) -> Result<Self, ManifestLuaError> {
204 let lua = Lua::new();
205
206 lua.load(manifest).exec()?;
207
208 let intermediate = IntermediateManifest {
209 repository: lua.from_value(lua.globals().get("repository")?)?,
210 };
211 let manifest = Self::from_intermediate(intermediate);
212
213 Ok(manifest)
214 }
215
216 pub fn has_rock(&self, rock_name: &PackageName) -> bool {
217 self.repository.contains_key(rock_name)
218 }
219
220 pub fn latest_match(
221 &self,
222 lua_package_req: &PackageReq,
223 filter: Option<RemotePackageTypeFilterSpec>,
224 ) -> Option<(PackageSpec, RemotePackageType)> {
225 let filter = filter.unwrap_or_default();
226 if !self.has_rock(lua_package_req.name()) {
227 return None;
228 }
229
230 let (version, rock_type) = self.repository[lua_package_req.name()]
231 .iter()
232 .filter(|(version, _)| lua_package_req.version_req().matches(version))
233 .flat_map(|(version, rock_types)| {
234 rock_types.iter().filter_map(move |rock_type| {
235 let include = match rock_type {
236 RemotePackageType::Rockspec => filter.rockspec,
237 RemotePackageType::Src => filter.src,
238 RemotePackageType::Binary => filter.binary,
239 };
240 if include {
241 Some((version, rock_type))
242 } else {
243 None
244 }
245 })
246 })
247 .max_by(
248 |(version_a, type_a), (version_b, type_b)| match version_a.cmp(version_b) {
249 Ordering::Equal => type_a.cmp(type_b),
250 ordering => ordering,
251 },
252 )?;
253
254 Some((
255 PackageSpec::new(lua_package_req.name().clone(), version.clone()),
256 rock_type.clone(),
257 ))
258 }
259
260 fn from_intermediate(intermediate: IntermediateManifest) -> Self {
263 let repository = intermediate
264 .repository
265 .into_iter()
266 .map(|(name, package_map)| {
267 (
268 name,
269 package_map
270 .into_iter()
271 .filter_map(|(version_str, entries)| {
272 let version = PackageVersion::parse(version_str.as_str()).ok()?;
273 let entries = entries
274 .into_iter()
275 .filter_map(|entry| RemotePackageType::try_from(entry).ok())
276 .collect_vec();
277 Some((version, entries))
278 })
279 .collect(),
280 )
281 })
282 .collect();
283 Self { repository }
284 }
285}
286
287#[derive(Clone, Debug)]
288pub(crate) struct Manifest {
289 server_url: Url,
290 metadata: ManifestMetadata,
291}
292
293impl Manifest {
294 pub fn new(server_url: Url, metadata: ManifestMetadata) -> Self {
295 Self {
296 server_url,
297 metadata,
298 }
299 }
300
301 pub async fn from_config(
302 server_url: Url,
303 config: &Config,
304 progress: &Progress<ProgressBar>,
305 ) -> Result<Self, ManifestError> {
306 let content =
307 crate::manifest::manifest_from_cache_or_server(&server_url, config, progress).await?;
308 match ManifestMetadata::new(&content) {
309 Ok(metadata) => Ok(Self::new(server_url, metadata)),
310 Err(_) => {
311 let manifest =
312 crate::manifest::manifest_from_server_only(&server_url, config, progress)
313 .await?;
314 Ok(Self::new(server_url, ManifestMetadata::new(&manifest)?))
315 }
316 }
317 }
318
319 pub fn server_url(&self) -> &Url {
320 &self.server_url
321 }
322
323 pub fn metadata(&self) -> &ManifestMetadata {
324 &self.metadata
325 }
326
327 pub fn find(
329 &self,
330 package_req: &PackageReq,
331 filter: Option<RemotePackageTypeFilterSpec>,
332 ) -> Option<RemotePackage> {
333 match self.metadata().latest_match(package_req, filter) {
334 None => None,
335 Some((package, package_type)) => {
336 let remote_source = match package_type {
337 RemotePackageType::Rockspec => {
338 RemotePackageSource::LuarocksRockspec(self.server_url().clone())
339 }
340 RemotePackageType::Src => {
341 RemotePackageSource::LuarocksSrcRock(self.server_url().clone())
342 }
343 RemotePackageType::Binary => {
344 RemotePackageSource::LuarocksBinaryRock(self.server_url().clone())
345 }
346 };
347 Some(RemotePackage::new(package, remote_source, None))
348 }
349 }
350 }
351}
352
353struct UnsupportedArchitectureError;
354
355impl TryFrom<ManifestRockEntry> for RemotePackageType {
356 type Error = UnsupportedArchitectureError;
357 fn try_from(
358 ManifestRockEntry { arch }: ManifestRockEntry,
359 ) -> Result<Self, UnsupportedArchitectureError> {
360 match arch.as_str() {
361 "rockspec" => Ok(RemotePackageType::Rockspec),
362 "src" => Ok(RemotePackageType::Src),
363 "all" => Ok(RemotePackageType::Binary),
364 arch if arch == crate::luarocks::current_platform_luarocks_identifier() => {
365 Ok(RemotePackageType::Binary)
366 }
367 _ => Err(UnsupportedArchitectureError),
368 }
369 }
370}
371
372#[derive(Clone, serde::Deserialize)]
373struct ManifestRockEntry {
374 pub arch: String,
376}
377
378#[derive(serde::Deserialize)]
380struct IntermediateManifest {
381 repository: HashMap<PackageName, HashMap<String, Vec<ManifestRockEntry>>>,
383}
384
385#[cfg(test)]
386mod tests {
387 use std::path::PathBuf;
388
389 use httptest::{matchers::request, responders::status_code, Expectation, Server};
390 use serial_test::serial;
391
392 use crate::{config::ConfigBuilder, package::PackageReq};
393
394 use super::*;
395
396 fn start_test_server(manifest_name: String) -> Server {
397 let server = Server::run();
398 let manifest_path = format!("/{}", manifest_name);
399 server.expect(
400 Expectation::matching(request::path(manifest_path + ".zip"))
401 .times(1..)
402 .respond_with(
403 status_code(200)
404 .append_header("Last-Modified", "Sat, 20 Jan 2024 13:14:12 GMT")
405 .body(
406 std::fs::read(
407 format!(
408 "{}/resources/test/manifest-5.1.zip",
409 env!("CARGO_MANIFEST_DIR")
410 )
411 .as_str(),
412 )
413 .unwrap(),
414 ),
415 ),
416 );
417 server
418 }
419
420 #[tokio::test]
421 #[serial]
422 pub async fn get_manifest_luajit() {
423 let cache_dir = assert_fs::TempDir::new().unwrap().to_path_buf();
424 let server = start_test_server("manifest-5.1".into());
425 let mut url_str = server.url_str(""); url_str.pop();
427 let config = ConfigBuilder::new()
428 .unwrap()
429 .cache_dir(Some(cache_dir))
430 .lua_version(Some(crate::config::LuaVersion::LuaJIT))
431 .build()
432 .unwrap();
433 manifest_from_cache_or_server(
434 &Url::parse(&url_str).unwrap(),
435 &config,
436 &Progress::NoProgress,
437 )
438 .await
439 .unwrap();
440 }
441
442 #[tokio::test]
443 #[serial]
444 pub async fn get_manifest_for_5_1() {
445 let cache_dir = assert_fs::TempDir::new().unwrap().to_path_buf();
446 let server = start_test_server("manifest-5.1".into());
447 let mut url_str = server.url_str(""); url_str.pop();
449
450 let config = ConfigBuilder::new()
451 .unwrap()
452 .cache_dir(Some(cache_dir))
453 .lua_version(Some(crate::config::LuaVersion::Lua51))
454 .build()
455 .unwrap();
456
457 manifest_from_cache_or_server(
458 &Url::parse(&url_str).unwrap(),
459 &config,
460 &Progress::NoProgress,
461 )
462 .await
463 .unwrap();
464 }
465
466 #[tokio::test]
467 #[serial]
468 pub async fn get_cached_manifest() {
469 let server = start_test_server("manifest-5.1".into());
470 let mut url_str = server.url_str(""); url_str.pop();
472 let manifest_content = std::fs::read_to_string(
473 format!("{}/resources/test/manifest-5.1", env!("CARGO_MANIFEST_DIR")).as_str(),
474 )
475 .unwrap();
476 let cache_dir = assert_fs::TempDir::new().unwrap();
477 let cache = cache_dir.join("manifest-5.1");
478 fs::write(&cache, &manifest_content).await.unwrap();
479 let _metadata = fs::metadata(&cache).await.unwrap();
480 let config = ConfigBuilder::new()
481 .unwrap()
482 .cache_dir(Some(cache_dir.to_path_buf()))
483 .lua_version(Some(crate::config::LuaVersion::Lua51))
484 .build()
485 .unwrap();
486 let result = manifest_from_cache_or_server(
487 &Url::parse(&url_str).unwrap(),
488 &config,
489 &Progress::NoProgress,
490 )
491 .await
492 .unwrap();
493 assert_eq!(result, manifest_content);
494 }
495
496 #[tokio::test]
497 pub async fn parse_metadata_from_empty_manifest() {
498 let manifest = "
499 commands = {}\n
500 modules = {}\n
501 repository = {}\n
502 "
503 .to_string();
504 ManifestMetadata::new(&manifest).unwrap();
505 }
506
507 #[tokio::test]
508 pub async fn parse_metadata_from_test_manifest() {
509 let mut test_manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
510 test_manifest_path.push("resources/test/manifest-5.1");
511 let manifest = String::from_utf8(fs::read(&test_manifest_path).await.unwrap()).unwrap();
512 ManifestMetadata::new(&manifest).unwrap();
513 }
514
515 #[tokio::test]
516 pub async fn latest_match_regression() {
517 let mut test_manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
518 test_manifest_path.push("resources/test/manifest-5.1");
519 let manifest = String::from_utf8(fs::read(&test_manifest_path).await.unwrap()).unwrap();
520 let metadata = ManifestMetadata::new(&manifest).unwrap();
521
522 let package_req: PackageReq = "30log > 1.3.0".parse().unwrap();
523 assert!(metadata.latest_match(&package_req, None).is_none());
524 }
525}