Skip to content

Commit

Permalink
ingredient assertion unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
karlobencic committed Jan 8, 2025
1 parent 793ae1c commit 2d6822c
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 6 deletions.
6 changes: 5 additions & 1 deletion src/manifest/AssertionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
22 changes: 22 additions & 0 deletions src/manifest/assertions/Assertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,26 @@ export abstract class Assertion implements ManifestComponent {
public async validate(manifest: Manifest): Promise<ValidationResult> {
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;
}
}
2 changes: 2 additions & 0 deletions src/manifest/assertions/AssertionLabels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
49 changes: 45 additions & 4 deletions src/manifest/assertions/IngredientAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ interface RawIngredientMapV2 extends Omit<RawIngredientMapV1, 'validationStatus'
informational_URI?: string;
}

interface RawIngredientMapV3 extends Omit<RawIngredientMapV2, 'dc:title' | 'dc:format' | 'informational_URI'> {
interface RawIngredientMapV3
extends Omit<RawIngredientMapV2, 'dc:title' | 'dc:format' | 'informational_URI' | 'documentID'> {
'dc:title'?: string;
'dc:format'?: string;
activeManifest?: raw.HashedURI;
Expand Down Expand Up @@ -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
Expand All @@ -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;

Expand All @@ -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);
Expand All @@ -118,6 +154,7 @@ export class IngredientAssertion extends Assertion {
}

if (content.description) this.description = content.description;
if (content.metadata) this.metadata = content.metadata;
}

/**
Expand All @@ -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,
};
Expand All @@ -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;
Expand Down
63 changes: 62 additions & 1 deletion tests/manifest/assertions/IngredientAssertion.test.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand Down Expand Up @@ -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);
});
});

0 comments on commit 2d6822c

Please sign in to comment.