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 std::{
11    collections::HashMap, convert::Infallible, fmt::Display, io, path::PathBuf, str::FromStr,
12};
13
14use ottavino::{Closure, Executor, Fuel};
15use ottavino_util::serde::from_value;
16use serde::{de::DeserializeOwned, Deserialize, Serialize};
17
18pub use build::*;
19pub use dependency::*;
20pub use deploy::*;
21use miette::{Diagnostic, NamedSource, SourceSpan};
22pub use partial::*;
23pub use platform::*;
24pub use rock_source::*;
25use ssri::Integrity;
26pub use test_spec::*;
27use thiserror::Error;
28use url::Url;
29
30pub(crate) use lux_macros::DisplayAsLuaKV;
31pub(crate) use serde_util::*;
32
33impl DisplayAsLuaValue for Url {
34    fn display_lua_value(&self) -> DisplayLuaValue {
35        DisplayLuaValue::String(self.to_string())
36    }
37}
38
39use crate::{
40    hash::HasIntegrity,
41    lua_version::{LuaVersion, LuaVersionUnset},
42    package::{PackageName, PackageSpec, PackageVersion, PackageVersionReq},
43    project::{project_toml::ProjectTomlError, 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(PackageName),
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(PackageName),
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(PackageName),
95    #[error(transparent)]
96    #[diagnostic(transparent)]
97    ProjectToml(#[from] ProjectTomlError),
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: PackageName,
158    /// The version of the package, plus a suffix indicating the revision of the rockspec. Example: "2.0.1-1"
159    version: PackageVersion,
160    description: RockDescription,
161    supported_platforms: PlatformSupport,
162    /// The Lua version requirement for this rock
163    lua: PackageVersionReq,
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(PackageVersionReq::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) -> &PackageName {
357        &self.package
358    }
359
360    fn version(&self) -> &PackageVersion {
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) -> &PackageVersionReq {
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: PackageSpec,
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: PackageVersionReq::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: &PackageSpec,
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) -> &PackageName {
572        self.local.package()
573    }
574
575    fn version(&self) -> &PackageVersion {
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) -> &PackageVersionReq {
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, PackageName, PackageVersion),
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    use crate::package::PackageSpec;
827
828    use super::*;
829
830    #[test]
831    pub fn parse_rockspec() {
832        let rockspec_content = "
833        rockspec_format = '1.0'\n
834        package = 'foo'\n
835        version = '1.0.0-1'\n
836        source = {\n
837            url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
838        }\n
839        "
840        .to_string();
841        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
842        assert_eq!(rockspec.local.rockspec_format, Some(RockspecFormat::_1_0));
843        assert_eq!(rockspec.local.package, "foo".into());
844        assert_eq!(rockspec.local.version, "1.0.0-1".parse().unwrap());
845        assert_eq!(rockspec.local.description, RockDescription::default());
846
847        let rockspec_content = "
848        package = 'bar'\n
849        version = '2.0.0-1'\n
850        description = {}\n
851        source = {\n
852            url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
853        }\n
854        "
855        .to_string();
856        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
857        assert_eq!(rockspec.local.rockspec_format, None);
858        assert_eq!(rockspec.local.package, "bar".into());
859        assert_eq!(rockspec.local.version, "2.0.0-1".parse().unwrap());
860        assert_eq!(rockspec.local.description, RockDescription::default());
861
862        let rockspec_content = "
863        package = 'rocks.nvim'\n
864        version = '3.0.0-1'\n
865        description = {\n
866            summary = 'some summary',
867            detailed = 'some detailed description',
868            license = 'MIT',
869            homepage = 'https://github.com/lumen-oss/rocks.nvim',
870            issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
871            maintainer = 'Lumen Labs',
872        }\n
873        source = {\n
874            url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
875        }\n
876        "
877        .to_string();
878        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
879        assert_eq!(rockspec.local.rockspec_format, None);
880        assert_eq!(rockspec.local.package, "rocks.nvim".into());
881        assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
882        let expected_description = RockDescription {
883            summary: Some("some summary".into()),
884            detailed: Some("some detailed description".into()),
885            license: Some("MIT".into()),
886            homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
887            issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
888            maintainer: Some("Lumen Labs".into()),
889            labels: Vec::new(),
890        };
891        assert_eq!(rockspec.local.description, expected_description);
892
893        let rockspec_content = "
894        package = 'rocks.nvim'\n
895        version = '3.0.0-1'\n
896        description = {\n
897            summary = 'some summary',
898            detailed = 'some detailed description',
899            license = 'MIT',
900            homepage = 'https://github.com/lumen-oss/rocks.nvim',
901            issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
902            maintainer = 'Lumen Labs',
903            labels = {},
904        }\n
905        external_dependencies = { FOO = { library = 'foo' } }\n
906        source = {\n
907            url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
908        }\n
909        "
910        .to_string();
911        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
912        assert_eq!(rockspec.local.rockspec_format, None);
913        assert_eq!(rockspec.local.package, "rocks.nvim".into());
914        assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
915        let expected_description = RockDescription {
916            summary: Some("some summary".into()),
917            detailed: Some("some detailed description".into()),
918            license: Some("MIT".into()),
919            homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
920            issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
921            maintainer: Some("Lumen Labs".into()),
922            labels: Vec::new(),
923        };
924        assert_eq!(rockspec.local.description, expected_description);
925        assert_eq!(
926            *rockspec
927                .local
928                .external_dependencies
929                .default
930                .get("FOO")
931                .unwrap(),
932            ExternalDependencySpec {
933                library: Some("foo".into()),
934                header: None
935            }
936        );
937
938        let rockspec_content = "
939        package = 'rocks.nvim'\n
940        version = '3.0.0-1'\n
941        description = {\n
942            summary = 'some summary',
943            detailed = 'some detailed description',
944            license = 'MIT',
945            homepage = 'https://github.com/lumen-oss/rocks.nvim',
946            issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
947            maintainer = 'Lumen Labs',
948            labels = { 'package management', },
949        }\n
950        supported_platforms = { 'unix', '!windows' }\n
951        dependencies = { 'neorg ~> 6' }\n
952        build_dependencies = { 'foo' }\n
953        external_dependencies = { FOO = { header = 'foo.h' } }\n
954        test_dependencies = { 'busted >= 2.0.0' }\n
955        source = {\n
956            url = 'git+https://github.com/lumen-oss/rocks.nvim',\n
957            hash = 'sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=',\n
958        }\n
959        "
960        .to_string();
961        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
962        assert_eq!(rockspec.local.rockspec_format, None);
963        assert_eq!(rockspec.local.package, "rocks.nvim".into());
964        assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
965        let expected_description = RockDescription {
966            summary: Some("some summary".into()),
967            detailed: Some("some detailed description".into()),
968            license: Some("MIT".into()),
969            homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
970            issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
971            maintainer: Some("Lumen Labs".into()),
972            labels: vec!["package management".into()],
973        };
974        assert_eq!(rockspec.local.description, expected_description);
975        assert!(rockspec
976            .local
977            .supported_platforms
978            .is_supported(&PlatformIdentifier::Unix));
979        assert!(!rockspec
980            .local
981            .supported_platforms
982            .is_supported(&PlatformIdentifier::Windows));
983        let neorg = PackageSpec::parse("neorg".into(), "6.0.0".into()).unwrap();
984        assert!(rockspec
985            .local
986            .dependencies
987            .default
988            .into_iter()
989            .any(|dep| dep.matches(&neorg)));
990        let foo = PackageSpec::parse("foo".into(), "1.0.0".into()).unwrap();
991        assert!(rockspec
992            .local
993            .build_dependencies
994            .default
995            .into_iter()
996            .any(|dep| dep.matches(&foo)));
997        let busted = PackageSpec::parse("busted".into(), "2.2.0".into()).unwrap();
998        assert_eq!(
999            *rockspec
1000                .local
1001                .external_dependencies
1002                .default
1003                .get("FOO")
1004                .unwrap(),
1005            ExternalDependencySpec {
1006                header: Some("foo.h".into()),
1007                library: None
1008            }
1009        );
1010        assert!(rockspec
1011            .local
1012            .test_dependencies
1013            .default
1014            .into_iter()
1015            .any(|dep| dep.matches(&busted)));
1016
1017        let rockspec_content = "
1018        rockspec_format = '1.0'\n
1019        package = 'foo'\n
1020        version = '1.0.0-1'\n
1021        source = {\n
1022            url = 'git+https://hub.com/owner/example-project/',\n
1023            branch = 'bar',\n
1024        }\n
1025        "
1026        .to_string();
1027        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1028        assert_eq!(
1029            rockspec.local.source.default.source_spec,
1030            RockSourceSpec::Git(GitSource {
1031                url: "https://hub.com/owner/example-project/".parse().unwrap(),
1032                git_ref: Some(GitRef::Branch("bar".into()))
1033            })
1034        );
1035        assert_eq!(rockspec.local.test, PerPlatform::default());
1036        let rockspec_content = "
1037        rockspec_format = '1.0'\n
1038        package = 'foo'\n
1039        version = '1.0.0-1'\n
1040        source = {\n
1041            url = 'git+https://hub.com/owner/example-project/',\n
1042            tag = 'bar',\n
1043        }\n
1044        "
1045        .to_string();
1046        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1047        assert_eq!(
1048            rockspec.local.source.default.source_spec,
1049            RockSourceSpec::Git(GitSource {
1050                url: "https://hub.com/owner/example-project/".parse().unwrap(),
1051                git_ref: Some(GitRef::Tag("bar".into()))
1052            })
1053        );
1054        let rockspec_content = "
1055        rockspec_format = '1.0'\n
1056        package = 'foo'\n
1057        version = '1.0.0-1'\n
1058        source = {\n
1059            url = 'git+https://hub.com/owner/example-project/',\n
1060            branch = 'bar',\n
1061            tag = 'baz',\n
1062        }\n
1063        "
1064        .to_string();
1065        let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1066        let rockspec_content = "
1067        rockspec_format = '1.0'\n
1068        package = 'foo'\n
1069        version = '1.0.0-1'\n
1070        source = {\n
1071            url = 'git+https://hub.com/owner/example-project/',\n
1072            tag = 'bar',\n
1073            file = 'foo.tar.gz',\n
1074        }\n
1075        build = {\n
1076            install = {\n
1077                conf = {['foo.bar'] = 'config/bar.toml'},\n
1078            },\n
1079        }\n
1080        "
1081        .to_string();
1082        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1083        assert_eq!(
1084            rockspec.local.source.default.archive_name,
1085            Some("foo.tar.gz".into())
1086        );
1087        let foo_bar_path = rockspec
1088            .local
1089            .build
1090            .default
1091            .install
1092            .conf
1093            .get("foo.bar")
1094            .unwrap();
1095        assert_eq!(*foo_bar_path, PathBuf::from("config/bar.toml"));
1096        let rockspec_content = "
1097        rockspec_format = '1.0'\n
1098        package = 'foo'\n
1099        version = '1.0.0-1'\n
1100        source = {\n
1101            url = 'git+https://hub.com/example-project/foo.zip',\n
1102        }\n
1103        build = {\n
1104            install = {\n
1105                lua = {\n
1106                    'foo.lua',\n
1107                    ['foo.bar'] = 'src/bar.lua',\n
1108                },\n
1109                bin = {['foo.bar'] = 'bin/bar'},\n
1110            },\n
1111        }\n
1112        "
1113        .to_string();
1114        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1115        assert!(matches!(
1116            rockspec.local.build.default.build_backend,
1117            Some(BuildBackendSpec::Builtin { .. })
1118        ));
1119        let install_lua_spec = rockspec.local.build.default.install.lua;
1120        let foo_bar_path = install_lua_spec
1121            .get(&LuaModule::from_str("foo.bar").unwrap())
1122            .unwrap();
1123        assert_eq!(*foo_bar_path, PathBuf::from("src/bar.lua"));
1124        let foo_path = install_lua_spec
1125            .get(&LuaModule::from_str("foo").unwrap())
1126            .unwrap();
1127        assert_eq!(*foo_path, PathBuf::from("foo.lua"));
1128        let foo_bar_path = rockspec
1129            .local
1130            .build
1131            .default
1132            .install
1133            .bin
1134            .get("foo.bar")
1135            .unwrap();
1136        assert_eq!(*foo_bar_path, PathBuf::from("bin/bar"));
1137        let rockspec_content = "
1138        rockspec_format = '1.0'\n
1139        package = 'foo'\n
1140        version = '1.0.0-1'\n
1141        source = {\n
1142            url = 'git+https://hub.com/example-project/',\n
1143        }\n
1144        build = {\n
1145            copy_directories = { 'lua' },\n
1146        }\n
1147        "
1148        .to_string();
1149        let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1150        let rockspec_content = "
1151        rockspec_format = '1.0'\n
1152        package = 'foo'\n
1153        version = '1.0.0-1'\n
1154        source = {\n
1155            url = 'git+https://hub.com/example-project/',\n
1156        }\n
1157        build = {\n
1158            copy_directories = { 'lib' },\n
1159        }\n
1160        "
1161        .to_string();
1162        let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1163        let rockspec_content = "
1164        rockspec_format = '1.0'\n
1165        package = 'foo'\n
1166        version = '1.0.0-1'\n
1167        source = {\n
1168            url = 'git+https://hub.com/example-project/',\n
1169        }\n
1170        build = {\n
1171            copy_directories = { 'rock_manifest' },\n
1172        }\n
1173        "
1174        .to_string();
1175        let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1176        let rockspec_content = "
1177        rockspec_format = '1.0'\n
1178        package = 'foo'\n
1179        version = '1.0.0-1'\n
1180        source = {\n
1181            url = 'git+https://hub.com/example-project/foo.zip',\n
1182            dir = 'baz',\n
1183        }\n
1184        build = {\n
1185            type = 'make',\n
1186            install = {\n
1187                lib = {['foo.so'] = 'lib/bar.so'},\n
1188            },\n
1189            copy_directories = {\n
1190                'plugin',\n
1191                'ftplugin',\n
1192            },\n
1193            patches = {\n
1194                ['lua51-support.diff'] = [[\n
1195                    --- before.c\n
1196                    +++ path/to/after.c\n
1197                ]],\n
1198            },\n
1199        }\n
1200        "
1201        .to_string();
1202        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1203        assert_eq!(rockspec.local.source.default.unpack_dir, Some("baz".into()));
1204        assert_eq!(
1205            rockspec.local.build.default.build_backend,
1206            Some(BuildBackendSpec::Make(MakeBuildSpec::default()))
1207        );
1208        let foo_bar_path = rockspec
1209            .local
1210            .build
1211            .default
1212            .install
1213            .lib
1214            .get("foo.so")
1215            .unwrap();
1216        assert_eq!(*foo_bar_path, PathBuf::from("lib/bar.so"));
1217        let copy_directories = rockspec.local.build.default.copy_directories;
1218        assert_eq!(
1219            copy_directories,
1220            vec![PathBuf::from("plugin"), PathBuf::from("ftplugin")]
1221        );
1222        let patches = rockspec.local.build.default.patches;
1223        let _patch = patches.get(&PathBuf::from("lua51-support.diff")).unwrap();
1224        let rockspec_content = "
1225        rockspec_format = '1.0'\n
1226        package = 'foo'\n
1227        version = '1.0.0-1'\n
1228        source = {\n
1229            url = 'git+https://hub.com/example-project/foo.zip',\n
1230        }\n
1231        build = {\n
1232            type = 'cmake',\n
1233        }\n
1234        "
1235        .to_string();
1236        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1237        assert_eq!(
1238            rockspec.local.build.default.build_backend,
1239            Some(BuildBackendSpec::CMake(CMakeBuildSpec::default()))
1240        );
1241        let rockspec_content = "
1242        rockspec_format = '1.0'\n
1243        package = 'foo'\n
1244        version = '1.0.0-1'\n
1245        source = {\n
1246            url = 'git+https://hub.com/example-project/foo.zip',\n
1247        }\n
1248        build = {\n
1249            type = 'command',\n
1250            build_command = 'foo',\n
1251            install_command = 'bar',\n
1252        }\n
1253        "
1254        .to_string();
1255        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1256        assert!(matches!(
1257            rockspec.local.build.default.build_backend,
1258            Some(BuildBackendSpec::Command(CommandBuildSpec { .. }))
1259        ));
1260        let rockspec_content = "
1261        rockspec_format = '1.0'\n
1262        package = 'foo'\n
1263        version = '1.0.0-1'\n
1264        source = {\n
1265            url = 'git+https://hub.com/example-project/foo.zip',\n
1266        }\n
1267        build = {\n
1268            type = 'command',\n
1269            install_command = 'foo',\n
1270        }\n
1271        "
1272        .to_string();
1273        RemoteLuaRockspec::new(&rockspec_content).unwrap();
1274        let rockspec_content = "
1275        rockspec_format = '1.0'\n
1276        package = 'foo'\n
1277        version = '1.0.0-1'\n
1278        source = {\n
1279            url = 'git+https://hub.com/example-project/foo.zip',\n
1280        }\n
1281        build = {\n
1282            type = 'command',\n
1283            build_command = 'foo',\n
1284        }\n
1285        "
1286        .to_string();
1287        RemoteLuaRockspec::new(&rockspec_content).unwrap();
1288        // platform overrides
1289        let rockspec_content = "
1290        package = 'rocks'\n
1291        version = '3.0.0-1'\n
1292        dependencies = {\n
1293          'neorg ~> 6',\n
1294          'toml-edit ~> 1',\n
1295          platforms = {\n
1296            windows = {\n
1297              'neorg = 5.0.0',\n
1298              'toml = 1.0.0',\n
1299            },\n
1300            unix = {\n
1301              'neorg = 5.0.0',\n
1302            },\n
1303            linux = {\n
1304              'toml = 1.0.0',\n
1305            },\n
1306          },\n
1307        }\n
1308        source = {\n
1309            url = 'git+https://github.com/lumen-oss/rocks.nvim',\n
1310            hash = 'sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=',\n
1311        }\n
1312        "
1313        .to_string();
1314        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1315        let neorg_override = PackageSpec::parse("neorg".into(), "5.0.0".into()).unwrap();
1316        let toml_edit = PackageSpec::parse("toml-edit".into(), "1.0.0".into()).unwrap();
1317        let toml = PackageSpec::parse("toml".into(), "1.0.0".into()).unwrap();
1318        assert_eq!(rockspec.local.dependencies.default.len(), 2);
1319        let per_platform = &rockspec.local.dependencies.per_platform;
1320        assert_eq!(
1321            per_platform
1322                .get(&PlatformIdentifier::Windows)
1323                .unwrap()
1324                .iter()
1325                .filter(|dep| dep.matches(&neorg_override)
1326                    || dep.matches(&toml_edit)
1327                    || dep.matches(&toml))
1328                .count(),
1329            3
1330        );
1331        assert_eq!(
1332            per_platform
1333                .get(&PlatformIdentifier::Unix)
1334                .unwrap()
1335                .iter()
1336                .filter(|dep| dep.matches(&neorg_override)
1337                    || dep.matches(&toml_edit)
1338                    || dep.matches(&toml))
1339                .count(),
1340            2
1341        );
1342        assert_eq!(
1343            per_platform
1344                .get(&PlatformIdentifier::Linux)
1345                .unwrap()
1346                .iter()
1347                .filter(|dep| dep.matches(&neorg_override)
1348                    || dep.matches(&toml_edit)
1349                    || dep.matches(&toml))
1350                .count(),
1351            3
1352        );
1353        let rockspec_content = "
1354        package = 'rocks'\n
1355        version = '3.0.0-1'\n
1356        external_dependencies = {\n
1357            FOO = { library = 'foo' },\n
1358            platforms = {\n
1359              windows = {\n
1360                FOO = { library = 'foo.dll' },\n
1361              },\n
1362              unix = {\n
1363                BAR = { header = 'bar.h' },\n
1364              },\n
1365              linux = {\n
1366                FOO = { library = 'foo.so' },\n
1367              },\n
1368            },\n
1369        }\n
1370        source = {\n
1371            url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1372        }\n
1373        "
1374        .to_string();
1375        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1376        assert_eq!(
1377            *rockspec
1378                .local
1379                .external_dependencies
1380                .default
1381                .get("FOO")
1382                .unwrap(),
1383            ExternalDependencySpec {
1384                library: Some("foo".into()),
1385                header: None
1386            }
1387        );
1388        let per_platform = rockspec.local.external_dependencies.per_platform;
1389        assert_eq!(
1390            *per_platform
1391                .get(&PlatformIdentifier::Windows)
1392                .and_then(|it| it.get("FOO"))
1393                .unwrap(),
1394            ExternalDependencySpec {
1395                library: Some("foo.dll".into()),
1396                header: None
1397            }
1398        );
1399        assert_eq!(
1400            *per_platform
1401                .get(&PlatformIdentifier::Unix)
1402                .and_then(|it| it.get("FOO"))
1403                .unwrap(),
1404            ExternalDependencySpec {
1405                library: Some("foo".into()),
1406                header: None
1407            }
1408        );
1409        assert_eq!(
1410            *per_platform
1411                .get(&PlatformIdentifier::Unix)
1412                .and_then(|it| it.get("BAR"))
1413                .unwrap(),
1414            ExternalDependencySpec {
1415                header: Some("bar.h".into()),
1416                library: None
1417            }
1418        );
1419        assert_eq!(
1420            *per_platform
1421                .get(&PlatformIdentifier::Linux)
1422                .and_then(|it| it.get("BAR"))
1423                .unwrap(),
1424            ExternalDependencySpec {
1425                header: Some("bar.h".into()),
1426                library: None
1427            }
1428        );
1429        assert_eq!(
1430            *per_platform
1431                .get(&PlatformIdentifier::Linux)
1432                .and_then(|it| it.get("FOO"))
1433                .unwrap(),
1434            ExternalDependencySpec {
1435                library: Some("foo.so".into()),
1436                header: None
1437            }
1438        );
1439        let rockspec_content = "
1440        rockspec_format = '1.0'\n
1441        package = 'foo'\n
1442        version = '1.0.0-1'\n
1443        source = {\n
1444            url = 'git+https://hub.com/example-project/.git',\n
1445            branch = 'bar',\n
1446            platforms = {\n
1447                macosx = {\n
1448                    branch = 'mac',\n
1449                },\n
1450                windows = {\n
1451                    url = 'git+https://winhub.com/example-project/.git',\n
1452                    branch = 'win',\n
1453                },\n
1454            },\n
1455        }\n
1456        "
1457        .to_string();
1458        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1459        assert_eq!(
1460            rockspec.local.source.default.source_spec,
1461            RockSourceSpec::Git(GitSource {
1462                url: "https://hub.com/example-project/.git".parse().unwrap(),
1463                git_ref: Some(GitRef::Branch("bar".into()))
1464            })
1465        );
1466        assert_eq!(
1467            rockspec
1468                .source
1469                .per_platform
1470                .get(&PlatformIdentifier::MacOSX)
1471                .map(|it| it.source_spec.clone())
1472                .unwrap(),
1473            RockSourceSpec::Git(GitSource {
1474                url: "https://hub.com/example-project/.git".parse().unwrap(),
1475                git_ref: Some(GitRef::Branch("mac".into()))
1476            })
1477        );
1478        assert_eq!(
1479            rockspec
1480                .source
1481                .per_platform
1482                .get(&PlatformIdentifier::Windows)
1483                .map(|it| it.source_spec.clone())
1484                .unwrap(),
1485            RockSourceSpec::Git(GitSource {
1486                url: "https://winhub.com/example-project/.git".parse().unwrap(),
1487                git_ref: Some(GitRef::Branch("win".into()))
1488            })
1489        );
1490        let rockspec_content = "
1491        rockspec_format = '1.0'\n
1492        package = 'foo'\n
1493        version = '1.0.0-1'\n
1494        source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1495        build = {\n
1496            type = 'make',\n
1497            install = {\n
1498                lib = {['foo.bar'] = 'lib/bar.so'},\n
1499            },\n
1500            copy_directories = { 'plugin' },\n
1501            platforms = {\n
1502                unix = {\n
1503                    copy_directories = { 'ftplugin' },\n
1504                },\n
1505                linux = {\n
1506                    copy_directories = { 'foo' },\n
1507                },\n
1508            },\n
1509        }\n
1510        "
1511        .to_string();
1512        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1513        let per_platform = rockspec.local.build.per_platform;
1514        let unix = per_platform.get(&PlatformIdentifier::Unix).unwrap();
1515        assert_eq!(
1516            unix.copy_directories,
1517            vec![PathBuf::from("plugin"), PathBuf::from("ftplugin")]
1518        );
1519        let linux = per_platform.get(&PlatformIdentifier::Linux).unwrap();
1520        assert_eq!(
1521            linux.copy_directories,
1522            vec![
1523                PathBuf::from("plugin"),
1524                PathBuf::from("foo"),
1525                PathBuf::from("ftplugin")
1526            ]
1527        );
1528        let rockspec_content = "
1529        package = 'foo'\n
1530        version = '1.0.0-1'\n
1531        source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1532        build = {\n
1533            type = 'builtin',\n
1534            modules = {\n
1535                cjson = {\n
1536                    sources = { 'lua_cjson.c', 'strbuf.c', 'fpconv.c' },\n
1537                }\n
1538            },\n
1539            platforms = {\n
1540                win32 = { modules = { cjson = { defines = {\n
1541                    'DISABLE_INVALID_NUMBERS', 'USE_INTERNAL_ISINF'\n
1542                } } } }\n
1543            },\n
1544        }\n
1545        "
1546        .to_string();
1547        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1548        let win32 = rockspec.local.build.get(&PlatformIdentifier::Windows);
1549        assert_eq!(
1550            win32.build_backend,
1551            Some(BuildBackendSpec::Builtin(BuiltinBuildSpec {
1552                modules: vec![(
1553                    LuaModule::from_str("cjson").unwrap(),
1554                    ModuleSpec::ModulePaths(ModulePaths {
1555                        sources: vec!["lua_cjson.c".into(), "strbuf.c".into(), "fpconv.c".into()],
1556                        libraries: Vec::default(),
1557                        defines: vec![
1558                            ("DISABLE_INVALID_NUMBERS".into(), None),
1559                            ("USE_INTERNAL_ISINF".into(), None)
1560                        ],
1561                        incdirs: Vec::default(),
1562                        libdirs: Vec::default(),
1563                    })
1564                )]
1565                .into_iter()
1566                .collect()
1567            }))
1568        );
1569        let rockspec_content = "
1570        rockspec_format = '1.0'\n
1571        package = 'foo'\n
1572        version = '1.0.0-1'\n
1573        deploy = {\n
1574            wrap_bin_scripts = false,\n
1575        }\n
1576        source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1577        ";
1578        let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1579        let deploy_spec = &rockspec.deploy().current_platform();
1580        assert!(!deploy_spec.wrap_bin_scripts);
1581    }
1582
1583    #[test]
1584    pub fn parse_scm_rockspec() {
1585        let rockspec_content = "
1586        package = 'foo'\n
1587        version = 'scm-1'\n
1588        source = {\n
1589            url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1590        }\n
1591        "
1592        .to_string();
1593        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1594        assert_eq!(rockspec.local.package, "foo".into());
1595        assert_eq!(rockspec.local.version, "scm-1".parse().unwrap());
1596    }
1597
1598    #[test]
1599    pub fn regression_luasystem() {
1600        let rockspec_content = String::from_utf8(
1601            std::fs::read(
1602                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1603                    .join("resources/test/luasystem-0.4.4-1.rockspec"),
1604            )
1605            .unwrap(),
1606        )
1607        .unwrap();
1608        let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1609        let build_spec = rockspec.local.build.current_platform();
1610        assert!(matches!(
1611            build_spec.build_backend,
1612            Some(BuildBackendSpec::Builtin { .. })
1613        ));
1614        if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) =
1615            &build_spec.build_backend
1616        {
1617            assert_eq!(
1618                modules.get(&LuaModule::from_str("system.init").unwrap()),
1619                Some(&ModuleSpec::SourcePath("system/init.lua".into()))
1620            );
1621            assert_eq!(
1622                modules.get(&LuaModule::from_str("system.core").unwrap()),
1623                Some(&ModuleSpec::ModulePaths(ModulePaths {
1624                    sources: vec![
1625                        "src/core.c".into(),
1626                        "src/compat.c".into(),
1627                        "src/time.c".into(),
1628                        "src/environment.c".into(),
1629                        "src/random.c".into(),
1630                        "src/term.c".into(),
1631                        "src/bitflags.c".into(),
1632                        "src/wcwidth.c".into(),
1633                    ],
1634                    defines: luasystem_expected_defines(),
1635                    libraries: luasystem_expected_libraries(),
1636                    incdirs: luasystem_expected_incdirs(),
1637                    libdirs: luasystem_expected_libdirs(),
1638                }))
1639            );
1640        }
1641        if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) = &rockspec
1642            .local
1643            .build
1644            .get(&PlatformIdentifier::Windows)
1645            .build_backend
1646        {
1647            if let ModuleSpec::ModulePaths(paths) = modules
1648                .get(&LuaModule::from_str("system.core").unwrap())
1649                .unwrap()
1650            {
1651                assert_eq!(paths.libraries, luasystem_expected_windows_libraries());
1652            };
1653        }
1654        if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) = &rockspec
1655            .local
1656            .build
1657            .get(&PlatformIdentifier::Win32)
1658            .build_backend
1659        {
1660            if let ModuleSpec::ModulePaths(paths) = modules
1661                .get(&LuaModule::from_str("system.core").unwrap())
1662                .unwrap()
1663            {
1664                assert_eq!(paths.libraries, luasystem_expected_windows_libraries());
1665            };
1666        }
1667    }
1668
1669    fn luasystem_expected_defines() -> Vec<(String, Option<String>)> {
1670        if cfg!(target_os = "windows") {
1671            vec![
1672                ("WINVER".into(), Some("0x0600".into())),
1673                ("_WIN32_WINNT".into(), Some("0x0600".into())),
1674            ]
1675        } else {
1676            Vec::default()
1677        }
1678    }
1679
1680    fn luasystem_expected_windows_libraries() -> Vec<PathBuf> {
1681        vec!["advapi32".into(), "winmm".into()]
1682    }
1683    fn luasystem_expected_libraries() -> Vec<PathBuf> {
1684        if cfg!(any(target_os = "linux", target_os = "android")) {
1685            vec!["rt".into()]
1686        } else if cfg!(target_os = "windows") {
1687            luasystem_expected_windows_libraries()
1688        } else {
1689            Vec::default()
1690        }
1691    }
1692
1693    fn luasystem_expected_incdirs() -> Vec<PathBuf> {
1694        Vec::default()
1695    }
1696
1697    fn luasystem_expected_libdirs() -> Vec<PathBuf> {
1698        Vec::default()
1699    }
1700
1701    #[test]
1702    pub fn rust_mlua_rockspec() {
1703        let rockspec_content = "
1704    package = 'foo'\n
1705    version = 'scm-1'\n
1706    source = {\n
1707        url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1708    }\n
1709    build = {
1710        type = 'rust-mlua',
1711        modules = {
1712            'foo',
1713            bar = 'baz',
1714        },
1715        target_path = 'path/to/cargo/target/directory',
1716        default_features = false,
1717        include = {
1718            'file.lua',
1719            ['path/to/another/file.lua'] = 'another-file.lua',
1720        },
1721        features = {'extra', 'features'},
1722    }
1723            ";
1724        let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1725        let build_spec = rockspec.local.build.current_platform();
1726        if let Some(BuildBackendSpec::RustMlua(build_spec)) = build_spec.build_backend.to_owned() {
1727            assert_eq!(
1728                build_spec.modules.get("foo").unwrap(),
1729                &PathBuf::from(format!("libfoo.{}", std::env::consts::DLL_EXTENSION))
1730            );
1731            assert_eq!(
1732                build_spec.modules.get("bar").unwrap(),
1733                &PathBuf::from(format!("libbaz.{}", std::env::consts::DLL_EXTENSION))
1734            );
1735            assert_eq!(
1736                build_spec.include.get(&PathBuf::from("file.lua")).unwrap(),
1737                &PathBuf::from("file.lua")
1738            );
1739            assert_eq!(
1740                build_spec
1741                    .include
1742                    .get(&PathBuf::from("path/to/another/file.lua"))
1743                    .unwrap(),
1744                &PathBuf::from("another-file.lua")
1745            );
1746        } else {
1747            panic!("Expected RustMlua build backend");
1748        }
1749    }
1750
1751    #[test]
1752    pub fn rust_binary_rockspec() {
1753        let rockspec_content = "
1754    package = 'foo'\n
1755    version = 'scm-1'\n
1756    source = {\n
1757        url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1758    }\n
1759    build = {\n
1760        type = 'rust-binary',\n
1761        package = 'foo',\n
1762        features = {'extra', 'features'},\n
1763    }\n
1764            ";
1765        let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1766        let build_spec = rockspec.local.build.current_platform();
1767        if let Some(BuildBackendSpec::RustBinary(build_spec)) = build_spec.build_backend.to_owned()
1768        {
1769            assert_eq!(build_spec.package, Some("foo".to_string()));
1770            assert_eq!(build_spec.features, vec!["extra", "features"]);
1771        } else {
1772            panic!("Expected RustBinary build backend");
1773        }
1774    }
1775
1776    #[tokio::test]
1777    pub async fn regression_ltui() {
1778        let content = String::from_utf8(
1779            std::fs::read(
1780                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1781                    .join("resources/test/ltui-2.8-2.rockspec"),
1782            )
1783            .unwrap(),
1784        )
1785        .unwrap();
1786        RemoteLuaRockspec::new(&content).unwrap();
1787    }
1788
1789    // Luarocks allows the `install.bin` field to be a list, even though it
1790    // should only allow a table.
1791    #[test]
1792    pub fn regression_off_spec_install_binaries() {
1793        let rockspec_content = r#"
1794            package = "WSAPI"
1795            version = "1.7-1"
1796
1797            source = {
1798              url = "git://github.com/keplerproject/wsapi",
1799              tag = "v1.7",
1800            }
1801
1802            build = {
1803              type = "builtin",
1804              modules = {
1805                ["wsapi"] = "src/wsapi.lua",
1806              },
1807              -- Offending Line
1808              install = { bin = { "src/launcher/wsapi.cgi" } }
1809            }
1810        "#;
1811
1812        let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1813
1814        assert_eq!(
1815            rockspec.build().current_platform().install.bin,
1816            HashMap::from([("wsapi.cgi".into(), PathBuf::from("src/launcher/wsapi.cgi"))])
1817        );
1818    }
1819
1820    #[test]
1821    pub fn regression_external_dependencies() {
1822        let content = String::from_utf8(
1823            std::fs::read(
1824                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1825                    .join("resources/test/luaossl-20220711-0.rockspec"),
1826            )
1827            .unwrap(),
1828        )
1829        .unwrap();
1830        let rockspec = RemoteLuaRockspec::new(&content).unwrap();
1831        if cfg!(target_family = "unix") {
1832            assert_eq!(
1833                rockspec
1834                    .local
1835                    .external_dependencies
1836                    .current_platform()
1837                    .get("OPENSSL")
1838                    .unwrap(),
1839                &ExternalDependencySpec {
1840                    library: Some("ssl".into()),
1841                    header: Some("openssl/ssl.h".into()),
1842                }
1843            );
1844        }
1845        let per_platform = rockspec.local.external_dependencies.per_platform;
1846        assert_eq!(
1847            *per_platform
1848                .get(&PlatformIdentifier::Windows)
1849                .and_then(|it| it.get("OPENSSL"))
1850                .unwrap(),
1851            ExternalDependencySpec {
1852                library: Some("libeay32".into()),
1853                header: Some("openssl/ssl.h".into()),
1854            }
1855        );
1856    }
1857
1858    #[test]
1859    pub fn remote_lua_rockspec_from_package_and_source_spec() {
1860        let package_req = "foo@1.0.5".parse().unwrap();
1861        let source = GitSource {
1862            url: "https://hub.com/owner/example-project.git".parse().unwrap(),
1863            git_ref: Some(GitRef::Tag("1.0.5".into())),
1864        };
1865        let source_spec = RockSourceSpec::Git(source);
1866        let rockspec = RemoteLuaRockspec::from_source_spec(
1867            package_req,
1868            source_spec.clone(),
1869            Vec::new(),
1870            HashMap::new(),
1871        );
1872        let generated_rockspec_str = rockspec.local.raw_content;
1873        let rockspec2 = RemoteLuaRockspec::new(&generated_rockspec_str).unwrap();
1874        assert_eq!(rockspec2.local.package, "foo".into());
1875        assert_eq!(rockspec2.local.version, "1.0.5".parse().unwrap());
1876        assert_eq!(rockspec2.local.source, PerPlatform::new(source_spec.into()));
1877    }
1878
1879    #[test]
1880    pub fn regression_complex_source_field() {
1881        let rockspec_content = r#"
1882            package = "say"
1883            local rock_version = "1.4.1"
1884            local rock_release = "3"
1885            local namespace = "lunarmodules"
1886            local repository = package
1887
1888            version = ("%s-%s"):format(rock_version, rock_release)
1889
1890            source = {
1891              url = ("git+https://github.com/%s/%s.git"):format(namespace, repository),
1892              branch = rock_version == "scm" and "master" or nil,
1893              tag = rock_version ~= "scm" and "v"..rock_version or nil,
1894            }
1895
1896            description = {
1897              summary = "Lua string hashing/indexing library",
1898            }
1899
1900            dependencies = {
1901              "lua >= 5.1",
1902            }
1903
1904            build = {
1905              type = "builtin",
1906            }
1907        "#
1908        .to_string();
1909        RemoteLuaRockspec::new(&rockspec_content).unwrap();
1910    }
1911
1912    fn eval_lua_global<T: serde::de::DeserializeOwned>(code: &str, key: &'static str) -> T {
1913        use ottavino::{Closure, Executor, Fuel, Lua};
1914        use ottavino_util::serde::from_value;
1915        Lua::core()
1916            .try_enter(|ctx| {
1917                let closure = Closure::load(ctx, None, code.as_bytes())?;
1918                let executor = Executor::start(ctx, closure.into(), ());
1919                executor.step(ctx, &mut Fuel::with(i32::MAX))?;
1920                from_value(ctx.globals().get_value(ctx, key)).map_err(ottavino::Error::from)
1921            })
1922            .unwrap()
1923    }
1924
1925    #[test]
1926    pub fn rock_description_roundtrip() {
1927        let desc = RockDescription {
1928            summary: Some("A great package".into()),
1929            detailed: Some("Detailed description here".into()),
1930            license: Some("MIT".into()),
1931            homepage: Some("https://example.com".parse().unwrap()),
1932            issues_url: Some("https://example.com/issues".parse().unwrap()),
1933            maintainer: Some("Some Maintainer <maintainer@example.com>".into()),
1934            labels: vec!["neovim".into(), "lua".into()],
1935        };
1936        let lua = desc.display_lua().to_string();
1937        let restored: RockDescription = eval_lua_global(&lua, "description");
1938        assert_eq!(desc, restored);
1939    }
1940
1941    #[test]
1942    pub fn rock_description_empty_roundtrip() {
1943        let desc = RockDescription::default();
1944        let lua = desc.display_lua().to_string();
1945        // An empty description produces an empty assignment; restore from empty table.
1946        let lua = if lua.trim().is_empty() {
1947            "description = {}".to_string()
1948        } else {
1949            lua
1950        };
1951        let restored: RockDescription = eval_lua_global(&lua, "description");
1952        assert_eq!(desc, restored);
1953    }
1954
1955    #[test]
1956    pub fn regression_luasec() {
1957        let rockspec_content = r#"
1958package = "LuaSec"
1959version = "1.3.2-1"
1960source = {
1961  url = "git+https://github.com/brunoos/luasec",
1962  tag = "v1.3.2",
1963}
1964external_dependencies = {
1965   platforms = {
1966      unix = {
1967         OPENSSL = {
1968            header = "openssl/ssl.h",
1969            library = "ssl"
1970         }
1971      },
1972      windows = {
1973         OPENSSL = {
1974            header = "openssl/ssl.h",
1975         }
1976      },
1977   }
1978}
1979"#;
1980        let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1981        let linux_external_deps = rockspec
1982            .external_dependencies()
1983            .get(&PlatformIdentifier::Linux);
1984        assert!(linux_external_deps.get("OPENSSL").is_some());
1985        let windows_external_deps = rockspec
1986            .external_dependencies()
1987            .get(&PlatformIdentifier::Windows);
1988        assert!(windows_external_deps.get("OPENSSL").is_some());
1989    }
1990
1991    #[test]
1992    pub fn find_value_span_spaces() {
1993        let source = "package = 'foo'\nversion = '1.0'";
1994        let span = find_value_span(source, "package");
1995        assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
1996    }
1997
1998    #[test]
1999    pub fn find_value_span_no_spaces() {
2000        let source = "package='foo'";
2001        let span = find_value_span(source, "package");
2002        assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
2003    }
2004
2005    #[test]
2006    pub fn find_value_span_returns_empty_when_not_found() {
2007        let source = "version = '1.0'";
2008        let span = find_value_span(source, "package");
2009        assert_eq!(span.len(), 0);
2010    }
2011
2012    #[test]
2013    pub fn find_value_span_skips_url_containing_field() {
2014        let source = "homepage = 'https://luapackager.com'\npackage = 'foo'";
2015        let span = find_value_span(source, "package");
2016        assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
2017    }
2018
2019    #[test]
2020    pub fn find_value_span_double_quoted() {
2021        let source = r#"package = "foo""#;
2022        let span = find_value_span(source, "package");
2023        assert_eq!(
2024            &source[span.offset()..span.offset() + span.len()],
2025            r#""foo""#
2026        );
2027    }
2028
2029    #[test]
2030    pub fn find_value_span_table() {
2031        let source = "description = {\n    summary = 'bar'\n}";
2032        let span = find_value_span(source, "description");
2033        assert_eq!(&source[span.offset()..span.offset() + span.len()], "{");
2034    }
2035
2036    #[test]
2037    pub fn parse_lua_rockspec_field_error() {
2038        let rockspec_content = "package = 'my-lua-lib'\nfoo = 123";
2039        let extern_err = ottavino::Lua::core()
2040            .try_enter(|_ctx| {
2041                Err(ParseLuaRockspecFieldError::Deserialize {
2042                    field: "foo".into(),
2043                    source: Box::new(ottavino_util::serde::de::Error::Message(
2044                        "expected bar".into(),
2045                    )),
2046                })?;
2047                Ok(())
2048            })
2049            .unwrap_err();
2050        let err = Error::from_ottavino(extern_err, rockspec_content);
2051        match err {
2052            Error::ExecutionError {
2053                cause,
2054                err_span: Some(span),
2055                ..
2056            } => {
2057                assert!(cause.contains("invalid rockspec field 'foo'"));
2058                assert_eq!(
2059                    &rockspec_content[span.offset()..span.offset() + span.len()],
2060                    "foo"
2061                );
2062            }
2063            other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2064        }
2065        let extern_err = ottavino::Lua::core()
2066            .try_enter(|_ctx| {
2067                Err(ParseLuaRockspecFieldError::DeserializeTable {
2068                    field: "foo".into(),
2069                    source: Box::new(ottavino_util::serde::de::Error::Message(
2070                        "expected bar".into(),
2071                    )),
2072                })?;
2073                Ok(())
2074            })
2075            .unwrap_err();
2076        let err = Error::from_ottavino(extern_err, rockspec_content);
2077        match err {
2078            Error::ExecutionError {
2079                cause,
2080                err_span: Some(span),
2081                ..
2082            } => {
2083                assert!(cause.contains("invalid rockspec field 'foo'"));
2084                assert_eq!(
2085                    &rockspec_content[span.offset()..span.offset() + span.len()],
2086                    "foo"
2087                );
2088            }
2089            other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2090        }
2091        let extern_err = ottavino::Lua::core()
2092            .try_enter(|_ctx| {
2093                Err(ParseLuaRockspecFieldError::NotATable {
2094                    field: "foo".into(),
2095                    invalid_type: "integer".into(),
2096                })?;
2097                Ok(())
2098            })
2099            .unwrap_err();
2100        let err = Error::from_ottavino(extern_err, rockspec_content);
2101        match err {
2102            Error::ExecutionError {
2103                cause,
2104                err_span: Some(span),
2105                ..
2106            } => {
2107                assert!(cause.contains("invalid rockspec field 'foo'"));
2108                assert_eq!(
2109                    &rockspec_content[span.offset()..span.offset() + span.len()],
2110                    "foo"
2111                );
2112            }
2113            other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2114        }
2115    }
2116}