Skip to main content

fraiseql_core/graphql/
complexity.rs

1// GraphQL query complexity analysis — AST-based to prevent DoS attacks.
2//
3// Uses `graphql-parser` to walk the document tree, correctly handling
4// operation names, arguments, fragment spreads, and aliases (which a
5// character-scan approach cannot distinguish from field names).
6
7use std::collections::{HashMap, HashSet};
8
9use graphql_parser::query::{
10    Definition, Document, Field, FragmentDefinition, OperationDefinition, Selection, SelectionSet,
11};
12
13/// Parse a GraphQL query source into a [`graphql_parser`] document.
14///
15/// Exposed so the server's HTTP handler can parse the request exactly once
16/// and reuse the AST for both complexity validation (via
17/// [`RequestValidator::validate_query_doc`]) and the downstream execution path
18/// — replacing the previous "validate then re-parse" pattern. See F001 in
19/// `IMPROVEMENTS.md`.
20///
21/// # Errors
22///
23/// Returns [`ComplexityValidationError::MalformedQuery`] when the source fails
24/// to parse as a GraphQL query document.
25pub fn parse_graphql_document(
26    query: &str,
27) -> Result<Document<'_, String>, ComplexityValidationError> {
28    if query.trim().is_empty() {
29        return Err(ComplexityValidationError::MalformedQuery("Empty query".to_string()));
30    }
31    graphql_parser::parse_query::<String>(query)
32        .map_err(|e| ComplexityValidationError::MalformedQuery(format!("{e}")))
33}
34
35/// Default maximum number of aliases per query (alias amplification protection).
36///
37/// This constant is the single source of truth used by [`ComplexityConfig`],
38/// [`RequestValidator`], the server HTTP handler, and the CLI `explain` command.
39pub const DEFAULT_MAX_ALIASES: usize = 30;
40
41/// Maximum number of variables per request (`DoS` protection).
42///
43/// A single GraphQL request with thousands of variables can cause excessive memory
44/// allocation during deserialization and variable injection. This constant caps
45/// the number of top-level keys in the `variables` JSON object.
46pub const MAX_VARIABLES_COUNT: usize = 1_000;
47
48/// Configuration for query complexity limits.
49#[derive(Debug, Clone)]
50pub struct ComplexityConfig {
51    /// Maximum query depth (nesting level) — default: 10
52    pub max_depth:      usize,
53    /// Maximum complexity score — default: 100
54    pub max_complexity: usize,
55    /// Maximum number of field aliases per query — default: 30
56    pub max_aliases:    usize,
57}
58
59impl Default for ComplexityConfig {
60    fn default() -> Self {
61        Self {
62            max_depth:      10,
63            max_complexity: 100,
64            max_aliases:    DEFAULT_MAX_ALIASES,
65        }
66    }
67}
68
69/// Metrics returned by the AST-based analyzer.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct QueryMetrics {
72    /// Maximum selection-set nesting depth.
73    pub depth:       usize,
74    /// Total complexity score (accounts for pagination multipliers).
75    pub complexity:  usize,
76    /// Number of aliased fields in the document.
77    pub alias_count: usize,
78}
79
80/// GraphQL query validation error types (depth, complexity, aliases).
81#[derive(Debug, thiserror::Error, Clone)]
82#[non_exhaustive]
83pub enum ComplexityValidationError {
84    /// Query exceeds maximum allowed depth.
85    #[error("Query exceeds maximum depth of {max_depth}: depth = {actual_depth}")]
86    QueryTooDeep {
87        /// Maximum allowed depth
88        max_depth:    usize,
89        /// Actual query depth
90        actual_depth: usize,
91    },
92
93    /// Query exceeds maximum complexity score.
94    #[error("Query exceeds maximum complexity of {max_complexity}: score = {actual_complexity}")]
95    QueryTooComplex {
96        /// Maximum allowed complexity
97        max_complexity:    usize,
98        /// Actual query complexity
99        actual_complexity: usize,
100    },
101
102    /// Query contains too many aliases (alias amplification attack).
103    #[error("Query exceeds maximum alias count of {max_aliases}: count = {actual_aliases}")]
104    TooManyAliases {
105        /// Maximum allowed alias count
106        max_aliases:    usize,
107        /// Actual alias count
108        actual_aliases: usize,
109    },
110
111    /// Invalid query variables.
112    #[error("Invalid variables: {0}")]
113    InvalidVariables(String),
114
115    /// Malformed GraphQL query.
116    #[error("Malformed GraphQL query: {0}")]
117    MalformedQuery(String),
118}
119
120/// AST-based GraphQL request validator.
121///
122/// Uses `graphql-parser` to walk the full document tree. Correctly handles
123/// operation names, arguments, fragment spreads, inline fragments, and aliases —
124/// none of which a character-scan can distinguish from field names.
125#[derive(Debug, Clone)]
126pub struct RequestValidator {
127    /// Maximum query depth allowed.
128    max_depth:             usize,
129    /// Maximum query complexity score allowed.
130    max_complexity:        usize,
131    /// Maximum number of field aliases per query (alias amplification protection).
132    max_aliases_per_query: usize,
133    /// Enable query depth validation.
134    validate_depth:        bool,
135    /// Enable query complexity validation.
136    validate_complexity:   bool,
137}
138
139impl RequestValidator {
140    /// Create a new validator with default settings.
141    #[must_use]
142    pub fn new() -> Self {
143        Self::default()
144    }
145
146    /// Create from a `ComplexityConfig`.
147    #[must_use]
148    pub const fn from_config(config: &ComplexityConfig) -> Self {
149        Self {
150            max_depth:             config.max_depth,
151            max_complexity:        config.max_complexity,
152            max_aliases_per_query: config.max_aliases,
153            validate_depth:        true,
154            validate_complexity:   true,
155        }
156    }
157
158    /// Set maximum query depth.
159    #[must_use]
160    pub const fn with_max_depth(mut self, max_depth: usize) -> Self {
161        self.max_depth = max_depth;
162        self
163    }
164
165    /// Set maximum query complexity.
166    #[must_use]
167    pub const fn with_max_complexity(mut self, max_complexity: usize) -> Self {
168        self.max_complexity = max_complexity;
169        self
170    }
171
172    /// Enable/disable depth validation.
173    #[must_use]
174    pub const fn with_depth_validation(mut self, enabled: bool) -> Self {
175        self.validate_depth = enabled;
176        self
177    }
178
179    /// Enable/disable complexity validation.
180    #[must_use]
181    pub const fn with_complexity_validation(mut self, enabled: bool) -> Self {
182        self.validate_complexity = enabled;
183        self
184    }
185
186    /// Set maximum number of aliases per query.
187    #[must_use]
188    pub const fn with_max_aliases(mut self, max_aliases: usize) -> Self {
189        self.max_aliases_per_query = max_aliases;
190        self
191    }
192
193    /// Compute query metrics without enforcing any limits.
194    ///
195    /// Returns [`QueryMetrics`] for the query.
196    /// Used by CLI tooling (`explain`, `cost` commands) where raw metrics
197    /// are needed without a hard rejection.
198    ///
199    /// # Errors
200    ///
201    /// Returns [`ComplexityValidationError::MalformedQuery`] if the query cannot be parsed.
202    pub fn analyze(&self, query: &str) -> Result<QueryMetrics, ComplexityValidationError> {
203        if query.trim().is_empty() {
204            return Err(ComplexityValidationError::MalformedQuery("Empty query".to_string()));
205        }
206        let document = graphql_parser::parse_query::<String>(query)
207            .map_err(|e| ComplexityValidationError::MalformedQuery(format!("{e}")))?;
208        Ok(self.document_metrics(&document))
209    }
210
211    /// Compute depth, complexity, and alias count in a single memoizing pass.
212    ///
213    /// Resolving each fragment's contribution exactly once (and rejecting
214    /// fragment cycles) keeps this linear in document size regardless of
215    /// fragment-spread topology — without it, B-way branching across D chained
216    /// spreads costs B^D walks and the validation step itself becomes the `DoS`
217    /// (audit H4).
218    fn document_metrics<'a>(&self, document: &'a Document<'a, String>) -> QueryMetrics {
219        let fragments = collect_fragments(document);
220        let mut analyzer = DocumentAnalyzer::new(&fragments, self.max_depth, self.max_complexity);
221        analyzer.analyze_document(document)
222    }
223
224    /// Returns true when every configured validation knob is disabled and the
225    /// expensive AST parse can be skipped entirely.
226    ///
227    /// A validator is "fully disabled" only when depth, complexity, AND alias
228    /// checks are all off. The alias-amplification check is a distinct `DoS`
229    /// vector — `max_aliases_per_query == 0` disables it, any other value
230    /// keeps it active even when depth/complexity validation are turned off.
231    #[must_use]
232    pub const fn is_no_op(&self) -> bool {
233        !self.validate_depth && !self.validate_complexity && self.max_aliases_per_query == 0
234    }
235
236    /// Validate a GraphQL query string, enforcing configured limits.
237    ///
238    /// # Errors
239    ///
240    /// Returns [`ComplexityValidationError`] if the query violates any validation rules.
241    pub fn validate_query(&self, query: &str) -> Result<(), ComplexityValidationError> {
242        if query.trim().is_empty() {
243            return Err(ComplexityValidationError::MalformedQuery("Empty query".to_string()));
244        }
245
246        if self.is_no_op() {
247            return Ok(());
248        }
249
250        let document = graphql_parser::parse_query::<String>(query)
251            .map_err(|e| ComplexityValidationError::MalformedQuery(format!("{e}")))?;
252        self.validate_query_doc(&document)
253    }
254
255    /// Validate an already-parsed GraphQL document, enforcing configured limits.
256    ///
257    /// This is the AST-only entry point used by the server's HTTP handler so the
258    /// document is parsed exactly once per request (rather than once by the
259    /// validator and once by the executor's matcher). See F001 in
260    /// `IMPROVEMENTS.md`.
261    ///
262    /// # Errors
263    ///
264    /// Returns [`ComplexityValidationError`] if the document violates depth,
265    /// complexity, or alias-amplification limits.
266    pub fn validate_query_doc<'a>(
267        &self,
268        document: &'a Document<'a, String>,
269    ) -> Result<(), ComplexityValidationError> {
270        if self.is_no_op() {
271            return Ok(());
272        }
273
274        let metrics = self.document_metrics(document);
275
276        if self.validate_depth && metrics.depth > self.max_depth {
277            return Err(ComplexityValidationError::QueryTooDeep {
278                max_depth:    self.max_depth,
279                actual_depth: metrics.depth,
280            });
281        }
282
283        if self.validate_complexity && metrics.complexity > self.max_complexity {
284            return Err(ComplexityValidationError::QueryTooComplex {
285                max_complexity:    self.max_complexity,
286                actual_complexity: metrics.complexity,
287            });
288        }
289
290        if metrics.alias_count > self.max_aliases_per_query {
291            return Err(ComplexityValidationError::TooManyAliases {
292                max_aliases:    self.max_aliases_per_query,
293                actual_aliases: metrics.alias_count,
294            });
295        }
296
297        Ok(())
298    }
299
300    /// Validate variables JSON.
301    ///
302    /// # Errors
303    ///
304    /// Returns [`ComplexityValidationError`] if variables are not a JSON object or exceed
305    /// [`MAX_VARIABLES_COUNT`].
306    ///
307    /// # Panics
308    ///
309    /// Cannot panic in practice — the `expect` on `as_object()` is guarded
310    /// by a preceding `is_object()` check that returns `Err` first.
311    pub fn validate_variables(
312        &self,
313        variables: Option<&serde_json::Value>,
314    ) -> Result<(), ComplexityValidationError> {
315        if let Some(vars) = variables {
316            if !vars.is_object() {
317                return Err(ComplexityValidationError::InvalidVariables(
318                    "Variables must be an object".to_string(),
319                ));
320            }
321            // Reason: `is_object()` check on the line above guarantees this is an object;
322            // `as_object()` cannot return None here.
323            let obj = vars.as_object().expect("invariant: vars.is_object() checked above");
324            if obj.len() > MAX_VARIABLES_COUNT {
325                return Err(ComplexityValidationError::InvalidVariables(format!(
326                    "Too many variables: {} exceeds maximum of {}",
327                    obj.len(),
328                    MAX_VARIABLES_COUNT
329                )));
330            }
331        }
332        Ok(())
333    }
334}
335
336/// Hard ceiling on fragment-spread chain length the analyzer resolves before
337/// declaring the document over-limit. Mirrors the pre-memoization 32-hop
338/// recursion guard: a document nesting fragment spreads beyond this is rejected
339/// (real APIs never approach it), which also bounds the analyzer's own
340/// recursion depth so a long fragment chain cannot overflow the stack. Fragment
341/// cycles are caught earlier by the [`DocumentAnalyzer`] `visiting` set.
342const FRAGMENT_SPREAD_DEPTH_LIMIT: usize = 32;
343
344/// Depth, complexity, and alias contribution of one selection set (or one
345/// fragment). Computed once per fragment and memoized by name so a fragment
346/// spread costs a map lookup instead of a re-walk.
347#[derive(Debug, Clone, Copy)]
348struct SelectionMetrics {
349    depth:      usize,
350    complexity: usize,
351    aliases:    usize,
352}
353
354/// Single-pass, memoizing analyzer shared by depth, complexity, and alias
355/// counting.
356///
357/// Each fragment's metrics are resolved exactly once (on demand) and cached by
358/// name; fragment cycles are detected via `visiting` and treated as over-limit
359/// rather than recursed into. This is what bounds the validator's own cost —
360/// the previous per-spread re-walk made the validation step itself a `DoS`:
361/// B-way branching across D chained fragment spreads cost B^D walks (audit H4),
362/// and aliases hidden inside spread fragments were never counted, bypassing the
363/// alias-amplification limit (audit H4, alias counter).
364struct DocumentAnalyzer<'a> {
365    by_name:        HashMap<&'a str, &'a FragmentDefinition<'a, String>>,
366    memo:           HashMap<&'a str, SelectionMetrics>,
367    visiting:       HashSet<&'a str>,
368    max_depth:      usize,
369    max_complexity: usize,
370}
371
372impl<'a> DocumentAnalyzer<'a> {
373    fn new(
374        fragments: &[&'a FragmentDefinition<'a, String>],
375        max_depth: usize,
376        max_complexity: usize,
377    ) -> Self {
378        let mut by_name = HashMap::with_capacity(fragments.len());
379        for frag in fragments {
380            // First definition wins, matching the previous `.find()` lookup.
381            by_name.entry(frag.name.as_str()).or_insert(*frag);
382        }
383        Self {
384            by_name,
385            memo: HashMap::new(),
386            visiting: HashSet::new(),
387            max_depth,
388            max_complexity,
389        }
390    }
391
392    /// Metrics that force rejection on both the depth and complexity gates,
393    /// used for fragment cycles and over-long spread chains (never executable).
394    const fn over_limit(&self) -> SelectionMetrics {
395        SelectionMetrics {
396            depth:      self.max_depth.saturating_add(1),
397            complexity: self.max_complexity.saturating_add(1),
398            aliases:    0,
399        }
400    }
401
402    /// Reduce the whole document to a single [`QueryMetrics`].
403    ///
404    /// Depth is the max across every definition (operations *and* fragment
405    /// definitions, preserving legacy behaviour); complexity sums operations
406    /// only; the alias total sums operations only, where each fragment spread
407    /// contributes the fragment's own alias count *per occurrence* — that is
408    /// the alias-amplification fix (the old counter scored spreads as 0).
409    fn analyze_document(&mut self, document: &'a Document<'a, String>) -> QueryMetrics {
410        let mut depth = 0usize;
411        let mut complexity = 0usize;
412        let mut aliases = 0usize;
413        for def in &document.definitions {
414            match def {
415                Definition::Operation(op) => {
416                    let m = self.selection_metrics(
417                        operation_selection_set(op),
418                        FRAGMENT_SPREAD_DEPTH_LIMIT,
419                    );
420                    depth = depth.max(m.depth);
421                    complexity = complexity.saturating_add(m.complexity);
422                    aliases = aliases.saturating_add(m.aliases);
423                },
424                Definition::Fragment(f) => {
425                    let m = self.resolve_fragment(f.name.as_str(), FRAGMENT_SPREAD_DEPTH_LIMIT);
426                    depth = depth.max(m.depth);
427                },
428            }
429        }
430        QueryMetrics {
431            depth,
432            complexity,
433            alias_count: aliases,
434        }
435    }
436
437    /// Memoized metrics for a fragment by name.
438    ///
439    /// `budget` is the remaining fragment-spread hops; it decrements on each
440    /// spread resolution and a cycle (`visiting`) or exhausted budget yields the
441    /// over-limit sentinel without recursing. Cut/cycle results are deliberately
442    /// not memoized — they are context-dependent, not the fragment's intrinsic
443    /// metric.
444    fn resolve_fragment(&mut self, name: &'a str, budget: usize) -> SelectionMetrics {
445        if let Some(m) = self.memo.get(name) {
446            return *m;
447        }
448        if self.visiting.contains(name) || budget == 0 {
449            return self.over_limit();
450        }
451        let Some(frag) = self.by_name.get(name).copied() else {
452            // Unknown fragment: preserve the legacy contribution — depth =
453            // max_depth (so the enclosing selection set trips the depth gate),
454            // complexity = 10, no aliases.
455            let m = SelectionMetrics {
456                depth:      self.max_depth,
457                complexity: 10,
458                aliases:    0,
459            };
460            self.memo.insert(name, m);
461            return m;
462        };
463        self.visiting.insert(name);
464        let m = self.selection_metrics(&frag.selection_set, budget - 1);
465        self.visiting.remove(name);
466        self.memo.insert(name, m);
467        m
468    }
469
470    fn selection_metrics(
471        &mut self,
472        selection_set: &'a SelectionSet<'a, String>,
473        budget: usize,
474    ) -> SelectionMetrics {
475        let mut max_child_depth = 0usize;
476        let mut complexity = 0usize;
477        let mut aliases = 0usize;
478        for sel in &selection_set.items {
479            let child = match sel {
480                Selection::Field(field) => {
481                    let self_alias = usize::from(field.alias.is_some());
482                    if field.selection_set.items.is_empty() {
483                        SelectionMetrics {
484                            depth:      0,
485                            complexity: 1,
486                            aliases:    self_alias,
487                        }
488                    } else {
489                        let nested = self.selection_metrics(&field.selection_set, budget);
490                        let multiplier = extract_limit_multiplier(&field.arguments);
491                        SelectionMetrics {
492                            depth:      nested.depth,
493                            // Saturating: the multiplier compounds per nesting
494                            // level, so a crafted deep query overflows `usize`.
495                            // Saturating to `usize::MAX` keeps the score
496                            // monotonic and fail-closed (the limit always
497                            // rejects it) rather than wrapping under the limit.
498                            complexity: 1usize
499                                .saturating_add(nested.complexity.saturating_mul(multiplier)),
500                            aliases:    self_alias.saturating_add(nested.aliases),
501                        }
502                    }
503                },
504                Selection::InlineFragment(inline) => {
505                    self.selection_metrics(&inline.selection_set, budget)
506                },
507                Selection::FragmentSpread(spread) => {
508                    self.resolve_fragment(spread.fragment_name.as_str(), budget)
509                },
510            };
511            max_child_depth = max_child_depth.max(child.depth);
512            complexity = complexity.saturating_add(child.complexity);
513            aliases = aliases.saturating_add(child.aliases);
514        }
515        let depth = if selection_set.items.is_empty() {
516            0
517        } else {
518            max_child_depth.saturating_add(1)
519        };
520        SelectionMetrics {
521            depth,
522            complexity,
523            aliases,
524        }
525    }
526
527    /// Complexity contribution of a single field — `1` for a leaf, else
528    /// `1 + nested_complexity * pagination_multiplier`. This is exactly the
529    /// per-field score [`selection_metrics`](Self::selection_metrics) assigns, so
530    /// a cost walk with no `@cost` overrides equals the complexity score.
531    fn field_complexity(&mut self, field: &'a Field<'a, String>, budget: usize) -> usize {
532        if field.selection_set.items.is_empty() {
533            1
534        } else {
535            let nested = self.selection_metrics(&field.selection_set, budget);
536            let multiplier = extract_limit_multiplier(&field.arguments);
537            1usize.saturating_add(nested.complexity.saturating_mul(multiplier))
538        }
539    }
540
541    /// Document cost for per-tenant budget enforcement.
542    ///
543    /// Equals the complexity score, except a top-level (root) operation field
544    /// whose name carries an `@cost` weight contributes exactly that weight (its
545    /// subtree is not walked). Root inline fragments/spreads fall back to the
546    /// normal complexity contribution.
547    fn document_cost<S: std::hash::BuildHasher>(
548        &mut self,
549        document: &'a Document<'a, String>,
550        root_weights: &HashMap<String, usize, S>,
551    ) -> usize {
552        let mut cost = 0usize;
553        for def in &document.definitions {
554            if let Definition::Operation(op) = def {
555                cost =
556                    cost.saturating_add(self.root_cost(operation_selection_set(op), root_weights));
557            }
558        }
559        cost
560    }
561
562    fn root_cost<S: std::hash::BuildHasher>(
563        &mut self,
564        selection_set: &'a SelectionSet<'a, String>,
565        root_weights: &HashMap<String, usize, S>,
566    ) -> usize {
567        let mut cost = 0usize;
568        for sel in &selection_set.items {
569            let c = match sel {
570                Selection::Field(field) => root_weights
571                    .get(field.name.as_str())
572                    .copied()
573                    .unwrap_or_else(|| self.field_complexity(field, FRAGMENT_SPREAD_DEPTH_LIMIT)),
574                Selection::InlineFragment(inline) => {
575                    self.root_cost(&inline.selection_set, root_weights)
576                },
577                Selection::FragmentSpread(spread) => {
578                    self.resolve_fragment(
579                        spread.fragment_name.as_str(),
580                        FRAGMENT_SPREAD_DEPTH_LIMIT,
581                    )
582                    .complexity
583                },
584            };
585            cost = cost.saturating_add(c);
586        }
587        cost
588    }
589}
590
591/// Estimate a parsed document's cost for per-tenant budget enforcement.
592///
593/// The cost equals the AST complexity score (so it is consistent with the
594/// `max_complexity` gate), except a top-level operation field whose name appears
595/// in `root_cost_weights` contributes exactly that `@cost` weight instead of its
596/// walked subtree. With an empty map the result equals
597/// [`RequestValidator::analyze`]'s `complexity`.
598#[must_use]
599pub fn estimate_query_cost<'a, S: std::hash::BuildHasher>(
600    document: &'a Document<'a, String>,
601    root_cost_weights: &HashMap<String, usize, S>,
602) -> usize {
603    let fragments = collect_fragments(document);
604    // usize::MAX sentinels so a fragment cycle yields a saturating (rejected) cost
605    // rather than an artificial cap.
606    let mut analyzer = DocumentAnalyzer::new(&fragments, usize::MAX, usize::MAX);
607    analyzer.document_cost(document, root_cost_weights)
608}
609
610/// The top-level selection set of any operation kind.
611const fn operation_selection_set<'a>(
612    op: &'a OperationDefinition<'a, String>,
613) -> &'a SelectionSet<'a, String> {
614    match op {
615        OperationDefinition::Query(q) => &q.selection_set,
616        OperationDefinition::Mutation(m) => &m.selection_set,
617        OperationDefinition::Subscription(s) => &s.selection_set,
618        OperationDefinition::SelectionSet(ss) => ss,
619    }
620}
621
622impl Default for RequestValidator {
623    fn default() -> Self {
624        Self {
625            max_depth:             10,
626            max_complexity:        100,
627            max_aliases_per_query: DEFAULT_MAX_ALIASES,
628            validate_depth:        true,
629            validate_complexity:   true,
630        }
631    }
632}
633
634/// Collect all fragment definitions from a parsed document.
635fn collect_fragments<'a>(
636    document: &'a Document<'a, String>,
637) -> Vec<&'a FragmentDefinition<'a, String>> {
638    document
639        .definitions
640        .iter()
641        .filter_map(|def| {
642            if let Definition::Fragment(f) = def {
643                Some(f)
644            } else {
645                None
646            }
647        })
648        .collect()
649}
650
651/// Extract pagination limit from field arguments to use as a cost multiplier.
652fn extract_limit_multiplier(arguments: &[(String, graphql_parser::query::Value<String>)]) -> usize {
653    for (name, value) in arguments {
654        if matches!(name.as_str(), "first" | "limit" | "take" | "last") {
655            if let graphql_parser::query::Value::Int(n) = value {
656                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
657                // Reason: value is clamped to [1, 100] immediately after; truncation and sign loss
658                // are safe
659                let limit = n.as_i64().unwrap_or(10) as usize;
660                return limit.clamp(1, 100);
661            }
662        }
663    }
664    1
665}