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    Ok(())
400}
401
402async fn diagnose_driver(
403    driver: &fdf::DriverInfo,
404    driver_dev_proxy: &fdd::ManagerProxy,
405    writer: &mut dyn io::Write,
406) -> Result<()> {
407    let driver_url = driver.url.as_deref().unwrap_or("unknown");
408    writeln!(writer, "Diagnosing driver {}", driver_url)?;
409
410    let nodes = fdev::get_device_info(driver_dev_proxy, &[], false).await?;
411    let bound_nodes = nodes
412        .iter()
413        .filter_map(|n| {
414            if let Some(bound_url) = &n.bound_driver_url {
415                if bound_url == driver_url {
416                    return Some(n.moniker.as_deref().unwrap_or("unknown"));
417                }
418            }
419            None
420        })
421        .collect_vec();
422
423    if !bound_nodes.is_empty() {
424        writeln!(writer, "\nDriver {} is bound to the following nodes:", driver_url)?;
425        for node in bound_nodes {
426            writeln!(writer, "  {}", node)?;
427        }
428    }
429
430    if let Some(bytecode) = &driver.bind_rules_bytecode {
431        let rules = DecodedRules::new(bytecode.clone())?;
432
433        match &rules {
434            DecodedRules::Normal(r) => {
435                writeln!(writer, "\nFuzzy matching against all unbound nodes...")?;
436                let unbound_nodes = nodes.into_iter().filter(|n| is_node_unbound(n)).collect_vec();
437                for node in unbound_nodes {
438                    let props = node_to_bind_properties(node.node_property_list.as_deref());
439                    if is_fuzzy_match(&rules, &props) {
440                        writeln!(
441                            writer,
442                            "\n------------------------------------------------------------\nPotential match found: node {}",
443                            node.moniker.as_deref().unwrap_or("unknown")
444                        )?;
445                        if match_bind(
446                            MatchBindData {
447                                symbol_table: &r.symbol_table,
448                                instructions: &r.instructions,
449                            },
450                            &props,
451                        )
452                        .unwrap_or(false)
453                        {
454                            writeln!(writer, "  Matches!")?;
455                        } else {
456                            let diags =
457                                evaluate_rules_bytecode(&r.symbol_table, &r.instructions, &props);
458                            report_diagnostics(&diags, writer, 2)?;
459                        }
460                    }
461                }
462            }
463            DecodedRules::Composite(r) => {
464                let specs = fdev::get_composite_node_specs(driver_dev_proxy, None).await?;
465                let (matched_specs, unmatched_specs): (Vec<_>, Vec<_>) =
466                    specs.into_iter().partition(|s| {
467                        if let Some(matched) = &s.matched_driver {
468                            if let Some(composite_driver) = &matched.composite_driver {
469                                if let Some(info) = &composite_driver.driver_info {
470                                    if let Some(url) = &info.url {
471                                        return url == driver_url;
472                                    }
473                                }
474                            }
475                        }
476                        false
477                    });
478
479                if !matched_specs.is_empty() {
480                    writeln!(writer, "\nDriver is matched to the following composite node specs:")?;
481                    for spec_info in matched_specs {
482                        let spec_name = spec_info
483                            .spec
484                            .as_ref()
485                            .and_then(|s| s.name.as_deref())
486                            .unwrap_or("unknown");
487                        writeln!(
488                            writer,
489                            "  {}\n  (Run `ffx driver doctor --composite-node-spec {}` to diagnose parents)",
490                            spec_name, spec_name
491                        )?;
492                    }
493                }
494
495                writeln!(writer, "\nFuzzy matching against all unmatched composite node specs...")?;
496                let unmatched_specs = unmatched_specs
497                    .into_iter()
498                    .filter(|s| s.matched_driver.is_none())
499                    .collect_vec();
500                for spec_info in unmatched_specs {
501                    if let Some(spec) = &spec_info.spec {
502                        let mut potential_match = false;
503                        if let Some(parents) = &spec.parents {
504                            if parents.iter().any(|p| is_fuzzy_match(&rules, &p.to_properties())) {
505                                potential_match = true;
506                            }
507                        } else if let Some(parents2) = &spec.parents2 {
508                            if parents2.iter().any(|p| is_fuzzy_match(&rules, &p.to_properties())) {
509                                potential_match = true;
510                            }
511                        }
512
513                        if potential_match {
514                            writeln!(
515                                writer,
516                                "\n------------------------------------------------------------\nPotential match found: spec {}",
517                                spec.name.as_deref().unwrap_or("unknown")
518                            )?;
519                            if let Some(parents) = &spec.parents {
520                                diagnose_composite_match(r, parents, writer)?;
521                            } else if let Some(parents2) = &spec.parents2 {
522                                diagnose_composite_match(r, parents2, writer)?;
523                            }
524                        }
525                    }
526                }
527            }
528        }
529    }
530    Ok(())
531}
532
533fn is_fuzzy_match(rules: &DecodedRules, properties: &DeviceProperties) -> bool {
534    let keys = match rules {
535        DecodedRules::Normal(r) => get_keys_from_instructions(&r.decoded_instructions),
536        DecodedRules::Composite(r) => {
537            let mut k = get_keys_from_instructions(&r.primary_parent.decoded_instructions);
538            for n in &r.additional_parents {
539                k.extend(get_keys_from_instructions(&n.decoded_instructions));
540            }
541            k
542        }
543    };
544    keys.iter().any(|k| properties.contains_key(k))
545}
546
547fn get_keys_from_instructions(
548    insts: &[bind::interpreter::instruction_decoder::DecodedInstruction],
549) -> Vec<PropertyKey> {
550    let mut keys = Vec::new();
551    for inst in insts {
552        if let bind::interpreter::instruction_decoder::DecodedInstruction::Condition(cond) = inst {
553            match &cond.lhs {
554                Symbol::NumberValue(v) => keys.push(PropertyKey::NumberKey(*v)),
555                Symbol::StringValue(v) => keys.push(PropertyKey::StringKey(v.clone())),
556                Symbol::Key(v, _) => keys.push(PropertyKey::StringKey(v.clone())),
557                _ => {}
558            }
559        }
560    }
561    keys
562}
563
564fn is_node_unbound(node: &fdd::NodeInfo) -> bool {
565    match &node.bound_driver_url {
566        None => true,
567        Some(url) => url == "unbound",
568    }
569}
570
571async fn diagnose_node_info(
572    node: &fdd::NodeInfo,
573    driver_dev_proxy: &fdd::ManagerProxy,
574    writer: &mut dyn io::Write,
575) -> Result<()> {
576    if !is_node_unbound(node) {
577        writeln!(
578            writer,
579            "  Node is bound to {}",
580            node.bound_driver_url.as_deref().unwrap_or("unknown")
581        )?;
582    } else {
583        writeln!(writer, "  Node is UNBOUND.")?;
584        if node.quarantined == Some(true) {
585            writeln!(writer, "  Node is QUARANTINED (driver failed to start). Check driver logs.")?;
586        }
587
588        let properties = node_to_bind_properties(node.node_property_list.as_deref());
589
590        writeln!(writer, "\nFuzzy matching against all drivers...")?;
591        let drivers = fdev::get_driver_info(driver_dev_proxy, &[]).await?;
592        for driver in drivers {
593            if let Some(bytecode) = &driver.bind_rules_bytecode {
594                if let Ok(rules) = DecodedRules::new(bytecode.clone()) {
595                    if is_fuzzy_match(&rules, &properties) {
596                        writeln!(
597                            writer,
598                            "\n------------------------------------------------------------\nPotential match: driver {}",
599                            driver.url.as_deref().unwrap_or("unknown")
600                        )?;
601                        match rules {
602                            DecodedRules::Normal(r) => {
603                                if match_bind(
604                                    MatchBindData {
605                                        symbol_table: &r.symbol_table,
606                                        instructions: &r.instructions,
607                                    },
608                                    &properties,
609                                )
610                                .unwrap_or(false)
611                                {
612                                    writeln!(writer, "  Matches!")?;
613                                } else {
614                                    let diags = evaluate_rules_bytecode(
615                                        &r.symbol_table,
616                                        &r.instructions,
617                                        &properties,
618                                    );
619                                    report_diagnostics(&diags, writer, 2)?;
620                                }
621                            }
622                            DecodedRules::Composite(_) => {
623                                writeln!(
624                                    writer,
625                                    "  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.",
626                                    driver.url.as_deref().unwrap_or("unknown")
627                                )?;
628                            }
629                        }
630                    }
631                }
632            }
633        }
634
635        writeln!(writer, "\nFuzzy matching against all composite node specs...")?;
636        let specs = fdev::get_composite_node_specs(driver_dev_proxy, None).await?;
637        for spec_info in specs {
638            if let Some(spec) = &spec_info.spec {
639                if let Some(parents) = &spec.parents {
640                    for (i, parent) in parents.iter().enumerate() {
641                        if parent.is_fuzzy_match(&properties) {
642                            let spec_name = spec.name.as_deref().unwrap_or("unknown");
643                            writeln!(
644                                writer,
645                                "\n------------------------------------------------------------\nPotential match: spec {} parent {}",
646                                spec_name, i
647                            )?;
648                            let diags = parent.evaluate_bind_rules(&properties);
649                            report_diagnostics(&diags, writer, 2)?;
650                        }
651                    }
652                } else if let Some(parents2) = &spec.parents2 {
653                    for (i, parent) in parents2.iter().enumerate() {
654                        if parent.is_fuzzy_match(&properties) {
655                            let spec_name = spec.name.as_deref().unwrap_or("unknown");
656                            writeln!(
657                                writer,
658                                "\n------------------------------------------------------------\nPotential match: spec {} parent {}",
659                                spec_name, i
660                            )?;
661                            let diags = parent.evaluate_bind_rules(&properties);
662                            report_diagnostics(&diags, writer, 2)?;
663                        }
664                    }
665                }
666            }
667        }
668    }
669    Ok(())
670}
671
672async fn diagnose_node(
673    node_moniker: &str,
674    driver_dev_proxy: &fdd::ManagerProxy,
675    writer: &mut dyn io::Write,
676) -> Result<()> {
677    writeln!(writer, "Diagnosing node {}", node_moniker)?;
678    let nodes = fdev::get_device_info(driver_dev_proxy, &[node_moniker.to_string()], true).await?;
679    if nodes.is_empty() {
680        writeln!(writer, "  ERROR: Node not found.")?;
681        return Ok(());
682    }
683    diagnose_node_info(&nodes[0], driver_dev_proxy, writer).await
684}
685
686fn is_spec_fuzzy_match(rules: &[fdf::BindRule], properties: &DeviceProperties) -> bool {
687    rules.iter().any(|rule| {
688        let key = match &rule.key {
689            fdf::NodePropertyKey::IntValue(v) => PropertyKey::NumberKey(*v as u64),
690            fdf::NodePropertyKey::StringValue(v) => PropertyKey::StringKey(v.clone()),
691        };
692        properties.contains_key(&key)
693    })
694}
695
696fn is_spec_fuzzy_match2(rules: &[fdf::BindRule2], properties: &DeviceProperties) -> bool {
697    rules.iter().any(|rule| {
698        let key = PropertyKey::StringKey(rule.key.clone());
699        properties.contains_key(&key)
700    })
701}
702
703async fn get_moniker_from_path(path: &str, proxy: &fdd::ManagerProxy) -> Result<String> {
704    let nodes = fdev::get_device_info(proxy, &[path.to_string()], true).await.unwrap_or_default();
705    if let Some(node) = nodes.first() {
706        if let Some(moniker) = &node.moniker {
707            return Ok(moniker.clone());
708        }
709    }
710    Ok(path.to_string())
711}
712
713async fn diagnose_spec_info(
714    spec_info: &fdf::CompositeInfo,
715    driver_dev_proxy: &fdd::ManagerProxy,
716    writer: &mut dyn io::Write,
717) -> Result<()> {
718    if let Some(driver) = spec_info
719        .matched_driver
720        .as_ref()
721        .and_then(|m| m.composite_driver.as_ref())
722        .and_then(|cd| cd.driver_info.as_ref())
723        .and_then(|di| di.url.as_deref())
724    {
725        writeln!(writer, "  Spec matched to driver: {}", driver)?;
726    } else {
727        writeln!(writer, "  Spec did NOT match any driver.")?;
728        writeln!(writer, "\nFuzzy matching against all composite drivers...")?;
729        if let Some(spec) = &spec_info.spec {
730            let drivers = fdev::get_driver_info(driver_dev_proxy, &[]).await?;
731            for driver in drivers {
732                if let Some(bytecode) = &driver.bind_rules_bytecode {
733                    if let Ok(DecodedRules::Composite(rules)) = DecodedRules::new(bytecode.clone())
734                    {
735                        let mut potential_match = false;
736                        if let Some(parents) = &spec.parents {
737                            if parents.iter().any(|p| {
738                                is_fuzzy_match(
739                                    &DecodedRules::Composite(rules.clone()),
740                                    &p.to_properties(),
741                                )
742                            }) {
743                                potential_match = true;
744                            }
745                        } else if let Some(parents2) = &spec.parents2 {
746                            if parents2.iter().any(|p| {
747                                is_fuzzy_match(
748                                    &DecodedRules::Composite(rules.clone()),
749                                    &p.to_properties(),
750                                )
751                            }) {
752                                potential_match = true;
753                            }
754                        }
755
756                        if potential_match {
757                            writeln!(
758                                writer,
759                                "\n------------------------------------------------------------\nPotential match: driver {}",
760                                driver.url.as_deref().unwrap_or("unknown")
761                            )?;
762                            if let Some(parents) = &spec.parents {
763                                diagnose_composite_match(&rules, parents, writer)?;
764                            } else if let Some(parents2) = &spec.parents2 {
765                                diagnose_composite_match(&rules, parents2, writer)?;
766                            }
767                        }
768                    }
769                }
770            }
771        }
772    }
773
774    writeln!(writer, "\nFuzzy matching spec parents against all unbound nodes...")?;
775
776    // Query for existing composite nodes to get their bound parents
777    let mut parent_paths: Vec<Option<String>> = Vec::new();
778    let mut parent_monikers: Vec<Option<String>> = Vec::new();
779    if let Some(spec) = &spec_info.spec {
780        let (iterator, iterator_server) =
781            driver_dev_proxy.domain().create_proxy::<fdd::CompositeInfoIteratorMarker>();
782        if driver_dev_proxy.get_composite_info(iterator_server).is_ok() {
783            loop {
784                if let Ok(composite_list) = iterator.get_next().await {
785                    if composite_list.is_empty() {
786                        break;
787                    }
788                    for composite_node in composite_list {
789                        if let Some(fdd::CompositeInfo::Composite(node_info)) =
790                            composite_node.composite
791                        {
792                            if node_info.spec.and_then(|s| s.name) == spec.name {
793                                parent_paths =
794                                    composite_node.parent_topological_paths.unwrap_or_default();
795                                parent_monikers =
796                                    composite_node.parent_monikers.unwrap_or_default();
797                                break;
798                            }
799                        }
800                    }
801                    if !parent_paths.is_empty() || !parent_monikers.is_empty() {
802                        break;
803                    }
804                } else {
805                    break;
806                }
807            }
808        }
809    }
810
811    let nodes = fdev::get_device_info(driver_dev_proxy, &[], false).await?;
812    let unbound_nodes = nodes.into_iter().filter(|n| is_node_unbound(n)).collect_vec();
813
814    let parent_names = spec_info.matched_driver.as_ref().and_then(|m| m.parent_names.as_ref());
815
816    if let Some(spec) = &spec_info.spec {
817        if let Some(parents) = &spec.parents {
818            for (i, parent) in parents.iter().enumerate() {
819                let name =
820                    parent_names.and_then(|n| n.get(i)).map(|s| s.as_str()).unwrap_or("unknown");
821                writeln!(writer, "  Parent {} ({}):", i, name)?;
822
823                let mut bound_node = None;
824                if let Some(Some(moniker)) = parent_monikers.get(i) {
825                    bound_node = Some(moniker.clone());
826                } else if let Some(Some(path)) = parent_paths.get(i) {
827                    bound_node = Some(
828                        get_moniker_from_path(path, driver_dev_proxy)
829                            .await
830                            .unwrap_or_else(|_| path.clone()),
831                    );
832                }
833
834                if let Some(bound_name) = bound_node {
835                    writeln!(writer, "    Already bound to node: {}", bound_name)?;
836                } else {
837                    let mut found_match = false;
838                    for node in &unbound_nodes {
839                        let properties =
840                            node_to_bind_properties(node.node_property_list.as_deref());
841                        if parent.is_fuzzy_match(&properties) {
842                            found_match = true;
843                            writeln!(
844                                writer,
845                                "    Potential match: node {}",
846                                node.moniker.as_deref().unwrap_or("unknown")
847                            )?;
848                            let diags = parent.evaluate_bind_rules(&properties);
849                            report_diagnostics(&diags, writer, 4)?;
850                        }
851                    }
852                    if !found_match {
853                        writeln!(writer, "    No unbound nodes matched this parent.")?;
854                    }
855                }
856            }
857        } else if let Some(parents2) = &spec.parents2 {
858            for (i, parent) in parents2.iter().enumerate() {
859                let name =
860                    parent_names.and_then(|n| n.get(i)).map(|s| s.as_str()).unwrap_or("unknown");
861                writeln!(writer, "  Parent {} ({}):", i, name)?;
862
863                let mut bound_node = None;
864                if let Some(Some(moniker)) = parent_monikers.get(i) {
865                    bound_node = Some(moniker.clone());
866                } else if let Some(Some(path)) = parent_paths.get(i) {
867                    bound_node = Some(
868                        get_moniker_from_path(path, driver_dev_proxy)
869                            .await
870                            .unwrap_or_else(|_| path.clone()),
871                    );
872                }
873
874                if let Some(bound_name) = bound_node {
875                    writeln!(writer, "    Already bound to node: {}", bound_name)?;
876                } else {
877                    let mut found_match = false;
878                    for node in &unbound_nodes {
879                        let properties =
880                            node_to_bind_properties(node.node_property_list.as_deref());
881                        if parent.is_fuzzy_match(&properties) {
882                            found_match = true;
883                            writeln!(
884                                writer,
885                                "    Potential match: node {}",
886                                node.moniker.as_deref().unwrap_or("unknown")
887                            )?;
888                            let diags = parent.evaluate_bind_rules(&properties);
889                            report_diagnostics(&diags, writer, 4)?;
890                        }
891                    }
892                    if !found_match {
893                        writeln!(writer, "    No unbound nodes matched this parent.")?;
894                    }
895                }
896            }
897        }
898    }
899
900    Ok(())
901}
902
903async fn diagnose_spec(
904    spec_name: &str,
905    driver_dev_proxy: &fdd::ManagerProxy,
906    writer: &mut dyn io::Write,
907) -> Result<()> {
908    writeln!(writer, "Diagnosing composite node spec {}", spec_name)?;
909    let specs = fdev::get_composite_node_specs(driver_dev_proxy, None).await?;
910    let spec = specs.iter().find(|s| {
911        s.spec
912            .as_ref()
913            .and_then(|spec| spec.name.as_ref())
914            .map(|name| name == spec_name)
915            .unwrap_or(false)
916    });
917
918    match spec {
919        None => writeln!(writer, "  ERROR: Composite node spec not found.")?,
920        Some(s) => {
921            diagnose_spec_info(s, driver_dev_proxy, writer).await?;
922        }
923    }
924
925    Ok(())
926}
927
928async fn diagnose_all(
929    driver_dev_proxy: &fdd::ManagerProxy,
930    writer: &mut dyn io::Write,
931) -> Result<()> {
932    writeln!(writer, "Diagnosing all unbound nodes")?;
933    let nodes = fdev::get_device_info(driver_dev_proxy, &[], false).await?;
934    let unbound_nodes = nodes.into_iter().filter(|n| is_node_unbound(n)).collect_vec();
935
936    if unbound_nodes.is_empty() {
937        writeln!(writer, "No unbound nodes found.")?;
938    } else {
939        for node in unbound_nodes {
940            writeln!(writer, "Diagnosing node {}", node.moniker.as_deref().unwrap_or("unknown"))?;
941            diagnose_node_info(&node, driver_dev_proxy, writer).await?;
942        }
943    }
944
945    writeln!(writer, "\n\nDiagnosing all unmatched composite node specs")?;
946    let specs = fdev::get_composite_node_specs(driver_dev_proxy, None).await?;
947    let unmatched_specs = specs.into_iter().filter(|s| s.matched_driver.is_none()).collect_vec();
948
949    if unmatched_specs.is_empty() {
950        writeln!(writer, "No unmatched composite node specs found.")?;
951    } else {
952        for spec in unmatched_specs {
953            let name = spec.spec.as_ref().and_then(|s| s.name.as_deref()).unwrap_or("unknown");
954            writeln!(writer, "Diagnosing composite node spec {}", name)?;
955            diagnose_spec_info(&spec, driver_dev_proxy, writer).await?;
956        }
957    }
958
959    Ok(())
960}
961
962fn node_to_bind_properties(node_props: Option<&[fdf::NodeProperty]>) -> DeviceProperties {
963    let mut props = HashMap::new();
964    if let Some(node_props) = node_props {
965        for prop in node_props {
966            let key = match &prop.key {
967                fdf::NodePropertyKey::IntValue(v) => PropertyKey::NumberKey(*v as u64),
968                fdf::NodePropertyKey::StringValue(v) => PropertyKey::StringKey(v.clone()),
969            };
970            let value = match &prop.value {
971                fdf::NodePropertyValue::IntValue(v) => Symbol::NumberValue(*v as u64),
972                fdf::NodePropertyValue::StringValue(v) => Symbol::StringValue(v.clone()),
973                fdf::NodePropertyValue::BoolValue(v) => Symbol::BoolValue(*v),
974                fdf::NodePropertyValue::EnumValue(v) => Symbol::EnumValue(v.clone()),
975                _ => continue,
976            };
977            props.insert(key, value);
978        }
979    }
980    props
981}
982
983fn node_to_bind_properties2(node_props: Option<&[fdf::NodeProperty2]>) -> DeviceProperties {
984    let mut props = HashMap::new();
985    if let Some(node_props) = node_props {
986        for prop in node_props {
987            let key = PropertyKey::StringKey(prop.key.clone());
988            let value = match &prop.value {
989                fdf::NodePropertyValue::IntValue(v) => Symbol::NumberValue(*v as u64),
990                fdf::NodePropertyValue::StringValue(v) => Symbol::StringValue(v.clone()),
991                fdf::NodePropertyValue::BoolValue(v) => Symbol::BoolValue(*v),
992                fdf::NodePropertyValue::EnumValue(v) => Symbol::EnumValue(v.clone()),
993                _ => continue,
994            };
995            props.insert(key, value);
996        }
997    }
998    props
999}
1000
1001#[derive(Debug)]
1002enum Diagnostic {
1003    Mismatch { key: PropertyKey, expected: Symbol, actual: Option<Symbol>, is_equal: bool },
1004    AbortReached,
1005}
1006
1007fn evaluate_rules_bytecode(
1008    symbol_table: &HashMap<u32, String>,
1009    instructions: &[u8],
1010    properties: &DeviceProperties,
1011) -> Vec<Diagnostic> {
1012    let mut diagnostics = Vec::new();
1013    let mut iter = instructions.iter();
1014
1015    while let Some(byte) = iter.next() {
1016        let op_byte_val = *byte;
1017        if op_byte_val == RawOp::EqualCondition as u8
1018            || op_byte_val == RawOp::InequalCondition as u8
1019        {
1020            let is_equal = op_byte_val == RawOp::EqualCondition as u8;
1021            let (res, diag) =
1022                read_and_evaluate_values(&mut iter, symbol_table, properties, is_equal);
1023            if !res {
1024                if let Some(d) = diag {
1025                    diagnostics.push(d);
1026                }
1027                break;
1028            }
1029        } else if op_byte_val == RawOp::Abort as u8 {
1030            diagnostics.push(Diagnostic::AbortReached);
1031            break;
1032        } else if op_byte_val == RawOp::UnconditionalJump as u8 {
1033            let offset = match next_u32(&mut iter) {
1034                Ok(o) => o,
1035                Err(_) => break,
1036            };
1037            for _ in 0..offset {
1038                if iter.next().is_none() {
1039                    break;
1040                }
1041            }
1042            if iter.next() != Some(&(RawOp::JumpLandPad as u8)) {
1043                break;
1044            }
1045        } else if op_byte_val == RawOp::JumpIfEqual as u8
1046            || op_byte_val == RawOp::JumpIfNotEqual as u8
1047        {
1048            let is_equal = op_byte_val == RawOp::JumpIfEqual as u8;
1049            let offset = match next_u32(&mut iter) {
1050                Ok(o) => o,
1051                Err(_) => break,
1052            };
1053            let (res, _) = read_and_evaluate_values(&mut iter, symbol_table, properties, is_equal);
1054            if res {
1055                for _ in 0..offset {
1056                    if iter.next().is_none() {
1057                        break;
1058                    }
1059                }
1060                if iter.next() != Some(&(RawOp::JumpLandPad as u8)) {
1061                    break;
1062                }
1063            }
1064        } else if op_byte_val == RawOp::JumpLandPad as u8 {
1065            // Nothing to do
1066        } else {
1067            // Unknown opcode, stop
1068            break;
1069        }
1070    }
1071    diagnostics
1072}
1073
1074fn read_and_evaluate_values(
1075    iter: &mut BytecodeIter<'_>,
1076    symbol_table: &HashMap<u32, String>,
1077    properties: &DeviceProperties,
1078    is_equal: bool,
1079) -> (bool, Option<Diagnostic>) {
1080    let lhs = match read_next_value(iter, symbol_table) {
1081        Ok(v) => v,
1082        Err(_) => return (false, None),
1083    };
1084    let rhs = match read_next_value(iter, symbol_table) {
1085        Ok(v) => v,
1086        Err(_) => return (false, None),
1087    };
1088
1089    let key = match lhs {
1090        Symbol::NumberValue(v) => PropertyKey::NumberKey(v),
1091        Symbol::StringValue(v) => PropertyKey::StringKey(v),
1092        Symbol::Key(v, _) => PropertyKey::StringKey(v),
1093        _ => return (false, None),
1094    };
1095
1096    let actual = properties.get(&key).cloned();
1097    let mut effective_actual = actual;
1098    if effective_actual.is_none() {
1099        if let PropertyKey::NumberKey(int_key) = &key {
1100            if let Some(str_key) = get_deprecated_key_identifier(*int_key as u32) {
1101                effective_actual = properties.get(&PropertyKey::StringKey(str_key)).cloned();
1102            }
1103        }
1104    }
1105
1106    let matches = match &effective_actual {
1107        Some(val) => {
1108            let mut val_for_compare = val.clone();
1109            if let Symbol::EnumValue(v) = val {
1110                val_for_compare = Symbol::StringValue(v.clone());
1111            }
1112            let mut rhs_for_compare = rhs.clone();
1113            if let Symbol::EnumValue(v) = &rhs {
1114                rhs_for_compare = Symbol::StringValue(v.clone());
1115            }
1116
1117            val_for_compare == rhs_for_compare
1118        }
1119        None => &bind::compiler::Symbol::NumberValue(0) == &rhs,
1120    };
1121
1122    if matches == is_equal {
1123        (true, None)
1124    } else {
1125        (
1126            false,
1127            Some(Diagnostic::Mismatch { key, expected: rhs, actual: effective_actual, is_equal }),
1128        )
1129    }
1130}
1131
1132fn read_next_value(
1133    iter: &mut BytecodeIter<'_>,
1134    symbol_table: &HashMap<u32, String>,
1135) -> Result<Symbol, ()> {
1136    let value_type_byte = next_u8(iter).map_err(|_| ())?;
1137    let value_type_val = *value_type_byte;
1138    let value = next_u32(iter).map_err(|_| ())?;
1139
1140    if value_type_val == RawValueType::NumberValue as u8 {
1141        Ok(Symbol::NumberValue(value as u64))
1142    } else if value_type_val == RawValueType::Key as u8 {
1143        let name = symbol_table.get(&value).ok_or(())?.clone();
1144        Ok(Symbol::Key(name, bind::parser::bind_library::ValueType::Str))
1145    } else if value_type_val == RawValueType::StringValue as u8 {
1146        let val = symbol_table.get(&value).ok_or(())?.clone();
1147        Ok(Symbol::StringValue(val))
1148    } else if value_type_val == RawValueType::BoolValue as u8 {
1149        match value {
1150            FALSE_VAL => Ok(Symbol::BoolValue(false)),
1151            TRUE_VAL => Ok(Symbol::BoolValue(true)),
1152            _ => Err(()),
1153        }
1154    } else if value_type_val == RawValueType::EnumValue as u8 {
1155        let val = symbol_table.get(&value).ok_or(())?.clone();
1156        Ok(Symbol::EnumValue(val))
1157    } else {
1158        Err(())
1159    }
1160}
1161
1162fn evaluate_bind_rules(rules: &[fdf::BindRule], properties: &DeviceProperties) -> Vec<Diagnostic> {
1163    let mut diagnostics = Vec::new();
1164    for rule in rules {
1165        let key = match &rule.key {
1166            fdf::NodePropertyKey::IntValue(v) => PropertyKey::NumberKey(*v as u64),
1167            fdf::NodePropertyKey::StringValue(v) => PropertyKey::StringKey(v.clone()),
1168        };
1169        let actual = properties.get(&key).cloned();
1170
1171        let mut matched = false;
1172        for val in &rule.values {
1173            let symbol_val = match val {
1174                fdf::NodePropertyValue::IntValue(v) => Symbol::NumberValue(*v as u64),
1175                fdf::NodePropertyValue::StringValue(v) => Symbol::StringValue(v.clone()),
1176                fdf::NodePropertyValue::BoolValue(v) => Symbol::BoolValue(*v),
1177                fdf::NodePropertyValue::EnumValue(v) => Symbol::EnumValue(v.clone()),
1178                _ => continue,
1179            };
1180            if actual.as_ref() == Some(&symbol_val) {
1181                matched = true;
1182                break;
1183            }
1184        }
1185
1186        match rule.condition {
1187            fdf::Condition::Accept => {
1188                if !matched {
1189                    diagnostics.push(Diagnostic::Mismatch {
1190                        key,
1191                        expected: Symbol::StringValue("one of requested values".to_string()),
1192                        actual,
1193                        is_equal: true,
1194                    });
1195                }
1196            }
1197            fdf::Condition::Reject => {
1198                if matched {
1199                    diagnostics.push(Diagnostic::Mismatch {
1200                        key,
1201                        expected: Symbol::StringValue("none of requested values".to_string()),
1202                        actual,
1203                        is_equal: false,
1204                    });
1205                }
1206            }
1207            _ => {}
1208        }
1209    }
1210    diagnostics
1211}
1212
1213fn evaluate_bind_rules2(
1214    rules: &[fdf::BindRule2],
1215    properties: &DeviceProperties,
1216) -> Vec<Diagnostic> {
1217    let mut diagnostics = Vec::new();
1218    for rule in rules {
1219        let key = PropertyKey::StringKey(rule.key.clone());
1220        let actual = properties.get(&key).cloned();
1221
1222        let mut matched = false;
1223        for val in &rule.values {
1224            let symbol_val = match val {
1225                fdf::NodePropertyValue::IntValue(v) => Symbol::NumberValue(*v as u64),
1226                fdf::NodePropertyValue::StringValue(v) => Symbol::StringValue(v.clone()),
1227                fdf::NodePropertyValue::BoolValue(v) => Symbol::BoolValue(*v),
1228                fdf::NodePropertyValue::EnumValue(v) => Symbol::EnumValue(v.clone()),
1229                _ => continue,
1230            };
1231            if actual.as_ref() == Some(&symbol_val) {
1232                matched = true;
1233                break;
1234            }
1235        }
1236
1237        match rule.condition {
1238            fdf::Condition::Accept => {
1239                if !matched {
1240                    diagnostics.push(Diagnostic::Mismatch {
1241                        key,
1242                        expected: Symbol::StringValue("one of requested values".to_string()),
1243                        actual,
1244                        is_equal: true,
1245                    });
1246                }
1247            }
1248            fdf::Condition::Reject => {
1249                if matched {
1250                    diagnostics.push(Diagnostic::Mismatch {
1251                        key,
1252                        expected: Symbol::StringValue("none of requested values".to_string()),
1253                        actual,
1254                        is_equal: false,
1255                    });
1256                }
1257            }
1258            _ => {}
1259        }
1260    }
1261    diagnostics
1262}
1263
1264fn report_diagnostics(
1265    diagnostics: &[Diagnostic],
1266    writer: &mut dyn io::Write,
1267    indent: usize,
1268) -> Result<()> {
1269    let indent_str = " ".repeat(indent);
1270    if diagnostics.is_empty() {
1271        writeln!(writer, "{}No issues found with direct property matching.", indent_str)?;
1272    } else {
1273        for diag in diagnostics {
1274            match diag {
1275                Diagnostic::Mismatch { key, expected, actual, is_equal } => {
1276                    let key_str = match key {
1277                        PropertyKey::NumberKey(v) => format!("{:#x}", v),
1278                        PropertyKey::StringKey(v) => v.clone(),
1279                    };
1280                    let actual_str = match actual {
1281                        Some(v) => v.to_string(),
1282                        None => "missing".to_string(),
1283                    };
1284                    if *is_equal {
1285                        writeln!(
1286                            writer,
1287                            "{}Mismatch: key {} expected {} but found {}",
1288                            indent_str, key_str, expected, actual_str
1289                        )?;
1290                    } else {
1291                        writeln!(
1292                            writer,
1293                            "{}Mismatch: key {} expected NOT {} but found {}",
1294                            indent_str, key_str, expected, actual_str
1295                        )?;
1296                    }
1297                }
1298                Diagnostic::AbortReached => {
1299                    writeln!(writer, "{}Unconditional Abort reached in bind rules.", indent_str)?;
1300                }
1301            }
1302        }
1303    }
1304    Ok(())
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309    use super::*;
1310    use bind::interpreter::instruction_decoder::{DecodedCondition, DecodedInstruction};
1311    use bind::parser::bind_library::ValueType;
1312
1313    #[test]
1314    fn test_evaluate_rules_mismatch() {
1315        let symbol_table = HashMap::new();
1316        let mut instructions = vec![RawOp::EqualCondition as u8];
1317        instructions.push(RawValueType::NumberValue as u8);
1318        instructions.extend_from_slice(&1u32.to_le_bytes());
1319        instructions.push(RawValueType::NumberValue as u8);
1320        instructions.extend_from_slice(&100u32.to_le_bytes());
1321
1322        let mut properties = HashMap::new();
1323        properties.insert(PropertyKey::NumberKey(1), Symbol::NumberValue(200));
1324
1325        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1326        assert_eq!(diagnostics.len(), 1);
1327        match &diagnostics[0] {
1328            Diagnostic::Mismatch { key, expected, actual, is_equal } => {
1329                assert_eq!(key, &PropertyKey::NumberKey(1));
1330                assert_eq!(expected, &Symbol::NumberValue(100));
1331                assert_eq!(actual, &Some(Symbol::NumberValue(200)));
1332                assert!(is_equal);
1333            }
1334            _ => panic!("Expected Mismatch"),
1335        }
1336    }
1337
1338    #[test]
1339    fn test_evaluate_rules_match() {
1340        let symbol_table = HashMap::new();
1341        let mut instructions = vec![RawOp::EqualCondition as u8];
1342        instructions.push(RawValueType::NumberValue as u8);
1343        instructions.extend_from_slice(&1u32.to_le_bytes());
1344        instructions.push(RawValueType::NumberValue as u8);
1345        instructions.extend_from_slice(&100u32.to_le_bytes());
1346
1347        let mut properties = HashMap::new();
1348        properties.insert(PropertyKey::NumberKey(1), Symbol::NumberValue(100));
1349
1350        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1351        assert_eq!(diagnostics.len(), 0);
1352    }
1353
1354    #[test]
1355    fn test_evaluate_rules_missing_property() {
1356        let mut symbol_table = HashMap::new();
1357        symbol_table.insert(1, "key1".to_string());
1358
1359        // IF key1 == 0 (should match if missing)
1360        let mut instructions = vec![RawOp::EqualCondition as u8];
1361        instructions.push(RawValueType::StringValue as u8);
1362        instructions.extend_from_slice(&1u32.to_le_bytes());
1363        instructions.push(RawValueType::NumberValue as u8);
1364        instructions.extend_from_slice(&0u32.to_le_bytes());
1365
1366        let properties = HashMap::new();
1367
1368        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1369        assert_eq!(diagnostics.len(), 0);
1370
1371        // IF key1 != 0 (should mismatch if missing)
1372        let mut instructions = vec![RawOp::InequalCondition as u8];
1373        instructions.push(RawValueType::StringValue as u8);
1374        instructions.extend_from_slice(&1u32.to_le_bytes());
1375        instructions.push(RawValueType::NumberValue as u8);
1376        instructions.extend_from_slice(&0u32.to_le_bytes());
1377
1378        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1379        assert_eq!(diagnostics.len(), 1);
1380        match &diagnostics[0] {
1381            Diagnostic::Mismatch { key, expected, actual, is_equal } => {
1382                assert_eq!(key, &PropertyKey::StringKey("key1".to_string()));
1383                assert_eq!(expected, &Symbol::NumberValue(0));
1384                assert_eq!(actual, &None);
1385                assert!(!is_equal);
1386            }
1387            _ => panic!("Expected Mismatch"),
1388        }
1389    }
1390
1391    #[test]
1392    fn test_evaluate_rules_control_flow() {
1393        let mut symbol_table = HashMap::new();
1394        symbol_table.insert(1, "protocol".to_string());
1395        symbol_table.insert(2, "vendor_id".to_string());
1396
1397        // if protocol == PCI { vendor_id == GOOGLE }
1398        // PCI = 10, GOOGLE = 0x1234
1399        // Bytecode:
1400        // JumpIfNotEqual(protocol, 10, target)
1401        // EqualCondition(vendor_id, 0x1234)
1402        // JumpLandPad(target)
1403
1404        let mut instructions = vec![RawOp::JumpIfNotEqual as u8];
1405        instructions.extend_from_slice(&11u32.to_le_bytes()); // Offset
1406        instructions.push(RawValueType::StringValue as u8);
1407        instructions.extend_from_slice(&1u32.to_le_bytes());
1408        instructions.push(RawValueType::NumberValue as u8);
1409        instructions.extend_from_slice(&10u32.to_le_bytes());
1410
1411        instructions.push(RawOp::EqualCondition as u8);
1412        instructions.push(RawValueType::StringValue as u8);
1413        instructions.extend_from_slice(&2u32.to_le_bytes());
1414        instructions.push(RawValueType::NumberValue as u8);
1415        instructions.extend_from_slice(&0x1234u32.to_le_bytes());
1416
1417        instructions.push(RawOp::JumpLandPad as u8);
1418
1419        // Case 1: protocol is USB (20). Should jump over vendor_id check.
1420        let mut properties = HashMap::new();
1421        properties.insert(PropertyKey::StringKey("protocol".to_string()), Symbol::NumberValue(20));
1422        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1423        assert_eq!(diagnostics.len(), 0);
1424
1425        // Case 2: protocol is PCI (10). Should NOT jump, and report vendor_id mismatch if it's wrong.
1426        let mut properties = HashMap::new();
1427        properties.insert(PropertyKey::StringKey("protocol".to_string()), Symbol::NumberValue(10));
1428        properties
1429            .insert(PropertyKey::StringKey("vendor_id".to_string()), Symbol::NumberValue(0x5678));
1430        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1431        assert_eq!(diagnostics.len(), 1);
1432        match &diagnostics[0] {
1433            Diagnostic::Mismatch { key, .. } => {
1434                assert_eq!(key, &PropertyKey::StringKey("vendor_id".to_string()));
1435            }
1436            _ => panic!("Expected vendor_id mismatch"),
1437        }
1438    }
1439
1440    #[test]
1441    fn test_evaluate_rules_abort() {
1442        let symbol_table = HashMap::new();
1443        let instructions = vec![RawOp::Abort as u8];
1444        let properties = HashMap::new();
1445
1446        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1447        assert_eq!(diagnostics.len(), 1);
1448        match &diagnostics[0] {
1449            Diagnostic::AbortReached => {}
1450            _ => panic!("Expected AbortReached"),
1451        }
1452    }
1453
1454    #[test]
1455    fn test_evaluate_rules_deprecated_key() {
1456        let symbol_table = HashMap::new();
1457        // fuchsia.BIND_PROTOCOL is 0x0001
1458        let mut instructions = vec![RawOp::EqualCondition as u8];
1459        instructions.push(RawValueType::NumberValue as u8);
1460        instructions.extend_from_slice(&1u32.to_le_bytes());
1461        instructions.push(RawValueType::NumberValue as u8);
1462        instructions.extend_from_slice(&85u32.to_le_bytes());
1463
1464        let mut properties = HashMap::new();
1465        properties.insert(
1466            PropertyKey::StringKey("fuchsia.BIND_PROTOCOL".to_string()),
1467            Symbol::NumberValue(85),
1468        );
1469
1470        let diagnostics = evaluate_rules_bytecode(&symbol_table, &instructions, &properties);
1471        assert_eq!(diagnostics.len(), 0);
1472    }
1473
1474    #[test]
1475    fn test_evaluate_bind_rules_mismatch() {
1476        let rules = vec![fdf::BindRule {
1477            key: fdf::NodePropertyKey::IntValue(1),
1478            condition: fdf::Condition::Accept,
1479            values: vec![fdf::NodePropertyValue::IntValue(100)],
1480        }];
1481        let mut properties = HashMap::new();
1482        properties.insert(PropertyKey::NumberKey(1), Symbol::NumberValue(200));
1483
1484        let diagnostics = evaluate_bind_rules(&rules, &properties);
1485        assert_eq!(diagnostics.len(), 1);
1486    }
1487
1488    #[test]
1489    fn test_evaluate_bind_rules2_mismatch() {
1490        let rules = vec![fdf::BindRule2 {
1491            key: "key1".to_string(),
1492            condition: fdf::Condition::Accept,
1493            values: vec![fdf::NodePropertyValue::IntValue(100)],
1494        }];
1495        let mut properties = HashMap::new();
1496        properties.insert(PropertyKey::StringKey("key1".to_string()), Symbol::NumberValue(200));
1497
1498        let diagnostics = evaluate_bind_rules2(&rules, &properties);
1499        assert_eq!(diagnostics.len(), 1);
1500        match &diagnostics[0] {
1501            Diagnostic::Mismatch { key, expected: _, actual, is_equal: _ } => {
1502                assert_eq!(key, &PropertyKey::StringKey("key1".to_string()));
1503                assert_eq!(actual, &Some(Symbol::NumberValue(200)));
1504            }
1505            _ => panic!("Expected Mismatch"),
1506        }
1507    }
1508
1509    #[test]
1510    fn test_is_fuzzy_match() {
1511        let mut symbol_table = HashMap::new();
1512        symbol_table.insert(1, "key1".to_string());
1513        let rules = DecodedRules::Normal(bind::interpreter::decode_bind_rules::DecodedBindRules {
1514            symbol_table,
1515            instructions: vec![],
1516            decoded_instructions: vec![DecodedInstruction::Condition(DecodedCondition {
1517                lhs: Symbol::Key("key1".to_string(), ValueType::Str),
1518                rhs: Symbol::NumberValue(100),
1519                is_equal: true,
1520            })],
1521            debug_info: None,
1522        });
1523        let mut properties = HashMap::new();
1524        properties.insert(PropertyKey::StringKey("key1".to_string()), Symbol::NumberValue(100));
1525        assert!(is_fuzzy_match(&rules, &properties));
1526    }
1527
1528    #[test]
1529    fn test_get_keys_from_instructions() {
1530        let instructions = vec![
1531            DecodedInstruction::Condition(DecodedCondition {
1532                lhs: Symbol::NumberValue(1),
1533                rhs: Symbol::NumberValue(100),
1534                is_equal: true,
1535            }),
1536            DecodedInstruction::Condition(DecodedCondition {
1537                lhs: Symbol::StringValue("key_str".to_string()),
1538                rhs: Symbol::NumberValue(200),
1539                is_equal: true,
1540            }),
1541        ];
1542        let keys = get_keys_from_instructions(&instructions);
1543        assert_eq!(keys.len(), 2);
1544        assert!(keys.contains(&PropertyKey::NumberKey(1)));
1545        assert!(keys.contains(&PropertyKey::StringKey("key_str".to_string())));
1546    }
1547}