Skip to main content

lux_lib/tree/
mod.rs

1use crate::{
2    build::utils::format_path,
3    config::{tree::RockLayoutConfig, Config},
4    fs,
5    lockfile::{LocalPackage, LocalPackageId, Lockfile, LockfileError, OptState, ReadOnly},
6    lua_version::LuaVersion,
7    package::{PackageName, PackageReq},
8    variables::{GetVariableError, HasVariables},
9};
10use std::{
11    collections::HashMap,
12    io,
13    path::{Path, PathBuf},
14};
15
16use itertools::Itertools;
17use miette::Diagnostic;
18use nonempty::NonEmpty;
19use thiserror::Error;
20mod dist;
21mod list;
22
23pub use dist::*;
24
25const LOCKFILE_NAME: &str = "lux.lock";
26
27/// A tree is a collection of files where installed rocks are located.
28///
29/// `lux` diverges from the traditional hierarchy employed by luarocks.
30/// Instead, we opt for a much simpler approach:
31///
32/// - /rocks/<lua-version> - contains rocks
33/// - /rocks/<lua-version>/<rock>/etc - documentation and supplementary files for the rock
34/// - /rocks/<lua-version>/<rock>/lib - shared libraries (.so files)
35/// - /rocks/<lua-version>/<rock>/src - library code for the rock
36/// - /bin - binary files produced by various rocks
37pub trait InstallTree {
38    /// The Lua version for which to install packages.
39    fn version(&self) -> &LuaVersion;
40    /// The root directory of the tree
41    fn root(&self) -> PathBuf;
42    /// The root directory of a package in this tree
43    fn root_for(&self, package: &LocalPackage) -> PathBuf;
44    /// Where wrapped package binaries are installed
45    fn bin(&self) -> PathBuf;
46    /// Where unwrapped package binaries are installed
47    fn unwrapped_bin(&self) -> PathBuf;
48    /// Create a [`RockLayout`] for an entrypoint package, creating the `lib` and `src` directories.
49    fn entrypoint(&self, package: &LocalPackage) -> io::Result<RockLayout>;
50    /// Create a [`RockLayout`] for a dependency package, creating the `lib` and `src` directories.
51    fn dependency(&self, package: &LocalPackage) -> io::Result<RockLayout>;
52    /// Create a [`Lockfile`] for this tree.
53    fn lockfile(&self) -> Result<Lockfile<ReadOnly>, TreeError>;
54    /// Get this tree's lockfile path.
55    fn lockfile_path(&self) -> PathBuf;
56    /// The tree in which to install build dependencies.
57    fn build_tree(&self, config: &Config) -> Result<Tree, TreeError>;
58    /// The tree in which to install test dependencies.
59    fn test_tree(&self, config: &Config) -> Result<Tree, TreeError>;
60    /// Get the [`RockLayout`] for an installed package.
61    fn installed_rock_layout(&self, package: &LocalPackage) -> Result<RockLayout, TreeError>;
62    /// List the packages that are installed in this tree.
63    fn list(&self) -> Result<HashMap<PackageName, Vec<LocalPackage>>, TreeError>;
64    /// Find installed rocks that match the given [`PackageReq`].
65    fn match_rocks(&self, req: &PackageReq) -> Result<RockMatches, TreeError>;
66}
67
68/// A Lux install tree that supports multiple versions of the same dependency,
69/// with packages addressed by their [`LocalPackageId`]
70#[derive(Clone, Debug)]
71pub struct Tree {
72    /// The Lua version of the tree.
73    version: LuaVersion,
74    /// The parent of this tree's root directory.
75    root_parent: PathBuf,
76    /// The rock layout config for this tree
77    entrypoint_layout: RockLayoutConfig,
78    /// The root of this tree's test dependency tree.
79    test_tree_dir: PathBuf,
80    /// The root of this tree's build dependency tree.
81    build_tree_dir: PathBuf,
82}
83
84#[derive(Debug, Error, Diagnostic)]
85pub enum TreeError {
86    #[error(transparent)]
87    #[diagnostic(transparent)]
88    Fs(#[from] fs::FsError),
89    #[error(transparent)]
90    #[diagnostic(transparent)]
91    Lockfile(#[from] LockfileError),
92}
93
94/// Change-agnostic way of referencing various paths for a rock
95#[derive(Debug, PartialEq)]
96pub struct RockLayout {
97    /// The local installation directory.
98    /// Can be substituted in a rockspec's `build.build_variables` and `build.install_variables`
99    /// using `$(PREFIX)`.
100    pub rock_path: PathBuf,
101    /// The `etc` directory, containing resources.
102    pub etc: PathBuf,
103    /// The `lib` directory, containing native libraries.
104    /// Can be substituted in a rockspec's `build.build_variables` and `build.install_variables`
105    /// using `$(LIBDIR)`.
106    pub lib: PathBuf,
107    /// The `src` directory, containing Lua sources.
108    /// Can be substituted in a rockspec's `build.build_variables` and `build.install_variables`
109    /// using `$(LUADIR)`.
110    pub src: PathBuf,
111    /// The `bin` directory, containing executables.
112    /// Can be substituted in a rockspec's `build.build_variables` and `build.install_variables`
113    /// using `$(BINDIR)`.
114    /// This points to a global binary path at the root of the current tree by default.
115    pub bin: PathBuf,
116    /// The `etc/conf` directory, containing configuration files.
117    /// Can be substituted in a rockspec's `build.build_variables` and `build.install_variables`
118    /// using `$(CONFDIR)`.
119    pub conf: PathBuf,
120    /// The `etc/doc` directory, containing documentation files.
121    /// Can be substituted in a rockspec's `build.build_variables` and `build.install_variables`
122    /// using `$(DOCDIR)`.
123    pub doc: PathBuf,
124}
125
126impl RockLayout {
127    pub fn rockspec_path(&self) -> PathBuf {
128        self.rock_path.join("package.rockspec")
129    }
130}
131
132impl HasVariables for RockLayout {
133    #[tracing::instrument(level = "trace")]
134    fn get_variable(&self, var: &str) -> Result<Option<String>, GetVariableError> {
135        Ok(match var {
136            "PREFIX" => Some(format_path(&self.rock_path)),
137            "LIBDIR" => Some(format_path(&self.lib)),
138            "LUADIR" => Some(format_path(&self.src)),
139            "BINDIR" => Some(format_path(&self.bin)),
140            "CONFDIR" => Some(format_path(&self.conf)),
141            "DOCDIR" => Some(format_path(&self.doc)),
142            _ => None,
143        })
144    }
145}
146
147impl Tree {
148    /// NOTE: This is exposed for use by the config module.
149    /// Use `Config::tree()`
150    pub(crate) fn new(
151        root: PathBuf,
152        version: LuaVersion,
153        config: &Config,
154    ) -> Result<Self, TreeError> {
155        let version_dir = root.join(version.to_string());
156        let test_tree_dir = version_dir.join("test_dependencies");
157        let build_tree_dir = version_dir.join("build_dependencies");
158        Self::new_with_paths(root, test_tree_dir, build_tree_dir, version, config)
159    }
160
161    fn new_with_paths(
162        root: PathBuf,
163        test_tree_dir: PathBuf,
164        build_tree_dir: PathBuf,
165        version: LuaVersion,
166        config: &Config,
167    ) -> Result<Self, TreeError> {
168        let path_with_version = root.join(version.to_string());
169
170        // Ensure that the root and the version directory exist.
171        fs::sync::create_dir_all(&path_with_version)?;
172
173        // In case the tree is in a git repository, we tell git to ignore it.
174        let gitignore_file = root.join(".gitignore");
175        fs::sync::write(&gitignore_file, "*")?;
176
177        // Ensure that the bin directory exists.
178        let bin_dir = path_with_version.join("bin");
179        fs::sync::create_dir_all(&bin_dir)?;
180
181        let lockfile_path = root.join(LOCKFILE_NAME);
182        let rock_layout_config = if lockfile_path.is_file() {
183            let lockfile = Lockfile::load(lockfile_path, None)?;
184            lockfile.entrypoint_layout
185        } else {
186            config.entrypoint_layout().clone()
187        };
188        Ok(Self {
189            root_parent: root,
190            version,
191            entrypoint_layout: rock_layout_config,
192            test_tree_dir,
193            build_tree_dir,
194        })
195    }
196
197    pub fn match_rocks_and<F>(&self, req: &PackageReq, filter: F) -> Result<RockMatches, TreeError>
198    where
199        F: Fn(&LocalPackage) -> bool,
200    {
201        match self.list()?.get(req.name()) {
202            Some(packages) => {
203                let found_packages = packages
204                    .iter()
205                    .rev()
206                    .filter(|package| {
207                        req.version_req().matches(package.version()) && filter(package)
208                    })
209                    .map(|package| package.id())
210                    .collect_vec();
211
212                Ok(match NonEmpty::try_from(found_packages) {
213                    Ok(found_packages) => {
214                        if found_packages.len() == 1 {
215                            RockMatches::Single(found_packages.last().clone())
216                        } else {
217                            RockMatches::Many(found_packages)
218                        }
219                    }
220                    Err(_) => RockMatches::NotFound(req.clone()),
221                })
222            }
223            None => Ok(RockMatches::NotFound(req.clone())),
224        }
225    }
226
227    /// Create a [`RockLayout`] for an entrypoint
228    pub(crate) fn entrypoint_layout(&self, package: &LocalPackage) -> RockLayout {
229        mk_rock_layout(
230            &self.root(),
231            &self.bin(),
232            &self.root_for(package),
233            package,
234            &self.entrypoint_layout,
235        )
236    }
237
238    /// Create a [`RockLayout`] for a dependency
239    fn dependency_layout(&self, package: &LocalPackage) -> RockLayout {
240        mk_rock_layout(
241            &self.root(),
242            &self.bin(),
243            &self.root_for(package),
244            package,
245            &RockLayoutConfig::default(),
246        )
247    }
248}
249
250impl InstallTree for Tree {
251    fn version(&self) -> &LuaVersion {
252        &self.version
253    }
254
255    fn root(&self) -> PathBuf {
256        self.root_parent.join(self.version.to_string())
257    }
258
259    fn entrypoint(&self, package: &LocalPackage) -> io::Result<RockLayout> {
260        let rock_layout = self.entrypoint_layout(package);
261        fs::sync::create_dir_all(&rock_layout.rock_path).map_err(io::Error::other)?;
262        fs::sync::create_dir_all(&rock_layout.lib).map_err(io::Error::other)?;
263        fs::sync::create_dir_all(&rock_layout.src).map_err(io::Error::other)?;
264        Ok(rock_layout)
265    }
266
267    fn dependency(&self, package: &LocalPackage) -> io::Result<RockLayout> {
268        let rock_layout = self.dependency_layout(package);
269        fs::sync::create_dir_all(&rock_layout.rock_path).map_err(io::Error::other)?;
270        fs::sync::create_dir_all(&rock_layout.lib).map_err(io::Error::other)?;
271        fs::sync::create_dir_all(&rock_layout.src).map_err(io::Error::other)?;
272        Ok(rock_layout)
273    }
274
275    fn lockfile(&self) -> Result<Lockfile<ReadOnly>, TreeError> {
276        Ok(Lockfile::new(
277            self.lockfile_path(),
278            self.entrypoint_layout.clone(),
279        )?)
280    }
281
282    fn lockfile_path(&self) -> PathBuf {
283        self.root().join(LOCKFILE_NAME)
284    }
285
286    fn root_for(&self, package: &LocalPackage) -> PathBuf {
287        self.root().join(format!(
288            "{}-{}@{}",
289            package.id(),
290            package.name(),
291            package.version()
292        ))
293    }
294
295    fn bin(&self) -> PathBuf {
296        self.root().join("bin")
297    }
298
299    fn unwrapped_bin(&self) -> PathBuf {
300        self.bin().join("unwrapped")
301    }
302
303    fn test_tree(&self, config: &Config) -> Result<Self, TreeError> {
304        let test_tree_dir = self.test_tree_dir.clone();
305        let build_tree_dir = self.build_tree_dir.clone();
306        Self::new_with_paths(
307            test_tree_dir.clone(),
308            test_tree_dir,
309            build_tree_dir,
310            self.version.clone(),
311            config,
312        )
313    }
314
315    fn build_tree(&self, config: &Config) -> Result<Self, TreeError> {
316        let test_tree_dir = self.test_tree_dir.clone();
317        let build_tree_dir = self.build_tree_dir.clone();
318        Self::new_with_paths(
319            build_tree_dir.clone(),
320            test_tree_dir,
321            build_tree_dir,
322            self.version.clone(),
323            config,
324        )
325    }
326
327    /// Get the `RockLayout` for an installed package.
328    fn installed_rock_layout(&self, package: &LocalPackage) -> Result<RockLayout, TreeError> {
329        let lockfile = self.lockfile()?;
330        if lockfile.is_entrypoint(&package.id()) {
331            Ok(self.entrypoint_layout(package))
332        } else {
333            Ok(self.dependency_layout(package))
334        }
335    }
336
337    fn list(&self) -> Result<HashMap<PackageName, Vec<LocalPackage>>, TreeError> {
338        Ok(self.lockfile()?.list())
339    }
340
341    fn match_rocks(&self, req: &PackageReq) -> Result<RockMatches, TreeError> {
342        let found_packages = self.lockfile()?.find_rocks(req);
343        Ok(match NonEmpty::try_from(found_packages) {
344            Ok(found_packages) => {
345                if found_packages.len() == 1 {
346                    RockMatches::Single(found_packages.last().clone())
347                } else {
348                    RockMatches::Many(found_packages)
349                }
350            }
351            Err(_) => RockMatches::NotFound(req.clone()),
352        })
353    }
354}
355
356#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
357pub enum EntryType {
358    Entrypoint,
359    DependencyOnly,
360}
361
362impl EntryType {
363    pub fn is_entrypoint(&self) -> bool {
364        matches!(self, Self::Entrypoint)
365    }
366}
367
368#[derive(Clone, Debug)]
369pub enum RockMatches {
370    NotFound(PackageReq),
371    Single(LocalPackageId),
372    Many(NonEmpty<LocalPackageId>),
373}
374
375// Loosely mimic the Option<T> functions.
376impl RockMatches {
377    pub fn is_found(&self) -> bool {
378        matches!(self, Self::Single(_) | Self::Many(_))
379    }
380}
381
382/// Create a [`RockLayout`] for a package.
383pub fn mk_rock_layout(
384    tree_root: &Path,
385    bin: &Path,
386    rock_path: &Path,
387    package: &LocalPackage,
388    layout_config: &RockLayoutConfig,
389) -> RockLayout {
390    let (etc, lib, src) = if let Some(ref root) = layout_config.root {
391        let base = tree_root.join(root);
392
393        let etc = match package.spec.opt {
394            OptState::Required => base.join(&layout_config.etc),
395            OptState::Optional => base.join(&layout_config.opt_etc),
396        }
397        .join(format!("{}", package.name()));
398
399        let lib = etc.join(&layout_config.lib);
400        let src = etc.join(&layout_config.src);
401
402        (etc, lib, src)
403    } else {
404        let etc = rock_path.join(&layout_config.etc);
405        let lib = rock_path.join(&layout_config.lib);
406        let src = rock_path.join(&layout_config.src);
407
408        (etc, lib, src)
409    };
410    let conf = etc.join(&layout_config.conf);
411    let doc = etc.join(&layout_config.doc);
412
413    RockLayout {
414        rock_path: rock_path.to_path_buf(),
415        etc,
416        lib,
417        src,
418        bin: bin.to_path_buf(),
419        conf,
420        doc,
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use assert_fs::prelude::PathCopy;
427    use itertools::Itertools;
428    use std::path::PathBuf;
429
430    use insta::assert_yaml_snapshot;
431
432    use crate::{
433        config::{tree::RockLayoutConfig, ConfigBuilder},
434        lockfile::{LocalPackage, LocalPackageHashes, LockConstraint},
435        lua_version::LuaVersion,
436        package::{PackageName, PackageSpec, PackageVersion},
437        remote_package_source::RemotePackageSource,
438        rockspec::RockBinaries,
439        tree::{InstallTree, RockLayout},
440        variables,
441    };
442
443    #[test]
444    fn rock_layout() {
445        let tree_path =
446            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
447
448        let temp = assert_fs::TempDir::new().unwrap();
449        temp.copy_from(&tree_path, &["**"]).unwrap();
450        let tree_path = temp.to_path_buf();
451
452        let config = ConfigBuilder::new()
453            .unwrap()
454            .user_tree(Some(tree_path.clone()))
455            .build()
456            .unwrap();
457        let tree = config.user_tree(LuaVersion::Lua51).unwrap();
458
459        let mock_hashes = LocalPackageHashes {
460            rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
461                .parse()
462                .unwrap(),
463            source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
464                .parse()
465                .unwrap(),
466        };
467
468        let package = LocalPackage::from(
469            &PackageSpec::parse("neorg".into(), "8.0.0-1".into()).unwrap(),
470            LockConstraint::Unconstrained,
471            RockBinaries::default(),
472            RemotePackageSource::Test,
473            None,
474            mock_hashes.clone(),
475        );
476
477        let id = package.id();
478
479        let neorg = tree.dependency(&package).unwrap();
480
481        assert_eq!(
482            neorg,
483            RockLayout {
484                bin: tree_path.join("5.1/bin"),
485                rock_path: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1")),
486                etc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc")),
487                lib: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/lib")),
488                src: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/src")),
489                conf: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/conf")),
490                doc: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1/etc/doc")),
491            }
492        );
493
494        let package = LocalPackage::from(
495            &PackageSpec::parse("lua-cjson".into(), "2.1.0-1".into()).unwrap(),
496            LockConstraint::Unconstrained,
497            RockBinaries::default(),
498            RemotePackageSource::Test,
499            None,
500            mock_hashes.clone(),
501        );
502
503        let id = package.id();
504
505        let lua_cjson = tree.dependency(&package).unwrap();
506
507        assert_eq!(
508            lua_cjson,
509            RockLayout {
510                bin: tree_path.join("5.1/bin"),
511                rock_path: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1")),
512                etc: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/etc")),
513                lib: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/lib")),
514                src: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/src")),
515                conf: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/etc/conf")),
516                doc: tree_path.join(format!("5.1/{id}-lua-cjson@2.1.0-1/etc/doc")),
517            }
518        );
519    }
520
521    #[test]
522    fn tree_list() {
523        let tree_path =
524            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
525
526        let temp = assert_fs::TempDir::new().unwrap();
527        temp.copy_from(&tree_path, &["**"]).unwrap();
528        let tree_path = temp.to_path_buf();
529
530        let config = ConfigBuilder::new()
531            .unwrap()
532            .user_tree(Some(tree_path.clone()))
533            .build()
534            .unwrap();
535        let tree = config.user_tree(LuaVersion::Lua51).unwrap();
536        let result = tree.list().unwrap();
537        // note: sorted_redaction doesn't work because we have a nested Vec
538        let sorted_result: Vec<(PackageName, Vec<PackageVersion>)> = result
539            .into_iter()
540            .sorted()
541            .map(|(name, package)| {
542                (
543                    name,
544                    package
545                        .into_iter()
546                        .map(|package| package.spec.version)
547                        .sorted()
548                        .collect_vec(),
549                )
550            })
551            .collect_vec();
552
553        assert_yaml_snapshot!(sorted_result)
554    }
555
556    #[test]
557    fn rock_layout_nvim() {
558        let tree_path =
559            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
560
561        let temp = assert_fs::TempDir::new().unwrap();
562        temp.copy_from(&tree_path, &["**"]).unwrap();
563
564        let tree_path = temp.to_path_buf();
565
566        let config = ConfigBuilder::new()
567            .unwrap()
568            .user_tree(Some(tree_path.clone()))
569            .entrypoint_layout(RockLayoutConfig::new_nvim_layout())
570            .build()
571            .unwrap();
572
573        let tree = config.user_tree(LuaVersion::Lua51).unwrap();
574
575        let mock_hashes = LocalPackageHashes {
576            rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
577                .parse()
578                .unwrap(),
579            source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
580                .parse()
581                .unwrap(),
582        };
583
584        let package = LocalPackage::from(
585            &PackageSpec::parse("neorg".into(), "8.0.0-1".into()).unwrap(),
586            LockConstraint::Unconstrained,
587            RockBinaries::default(),
588            RemotePackageSource::Test,
589            None,
590            mock_hashes,
591        );
592
593        let id = package.id();
594        let neorg = tree.entrypoint(&package).unwrap();
595
596        assert_eq!(
597            neorg,
598            RockLayout {
599                bin: tree_path.join("5.1/bin"),
600                rock_path: tree_path.join(format!("5.1/{id}-neorg@8.0.0-1")),
601                etc: tree_path.join("5.1/site/pack/lux/start/neorg"),
602                lib: tree_path.join("5.1/site/pack/lux/start/neorg/lib"),
603                src: tree_path.join("5.1/site/pack/lux/start/neorg/lua"),
604                conf: tree_path.join("5.1/site/pack/lux/start/neorg/conf"),
605                doc: tree_path.join("5.1/site/pack/lux/start/neorg/doc"),
606            }
607        );
608    }
609
610    #[test]
611    fn rock_layout_substitute() {
612        let tree_path =
613            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
614
615        let temp = assert_fs::TempDir::new().unwrap();
616        temp.copy_from(&tree_path, &["**"]).unwrap();
617        let tree_path = temp.to_path_buf();
618
619        let config = ConfigBuilder::new()
620            .unwrap()
621            .user_tree(Some(tree_path.clone()))
622            .build()
623            .unwrap();
624        let tree = config.user_tree(LuaVersion::Lua51).unwrap();
625
626        let mock_hashes = LocalPackageHashes {
627            rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
628                .parse()
629                .unwrap(),
630            source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
631                .parse()
632                .unwrap(),
633        };
634
635        let neorg = tree
636            .dependency(&LocalPackage::from(
637                &PackageSpec::parse("neorg".into(), "8.0.0-1-1".into()).unwrap(),
638                LockConstraint::Unconstrained,
639                RockBinaries::default(),
640                RemotePackageSource::Test,
641                None,
642                mock_hashes.clone(),
643            ))
644            .unwrap();
645        let build_variables = vec![
646            "$(PREFIX)",
647            "$(LIBDIR)",
648            "$(LUADIR)",
649            "$(BINDIR)",
650            "$(CONFDIR)",
651            "$(DOCDIR)",
652        ];
653        let result: Vec<String> = build_variables
654            .into_iter()
655            .map(|var| variables::substitute(&[&neorg], var))
656            .try_collect()
657            .unwrap();
658        assert_eq!(
659            result,
660            vec![
661                neorg.rock_path.to_string_lossy().to_string(),
662                neorg.lib.to_string_lossy().to_string(),
663                neorg.src.to_string_lossy().to_string(),
664                neorg.bin.to_string_lossy().to_string(),
665                neorg.conf.to_string_lossy().to_string(),
666                neorg.doc.to_string_lossy().to_string(),
667            ]
668        );
669    }
670}