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