Skip to main content

lux_lib/manifest/
metadata.rs

1use 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: &RemotePackageTypeFilterSpec,
74    ) -> Option<(PackageSpec, 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            PackageSpec::new(lua_package_req.name().clone(), version.clone()),
105            rock_type.clone(),
106        ))
107    }
108
109    /// Construct a `ManifestMetadata` from an intermediate representation,
110    /// silently skipping entries for versions we don't know how to parse.
111    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 = PackageVersion::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    /// e.g. "linux-x86_64", "rockspec", "src", ...
141    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/// Intermediate implementation for deserializing
162#[derive(serde::Deserialize)]
163struct IntermediateManifest {
164    /// The key of each package's HashMap is the version string
165    repository: HashMap<PackageName, HashMap<String, Vec<ManifestRockEntry>>>,
166}
167
168#[cfg(test)]
169mod tests {
170    use std::path::PathBuf;
171
172    use tokio::fs;
173
174    use crate::package::PackageReq;
175
176    use super::*;
177
178    #[tokio::test]
179    pub async fn parse_metadata_from_empty_manifest() {
180        let manifest = "
181            commands = {}\n
182            modules = {}\n
183            repository = {}\n
184            "
185        .to_string();
186        ManifestMetadata::new(&manifest).unwrap();
187    }
188
189    #[tokio::test]
190    pub async fn parse_metadata_from_test_manifest() {
191        let test_manifest_path =
192            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
193        let manifest = String::from_utf8(fs::read(&test_manifest_path).await.unwrap()).unwrap();
194        ManifestMetadata::new(&manifest).unwrap();
195    }
196
197    #[tokio::test]
198    pub async fn latest_match_regression() {
199        let test_manifest_path =
200            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
201        let manifest = String::from_utf8(fs::read(&test_manifest_path).await.unwrap()).unwrap();
202        let metadata = ManifestMetadata::new(&manifest).unwrap();
203
204        let package_req: PackageReq = "30log > 1.3.0".parse().unwrap();
205        assert!(metadata
206            .latest_match(&package_req, &Default::default())
207            .is_none());
208    }
209    #[tokio::test]
210    pub async fn latest_match_respects_type_filter() {
211        let test_manifest_path =
212            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/manifest-5.1");
213        let manifest = String::from_utf8(fs::read(&test_manifest_path).await.unwrap()).unwrap();
214        let metadata = ManifestMetadata::new(&manifest).unwrap();
215        let package_req: PackageReq = "combine == 1.0-1".parse().unwrap();
216        assert!(metadata
217            .latest_match(&package_req, &Default::default())
218            .is_some());
219        let filter = RemotePackageTypeFilterSpec {
220            rockspec: false,
221            src: true,
222            binary: true,
223        };
224        assert!(metadata.latest_match(&package_req, &filter).is_none());
225    }
226}