Skip to main content

lux_lib/lua_rockspec/
partial.rs

1use std::collections::HashMap;
2
3use ottavino::{Closure, Executor, Fuel};
4use ottavino_util::serde::from_value;
5use thiserror::Error;
6
7use crate::{
8    lua_rockspec::RockspecFormat, package::PackageName,
9    rockspec::lua_dependency::LuaDependencySpec, ROCKSPEC_FUEL_LIMIT,
10};
11
12use super::{
13    parse_lua_tbl_or_default, BuildSpecInternal, DeploySpec, ExternalDependencySpec,
14    PlatformSupport, RockDescription, TestSpecInternal,
15};
16
17/// Deserialized from a Lua `.rockspec`, not yet validated
18#[derive(Debug)]
19pub struct PartialLuaRockspec {
20    pub(crate) rockspec_format: Option<RockspecFormat>,
21    pub(crate) package: Option<PackageName>,
22    pub(crate) build: Option<BuildSpecInternal>,
23    pub(crate) deploy: Option<DeploySpec>,
24    pub(crate) description: Option<RockDescription>,
25    pub(crate) supported_platforms: Option<PlatformSupport>,
26    pub(crate) dependencies: Option<Vec<LuaDependencySpec>>,
27    pub(crate) build_dependencies: Option<Vec<LuaDependencySpec>>,
28    pub(crate) external_dependencies: Option<HashMap<String, ExternalDependencySpec>>,
29    pub(crate) test_dependencies: Option<Vec<LuaDependencySpec>>,
30    pub(crate) test: Option<TestSpecInternal>,
31}
32
33#[derive(Debug, Error)]
34pub enum PartialRockspecError {
35    #[error("rockspec execution exceeded fuel limit of {ROCKSPEC_FUEL_LIMIT} steps")]
36    FuelLimitExceeded,
37    #[error("field '{0}' should not be declared in extra.rockspec")]
38    ExtraneousField(String),
39    #[error("error while parsing rockspec: {0}")]
40    Lua(#[from] ottavino::ExternError),
41    #[error(transparent)]
42    Io(#[from] std::io::Error),
43}
44
45impl PartialLuaRockspec {
46    pub fn new(rockspec_content: &str) -> Result<Self, PartialRockspecError> {
47        let mut lua = ottavino::Lua::core();
48
49        let rockspec = lua.try_enter(|ctx| {
50            let closure = Closure::load(ctx, None, rockspec_content.as_bytes())?;
51
52            let executor = Executor::start(ctx, closure.into(), ());
53
54            let output = executor.step(ctx, &mut Fuel::with(ROCKSPEC_FUEL_LIMIT))?;
55
56            if !output {
57                return Ok(Err(PartialRockspecError::FuelLimitExceeded));
58            }
59
60            let globals = ctx.globals();
61
62            if !matches!(globals.get_value(ctx, "version"), ottavino::Value::Nil) {
63                return Ok(Err(PartialRockspecError::ExtraneousField(
64                    "version".to_string(),
65                )));
66            }
67            if !matches!(globals.get_value(ctx, "source"), ottavino::Value::Nil) {
68                return Ok(Err(PartialRockspecError::ExtraneousField(
69                    "source".to_string(),
70                )));
71            }
72
73            let rockspec = PartialLuaRockspec {
74                rockspec_format: from_value(globals.get_value(ctx, "rockspec_format"))
75                    .unwrap_or_default(),
76                package: from_value(globals.get_value(ctx, "package")).unwrap_or_default(),
77                description: parse_lua_tbl_or_default(ctx, "description").unwrap_or_default(),
78                supported_platforms: parse_lua_tbl_or_default(ctx, "supported_platforms")
79                    .unwrap_or_default(),
80                dependencies: from_value(globals.get_value(ctx, "dependencies"))
81                    .unwrap_or_default(),
82                build_dependencies: from_value(globals.get_value(ctx, "build_dependencies"))
83                    .unwrap_or_default(),
84                test_dependencies: from_value(globals.get_value(ctx, "test_dependencies"))
85                    .unwrap_or_default(),
86                external_dependencies: from_value(globals.get_value(ctx, "external_dependencies"))
87                    .unwrap_or_default(),
88                build: from_value(globals.get_value(ctx, "build")).unwrap_or_default(),
89                test: from_value(globals.get_value(ctx, "test")).unwrap_or_default(),
90                deploy: from_value(globals.get_value(ctx, "deploy")).unwrap_or_default(),
91            };
92
93            Ok(Ok(rockspec))
94        })??;
95
96        Ok(rockspec)
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn parse_partial_rockspec() {
106        let partial_rockspec = r#"
107            package = "my-package"
108        "#;
109
110        PartialLuaRockspec::new(partial_rockspec).unwrap();
111
112        // Whether the partial rockspec format can still support entire rockspecs
113        let full_rockspec = r#"
114            rockspec_format = "3.0"
115            package = "my-package"
116
117            description = {
118                summary = "A summary",
119                detailed = "A detailed description",
120                license = "MIT",
121                homepage = "https://example.com",
122                issues_url = "https://example.com/issues",
123                maintainer = "John Doe",
124                labels = {"label1", "label2"},
125            }
126
127            supported_platforms = {"linux", "!windows"}
128
129            dependencies = {
130                "lua 5.1",
131                "foo 1.0",
132                "bar >=2.0",
133            }
134
135            build_dependencies = {
136                "baz 1.0",
137            }
138
139            external_dependencies = {
140                foo = { header = "foo.h" },
141                bar = { library = "libbar.so" },
142            }
143
144            test_dependencies = {
145                "busted 1.0",
146            }
147
148            test = {
149                type = "command",
150                script = "test.lua",
151                flags = {"foo", "bar"},
152            }
153
154            build = {
155                type = "builtin",
156            }
157        "#;
158
159        let rockspec = PartialLuaRockspec::new(full_rockspec).unwrap();
160
161        // No need to verify if the fields were parsed correctly, but worth checking if they were
162        // parsed at all.
163
164        assert!(rockspec.rockspec_format.is_some());
165        assert!(rockspec.package.is_some());
166        assert!(rockspec.description.is_some());
167        assert!(rockspec.supported_platforms.is_some());
168        assert!(rockspec.dependencies.is_some());
169        assert!(rockspec.build_dependencies.is_some());
170        assert!(rockspec.external_dependencies.is_some());
171        assert!(rockspec.test_dependencies.is_some());
172        assert!(rockspec.build.is_some());
173        assert!(rockspec.test.is_some());
174
175        // We don't allow version and source in extra.rockspec
176        let partial_rockspec = r#"
177            version = "2.0.0"
178        "#;
179
180        PartialLuaRockspec::new(partial_rockspec).unwrap_err();
181
182        let partial_rockspec = r#"
183            source = {
184                url = "https://example.com",
185            }
186        "#;
187
188        PartialLuaRockspec::new(partial_rockspec).unwrap_err();
189    }
190}