lux_lib/manifest/
metadata.rs1use crate::package::{PackageName, PackageReq, PackageSpec, PackageVersion};
2use crate::package::{RemotePackageType, RemotePackageTypeFilterSpec};
3use crate::ROCKSPEC_FUEL_LIMIT;
4use itertools::Itertools;
5use miette::Diagnostic;
6use ottavino::{Closure, Executor, Fuel, Lua};
7use ottavino_util::serde::from_value;
8use std::{cmp::Ordering, collections::HashMap};
9use thiserror::Error;
10
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub(crate) struct ManifestMetadata {
13 pub repository: HashMap<PackageName, HashMap<PackageVersion, Vec<RemotePackageType>>>,
14}
15
16impl<'de> serde::Deserialize<'de> for ManifestMetadata {
17 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
18 where
19 D: serde::Deserializer<'de>,
20 {
21 let intermediate = IntermediateManifest::deserialize(deserializer)?;
22 Ok(Self::from_intermediate(intermediate))
23 }
24}
25
26#[derive(Error, Debug, Diagnostic)]
27#[non_exhaustive]
28pub enum ManifestLuaError {
29 #[error("failed to parse Lua manifest:\n{0}")]
30 ExecutionError(#[from] ottavino::ExternError),
31 #[error("failed to deserialize Lua manifest:\n{0}")]
32 DeserializationError(#[from] ottavino_util::serde::de::Error),
33 #[error("manifest exceeds computational limit of {ROCKSPEC_FUEL_LIMIT} steps")]
34 #[diagnostic(help(
35 r#"the rockspec may be too complex or contain too many dependencies.
36 Try reducing the number of dependencies or simplifying build instructions."#
37 ))]
38 FuelLimitExceeded,
39}
40
41impl ManifestMetadata {
42 pub fn new(manifest: &String) -> Result<Self, ManifestLuaError> {
43 let mut lua = Lua::core();
44
45 let success = lua.try_enter(|ctx| {
46 let closure = Closure::load(ctx, None, manifest.as_bytes())?;
47
48 let executor = Executor::start(ctx, closure.into(), ());
49
50 Ok(executor.step(ctx, &mut Fuel::with(ROCKSPEC_FUEL_LIMIT))?)
51 })?;
52
53 if !success {
54 return Err(ManifestLuaError::FuelLimitExceeded);
55 }
56
57 let intermediate = IntermediateManifest {
58 repository: lua.enter(|ctx| from_value(ctx.globals().get_value(ctx, "repository")))?,
59 };
60
61 let manifest = Self::from_intermediate(intermediate);
62
63 Ok(manifest)
64 }
65
66 pub fn has_rock(&self, rock_name: &PackageName) -> bool {
67 self.repository.contains_key(rock_name)
68 }
69
70 pub fn latest_match(
71 &self,
72 lua_package_req: &PackageReq,
73 filter: Option<RemotePackageTypeFilterSpec>,
74 ) -> Option<(PackageSpec, RemotePackageType)> {
75 let filter = filter.unwrap_or_default();
76 if !self.has_rock(lua_package_req.name()) {
77 return None;
78 }
79
80 let (version, rock_type) = self.repository[lua_package_req.name()]
81 .iter()
82 .filter(|(version, _)| lua_package_req.version_req().matches(version))
83 .flat_map(|(version, rock_types)| {
84 rock_types.iter().filter_map(move |rock_type| {
85 let include = match rock_type {
86 RemotePackageType::Rockspec => filter.rockspec,
87 RemotePackageType::Src => filter.src,
88 RemotePackageType::Binary => filter.binary,
89 };
90 if include {
91 Some((version, rock_type))
92 } else {
93 None
94 }
95 })
96 })
97 .max_by(
98 |(version_a, type_a), (version_b, type_b)| match version_a.cmp(version_b) {
99 Ordering::Equal => type_a.cmp(type_b),
100 ordering => ordering,
101 },
102 )?;
103
104 Some((
105 PackageSpec::new(lua_package_req.name().clone(), version.clone()),
106 rock_type.clone(),
107 ))
108 }
109
110 fn from_intermediate(intermediate: IntermediateManifest) -> Self {
113 let repository = intermediate
114 .repository
115 .into_iter()
116 .map(|(name, package_map)| {
117 (
118 name,
119 package_map
120 .into_iter()
121 .filter_map(|(version_str, entries)| {
122 let version = PackageVersion::parse(version_str.as_str()).ok()?;
123 let entries = entries
124 .into_iter()
125 .filter_map(|entry| RemotePackageType::try_from(entry).ok())
126 .collect_vec();
127 Some((version, entries))
128 })
129 .collect(),
130 )
131 })
132 .collect();
133 Self { repository }
134 }
135}
136
137struct UnsupportedArchitectureError;
138
139#[derive(Clone, serde::Deserialize)]
140struct ManifestRockEntry {
141 pub arch: String,
143}
144
145impl TryFrom<ManifestRockEntry> for RemotePackageType {
146 type Error = UnsupportedArchitectureError;
147 fn try_from(
148 ManifestRockEntry { arch }: ManifestRockEntry,
149 ) -> Result<Self, UnsupportedArchitectureError> {
150 match arch.as_str() {
151 "rockspec" => Ok(RemotePackageType::Rockspec),
152 "src" => Ok(RemotePackageType::Src),
153 "all" => Ok(RemotePackageType::Binary),
154 arch if arch == crate::luarocks::current_platform_luarocks_identifier() => {
155 Ok(RemotePackageType::Binary)
156 }
157 _ => Err(UnsupportedArchitectureError),
158 }
159 }
160}
161
162#[derive(serde::Deserialize)]
164struct IntermediateManifest {
165 repository: HashMap<PackageName, HashMap<String, Vec<ManifestRockEntry>>>,
167}
168
169#[cfg(test)]
170mod tests {
171 use std::path::PathBuf;
172
173 use tokio::fs;
174
175 use crate::package::PackageReq;
176
177 use super::*;
178
179 #[tokio::test]
180 pub async fn parse_metadata_from_empty_manifest() {
181 let manifest = "
182 commands = {}\n
183 modules = {}\n
184 repository = {}\n
185 "
186 .to_string();
187 ManifestMetadata::new(&manifest).unwrap();
188 }
189
190 #[tokio::test]
191 pub async fn parse_metadata_from_test_manifest() {
192 let test_manifest_path =
193 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
194 let manifest = String::from_utf8(fs::read(&test_manifest_path).await.unwrap()).unwrap();
195 ManifestMetadata::new(&manifest).unwrap();
196 }
197
198 #[tokio::test]
199 pub async fn latest_match_regression() {
200 let test_manifest_path =
201 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
202 let manifest = String::from_utf8(fs::read(&test_manifest_path).await.unwrap()).unwrap();
203 let metadata = ManifestMetadata::new(&manifest).unwrap();
204
205 let package_req: PackageReq = "30log > 1.3.0".parse().unwrap();
206 assert!(metadata.latest_match(&package_req, None).is_none());
207 }
208}