Skip to main content

lux_lib/manifest/
metadata.rs

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