Skip to main content

sbom_tools/config/
types.rs

1//! Configuration types for sbom-tools operations.
2//!
3//! Provides structured configuration for diff, view, and multi-comparison operations.
4
5use crate::matching::FuzzyMatchConfig;
6use crate::reports::{ReportFormat, ReportType};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10
11// ============================================================================
12// Unified Application Configuration
13// ============================================================================
14
15/// Unified application configuration that can be loaded from CLI args or config files.
16///
17/// This is the top-level configuration struct that aggregates all configuration
18/// options. It can be constructed from CLI arguments, config files, or both
19/// (with CLI overriding file settings).
20#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
21#[serde(default)]
22pub struct AppConfig {
23    /// Matching configuration (thresholds, presets)
24    pub matching: MatchingConfig,
25    /// Output configuration (format, file, colors)
26    pub output: OutputConfig,
27    /// Filtering options
28    pub filtering: FilterConfig,
29    /// Behavior flags
30    pub behavior: BehaviorConfig,
31    /// Graph-aware diffing configuration
32    pub graph_diff: GraphAwareDiffConfig,
33    /// Custom matching rules configuration
34    pub rules: MatchingRulesPathConfig,
35    /// Ecosystem-specific rules configuration
36    pub ecosystem_rules: EcosystemRulesConfig,
37    /// TUI-specific configuration
38    pub tui: TuiConfig,
39    /// Enrichment configuration (OSV, etc.)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub enrichment: Option<EnrichmentConfig>,
42}
43
44impl AppConfig {
45    /// Create a new AppConfig with default values.
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Create an AppConfig builder.
51    pub fn builder() -> AppConfigBuilder {
52        AppConfigBuilder::default()
53    }
54}
55
56// ============================================================================
57// Builder for AppConfig
58// ============================================================================
59
60/// Builder for constructing AppConfig with fluent API.
61#[derive(Debug, Default)]
62pub struct AppConfigBuilder {
63    config: AppConfig,
64}
65
66impl AppConfigBuilder {
67    /// Set the fuzzy matching preset.
68    pub fn fuzzy_preset(mut self, preset: impl Into<String>) -> Self {
69        self.config.matching.fuzzy_preset = preset.into();
70        self
71    }
72
73    /// Set the matching threshold.
74    pub fn matching_threshold(mut self, threshold: f64) -> Self {
75        self.config.matching.threshold = Some(threshold);
76        self
77    }
78
79    /// Set the output format.
80    pub fn output_format(mut self, format: ReportFormat) -> Self {
81        self.config.output.format = format;
82        self
83    }
84
85    /// Set the output file.
86    pub fn output_file(mut self, file: Option<PathBuf>) -> Self {
87        self.config.output.file = file;
88        self
89    }
90
91    /// Disable colored output.
92    pub fn no_color(mut self, no_color: bool) -> Self {
93        self.config.output.no_color = no_color;
94        self
95    }
96
97    /// Include unchanged components.
98    pub fn include_unchanged(mut self, include: bool) -> Self {
99        self.config.matching.include_unchanged = include;
100        self
101    }
102
103    /// Enable fail-on-vulnerability mode.
104    pub fn fail_on_vuln(mut self, fail: bool) -> Self {
105        self.config.behavior.fail_on_vuln = fail;
106        self
107    }
108
109    /// Enable fail-on-change mode.
110    pub fn fail_on_change(mut self, fail: bool) -> Self {
111        self.config.behavior.fail_on_change = fail;
112        self
113    }
114
115    /// Enable quiet mode.
116    pub fn quiet(mut self, quiet: bool) -> Self {
117        self.config.behavior.quiet = quiet;
118        self
119    }
120
121    /// Enable graph-aware diffing.
122    pub fn graph_diff(mut self, enabled: bool) -> Self {
123        self.config.graph_diff = if enabled {
124            GraphAwareDiffConfig::enabled()
125        } else {
126            GraphAwareDiffConfig::default()
127        };
128        self
129    }
130
131    /// Set matching rules file.
132    pub fn matching_rules_file(mut self, file: Option<PathBuf>) -> Self {
133        self.config.rules.rules_file = file;
134        self
135    }
136
137    /// Set ecosystem rules file.
138    pub fn ecosystem_rules_file(mut self, file: Option<PathBuf>) -> Self {
139        self.config.ecosystem_rules.config_file = file;
140        self
141    }
142
143    /// Enable enrichment.
144    pub fn enrichment(mut self, config: EnrichmentConfig) -> Self {
145        self.config.enrichment = Some(config);
146        self
147    }
148
149    /// Build the AppConfig.
150    pub fn build(self) -> AppConfig {
151        self.config
152    }
153}
154
155// ============================================================================
156// TUI Preferences (persisted)
157// ============================================================================
158
159/// TUI preferences that persist across sessions.
160#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
161pub struct TuiPreferences {
162    /// Theme name: "dark", "light", or "high-contrast"
163    pub theme: String,
164}
165
166impl Default for TuiPreferences {
167    fn default() -> Self {
168        Self {
169            theme: "dark".to_string(),
170        }
171    }
172}
173
174impl TuiPreferences {
175    /// Get the path to the preferences file.
176    pub fn config_path() -> Option<PathBuf> {
177        dirs::config_dir().map(|p| p.join("sbom-tools").join("preferences.json"))
178    }
179
180    /// Load preferences from disk, or return defaults if not found.
181    pub fn load() -> Self {
182        Self::config_path()
183            .and_then(|p| std::fs::read_to_string(p).ok())
184            .and_then(|s| serde_json::from_str(&s).ok())
185            .unwrap_or_default()
186    }
187
188    /// Save preferences to disk.
189    pub fn save(&self) -> std::io::Result<()> {
190        if let Some(path) = Self::config_path() {
191            if let Some(parent) = path.parent() {
192                std::fs::create_dir_all(parent)?;
193            }
194            let json = serde_json::to_string_pretty(self)
195                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
196            std::fs::write(path, json)?;
197        }
198        Ok(())
199    }
200}
201
202// ============================================================================
203// TUI Configuration
204// ============================================================================
205
206/// TUI-specific configuration.
207#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
208#[serde(default)]
209pub struct TuiConfig {
210    /// Theme name: "dark", "light", or "high-contrast"
211    pub theme: String,
212    /// Show line numbers in code views
213    pub show_line_numbers: bool,
214    /// Enable mouse support
215    pub mouse_enabled: bool,
216    /// Initial matching threshold for TUI threshold tuning
217    #[schemars(range(min = 0.0, max = 1.0))]
218    pub initial_threshold: f64,
219}
220
221impl Default for TuiConfig {
222    fn default() -> Self {
223        Self {
224            theme: "dark".to_string(),
225            show_line_numbers: true,
226            mouse_enabled: true,
227            initial_threshold: 0.8,
228        }
229    }
230}
231
232// ============================================================================
233// Command-specific Configuration Types
234// ============================================================================
235
236/// Configuration for diff operations
237#[derive(Debug, Clone)]
238pub struct DiffConfig {
239    /// Paths to compare
240    pub paths: DiffPaths,
241    /// Output configuration
242    pub output: OutputConfig,
243    /// Matching configuration
244    pub matching: MatchingConfig,
245    /// Filtering options
246    pub filtering: FilterConfig,
247    /// Behavior flags
248    pub behavior: BehaviorConfig,
249    /// Graph-aware diffing configuration
250    pub graph_diff: GraphAwareDiffConfig,
251    /// Custom matching rules configuration
252    pub rules: MatchingRulesPathConfig,
253    /// Ecosystem-specific rules configuration
254    pub ecosystem_rules: EcosystemRulesConfig,
255    /// Enrichment configuration (always defined, runtime feature check)
256    pub enrichment: EnrichmentConfig,
257}
258
259/// Paths for diff operation
260#[derive(Debug, Clone)]
261pub struct DiffPaths {
262    /// Path to old/baseline SBOM
263    pub old: PathBuf,
264    /// Path to new SBOM
265    pub new: PathBuf,
266}
267
268/// Configuration for view operations
269#[derive(Debug, Clone)]
270pub struct ViewConfig {
271    /// Path to SBOM file
272    pub sbom_path: PathBuf,
273    /// Output configuration
274    pub output: OutputConfig,
275    /// Whether to validate against NTIA
276    pub validate_ntia: bool,
277    /// Filter by minimum vulnerability severity (critical, high, medium, low)
278    pub min_severity: Option<String>,
279    /// Only show components with vulnerabilities
280    pub vulnerable_only: bool,
281    /// Filter by ecosystem
282    pub ecosystem_filter: Option<String>,
283}
284
285/// Configuration for multi-diff operations
286#[derive(Debug, Clone)]
287pub struct MultiDiffConfig {
288    /// Path to baseline SBOM
289    pub baseline: PathBuf,
290    /// Paths to target SBOMs
291    pub targets: Vec<PathBuf>,
292    /// Output configuration
293    pub output: OutputConfig,
294    /// Matching configuration
295    pub matching: MatchingConfig,
296}
297
298/// Configuration for timeline analysis
299#[derive(Debug, Clone)]
300pub struct TimelineConfig {
301    /// Paths to SBOMs in chronological order
302    pub sbom_paths: Vec<PathBuf>,
303    /// Output configuration
304    pub output: OutputConfig,
305    /// Matching configuration
306    pub matching: MatchingConfig,
307}
308
309/// Configuration for matrix comparison
310#[derive(Debug, Clone)]
311pub struct MatrixConfig {
312    /// Paths to SBOMs
313    pub sbom_paths: Vec<PathBuf>,
314    /// Output configuration
315    pub output: OutputConfig,
316    /// Matching configuration
317    pub matching: MatchingConfig,
318    /// Similarity threshold for clustering (0.0-1.0)
319    pub cluster_threshold: f64,
320}
321
322// ============================================================================
323// Sub-configuration Types
324// ============================================================================
325
326/// Output-related configuration
327#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
328#[serde(default)]
329pub struct OutputConfig {
330    /// Output format
331    pub format: ReportFormat,
332    /// Output file path (None for stdout)
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub file: Option<PathBuf>,
335    /// Report types to include
336    pub report_types: ReportType,
337    /// Disable colored output
338    pub no_color: bool,
339    /// Streaming configuration for large SBOMs
340    pub streaming: StreamingConfig,
341}
342
343impl Default for OutputConfig {
344    fn default() -> Self {
345        Self {
346            format: ReportFormat::Auto,
347            file: None,
348            report_types: ReportType::All,
349            no_color: false,
350            streaming: StreamingConfig::default(),
351        }
352    }
353}
354
355/// Streaming configuration for memory-efficient processing of large SBOMs.
356///
357/// When streaming is enabled, the tool uses streaming parsers and reporters
358/// to avoid loading entire SBOMs into memory. This is essential for SBOMs
359/// with thousands of components.
360#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
361#[serde(default)]
362pub struct StreamingConfig {
363    /// Enable streaming mode automatically for files larger than this threshold (in bytes).
364    /// Default: 10 MB (10_485_760 bytes)
365    #[schemars(range(min = 0))]
366    pub threshold_bytes: u64,
367    /// Force streaming mode regardless of file size.
368    /// Useful for testing or when processing stdin.
369    pub force: bool,
370    /// Disable streaming mode entirely (always load full SBOMs into memory).
371    pub disabled: bool,
372    /// Enable streaming for stdin input (since size is unknown).
373    /// Default: true
374    pub stream_stdin: bool,
375}
376
377impl Default for StreamingConfig {
378    fn default() -> Self {
379        Self {
380            threshold_bytes: 10 * 1024 * 1024, // 10 MB
381            force: false,
382            disabled: false,
383            stream_stdin: true,
384        }
385    }
386}
387
388impl StreamingConfig {
389    /// Check if streaming should be used for a file of the given size.
390    pub fn should_stream(&self, file_size: Option<u64>, is_stdin: bool) -> bool {
391        if self.disabled {
392            return false;
393        }
394        if self.force {
395            return true;
396        }
397        if is_stdin && self.stream_stdin {
398            return true;
399        }
400        match file_size {
401            Some(size) => size >= self.threshold_bytes,
402            None => self.stream_stdin, // Unknown size (e.g., stdin)
403        }
404    }
405
406    /// Create a streaming config that always streams.
407    pub fn always() -> Self {
408        Self {
409            force: true,
410            ..Default::default()
411        }
412    }
413
414    /// Create a streaming config that never streams.
415    pub fn never() -> Self {
416        Self {
417            disabled: true,
418            ..Default::default()
419        }
420    }
421
422    /// Set the threshold in megabytes.
423    pub fn with_threshold_mb(mut self, mb: u64) -> Self {
424        self.threshold_bytes = mb * 1024 * 1024;
425        self
426    }
427}
428
429/// Matching and comparison configuration
430#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
431#[serde(default)]
432pub struct MatchingConfig {
433    /// Fuzzy matching preset name
434    pub fuzzy_preset: String,
435    /// Custom matching threshold (overrides preset)
436    #[serde(skip_serializing_if = "Option::is_none")]
437    #[schemars(range(min = 0.0, max = 1.0))]
438    pub threshold: Option<f64>,
439    /// Include unchanged components in output
440    pub include_unchanged: bool,
441}
442
443impl Default for MatchingConfig {
444    fn default() -> Self {
445        Self {
446            fuzzy_preset: "balanced".to_string(),
447            threshold: None,
448            include_unchanged: false,
449        }
450    }
451}
452
453impl MatchingConfig {
454    /// Convert preset name to FuzzyMatchConfig
455    pub fn to_fuzzy_config(&self) -> FuzzyMatchConfig {
456        let mut config = FuzzyMatchConfig::from_preset(&self.fuzzy_preset).unwrap_or_else(|| {
457            tracing::warn!(
458                "Unknown fuzzy preset '{}', using 'balanced'. Valid: strict, balanced, permissive",
459                self.fuzzy_preset
460            );
461            FuzzyMatchConfig::balanced()
462        });
463
464        // Apply custom threshold if specified
465        if let Some(threshold) = self.threshold {
466            config = config.with_threshold(threshold);
467        }
468
469        config
470    }
471}
472
473/// Filtering options for diff results
474#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
475#[serde(default)]
476pub struct FilterConfig {
477    /// Only show items with changes
478    pub only_changes: bool,
479    /// Minimum severity filter
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub min_severity: Option<String>,
482    /// Exclude vulnerabilities with VEX status not_affected or fixed
483    #[serde(alias = "exclude_vex_not_affected")]
484    pub exclude_vex_resolved: bool,
485}
486
487/// Behavior flags for diff operations
488#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
489#[serde(default)]
490pub struct BehaviorConfig {
491    /// Exit with code 2 if new vulnerabilities are introduced
492    pub fail_on_vuln: bool,
493    /// Exit with code 1 if any changes detected
494    pub fail_on_change: bool,
495    /// Suppress non-essential output
496    pub quiet: bool,
497    /// Show detailed match explanations for each matched component
498    pub explain_matches: bool,
499    /// Recommend optimal matching threshold based on the SBOMs
500    pub recommend_threshold: bool,
501}
502
503/// Graph-aware diffing configuration
504#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
505#[serde(default)]
506pub struct GraphAwareDiffConfig {
507    /// Enable graph-aware diffing
508    pub enabled: bool,
509    /// Detect component reparenting
510    pub detect_reparenting: bool,
511    /// Detect depth changes
512    pub detect_depth_changes: bool,
513}
514
515impl GraphAwareDiffConfig {
516    /// Create enabled graph diff options with defaults
517    pub fn enabled() -> Self {
518        Self {
519            enabled: true,
520            detect_reparenting: true,
521            detect_depth_changes: true,
522        }
523    }
524}
525
526/// Custom matching rules configuration
527#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
528#[serde(default)]
529pub struct MatchingRulesPathConfig {
530    /// Path to matching rules YAML file
531    #[serde(skip_serializing_if = "Option::is_none")]
532    pub rules_file: Option<PathBuf>,
533    /// Dry-run mode (show what would match without applying)
534    pub dry_run: bool,
535}
536
537/// Ecosystem-specific rules configuration
538#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
539#[serde(default)]
540pub struct EcosystemRulesConfig {
541    /// Path to ecosystem rules configuration file
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub config_file: Option<PathBuf>,
544    /// Disable ecosystem-specific normalization
545    pub disabled: bool,
546    /// Enable typosquat detection warnings
547    pub detect_typosquats: bool,
548}
549
550/// Enrichment configuration for vulnerability data sources.
551///
552/// This configuration is always defined regardless of the `enrichment` feature flag.
553/// When the feature is disabled, the configuration is silently ignored at runtime.
554#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
555#[serde(default)]
556pub struct EnrichmentConfig {
557    /// Enable enrichment (if false, no enrichment is performed)
558    pub enabled: bool,
559    /// Enrichment provider ("osv", "nvd", etc.)
560    pub provider: String,
561    /// Cache time-to-live in hours
562    #[schemars(range(min = 1))]
563    pub cache_ttl_hours: u64,
564    /// Maximum concurrent requests
565    #[schemars(range(min = 1))]
566    pub max_concurrent: usize,
567    /// Cache directory for vulnerability data
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub cache_dir: Option<std::path::PathBuf>,
570    /// Bypass cache and fetch fresh vulnerability data
571    pub bypass_cache: bool,
572    /// API timeout in seconds
573    #[schemars(range(min = 1))]
574    pub timeout_secs: u64,
575}
576
577impl Default for EnrichmentConfig {
578    fn default() -> Self {
579        Self {
580            enabled: false,
581            provider: "osv".to_string(),
582            cache_ttl_hours: 24,
583            max_concurrent: 10,
584            cache_dir: None,
585            bypass_cache: false,
586            timeout_secs: 30,
587        }
588    }
589}
590
591impl EnrichmentConfig {
592    /// Create an enabled enrichment config with OSV provider.
593    pub fn osv() -> Self {
594        Self {
595            enabled: true,
596            provider: "osv".to_string(),
597            ..Default::default()
598        }
599    }
600
601    /// Create an enabled enrichment config with custom settings.
602    pub fn with_cache_dir(mut self, dir: std::path::PathBuf) -> Self {
603        self.cache_dir = Some(dir);
604        self
605    }
606
607    /// Set the cache TTL in hours.
608    pub fn with_cache_ttl_hours(mut self, hours: u64) -> Self {
609        self.cache_ttl_hours = hours;
610        self
611    }
612
613    /// Enable cache bypass (refresh).
614    pub fn with_bypass_cache(mut self) -> Self {
615        self.bypass_cache = true;
616        self
617    }
618
619    /// Set the API timeout in seconds.
620    pub fn with_timeout_secs(mut self, secs: u64) -> Self {
621        self.timeout_secs = secs;
622        self
623    }
624}
625
626// ============================================================================
627// Builder for DiffConfig
628// ============================================================================
629
630/// Builder for DiffConfig
631#[derive(Debug, Default)]
632pub struct DiffConfigBuilder {
633    old: Option<PathBuf>,
634    new: Option<PathBuf>,
635    output: OutputConfig,
636    matching: MatchingConfig,
637    filtering: FilterConfig,
638    behavior: BehaviorConfig,
639    graph_diff: GraphAwareDiffConfig,
640    rules: MatchingRulesPathConfig,
641    ecosystem_rules: EcosystemRulesConfig,
642    enrichment: EnrichmentConfig,
643}
644
645impl DiffConfigBuilder {
646    pub fn new() -> Self {
647        Self::default()
648    }
649
650    pub fn old_path(mut self, path: PathBuf) -> Self {
651        self.old = Some(path);
652        self
653    }
654
655    pub fn new_path(mut self, path: PathBuf) -> Self {
656        self.new = Some(path);
657        self
658    }
659
660    pub fn output_format(mut self, format: ReportFormat) -> Self {
661        self.output.format = format;
662        self
663    }
664
665    pub fn output_file(mut self, file: Option<PathBuf>) -> Self {
666        self.output.file = file;
667        self
668    }
669
670    pub fn report_types(mut self, types: ReportType) -> Self {
671        self.output.report_types = types;
672        self
673    }
674
675    pub fn no_color(mut self, no_color: bool) -> Self {
676        self.output.no_color = no_color;
677        self
678    }
679
680    pub fn fuzzy_preset(mut self, preset: String) -> Self {
681        self.matching.fuzzy_preset = preset;
682        self
683    }
684
685    pub fn matching_threshold(mut self, threshold: Option<f64>) -> Self {
686        self.matching.threshold = threshold;
687        self
688    }
689
690    pub fn include_unchanged(mut self, include: bool) -> Self {
691        self.matching.include_unchanged = include;
692        self
693    }
694
695    pub fn only_changes(mut self, only: bool) -> Self {
696        self.filtering.only_changes = only;
697        self
698    }
699
700    pub fn min_severity(mut self, severity: Option<String>) -> Self {
701        self.filtering.min_severity = severity;
702        self
703    }
704
705    pub fn fail_on_vuln(mut self, fail: bool) -> Self {
706        self.behavior.fail_on_vuln = fail;
707        self
708    }
709
710    pub fn fail_on_change(mut self, fail: bool) -> Self {
711        self.behavior.fail_on_change = fail;
712        self
713    }
714
715    pub fn quiet(mut self, quiet: bool) -> Self {
716        self.behavior.quiet = quiet;
717        self
718    }
719
720    pub fn explain_matches(mut self, explain: bool) -> Self {
721        self.behavior.explain_matches = explain;
722        self
723    }
724
725    pub fn recommend_threshold(mut self, recommend: bool) -> Self {
726        self.behavior.recommend_threshold = recommend;
727        self
728    }
729
730    pub fn graph_diff(mut self, enabled: bool) -> Self {
731        self.graph_diff = if enabled {
732            GraphAwareDiffConfig::enabled()
733        } else {
734            GraphAwareDiffConfig::default()
735        };
736        self
737    }
738
739    pub fn matching_rules_file(mut self, file: Option<PathBuf>) -> Self {
740        self.rules.rules_file = file;
741        self
742    }
743
744    pub fn dry_run_rules(mut self, dry_run: bool) -> Self {
745        self.rules.dry_run = dry_run;
746        self
747    }
748
749    pub fn ecosystem_rules_file(mut self, file: Option<PathBuf>) -> Self {
750        self.ecosystem_rules.config_file = file;
751        self
752    }
753
754    pub fn disable_ecosystem_rules(mut self, disabled: bool) -> Self {
755        self.ecosystem_rules.disabled = disabled;
756        self
757    }
758
759    pub fn detect_typosquats(mut self, detect: bool) -> Self {
760        self.ecosystem_rules.detect_typosquats = detect;
761        self
762    }
763
764    pub fn enrichment(mut self, config: EnrichmentConfig) -> Self {
765        self.enrichment = config;
766        self
767    }
768
769    pub fn enable_enrichment(mut self, enabled: bool) -> Self {
770        self.enrichment.enabled = enabled;
771        self
772    }
773
774    pub fn build(self) -> anyhow::Result<DiffConfig> {
775        let old = self.old.ok_or_else(|| anyhow::anyhow!("old path is required"))?;
776        let new = self.new.ok_or_else(|| anyhow::anyhow!("new path is required"))?;
777
778        Ok(DiffConfig {
779            paths: DiffPaths { old, new },
780            output: self.output,
781            matching: self.matching,
782            filtering: self.filtering,
783            behavior: self.behavior,
784            graph_diff: self.graph_diff,
785            rules: self.rules,
786            ecosystem_rules: self.ecosystem_rules,
787            enrichment: self.enrichment,
788        })
789    }
790}