From 2d6822cddeb0739948676e70f925337415b042f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karlo=20Ben=C4=8Di=C4=87?= Date: Wed, 8 Jan 2025 17:00:17 +0100 Subject: [PATCH] ingredient assertion unit tests --- src/manifest/AssertionStore.ts | 6 +- src/manifest/assertions/Assertion.ts | 22 +++++++ src/manifest/assertions/AssertionLabels.ts | 2 + .../assertions/IngredientAssertion.ts | 49 +++++++++++++-- .../assertions/IngredientAssertion.test.ts | 63 ++++++++++++++++++- 5 files changed, 136 insertions(+), 6 deletions(-) diff --git a/src/manifest/AssertionStore.ts b/src/manifest/AssertionStore.ts index 53df61df..1e0e3f40 100644 --- a/src/manifest/AssertionStore.ts +++ b/src/manifest/AssertionStore.ts @@ -74,7 +74,11 @@ export class AssertionStore implements ManifestComponent { assertion = new CreativeWorkAssertion(); } else if (label.label === AssertionLabels.dataHash) { assertion = new DataHashAssertion(); - } else if (label.label === AssertionLabels.ingredient) { + } else if ( + label.label === AssertionLabels.ingredient || + label.label === AssertionLabels.ingredientV2 || + label.label === AssertionLabels.ingredientV3 + ) { assertion = new IngredientAssertion(); } else if (AssertionLabels.metadataAssertions.includes(label.label)) { assertion = new MetadataAssertion(); diff --git a/src/manifest/assertions/Assertion.ts b/src/manifest/assertions/Assertion.ts index d42f8595..f07ce06b 100644 --- a/src/manifest/assertions/Assertion.ts +++ b/src/manifest/assertions/Assertion.ts @@ -132,4 +132,26 @@ export abstract class Assertion implements ManifestComponent { public async validate(manifest: Manifest): Promise { return new ValidationResult(); } + + /** + * Extracts the version number from a label string + * @param label - The label string to extract the version from + * @returns The version number, or undefined if no version is found + */ + public static getVersion(label: string): number | undefined { + const components = label.split('.'); + const lastComponent = components[components.length - 1]; + + if (lastComponent?.length > 1) { + const [prefix, versionStr] = [lastComponent[0], lastComponent.slice(1)]; + if (prefix === 'v') { + const version = parseInt(versionStr, 10); + if (!isNaN(version)) { + return version; + } + } + } + + return undefined; + } } diff --git a/src/manifest/assertions/AssertionLabels.ts b/src/manifest/assertions/AssertionLabels.ts index d246dd8b..2c2d5b22 100644 --- a/src/manifest/assertions/AssertionLabels.ts +++ b/src/manifest/assertions/AssertionLabels.ts @@ -14,6 +14,8 @@ export class AssertionLabels { ]; public static readonly ingredient = 'c2pa.ingredient'; + public static readonly ingredientV2 = 'c2pa.ingredient.v2'; + public static readonly ingredientV3 = 'c2pa.ingredient.v3'; public static readonly actions = 'c2pa.actions'; public static readonly actionsV2 = 'c2pa.actions.v2'; diff --git a/src/manifest/assertions/IngredientAssertion.ts b/src/manifest/assertions/IngredientAssertion.ts index 47e787aa..2a4a436c 100644 --- a/src/manifest/assertions/IngredientAssertion.ts +++ b/src/manifest/assertions/IngredientAssertion.ts @@ -28,7 +28,8 @@ interface RawIngredientMapV2 extends Omit { +interface RawIngredientMapV3 + extends Omit { 'dc:title'?: string; 'dc:format'?: string; activeManifest?: raw.HashedURI; @@ -69,6 +70,38 @@ export class IngredientAssertion extends Assertion { public informationalURI?: string; public data?: HashedURI; public description?: string; + public metadata?: raw.AssertionMetadataMap; + public validationStatus?: ValidationStatusCode[]; + public c2pa_manifest?: HashedURI; + + public isV1Compatible(): boolean { + return ( + this.title !== undefined && + this.format !== undefined && + this.instanceID !== undefined && + this.data === undefined && + this.dataTypes === undefined && + this.description === undefined && + this.informationalURI === undefined && + this.validationResults === undefined && + this.activeManifest === undefined && + this.claimSignature === undefined + ); + } + + public isV2Compatible(): boolean { + return ( + this.title !== undefined && + this.format !== undefined && + this.validationResults === undefined && + this.activeManifest === undefined && + this.claimSignature === undefined + ); + } + + public isV3Compatible(): boolean { + return this.documentID === undefined && this.validationStatus === undefined && this.c2pa_manifest === undefined; + } /** * Reads the content of this assertion from a JUMBF box @@ -95,7 +128,6 @@ export class IngredientAssertion extends Assertion { this.title = content['dc:title']; this.format = content['dc:format']; - this.documentID = content.documentID; this.instanceID = content.instanceID; this.relationship = content.relationship; @@ -105,6 +137,10 @@ export class IngredientAssertion extends Assertion { this.activeManifest = claim.mapHashedURI(content.c2pa_manifest); } + if ('documentID' in content && content.documentID) { + this.documentID = content.documentID; + } + if (content.thumbnail) this.thumbnail = claim.mapHashedURI(content.thumbnail); if (content.dataTypes) this.dataTypes = content.dataTypes; if (content.claimSignature) this.claimSignature = claim.mapHashedURI(content.claimSignature); @@ -118,6 +154,7 @@ export class IngredientAssertion extends Assertion { } if (content.description) this.description = content.description; + if (content.metadata) this.metadata = content.metadata; } /** @@ -129,8 +166,7 @@ export class IngredientAssertion extends Assertion { public generateJUMBFBoxForContent(claim: Claim): JUMBF.IBox { if (!this.relationship) throw new Error('Assertion has no relationship'); - const content: RawIngredientMapV3 = { - documentID: this.documentID, + const content: RawIngredientMapV3 | RawIngredientMapV2 | RawIngredientMapV1 = { instanceID: this.instanceID!, relationship: this.relationship, }; @@ -145,6 +181,11 @@ export class IngredientAssertion extends Assertion { if (this.description) content.description = this.description; if (this.title) content['dc:title'] = this.title; if (this.format) content['dc:format'] = this.format; + if (this.metadata) content.metadata = this.metadata; + if (this.documentID) (content as RawIngredientMapV2).documentID = this.documentID; + if (this.validationStatus) (content as RawIngredientMapV1).validationStatus = this.validationStatus; + if (this.c2pa_manifest) + (content as RawIngredientMapV1).c2pa_manifest = claim.reverseMapHashedURI(this.c2pa_manifest); const box = new JUMBF.CBORBox(); box.content = content; diff --git a/tests/manifest/assertions/IngredientAssertion.test.ts b/tests/manifest/assertions/IngredientAssertion.test.ts index 6ed8594b..eef6d398 100644 --- a/tests/manifest/assertions/IngredientAssertion.test.ts +++ b/tests/manifest/assertions/IngredientAssertion.test.ts @@ -1,10 +1,19 @@ import assert from 'node:assert/strict'; import * as bin from 'typed-binary'; import { CBORBox, SuperBox } from '../../../src/jumbf'; -import { Assertion, Claim, HashedURI, IngredientAssertion } from '../../../src/manifest'; +import { Assertion, Claim, HashedURI, IngredientAssertion, RelationshipType, ReviewCode } from '../../../src/manifest'; import * as raw from '../../../src/manifest/rawTypes'; import { BinaryHelper } from '../../../src/util'; +// Helper function to create a HashedURI +function createHashedUri(uri: string): HashedURI { + return { + uri, + hash: new Uint8Array(32), // Placeholder hash + algorithm: 'SHA-256', + }; +} + describe('IngredientAssertion Tests', function () { this.timeout(0); @@ -105,4 +114,56 @@ describe('IngredientAssertion Tests', function () { }, }); }); + + it('should read and write a simple ingredient assertion (v1)', () => { + const claim = new Claim(); + claim.defaultAlgorithm = 'SHA-256'; + + const original = new IngredientAssertion(); + original.title = 'image 1.jpg'; + original.format = 'image/jpeg'; + original.instanceID = 'xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d'; + original.documentID = 'xmp.did:87d51599-286e-43b2-9478-88c79f49c347'; + original.thumbnail = createHashedUri('#c2pa.ingredient.thumbnail.jpeg'); + original.relationship = RelationshipType.ComponentOf; + + const assertion = original.generateJUMBFBox(claim); + const restored = new IngredientAssertion(); + restored.readFromJUMBF(assertion, claim); + + assert.equal(restored.title, original.title); + assert.equal(restored.format, original.format); + assert.equal(restored.documentID, original.documentID); + assert.equal(restored.instanceID, original.instanceID); + assert.deepEqual(restored.thumbnail, original.thumbnail); + }); + + it('should handle reviews in ingredient assertion', () => { + const claim = new Claim(); + claim.defaultAlgorithm = 'SHA-256'; + + const reviewRating = { + value: 1, + explanation: 'a 3rd party plugin was used', + code: ReviewCode.ActionsUnknownActionsPerformed, + }; + const metadata: raw.AssertionMetadataMap = { + dateTime: new Date().toISOString(), + reviewRatings: [reviewRating], + }; + + const original = new IngredientAssertion(); + original.title = 'image 1.jpg'; + original.format = 'image/jpeg'; + original.instanceID = 'xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d'; + original.documentID = 'xmp.did:87d51599-286e-43b2-9478-88c79f49c347'; + original.metadata = metadata; + original.relationship = RelationshipType.ComponentOf; + + const assertion = original.generateJUMBFBox(claim); + const restored = new IngredientAssertion(); + restored.readFromJUMBF(assertion, claim); + + assert.deepEqual(restored.metadata, metadata); + }); });