Skip to content

Commit

Permalink
ActionAssertion: Fix field mappings for v1 and v2 (#214)
Browse files Browse the repository at this point in the history
- Remove invalid fields from v1 and v2 raw mappings
- Omit empty properties instead of setting to undefined
- Add test cases for v1 and v2 mappings
  • Loading branch information
cyraxx authored Jan 15, 2025
1 parent bbb6b32 commit 48a0242
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 49 deletions.
5 changes: 5 additions & 0 deletions .changeset/neat-trains-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@trustnxt/c2pa-ts': minor
---

Fix field mappings for actions assertion
71 changes: 36 additions & 35 deletions src/manifest/assertions/ActionAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ interface RawAction {
action: ActionType | string;
// `when` currently not implemented
softwareAgent?: string;
reason?: ActionReason | string;
changed?: string;
instanceID?: string;
parameters?: {
Expand All @@ -30,13 +29,13 @@ interface RawActionsMap {
interface RawActionV2 {
action: ActionType | string;
softwareAgent?: raw.ClaimGeneratorInfo;
softwareAgentIndex?: number;
description?: string;
digitalSourceType?: DigitalSourceType;
// `when` currently not implemented
// `changed` currently not implemented
// `changes` currently not implemented
// `related` currently not implemented
reason?: ActionReason | string;
instanceID?: string;
parameters?: {
[key: string]: unknown;
instanceID?: string;
Expand All @@ -48,6 +47,7 @@ interface RawActionV2 {
interface RawTemplateV2 {
action: ActionType | string;
softwareAgent?: raw.ClaimGeneratorInfo;
softwareAgentIndex?: number;
description?: string;
digitalSourceType?: DigitalSourceType;
icon?: raw.HashedURI; // TODO could also be extURI
Expand All @@ -57,6 +57,7 @@ interface RawTemplateV2 {
interface RawActionsMapV2 {
actions: RawActionV2[];
templates?: RawTemplateV2[];
softwareAgents?: raw.ClaimGeneratorInfo[];
metadata?: raw.AssertionMetadataMap;
}

Expand Down Expand Up @@ -89,23 +90,20 @@ export class ActionAssertion extends Assertion {
for (const rawAction of rawContent.actions) {
const action: Action = {
action: rawAction.action as ActionType,
reason: rawAction.reason,
instanceID: rawAction.instanceID,
parameters:
rawAction.parameters ?
{
...rawAction.parameters,
ingredients:
rawAction.parameters.ingredient ?
[claim.mapHashedURI(rawAction.parameters.ingredient)]
: [],
ingredient: undefined,
}
: undefined,
digitalSourceType: this.fixDigitalSourceType(rawAction.digitalSourceType),
};

if (rawAction.instanceID) action.instanceID = rawAction.instanceID;
if (rawAction.digitalSourceType)
action.digitalSourceType = this.fixDigitalSourceType(rawAction.digitalSourceType);
if (rawAction.softwareAgent) action.softwareAgent = { name: rawAction.softwareAgent };
if (rawAction.parameters) {
action.parameters = {
...rawAction.parameters,
ingredients:
rawAction.parameters.ingredient ? [claim.mapHashedURI(rawAction.parameters.ingredient)] : [],
};
delete action.parameters.ingredient;
}

this.actions.push(action);
}
Expand All @@ -119,27 +117,34 @@ export class ActionAssertion extends Assertion {
for (const rawAction of rawContent.actions) {
const action: Action = {
action: rawAction.action as ActionType,
reason: rawAction.reason,
instanceID: rawAction.instanceID,
parameters:
rawAction.parameters ?
{
...rawAction.parameters,
ingredients: rawAction.parameters.ingredients?.map(ingredient =>
claim.mapHashedURI(ingredient),
),
}
: undefined,
digitalSourceType: rawAction.digitalSourceType,
};

if (rawAction.reason) action.reason = rawAction.reason;
if (rawAction.digitalSourceType)
action.digitalSourceType = this.fixDigitalSourceType(rawAction.digitalSourceType);
if (rawAction.parameters)
action.parameters = {
...rawAction.parameters,
ingredients: rawAction.parameters.ingredients?.map(ingredient => claim.mapHashedURI(ingredient)),
};

if (rawAction.softwareAgent) {
action.softwareAgent = {
name: rawAction.softwareAgent.name,
version: rawAction.softwareAgent.version,
icon: rawAction.softwareAgent.icon ? claim.mapHashedURI(rawAction.softwareAgent.icon) : undefined,
operatingSystem: rawAction.softwareAgent.operating_system,
};
} else if (rawAction.softwareAgentIndex !== undefined) {
const softwareAgent = rawContent.softwareAgents?.[rawAction.softwareAgentIndex];
if (softwareAgent) {
action.softwareAgent = {
name: softwareAgent.name,
version: softwareAgent.version,
icon: softwareAgent.icon ? claim.mapHashedURI(softwareAgent.icon) : undefined,
operatingSystem: softwareAgent.operating_system,
};
}
}

const template = rawContent.templates?.find(t => t.action === rawAction.action);
Expand All @@ -148,8 +153,6 @@ export class ActionAssertion extends Assertion {
action.digitalSourceType = action.digitalSourceType ?? template.digitalSourceType;
}

action.digitalSourceType = this.fixDigitalSourceType(action.digitalSourceType);

this.actions.push(action);
}
}
Expand All @@ -173,7 +176,6 @@ export class ActionAssertion extends Assertion {
return {
actions: this.actions.map(action => {
const res: RawAction = { action: action.action };
if (action.reason) res.reason = action.reason;
if (action.parameters) {
res.parameters = {};
for (const [name, value] of Object.entries(action.parameters)) {
Expand Down Expand Up @@ -225,7 +227,6 @@ export class ActionAssertion extends Assertion {
}
}
if (action.digitalSourceType) res.digitalSourceType = action.digitalSourceType;
if (action.instanceID) res.instanceID = action.instanceID;
if (action.softwareAgent) {
const softwareAgent: raw.ClaimGeneratorInfo = {
name: action.softwareAgent.name,
Expand All @@ -247,8 +248,8 @@ export class ActionAssertion extends Assertion {
/**
* Some implementations prefix digital source type values with https, convert those to http
*/
private fixDigitalSourceType(digitalSourceType: DigitalSourceType | undefined): DigitalSourceType | undefined {
if (digitalSourceType?.startsWith('https://cv.iptc.org/')) {
private fixDigitalSourceType(digitalSourceType: DigitalSourceType): DigitalSourceType {
if (digitalSourceType.startsWith('https://cv.iptc.org/')) {
return ('http:' + digitalSourceType.substring('https:'.length)) as DigitalSourceType;
}
return digitalSourceType;
Expand Down
92 changes: 78 additions & 14 deletions tests/manifest/assertions/ActionAssertion.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import assert from 'node:assert/strict';
import * as bin from 'typed-binary';
import { CBORBox, SuperBox } from '../../../src/jumbf';
import { ActionAssertion, Assertion, Claim } from '../../../src/manifest';
import {
ActionAssertion,
ActionType,
Assertion,
AssertionLabels,
Claim,
DigitalSourceType,
} from '../../../src/manifest';
import * as raw from '../../../src/manifest/rawTypes';
import { BinaryHelper } from '../../../src/util';

Expand All @@ -10,12 +17,12 @@ describe('ActionAssertion Tests', function () {

const claim = new Claim();

const serializedString =
const serializedStringV1 =
'000000846a756d62000000266a756d6463626f7200110010800000aa00389b7103633270612e616374696f6e73000000005663626f72a167616374696f6e7382a166616374696f6e6c633270612e63726561746564a266616374696f6e6c633270612e64726177696e676a706172616d6574657273a1646e616d65686772616469656e74';

let superBox: SuperBox;
it('read a JUMBF box', function () {
const buffer = BinaryHelper.fromHexString(serializedString);
it('read a v1 JUMBF box', function () {
const buffer = BinaryHelper.fromHexString(serializedStringV1);

// fetch schema from the box class
const schema = SuperBox.schema;
Expand Down Expand Up @@ -49,7 +56,7 @@ describe('ActionAssertion Tests', function () {
});

let assertion: Assertion;
it('construct an assertion from the JUMBF box', function () {
it('construct an assertion from the v1 JUMBF box', function () {
if (!superBox) this.skip();

const actionAssertion = new ActionAssertion();
Expand All @@ -62,27 +69,19 @@ describe('ActionAssertion Tests', function () {
assert.equal(actionAssertion.actions.length, 2);
assert.deepEqual(actionAssertion.actions[0], {
action: 'c2pa.created',
reason: undefined,
instanceID: undefined,
parameters: undefined,
digitalSourceType: undefined,
});
assert.deepEqual(actionAssertion.actions[1], {
action: 'c2pa.drawing',
reason: undefined,
instanceID: undefined,
parameters: {
name: 'gradient',
ingredients: [],
ingredient: undefined,
},
digitalSourceType: undefined,
});

assertion = actionAssertion;
});

it('construct a JUMBF box from the assertion', function () {
it('construct a JUMBF box from the v1 assertion', function () {
if (!assertion) this.skip();

const box = assertion.generateJUMBFBox(claim);
Expand Down Expand Up @@ -111,4 +110,69 @@ describe('ActionAssertion Tests', function () {
],
});
});

const constructedAssertion = new ActionAssertion();
constructedAssertion.actions.push({
action: ActionType.C2paOpened,
digitalSourceType: DigitalSourceType.DigitalArt,
reason: 'Opened the media',
instanceID: 'Dummy-Instance-ID',
});

it('create and read back a v2 assertion', function () {
const box = constructedAssertion.generateJUMBFBox();

assert.equal(box.descriptionBox?.label, 'c2pa.actions.v2');
assert.deepEqual(box.descriptionBox?.uuid, raw.UUIDs.cborAssertion);
assert.equal(box.contentBoxes.length, 1);
assert.ok(box.contentBoxes[0] instanceof CBORBox);
assert.deepEqual(box.contentBoxes[0].content, {
actions: [
{
action: 'c2pa.opened',
digitalSourceType: 'http://cv.iptc.org/newscodes/digitalsourcetype/digitalArt',
reason: 'Opened the media',
},
],
});

const readBackAssertion = new ActionAssertion();
readBackAssertion.readFromJUMBF(box, claim);

assert.equal(readBackAssertion.label, 'c2pa.actions.v2');
assert.deepEqual(readBackAssertion.actions[0], {
action: ActionType.C2paOpened,
digitalSourceType: DigitalSourceType.DigitalArt,
reason: 'Opened the media',
});
});

it('create and read back a v1 assertion', function () {
constructedAssertion.label = AssertionLabels.actions;
const box = constructedAssertion.generateJUMBFBox();

assert.equal(box.descriptionBox?.label, 'c2pa.actions');
assert.deepEqual(box.descriptionBox?.uuid, raw.UUIDs.cborAssertion);
assert.equal(box.contentBoxes.length, 1);
assert.ok(box.contentBoxes[0] instanceof CBORBox);
assert.deepEqual(box.contentBoxes[0].content, {
actions: [
{
action: 'c2pa.opened',
digitalSourceType: 'http://cv.iptc.org/newscodes/digitalsourcetype/digitalArt',
instanceID: 'Dummy-Instance-ID',
},
],
});

const readBackAssertion = new ActionAssertion();
readBackAssertion.readFromJUMBF(box, claim);

assert.equal(readBackAssertion.label, 'c2pa.actions');
assert.deepEqual(readBackAssertion.actions[0], {
action: ActionType.C2paOpened,
digitalSourceType: DigitalSourceType.DigitalArt,
instanceID: 'Dummy-Instance-ID',
});
});
});

0 comments on commit 48a0242

Please sign in to comment.