1use crate::package;
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<package::Name, HashMap<package::Version, 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")]
30 ExecutionError(#[from] ottavino::ExternError),
31 #[error("failed to deserialize Lua manifest")]
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: &package::Name) -> bool {
67 self.repository.contains_key(rock_name)
68 }
69
70 pub fn latest_match(
71 &self,
72 lua_package_req: &package::Req,
73 filter: &RemotePackageTypeFilterSpec,
74 ) -> Option<(package::Spec, RemotePackageType)> {
75 if !self.has_rock(lua_package_req.name()) {
76 return None;
77 }
78
79 let (version, rock_type) = self.repository[lua_package_req.name()]
80 .iter()
81 .filter(|(version, _)| lua_package_req.version_req().matches(version))
82 .flat_map(|(version, rock_types)| {
83 rock_types.iter().filter_map(move |rock_type| {
84 let include = match rock_type {
85 RemotePackageType::Rockspec => filter.rockspec,
86 RemotePackageType::Src => filter.src,
87 RemotePackageType::Binary => filter.binary,
88 };
89 if include {
90 Some((version, rock_type))
91 } else {
92 None
93 }
94 })
95 })
96 .max_by(
97 |(version_a, type_a), (version_b, type_b)| match version_a.cmp(version_b) {
98 Ordering::Equal => type_a.cmp(type_b),
99 ordering => ordering,
100 },
101 )?;
102
103 Some((
104 package::Spec::new(lua_package_req.name().clone(), version.clone()),
105 rock_type.clone(),
106 ))
107 }
108
109 fn from_intermediate(intermediate: IntermediateManifest) -> Self {
112 let repository = intermediate
113 .repository
114 .into_iter()
115 .map(|(name, package_map)| {
116 (
117 name,
118 package_map
119 .into_iter()
120 .filter_map(|(version_str, entries)| {
121 let version = package::Version::parse(version_str.as_str()).ok()?;
122 let entries = entries
123 .into_iter()
124 .filter_map(|entry| RemotePackageType::try_from(entry).ok())
125 .collect_vec();
126 Some((version, entries))
127 })
128 .collect(),
129 )
130 })
131 .collect();
132 Self { repository }
133 }
134}
135
136struct UnsupportedArchitectureError;
137
138#[derive(Clone, serde::Deserialize)]
139struct ManifestRockEntry {
140 pub arch: String,
142}
143
144impl TryFrom<ManifestRockEntry> for RemotePackageType {
145 type Error = UnsupportedArchitectureError;
146 fn try_from(
147 ManifestRockEntry { arch }: ManifestRockEntry,
148 ) -> Result<Self, UnsupportedArchitectureError> {
149 match arch.as_str() {
150 "rockspec" => Ok(RemotePackageType::Rockspec),
151 "src" => Ok(RemotePackageType::Src),
152 "all" => Ok(RemotePackageType::Binary),
153 arch if arch == crate::luarocks::current_platform_luarocks_identifier() => {
154 Ok(RemotePackageType::Binary)
155 }
156 _ => Err(UnsupportedArchitectureError),
157 }
158 }
159}
160
161#[derive(serde::Deserialize)]
163struct IntermediateManifest {
164 repository: HashMap<package::Name, HashMap<String, Vec<ManifestRockEntry>>>,
166}
167
168#[cfg(test)]
169mod tests {
170 use std::path::PathBuf;
171
172 use tokio::fs;
173
174 use super::*;
175
176 #[tokio::test]
177 pub async fn parse_metadata_from_empty_manifest() {
178 let manifest = "
179 commands = {}\n
180 modules = {}\n
181 repository = {}\n
182 "
183 .to_string();
184 ManifestMetadata::new(&manifest).unwrap();
185 }
186
187 #[tokio::test]
188 pub async fn parse_metadata_from_test_manifest() {
189 let test_manifest_path =
190 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
191 let manifest = String::from_utf8(fs::read(&test_manifest_path).await.unwrap()).unwrap();
192 ManifestMetadata::new(&manifest).unwrap();
193 }
194
195 #[tokio::test]
196 pub async fn latest_match_regression() {
197 let test_manifest_path =
198 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
199 let manifest = String::from_utf8(fs::read(&test_manifest_path).await.unwrap()).unwrap();
200 let metadata = ManifestMetadata::new(&manifest).unwrap();
201
202 let package_req: package::Req = "30log > 1.3.0".parse().unwrap();
203 assert!(metadata
204 .latest_match(&package_req, &Default::default())
205 .is_none());
206 }
207 #[tokio::test]
208 pub async fn latest_match_respects_type_filter() {
209 let test_manifest_path =
210 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
211 let manifest = String::from_utf8(fs::read(&test_manifest_path).await.unwrap()).unwrap();
212 let metadata = ManifestMetadata::new(&manifest).unwrap();
213 let package_req: package::Req = "combine == 1.0-1".parse().unwrap();
214 assert!(metadata
215 .latest_match(&package_req, &Default::default())
216 .is_some());
217 let filter = RemotePackageTypeFilterSpec {
218 rockspec: false,
219 src: true,
220 binary: true,
221 };
222 assert!(metadata.latest_match(&package_req, &filter).is_none());
223 }
224
225 #[tokio::test]
226 pub async fn latest_match_prefers_binary_then_rockspec_then_src() {
227 let manifest = r#"
228 commands = {}
229 modules = {}
230 repository = {
231 ['testpkg'] = {
232 ['1.0-1'] = {
233 { arch = "rockspec" }, { arch = "src" }, { arch = "all" }
234 }
235 }
236 }
237 "#
238 .to_string();
239 let metadata = ManifestMetadata::new(&manifest).unwrap();
240 let package_req: package::Req = "testpkg == 1.0-1".parse().unwrap();
241
242 let (_, rock_type) = metadata
243 .latest_match(&package_req, &Default::default())
244 .unwrap();
245 assert_eq!(rock_type, RemotePackageType::Binary);
246
247 let no_binary = RemotePackageTypeFilterSpec {
248 rockspec: true,
249 src: true,
250 binary: false,
251 };
252 let (_, rock_type) = metadata.latest_match(&package_req, &no_binary).unwrap();
253 assert_eq!(rock_type, RemotePackageType::Rockspec);
254
255 let src_only = RemotePackageTypeFilterSpec {
256 rockspec: false,
257 src: true,
258 binary: false,
259 };
260 let (_, rock_type) = metadata.latest_match(&package_req, &src_only).unwrap();
261 assert_eq!(rock_type, RemotePackageType::Src);
262 }
263}