Skip to main content

lux_lib/lua_rockspec/
mod.rs

1mod build;
2mod dependency;
3mod deploy;
4mod partial;
5mod platform;
6mod rock_source;
7mod serde_util;
8mod test_spec;
9
10use crate::package;
11use std::{
12    collections::HashMap, convert::Infallible, fmt::Display, io, path::PathBuf, str::FromStr,
13};
14
15use ottavino::{Closure, Executor, Fuel};
16use ottavino_util::serde::from_value;
17use serde::{de::DeserializeOwned, Deserialize, Serialize};
18
19pub use build::*;
20pub use dependency::*;
21pub use deploy::*;
22use miette::{Diagnostic, NamedSource, SourceSpan};
23pub use partial::*;
24pub use platform::*;
25pub use rock_source::*;
26use ssri::Integrity;
27pub use test_spec::*;
28use thiserror::Error;
29use url::Url;
30
31pub(crate) use lux_macros::DisplayAsLuaKV;
32pub(crate) use serde_util::*;
33
34impl DisplayAsLuaValue for Url {
35    fn display_lua_value(&self) -> DisplayLuaValue {
36        DisplayLuaValue::String(self.to_string())
37    }
38}
39
40use crate::{
41    hash::HasIntegrity,
42    lua_version::{LuaVersion, LuaVersionUnset},
43    project::{project_toml, ProjectRoot},
44    rockspec::{lua_dependency::LuaDependencySpec, Rockspec},
45    ROCKSPEC_FUEL_LIMIT,
46};
47
48#[derive(Error, Debug, Diagnostic)]
49#[non_exhaustive]
50pub enum Error {
51    #[error("manifest exceeds computational limit of {ROCKSPEC_FUEL_LIMIT} steps")]
52    #[diagnostic(
53        code(lux_lib::lua_rockspec::fuel_limit_exceeded),
54        help(
55            r#"the rockspec may be too complex or contain too many dependencies.
56try reducing the number of dependencies or simplifying build instructions.
57"#
58        )
59    )]
60    FuelLimitExceeded,
61    #[error("could not parse rockspec")]
62    #[diagnostic(code(lux_lib::lua_rockspec::execution),
63        help("check the rockspec or lux.toml for valid syntax and make sure it matches the specification."),
64        url("https://lux.lumen-labs.org/reference/lux-toml")
65    )]
66    ExecutionError {
67        cause: Box<String>,
68        #[source_code]
69        content: NamedSource<String>,
70        #[label("this package")]
71        pkg_span: SourceSpan,
72        #[label("{cause}")]
73        err_span: Option<SourceSpan>,
74    },
75    #[error("{}copy_directories cannot contain the rockspec name", ._0.as_ref().map(|p| format!("{p}: ")).unwrap_or_default())]
76    CopyDirectoriesContainRockspecName(Option<String>),
77    #[error("cannot create Lua rockspec with off-spec dependency")]
78    #[diagnostic(
79        code(lux_lib::lua_rockspec::off_spec_dependency),
80        url("https://lux.lumen-labs.org/reference/lux-toml#dependencies")
81    )]
82    OffSpecDependency(package::Name),
83    #[error("cannot create Lua rockspec with off-spec build dependency")]
84    #[diagnostic(
85        code(lux_lib::lua_rockspec::off_spec_build_dependency),
86        url("https://lux.lumen-labs.org/reference/lux-toml#dependencies")
87    )]
88    OffSpecBuildDependency(package::Name),
89    #[error("cannot create Lua rockspec with off-spec test dependency")]
90    #[diagnostic(
91        code(lux_lib::lua_rockspec::off_spec_test_dependency),
92        url("https://lux.lumen-labs.org/reference/lux-toml#dependencies")
93    )]
94    OffSpecTestDependency(package::Name),
95    #[error(transparent)]
96    #[diagnostic(transparent)]
97    ProjectToml(#[from] project_toml::Error),
98}
99
100impl Error {
101    fn from_ottavino(cause: ottavino::ExternError, rockspec_content: &str) -> Self {
102        let msg = match &cause {
103            ottavino::ExternError::Runtime(rt) => rt.to_string(),
104            _ => cause.to_string(),
105        };
106        let field = msg
107            .strip_prefix("invalid rockspec field '")
108            .and_then(|rest| rest.split('\'').next())
109            .map(|f| f.to_owned());
110        let err_span = field.as_deref().and_then(|field| {
111            let span = find_key_span(rockspec_content, field);
112            if span.is_empty() {
113                None
114            } else {
115                Some(span)
116            }
117        });
118        Error::ExecutionError {
119            cause: Box::new(msg),
120            content: NamedSource::new("rockspec", rockspec_content.to_string()),
121            pkg_span: find_value_span(rockspec_content, "package"),
122            err_span,
123        }
124    }
125}
126
127/// This error can be returned within `ottavino::Lua::try_enter`.
128/// ottavino wraps it in an [`ottavino::ExternError`], which Lux wraps inn
129/// a [`Error::ExecutionError`]
130#[derive(Error, Debug, Diagnostic)]
131#[non_exhaustive]
132enum ParseLuaRockspecFieldError {
133    #[error("invalid rockspec field '{field}': {source}")]
134    Deserialize {
135        field: String,
136        source: Box<ottavino_util::serde::de::Error>,
137    },
138    #[error(
139        r#"invalid rockspec field '{field}'.
140Failed to parse a table field: {source}"#
141    )]
142    DeserializeTable {
143        field: String,
144        source: Box<ottavino_util::serde::de::Error>,
145    },
146    #[error("invalid rockspec field '{field}'. expected a table, but got: {invalid_type}")]
147    NotATable { field: String, invalid_type: String },
148}
149
150/// RockSpec for a local rock installation, deserialized from a `.rockspec` file
151#[derive(Clone, Debug)]
152#[cfg_attr(test, derive(PartialEq))]
153pub struct LocalLuaRockspec {
154    /// The file format version. Example: "1.0"
155    rockspec_format: Option<RockspecFormat>,
156    /// The name of the package. Example: "luasocket"
157    package: package::Name,
158    /// The version of the package, plus a suffix indicating the revision of the rockspec. Example: "2.0.1-1"
159    version: package::Version,
160    description: RockDescription,
161    supported_platforms: PlatformSupport,
162    /// The Lua version requirement for this rock
163    lua: package::VersionReq,
164    dependencies: PerPlatform<Vec<LuaDependencySpec>>,
165    build_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
166    external_dependencies: PerPlatform<HashMap<String, ExternalDependencySpec>>,
167    test_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
168    build: PerPlatform<BuildSpec>,
169    source: PerPlatform<RemoteRockSource>,
170    test: PerPlatform<TestSpec>,
171    deploy: PerPlatform<DeploySpec>,
172    /// The original content of this rockspec, needed by luarocks
173    raw_content: String,
174}
175
176trait HasRockspecKey<'gc> {
177    fn get_rockspec_key<V: Deserialize<'gc>>(
178        &self,
179        ctx: ottavino::Context<'gc>,
180        key: &str,
181    ) -> Result<V, ParseLuaRockspecFieldError>;
182}
183
184impl<'gc> HasRockspecKey<'gc> for ottavino::Table<'gc> {
185    fn get_rockspec_key<V: Deserialize<'gc>>(
186        &self,
187        ctx: ottavino::Context<'gc>,
188        key: &str,
189    ) -> Result<V, ParseLuaRockspecFieldError> {
190        let value = self.get_value(ctx, key.to_string());
191        let is_table = matches!(&value, ottavino::Value::Table(_));
192        from_value(value).map_err(|source| {
193            if is_table {
194                ParseLuaRockspecFieldError::DeserializeTable {
195                    field: key.into(),
196                    source: Box::new(source),
197                }
198            } else {
199                ParseLuaRockspecFieldError::Deserialize {
200                    field: key.into(),
201                    source: Box::new(source),
202                }
203            }
204        })
205    }
206}
207
208impl LocalLuaRockspec {
209    /// Builds a [`LocalLuaRockspec`] from an already-parsed project.
210    pub(crate) fn from_project_toml(
211        project: &crate::project::project_toml::LocalProjectToml,
212        raw_content: String,
213    ) -> Self {
214        use crate::rockspec::Rockspec;
215
216        Self {
217            rockspec_format: project.format().clone(),
218            package: project.package().clone(),
219            version: project.version().clone(),
220            description: project.description().clone(),
221            supported_platforms: project.supported_platforms().clone(),
222            lua: project.lua().clone(),
223            dependencies: project.dependencies().clone(),
224            build_dependencies: project.build_dependencies().clone(),
225            external_dependencies: project.external_dependencies().clone(),
226            test_dependencies: project.test_dependencies().clone(),
227            build: project.build().clone(),
228            source: project.source().clone(),
229            test: project.test().clone(),
230            deploy: project.deploy().clone(),
231            raw_content,
232        }
233    }
234
235    /// Wraps this rockspec into a [`RemoteLuaRockspec`], overriding its source
236    /// with the given `source`.
237    pub fn into_remote_with_source(
238        self,
239        source: PerPlatform<RemoteRockSource>,
240    ) -> RemoteLuaRockspec {
241        RemoteLuaRockspec {
242            local: self,
243            source,
244        }
245    }
246
247    pub fn new(rockspec_content: &str, project_root: ProjectRoot) -> Result<Self, Error> {
248        let mut lua = ottavino::Lua::core();
249
250        let rockspec = lua
251            .try_enter(|ctx| {
252                let closure = Closure::load(ctx, None, rockspec_content.as_bytes())?;
253
254                let executor = Executor::start(ctx, closure.into(), ());
255
256                let output = executor.step(ctx, &mut Fuel::with(ROCKSPEC_FUEL_LIMIT))?;
257
258                if !output {
259                    return Ok(Err(Error::FuelLimitExceeded));
260                }
261
262                let globals = ctx.globals();
263
264                let dependencies: PerPlatform<Vec<LuaDependencySpec>> =
265                    globals.get_rockspec_key(ctx, "dependencies")?;
266
267                let lua_version_req = dependencies
268                    .current_platform()
269                    .iter()
270                    .find(|dep| dep.name().to_string() == "lua")
271                    .cloned()
272                    .map(|dep| dep.version_req().clone())
273                    .unwrap_or(package::VersionReq::Any);
274
275                fn strip_lua(
276                    dependencies: PerPlatform<Vec<LuaDependencySpec>>,
277                ) -> PerPlatform<Vec<LuaDependencySpec>> {
278                    dependencies.map(|deps| {
279                        deps.iter()
280                            .filter(|dep| dep.name().to_string() != "lua")
281                            .cloned()
282                            .collect()
283                    })
284                }
285
286                let build_dependencies: PerPlatform<Vec<LuaDependencySpec>> =
287                    globals.get_rockspec_key(ctx, "build_dependencies")?;
288
289                let test_dependencies: PerPlatform<Vec<LuaDependencySpec>> =
290                    globals.get_rockspec_key(ctx, "test_dependencies")?;
291
292                let source: PerPlatform<RemoteRockSource> = match globals.get_value(ctx, "source") {
293                    ottavino::Value::Nil => {
294                        PerPlatform::new(RockSourceSpec::File(project_root.to_path_buf()).into())
295                    }
296                    value => from_value(value).map_err(|source| {
297                        ParseLuaRockspecFieldError::Deserialize {
298                            field: "source".into(),
299                            source: Box::new(source),
300                        }
301                    })?,
302                };
303
304                let rockspec = LocalLuaRockspec {
305                    rockspec_format: globals.get_rockspec_key(ctx, "rockspec_format")?,
306                    package: globals.get_rockspec_key(ctx, "package")?,
307                    version: globals.get_rockspec_key(ctx, "version")?,
308                    description: parse_lua_tbl_or_default(ctx, "description")?,
309                    supported_platforms: parse_lua_tbl_or_default(ctx, "supported_platforms")?,
310                    lua: lua_version_req,
311                    dependencies: strip_lua(dependencies),
312                    build_dependencies: strip_lua(build_dependencies),
313                    test_dependencies: strip_lua(test_dependencies),
314                    external_dependencies: globals
315                        .get_rockspec_key(ctx, "external_dependencies")?,
316                    build: globals.get_rockspec_key(ctx, "build")?,
317                    test: globals.get_rockspec_key(ctx, "test")?,
318                    deploy: globals.get_rockspec_key(ctx, "deploy")?,
319                    raw_content: rockspec_content.into(),
320
321                    source,
322                };
323
324                Ok(Ok(rockspec))
325            })
326            .map_err(|cause| Error::from_ottavino(cause, rockspec_content))??;
327
328        let rockspec_file_name = format!("{}-{}.rockspec", rockspec.package(), rockspec.version());
329
330        if rockspec
331            .build()
332            .default
333            .copy_directories
334            .contains(&PathBuf::from(&rockspec_file_name))
335        {
336            return Err(Error::CopyDirectoriesContainRockspecName(None));
337        }
338
339        for (platform, build_override) in &rockspec.build().per_platform {
340            if build_override
341                .copy_directories
342                .contains(&PathBuf::from(&rockspec_file_name))
343            {
344                return Err(Error::CopyDirectoriesContainRockspecName(Some(
345                    platform.to_string(),
346                )));
347            }
348        }
349        Ok(rockspec)
350    }
351}
352
353impl Rockspec for LocalLuaRockspec {
354    type Error = Infallible;
355
356    fn package(&self) -> &package::Name {
357        &self.package
358    }
359
360    fn version(&self) -> &package::Version {
361        &self.version
362    }
363
364    fn description(&self) -> &RockDescription {
365        &self.description
366    }
367
368    fn supported_platforms(&self) -> &PlatformSupport {
369        &self.supported_platforms
370    }
371
372    fn lua(&self) -> &package::VersionReq {
373        &self.lua
374    }
375
376    fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
377        &self.dependencies
378    }
379
380    fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
381        &self.build_dependencies
382    }
383
384    fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
385        &self.external_dependencies
386    }
387
388    fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
389        &self.test_dependencies
390    }
391
392    fn build(&self) -> &PerPlatform<BuildSpec> {
393        &self.build
394    }
395
396    fn test(&self) -> &PerPlatform<TestSpec> {
397        &self.test
398    }
399
400    fn source(&self) -> &PerPlatform<RemoteRockSource> {
401        &self.source
402    }
403
404    fn deploy(&self) -> &PerPlatform<DeploySpec> {
405        &self.deploy
406    }
407
408    fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
409        &mut self.build
410    }
411
412    fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
413        &mut self.test
414    }
415
416    fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
417        &mut self.source
418    }
419
420    fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
421        &mut self.deploy
422    }
423
424    fn format(&self) -> &Option<RockspecFormat> {
425        &self.rockspec_format
426    }
427
428    fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
429        Ok(self.raw_content.clone())
430    }
431}
432
433impl HasIntegrity for LocalLuaRockspec {
434    async fn hash(&self) -> io::Result<Integrity> {
435        Ok(Integrity::from(&self.raw_content))
436    }
437}
438
439/// RockSpec for a remote rock, deserialized from a `.rockspec` file
440#[derive(Clone, Debug)]
441#[cfg_attr(test, derive(PartialEq))]
442pub struct RemoteLuaRockspec {
443    local: LocalLuaRockspec,
444    source: PerPlatform<RemoteRockSource>,
445}
446
447impl RemoteLuaRockspec {
448    pub fn new(rockspec_content: &str) -> Result<Self, Error> {
449        let mut lua = ottavino::Lua::core();
450
451        let source = lua
452            .try_enter(|ctx| {
453                let closure = Closure::load(ctx, None, rockspec_content.as_bytes())?;
454
455                let executor = Executor::start(ctx, closure.into(), ());
456
457                let output = executor.step(ctx, &mut Fuel::with(ROCKSPEC_FUEL_LIMIT))?;
458
459                if !output {
460                    return Ok(Err(Error::FuelLimitExceeded));
461                }
462
463                let globals = ctx.globals();
464
465                let source: PerPlatform<RemoteRockSource> =
466                    globals.get_rockspec_key(ctx, "source")?;
467
468                Ok(Ok(source))
469            })
470            .map_err(|cause| Error::from_ottavino(cause, rockspec_content))??;
471
472        Ok(RemoteLuaRockspec {
473            // NOTE: Calling this outside of `lua.try_enter` reduces error message repetition
474            local: LocalLuaRockspec::new(rockspec_content, ProjectRoot::new())?,
475            source,
476        })
477    }
478
479    /// Builds a rockspec from a source spec and dependencies.
480    ///
481    /// With no modules, the rockspec uses the `source` build backend.
482    /// Otherwise, it uses the `builtin` build backend with the given modules.
483    pub(crate) fn from_source_spec(
484        package_spec: package::Spec,
485        source_spec: RockSourceSpec,
486        dependencies: Vec<LuaDependencySpec>,
487        modules: HashMap<LuaModule, ModuleSpec>,
488    ) -> Self {
489        let version = package_spec.version().clone();
490        let rockspec_format = RockspecFormat::default();
491        let (build_backend, build_lua) = if modules.is_empty() {
492            (BuildBackendSpec::Source, r#"type = "source""#.to_string())
493        } else {
494            let modules_lua = modules_lua(&modules);
495            (
496                BuildBackendSpec::Builtin(BuiltinBuildSpec { modules }),
497                format!("type = \"builtin\",\n  modules = {{\n{modules_lua}\n  }}"),
498            )
499        };
500        let build_spec = BuildSpec {
501            build_backend: Some(build_backend),
502            ..Default::default()
503        };
504        let raw_content = rockspec_content(&package_spec, &source_spec, &build_lua);
505        let source: RemoteRockSource = source_spec.into();
506
507        let local = LocalLuaRockspec {
508            rockspec_format: Some(rockspec_format),
509            package: package_spec.name().clone(),
510            version,
511            description: RockDescription::default(),
512            supported_platforms: PlatformSupport::default(),
513            lua: package::VersionReq::Any,
514            dependencies: PerPlatform::new(dependencies),
515            build_dependencies: PerPlatform::default(),
516            external_dependencies: PerPlatform::default(),
517            test_dependencies: PerPlatform::default(),
518            build: PerPlatform::new(build_spec),
519            source: PerPlatform::new(source.clone()),
520            test: PerPlatform::default(),
521            deploy: PerPlatform::default(),
522            raw_content,
523        };
524        Self {
525            local,
526            source: PerPlatform::new(source),
527        }
528    }
529}
530
531fn rockspec_content(
532    package_spec: &package::Spec,
533    source_spec: &RockSourceSpec,
534    build_lua: &str,
535) -> String {
536    format!(
537        r#"
538rockspec_format = "{}"
539package = "{}"
540version = "{}"
541{}
542build = {{
543{}
544}}"#,
545        RockspecFormat::default(),
546        package_spec.name(),
547        package_spec.version(),
548        source_spec.display_lua(),
549        build_lua,
550    )
551}
552
553fn modules_lua(modules: &HashMap<LuaModule, ModuleSpec>) -> String {
554    let mut entries: Vec<_> = modules.iter().collect();
555    entries.sort_by_key(|(module, _)| module.as_str());
556    DisplayLuaValue::Table(
557        entries
558            .into_iter()
559            .map(|(module, spec)| DisplayLuaKV {
560                key: module.to_string(),
561                value: spec.display_lua_value(),
562            })
563            .collect(),
564    )
565    .to_string()
566}
567
568impl Rockspec for RemoteLuaRockspec {
569    type Error = Infallible;
570
571    fn package(&self) -> &package::Name {
572        self.local.package()
573    }
574
575    fn version(&self) -> &package::Version {
576        self.local.version()
577    }
578
579    fn description(&self) -> &RockDescription {
580        self.local.description()
581    }
582
583    fn supported_platforms(&self) -> &PlatformSupport {
584        self.local.supported_platforms()
585    }
586
587    fn lua(&self) -> &package::VersionReq {
588        self.local.lua()
589    }
590
591    fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
592        self.local.dependencies()
593    }
594
595    fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
596        match self.format() {
597            // Rockspec formats < 3.0 don't support `build_dependencies`,
598            // so we have to return regular dependencies if the build backend might need to use them.
599            Some(RockspecFormat::_1_0 | RockspecFormat::_2_0)
600                if self
601                    .build()
602                    .current_platform()
603                    .build_backend
604                    .as_ref()
605                    .is_some_and(|build_backend| build_backend.can_use_build_dependencies()) =>
606            {
607                self.local.dependencies()
608            }
609            _ => self.local.build_dependencies(),
610        }
611    }
612
613    fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
614        self.local.external_dependencies()
615    }
616
617    fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
618        self.local.test_dependencies()
619    }
620
621    fn build(&self) -> &PerPlatform<BuildSpec> {
622        self.local.build()
623    }
624
625    fn test(&self) -> &PerPlatform<TestSpec> {
626        self.local.test()
627    }
628
629    fn source(&self) -> &PerPlatform<RemoteRockSource> {
630        &self.source
631    }
632
633    fn deploy(&self) -> &PerPlatform<DeploySpec> {
634        self.local.deploy()
635    }
636
637    fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
638        self.local.build_mut()
639    }
640
641    fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
642        self.local.test_mut()
643    }
644
645    fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
646        &mut self.source
647    }
648
649    fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
650        self.local.deploy_mut()
651    }
652
653    fn format(&self) -> &Option<RockspecFormat> {
654        self.local.format()
655    }
656
657    fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
658        Ok(self.local.raw_content.clone())
659    }
660}
661
662#[derive(Error, Debug, Diagnostic)]
663#[non_exhaustive]
664pub enum LuaVersionError {
665    #[error(
666        r#"
667The lua version {0} is not supported by {1} version {2}.
668
669HINT: If Lux has auto-detected an incompatible Lua installation,
670      use `--lua-version` to specify the Lua version to use.
671      Valid versions are: '5.1', '5.2', '5.3', '5.4', '5.5', 'jit', 'jit52' and 'luau'.
672"#
673    )]
674    LuaVersionUnsupported(LuaVersion, package::Name, package::Version),
675    #[error(transparent)]
676    #[diagnostic(transparent)]
677    LuaVersionUnset(#[from] LuaVersionUnset),
678}
679
680impl HasIntegrity for RemoteLuaRockspec {
681    async fn hash(&self) -> io::Result<Integrity> {
682        Ok(Integrity::from(&self.local.raw_content))
683    }
684}
685
686/// A rock's metadata, to be displayed on the remote package server
687#[derive(Clone, Deserialize, Debug, PartialEq, Default, lux_macros::DisplayAsLuaKV)]
688#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
689#[display_lua(key = "description")]
690pub struct RockDescription {
691    /// A one-line description of the package.
692    pub summary: Option<String>,
693    /// A longer description of the package.
694    pub detailed: Option<String>,
695    /// The license used by the package.
696    pub license: Option<String>,
697    /// An URL for the project. This is not the URL for the tarball, but the address of a website.
698    #[serde(default, deserialize_with = "deserialize_url")]
699    #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
700    pub homepage: Option<Url>,
701    /// An URL for the project's issue tracker.
702    #[serde(default, deserialize_with = "deserialize_url")]
703    #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
704    pub issues_url: Option<Url>,
705    /// Contact information for the rockspec maintainer.
706    pub maintainer: Option<String>,
707    /// A list of short strings that specify labels for categorization of this rock.
708    #[serde(default)]
709    pub labels: Vec<String>,
710}
711
712fn find_key_span(rockspec_content: &str, key: &str) -> SourceSpan {
713    rockspec_content
714        .match_indices(key)
715        .find(|&(start, _)| {
716            let end = start + key.len();
717            let prefix_ok =
718                start == 0 || !rockspec_content.as_bytes()[start - 1].is_ascii_alphanumeric();
719            let suffix_ok = rockspec_content
720                .get(end..)
721                .is_some_and(|s| s.starts_with('=') || s.trim_start().starts_with('='));
722            prefix_ok && suffix_ok
723        })
724        .map(|(start, _)| (start..start + key.len()).into())
725        .unwrap_or((0..0).into())
726}
727
728fn find_value_span(rockspec_content: &str, key: &str) -> SourceSpan {
729    let key = find_key_span(rockspec_content, key);
730    if key.is_empty() {
731        return key;
732    }
733    let key_offset = key.offset();
734    let key_end = key_offset + key.len();
735    let eq_start = key_end + rockspec_content[key_end..].find('=').unwrap_or(0);
736    let value_offset = eq_start + 1;
737    let line_end = rockspec_content[value_offset..]
738        .find('\n')
739        .map(|i| value_offset + i)
740        .unwrap_or(rockspec_content.len());
741    let trimmed = &rockspec_content[value_offset..line_end];
742    let start = value_offset + (trimmed.len() - trimmed.trim_start().len());
743    let end = line_end - (trimmed.len() - trimmed.trim_end().len());
744    (start..end).into()
745}
746
747fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
748where
749    D: serde::Deserializer<'de>,
750{
751    let s = Option::<String>::deserialize(deserializer)?;
752    s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
753        .transpose()
754}
755
756#[derive(Error, Debug, Diagnostic)]
757#[non_exhaustive]
758#[error("invalid rockspec format")]
759pub struct InvalidRockspecFormat(String);
760
761#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
762#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
763pub enum RockspecFormat {
764    #[serde(rename = "1.0")]
765    _1_0,
766    #[serde(rename = "2.0")]
767    _2_0,
768    #[serde(rename = "3.0")]
769    #[default]
770    _3_0,
771}
772
773impl FromStr for RockspecFormat {
774    type Err = InvalidRockspecFormat;
775
776    fn from_str(s: &str) -> Result<Self, Self::Err> {
777        match s {
778            "1.0" => Ok(Self::_1_0),
779            "2.0" => Ok(Self::_2_0),
780            "3.0" => Ok(Self::_3_0),
781            txt => Err(InvalidRockspecFormat(txt.to_string())),
782        }
783    }
784}
785
786impl Display for RockspecFormat {
787    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
788        match self {
789            Self::_1_0 => write!(f, "1.0"),
790            Self::_2_0 => write!(f, "2.0"),
791            Self::_3_0 => write!(f, "3.0"),
792        }
793    }
794}
795
796fn parse_lua_tbl_or_default<T>(
797    ctx: ottavino::Context<'_>,
798    field: &str,
799) -> Result<T, ParseLuaRockspecFieldError>
800where
801    T: Default,
802    T: DeserializeOwned,
803{
804    let ret = match ctx.globals().get_value(ctx, field.to_string()) {
805        ottavino::Value::Nil => T::default(),
806        value @ ottavino::Value::Table(_) => {
807            from_value(value).map_err(|source| ParseLuaRockspecFieldError::DeserializeTable {
808                field: field.to_string(),
809                source: Box::new(source),
810            })?
811        }
812        value => Err(ParseLuaRockspecFieldError::NotATable {
813            field: field.to_string(),
814            invalid_type: value.type_name().to_string(),
815        })?,
816    };
817    Ok(ret)
818}
819
820#[cfg(test)]
821mod tests {
822    use std::path::PathBuf;
823
824    use crate::git::GitSource;
825    use crate::lua_rockspec::PlatformIdentifier;
826
827    use super::*;
828
829    #[test]
830    pub fn parse_rockspec() {
831        let rockspec_content = "
832        rockspec_format = '1.0'\n
833        package = 'foo'\n
834        version = '1.0.0-1'\n
835        source = {\n
836            url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
837        }\n
838        "
839        .to_string();
840        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
841        assert_eq!(rockspec.local.rockspec_format, Some(RockspecFormat::_1_0));
842        assert_eq!(rockspec.local.package, "foo".into());
843        assert_eq!(rockspec.local.version, "1.0.0-1".parse().unwrap());
844        assert_eq!(rockspec.local.description, RockDescription::default());
845
846        let rockspec_content = "
847        package = 'bar'\n
848        version = '2.0.0-1'\n
849        description = {}\n
850        source = {\n
851            url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
852        }\n
853        "
854        .to_string();
855        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
856        assert_eq!(rockspec.local.rockspec_format, None);
857        assert_eq!(rockspec.local.package, "bar".into());
858        assert_eq!(rockspec.local.version, "2.0.0-1".parse().unwrap());
859        assert_eq!(rockspec.local.description, RockDescription::default());
860
861        let rockspec_content = "
862        package = 'rocks.nvim'\n
863        version = '3.0.0-1'\n
864        description = {\n
865            summary = 'some summary',
866            detailed = 'some detailed description',
867            license = 'MIT',
868            homepage = 'https://github.com/lumen-oss/rocks.nvim',
869            issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
870            maintainer = 'Lumen Labs',
871        }\n
872        source = {\n
873            url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
874        }\n
875        "
876        .to_string();
877        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
878        assert_eq!(rockspec.local.rockspec_format, None);
879        assert_eq!(rockspec.local.package, "rocks.nvim".into());
880        assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
881        let expected_description = RockDescription {
882            summary: Some("some summary".into()),
883            detailed: Some("some detailed description".into()),
884            license: Some("MIT".into()),
885            homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
886            issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
887            maintainer: Some("Lumen Labs".into()),
888            labels: Vec::new(),
889        };
890        assert_eq!(rockspec.local.description, expected_description);
891
892        let rockspec_content = "
893        package = 'rocks.nvim'\n
894        version = '3.0.0-1'\n
895        description = {\n
896            summary = 'some summary',
897            detailed = 'some detailed description',
898            license = 'MIT',
899            homepage = 'https://github.com/lumen-oss/rocks.nvim',
900            issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
901            maintainer = 'Lumen Labs',
902            labels = {},
903        }\n
904        external_dependencies = { FOO = { library = 'foo' } }\n
905        source = {\n
906            url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
907        }\n
908        "
909        .to_string();
910        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
911        assert_eq!(rockspec.local.rockspec_format, None);
912        assert_eq!(rockspec.local.package, "rocks.nvim".into());
913        assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
914        let expected_description = RockDescription {
915            summary: Some("some summary".into()),
916            detailed: Some("some detailed description".into()),
917            license: Some("MIT".into()),
918            homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
919            issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
920            maintainer: Some("Lumen Labs".into()),
921            labels: Vec::new(),
922        };
923        assert_eq!(rockspec.local.description, expected_description);
924        assert_eq!(
925            *rockspec
926                .local
927                .external_dependencies
928                .default
929                .get("FOO")
930                .unwrap(),
931            ExternalDependencySpec {
932                library: Some("foo".into()),
933                header: None
934            }
935        );
936
937        let rockspec_content = "
938        package = 'rocks.nvim'\n
939        version = '3.0.0-1'\n
940        description = {\n
941            summary = 'some summary',
942            detailed = 'some detailed description',
943            license = 'MIT',
944            homepage = 'https://github.com/lumen-oss/rocks.nvim',
945            issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
946            maintainer = 'Lumen Labs',
947            labels = { 'package management', },
948        }\n
949        supported_platforms = { 'unix', '!windows' }\n
950        dependencies = { 'neorg ~> 6' }\n
951        build_dependencies = { 'foo' }\n
952        external_dependencies = { FOO = { header = 'foo.h' } }\n
953        test_dependencies = { 'busted >= 2.0.0' }\n
954        source = {\n
955            url = 'git+https://github.com/lumen-oss/rocks.nvim',\n
956            hash = 'sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=',\n
957        }\n
958        "
959        .to_string();
960        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
961        assert_eq!(rockspec.local.rockspec_format, None);
962        assert_eq!(rockspec.local.package, "rocks.nvim".into());
963        assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
964        let expected_description = RockDescription {
965            summary: Some("some summary".into()),
966            detailed: Some("some detailed description".into()),
967            license: Some("MIT".into()),
968            homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
969            issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
970            maintainer: Some("Lumen Labs".into()),
971            labels: vec!["package management".into()],
972        };
973        assert_eq!(rockspec.local.description, expected_description);
974        assert!(rockspec
975            .local
976            .supported_platforms
977            .is_supported(&PlatformIdentifier::Unix));
978        assert!(!rockspec
979            .local
980            .supported_platforms
981            .is_supported(&PlatformIdentifier::Windows));
982        let neorg = package::Spec::parse("neorg".into(), "6.0.0".into()).unwrap();
983        assert!(rockspec
984            .local
985            .dependencies
986            .default
987            .into_iter()
988            .any(|dep| dep.matches(&neorg)));
989        let foo = package::Spec::parse("foo".into(), "1.0.0".into()).unwrap();
990        assert!(rockspec
991            .local
992            .build_dependencies
993            .default
994            .into_iter()
995            .any(|dep| dep.matches(&foo)));
996        let busted = package::Spec::parse("busted".into(), "2.2.0".into()).unwrap();
997        assert_eq!(
998            *rockspec
999                .local
1000                .external_dependencies
1001                .default
1002                .get("FOO")
1003                .unwrap(),
1004            ExternalDependencySpec {
1005                header: Some("foo.h".into()),
1006                library: None
1007            }
1008        );
1009        assert!(rockspec
1010            .local
1011            .test_dependencies
1012            .default
1013            .into_iter()
1014            .any(|dep| dep.matches(&busted)));
1015
1016        let rockspec_content = "
1017        rockspec_format = '1.0'\n
1018        package = 'foo'\n
1019        version = '1.0.0-1'\n
1020        source = {\n
1021            url = 'git+https://hub.com/owner/example-project/',\n
1022            branch = 'bar',\n
1023        }\n
1024        "
1025        .to_string();
1026        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1027        assert_eq!(
1028            rockspec.local.source.default.source_spec,
1029            RockSourceSpec::Git(GitSource {
1030                url: "https://hub.com/owner/example-project/".parse().unwrap(),
1031                git_ref: Some(GitRef::Branch("bar".into()))
1032            })
1033        );
1034        assert_eq!(rockspec.local.test, PerPlatform::default());
1035        let rockspec_content = "
1036        rockspec_format = '1.0'\n
1037        package = 'foo'\n
1038        version = '1.0.0-1'\n
1039        source = {\n
1040            url = 'git+https://hub.com/owner/example-project/',\n
1041            tag = 'bar',\n
1042        }\n
1043        "
1044        .to_string();
1045        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1046        assert_eq!(
1047            rockspec.local.source.default.source_spec,
1048            RockSourceSpec::Git(GitSource {
1049                url: "https://hub.com/owner/example-project/".parse().unwrap(),
1050                git_ref: Some(GitRef::Tag("bar".into()))
1051            })
1052        );
1053        let rockspec_content = "
1054        rockspec_format = '1.0'\n
1055        package = 'foo'\n
1056        version = '1.0.0-1'\n
1057        source = {\n
1058            url = 'git+https://hub.com/owner/example-project/',\n
1059            branch = 'bar',\n
1060            tag = 'baz',\n
1061        }\n
1062        "
1063        .to_string();
1064        let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1065        let rockspec_content = "
1066        rockspec_format = '1.0'\n
1067        package = 'foo'\n
1068        version = '1.0.0-1'\n
1069        source = {\n
1070            url = 'git+https://hub.com/owner/example-project/',\n
1071            tag = 'bar',\n
1072            file = 'foo.tar.gz',\n
1073        }\n
1074        build = {\n
1075            install = {\n
1076                conf = {['foo.bar'] = 'config/bar.toml'},\n
1077            },\n
1078        }\n
1079        "
1080        .to_string();
1081        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1082        assert_eq!(
1083            rockspec.local.source.default.archive_name,
1084            Some("foo.tar.gz".into())
1085        );
1086        let foo_bar_path = rockspec
1087            .local
1088            .build
1089            .default
1090            .install
1091            .conf
1092            .get("foo.bar")
1093            .unwrap();
1094        assert_eq!(*foo_bar_path, PathBuf::from("config/bar.toml"));
1095        let rockspec_content = "
1096        rockspec_format = '1.0'\n
1097        package = 'foo'\n
1098        version = '1.0.0-1'\n
1099        source = {\n
1100            url = 'git+https://hub.com/example-project/foo.zip',\n
1101        }\n
1102        build = {\n
1103            install = {\n
1104                lua = {\n
1105                    'foo.lua',\n
1106                    ['foo.bar'] = 'src/bar.lua',\n
1107                },\n
1108                bin = {['foo.bar'] = 'bin/bar'},\n
1109            },\n
1110        }\n
1111        "
1112        .to_string();
1113        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1114        assert!(matches!(
1115            rockspec.local.build.default.build_backend,
1116            Some(BuildBackendSpec::Builtin { .. })
1117        ));
1118        let install_lua_spec = rockspec.local.build.default.install.lua;
1119        let foo_bar_path = install_lua_spec
1120            .get(&LuaModule::from_str("foo.bar").unwrap())
1121            .unwrap();
1122        assert_eq!(*foo_bar_path, PathBuf::from("src/bar.lua"));
1123        let foo_path = install_lua_spec
1124            .get(&LuaModule::from_str("foo").unwrap())
1125            .unwrap();
1126        assert_eq!(*foo_path, PathBuf::from("foo.lua"));
1127        let foo_bar_path = rockspec
1128            .local
1129            .build
1130            .default
1131            .install
1132            .bin
1133            .get("foo.bar")
1134            .unwrap();
1135        assert_eq!(*foo_bar_path, PathBuf::from("bin/bar"));
1136        let rockspec_content = "
1137        rockspec_format = '1.0'\n
1138        package = 'foo'\n
1139        version = '1.0.0-1'\n
1140        source = {\n
1141            url = 'git+https://hub.com/example-project/',\n
1142        }\n
1143        build = {\n
1144            copy_directories = { 'lua' },\n
1145        }\n
1146        "
1147        .to_string();
1148        let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1149        let rockspec_content = "
1150        rockspec_format = '1.0'\n
1151        package = 'foo'\n
1152        version = '1.0.0-1'\n
1153        source = {\n
1154            url = 'git+https://hub.com/example-project/',\n
1155        }\n
1156        build = {\n
1157            copy_directories = { 'lib' },\n
1158        }\n
1159        "
1160        .to_string();
1161        let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1162        let rockspec_content = "
1163        rockspec_format = '1.0'\n
1164        package = 'foo'\n
1165        version = '1.0.0-1'\n
1166        source = {\n
1167            url = 'git+https://hub.com/example-project/',\n
1168        }\n
1169        build = {\n
1170            copy_directories = { 'rock_manifest' },\n
1171        }\n
1172        "
1173        .to_string();
1174        let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1175        let rockspec_content = "
1176        rockspec_format = '1.0'\n
1177        package = 'foo'\n
1178        version = '1.0.0-1'\n
1179        source = {\n
1180            url = 'git+https://hub.com/example-project/foo.zip',\n
1181            dir = 'baz',\n
1182        }\n
1183        build = {\n
1184            type = 'make',\n
1185            install = {\n
1186                lib = {['foo.so'] = 'lib/bar.so'},\n
1187            },\n
1188            copy_directories = {\n
1189                'plugin',\n
1190                'ftplugin',\n
1191            },\n
1192            patches = {\n
1193                ['lua51-support.diff'] = [[\n
1194                    --- before.c\n
1195                    +++ path/to/after.c\n
1196                ]],\n
1197            },\n
1198        }\n
1199        "
1200        .to_string();
1201        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1202        assert_eq!(rockspec.local.source.default.unpack_dir, Some("baz".into()));
1203        assert_eq!(
1204            rockspec.local.build.default.build_backend,
1205            Some(BuildBackendSpec::Make(MakeBuildSpec::default()))
1206        );
1207        let foo_bar_path = rockspec
1208            .local
1209            .build
1210            .default
1211            .install
1212            .lib
1213            .get("foo.so")
1214            .unwrap();
1215        assert_eq!(*foo_bar_path, PathBuf::from("lib/bar.so"));
1216        let copy_directories = rockspec.local.build.default.copy_directories;
1217        assert_eq!(
1218            copy_directories,
1219            vec![PathBuf::from("plugin"), PathBuf::from("ftplugin")]
1220        );
1221        let patches = rockspec.local.build.default.patches;
1222        let _patch = patches.get(&PathBuf::from("lua51-support.diff")).unwrap();
1223        let rockspec_content = "
1224        rockspec_format = '1.0'\n
1225        package = 'foo'\n
1226        version = '1.0.0-1'\n
1227        source = {\n
1228            url = 'git+https://hub.com/example-project/foo.zip',\n
1229        }\n
1230        build = {\n
1231            type = 'cmake',\n
1232        }\n
1233        "
1234        .to_string();
1235        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1236        assert_eq!(
1237            rockspec.local.build.default.build_backend,
1238            Some(BuildBackendSpec::CMake(CMakeBuildSpec::default()))
1239        );
1240        let rockspec_content = "
1241        rockspec_format = '1.0'\n
1242        package = 'foo'\n
1243        version = '1.0.0-1'\n
1244        source = {\n
1245            url = 'git+https://hub.com/example-project/foo.zip',\n
1246        }\n
1247        build = {\n
1248            type = 'command',\n
1249            build_command = 'foo',\n
1250            install_command = 'bar',\n
1251        }\n
1252        "
1253        .to_string();
1254        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1255        assert!(matches!(
1256            rockspec.local.build.default.build_backend,
1257            Some(BuildBackendSpec::Command(CommandBuildSpec { .. }))
1258        ));
1259        let rockspec_content = "
1260        rockspec_format = '1.0'\n
1261        package = 'foo'\n
1262        version = '1.0.0-1'\n
1263        source = {\n
1264            url = 'git+https://hub.com/example-project/foo.zip',\n
1265        }\n
1266        build = {\n
1267            type = 'command',\n
1268            install_command = 'foo',\n
1269        }\n
1270        "
1271        .to_string();
1272        RemoteLuaRockspec::new(&rockspec_content).unwrap();
1273        let rockspec_content = "
1274        rockspec_format = '1.0'\n
1275        package = 'foo'\n
1276        version = '1.0.0-1'\n
1277        source = {\n
1278            url = 'git+https://hub.com/example-project/foo.zip',\n
1279        }\n
1280        build = {\n
1281            type = 'command',\n
1282            build_command = 'foo',\n
1283        }\n
1284        "
1285        .to_string();
1286        RemoteLuaRockspec::new(&rockspec_content).unwrap();
1287        // platform overrides
1288        let rockspec_content = "
1289        package = 'rocks'\n
1290        version = '3.0.0-1'\n
1291        dependencies = {\n
1292          'neorg ~> 6',\n
1293          'toml-edit ~> 1',\n
1294          platforms = {\n
1295            windows = {\n
1296              'neorg = 5.0.0',\n
1297              'toml = 1.0.0',\n
1298            },\n
1299            unix = {\n
1300              'neorg = 5.0.0',\n
1301            },\n
1302            linux = {\n
1303              'toml = 1.0.0',\n
1304            },\n
1305          },\n
1306        }\n
1307        source = {\n
1308            url = 'git+https://github.com/lumen-oss/rocks.nvim',\n
1309            hash = 'sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=',\n
1310        }\n
1311        "
1312        .to_string();
1313        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1314        let neorg_override = package::Spec::parse("neorg".into(), "5.0.0".into()).unwrap();
1315        let toml_edit = package::Spec::parse("toml-edit".into(), "1.0.0".into()).unwrap();
1316        let toml = package::Spec::parse("toml".into(), "1.0.0".into()).unwrap();
1317        assert_eq!(rockspec.local.dependencies.default.len(), 2);
1318        let per_platform = &rockspec.local.dependencies.per_platform;
1319        assert_eq!(
1320            per_platform
1321                .get(&PlatformIdentifier::Windows)
1322                .unwrap()
1323                .iter()
1324                .filter(|dep| dep.matches(&neorg_override)
1325                    || dep.matches(&toml_edit)
1326                    || dep.matches(&toml))
1327                .count(),
1328            3
1329        );
1330        assert_eq!(
1331            per_platform
1332                .get(&PlatformIdentifier::Unix)
1333                .unwrap()
1334                .iter()
1335                .filter(|dep| dep.matches(&neorg_override)
1336                    || dep.matches(&toml_edit)
1337                    || dep.matches(&toml))
1338                .count(),
1339            2
1340        );
1341        assert_eq!(
1342            per_platform
1343                .get(&PlatformIdentifier::Linux)
1344                .unwrap()
1345                .iter()
1346                .filter(|dep| dep.matches(&neorg_override)
1347                    || dep.matches(&toml_edit)
1348                    || dep.matches(&toml))
1349                .count(),
1350            3
1351        );
1352        let rockspec_content = "
1353        package = 'rocks'\n
1354        version = '3.0.0-1'\n
1355        external_dependencies = {\n
1356            FOO = { library = 'foo' },\n
1357            platforms = {\n
1358              windows = {\n
1359                FOO = { library = 'foo.dll' },\n
1360              },\n
1361              unix = {\n
1362                BAR = { header = 'bar.h' },\n
1363              },\n
1364              linux = {\n
1365                FOO = { library = 'foo.so' },\n
1366              },\n
1367            },\n
1368        }\n
1369        source = {\n
1370            url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1371        }\n
1372        "
1373        .to_string();
1374        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1375        assert_eq!(
1376            *rockspec
1377                .local
1378                .external_dependencies
1379                .default
1380                .get("FOO")
1381                .unwrap(),
1382            ExternalDependencySpec {
1383                library: Some("foo".into()),
1384                header: None
1385            }
1386        );
1387        let per_platform = rockspec.local.external_dependencies.per_platform;
1388        assert_eq!(
1389            *per_platform
1390                .get(&PlatformIdentifier::Windows)
1391                .and_then(|it| it.get("FOO"))
1392                .unwrap(),
1393            ExternalDependencySpec {
1394                library: Some("foo.dll".into()),
1395                header: None
1396            }
1397        );
1398        assert_eq!(
1399            *per_platform
1400                .get(&PlatformIdentifier::Unix)
1401                .and_then(|it| it.get("FOO"))
1402                .unwrap(),
1403            ExternalDependencySpec {
1404                library: Some("foo".into()),
1405                header: None
1406            }
1407        );
1408        assert_eq!(
1409            *per_platform
1410                .get(&PlatformIdentifier::Unix)
1411                .and_then(|it| it.get("BAR"))
1412                .unwrap(),
1413            ExternalDependencySpec {
1414                header: Some("bar.h".into()),
1415                library: None
1416            }
1417        );
1418        assert_eq!(
1419            *per_platform
1420                .get(&PlatformIdentifier::Linux)
1421                .and_then(|it| it.get("BAR"))
1422                .unwrap(),
1423            ExternalDependencySpec {
1424                header: Some("bar.h".into()),
1425                library: None
1426            }
1427        );
1428        assert_eq!(
1429            *per_platform
1430                .get(&PlatformIdentifier::Linux)
1431                .and_then(|it| it.get("FOO"))
1432                .unwrap(),
1433            ExternalDependencySpec {
1434                library: Some("foo.so".into()),
1435                header: None
1436            }
1437        );
1438        let rockspec_content = "
1439        rockspec_format = '1.0'\n
1440        package = 'foo'\n
1441        version = '1.0.0-1'\n
1442        source = {\n
1443            url = 'git+https://hub.com/example-project/.git',\n
1444            branch = 'bar',\n
1445            platforms = {\n
1446                macosx = {\n
1447                    branch = 'mac',\n
1448                },\n
1449                windows = {\n
1450                    url = 'git+https://winhub.com/example-project/.git',\n
1451                    branch = 'win',\n
1452                },\n
1453            },\n
1454        }\n
1455        "
1456        .to_string();
1457        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1458        assert_eq!(
1459            rockspec.local.source.default.source_spec,
1460            RockSourceSpec::Git(GitSource {
1461                url: "https://hub.com/example-project/.git".parse().unwrap(),
1462                git_ref: Some(GitRef::Branch("bar".into()))
1463            })
1464        );
1465        assert_eq!(
1466            rockspec
1467                .source
1468                .per_platform
1469                .get(&PlatformIdentifier::MacOSX)
1470                .map(|it| it.source_spec.clone())
1471                .unwrap(),
1472            RockSourceSpec::Git(GitSource {
1473                url: "https://hub.com/example-project/.git".parse().unwrap(),
1474                git_ref: Some(GitRef::Branch("mac".into()))
1475            })
1476        );
1477        assert_eq!(
1478            rockspec
1479                .source
1480                .per_platform
1481                .get(&PlatformIdentifier::Windows)
1482                .map(|it| it.source_spec.clone())
1483                .unwrap(),
1484            RockSourceSpec::Git(GitSource {
1485                url: "https://winhub.com/example-project/.git".parse().unwrap(),
1486                git_ref: Some(GitRef::Branch("win".into()))
1487            })
1488        );
1489        let rockspec_content = "
1490        rockspec_format = '1.0'\n
1491        package = 'foo'\n
1492        version = '1.0.0-1'\n
1493        source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1494        build = {\n
1495            type = 'make',\n
1496            install = {\n
1497                lib = {['foo.bar'] = 'lib/bar.so'},\n
1498            },\n
1499            copy_directories = { 'plugin' },\n
1500            platforms = {\n
1501                unix = {\n
1502                    copy_directories = { 'ftplugin' },\n
1503                },\n
1504                linux = {\n
1505                    copy_directories = { 'foo' },\n
1506                },\n
1507            },\n
1508        }\n
1509        "
1510        .to_string();
1511        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1512        let per_platform = rockspec.local.build.per_platform;
1513        let unix = per_platform.get(&PlatformIdentifier::Unix).unwrap();
1514        assert_eq!(
1515            unix.copy_directories,
1516            vec![PathBuf::from("plugin"), PathBuf::from("ftplugin")]
1517        );
1518        let linux = per_platform.get(&PlatformIdentifier::Linux).unwrap();
1519        assert_eq!(
1520            linux.copy_directories,
1521            vec![
1522                PathBuf::from("plugin"),
1523                PathBuf::from("foo"),
1524                PathBuf::from("ftplugin")
1525            ]
1526        );
1527        let rockspec_content = "
1528        package = 'foo'\n
1529        version = '1.0.0-1'\n
1530        source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1531        build = {\n
1532            type = 'builtin',\n
1533            modules = {\n
1534                cjson = {\n
1535                    sources = { 'lua_cjson.c', 'strbuf.c', 'fpconv.c' },\n
1536                }\n
1537            },\n
1538            platforms = {\n
1539                win32 = { modules = { cjson = { defines = {\n
1540                    'DISABLE_INVALID_NUMBERS', 'USE_INTERNAL_ISINF'\n
1541                } } } }\n
1542            },\n
1543        }\n
1544        "
1545        .to_string();
1546        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1547        let win32 = rockspec.local.build.get(&PlatformIdentifier::Windows);
1548        assert_eq!(
1549            win32.build_backend,
1550            Some(BuildBackendSpec::Builtin(BuiltinBuildSpec {
1551                modules: vec![(
1552                    LuaModule::from_str("cjson").unwrap(),
1553                    ModuleSpec::ModulePaths(ModulePaths {
1554                        sources: vec!["lua_cjson.c".into(), "strbuf.c".into(), "fpconv.c".into()],
1555                        libraries: Vec::default(),
1556                        defines: vec![
1557                            ("DISABLE_INVALID_NUMBERS".into(), None),
1558                            ("USE_INTERNAL_ISINF".into(), None)
1559                        ],
1560                        incdirs: Vec::default(),
1561                        libdirs: Vec::default(),
1562                    })
1563                )]
1564                .into_iter()
1565                .collect()
1566            }))
1567        );
1568        let rockspec_content = "
1569        rockspec_format = '1.0'\n
1570        package = 'foo'\n
1571        version = '1.0.0-1'\n
1572        deploy = {\n
1573            wrap_bin_scripts = false,\n
1574        }\n
1575        source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1576        ";
1577        let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1578        let deploy_spec = &rockspec.deploy().current_platform();
1579        assert!(!deploy_spec.wrap_bin_scripts);
1580    }
1581
1582    #[test]
1583    pub fn parse_scm_rockspec() {
1584        let rockspec_content = "
1585        package = 'foo'\n
1586        version = 'scm-1'\n
1587        source = {\n
1588            url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1589        }\n
1590        "
1591        .to_string();
1592        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1593        assert_eq!(rockspec.local.package, "foo".into());
1594        assert_eq!(rockspec.local.version, "scm-1".parse().unwrap());
1595    }
1596
1597    #[test]
1598    pub fn regression_luasystem() {
1599        let rockspec_content = String::from_utf8(
1600            std::fs::read(
1601                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1602                    .join("resources/test/luasystem-0.4.4-1.rockspec"),
1603            )
1604            .unwrap(),
1605        )
1606        .unwrap();
1607        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1608        let build_spec = rockspec.local.build.current_platform();
1609        assert!(matches!(
1610            build_spec.build_backend,
1611            Some(BuildBackendSpec::Builtin { .. })
1612        ));
1613        if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) =
1614            &build_spec.build_backend
1615        {
1616            assert_eq!(
1617                modules.get(&LuaModule::from_str("system.init").unwrap()),
1618                Some(&ModuleSpec::SourcePath("system/init.lua".into()))
1619            );
1620            assert_eq!(
1621                modules.get(&LuaModule::from_str("system.core").unwrap()),
1622                Some(&ModuleSpec::ModulePaths(ModulePaths {
1623                    sources: vec![
1624                        "src/core.c".into(),
1625                        "src/compat.c".into(),
1626                        "src/time.c".into(),
1627                        "src/environment.c".into(),
1628                        "src/random.c".into(),
1629                        "src/term.c".into(),
1630                        "src/bitflags.c".into(),
1631                        "src/wcwidth.c".into(),
1632                    ],
1633                    defines: luasystem_expected_defines(),
1634                    libraries: luasystem_expected_libraries(),
1635                    incdirs: luasystem_expected_incdirs(),
1636                    libdirs: luasystem_expected_libdirs(),
1637                }))
1638            );
1639        }
1640        if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) = &rockspec
1641            .local
1642            .build
1643            .get(&PlatformIdentifier::Windows)
1644            .build_backend
1645        {
1646            if let ModuleSpec::ModulePaths(paths) = modules
1647                .get(&LuaModule::from_str("system.core").unwrap())
1648                .unwrap()
1649            {
1650                assert_eq!(paths.libraries, luasystem_expected_windows_libraries());
1651            };
1652        }
1653        if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) = &rockspec
1654            .local
1655            .build
1656            .get(&PlatformIdentifier::Win32)
1657            .build_backend
1658        {
1659            if let ModuleSpec::ModulePaths(paths) = modules
1660                .get(&LuaModule::from_str("system.core").unwrap())
1661                .unwrap()
1662            {
1663                assert_eq!(paths.libraries, luasystem_expected_windows_libraries());
1664            };
1665        }
1666    }
1667
1668    fn luasystem_expected_defines() -> Vec<(String, Option<String>)> {
1669        if cfg!(target_os = "windows") {
1670            vec![
1671                ("WINVER".into(), Some("0x0600".into())),
1672                ("_WIN32_WINNT".into(), Some("0x0600".into())),
1673            ]
1674        } else {
1675            Vec::default()
1676        }
1677    }
1678
1679    fn luasystem_expected_windows_libraries() -> Vec<PathBuf> {
1680        vec!["advapi32".into(), "winmm".into()]
1681    }
1682    fn luasystem_expected_libraries() -> Vec<PathBuf> {
1683        if cfg!(any(target_os = "linux", target_os = "android")) {
1684            vec!["rt".into()]
1685        } else if cfg!(target_os = "windows") {
1686            luasystem_expected_windows_libraries()
1687        } else {
1688            Vec::default()
1689        }
1690    }
1691
1692    fn luasystem_expected_incdirs() -> Vec<PathBuf> {
1693        Vec::default()
1694    }
1695
1696    fn luasystem_expected_libdirs() -> Vec<PathBuf> {
1697        Vec::default()
1698    }
1699
1700    #[test]
1701    pub fn rust_mlua_rockspec() {
1702        let rockspec_content = "
1703    package = 'foo'\n
1704    version = 'scm-1'\n
1705    source = {\n
1706        url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1707    }\n
1708    build = {
1709        type = 'rust-mlua',
1710        modules = {
1711            'foo',
1712            bar = 'baz',
1713        },
1714        target_path = 'path/to/cargo/target/directory',
1715        default_features = false,
1716        include = {
1717            'file.lua',
1718            ['path/to/another/file.lua'] = 'another-file.lua',
1719        },
1720        features = {'extra', 'features'},
1721    }
1722            ";
1723        let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1724        let build_spec = rockspec.local.build.current_platform();
1725        if let Some(BuildBackendSpec::RustMlua(build_spec)) = build_spec.build_backend.to_owned() {
1726            assert_eq!(
1727                build_spec.modules.get("foo").unwrap(),
1728                &PathBuf::from(format!("libfoo.{}", std::env::consts::DLL_EXTENSION))
1729            );
1730            assert_eq!(
1731                build_spec.modules.get("bar").unwrap(),
1732                &PathBuf::from(format!("libbaz.{}", std::env::consts::DLL_EXTENSION))
1733            );
1734            assert_eq!(
1735                build_spec.include.get(&PathBuf::from("file.lua")).unwrap(),
1736                &PathBuf::from("file.lua")
1737            );
1738            assert_eq!(
1739                build_spec
1740                    .include
1741                    .get(&PathBuf::from("path/to/another/file.lua"))
1742                    .unwrap(),
1743                &PathBuf::from("another-file.lua")
1744            );
1745        } else {
1746            panic!("Expected RustMlua build backend");
1747        }
1748    }
1749
1750    #[test]
1751    pub fn rust_binary_rockspec() {
1752        let rockspec_content = "
1753    package = 'foo'\n
1754    version = 'scm-1'\n
1755    source = {\n
1756        url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1757    }\n
1758    build = {\n
1759        type = 'rust-binary',\n
1760        package = 'foo',\n
1761        features = {'extra', 'features'},\n
1762    }\n
1763            ";
1764        let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1765        let build_spec = rockspec.local.build.current_platform();
1766        if let Some(BuildBackendSpec::RustBinary(build_spec)) = build_spec.build_backend.to_owned()
1767        {
1768            assert_eq!(build_spec.package, Some("foo".to_string()));
1769            assert_eq!(build_spec.features, vec!["extra", "features"]);
1770        } else {
1771            panic!("Expected RustBinary build backend");
1772        }
1773    }
1774
1775    #[tokio::test]
1776    pub async fn regression_ltui() {
1777        let content = String::from_utf8(
1778            std::fs::read(
1779                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1780                    .join("resources/test/ltui-2.8-2.rockspec"),
1781            )
1782            .unwrap(),
1783        )
1784        .unwrap();
1785        RemoteLuaRockspec::new(&content).unwrap();
1786    }
1787
1788    // Luarocks allows the `install.bin` field to be a list, even though it
1789    // should only allow a table.
1790    #[test]
1791    pub fn regression_off_spec_install_binaries() {
1792        let rockspec_content = r#"
1793            package = "WSAPI"
1794            version = "1.7-1"
1795
1796            source = {
1797              url = "git://github.com/keplerproject/wsapi",
1798              tag = "v1.7",
1799            }
1800
1801            build = {
1802              type = "builtin",
1803              modules = {
1804                ["wsapi"] = "src/wsapi.lua",
1805              },
1806              -- Offending Line
1807              install = { bin = { "src/launcher/wsapi.cgi" } }
1808            }
1809        "#;
1810
1811        let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1812
1813        assert_eq!(
1814            rockspec.build().current_platform().install.bin,
1815            HashMap::from([("wsapi.cgi".into(), PathBuf::from("src/launcher/wsapi.cgi"))])
1816        );
1817    }
1818
1819    #[test]
1820    pub fn regression_external_dependencies() {
1821        let content = String::from_utf8(
1822            std::fs::read(
1823                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1824                    .join("resources/test/luaossl-20220711-0.rockspec"),
1825            )
1826            .unwrap(),
1827        )
1828        .unwrap();
1829        let rockspec = RemoteLuaRockspec::new(&content).unwrap();
1830        if cfg!(target_family = "unix") {
1831            assert_eq!(
1832                rockspec
1833                    .local
1834                    .external_dependencies
1835                    .current_platform()
1836                    .get("OPENSSL")
1837                    .unwrap(),
1838                &ExternalDependencySpec {
1839                    library: Some("ssl".into()),
1840                    header: Some("openssl/ssl.h".into()),
1841                }
1842            );
1843        }
1844        let per_platform = rockspec.local.external_dependencies.per_platform;
1845        assert_eq!(
1846            *per_platform
1847                .get(&PlatformIdentifier::Windows)
1848                .and_then(|it| it.get("OPENSSL"))
1849                .unwrap(),
1850            ExternalDependencySpec {
1851                library: Some("libeay32".into()),
1852                header: Some("openssl/ssl.h".into()),
1853            }
1854        );
1855    }
1856
1857    #[test]
1858    pub fn remote_lua_rockspec_from_package_and_source_spec() {
1859        let package_req = "foo@1.0.5".parse().unwrap();
1860        let source = GitSource {
1861            url: "https://hub.com/owner/example-project.git".parse().unwrap(),
1862            git_ref: Some(GitRef::Tag("1.0.5".into())),
1863        };
1864        let source_spec = RockSourceSpec::Git(source);
1865        let rockspec = RemoteLuaRockspec::from_source_spec(
1866            package_req,
1867            source_spec.clone(),
1868            Vec::new(),
1869            HashMap::new(),
1870        );
1871        let generated_rockspec_str = rockspec.local.raw_content;
1872        let rockspec2 = RemoteLuaRockspec::new(&generated_rockspec_str).unwrap();
1873        assert_eq!(rockspec2.local.package, "foo".into());
1874        assert_eq!(rockspec2.local.version, "1.0.5".parse().unwrap());
1875        assert_eq!(rockspec2.local.source, PerPlatform::new(source_spec.into()));
1876    }
1877
1878    #[test]
1879    pub fn regression_complex_source_field() {
1880        let rockspec_content = r#"
1881            package = "say"
1882            local rock_version = "1.4.1"
1883            local rock_release = "3"
1884            local namespace = "lunarmodules"
1885            local repository = package
1886
1887            version = ("%s-%s"):format(rock_version, rock_release)
1888
1889            source = {
1890              url = ("git+https://github.com/%s/%s.git"):format(namespace, repository),
1891              branch = rock_version == "scm" and "master" or nil,
1892              tag = rock_version ~= "scm" and "v"..rock_version or nil,
1893            }
1894
1895            description = {
1896              summary = "Lua string hashing/indexing library",
1897            }
1898
1899            dependencies = {
1900              "lua >= 5.1",
1901            }
1902
1903            build = {
1904              type = "builtin",
1905            }
1906        "#
1907        .to_string();
1908        RemoteLuaRockspec::new(&rockspec_content).unwrap();
1909    }
1910
1911    fn eval_lua_global<T: serde::de::DeserializeOwned>(code: &str, key: &'static str) -> T {
1912        use ottavino::{Closure, Executor, Fuel, Lua};
1913        use ottavino_util::serde::from_value;
1914        Lua::core()
1915            .try_enter(|ctx| {
1916                let closure = Closure::load(ctx, None, code.as_bytes())?;
1917                let executor = Executor::start(ctx, closure.into(), ());
1918                executor.step(ctx, &mut Fuel::with(i32::MAX))?;
1919                from_value(ctx.globals().get_value(ctx, key)).map_err(ottavino::Error::from)
1920            })
1921            .unwrap()
1922    }
1923
1924    #[test]
1925    pub fn rock_description_roundtrip() {
1926        let desc = RockDescription {
1927            summary: Some("A great package".into()),
1928            detailed: Some("Detailed description here".into()),
1929            license: Some("MIT".into()),
1930            homepage: Some("https://example.com".parse().unwrap()),
1931            issues_url: Some("https://example.com/issues".parse().unwrap()),
1932            maintainer: Some("Some Maintainer <maintainer@example.com>".into()),
1933            labels: vec!["neovim".into(), "lua".into()],
1934        };
1935        let lua = desc.display_lua().to_string();
1936        let restored: RockDescription = eval_lua_global(&lua, "description");
1937        assert_eq!(desc, restored);
1938    }
1939
1940    #[test]
1941    pub fn rock_description_empty_roundtrip() {
1942        let desc = RockDescription::default();
1943        let lua = desc.display_lua().to_string();
1944        // An empty description produces an empty assignment; restore from empty table.
1945        let lua = if lua.trim().is_empty() {
1946            "description = {}".to_string()
1947        } else {
1948            lua
1949        };
1950        let restored: RockDescription = eval_lua_global(&lua, "description");
1951        assert_eq!(desc, restored);
1952    }
1953
1954    #[test]
1955    pub fn regression_luasec() {
1956        let rockspec_content = r#"
1957package = "LuaSec"
1958version = "1.3.2-1"
1959source = {
1960  url = "git+https://github.com/brunoos/luasec",
1961  tag = "v1.3.2",
1962}
1963external_dependencies = {
1964   platforms = {
1965      unix = {
1966         OPENSSL = {
1967            header = "openssl/ssl.h",
1968            library = "ssl"
1969         }
1970      },
1971      windows = {
1972         OPENSSL = {
1973            header = "openssl/ssl.h",
1974         }
1975      },
1976   }
1977}
1978"#;
1979        let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1980        let linux_external_deps = rockspec
1981            .external_dependencies()
1982            .get(&PlatformIdentifier::Linux);
1983        assert!(linux_external_deps.get("OPENSSL").is_some());
1984        let windows_external_deps = rockspec
1985            .external_dependencies()
1986            .get(&PlatformIdentifier::Windows);
1987        assert!(windows_external_deps.get("OPENSSL").is_some());
1988    }
1989
1990    #[test]
1991    pub fn find_value_span_spaces() {
1992        let source = "package = 'foo'\nversion = '1.0'";
1993        let span = find_value_span(source, "package");
1994        assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
1995    }
1996
1997    #[test]
1998    pub fn find_value_span_no_spaces() {
1999        let source = "package='foo'";
2000        let span = find_value_span(source, "package");
2001        assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
2002    }
2003
2004    #[test]
2005    pub fn find_value_span_returns_empty_when_not_found() {
2006        let source = "version = '1.0'";
2007        let span = find_value_span(source, "package");
2008        assert_eq!(span.len(), 0);
2009    }
2010
2011    #[test]
2012    pub fn find_value_span_skips_url_containing_field() {
2013        let source = "homepage = 'https://luapackager.com'\npackage = 'foo'";
2014        let span = find_value_span(source, "package");
2015        assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
2016    }
2017
2018    #[test]
2019    pub fn find_value_span_double_quoted() {
2020        let source = r#"package = "foo""#;
2021        let span = find_value_span(source, "package");
2022        assert_eq!(
2023            &source[span.offset()..span.offset() + span.len()],
2024            r#""foo""#
2025        );
2026    }
2027
2028    #[test]
2029    pub fn find_value_span_table() {
2030        let source = "description = {\n    summary = 'bar'\n}";
2031        let span = find_value_span(source, "description");
2032        assert_eq!(&source[span.offset()..span.offset() + span.len()], "{");
2033    }
2034
2035    #[test]
2036    pub fn parse_lua_rockspec_field_error() {
2037        let rockspec_content = "package = 'my-lua-lib'\nfoo = 123";
2038        let extern_err = ottavino::Lua::core()
2039            .try_enter(|_ctx| {
2040                Err(ParseLuaRockspecFieldError::Deserialize {
2041                    field: "foo".into(),
2042                    source: Box::new(ottavino_util::serde::de::Error::Message(
2043                        "expected bar".into(),
2044                    )),
2045                })?;
2046                Ok(())
2047            })
2048            .unwrap_err();
2049        let err = Error::from_ottavino(extern_err, rockspec_content);
2050        match err {
2051            Error::ExecutionError {
2052                cause,
2053                err_span: Some(span),
2054                ..
2055            } => {
2056                assert!(cause.contains("invalid rockspec field 'foo'"));
2057                assert_eq!(
2058                    &rockspec_content[span.offset()..span.offset() + span.len()],
2059                    "foo"
2060                );
2061            }
2062            other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2063        }
2064        let extern_err = ottavino::Lua::core()
2065            .try_enter(|_ctx| {
2066                Err(ParseLuaRockspecFieldError::DeserializeTable {
2067                    field: "foo".into(),
2068                    source: Box::new(ottavino_util::serde::de::Error::Message(
2069                        "expected bar".into(),
2070                    )),
2071                })?;
2072                Ok(())
2073            })
2074            .unwrap_err();
2075        let err = Error::from_ottavino(extern_err, rockspec_content);
2076        match err {
2077            Error::ExecutionError {
2078                cause,
2079                err_span: Some(span),
2080                ..
2081            } => {
2082                assert!(cause.contains("invalid rockspec field 'foo'"));
2083                assert_eq!(
2084                    &rockspec_content[span.offset()..span.offset() + span.len()],
2085                    "foo"
2086                );
2087            }
2088            other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2089        }
2090        let extern_err = ottavino::Lua::core()
2091            .try_enter(|_ctx| {
2092                Err(ParseLuaRockspecFieldError::NotATable {
2093                    field: "foo".into(),
2094                    invalid_type: "integer".into(),
2095                })?;
2096                Ok(())
2097            })
2098            .unwrap_err();
2099        let err = Error::from_ottavino(extern_err, rockspec_content);
2100        match err {
2101            Error::ExecutionError {
2102                cause,
2103                err_span: Some(span),
2104                ..
2105            } => {
2106                assert!(cause.contains("invalid rockspec field 'foo'"));
2107                assert_eq!(
2108                    &rockspec_content[span.offset()..span.offset() + span.len()],
2109                    "foo"
2110                );
2111            }
2112            other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2113        }
2114    }
2115}