actions.rs (27720B)
1 // Copyright 2022 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 use std::collections::HashMap; 15 16 use serde::{Deserialize, Serialize}; 17 use serde_cbor::Value; 18 19 use crate::{ 20 assertion::{Assertion, AssertionBase, AssertionCbor}, 21 assertions::{labels, Actor, Metadata}, 22 error::Result, 23 resource_store::UriOrResource, 24 utils::cbor_types::DateT, 25 ClaimGeneratorInfo, 26 }; 27 28 const ASSERTION_CREATION_VERSION: usize = 2; 29 30 /// Specification defined C2PA actions 31 pub mod c2pa_action { 32 /// Changes to tone, saturation, etc. 33 pub const COLOR_ADJUSTMENTS: &str = "c2pa.color_adjustments"; 34 /// The format of the asset was changed. 35 pub const CONVERTED: &str = "c2pa.converted"; 36 /// The asset was first created, usually the asset's origin. 37 pub const CREATED: &str = "c2pa.created"; 38 /// Areas of the asset's "editorial" content were cropped out. 39 pub const CROPPED: &str = "c2pa.cropped"; 40 /// Changes using drawing tools including brushes or eraser. 41 pub const DRAWING: &str = "c2pa.drawing"; 42 /// Generalized actions that affect the "editorial" meaning of the content. 43 pub const EDITED: &str = "c2pa.edited"; 44 /// Changes to appearance with applied filters, styles, etc. 45 pub const FILTERED: &str = "c2pa.filtered"; 46 /// An existing asset was opened and is being set as the `parentOf` ingredient. 47 pub const OPENED: &str = "c2pa.opened"; 48 /// Changes to the direction and position of content. 49 pub const ORIENTATION: &str = "c2pa.orientation"; 50 /// Added/Placed a `componentOf` ingredient into the asset. 51 pub const PLACED: &str = "c2pa.placed"; 52 /// Asset is released to a wider audience. 53 pub const PUBLISHED: &str = "c2pa.published"; 54 /// A conversion of one packaging or container format to another. Content may be repackaged without transcoding. 55 /// Does not include any adjustments that would affect the "editorial" meaning of the content. 56 pub const REPACKAGED: &str = "c2pa.repackaged"; 57 /// Changes to content dimensions and/or file size 58 pub const RESIZED: &str = "c2pa.resized"; 59 /// A direct conversion of one encoding to another, including resolution scaling, bitrate adjustment and encoding format change. 60 /// Does not include any adjustments that would affect the "editorial" meaning of the content. 61 pub const TRANSCODED: &str = "c2pa.transcoded"; 62 /// Something happened, but the claim_generator cannot specify what. 63 pub const UNKNOWN: &str = "c2pa.unknown"; 64 } 65 66 /// We use this to allow SourceAgent to be either a string or a ClaimGeneratorInfo 67 #[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] 68 #[serde(untagged)] 69 pub enum SoftwareAgent { 70 String(String), 71 ClaimGeneratorInfo(ClaimGeneratorInfo), 72 } 73 74 impl From<&str> for SoftwareAgent { 75 fn from(s: &str) -> Self { 76 Self::String(s.to_owned()) 77 } 78 } 79 80 impl From<ClaimGeneratorInfo> for SoftwareAgent { 81 fn from(c: ClaimGeneratorInfo) -> Self { 82 Self::ClaimGeneratorInfo(c) 83 } 84 } 85 86 /// Defines a single action taken on an asset. 87 /// 88 /// An [`Action`] describes what took place on the asset, when it took place, 89 /// along with possible other information such as what software performed 90 /// the action. 91 /// 92 /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>. 93 #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq, Eq)] 94 pub struct Action { 95 /// The label associated with this action. See ([`c2pa_action`]). 96 action: String, 97 98 /// Timestamp of when the action occurred. 99 #[serde(skip_serializing_if = "Option::is_none")] 100 when: Option<DateT>, 101 102 /// The software agent that performed the action. 103 #[serde(rename = "softwareAgent", skip_serializing_if = "Option::is_none")] 104 software_agent: Option<SoftwareAgent>, 105 106 /// A semicolon-delimited list of the parts of the resource that were changed since the previous event history. 107 #[serde(skip_serializing_if = "Option::is_none")] 108 changed: Option<String>, 109 110 /// A list of the regions of interest of the resource that were changed. 111 /// 112 /// If not present, presumed to be undefined. 113 /// When tracking changes and the scope of the changed components is unknown, 114 /// it should be assumed that anything might have changed. 115 #[serde(skip_serializing_if = "Option::is_none")] 116 changes: Option<Vec<serde_json::Value>>, 117 118 /// The value of the `xmpMM:InstanceID` property for the modified (output) resource. 119 #[serde(rename = "instanceId", skip_serializing_if = "Option::is_none")] 120 instance_id: Option<String>, 121 122 /// Additional parameters of the action. These vary by the type of action. 123 #[serde(skip_serializing_if = "Option::is_none")] 124 parameters: Option<HashMap<String, Value>>, 125 126 /// An array of the creators that undertook this action. 127 #[serde(skip_serializing_if = "Option::is_none")] 128 actors: Option<Vec<Actor>>, 129 130 /// One of the defined URI values at `<https://cv.iptc.org/newscodes/digitalsourcetype/>` 131 #[serde(rename = "digitalSourceType", skip_serializing_if = "Option::is_none")] 132 source_type: Option<String>, 133 134 /// List of related actions. 135 #[serde(skip_serializing_if = "Option::is_none")] 136 related: Option<Vec<Action>>, 137 138 // The reason why this action was performed, required when the action is `c2pa.redacted` 139 #[serde(skip_serializing_if = "Option::is_none")] 140 reason: Option<String>, 141 } 142 143 impl Action { 144 /// Create a new action with a specific action label. 145 /// 146 /// This label is often one of the labels defined in [`c2pa_action`], 147 /// but can also be a custom string in reverse-domain format. 148 pub fn new(label: &str) -> Self { 149 Self { 150 action: label.to_owned(), 151 ..Default::default() 152 } 153 } 154 155 const fn is_v2(&self) -> bool { 156 matches!( 157 self.software_agent, 158 Some(SoftwareAgent::ClaimGeneratorInfo(_)) 159 ) || self.changes.is_some() // only defined for v2 160 } 161 162 /// Returns the label for this action. 163 /// 164 /// This label is often one of the labels defined in [`c2pa_action`], 165 /// but can also be a custom string in reverse-domain format. 166 pub fn action(&self) -> &str { 167 &self.action 168 } 169 170 /// Returns the timestamp of when the action occurred. 171 /// 172 /// This string, if present, will be in ISO-8601 date format. 173 pub fn when(&self) -> Option<&str> { 174 self.when.as_deref() 175 } 176 177 /// Returns the software agent that performed the action. 178 pub const fn software_agent(&self) -> Option<&SoftwareAgent> { 179 self.software_agent.as_ref() 180 } 181 182 /// Returns a mutable software agent that performed the action. 183 pub fn software_agent_mut(&mut self) -> Option<&mut SoftwareAgent> { 184 self.software_agent.as_mut() 185 } 186 187 /// Returns the value of the `xmpMM:InstanceID` property for the modified 188 /// (output) resource. 189 pub fn instance_id(&self) -> Option<&str> { 190 self.instance_id.as_deref() 191 } 192 193 /// Returns the additional parameters for this action. 194 /// 195 /// These vary by the type of action. 196 pub const fn parameters(&self) -> Option<&HashMap<String, Value>> { 197 self.parameters.as_ref() 198 } 199 200 /// Returns an individual action parameter if it exists. 201 pub fn get_parameter(&self, key: &str) -> Option<&Value> { 202 match self.parameters.as_ref() { 203 Some(parameters) => parameters.get(key), 204 None => None, 205 } 206 } 207 208 /// An array of the [`Actor`]s that undertook this action. 209 pub fn actors(&self) -> Option<&[Actor]> { 210 self.actors.as_deref() 211 } 212 213 /// Returns a digitalSourceType as defined at <https://cv.iptc.org/newscodes/digitalsourcetype/>. 214 pub fn source_type(&self) -> Option<&str> { 215 self.source_type.as_deref() 216 } 217 218 /// Returns the list of related actions. 219 /// 220 /// This is only present in C2PA v2. 221 /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_related>. 222 pub fn related(&self) -> Option<&[Action]> { 223 self.related.as_deref() 224 } 225 226 /// Returns the reason why this action was performed. 227 /// 228 /// This is only present in C2PA v2. 229 /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_reason>. 230 pub fn reason(&self) -> Option<&str> { 231 self.reason.as_deref() 232 } 233 234 /// Sets the timestamp for when the action occurred. 235 /// 236 /// This timestamp must be in ISO-8601 date. 237 pub fn set_when<S: Into<String>>(mut self, when: S) -> Self { 238 self.when = Some(DateT(when.into())); 239 self 240 } 241 242 /// Sets the software agent that performed the action. 243 pub fn set_software_agent<S: Into<SoftwareAgent>>(mut self, software_agent: S) -> Self { 244 self.software_agent = Some(software_agent.into()); 245 self 246 } 247 248 /// Sets the list of the parts of the resource that were changed 249 /// since the previous event history. 250 pub fn set_changed(mut self, changed: Option<&Vec<&str>>) -> Self { 251 self.changed = changed.map(|v| v.join(";")); 252 self 253 } 254 255 /// Sets the value of the `xmpMM:InstanceID` property for the 256 /// modified (output) resource. 257 pub fn set_instance_id<S: Into<String>>(mut self, id: S) -> Self { 258 self.instance_id = Some(id.into()); 259 self 260 } 261 262 /// Sets the additional parameters for this action. 263 /// 264 /// These vary by the type of action. 265 pub fn set_parameter<S: Into<String>, T: Serialize>( 266 mut self, 267 key: S, 268 value: T, 269 ) -> Result<Self> { 270 let value_bytes = serde_cbor::ser::to_vec(&value)?; 271 let value = serde_cbor::from_slice(&value_bytes)?; 272 273 self.parameters = Some(match self.parameters { 274 Some(mut parameters) => { 275 parameters.insert(key.into(), value); 276 parameters 277 } 278 None => { 279 let mut p = HashMap::new(); 280 p.insert(key.into(), value); 281 p 282 } 283 }); 284 Ok(self) 285 } 286 287 /// Sets the array of [`Actor`]s that undertook this action. 288 pub fn set_actors(mut self, actors: Option<&Vec<Actor>>) -> Self { 289 self.actors = actors.cloned(); 290 self 291 } 292 293 /// Set a digitalSourceType URI as defined at <https://cv.iptc.org/newscodes/digitalsourcetype/>. 294 pub fn set_source_type<S: Into<String>>(mut self, uri: S) -> Self { 295 self.source_type = Some(uri.into()); 296 self 297 } 298 299 /// Sets the list of related actions. 300 /// 301 /// This is only present in C2PA v2. 302 /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_related>. 303 pub fn set_related(mut self, related: Option<&Vec<Action>>) -> Self { 304 self.related = related.cloned(); 305 self 306 } 307 308 /// Sets the reason why this action was performed. 309 /// 310 /// This is only present in C2PA v2. 311 /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_reason>. 312 pub fn set_reason<S: Into<String>>(mut self, reason: S) -> Self { 313 self.reason = Some(reason.into()); 314 self 315 } 316 } 317 318 #[derive(Deserialize, Serialize, Debug, Default, PartialEq, Eq)] 319 #[non_exhaustive] 320 pub struct ActionTemplate { 321 /// The label associated with this action. See ([`c2pa_action`]). 322 pub action: String, 323 324 /// The software agent that performed the action. 325 #[serde(rename = "softwareAgent", skip_serializing_if = "Option::is_none")] 326 pub software_agent: Option<SoftwareAgent>, 327 328 /// One of the defined URI values at `<https://cv.iptc.org/newscodes/digitalsourcetype/>` 329 #[serde(rename = "digitalSourceType", skip_serializing_if = "Option::is_none")] 330 pub source_type: Option<String>, 331 332 #[serde(skip_serializing_if = "Option::is_none")] 333 pub icon: Option<UriOrResource>, 334 335 #[serde(skip_serializing_if = "Option::is_none")] 336 pub description: Option<String>, 337 338 #[serde(skip_serializing_if = "Option::is_none")] 339 pub parameters: Option<HashMap<String, Value>>, 340 } 341 342 impl ActionTemplate { 343 /// Creates a new ActionTemplate. 344 pub fn new<S: Into<String>>(action: S) -> Self { 345 Self { 346 action: action.into(), 347 ..Default::default() 348 } 349 } 350 } 351 352 /// An `Actions` assertion provides information on edits and other 353 /// actions taken that affect the asset’s content. 354 /// 355 /// This assertion contains a list of [`Action`], each one declaring 356 /// what took place on the asset, when it took place, along with possible 357 /// other information such as what software performed the action. 358 /// 359 /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>. 360 #[derive(Deserialize, Serialize, Debug, PartialEq, Eq)] 361 #[non_exhaustive] 362 pub struct Actions { 363 /// A list of [`Action`]s. 364 pub actions: Vec<Action>, 365 366 /// list of templates for the [`Action`]s 367 #[serde(skip_serializing_if = "Option::is_none")] 368 pub templates: Option<Vec<ActionTemplate>>, 369 370 /// Additional information about the assertion. 371 #[serde(skip_serializing_if = "Option::is_none")] 372 pub metadata: Option<Metadata>, 373 } 374 375 impl Actions { 376 /// Label prefix for an [`Actions`] assertion. 377 /// 378 /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>. 379 pub const LABEL: &'static str = labels::ACTIONS; 380 381 /// Creates a new [`Actions`] assertion struct. 382 pub const fn new() -> Self { 383 Self { 384 actions: Vec::new(), 385 templates: None, 386 metadata: None, 387 } 388 } 389 390 /// determines if actions is V2 391 fn is_v2(&self) -> bool { 392 if self.templates.is_some() { 393 return true; 394 }; 395 self.actions.iter().any(|a| a.is_v2()) 396 } 397 398 /// Returns the list of [`Action`]s. 399 pub fn actions(&self) -> &[Action] { 400 &self.actions 401 } 402 403 /// Returns mutable list of [`Action`]s. 404 pub fn actions_mut(&mut self) -> &mut [Action] { 405 &mut self.actions 406 } 407 408 /// Returns the assertion's [`Metadata`], if it exists. 409 pub const fn metadata(&self) -> Option<&Metadata> { 410 self.metadata.as_ref() 411 } 412 413 /// Internal method to update actions to meet spec requirements 414 pub(crate) fn update_action(mut self, index: usize, action: Action) -> Self { 415 self.actions[index] = action; 416 self 417 } 418 419 /// Adds an [`Action`] to this assertion's list of actions. 420 pub fn add_action(mut self, action: Action) -> Self { 421 self.actions.push(action); 422 self 423 } 424 425 /// Sets [`Metadata`] for the action. 426 pub fn add_metadata(mut self, metadata: Metadata) -> Self { 427 self.metadata = Some(metadata); 428 self 429 } 430 431 /// Creates a CBOR [`Actions`] assertion from a compatible JSON value. 432 pub fn from_json_value(json: &serde_json::Value) -> Result<Self> { 433 let buf: Vec<u8> = Vec::new(); 434 let json_str = json.to_string(); 435 let mut from = serde_json::Deserializer::from_str(&json_str); 436 let mut to = serde_cbor::Serializer::new(buf); 437 438 serde_transcode::transcode(&mut from, &mut to)?; 439 let buf2 = to.into_inner(); 440 441 let actions: Actions = serde_cbor::from_slice(&buf2)?; 442 Ok(actions) 443 } 444 } 445 446 impl AssertionCbor for Actions {} 447 448 impl AssertionBase for Actions { 449 const LABEL: &'static str = labels::ACTIONS; 450 const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION); 451 452 /// if we require v2 fields then use V2 453 fn version(&self) -> Option<usize> { 454 if self.is_v2() { 455 Some(2) 456 } else { 457 Some(1) 458 } 459 } 460 461 /// if we require v2 fields then use V2 462 fn label(&self) -> &str { 463 if self.is_v2() { 464 "c2pa.actions.v2" 465 } else { 466 labels::ACTIONS 467 } 468 } 469 470 fn to_assertion(&self) -> Result<Assertion> { 471 Self::to_cbor_assertion(self) 472 } 473 474 fn from_assertion(assertion: &Assertion) -> Result<Self> { 475 Self::from_cbor_assertion(assertion) 476 } 477 } 478 479 impl Default for Actions { 480 fn default() -> Self { 481 Self::new() 482 } 483 } 484 485 #[cfg(test)] 486 pub mod tests { 487 #![allow(clippy::expect_used)] 488 #![allow(clippy::panic)] 489 #![allow(clippy::unwrap_used)] 490 491 use super::*; 492 use crate::{ 493 assertion::AssertionData, 494 assertions::metadata::{c2pa_source::GENERATOR_REE, DataSource, ReviewRating}, 495 hashed_uri::HashedUri, 496 }; 497 498 fn make_hashed_uri1() -> HashedUri { 499 HashedUri::new( 500 "self#jumbf=verified_credentials/1234".to_string(), 501 None, 502 b"hashed", 503 ) 504 } 505 506 fn make_action1() -> Action { 507 Action::new(c2pa_action::CROPPED) 508 .set_software_agent("test") 509 .set_when("2015-06-26T16:43:23+0200") 510 .set_parameter( 511 "foo".to_owned(), 512 r#"{ 513 "left": 0, 514 "right": 2000, 515 "top": 1000, 516 "bottom": 4000 517 }"# 518 .to_owned(), 519 ) 520 .unwrap() 521 .set_parameter("ingredient".to_owned(), make_hashed_uri1()) 522 .unwrap() 523 .set_changed(Some(&["this", "that"].to_vec())) 524 .set_instance_id("xmp.iid:cb9f5498-bb58-4572-8043-8c369e6bfb9b") 525 .set_actors(Some( 526 &[Actor::new( 527 Some("Somebody"), 528 Some(&[make_hashed_uri1()].to_vec()), 529 )] 530 .to_vec(), 531 )) 532 } 533 534 #[test] 535 fn assertion_actions() { 536 let original = Actions::new() 537 .add_action(make_action1()) 538 .add_action( 539 Action::new("c2pa.filtered") 540 .set_parameter("name".to_owned(), "gaussian blur") 541 .unwrap() 542 .set_when("2015-06-26T16:43:23+0200") 543 .set_source_type("digsrctype:algorithmicMedia"), 544 ) 545 .add_metadata( 546 Metadata::new() 547 .add_review(ReviewRating::new("foo", Some("bar".to_owned()), 3)) 548 .set_reference(make_hashed_uri1()) 549 .set_data_source(DataSource::new(GENERATOR_REE)), 550 ); 551 552 assert_eq!(original.actions.len(), 2); 553 let assertion = original.to_assertion().expect("build_assertion"); 554 assert_eq!(assertion.mime_type(), "application/cbor"); 555 assert_eq!(assertion.label(), Actions::LABEL); 556 557 let result = Actions::from_assertion(&assertion).expect("extract_assertion"); 558 assert_eq!(result.actions.len(), 2); 559 assert_eq!(result.actions[0].action(), original.actions[0].action()); 560 assert_eq!( 561 result.actions[0].parameters().unwrap().get("name"), 562 original.actions[0].parameters().unwrap().get("name") 563 ); 564 assert_eq!(result.actions[1].action(), original.actions[1].action()); 565 assert_eq!( 566 result.actions[1].parameters.as_ref().unwrap().get("name"), 567 original.actions[1].parameters.as_ref().unwrap().get("name") 568 ); 569 assert_eq!(result.actions[1].when(), original.actions[1].when()); 570 assert_eq!( 571 result.actions[1].source_type().unwrap(), 572 "digsrctype:algorithmicMedia" 573 ); 574 assert_eq!( 575 result.metadata.unwrap().date_time(), 576 original.metadata.unwrap().date_time() 577 ); 578 } 579 580 #[test] 581 fn test_build_assertion() { 582 let assertion = Actions::new() 583 .add_action( 584 Action::new("c2pa.cropped") 585 .set_parameter( 586 "coordinate".to_owned(), 587 r#"{ 588 "left": 0, 589 "right": 2000, 590 "top": 1000, 591 "bottom": 4000 592 }"#, 593 ) 594 .unwrap(), 595 ) 596 .add_action( 597 Action::new("c2pa.filtered") 598 .set_parameter("name".to_owned(), "gaussian blur") 599 .unwrap() 600 .set_when("2015-06-26T16:43:23+0200"), 601 ) 602 .to_assertion() 603 .unwrap(); 604 605 println!("assertion label: {}", assertion.label()); 606 607 let j = assertion.data(); 608 //println!("assertion as json {:#?}", j); 609 610 let from_j = Assertion::from_data_cbor(&assertion.label(), j); 611 let ad_ref = from_j.decode_data(); 612 613 if let AssertionData::Cbor(ref ad_cbor) = ad_ref { 614 // compare results 615 let orig_d = assertion.decode_data(); 616 if let AssertionData::Cbor(ref orig_cbor) = orig_d { 617 assert_eq!(orig_cbor, ad_cbor); 618 } else { 619 panic!("Couldn't decode orig_d"); 620 } 621 } else { 622 panic!("Couldn't decode ad_ref"); 623 } 624 } 625 626 #[test] 627 fn test_binary_round_trip() { 628 let assertion = Actions::new() 629 .add_action( 630 Action::new("c2pa.cropped") 631 .set_parameter( 632 "name".to_owned(), 633 r#"{ 634 "left": 0, 635 "right": 2000, 636 "top": 1000, 637 "bottom": 4000 638 }"#, 639 ) 640 .unwrap(), 641 ) 642 .add_action( 643 Action::new("c2pa.filtered") 644 .set_parameter("name".to_owned(), "gaussian blur") 645 .unwrap() 646 .set_when("2015-06-26T16:43:23+0200"), 647 ) 648 .to_assertion() 649 .unwrap(); 650 651 let orig_bytes = assertion.data(); 652 653 let assertion_from_binary = Assertion::from_data_cbor(&assertion.label(), orig_bytes); 654 655 println!( 656 "Label Match Test {} = {}", 657 assertion.label(), 658 assertion_from_binary.label() 659 ); 660 661 assert_eq!(assertion.label(), assertion_from_binary.label()); 662 663 // compare the data as bytes 664 assert_eq!(orig_bytes, assertion_from_binary.data()); 665 println!("Decoded binary matches") 666 } 667 668 #[test] 669 fn test_json_round_trip() { 670 let json = serde_json::json!({ 671 "actions": [ 672 { 673 "action": "c2pa.edited", 674 "parameters": { 675 "description": "gradient", 676 "name": "any value" 677 }, 678 "softwareAgent": "TestApp" 679 }, 680 { 681 "action": "c2pa.opened", 682 "instanceId": "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d", 683 "parameters": { 684 "description": "import" 685 }, 686 "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia", 687 "softwareAgent": "TestApp 1.0", 688 }, 689 ], 690 "metadata": { 691 "mytag": "myvalue" 692 } 693 }); 694 let original = Actions::from_json_value(&json).expect("from json"); 695 let assertion = original.to_assertion().expect("build_assertion"); 696 let result = Actions::from_assertion(&assertion).expect("extract_assertion"); 697 assert_eq!(result.label(), labels::ACTIONS); 698 println!("{}", serde_json::to_string_pretty(&result).unwrap()); 699 assert_eq!(original.actions, result.actions); 700 assert_eq!( 701 result.actions[0].software_agent().unwrap(), 702 &SoftwareAgent::String("TestApp".to_string()) 703 ); 704 } 705 706 #[test] 707 fn test_json_v2_round_trip() { 708 let json = serde_json::json!({ 709 "actions": [ 710 { 711 "action": "c2pa.edited", 712 "parameters": { 713 "description": "gradient", 714 "name": "any value" 715 }, 716 "softwareAgent": "TestApp" 717 }, 718 { 719 "action": "c2pa.opened", 720 "instanceId": "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d", 721 "parameters": { 722 "description": "import" 723 }, 724 "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia", 725 "softwareAgent": { 726 "name": "TestApp", 727 "version": "1.0", 728 "something": "else" 729 }, 730 }, 731 { 732 "action": "com.joesphoto.filter", 733 }, 734 { 735 "action": "c2pa.dubbed", 736 "changes": [ 737 { 738 "description": "translated to klingon", 739 "region": [ 740 { 741 "type": "temporal", 742 "time": {} 743 }, 744 { 745 "type": "identified", 746 "item": { 747 "identifier": "https://bioportal.bioontology.org/ontologies/FMA", 748 "value": "lips" 749 } 750 } 751 ] 752 } 753 ] 754 } 755 756 ], 757 "templates": [ 758 { 759 "action": "com.joesphoto.filter", 760 "description": "Magic Filter", 761 "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic", 762 "softwareAgent" : { 763 "name": "Joe's Photo Editor", 764 "version": "2.0", 765 "schema.org.SoftwareApplication.operatingSystem": "Windows 10" 766 } 767 } 768 ], 769 "metadata": { 770 "mytag": "myvalue" 771 } 772 }); 773 let original = Actions::from_json_value(&json).expect("from json"); 774 let assertion = original.to_assertion().expect("build_assertion"); 775 let result = Actions::from_assertion(&assertion).expect("extract_assertion"); 776 println!("{}", serde_json::to_string_pretty(&result).unwrap()); 777 assert_eq!(result.label(), "c2pa.actions.v2"); 778 assert_eq!(original.actions, result.actions); 779 assert_eq!(original.templates, result.templates); 780 assert_eq!( 781 result.actions[0].software_agent().unwrap(), 782 &SoftwareAgent::String("TestApp".to_string()) 783 ); 784 assert_eq!( 785 result.actions[3].changes.as_deref().unwrap()[0] 786 .get("description") 787 .unwrap(), 788 "translated to klingon" 789 ); 790 } 791 }