// Copyright 2024 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Compare device tree contents. //! Allows skipping over fields provided. use anyhow::anyhow; use anyhow::Context; use anyhow::Result; use clap::Parser; use hex::encode; use libfdt::Fdt; use libfdt::FdtNode; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::fs::read; use std::path::PathBuf; #[derive(Debug, Parser)] /// Device Tree Compare arguments. struct DtCompareArgs { /// first device tree #[arg(long)] dt1: PathBuf, /// second device tree #[arg(long)] dt2: PathBuf, /// list of properties that should exist but are expected to hold different values in the /// trees. #[arg(short = 'I', long)] ignore_path_value: Vec, /// list of paths that will be ignored, whether added, removed, or changed. /// Paths can be nodes, subnodes, or even properties: /// Ex: /avf/unstrusted // this is a path to a subnode. All properties and subnodes underneath /// // it will also be ignored. /// /avf/name // This is a path for a property. Only this property will be ignored. #[arg(short = 'S', long)] ignore_path: Vec, } fn main() -> Result<()> { let args = DtCompareArgs::parse(); let dt1: Vec = read(args.dt1)?; let dt2: Vec = read(args.dt2)?; let ignore_value_set = BTreeSet::from_iter(args.ignore_path_value); let ignore_set = BTreeSet::from_iter(args.ignore_path); compare_device_trees(dt1.as_slice(), dt2.as_slice(), ignore_value_set, ignore_set) } // Compare device trees by doing a pre-order traversal of the trees. fn compare_device_trees( dt1: &[u8], dt2: &[u8], ignore_value_set: BTreeSet, ignore_set: BTreeSet, ) -> Result<()> { let fdt1 = Fdt::from_slice(dt1).context("invalid device tree: Dt1")?; let fdt2 = Fdt::from_slice(dt2).context("invalid device tree: Dt2")?; let mut errors = Vec::new(); compare_subnodes( &fdt1.root(), &fdt2.root(), &ignore_value_set, &ignore_set, /* path */ &mut ["".to_string()], &mut errors, )?; if !errors.is_empty() { return Err(anyhow!( "Following properties had different values: [\n{}\n]\ndetected {} diffs", errors.join("\n"), errors.len() )); } Ok(()) } fn compare_props( root1: &FdtNode, root2: &FdtNode, ignore_value_set: &BTreeSet, ignore_set: &BTreeSet, path: &mut [String], errors: &mut Vec, ) -> Result<()> { let mut prop_map: BTreeMap = BTreeMap::new(); for prop in root1.properties().context("Error getting properties")? { let prop_path = path.join("/") + "/" + prop.name().context("Error getting property name")?.to_str()?; // Do not add to prop map if skipping if ignore_set.contains(&prop_path) { continue; } let value = prop.value().context("Error getting value")?; if prop_map.insert(prop_path.clone(), value).is_some() { return Err(anyhow!("Duplicate property detected in subnode: {}", prop_path)); } } for prop in root2.properties().context("Error getting properties")? { let prop_path = path.join("/") + "/" + prop.name().context("Error getting property name")?.to_str()?; if ignore_set.contains(&prop_path) { continue; } let Some(prop1_value) = prop_map.remove(&prop_path) else { errors.push(format!("added prop_path: {}", prop_path)); continue; }; let prop_compare = prop1_value == prop.value().context("Error getting value")?; // Check if value should be ignored. If yes, skip field. if ignore_value_set.contains(&prop_path) { continue; } if !prop_compare { errors.push(format!( "prop {} value mismatch: old: {} -> new: {}", prop_path, encode(prop1_value), encode(prop.value().context("Error getting value")?) )); } } if !prop_map.is_empty() { errors.push(format!("missing properties: {:?}", prop_map)); } Ok(()) } fn compare_subnodes( node1: &FdtNode, node2: &FdtNode, ignore_value_set: &BTreeSet, ignore_set: &BTreeSet, path: &mut [String], errors: &mut Vec, ) -> Result<()> { let mut subnodes_map: BTreeMap = BTreeMap::new(); for subnode in node1.subnodes().context("Error getting subnodes of first FDT")? { let sn_path = path.join("/") + "/" + subnode.name().context("Error getting property name")?.to_str()?; // Do not add to subnode map if skipping if ignore_set.contains(&sn_path) { continue; } if subnodes_map.insert(sn_path.clone(), subnode).is_some() { return Err(anyhow!("Duplicate subnodes detected: {}", sn_path)); } } for sn2 in node2.subnodes().context("Error getting subnodes of second FDT")? { let sn_path = path.join("/") + "/" + sn2.name().context("Error getting subnode name")?.to_str()?; let sn1 = subnodes_map.remove(&sn_path); match sn1 { Some(sn) => { compare_props( &sn, &sn2, ignore_value_set, ignore_set, &mut [sn_path.clone()], errors, )?; compare_subnodes( &sn, &sn2, ignore_value_set, ignore_set, &mut [sn_path.clone()], errors, )?; } None => errors.push(format!("added node: {}", sn_path)), } } if !subnodes_map.is_empty() { errors.push(format!("missing nodes: {:?}", subnodes_map)); } Ok(()) }