Skip to main content

driver_tools/subcommands/doctor/
mod.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5pub mod args;
6
7use anyhow::{Context, Result, anyhow};
8use bind::bytecode_constants::{FALSE_VAL, RawOp, RawValueType, TRUE_VAL};
9use bind::compiler::Symbol;
10use bind::compiler::symbol_table::get_deprecated_key_identifier;
11use bind::interpreter::common::{BytecodeIter, next_u8, next_u32};
12use bind::interpreter::decode_bind_rules::{DecodedCompositeBindRules, DecodedRules};
13use bind::interpreter::match_bind::{DeviceProperties, MatchBindData, PropertyKey, match_bind};
14use flex_client::ProxyHasDomain;
15use flex_fuchsia_driver_development as fdd;
16use flex_fuchsia_driver_framework as fdf;
17#[cfg(not(feature = "fdomain"))]
18use fuchsia_driver_dev as fdev;
19#[cfg(feature = "fdomain")]
20use fuchsia_driver_dev_fdomain as fdev;
21use itertools::Itertools;
22use std::collections::{HashMap, HashSet};
23use std::io;
24
25trait DiagnosableParent {
26    fn to_properties(&self) -> DeviceProperties;
27    fn evaluate_bind_rules(&self, properties: &DeviceProperties) -> Vec<Diagnostic>;
28    fn is_fuzzy_match(&self, properties: &DeviceProperties) -> bool;
29}
30
31impl DiagnosableParent for fdf::ParentSpec {
32    fn to_properties(&self) -> DeviceProperties {
33        node_to_bind_properties(Some(&self.properties))
34    }
35    fn evaluate_bind_rules(&self, properties: &DeviceProperties) -> Vec<Diagnostic> {
36        evaluate_bind_rules(&self.bind_rules, properties)
37    }
38    fn is_fuzzy_match(&self, properties: &DeviceProperties) -> bool {
39        is_spec_fuzzy_match(&self.bind_rules, properties)
40    }
41}
42
43impl DiagnosableParent for fdf::ParentSpec2 {
44    fn to_properties(&self) -> DeviceProperties {
45        node_to_bind_properties2(Some(&self.properties))
46    }
47    fn evaluate_bind_rules(&self, properties: &DeviceProperties) -> Vec<Diagnostic> {
48        evaluate_bind_rules2(&self.bind_rules, properties)
49    }
50    fn is_fuzzy_match(&self, properties: &DeviceProperties) -> bool {
51        is_spec_fuzzy_match2(&self.bind_rules, properties)
52    }
53}
54
55pub async fn doctor(
56    cmd: args::DoctorCommand,
57    driver_dev_proxy: fdd::ManagerProxy,
58    writer: &mut dyn io::Write,
59) -> Result<()> {
60    match (cmd.driver.as_ref(), cmd.node.as_ref(), cmd.composite_node_spec.as_ref()) {
61        (Some(driver_filter), Some(node_moniker), _) => {
62            let driver = resolve_driver(driver_filter, &driver_dev_proxy, writer).await?;
63            if let Some(driver) = driver {
64                diagnose_driver_and_node(&driver, node_moniker, &driver_dev_proxy, writer).await?;
65            }
66        }
67        (None, Some(node_moniker), Some(spec_name)) => {
68            diagnose_spec_and_node(spec_name, node_moniker, &driver_dev_proxy, writer).await?;
69        }
70        (Some(driver_filter), None, Some(spec_name)) => {
71            let driver = resolve_driver(driver_filter, &driver_dev_proxy, writer).await?;
72            if let Some(driver) = driver {
73                diagnose_driver_and_spec(&driver, spec_name, &driver_dev_proxy, writer).await?;
74            }
75        }
76        (Some(driver_filter), None, None) => {
77            let driver = resolve_driver(driver_filter, &driver_dev_proxy, writer).await?;
78            if let Some(driver) = driver {
79                diagnose_driver(&driver, &driver_dev_proxy, writer).await?;
80            }
81        }
82        (None, Some(node_moniker), None) => {
83            diagnose_node(node_moniker, &driver_dev_proxy, writer).await?;
84        }
85        (None, None, Some(spec_name)) => {
86            diagnose_spec(spec_name, &driver_dev_proxy, writer).await?;
87        }
88        (None, None, None) => {
89            diagnose_all(&driver_dev_proxy, writer).await?;
90        }
91    }
92
93    Ok(())
94}
95
96async fn resolve_driver(
97    filter: &str,
98    driver_dev_proxy: &fdd::ManagerProxy,
99    writer: &mut dyn io::Write,
100) -> Result<Option<fdf::DriverInfo>> {
101    let matched_drivers = fdev::get_drivers_from_query(filter, driver_dev_proxy).await?;
102
103    if matched_drivers.is_empty() {
104        writeln!(writer, "ERROR: No drivers matched the filter '{}'.", filter)?;
105        return Ok(None);
106    }
107
108    if matched_drivers.len() > 1 {
109        if let Some(exact) = matched_drivers.iter().find(|d| d.url.as_deref() == Some(filter)) {
110            return Ok(Some(exact.clone()));
111        }
112
113        writeln!(writer, "ERROR: Multiple drivers matched the filter '{}':", filter)?;
114        for driver in &matched_drivers {
115            writeln!(writer, "  {}", driver.url.as_deref().unwrap_or("unknown"))?;
116        }
117        writeln!(writer, "Please provide a more specific filter.")?;
118        return Ok(None);
119    }
120
121    Ok(Some(matched_drivers[0].clone()))
122}
123
124async fn diagnose_driver_and_node(
125    driver: &fdf::DriverInfo,
126    node_moniker: &str,
127    driver_dev_proxy: &fdd::ManagerProxy,
128    writer: &mut dyn io::Write,
129) -> Result<()> {
130    let driver_url = driver.url.as_deref().unwrap_or("unknown");
131    writeln!(writer, "Diagnosing driver {} against node {}", driver_url, node_moniker)?;
132    let nodes = fdev::get_device_info(driver_dev_proxy, &[node_moniker.to_string()], true)
133        .await
134        .context("Failed to find node")?;
135    if nodes.is_empty() {
136        return Err(anyhow!("Node not found: {}", node_moniker));
137    }
138    let node = &nodes[0];
139
140    if !is_node_unbound(node) {
141        if let Some(bound_url) = &node.bound_driver_url {
142            if bound_url == driver_url {
143                writeln!(writer, "  Node is already bound to this driver.")?;
144            } else {
145                writeln!(writer, "  Node is bound to a different driver: {}", bound_url)?;
146            }
147        }
148    }
149
150    if let Some(bytecode) = &driver.bind_rules_bytecode {
151        match DecodedRules::new(bytecode.clone())? {
152            DecodedRules::Normal(rules) => {
153                let properties = node_to_bind_properties(node.node_property_list.as_deref());
154                if match_bind(
155                    MatchBindData {
156                        symbol_table: &rules.symbol_table,
157                        instructions: &rules.instructions,
158                    },
159                    &properties,
160                )
161                .unwrap_or(false)
162                {
163                    writeln!(writer, "  Matches!")?;
164                } else {
165                    let diagnostics = evaluate_rules_bytecode(
166                        &rules.symbol_table,
167                        &rules.instructions,
168                        &properties,
169                    );
170                    report_diagnostics(&diagnostics, writer, 2)?;
171                }
172            }
173            DecodedRules::Composite(_) => {
174                writeln!(
175                    writer,
176                    "Driver is a composite driver. Use --composite-node-spec to diagnose if you know which spec it should match."
177                )?;
178            }
179        }
180    } else {
181        writeln!(writer, "Driver has no bind rules bytecode.")?;
182    }
183
184    Ok(())
185}
186
187async fn diagnose_spec_and_node(
188    spec_name: &str,
189    node_moniker: &str,
190    driver_dev_proxy: &fdd::ManagerProxy,
191    writer: &mut dyn io::Write,
192) -> Result<()> {
193    writeln!(writer, "Diagnosing composite node spec {} against node {}", spec_name, node_moniker)?;
194    let all_specs = fdev::get_composite_node_specs(driver_dev_proxy, None)
195        .await
196        .context("Failed to get composite node specs")?;
197    let spec = all_specs
198        .iter()
199        .find(|s| {
200            s.spec
201                .as_ref()
202                .and_then(|spec| spec.name.as_ref())
203                .map(|name| name == spec_name)
204                .unwrap_or(false)
205        })
206        .ok_or_else(|| anyhow!("Spec not found: {}", spec_name))?;
207
208    let nodes = fdev::get_device_info(driver_dev_proxy, &[node_moniker.to_string()], true)
209        .await
210        .context("Failed to find node")?;
211    if nodes.is_empty() {
212        return Err(anyhow!("Node not found: {}", node_moniker));
213    }
214    let node = &nodes[0];
215    let node_properties = node_to_bind_properties(node.node_property_list.as_deref());
216
217    if let Some(spec) = &spec.spec {
218        if let Some(parents) = &spec.parents {
219            for (i, parent) in parents.iter().enumerate() {
220                writeln!(writer, "\nComparing against spec parent {}:", i)?;
221                let diagnostics = parent.evaluate_bind_rules(&node_properties);
222                report_diagnostics(&diagnostics, writer, 2)?;
223            }
224        } else if let Some(parents2) = &spec.parents2 {
225            for (i, parent) in parents2.iter().enumerate() {
226                writeln!(writer, "\nComparing against spec parent {}:", i)?;
227                let diagnostics = parent.evaluate_bind_rules(&node_properties);
228                report_diagnostics(&diagnostics, writer, 2)?;
229            }
230        }
231    }
232
233    Ok(())
234}
235
236async fn diagnose_driver_and_spec(
237    driver: &fdf::DriverInfo,
238    spec_name: &str,
239    driver_dev_proxy: &fdd::ManagerProxy,
240    writer: &mut dyn io::Write,
241) -> Result<()> {
242    let driver_url = driver.url.as_deref().unwrap_or("unknown");
243    writeln!(writer, "Diagnosing driver {} against composite node spec {}", driver_url, spec_name)?;
244
245    let all_specs = fdev::get_composite_node_specs(driver_dev_proxy, None)
246        .await
247        .context("Failed to get composite node specs")?;
248
249    let spec = all_specs
250        .iter()
251        .find(|s| {
252            s.spec
253                .as_ref()
254                .and_then(|spec| spec.name.as_ref())
255                .map(|name| name == spec_name)
256                .unwrap_or(false)
257        })
258        .ok_or_else(|| anyhow!("Spec not found: {}", spec_name))?;
259
260    if let Some(bytecode) = &driver.bind_rules_bytecode {
261        match DecodedRules::new(bytecode.clone())? {
262            DecodedRules::Normal(_) => {
263                writeln!(
264                    writer,
265                    "Driver is NOT a composite driver, but it is being compared against a composite node spec."
266                )?;
267            }
268            DecodedRules::Composite(rules) => {
269                if let Some(spec) = &spec.spec {
270                    if let Some(parents) = &spec.parents {
271                        diagnose_composite_match(&rules, parents, writer)?;
272                    } else if let Some(parents2) = &spec.parents2 {
273                        diagnose_composite_match(&rules, parents2, writer)?;
274                    } else {
275                        writeln!(writer, "  Spec '{}' has no parents.", spec_name)?;
276                    }
277                } else {
278                    writeln!(writer, "  Spec '{}' is missing detailed information.", spec_name)?;
279                }
280            }
281        }
282    } else {
283        writeln!(writer, "  Driver '{}' has no bind rules bytecode.", driver_url)?;
284    }
285
286    Ok(())
287}
288
289fn diagnose_composite_match<P: DiagnosableParent>(
290    rules: &DecodedCompositeBindRules,
291    parents: &[P],
292    writer: &mut dyn io::Write,
293) -> Result<()> {
294    if parents.is_empty() {
295        writeln!(writer, "  Spec has no parents.")?;
296        return Ok(());
297    }
298
299    let mut matched_parents = HashSet::new();
300
301    writeln!(writer, "\nMatching primary node...")?;
302
303    let mut primary_matched = false;
304    for (p_idx, parent) in parents.iter().enumerate() {
305        let props = parent.to_properties();
306        if match_bind(
307            MatchBindData {
308                symbol_table: &rules.symbol_table,
309                instructions: &rules.primary_parent.instructions,
310            },
311            &props,
312        )
313        .unwrap_or(false)
314        {
315            writeln!(writer, "  Primary node matches parent {}.", p_idx)?;
316            primary_matched = true;
317            matched_parents.insert(p_idx);
318            break;
319        }
320    }
321
322    if !primary_matched {
323        writeln!(
324            writer,
325            "  ERROR: Primary node matches no parent in spec. Mismatches against parent 0:"
326        )?;
327        let primary_props = parents[0].to_properties();
328        let diagnostics = evaluate_rules_bytecode(
329            &rules.symbol_table,
330            &rules.primary_parent.instructions,
331            &primary_props,
332        );
333        report_diagnostics(&diagnostics, writer, 4)?;
334    }
335
336    for (i, node) in rules.additional_parents.iter().enumerate() {
337        let node_name = rules
338            .symbol_table
339            .get(&node.name_id)
340            .cloned()
341            .unwrap_or_else(|| format!("additional_{}", i));
342        writeln!(writer, "\nMatching additional node {} ({})...", i, node_name)?;
343
344        let mut matched = false;
345        for (p_idx, parent) in parents.iter().enumerate() {
346            if matched_parents.contains(&p_idx) {
347                continue;
348            }
349            let props = parent.to_properties();
350            if match_bind(
351                MatchBindData {
352                    symbol_table: &rules.symbol_table,
353                    instructions: &node.instructions,
354                },
355                &props,
356            )
357            .unwrap_or(false)
358            {
359                writeln!(writer, "  Matches parent {}.", p_idx)?;
360                matched = true;
361                matched_parents.insert(p_idx);
362                break;
363            }
364        }
365
366        if !matched {
367            writeln!(writer, "  ERROR: No parent in spec matches additional node {}.", node_name)?;
368
369            let mut best_parent = None;
370            let mut best_score = 0;
371            let driver_keys = get_keys_from_instructions(&node.decoded_instructions);
372
373            for (p_idx, parent) in parents.iter().enumerate() {
374                if matched_parents.contains(&p_idx) {
375                    continue;
376                }
377                let props = parent.to_properties();
378                let score = driver_keys.iter().filter(|k| props.contains_key(k)).count();
379                if score >= best_score {
380                    best_score = score;
381                    best_parent = Some(p_idx);
382                }
383            }
384
385            if let Some(p_idx) = best_parent {
386                writeln!(writer, "  Mismatches against parent {}:", p_idx)?;
387                let props = parents[p_idx].to_properties();
388                let diags =
389                    evaluate_rules_bytecode(&rules.symbol_table, &node.instructions, &props);
390                report_diagnostics(&diags, writer, 4)?;
391            } else {
392                writeln!(
393                    writer,
394                    "  No available parent in the spec to compare against. This can happen if the driver has more nodes than the spec has parents."
395                )?;
396            }
397        }
398    }
399
400    let mut unmatched_parents = Vec::new();
401    for p_idx in 0..parents.len() {
402        if !matched_parents.contains(&p_idx) {
403            unmatched_parents.push(p_idx);
404        }
405    }
406
407    if !unmatched_parents.is_empty() {
408        writeln!(
409            writer,
410            "\n  ERROR: Driver bind rules missing matches for {} parents in the spec.",
411            unmatched_parents.len()
412        )?;
413        for p_idx in unmatched_parents {
414            writeln!(writer, "  Parent {} is unmatched.", p_idx)?;
415        }
416    }
417
418    Ok(())
419}
420
421async fn diagnose_driver(
422    driver: &fdf::DriverInfo,
423    driver_dev_proxy: &fdd::ManagerProxy,
424    writer: &mut dyn io::Write,
425) -> Result<()> {
426    let driver_url = driver.url.as_deref().unwrap_or("unknown");
427    writeln!(writer, "Diagnosing driver {}", driver_url)?;
428
429    let nodes = fdev::get_device_info(driver_dev_proxy, &[], false).await?;
430    let bound_nodes = nodes
431        .iter()
432        .filter_map(|n| {
433            if let Some(bound_url) = &n.bound_driver_url {
434                if bound_url == driver_url {
435                    return Some(n.moniker.as_deref().unwrap_or("unknown"));
436                }
437            }
438            None
439        })
440        .collect_vec();
441
442    if !bound_nodes.is_empty() {
443        writeln!(writer, "\nDriver {} is bound to the following nodes:", driver_url)?;
444        for node in bound_nodes {
445            writeln!(writer, "  {}", node)?;
446        }
447    }
448
449    if let Some(bytecode) = &driver.bind_rules_bytecode {
450        let rules = DecodedRules::new(bytecode.clone())?;
451
452        match &rules {
453            DecodedRules::Normal(r) => {
454                writeln!(writer, "\nFuzzy matching against all unbound nodes...")?;
455                let unbound_nodes = nodes.into_iter().filter(|n| is_node_unbound(n)).collect_vec();
456                for node in unbound_nodes {
457                    let props = node_to_bind_properties(node.node_property_list.as_deref());
458                    if is_fuzzy_match(&rules, &props) {
459                        writeln!(
460                            writer,
461                            "\n------------------------------------------------------------\nPotential match found: node {}",
462                            node.moniker.as_deref().unwrap_or("unknown")
463                        )?;
464                        if match_bind(
465                            MatchBindData {
466                                symbol_table: &r.symbol_table,
467                                instructions: &r.instructions,
468                            },
469                            &props,
470                        )
471                        .unwrap_or(false)
472                        {
473                            writeln!(writer, "  Matches!")?;
474                        } else {
475                            let diags =
476                                evaluate_rules_bytecode(&r.symbol_table, &r.instructions, &props);
477                            report_diagnostics(&diags, writer, 2)?;
478                        }
479                    }
480                }
481            }
482            DecodedRules::Composite(r) => {
483                let specs = fdev::get_composite_node_specs(driver_dev_proxy, None).await?;
484                let (matched_specs, unmatched_specs): (Vec<_>, Vec<_>) =
485                    specs.into_iter().partition(|s| {
486                        if let Some(matched) = &s.matched_driver {
487                            if let Some(composite_driver) = &matched.composite_driver {
488                                if let Some(info) = &composite_driver.driver_info {
489                                    if let Some(url) = &info.url {
490                                        return url == driver_url;
491                                    }
492                                }
493                            }
494                        }
495                        false
496                    });
497
498                if !matched_specs.is_empty() {
499                    writeln!(writer, "\nDriver is matched to the following composite node specs:")?;
500                    for spec_info in matched_specs {
501                        let spec_name = spec_info
502                            .spec
503                            .as_ref()
504                            .and_then(|s| s.name.as_deref())
505                            .unwrap_or("unknown");
506                        writeln!(
507                            writer,
508                            "  {}\n  (Run `ffx driver doctor --composite-node-spec {}` to diagnose parents)",
509                            spec_name, spec_name
510                        )?;
511                    }
512                }
513
514                writeln!(writer, "\nFuzzy matching against all unmatched composite node specs...")?;
515                let unmatched_specs = unmatched_specs
516                    .into_iter()
517                    .filter(|s| s.matched_driver.is_none())
518                    .collect_vec();
519                for spec_info in unmatched_specs {
520                    if let Some(spec) = &spec_info.spec {
521                        let mut potential_match = false;
522                        if let Some(parents) = &spec.parents {
523                            if parents.iter().any(|p| is_fuzzy_match(&rules, &p.to_properties())) {
524                                potential_match = true;
525                            }
526                        } else if let Some(parents2) = &spec.parents2 {
527                            if parents2.iter().any(|p| is_fuzzy_match(&rules, &p.to_properties())) {
528                                potential_match = true;
529                            }
530                        }
531
532                        if potential_match {
533                            writeln!(
534                                writer,
535                                "\n------------------------------------------------------------\nPotential match found: spec {}",
536                                spec.name.as_deref().unwrap_or("unknown")
537                            )?;
538                            if let Some(parents) = &spec.parents {
539                                diagnose_composite_match(r, parents, writer)?;
540                            } else if let Some(parents2) = &spec.parents2 {
541                                diagnose_composite_match(r, parents2, writer)?;
542                            }
543                        }
544                    }
545                }
546            }
547        }
548    }
549    Ok(())
550}
551
552fn is_fuzzy_match(rules: &DecodedRules, properties: &DeviceProperties) -> bool {
553    let keys = match rules {
554        DecodedRules::Normal(r) => get_keys_from_instructions(&r.decoded_instructions),
555        DecodedRules::Composite(r) => {
556            let mut k = get_keys_from_instructions(&r.primary_parent.decoded_instructions);
557            for n in &r.additional_parents {
558                k.extend(get_keys_from_instructions(&n.decoded_instructions));
559            }
560            k
561        }
562    };
563    keys.iter().any(|k| properties.contains_key(k))
564}
565
566fn get_keys_from_instructions(
567    insts: &[bind::interpreter::instruction_decoder::DecodedInstruction],
568) -> Vec<PropertyKey> {
569    let mut keys = Vec::new();
570    for inst in insts {
571        if let bind::interpreter::instruction_decoder::DecodedInstruction::Condition(cond) = inst {
572            match &cond.lhs {
573                Symbol::NumberValue(v) => keys.push(PropertyKey::NumberKey(*v)),
574                Symbol::StringValue(v) => keys.push(PropertyKey::StringKey(v.clone())),
575                Symbol::Key(v, _) => keys.push(PropertyKey::StringKey(v.clone())),
576                _ => {}
577            }
578        }
579    }
580    keys
581}
582
583fn is_node_unbound(node: &fdd::NodeInfo) -> bool {
584    match &node.bound_driver_url {
585        None => true,
586        Some(url) => url == "unbound",
587    }
588}
589
590async fn diagnose_node_info(
591    node: &fdd::NodeInfo,
592    driver_dev_proxy: &fdd::ManagerProxy,
593    writer: &mut dyn io::Write,
594) -> Result<()> {
595    if !is_node_unbound(node) {
596        writeln!(
597            writer,
598            "  Node is bound to {}",
599            node.bound_driver_url.as_deref().unwrap_or("unknown")
600        )?;
601    } else {
602        writeln!(writer, "  Node is UNBOUND.")?;
603        if node.quarantined == Some(true) {
604            writeln!(writer, "  Node is QUARANTINED (driver failed to start). Check driver logs.")?;
605        }
606
607        let properties = node_to_bind_properties(node.node_property_list.as_deref());
608
609        writeln!(writer, "\nFuzzy matching against all drivers...")?;
610        let drivers = fdev::get_driver_info(driver_dev_proxy, &[]).await?;
611        for driver in drivers {
612            if let Some(bytecode) = &driver.bind_rules_bytecode {
613                if let Ok(rules) = DecodedRules::new(bytecode.clone()) {
614                    if is_fuzzy_match(&rules, &properties) {
615                        writeln!(
616                            writer,
617                            "\n------------------------------------------------------------\nPotential match: driver {}",
618                            driver.url.as_deref().unwrap_or("unknown")
619                        )?;
620                        match rules {
621                            DecodedRules::Normal(r) => {
622                                if match_bind(
623                                    MatchBindData {
624                                        symbol_table: &r.symbol_table,
625                                        instructions: &r.instructions,
626                                    },
627                                    &properties,
628                                )
629                                .unwrap_or(false)
630                                {
631                                    writeln!(writer, "  Matches!")?;
632                                } else {
633                                    let diags = evaluate_rules_bytecode(
634                                        &r.symbol_table,
635                                        &r.instructions,
636                                        &properties,
637                                    );
638                                    report_diagnostics(&diags, writer, 2)?;
639                                }
640                            }
641                            DecodedRules::Composite(_) => {
642                                writeln!(
643                                    writer,
644                                    "  This is a composite driver. For a detailed analysis, run `ffx driver doctor --driver {}` or use --composite-node-spec if you know which spec it should match.",
645                                    driver.url.as_deref().unwrap_or("unknown")
646                                )?;
647                            }
648                        }
649                    }
650                }
651            }
652        }
653
654        writeln!(writer, "\nFuzzy matching against all composite node specs...")?;
655        let specs = fdev::get_composite_node_specs(driver_dev_proxy, None).await?;
656        for spec_info in specs {
657            if let Some(spec) = &spec_info.spec {
658                if let Some(parents) = &spec.parents {
659                    for (i, parent) in parents.iter().enumerate() {
660                        if parent.is_fuzzy_match(&properties) {
661                            let spec_name = spec.name.as_deref().unwrap_or("unknown");
662                            writeln!(
663                                writer,
664                                "\n------------------------------------------------------------\nPotential match: spec {} parent {}",
665                                spec_name, i
666                            )?;
667                            let diags = parent.evaluate_bind_rules(&properties);
668                            report_diagnostics(&diags, writer, 2)?;
669                        }
670                    }
671                } else if let Some(parents2) = &spec.parents2 {
672                    for (i, parent) in parents2.iter().enumerate() {
673                        if parent.is_fuzzy_match(&properties) {
674                            let spec_name = spec.name.as_deref().unwrap_or("unknown");
675                            writeln!(
676                                writer,
677                                "\n------------------------------------------------------------\nPotential match: spec {} parent {}",
678                                spec_name, i
679                            )?;
680                            let diags = parent.evaluate_bind_rules(&properties);
681                            report_diagnostics(&diags, writer, 2)?;
682                        }
683                    }
684                }
685            }
686        }
687    }
688    Ok(())
689}
690
691async fn diagnose_node(
692    node_moniker: &str,
693    driver_dev_proxy: &fdd::ManagerProxy,
694    writer: &mut dyn io::Write,
695) -> Result<()> {
696    writeln!(writer, "Diagnosing node {}", node_moniker)?;
697    let nodes = fdev::get_device_info(driver_dev_proxy, &[node_moniker.to_string()], true).await?;
698    if nodes.is_empty() {
699        writeln!(writer, "  ERROR: Node not found.")?;
700        return Ok(());
701    }
702    diagnose_node_info(&nodes[0], driver_dev_proxy, writer).await
703}
704
705fn is_spec_fuzzy_match(rules: &[fdf::BindRule], properties: &DeviceProperties) -> bool {
706    rules.iter().any(|rule| {
707        let key = match &rule.key {
708            fdf::NodePropertyKey::IntValue(v) => PropertyKey::NumberKey(*v as u64),
709            fdf::NodePropertyKey::StringValue(v) => PropertyKey::StringKey(v.clone()),
710        };
711        properties.contains_key(&key)
712    })
713}
714
715fn is_spec_fuzzy_match2(rules: &[fdf::BindRule2], properties: &DeviceProperties) -> bool {
716    rules.iter().any(|rule| {
717        let key = PropertyKey::StringKey(rule.key.clone());
718        properties.contains_key(&key)
719    })
720}
721
722async fn get_moniker_from_path(path: &str, proxy: &fdd::ManagerProxy) -> Result<String> {
723    let nodes = fdev::get_device_info(proxy, &[path.to_string()], true).await.unwrap_or_default();
724    if let Some(node) = nodes.first() {
725        if let Some(moniker) = &node.moniker {
726            return Ok(moniker.clone());
727        }
728    }
729    Ok(path.to_string())
730}
731
732async fn diagnose_spec_info(
733    spec_info: &fdf::CompositeInfo,
734    driver_dev_proxy: &fdd::ManagerProxy,
735    writer: &mut dyn io::Write,
736) -> Result<()> {
737    if let Some(driver) = spec_info
738        .matched_driver
739        .as_ref()
740        .and_then(|m| m.composite_driver.as_ref())
741        .and_then(|cd| cd.driver_info.as_ref())
742        .and_then(|di| di.url.as_deref())
743    {
744        writeln!(writer, "  Spec matched to driver: {}", driver)?;
745    } else {
746        writeln!(writer, "  Spec did NOT match any driver.")?;
747        writeln!(writer, "\nFuzzy matching against all composite drivers...")?;
748        if let Some(spec) = &spec_info.spec {
749            let drivers = fdev::get_driver_info(driver_dev_proxy, &[]).await?;
750            for driver in drivers {
751                if let Some(bytecode) = &driver.bind_rules_bytecode {
752                    if let Ok(DecodedRules::Composite(rules)) = DecodedRules::new(bytecode.clone())
753                    {
754                        let mut potential_match = false;
755                        if let Some(parents) = &spec.parents {
756                            if parents.iter().any(|p| {
757                                is_fuzzy_match(
758                                    &DecodedRules::Composite(rules.clone()),
759                                    &p.to_properties(),
760                                )
761                            }) {
762                                potential_match = true;
763                            }
764                        } else if let Some(parents2) = &spec.parents2 {
765                            if parents2.iter().any(|p| {
766                                is_fuzzy_match(
767                                    &DecodedRules::Composite(rules.clone()),
768                                    &p.to_properties(),
769                                )
770                            }) {
771                                potential_match = true;
772                            }
773                        }
774
775                        if potential_match {
776                            writeln!(
777                                writer,
778                                "\n------------------------------------------------------------\nPotential match: driver {}",
779                                driver.url.as_deref().unwrap_or("unknown")
780                            )?;
781                            if let Some(parents) = &spec.parents {
782                                diagnose_composite_match(&rules, parents, writer)?;
783                            } else if let Some(parents2) = &spec.parents2 {
784                                diagnose_composite_match(&rules, parents2, writer)?;
785                            }
786                        }
787                    }
788                }
789            }
790        }
791    }
792
793    writeln!(writer, "\nFuzzy matching spec parents against all unbound nodes...")?;
794
795    // Query for existing composite nodes to get their bound parents
796    let mut parent_paths: Vec<Option<String>> = Vec::new();
797    let mut parent_monikers: Vec<Option<String>> = Vec::new();
798    if let Some(spec) = &spec_info.spec {
799        let (iterator, iterator_server) =
800            driver_dev_proxy.domain().create_proxy::<fdd::CompositeInfoIteratorMarker>();
801        if driver_dev_proxy.get_composite_info(iterator_server).is_ok() {
802            loop {
803                if let Ok(composite_list) = iterator.get_next().await {
804                    if composite_list.is_empty() {
805                        break;
806                    }
807                    for composite_node in composite_list {
808                        if let Some(fdd::CompositeInfo::Composite(node_info)) =
809                            composite_node.composite
810                        {
811                            if node_info.spec.and_then(|s| s.name) == spec.name {
812                                parent_paths =
813                                    composite_node.parent_topological_paths.unwrap_or_default();
814                                parent_monikers =
815                                    composite_node.parent_monikers.unwrap_or_default();
816                                break;
817                            }
818                        }
819                    }
820                    if !parent_paths.is_empty() || !parent_monikers.is_empty() {
821                        break;
822                    }
823                } else {
824                    break;
825                }
826            }
827        }
828    }
829
830    let nodes = fdev::get_device_info(driver_dev_proxy, &[], false).await?;
831    let unbound_nodes = nodes.into_iter().filter(|n| is_node_unbound(n)).collect_vec();
832
833    let parent_names = spec_info.matched_driver.as_ref().and_then(|m| m.parent_names.as_ref());
834
835    if let Some(spec) = &spec_info.spec {
836        if let Some(parents) = &spec.parents {
837            for (i, parent) in parents.iter().enumerate() {
838                let name =
839                    parent_names.and_then(|n| n.get(i)).map(|s| s.as_str()).unwrap_or("unknown");
840                writeln!(writer, "  Parent {} ({}):", i, name)?;
841
842                let mut bound_node = None;
843                if let Some(Some(moniker)) = parent_monikers.get(i) {
844                    bound_node = Some(moniker.clone());
845                } else if let Some(Some(path)) = parent_paths.get(i) {
846                    bound_node = Some(
847                        get_moniker_from_path(path, driver_dev_proxy)
848                            .await
849                            .unwrap_or_else(|_| path.clone()),
850                    );
851                }
852
853                if let Some(bound_name) = bound_node {
854                    writeln!(writer, "    Already bound to node: {}", bound_name)?;
855                } else {
856                    let mut found_match = false;
857                    for node in &unbound_nodes {
858                        let properties =
859                            node_to_bind_properties(node.node_property_list.as_deref());
860                        if parent.is_fuzzy_match(&properties) {
861                            found_match = true;
862                            writeln!(
863                                writer,
864                                "    Potential match: node {}",
865                                node.moniker.as_deref().unwrap_or("unknown")
866                            )?;
867                            let diags = parent.evaluate_bind_rules(&properties);
868                            report_diagnostics(&diags, writer, 4)?;
869                        }
870                    }
871                    if !found_match {
872                        writeln!(writer, "    No unbound nodes matched this parent.")?;
873                    }
874                }
875            }
876        } else if let Some(parents2) = &spec.parents2 {
877            for (i, parent) in parents2.iter().enumerate() {
878                let name =
879                    parent_names.and_then(|n| n.get(i)).map(|s| s.as_str()).unwrap_or("unknown");
880                writeln!(writer, "  Parent {} ({}):", i, name)?;
881
882                let mut bound_node = None;
883                if let Some(Some(moniker)) = parent_monikers.get(i) {
884                    bound_node = Some(moniker.clone());
885                } else if let Some(Some(path)) = parent_paths.get(i) {
886                    bound_node = Some(
887                        get_moniker_from_path(path, driver_dev_proxy)
888                            .await
889                            .unwrap_or_else(|_| path.clone()),
890                    );
891                }
892
893                if let Some(bound_name) = bound_node {
894                    writeln!(writer, "    Already bound to node: {}", bound_name)?;
895                } else {
896                    let mut found_match = false;
897                    for node in &unbound_nodes {
898                        let properties =
899                            node_to_bind_properties(node.node_property_list.as_deref());
900                        if parent.is_fuzzy_match(&properties) {
901                            found_match = true;
902                            writeln!(
903                                writer,
904                                "    Potential match: node {}",
905                                node.moniker.as_deref().unwrap_or("unknown")
906                            )?;
907                            let diags = parent.evaluate_bind_rules(&properties);
908                            report_diagnostics(&diags, writer, 4)?;
909                        }
910                    }
911                    if !found_match {
912                        writeln!(writer, "    No unbound nodes matched this parent.")?;
913                    }
914                }
915            }
916        }
917    }
918
919    Ok(())
920}
921
922async fn diagnose_spec(
923    spec_name: &str,
924    driver_dev_proxy: &fdd::ManagerProxy,
925    writer: &mut dyn io::Write,
926) -> Result<()> {
927    writeln!(writer, "Diagnosing composite node spec {}", spec_name)?;
928    let specs = fdev::get_composite_node_specs(driver_dev_proxy, None).await?;
929    let spec = specs.iter().find(|s| {
930        s.spec
931            .as_ref()
932            .and_then(|spec| spec.name.as_ref())
933            .map(|name| name == spec_name)
934            .unwrap_or(false)
935    });
936
937    match spec {
938        None => writeln!(writer, "  ERROR: Composite node spec not found.")?,
939        Some(s) => {
940            diagnose_spec_info(s, driver_dev_proxy, writer).await?;
941        }
942    }
943
944    Ok(())
945}
946
947async fn diagnose_all(
948    driver_dev_proxy: &fdd::ManagerProxy,
949    writer: &mut dyn io::Write,
950) -> Result<()> {
951    writeln!(writer, "Diagnosing all unbound nodes")?;
952    let nodes = fdev::get_device_info(driver_dev_proxy, &[], false).await?;
953    let unbound_nodes = nodes.into_iter().filter(|n| is_node_unbound(n)).collect_vec();
954
955    if unbound_nodes.is_empty() {
956        writeln!(writer, "No unbound nodes found.")?;
957    } else {
958        for node in unbound_nodes {
959            writeln!(writer, "Diagnosing node {}", node.moniker.as_deref().unwrap_or("unknown"))?;
960            diagnose_node_info(&node, driver_dev_proxy, writer).await?;
961        }
962    }
963
964    writeln!(writer, "\n\nDiagnosing all unmatched composite node specs")?;
965    let specs = fdev::get_composite_node_specs(driver_dev_proxy, None).await?;
966    let unmatched_specs = specs.into_iter().filter(|s| s.matched_driver.is_none()).collect_vec();
967
968    if unmatched_specs.is_empty() {
969        writeln!(writer, "No unmatched composite node specs found.")?;
970    } else {
971        for spec in unmatched_specs {
972            let name = spec.spec.as_ref().and_then(|s| s.name.as_deref()).unwrap_or("unknown");
973            writeln!(writer, "Diagnosing composite node spec {}", name)?;
974            diagnose_spec_info(&spec, driver_dev_proxy, writer).await?;
975        }
976    }
977
978    Ok(())
979}
980
981fn node_to_bind_properties(node_props: Option<&[fdf::NodeProperty]>) -> DeviceProperties {
982    let mut props = HashMap::new();
983    if let Some(node_props) = node_props {
984        for prop in node_props {
985            let key = match &prop.key {
986                fdf::NodePropertyKey::IntValue(v) => PropertyKey::NumberKey(*v as u64),
987                fdf::NodePropertyKey::StringValue(v) => PropertyKey::StringKey(v.clone()),
988            };
989            let value = match &prop.value {
990                fdf::NodePropertyValue::IntValue(v) => Symbol::NumberValue(*v as u64),
991                fdf::NodePropertyValue::StringValue(v) => Symbol::StringValue(v.clone()),
992                fdf::NodePropertyValue::BoolValue(v) => Symbol::BoolValue(*v),
993                fdf::NodePropertyValue::EnumValue(v) => Symbol::EnumValue(v.clone()),
994                _ => continue,
995            };
996            props.insert(key, value);
997        }
998    }
999    props
1000}
1001
1002fn node_to_bind_properties2(node_props: Option<&[fdf::NodeProperty2]>) -> DeviceProperties {
1003    let mut props = HashMap::new();
1004    if let Some(node_props) = node_props {
1005        for prop in node_props {
1006            let key = PropertyKey::StringKey(prop.key.clone());
1007            let value = match &prop.value {
1008                fdf::NodePropertyValue::IntValue(v) => Symbol::NumberValue(*v as u64),
1009                fdf::NodePropertyValue::StringValue(v) => Symbol::StringValue(v.clone()),
1010                fdf::NodePropertyValue::BoolValue(v) => Symbol::BoolValue(*v),
1011                fdf::NodePropertyValue::EnumValue(v) => Symbol::EnumValue(v.clone()),
1012                _ => continue,
1013            };
1014            props.insert(key, value);
1015        }
1016    }
1017    props
1018}
1019
1020#[derive(Debug)]
1021enum Diagnostic {
1022    Mismatch { key: PropertyKey, expected: Symbol, actual: Option<Symbol>, is_equal: bool },
1023    AbortReached,
1024}
1025
1026fn evaluate_rules_bytecode(
1027    symbol_table: &HashMap<u32, String>,
1028    instructions: &[u8],
1029    properties: &DeviceProperties,
1030) -> Vec<Diagnostic> {
1031    let mut diagnostics = Vec::new();
1032    let mut iter = instructions.iter();
1033
1034    while let Some(byte) = iter.next() {
1035        let op_byte_val = *byte;
1036        if op_byte_val == RawOp::EqualCondition as u8
1037            || op_byte_val == RawOp::InequalCondition as u8
1038        {
1039            let is_equal = op_byte_val == RawOp::EqualCondition as u8;
1040            let (res, diag) =
1041                read_and_evaluate_values(&mut iter, symbol_table, properties, is_equal);
1042            if !res {
1043                if let Some(d) = diag {
1044                    diagnostics.push(d);
1045                }
1046                break;
1047            }
1048        } else if op_byte_val == RawOp::Abort as u8 {
1049            diagnostics.push(Diagnostic::AbortReached);
1050            break;
1051        } else if op_byte_val == RawOp::UnconditionalJump as u8 {
1052            let offset = match next_u32(&mut iter) {
1053                Ok(o) => o,
1054                Err(_) => break,
1055            };
1056            for _ in 0..offset {
1057                if iter.next().is_none() {
1058                    break;
1059                }
1060            }
1061            if iter.next() != Some(&(RawOp::JumpLandPad as u8)) {
1062                break;
1063            }
1064        } else if op_byte_val == RawOp::JumpIfEqual as u8
1065            || op_byte_val == RawOp::JumpIfNotEqual as u8
1066        {
1067            let is_equal = op_byte_val == RawOp::JumpIfEqual as u8;
1068            let offset = match next_u32(&mut iter) {
1069                Ok(o) => o,
1070                Err(_) => break,
1071            };
1072            let (res, _) = read_and_evaluate_values(&mut iter, symbol_table, properties, is_equal);
1073            if res {
1074                for _ in 0..offset {
1075                    if iter.next().is_none() {
1076                        break;
1077                    }
1078                }
1079                if iter.next() != Some(&(RawOp::JumpLandPad as u8)) {
1080                    break;
1081                }
1082            }
1083        } else if op_byte_val == RawOp::JumpLandPad as u8 {
1084            // Nothing to do
1085        } else {
1086            // Unknown opcode, stop
1087            break;
1088        }
1089    }
1090    diagnostics
1091}
1092
1093fn read_and_evaluate_values(
1094    iter: &mut BytecodeIter<'_>,
1095    symbol_table: &HashMap<u32, String>,
1096    properties: &DeviceProperties,
1097    is_equal: bool,
1098) -> (bool, Option<Diagnostic>) {
1099    let lhs = match read_next_value(iter, symbol_table) {
1100        Ok(v) => v,
1101        Err(_) => return (false, None),
1102    };
1103    let rhs = match read_next_value(iter, symbol_table) {
1104        Ok(v) => v,
1105        Err(_) => return (false, None),
1106    };
1107
1108    let key = match lhs {
1109        Symbol::NumberValue(v) => PropertyKey::NumberKey(v),
1110        Symbol::StringValue(v) => PropertyKey::StringKey(v),
1111        Symbol::Key(v, _) => PropertyKey::StringKey(v),
1112        _ => return (false, None),
1113    };
1114
1115    let actual = properties.get(&key).cloned();
1116    let mut effective_actual = actual;
1117    if effective_actual.is_none() {
1118        if let PropertyKey::NumberKey(int_key) = &key {
1119            if let Some(str_key) = get_deprecated_key_identifier(*int_key as u32) {
1120                effective_actual = properties.get(&PropertyKey::StringKey(str_key)).cloned();
1121            }
1122        }
1123    }
1124
1125    let matches = match &effective_actual {
1126        Some(val) => {
1127            let mut val_for_compare = val.clone();
1128            if let Symbol::EnumValue(v) = val {
1129                val_for_compare = Symbol::StringValue(v.clone());
1130            }
1131            let mut rhs_for_compare = rhs.clone();
1132            if let Symbol::EnumValue(v) = &rhs {
1133                rhs_for_compare = Symbol::StringValue(v.clone());
1134            }
1135
1136            val_for_compare == rhs_for_compare
1137        }
1138        None => &bind::compiler::Symbol::NumberValue(0) == &rhs,
1139    };
1140
1141    if matches == is_equal {
1142        (true, None)
1143    } else {
1144        (
1145            false,
1146            Some(Diagnostic::Mismatch { key, expected: rhs, actual: effective_actual, is_equal }),
1147        )
1148    }
1149}
1150
1151fn read_next_value(
1152    iter: &mut BytecodeIter<'_>,
1153    symbol_table: &HashMap<u32, String>,
1154) -> Result<Symbol, ()> {
1155    let value_type_byte = next_u8(iter).map_err(|_| ())?;
1156    let value_type_val = *value_type_byte;
1157    let value = next_u32(iter).map_err(|_| ())?;
1158
1159    if value_type_val == RawValueType::NumberValue as u8 {
1160        Ok(Symbol::NumberValue(value as u64))
1161    } else if value_type_val == RawValueType::Key as u8 {
1162        let name = symbol_table.get(&value).ok_or(())?.clone();
1163        Ok(Symbol::Key(name, bind::parser::bind_library::ValueType::Str))
1164    } else if value_type_val == RawValueType::StringValue as u8 {
1165        let val = symbol_table.get(&value).ok_or(())?.clone();
1166        Ok(Symbol::StringValue(val))
1167    } else if value_type_val == RawValueType::BoolValue as u8 {
1168        match value {
1169            FALSE_VAL => Ok(Symbol::BoolValue(false)),
1170            TRUE_VAL => Ok(Symbol::BoolValue(true)),
1171            _ => Err(()),
1172        }
1173    } else if value_type_val == RawValueType::EnumValue as u8 {
1174        let val = symbol_table.get(&value).ok_or(())?.clone();
1175        Ok(Symbol::EnumValue(val))
1176    } else {
1177        Err(())
1178    }
1179}
1180
1181fn evaluate_bind_rules(rules: &[fdf::BindRule], properties: &DeviceProperties) -> Vec<Diagnostic> {
1182    let mut diagnostics = Vec::new();
1183    for rule in rules {
1184        let key = match &rule.key {
1185            fdf::NodePropertyKey::IntValue(v) => PropertyKey::NumberKey(*v as u64),
1186            fdf::NodePropertyKey::StringValue(v) => PropertyKey::StringKey(v.clone()),
1187        };
1188        let actual = properties.get(&key).cloned();
1189
1190        let mut matched = false;
1191        for val in &rule.values {
1192            let symbol_val = match val {
1193                fdf::NodePropertyValue::IntValue(v) => Symbol::NumberValue(*v as u64),
1194                fdf::NodePropertyValue::StringValue(v) => Symbol::StringValue(v.clone()),
1195                fdf::NodePropertyValue::BoolValue(v) => Symbol::BoolValue(*v),
1196                fdf::NodePropertyValue::EnumValue(v) => Symbol::EnumValue(v.clone()),
1197                _ => continue,
1198            };
1199            if actual.as_ref() == Some(&symbol_val) {
1200                matched = true;
1201                break;
1202            }
1203        }
1204
1205        match rule.condition {
1206            fdf::Condition::Accept => {
1207                if !matched {
1208                    diagnostics.push(Diagnostic::Mismatch {
1209                        key,
1210                        expected: Symbol::StringValue("one of requested values".to_string()),
1211                        actual,
1212                        is_equal: true,
1213                    });
1214                }
1215            }
1216            fdf::Condition::Reject => {
1217                if matched {
1218                    diagnostics.push(Diagnostic::Mismatch {
1219                        key,
1220                        expected: Symbol::StringValue("none of requested values".to_string()),
1221                        actual,
1222                        is_equal: false,
1223                    });
1224                }
1225            }
1226            _ => {}
1227        }
1228    }
1229    diagnostics
1230}
1231
1232fn evaluate_bind_rules2(
1233    rules: &[fdf::BindRule2],
1234    properties: &DeviceProperties,
1235) -> Vec<Diagnostic> {
1236    let mut diagnostics = Vec::new();
1237    for rule in rules {
1238        let key = PropertyKey::StringKey(rule.key.clone());
1239        let actual = properties.get(&key).cloned();
1240
1241        let mut matched = false;
1242        for val in &rule.values {
1243            let symbol_val = match val {
1244                fdf::NodePropertyValue::IntValue(v) => Symbol::NumberValue(*v as u64),
1245                fdf::NodePropertyValue::StringValue(v) => Symbol::StringValue(v.clone()),
1246                fdf::NodePropertyValue::BoolValue(v) => Symbol::BoolValue(*v),
1247                fdf::NodePropertyValue::EnumValue(v) => Symbol::EnumValue(v.clone()),
1248                _ => continue,
1249            };
1250            if actual.as_ref() == Some(&symbol_val) {
1251                matched = true;
1252                break;
1253            }
1254        }
1255
1256        match rule.condition {
1257            fdf::Condition::Accept => {
1258                if !matched {
1259                    diagnostics.push(Diagnostic::Mismatch {
1260                        key,
1261                        expected: Symbol::StringValue("one of requested values".to_string()),
1262                        actual,
1263                        is_equal: true,
1264                    });
1265                }
1266            }
1267            fdf::Condition::Reject => {
1268                if matched {
1269                    diagnostics.push(Diagnostic::Mismatch {
1270                        key,
1271                        expected: Symbol::StringValue("none of requested values".to_string()),
1272                        actual,
1273                        is_equal: false,
1274                    });
1275                }
1276            }
1277            _ => {}
1278        }
1279    }
1280    diagnostics
1281}
1282
1283fn report_diagnostics(
1284    diagnostics: &[Diagnostic],
1285    writer: &mut dyn io::Write,
1286    indent: usize,
1287) -> Result<()> {
1288    let indent_str = " ".repeat(indent);
1289    if diagnostics.is_empty() {
1290        writeln!(writer, "{}No issues found with direct property matching.", indent_str)?;
1291    } else {
1292        for diag in diagnostics {
1293            match diag {
1294                Diagnostic::Mismatch { key, expected, actual, is_equal } => {
1295                    let key_str = match key {
1296                        PropertyKey::NumberKey(v) => format!("{:#x}", v),
1297                        PropertyKey::StringKey(v) => v.clone(),
1298                    };
1299                    let actual_str = match actual {
1300                        Some(v) => v.to_string(),
1301                        None => "missing".to_string(),
1302                    };
1303                    if *is_equal {
1304                        writeln!(
1305                            writer,
1306                            "{}Mismatch: key {} expected {} but found {}",
1307                            indent_str, key_str, expected, actual_str
1308                        )?;
1309                    } else {
1310                        writeln!(
1311                            writer,
1312                            "{}Mismatch: key {} expected NOT {} but found {}",
1313                            indent_str, key_str, expected, actual_str
1314                        )?;
1315                    }
1316                }
1317                Diagnostic::AbortReached => {
1318                    writeln!(writer, "{}Unconditional Abort reached in bind rules.", indent_str)?;
1319                }
1320            }
1321        }
1322    }
1323    Ok(())
1324}
1325
1326#[cfg(test)]
1327mod tests {
1328    use super::*;
1329    use bind::interpreter::instruction_decoder::{DecodedCondition, DecodedInstruction};
1330    use bind::parser::bind_library::ValueType;
1331
1332    #[test]
1333    fn test_evaluate_rules_mismatch() {
1334        let symbol_table = HashMap::new();
1335        let mut instructions = vec![RawOp::EqualCondition as u8];
1336        instructions.push(RawValueType::NumberValue as u8);
1337        instructions.extend_from_slice(&1u32.to_le_bytes());
1338        instructions.push(RawValueType::NumberValue as u8);
1339        instructions.extend_from_slice(&100u32.to_le_bytes());
1340
1341        let mut properties = HashMap::new();
1342        properties.insert(PropertyKey::NumberKey(1), Symbol::NumberValue(200));
1343
1344        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1345        assert_eq!(diagnostics.len(), 1);
1346        match &diagnostics[0] {
1347            Diagnostic::Mismatch { key, expected, actual, is_equal } => {
1348                assert_eq!(key, &PropertyKey::NumberKey(1));
1349                assert_eq!(expected, &Symbol::NumberValue(100));
1350                assert_eq!(actual, &Some(Symbol::NumberValue(200)));
1351                assert!(is_equal);
1352            }
1353            _ => panic!("Expected Mismatch"),
1354        }
1355    }
1356
1357    #[test]
1358    fn test_evaluate_rules_match() {
1359        let symbol_table = HashMap::new();
1360        let mut instructions = vec![RawOp::EqualCondition as u8];
1361        instructions.push(RawValueType::NumberValue as u8);
1362        instructions.extend_from_slice(&1u32.to_le_bytes());
1363        instructions.push(RawValueType::NumberValue as u8);
1364        instructions.extend_from_slice(&100u32.to_le_bytes());
1365
1366        let mut properties = HashMap::new();
1367        properties.insert(PropertyKey::NumberKey(1), Symbol::NumberValue(100));
1368
1369        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1370        assert_eq!(diagnostics.len(), 0);
1371    }
1372
1373    #[test]
1374    fn test_evaluate_rules_missing_property() {
1375        let mut symbol_table = HashMap::new();
1376        symbol_table.insert(1, "key1".to_string());
1377
1378        // IF key1 == 0 (should match if missing)
1379        let mut instructions = vec![RawOp::EqualCondition as u8];
1380        instructions.push(RawValueType::StringValue as u8);
1381        instructions.extend_from_slice(&1u32.to_le_bytes());
1382        instructions.push(RawValueType::NumberValue as u8);
1383        instructions.extend_from_slice(&0u32.to_le_bytes());
1384
1385        let properties = HashMap::new();
1386
1387        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1388        assert_eq!(diagnostics.len(), 0);
1389
1390        // IF key1 != 0 (should mismatch if missing)
1391        let mut instructions = vec![RawOp::InequalCondition as u8];
1392        instructions.push(RawValueType::StringValue as u8);
1393        instructions.extend_from_slice(&1u32.to_le_bytes());
1394        instructions.push(RawValueType::NumberValue as u8);
1395        instructions.extend_from_slice(&0u32.to_le_bytes());
1396
1397        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1398        assert_eq!(diagnostics.len(), 1);
1399        match &diagnostics[0] {
1400            Diagnostic::Mismatch { key, expected, actual, is_equal } => {
1401                assert_eq!(key, &PropertyKey::StringKey("key1".to_string()));
1402                assert_eq!(expected, &Symbol::NumberValue(0));
1403                assert_eq!(actual, &None);
1404                assert!(!is_equal);
1405            }
1406            _ => panic!("Expected Mismatch"),
1407        }
1408    }
1409
1410    #[test]
1411    fn test_evaluate_rules_control_flow() {
1412        let mut symbol_table = HashMap::new();
1413        symbol_table.insert(1, "protocol".to_string());
1414        symbol_table.insert(2, "vendor_id".to_string());
1415
1416        // if protocol == PCI { vendor_id == GOOGLE }
1417        // PCI = 10, GOOGLE = 0x1234
1418        // Bytecode:
1419        // JumpIfNotEqual(protocol, 10, target)
1420        // EqualCondition(vendor_id, 0x1234)
1421        // JumpLandPad(target)
1422
1423        let mut instructions = vec![RawOp::JumpIfNotEqual as u8];
1424        instructions.extend_from_slice(&11u32.to_le_bytes()); // Offset
1425        instructions.push(RawValueType::StringValue as u8);
1426        instructions.extend_from_slice(&1u32.to_le_bytes());
1427        instructions.push(RawValueType::NumberValue as u8);
1428        instructions.extend_from_slice(&10u32.to_le_bytes());
1429
1430        instructions.push(RawOp::EqualCondition as u8);
1431        instructions.push(RawValueType::StringValue as u8);
1432        instructions.extend_from_slice(&2u32.to_le_bytes());
1433        instructions.push(RawValueType::NumberValue as u8);
1434        instructions.extend_from_slice(&0x1234u32.to_le_bytes());
1435
1436        instructions.push(RawOp::JumpLandPad as u8);
1437
1438        // Case 1: protocol is USB (20). Should jump over vendor_id check.
1439        let mut properties = HashMap::new();
1440        properties.insert(PropertyKey::StringKey("protocol".to_string()), Symbol::NumberValue(20));
1441        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1442        assert_eq!(diagnostics.len(), 0);
1443
1444        // Case 2: protocol is PCI (10). Should NOT jump, and report vendor_id mismatch if it's wrong.
1445        let mut properties = HashMap::new();
1446        properties.insert(PropertyKey::StringKey("protocol".to_string()), Symbol::NumberValue(10));
1447        properties
1448            .insert(PropertyKey::StringKey("vendor_id".to_string()), Symbol::NumberValue(0x5678));
1449        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1450        assert_eq!(diagnostics.len(), 1);
1451        match &diagnostics[0] {
1452            Diagnostic::Mismatch { key, .. } => {
1453                assert_eq!(key, &PropertyKey::StringKey("vendor_id".to_string()));
1454            }
1455            _ => panic!("Expected vendor_id mismatch"),
1456        }
1457    }
1458
1459    #[test]
1460    fn test_evaluate_rules_abort() {
1461        let symbol_table = HashMap::new();
1462        let instructions = vec![RawOp::Abort as u8];
1463        let properties = HashMap::new();
1464
1465        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1466        assert_eq!(diagnostics.len(), 1);
1467        match &diagnostics[0] {
1468            Diagnostic::AbortReached => {}
1469            _ => panic!("Expected AbortReached"),
1470        }
1471    }
1472
1473    #[test]
1474    fn test_evaluate_rules_deprecated_key() {
1475        let symbol_table = HashMap::new();
1476        // fuchsia.BIND_PROTOCOL is 0x0001
1477        let mut instructions = vec![RawOp::EqualCondition as u8];
1478        instructions.push(RawValueType::NumberValue as u8);
1479        instructions.extend_from_slice(&1u32.to_le_bytes());
1480        instructions.push(RawValueType::NumberValue as u8);
1481        instructions.extend_from_slice(&85u32.to_le_bytes());
1482
1483        let mut properties = HashMap::new();
1484        properties.insert(
1485            PropertyKey::StringKey("fuchsia.BIND_PROTOCOL".to_string()),
1486            Symbol::NumberValue(85),
1487        );
1488
1489        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1490        assert_eq!(diagnostics.len(), 0);
1491    }
1492
1493    #[test]
1494    fn test_evaluate_bind_rules_mismatch() {
1495        let rules = vec![fdf::BindRule {
1496            key: fdf::NodePropertyKey::IntValue(1),
1497            condition: fdf::Condition::Accept,
1498            values: vec![fdf::NodePropertyValue::IntValue(100)],
1499        }];
1500        let mut properties = HashMap::new();
1501        properties.insert(PropertyKey::NumberKey(1), Symbol::NumberValue(200));
1502
1503        let diagnostics = evaluate_bind_rules(&rules, &properties);
1504        assert_eq!(diagnostics.len(), 1);
1505    }
1506
1507    #[test]
1508    fn test_evaluate_bind_rules2_mismatch() {
1509        let rules = vec![fdf::BindRule2 {
1510            key: "key1".to_string(),
1511            condition: fdf::Condition::Accept,
1512            values: vec![fdf::NodePropertyValue::IntValue(100)],
1513        }];
1514        let mut properties = HashMap::new();
1515        properties.insert(PropertyKey::StringKey("key1".to_string()), Symbol::NumberValue(200));
1516
1517        let diagnostics = evaluate_bind_rules2(&rules, &properties);
1518        assert_eq!(diagnostics.len(), 1);
1519        match &diagnostics[0] {
1520            Diagnostic::Mismatch { key, expected: _, actual, is_equal: _ } => {
1521                assert_eq!(key, &PropertyKey::StringKey("key1".to_string()));
1522                assert_eq!(actual, &Some(Symbol::NumberValue(200)));
1523            }
1524            _ => panic!("Expected Mismatch"),
1525        }
1526    }
1527
1528    #[test]
1529    fn test_is_fuzzy_match() {
1530        let mut symbol_table = HashMap::new();
1531        symbol_table.insert(1, "key1".to_string());
1532        let rules = DecodedRules::Normal(bind::interpreter::decode_bind_rules::DecodedBindRules {
1533            symbol_table,
1534            instructions: vec![],
1535            decoded_instructions: vec![DecodedInstruction::Condition(DecodedCondition {
1536                lhs: Symbol::Key("key1".to_string(), ValueType::Str),
1537                rhs: Symbol::NumberValue(100),
1538                is_equal: true,
1539            })],
1540            debug_info: None,
1541        });
1542        let mut properties = HashMap::new();
1543        properties.insert(PropertyKey::StringKey("key1".to_string()), Symbol::NumberValue(100));
1544        assert!(is_fuzzy_match(&rules, &properties));
1545    }
1546
1547    #[test]
1548    fn test_get_keys_from_instructions() {
1549        let instructions = vec![
1550            DecodedInstruction::Condition(DecodedCondition {
1551                lhs: Symbol::NumberValue(1),
1552                rhs: Symbol::NumberValue(100),
1553                is_equal: true,
1554            }),
1555            DecodedInstruction::Condition(DecodedCondition {
1556                lhs: Symbol::StringValue("key_str".to_string()),
1557                rhs: Symbol::NumberValue(200),
1558                is_equal: true,
1559            }),
1560        ];
1561        let keys = get_keys_from_instructions(&instructions);
1562        assert_eq!(keys.len(), 2);
1563        assert!(keys.contains(&PropertyKey::NumberKey(1)));
1564        assert!(keys.contains(&PropertyKey::StringKey("key_str".to_string())));
1565    }
1566
1567    #[test]
1568    fn test_diagnose_composite_match_missing_parents() {
1569        struct MockParent {
1570            properties: DeviceProperties,
1571        }
1572
1573        impl DiagnosableParent for MockParent {
1574            fn to_properties(&self) -> DeviceProperties {
1575                self.properties.clone()
1576            }
1577            fn evaluate_bind_rules(&self, _properties: &DeviceProperties) -> Vec<Diagnostic> {
1578                vec![]
1579            }
1580            fn is_fuzzy_match(&self, _properties: &DeviceProperties) -> bool {
1581                true
1582            }
1583        }
1584
1585        let symbol_table = HashMap::new();
1586
1587        // Primary parent instructions: property 1 == 100
1588        let mut primary_instructions = vec![RawOp::EqualCondition as u8];
1589        primary_instructions.push(RawValueType::NumberValue as u8);
1590        primary_instructions.extend_from_slice(&1u32.to_le_bytes());
1591        primary_instructions.push(RawValueType::NumberValue as u8);
1592        primary_instructions.extend_from_slice(&100u32.to_le_bytes());
1593
1594        // Additional parent instructions: property 2 == 200
1595        let mut additional_instructions = vec![RawOp::EqualCondition as u8];
1596        additional_instructions.push(RawValueType::NumberValue as u8);
1597        additional_instructions.extend_from_slice(&2u32.to_le_bytes());
1598        additional_instructions.push(RawValueType::NumberValue as u8);
1599        additional_instructions.extend_from_slice(&200u32.to_le_bytes());
1600
1601        let rules = DecodedCompositeBindRules {
1602            symbol_table,
1603            device_name_id: 0,
1604            primary_parent: bind::interpreter::decode_bind_rules::Parent {
1605                name_id: 0,
1606                instructions: primary_instructions,
1607                decoded_instructions: vec![],
1608            },
1609            additional_parents: vec![bind::interpreter::decode_bind_rules::Parent {
1610                name_id: 0,
1611                instructions: additional_instructions,
1612                decoded_instructions: vec![],
1613            }],
1614            optional_parents: vec![],
1615            debug_info: None,
1616        };
1617
1618        let mut props1 = HashMap::new();
1619        props1.insert(PropertyKey::NumberKey(1), Symbol::NumberValue(100));
1620
1621        let mut props2 = HashMap::new();
1622        props2.insert(PropertyKey::NumberKey(2), Symbol::NumberValue(200));
1623
1624        let mut props3 = HashMap::new();
1625        props3.insert(PropertyKey::NumberKey(3), Symbol::NumberValue(300));
1626
1627        let parents = vec![
1628            MockParent { properties: props1 },
1629            MockParent { properties: props2 },
1630            MockParent { properties: props3 }, // This one should be unmatched
1631        ];
1632
1633        let mut output = Vec::new();
1634        let result = diagnose_composite_match(&rules, &parents, &mut output);
1635        assert!(result.is_ok());
1636
1637        let output_str = String::from_utf8(output).unwrap();
1638        assert!(
1639            output_str
1640                .contains("ERROR: Driver bind rules missing matches for 1 parents in the spec.")
1641        );
1642        assert!(output_str.contains("Parent 2 is unmatched."));
1643    }
1644}