Elie Kheirallah | ea5dd52 | 2024-11-26 22:16:01 +0000 | [diff] [blame] | 1 | // Copyright 2024 The Android Open Source Project |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | //! Compare device tree contents. |
| 16 | //! Allows skipping over fields provided. |
| 17 | |
| 18 | use anyhow::anyhow; |
| 19 | use anyhow::Context; |
| 20 | use anyhow::Result; |
| 21 | use clap::Parser; |
| 22 | use hex::encode; |
| 23 | use libfdt::Fdt; |
| 24 | use libfdt::FdtNode; |
| 25 | |
| 26 | use std::collections::BTreeMap; |
| 27 | use std::collections::BTreeSet; |
| 28 | use std::fs::read; |
| 29 | use std::path::PathBuf; |
| 30 | |
| 31 | #[derive(Debug, Parser)] |
| 32 | /// Device Tree Compare arguments. |
| 33 | struct DtCompareArgs { |
| 34 | /// first device tree |
| 35 | #[arg(long)] |
| 36 | dt1: PathBuf, |
| 37 | /// second device tree |
| 38 | #[arg(long)] |
| 39 | dt2: PathBuf, |
| 40 | /// list of properties that should exist but are expected to hold different values in the |
| 41 | /// trees. |
| 42 | #[arg(short = 'I', long)] |
| 43 | ignore_path_value: Vec<String>, |
| 44 | /// list of paths that will be ignored, whether added, removed, or changed. |
| 45 | /// Paths can be nodes, subnodes, or even properties: |
| 46 | /// Ex: /avf/unstrusted // this is a path to a subnode. All properties and subnodes underneath |
| 47 | /// // it will also be ignored. |
| 48 | /// /avf/name // This is a path for a property. Only this property will be ignored. |
| 49 | #[arg(short = 'S', long)] |
| 50 | ignore_path: Vec<String>, |
| 51 | } |
| 52 | |
| 53 | fn main() -> Result<()> { |
| 54 | let args = DtCompareArgs::parse(); |
| 55 | let dt1: Vec<u8> = read(args.dt1)?; |
| 56 | let dt2: Vec<u8> = read(args.dt2)?; |
| 57 | let ignore_value_set = BTreeSet::from_iter(args.ignore_path_value); |
| 58 | let ignore_set = BTreeSet::from_iter(args.ignore_path); |
| 59 | compare_device_trees(dt1.as_slice(), dt2.as_slice(), ignore_value_set, ignore_set) |
| 60 | } |
| 61 | |
| 62 | // Compare device trees by doing a pre-order traversal of the trees. |
| 63 | fn compare_device_trees( |
| 64 | dt1: &[u8], |
| 65 | dt2: &[u8], |
| 66 | ignore_value_set: BTreeSet<String>, |
| 67 | ignore_set: BTreeSet<String>, |
| 68 | ) -> Result<()> { |
| 69 | let fdt1 = Fdt::from_slice(dt1).context("invalid device tree: Dt1")?; |
| 70 | let fdt2 = Fdt::from_slice(dt2).context("invalid device tree: Dt2")?; |
| 71 | let mut errors = Vec::new(); |
| 72 | compare_subnodes( |
| 73 | &fdt1.root(), |
| 74 | &fdt2.root(), |
| 75 | &ignore_value_set, |
| 76 | &ignore_set, |
| 77 | /* path */ &mut ["".to_string()], |
| 78 | &mut errors, |
| 79 | )?; |
| 80 | if !errors.is_empty() { |
| 81 | return Err(anyhow!( |
| 82 | "Following properties had different values: [\n{}\n]\ndetected {} diffs", |
| 83 | errors.join("\n"), |
| 84 | errors.len() |
| 85 | )); |
| 86 | } |
| 87 | Ok(()) |
| 88 | } |
| 89 | |
| 90 | fn compare_props( |
| 91 | root1: &FdtNode, |
| 92 | root2: &FdtNode, |
| 93 | ignore_value_set: &BTreeSet<String>, |
| 94 | ignore_set: &BTreeSet<String>, |
| 95 | path: &mut [String], |
| 96 | errors: &mut Vec<String>, |
| 97 | ) -> Result<()> { |
| 98 | let mut prop_map: BTreeMap<String, &[u8]> = BTreeMap::new(); |
| 99 | for prop in root1.properties().context("Error getting properties")? { |
| 100 | let prop_path = |
| 101 | path.join("/") + "/" + prop.name().context("Error getting property name")?.to_str()?; |
| 102 | // Do not add to prop map if skipping |
| 103 | if ignore_set.contains(&prop_path) { |
| 104 | continue; |
| 105 | } |
| 106 | let value = prop.value().context("Error getting value")?; |
| 107 | if prop_map.insert(prop_path.clone(), value).is_some() { |
| 108 | return Err(anyhow!("Duplicate property detected in subnode: {}", prop_path)); |
| 109 | } |
| 110 | } |
| 111 | for prop in root2.properties().context("Error getting properties")? { |
| 112 | let prop_path = |
| 113 | path.join("/") + "/" + prop.name().context("Error getting property name")?.to_str()?; |
| 114 | if ignore_set.contains(&prop_path) { |
| 115 | continue; |
| 116 | } |
| 117 | let Some(prop1_value) = prop_map.remove(&prop_path) else { |
| 118 | errors.push(format!("added prop_path: {}", prop_path)); |
| 119 | continue; |
| 120 | }; |
| 121 | let prop_compare = prop1_value == prop.value().context("Error getting value")?; |
| 122 | // Check if value should be ignored. If yes, skip field. |
| 123 | if ignore_value_set.contains(&prop_path) { |
| 124 | continue; |
| 125 | } |
| 126 | if !prop_compare { |
| 127 | errors.push(format!( |
| 128 | "prop {} value mismatch: old: {} -> new: {}", |
| 129 | prop_path, |
| 130 | encode(prop1_value), |
| 131 | encode(prop.value().context("Error getting value")?) |
| 132 | )); |
| 133 | } |
| 134 | } |
| 135 | if !prop_map.is_empty() { |
| 136 | errors.push(format!("missing properties: {:?}", prop_map)); |
| 137 | } |
| 138 | Ok(()) |
| 139 | } |
| 140 | |
| 141 | fn compare_subnodes( |
| 142 | node1: &FdtNode, |
| 143 | node2: &FdtNode, |
| 144 | ignore_value_set: &BTreeSet<String>, |
| 145 | ignore_set: &BTreeSet<String>, |
| 146 | path: &mut [String], |
| 147 | errors: &mut Vec<String>, |
| 148 | ) -> Result<()> { |
| 149 | let mut subnodes_map: BTreeMap<String, FdtNode> = BTreeMap::new(); |
| 150 | for subnode in node1.subnodes().context("Error getting subnodes of first FDT")? { |
| 151 | let sn_path = path.join("/") |
| 152 | + "/" |
| 153 | + subnode.name().context("Error getting property name")?.to_str()?; |
| 154 | // Do not add to subnode map if skipping |
| 155 | if ignore_set.contains(&sn_path) { |
| 156 | continue; |
| 157 | } |
| 158 | if subnodes_map.insert(sn_path.clone(), subnode).is_some() { |
| 159 | return Err(anyhow!("Duplicate subnodes detected: {}", sn_path)); |
| 160 | } |
| 161 | } |
| 162 | for sn2 in node2.subnodes().context("Error getting subnodes of second FDT")? { |
| 163 | let sn_path = |
| 164 | path.join("/") + "/" + sn2.name().context("Error getting subnode name")?.to_str()?; |
| 165 | let sn1 = subnodes_map.remove(&sn_path); |
| 166 | match sn1 { |
| 167 | Some(sn) => { |
| 168 | compare_props( |
| 169 | &sn, |
| 170 | &sn2, |
| 171 | ignore_value_set, |
| 172 | ignore_set, |
| 173 | &mut [sn_path.clone()], |
| 174 | errors, |
| 175 | )?; |
| 176 | compare_subnodes( |
| 177 | &sn, |
| 178 | &sn2, |
| 179 | ignore_value_set, |
| 180 | ignore_set, |
| 181 | &mut [sn_path.clone()], |
| 182 | errors, |
| 183 | )?; |
| 184 | } |
| 185 | None => errors.push(format!("added node: {}", sn_path)), |
| 186 | } |
| 187 | } |
| 188 | if !subnodes_map.is_empty() { |
| 189 | errors.push(format!("missing nodes: {:?}", subnodes_map)); |
| 190 | } |
| 191 | Ok(()) |
| 192 | } |