Skip to main content

turbopack/module_options/
rule_condition.rs

1use std::{
2    iter,
3    mem::{replace, take},
4};
5
6use anyhow::Result;
7use bincode::{Decode, Encode};
8use either::Either;
9use smallvec::SmallVec;
10use turbo_esregex::EsRegex;
11use turbo_tasks::{NonLocalValue, ReadRef, ResolvedVc};
12use turbo_tasks_fs::{FileContent, FileSystemPath, glob::Glob};
13use turbopack_core::{
14    asset::Asset,
15    reference_type::{ReferenceType, ReferenceTypeCondition},
16    source::Source,
17    virtual_source::VirtualSource,
18};
19
20#[derive(Debug, Clone, PartialEq, Eq, NonLocalValue, Encode, Decode)]
21pub enum RuleCondition {
22    All(Vec<RuleCondition>),
23    Any(Vec<RuleCondition>),
24    Not(Box<RuleCondition>),
25    True,
26    False,
27    ReferenceType(ReferenceTypeCondition),
28    ResourceIsVirtualSource,
29    ResourcePathEquals(FileSystemPath),
30    ResourcePathHasNoExtension,
31    ResourcePathEndsWith(String),
32    ResourcePathInDirectory(String),
33    ResourcePathInExactDirectory(FileSystemPath),
34    ContentTypeStartsWith(String),
35    ContentTypeEmpty,
36    ResourcePathEsRegex(ReadRef<EsRegex>),
37    ResourceContentEsRegex(ReadRef<EsRegex>),
38    /// For paths that are within the same filesystem as the `base`, it need to
39    /// match the relative path from base to resource. This includes `./` or
40    /// `../` prefix. For paths in a different filesystem, it need to match
41    /// the resource path in that filesystem without any prefix. This means
42    /// any glob starting with `./` or `../` will only match paths in the
43    /// project. Globs starting with `**` can match any path.
44    ResourcePathGlob {
45        base: FileSystemPath,
46        glob: ReadRef<Glob>,
47    },
48    ResourceBasePathGlob(ReadRef<Glob>),
49    ResourceQueryContains(String),
50    ResourceQueryEquals(String),
51    ResourceQueryEsRegex(ReadRef<EsRegex>),
52    ContentTypeGlob(ReadRef<Glob>),
53    ContentTypeEsRegex(ReadRef<EsRegex>),
54}
55
56impl RuleCondition {
57    pub fn all(conditions: Vec<RuleCondition>) -> RuleCondition {
58        RuleCondition::All(conditions)
59    }
60
61    pub fn any(conditions: Vec<RuleCondition>) -> RuleCondition {
62        RuleCondition::Any(conditions)
63    }
64
65    #[allow(clippy::should_implement_trait)]
66    pub fn not(condition: RuleCondition) -> RuleCondition {
67        RuleCondition::Not(Box::new(condition))
68    }
69
70    /// Slightly optimize a `RuleCondition` by flattening nested `Any`, `All`, or `Not` variants.
71    ///
72    /// Does not apply general re-ordering of rules (which may also be a valid optimization using a
73    /// cost heuristic), but does flatten constant `True` and `False` conditions, potentially
74    /// skipping other rules.
75    pub fn flatten(&mut self) {
76        match self {
77            RuleCondition::Any(conds) => {
78                // fast path: flatten children in-place and avoid constructing an additional vec
79                let mut needs_flattening = false;
80                for c in conds.iter_mut() {
81                    c.flatten();
82                    if *c == RuleCondition::True {
83                        // short-circuit: all conditions are side-effect free
84                        *self = RuleCondition::True;
85                        return;
86                    }
87                    needs_flattening = needs_flattening
88                        || matches!(c, RuleCondition::Any(_) | RuleCondition::False);
89                }
90
91                if needs_flattening {
92                    *conds = take(conds)
93                        .into_iter()
94                        .flat_map(|c| match c {
95                            RuleCondition::Any(nested) => {
96                                debug_assert!(!nested.is_empty(), "empty Any should be False");
97                                Either::Left(nested.into_iter())
98                            }
99                            RuleCondition::False => Either::Right(Either::Left(iter::empty())),
100                            c => Either::Right(Either::Right(iter::once(c))),
101                        })
102                        .collect();
103                }
104
105                match conds.len() {
106                    0 => *self = RuleCondition::False,
107                    1 => *self = take(conds).into_iter().next().unwrap(),
108                    _ => {}
109                }
110            }
111            RuleCondition::All(conds) => {
112                // fast path: flatten children in-place and avoid constructing an additional vec
113                let mut needs_flattening = false;
114                for c in conds.iter_mut() {
115                    c.flatten();
116                    if *c == RuleCondition::False {
117                        // short-circuit: all conditions are side-effect free
118                        *self = RuleCondition::False;
119                        return;
120                    }
121                    needs_flattening = needs_flattening
122                        || matches!(c, RuleCondition::All(_) | RuleCondition::True);
123                }
124
125                if needs_flattening {
126                    *conds = take(conds)
127                        .into_iter()
128                        .flat_map(|c| match c {
129                            RuleCondition::All(nested) => {
130                                debug_assert!(!nested.is_empty(), "empty All should be True");
131                                Either::Left(nested.into_iter())
132                            }
133                            RuleCondition::True => Either::Right(Either::Left(iter::empty())),
134                            c => Either::Right(Either::Right(iter::once(c))),
135                        })
136                        .collect();
137                }
138
139                match conds.len() {
140                    0 => *self = RuleCondition::True,
141                    1 => *self = take(conds).into_iter().next().unwrap(),
142                    _ => {}
143                }
144            }
145            RuleCondition::Not(cond) => {
146                match &mut **cond {
147                    // nested `Not`s negate each other
148                    RuleCondition::Not(inner) => {
149                        let inner = &mut **inner;
150                        inner.flatten();
151                        // Use `replace` with a dummy condition instead of `take` since
152                        // `RuleCondition` doesn't implement `Default`.
153                        *self = replace(inner, RuleCondition::False)
154                    }
155                    RuleCondition::True => *self = RuleCondition::False,
156                    RuleCondition::False => *self = RuleCondition::True,
157                    other => other.flatten(),
158                }
159            }
160            _ => {}
161        }
162    }
163
164    pub async fn matches(
165        &self,
166        source: ResolvedVc<Box<dyn Source>>,
167        path: &FileSystemPath,
168        reference_type: &ReferenceType,
169    ) -> Result<bool> {
170        enum Op<'a> {
171            All(&'a [RuleCondition]), // Remaining conditions in an All
172            Any(&'a [RuleCondition]), // Remaining conditions in an Any
173            Not,                      // Inverts the previous condition
174        }
175
176        // Evaluates the condition returning the result and possibly pushing additional operations
177        // onto the stack as a kind of continuation.
178        async fn process_condition<'a, const SZ: usize>(
179            source: ResolvedVc<Box<dyn Source + 'static>>,
180            path: &FileSystemPath,
181            reference_type: &ReferenceType,
182            stack: &mut SmallVec<[Op<'a>; SZ]>,
183            mut cond: &'a RuleCondition,
184        ) -> Result<bool, anyhow::Error> {
185            // Use a loop to avoid recursion and unnecessary stack operations.
186            loop {
187                match cond {
188                    RuleCondition::All(conditions) => {
189                        if conditions.is_empty() {
190                            return Ok(true);
191                        } else {
192                            if conditions.len() > 1 {
193                                stack.push(Op::All(&conditions[1..]));
194                            }
195                            cond = &conditions[0];
196                            // jump directly to the next condition, no need to deal with
197                            // the stack.
198                            continue;
199                        }
200                    }
201                    RuleCondition::Any(conditions) => {
202                        if conditions.is_empty() {
203                            return Ok(false);
204                        } else {
205                            if conditions.len() > 1 {
206                                stack.push(Op::Any(&conditions[1..]));
207                            }
208                            cond = &conditions[0];
209                            continue;
210                        }
211                    }
212                    RuleCondition::Not(inner) => {
213                        stack.push(Op::Not);
214                        cond = inner.as_ref();
215                        continue;
216                    }
217                    RuleCondition::True => {
218                        return Ok(true);
219                    }
220                    RuleCondition::False => {
221                        return Ok(false);
222                    }
223                    RuleCondition::ReferenceType(condition_ty) => {
224                        return Ok(condition_ty.includes(reference_type));
225                    }
226                    RuleCondition::ResourceIsVirtualSource => {
227                        return Ok(ResolvedVc::try_downcast_type::<VirtualSource>(source).is_some());
228                    }
229                    RuleCondition::ResourcePathEquals(other) => {
230                        return Ok(path == other);
231                    }
232                    RuleCondition::ResourcePathEndsWith(end) => {
233                        return Ok(path.path.ends_with(end));
234                    }
235                    RuleCondition::ResourcePathHasNoExtension => {
236                        return Ok(if let Some(i) = path.path.rfind('.') {
237                            if let Some(j) = path.path.rfind('/') {
238                                j > i
239                            } else {
240                                false
241                            }
242                        } else {
243                            true
244                        });
245                    }
246                    RuleCondition::ResourcePathInDirectory(dir) => {
247                        return Ok(path.path.starts_with(&format!("{dir}/"))
248                            || path.path.contains(&format!("/{dir}/")));
249                    }
250                    RuleCondition::ResourcePathInExactDirectory(parent_path) => {
251                        return Ok(path.is_inside_ref(parent_path));
252                    }
253                    RuleCondition::ContentTypeStartsWith(start) => {
254                        let content_type = &source.ident().await?.content_type;
255                        return Ok(content_type
256                            .as_ref()
257                            .is_some_and(|ct| ct.starts_with(start.as_str())));
258                    }
259                    RuleCondition::ContentTypeEmpty => {
260                        return Ok(source.ident().await?.content_type.is_none());
261                    }
262                    RuleCondition::ResourcePathGlob { glob, base } => {
263                        return Ok(if let Some(rel_path) = base.get_relative_request_to(path) {
264                            glob.matches(&rel_path)
265                        } else {
266                            glob.matches(&path.path)
267                        });
268                    }
269                    RuleCondition::ResourceBasePathGlob(glob) => {
270                        let basename = path
271                            .path
272                            .rsplit_once('/')
273                            .map_or(path.path.as_str(), |(_, b)| b);
274                        return Ok(glob.matches(basename));
275                    }
276                    RuleCondition::ResourcePathEsRegex(regex) => {
277                        return Ok(regex.is_match(&path.path));
278                    }
279                    RuleCondition::ResourceContentEsRegex(regex) => {
280                        let content = source.content().file_content().await?;
281                        match &*content {
282                            FileContent::Content(file_content) => {
283                                return Ok(regex.is_match(&file_content.content().to_str()?));
284                            }
285                            FileContent::NotFound => return Ok(false),
286                        }
287                    }
288                    RuleCondition::ResourceQueryContains(query) => {
289                        let ident = source.ident().await?;
290                        return Ok(ident.query.contains(query));
291                    }
292                    RuleCondition::ResourceQueryEquals(query) => {
293                        let ident = source.ident().await?;
294                        return Ok(ident.query == *query);
295                    }
296                    RuleCondition::ResourceQueryEsRegex(regex) => {
297                        let ident = source.ident().await?;
298                        return Ok(regex.is_match(&ident.query));
299                    }
300                    RuleCondition::ContentTypeGlob(glob) => {
301                        let ident = source.ident().await?;
302                        return Ok(ident
303                            .content_type
304                            .as_ref()
305                            .is_some_and(|ct| glob.matches(ct)));
306                    }
307                    RuleCondition::ContentTypeEsRegex(regex) => {
308                        let ident = source.ident().await?;
309                        return Ok(ident
310                            .content_type
311                            .as_ref()
312                            .is_some_and(|ct| regex.is_match(ct)));
313                    }
314                }
315            }
316        }
317        // Allocate a small inline stack to avoid heap allocations in the common case where
318        // conditions are not deeply stacked.  Additionally we take care to avoid stack
319        // operations unless strictly necessary.
320        const EXPECTED_SIZE: usize = 8;
321        let mut stack = SmallVec::<[Op; EXPECTED_SIZE]>::with_capacity(EXPECTED_SIZE);
322        let mut result = process_condition(source, path, reference_type, &mut stack, self).await?;
323        while let Some(op) = stack.pop() {
324            match op {
325                Op::All(remaining) => {
326                    // Previous was true, keep going
327                    if result {
328                        if remaining.len() > 1 {
329                            stack.push(Op::All(&remaining[1..]));
330                        }
331                        result = process_condition(
332                            source,
333                            path,
334                            reference_type,
335                            &mut stack,
336                            &remaining[0],
337                        )
338                        .await?;
339                    }
340                }
341                Op::Any(remaining) => {
342                    // Previous was false, keep going
343                    if !result {
344                        if remaining.len() > 1 {
345                            stack.push(Op::Any(&remaining[1..]));
346                        }
347                        // If the stack didn't change, we can loop inline, but we would still need
348                        // to pop the item.  This might be faster since we would avoid the `match`
349                        // but overall, that is quite minor for an enum with 3 cases.
350                        result = process_condition(
351                            source,
352                            path,
353                            reference_type,
354                            &mut stack,
355                            &remaining[0],
356                        )
357                        .await?;
358                    }
359                }
360                Op::Not => {
361                    result = !result;
362                }
363            }
364        }
365        Ok(result)
366    }
367}
368
369#[cfg(test)]
370pub mod tests {
371    use turbo_tasks::Vc;
372    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
373    use turbo_tasks_fs::{FileContent, FileSystem, VirtualFileSystem};
374    use turbopack_core::{asset::AssetContent, file_source::FileSource};
375
376    use super::*;
377
378    #[test]
379    fn flatten_any_with_single_child_collapses() {
380        let mut rc = RuleCondition::Any(vec![RuleCondition::True]);
381        rc.flatten();
382        assert_eq!(rc, RuleCondition::True);
383
384        let mut rc = RuleCondition::Any(vec![RuleCondition::ContentTypeEmpty]);
385        rc.flatten();
386        assert_eq!(rc, RuleCondition::ContentTypeEmpty);
387    }
388
389    #[test]
390    fn flatten_any_nested_and_false() {
391        let mut rc = RuleCondition::Any(vec![
392            RuleCondition::False,
393            RuleCondition::Any(vec![RuleCondition::ContentTypeEmpty, RuleCondition::False]),
394        ]);
395        rc.flatten();
396        assert_eq!(rc, RuleCondition::ContentTypeEmpty);
397    }
398
399    #[test]
400    fn flatten_any_short_circuits_on_true() {
401        let mut rc = RuleCondition::Any(vec![
402            RuleCondition::False,
403            RuleCondition::True,
404            RuleCondition::ContentTypeEmpty,
405        ]);
406        rc.flatten();
407        assert_eq!(rc, RuleCondition::True);
408    }
409
410    #[test]
411    fn flatten_any_empty_becomes_false() {
412        let mut rc = RuleCondition::Any(vec![]);
413        rc.flatten();
414        assert_eq!(rc, RuleCondition::False);
415    }
416
417    #[test]
418    fn flatten_all_with_single_child_collapses() {
419        let mut rc = RuleCondition::All(vec![RuleCondition::ContentTypeEmpty]);
420        rc.flatten();
421        assert_eq!(rc, RuleCondition::ContentTypeEmpty);
422
423        let mut rc = RuleCondition::All(vec![RuleCondition::True]);
424        rc.flatten();
425        assert_eq!(rc, RuleCondition::True);
426    }
427
428    #[test]
429    fn flatten_all_nested_and_true() {
430        let mut rc = RuleCondition::All(vec![
431            RuleCondition::True,
432            RuleCondition::All(vec![RuleCondition::ContentTypeEmpty, RuleCondition::True]),
433        ]);
434        rc.flatten();
435        assert_eq!(rc, RuleCondition::ContentTypeEmpty);
436    }
437
438    #[test]
439    fn flatten_all_short_circuits_on_false() {
440        let mut rc = RuleCondition::All(vec![
441            RuleCondition::True,
442            RuleCondition::False,
443            RuleCondition::ContentTypeEmpty,
444        ]);
445        rc.flatten();
446        assert_eq!(rc, RuleCondition::False);
447    }
448
449    #[test]
450    fn flatten_all_empty_becomes_true() {
451        let mut rc = RuleCondition::All(vec![]);
452        rc.flatten();
453        assert_eq!(rc, RuleCondition::True);
454    }
455
456    #[test]
457    fn flatten_not_of_not() {
458        let mut rc = RuleCondition::Not(Box::new(RuleCondition::Not(Box::new(
459            RuleCondition::All(vec![RuleCondition::ContentTypeEmpty]),
460        ))));
461        rc.flatten();
462        assert_eq!(rc, RuleCondition::ContentTypeEmpty);
463    }
464
465    #[test]
466    fn flatten_not_constants() {
467        let mut rc = RuleCondition::Not(Box::new(RuleCondition::True));
468        rc.flatten();
469        assert_eq!(rc, RuleCondition::False);
470
471        let mut rc = RuleCondition::Not(Box::new(RuleCondition::False));
472        rc.flatten();
473        assert_eq!(rc, RuleCondition::True);
474    }
475
476    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
477    async fn test_rule_condition_leaves() {
478        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
479            BackendOptions::default(),
480            noop_backing_storage(),
481        ));
482        tt.run_once(async { run_leaves_test_operation().read_strongly_consistent().await })
483            .await
484            .unwrap();
485    }
486
487    #[turbo_tasks::function(operation, root)]
488    pub async fn run_leaves_test_operation() -> Result<()> {
489        let fs = VirtualFileSystem::new();
490        let virtual_path = fs.root().await?.join("foo.js")?;
491        let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
492            virtual_path.clone(),
493            AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
494        ))
495        .to_resolved()
496        .await?;
497
498        let non_virtual_path = fs.root().await?.join("bar.js")?;
499        let non_virtual_source =
500            Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
501                .to_resolved()
502                .await?;
503
504        {
505            let condition = RuleCondition::ReferenceType(ReferenceTypeCondition::Runtime);
506            assert!(
507                condition
508                    .matches(virtual_source, &virtual_path, &ReferenceType::Runtime)
509                    .await
510                    .unwrap()
511            );
512            assert!(
513                !condition
514                    .matches(
515                        non_virtual_source,
516                        &non_virtual_path,
517                        &ReferenceType::Css(
518                            turbopack_core::reference_type::CssReferenceSubType::Compose
519                        )
520                    )
521                    .await
522                    .unwrap()
523            );
524        }
525
526        {
527            let condition = RuleCondition::ResourceIsVirtualSource;
528            assert!(
529                condition
530                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
531                    .await
532                    .unwrap()
533            );
534            assert!(
535                !condition
536                    .matches(
537                        non_virtual_source,
538                        &non_virtual_path,
539                        &ReferenceType::Undefined
540                    )
541                    .await
542                    .unwrap()
543            );
544        }
545        {
546            let condition = RuleCondition::ResourcePathEquals(virtual_path.clone());
547            assert!(
548                condition
549                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
550                    .await
551                    .unwrap()
552            );
553            assert!(
554                !condition
555                    .matches(
556                        non_virtual_source,
557                        &non_virtual_path,
558                        &ReferenceType::Undefined
559                    )
560                    .await
561                    .unwrap()
562            );
563        }
564        {
565            let condition = RuleCondition::ResourcePathHasNoExtension;
566            assert!(
567                condition
568                    .matches(
569                        virtual_source,
570                        &fs.root().await?.join("foo")?,
571                        &ReferenceType::Undefined
572                    )
573                    .await
574                    .unwrap()
575            );
576            assert!(
577                !condition
578                    .matches(
579                        non_virtual_source,
580                        &non_virtual_path,
581                        &ReferenceType::Undefined
582                    )
583                    .await
584                    .unwrap()
585            );
586        }
587        {
588            let condition = RuleCondition::ResourcePathEndsWith("foo.js".to_string());
589            assert!(
590                condition
591                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
592                    .await
593                    .unwrap()
594            );
595            assert!(
596                !condition
597                    .matches(
598                        non_virtual_source,
599                        &non_virtual_path,
600                        &ReferenceType::Undefined
601                    )
602                    .await
603                    .unwrap()
604            );
605        }
606        anyhow::Ok(())
607    }
608
609    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
610    async fn test_rule_condition_tree() {
611        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
612            BackendOptions::default(),
613            noop_backing_storage(),
614        ));
615        tt.run_once(async {
616            run_rule_condition_tree_test_operation()
617                .read_strongly_consistent()
618                .await
619        })
620        .await
621        .unwrap();
622    }
623
624    #[turbo_tasks::function(operation, root)]
625    pub async fn run_rule_condition_tree_test_operation() -> Result<()> {
626        let fs = VirtualFileSystem::new();
627        let virtual_path = fs.root().await?.join("foo.js")?;
628        let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
629            virtual_path.clone(),
630            AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
631        ))
632        .to_resolved()
633        .await?;
634
635        let non_virtual_path = fs.root().await?.join("bar.js")?;
636        let non_virtual_source =
637            Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
638                .to_resolved()
639                .await?;
640
641        {
642            // not
643            let condition = RuleCondition::not(RuleCondition::ResourceIsVirtualSource);
644            assert!(
645                !condition
646                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
647                    .await
648                    .unwrap()
649            );
650            assert!(
651                condition
652                    .matches(
653                        non_virtual_source,
654                        &non_virtual_path,
655                        &ReferenceType::Undefined
656                    )
657                    .await
658                    .unwrap()
659            );
660        }
661        {
662            // any
663            // Only one of the conditions matches our virtual source
664            let condition = RuleCondition::any(vec![
665                RuleCondition::ResourcePathInDirectory("doesnt/exist".to_string()),
666                RuleCondition::ResourceIsVirtualSource,
667                RuleCondition::ResourcePathHasNoExtension,
668            ]);
669            assert!(
670                condition
671                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
672                    .await
673                    .unwrap()
674            );
675            assert!(
676                !condition
677                    .matches(
678                        non_virtual_source,
679                        &non_virtual_path,
680                        &ReferenceType::Undefined
681                    )
682                    .await
683                    .unwrap()
684            );
685        }
686        {
687            // all
688            // Only one of the conditions matches our virtual source
689            let condition = RuleCondition::all(vec![
690                RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
691                RuleCondition::ResourceIsVirtualSource,
692                RuleCondition::ResourcePathEquals(virtual_path.clone()),
693            ]);
694            assert!(
695                condition
696                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
697                    .await
698                    .unwrap()
699            );
700            assert!(
701                !condition
702                    .matches(
703                        non_virtual_source,
704                        &non_virtual_path,
705                        &ReferenceType::Undefined
706                    )
707                    .await
708                    .unwrap()
709            );
710        }
711        {
712            // bigger tree
713
714            // Build a simple tree to cover our various composite conditions
715            let condition = RuleCondition::all(vec![
716                RuleCondition::ResourceIsVirtualSource,
717                RuleCondition::ResourcePathEquals(virtual_path.clone()),
718                RuleCondition::Not(Box::new(RuleCondition::ResourcePathHasNoExtension)),
719                RuleCondition::Any(vec![
720                    RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
721                    RuleCondition::ContentTypeEmpty,
722                ]),
723            ]);
724            assert!(
725                condition
726                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
727                    .await
728                    .unwrap()
729            );
730            assert!(
731                !condition
732                    .matches(
733                        non_virtual_source,
734                        &non_virtual_path,
735                        &ReferenceType::Undefined
736                    )
737                    .await
738                    .unwrap()
739            );
740        }
741        anyhow::Ok(())
742    }
743}