c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

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 }