compare_readers.rs (7160B)
1 // Copyright 2024 Adobe. All rights reserved. 2 // This file is licensed to you under the Apache License, 3 // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 // or the MIT license (http://opensource.org/licenses/MIT), 5 // at your option. 6 7 // Unless required by applicable law or agreed to in writing, 8 // this software is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 10 // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 11 // specific language governing permissions and limitations under 12 // each license. 13 14 /// Compares two manifest stores and prints out the differences. 15 use std::collections::HashMap; 16 use std::{fs, path::Path}; 17 18 use c2pa::{Error, Reader, Result}; 19 20 use super::reader_from_file; 21 22 #[allow(unused)] 23 /// Compares all the files in two directories and returns a list of issues 24 pub fn compare_folders<P: AsRef<Path>, Q: AsRef<Path>>(folder1: P, folder2: Q) -> Result<()> { 25 let folder1 = folder1.as_ref(); 26 let folder2 = folder2.as_ref(); 27 28 // handle the case we have files instead of folders 29 if folder1.is_file() && folder2.is_file() { 30 let issues = compare_files(folder1, folder2)?; 31 if !issues.is_empty() { 32 eprintln!("Failed {:?}", folder1); 33 for issue in issues { 34 eprintln!(" {}", issue); 35 } 36 } else { 37 println!("Passed {:?}", folder1); 38 } 39 return Ok(()); 40 } else if !(folder1.is_dir() && folder2.is_dir()) { 41 eprintln!("must be two folders or two files"); 42 return Err(Error::BadParam( 43 "must be two folders or two files".to_string(), 44 )); 45 } 46 47 for entry in fs::read_dir(folder1)? { 48 let entry = entry?; 49 let path = entry.path(); 50 if path.is_file() { 51 let relative_path = path.strip_prefix(folder1).unwrap(); 52 let other_path = folder2.join(relative_path); 53 //println!("Comparing {:?} to {:?}", path, other_path); 54 let mut issues = Vec::new(); 55 if other_path.exists() { 56 let result = compare_files(&path, &other_path)?; 57 issues.extend(result); 58 } else { 59 issues.push(format!( 60 "File {} does not exist in {}", 61 relative_path.display(), 62 folder2.display() 63 )); 64 } 65 if !issues.is_empty() { 66 eprintln!("Failed {:?}", relative_path); 67 for issue in issues { 68 eprintln!(" {}", issue); 69 } 70 } else { 71 println!("Passed {:?}", relative_path); 72 } 73 } 74 } 75 Ok(()) 76 } 77 78 /// Compares files with manifest stores and returns a list of issues 79 pub fn compare_files<P: AsRef<Path>, Q: AsRef<Path>>(m1: P, m2: Q) -> Result<Vec<String>> { 80 let result1 = match m1.as_ref().extension() { 81 Some(ext) if ext == "json" => Reader::from_json(&fs::read_to_string(m1)?), 82 _ => reader_from_file(m1.as_ref()), 83 }; 84 let result2 = match m2.as_ref().extension() { 85 Some(ext) if ext == "json" => Reader::from_json(&fs::read_to_string(m2)?), 86 _ => reader_from_file(m2.as_ref()), 87 }; 88 match (result1, result2) { 89 (Ok(reader1), Ok(reader2)) => compare_readers(&reader1, &reader2), 90 (Err(Error::JumbfNotFound), Err(Error::JumbfNotFound)) => Ok(Vec::new()), 91 (_, Err(err)) => Err(err), 92 (Err(err), _) => Err(err), 93 } 94 } 95 96 /// Compares two manifest stores and returns a list of issues. 97 pub fn compare_readers(reader1: &Reader, reader2: &Reader) -> Result<Vec<String>> { 98 // first we need to gather all the manifests in the order they are first seen recursively 99 let mut labels1 = Vec::new(); 100 if let Some(label) = reader1.active_label() { 101 gather_manifests(reader1, label, &mut labels1); 102 } 103 let mut labels2 = Vec::new(); 104 if let Some(label) = reader2.active_label() { 105 gather_manifests(reader2, label, &mut labels2); 106 } 107 // now we have two lists of manifests, we need to match them up 108 let manifest_map: HashMap<_, _> = labels1.into_iter().zip(labels2).collect(); 109 110 // now we can compare the manifests 111 let mut issues = Vec::new(); 112 for (label1, label2) in manifest_map.iter() { 113 // convert manifests into json values and compare them 114 let value1 = serde_json::to_value(reader1.get_manifest(label1))?; 115 let value2 = serde_json::to_value(reader2.get_manifest(label2))?; 116 compare_json_values( 117 &format!("manifests.{}", label1), 118 &value1, 119 &value2, 120 &mut issues, 121 ); 122 } 123 Ok(issues) 124 } 125 126 // creates list of manifests in the order they are first seen from the active manifest 127 fn gather_manifests(manifest_store: &Reader, manifest_label: &str, labels: &mut Vec<String>) { 128 if !labels.contains(&manifest_label.to_string()) { 129 labels.push(manifest_label.to_string()); 130 } 131 if let Some(manifest) = manifest_store.get_manifest(manifest_label) { 132 for ingredient in manifest.ingredients() { 133 if let Some(label) = ingredient.active_manifest() { 134 gather_manifests(manifest_store, label, labels); 135 } 136 } 137 } 138 } 139 140 /// Recursively compare two ManifestStore JSON values 141 fn compare_json_values( 142 path: &str, 143 val1: &serde_json::Value, 144 val2: &serde_json::Value, 145 issues: &mut Vec<String>, 146 ) { 147 match (val1, val2) { 148 (serde_json::Value::Object(map1), serde_json::Value::Object(map2)) => { 149 for (key, val1) in map1 { 150 let val2 = map2.get(key).unwrap_or(&serde_json::Value::Null); 151 compare_json_values(&format!("{}.{}", path, key), val1, val2, issues); 152 } 153 154 for (key, value) in map2 { 155 if map1.get(key).is_none() { 156 issues.push(format!("Added {}.{}: {}", path, key, value)); 157 } 158 } 159 } 160 (serde_json::Value::Array(arr1), serde_json::Value::Array(arr2)) => { 161 for (i, (val1, val2)) in arr1.iter().zip(arr2.iter()).enumerate() { 162 compare_json_values(&format!("{}[{}]", path, i), val1, val2, issues); 163 } 164 } 165 (val1, val2) if val1 != val2 => { 166 if !(path.ends_with(".instance_id") 167 || path.ends_with(".instanceId") 168 || path.ends_with(".time") 169 || path.contains(".hash") 170 || path.contains("claim_generator") // changes with every version (todo: get more specific) 171 || val1.is_string() && val2.is_string() && val1.to_string().contains("urn:uuid:")) 172 { 173 if val2.is_null() { 174 issues.push(format!("Missing {}: {}", path, val1)); 175 } else if val2.is_null() { 176 dbg!(&path); 177 issues.push(format!("Added {}: {}", path, val2)); 178 } else { 179 issues.push(format!("Changed {}: {} vs {}", path, val1, val2)); 180 } 181 } 182 } 183 _ => (), 184 } 185 }