Move exif info into common "details" view
This commit is contained in:
parent
609e4a97c2
commit
0f7b9ac7ab
12 changed files with 308 additions and 230 deletions
|
|
@ -61,10 +61,10 @@ export type Proof = {
|
|||
|
||||
export type ProofHintFlagsGenerator = "unknown" | "camera" | "screenshot" | "ai";
|
||||
export type ProofHintFlagsGeneratorSource = "c2pa" | "exif" | "metadata";
|
||||
export type ProofHintFlagsEditor = "unknown" | "manual" | "ai";
|
||||
//export type ProofHintFlagsEditor = "unknown" | "manual" | "ai";
|
||||
|
||||
export type ProofHintFlagsEdit = {
|
||||
editor: ProofHintFlagsEditor;
|
||||
editor: string;
|
||||
date?: Date;
|
||||
}
|
||||
|
||||
|
|
@ -82,9 +82,14 @@ type FlagMatchRule = {
|
|||
description: string;
|
||||
};
|
||||
|
||||
type FlagMatchRuleValue = {
|
||||
type FlagMatchRulePathSegment = {
|
||||
object: any;
|
||||
path: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
type FlagMatchRuleValue = {
|
||||
path: FlagMatchRulePathSegment[];
|
||||
value: string | object;
|
||||
};
|
||||
|
||||
type FlagMatchInfo = {
|
||||
|
|
@ -161,18 +166,18 @@ const ruleAiMeta = (): FlagMatchRule[] => {
|
|||
];
|
||||
};
|
||||
|
||||
const matchFlag = (rules: FlagMatchRule[], file: any) => {
|
||||
const matchFlag = (rules: FlagMatchRule[], path: FlagMatchRulePathSegment[]) => {
|
||||
let result = false;
|
||||
let resultInfo: FlagMatchInfo[] = [];
|
||||
for (let rule of rules) {
|
||||
try {
|
||||
const re: RegExp[] = (!Array.isArray(rule.match) ? [rule.match] : rule.match).map((m) => new RegExp(m, "gi"));
|
||||
const values = extractFlagValues(rule.field, file);
|
||||
const values = extractFlagValues(rule.field, path);
|
||||
values.forEach((v) => {
|
||||
re.forEach((r) => {
|
||||
if (r.test(v.value)) {
|
||||
if (typeof v.value === "string" && r.test(v.value)) {
|
||||
result = true;
|
||||
resultInfo.push({ field: v.path, value: v.value, re: r.source });
|
||||
resultInfo.push({ field: v.path.map((v) => v.path).join("/"), value: v.value, re: r.source });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -183,41 +188,53 @@ const matchFlag = (rules: FlagMatchRule[], file: any) => {
|
|||
return { result: result, matches: resultInfo };
|
||||
};
|
||||
|
||||
const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] => {
|
||||
const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] => {
|
||||
const getValues = (
|
||||
path: string[],
|
||||
objectPath: any[],
|
||||
actualPath: string,
|
||||
o: any
|
||||
keys: string[],
|
||||
path: FlagMatchRulePathSegment[]
|
||||
): FlagMatchRuleValue[] | undefined => {
|
||||
if (path.length == 0 || o == undefined) return undefined;
|
||||
let part = path[0];
|
||||
const lastBracket = part.lastIndexOf("[");
|
||||
if (part === "..") {
|
||||
return getValues(path.slice(1), objectPath.slice(1), actualPath + "/..", objectPath[0]);
|
||||
if (keys.length == 0 || path.length == 0) return undefined;
|
||||
|
||||
const o = path[0].object;
|
||||
let key = keys[0];
|
||||
|
||||
const lastBracket = key.lastIndexOf("[");
|
||||
if (key === "..") {
|
||||
return getValues(keys.slice(1), path.slice(1));
|
||||
}
|
||||
if (part.endsWith("]") && lastBracket > 0) {
|
||||
const optionalConstraint = part.substring(lastBracket + 1, part.length - 1);
|
||||
part = part.substring(0, lastBracket);
|
||||
if (o[part] != undefined) {
|
||||
let opart: any[] = o[part];
|
||||
if (key.endsWith("]") && lastBracket > 0) {
|
||||
const optionalConstraint = key.substring(lastBracket + 1, key.length - 1);
|
||||
key = key.substring(0, lastBracket);
|
||||
if (o[key] != undefined) {
|
||||
let opart: any[] = o[key];
|
||||
if (!Array.isArray(opart)) {
|
||||
opart = Object.values(opart) ?? [];
|
||||
}
|
||||
|
||||
// Any constraints controlling what array object(s) to consider?
|
||||
if (optionalConstraint) {
|
||||
const [prop, val] = optionalConstraint.split("=");
|
||||
opart = opart.filter((item) => item[prop] === val);
|
||||
if (optionalConstraint.startsWith("!!")) {
|
||||
// Ignore this path in the tree if ANY of the values contain the constraint match.
|
||||
const [prop, val] = optionalConstraint.substring(2).split("=");
|
||||
if (opart.some((item) => item[prop] === val)) {
|
||||
return undefined;
|
||||
}
|
||||
} else if (optionalConstraint.startsWith("!")) {
|
||||
const [prop, val] = optionalConstraint.substring(1).split("=");
|
||||
opart = opart.filter((item) => item[prop] !== val);
|
||||
} else {
|
||||
const [prop, val] = optionalConstraint.split("=");
|
||||
opart = opart.filter((item) => item[prop] === val);
|
||||
}
|
||||
}
|
||||
|
||||
if (path.length == 1) {
|
||||
let strings = opart as string[];
|
||||
return strings.map((s, i) => ({ path: actualPath + "/" + part + "[" + i + "]", value: s }));
|
||||
if (keys.length == 1) {
|
||||
return opart.map((s, i) => {
|
||||
return { value: s, path: [{ object: o, path: key + "[" + i + "]"}, ... path] };
|
||||
});
|
||||
} else {
|
||||
return opart.reduce((res: FlagMatchRuleValue[] | undefined, o: any, i: number) => {
|
||||
const newObjectPaths = [o, ...objectPath];
|
||||
let matches = getValues(path.slice(1), newObjectPaths, actualPath + "/" + part + "[" + i + "]", o);
|
||||
return opart.reduce((res: FlagMatchRuleValue[] | undefined, oin: any, i: number) => {
|
||||
let matches = getValues(keys.slice(1), [ { object: oin, path: key + "[" + i + "]"}, ... path]);
|
||||
if (matches) {
|
||||
const r2 = res || [];
|
||||
r2.push(...matches);
|
||||
|
|
@ -230,12 +247,11 @@ const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] =>
|
|||
return undefined;
|
||||
}
|
||||
} else {
|
||||
if (o[part] != undefined) {
|
||||
if (path.length == 1) {
|
||||
return [{ path: actualPath + "/" + part, value: o[part] }];
|
||||
if (o[key] != undefined) {
|
||||
if (keys.length == 1) {
|
||||
return [{ value: o[key], path: [{object: o[key], path: key}, ...path] }];
|
||||
} else {
|
||||
const newObjectPaths = [o, ...objectPath];
|
||||
return getValues(path.slice(1), newObjectPaths, actualPath + "/" + part, o[part]);
|
||||
return getValues(keys.slice(1), [{object: o[key], path: key}, ...path]);
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
|
|
@ -245,14 +261,43 @@ const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] =>
|
|||
|
||||
let result: FlagMatchRuleValue[] = [];
|
||||
try {
|
||||
let parts = flagPath.split("/");
|
||||
result = getValues(parts, [], "", file) ?? [];
|
||||
let keys = flagPath.split("/");
|
||||
result = getValues(keys, path) ?? [];
|
||||
} catch (e) {
|
||||
console.error("Invalid RE", e);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const getFirstWithData = (flagPaths: string[], path: FlagMatchRulePathSegment[]): string | undefined => {
|
||||
for (let idx = 0; idx < flagPaths.length; idx++) {
|
||||
const result = extractFlagValues(flagPaths[idx], path);
|
||||
if (result.length > 0) {
|
||||
return result[0].value as string;
|
||||
}
|
||||
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getFirstWithDataAsDate = (flagPaths: string[], path: FlagMatchRulePathSegment[]): Date | undefined => {
|
||||
const val = getFirstWithData(flagPaths, path);
|
||||
if (val) {
|
||||
try {
|
||||
const date = new Date(Date.parse(val));
|
||||
return date;
|
||||
} catch (error) {}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const softwareAgentFromAction = (action: FlagMatchRuleValue): string | undefined => {
|
||||
const agent = getFirstWithData([
|
||||
"softwareAgent", "../../../claim_generator"
|
||||
], action.path);
|
||||
return agent;
|
||||
};
|
||||
|
||||
export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => {
|
||||
if (!proof) return undefined;
|
||||
|
||||
|
|
@ -265,46 +310,59 @@ export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined
|
|||
valid = results.failure.length == 0 && results.success.length > 0;
|
||||
}
|
||||
|
||||
const source = extractFlagValues(
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/softwareAgent",
|
||||
proof
|
||||
);
|
||||
const rootMatchPath = [{object: proof, path: ""}];
|
||||
|
||||
const dateCreated = extractFlagValues(
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/../../../../signature_info/time",
|
||||
proof
|
||||
let source: string | undefined = undefined;
|
||||
let dateCreated: Date | undefined = undefined;
|
||||
|
||||
const creationAction = extractFlagValues(
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]",
|
||||
rootMatchPath
|
||||
);
|
||||
let date: Date | undefined = undefined;
|
||||
if (dateCreated && dateCreated.length == 1) {
|
||||
try {
|
||||
date = new Date(Date.parse(dateCreated[0].value));
|
||||
} catch (error) {}
|
||||
if (creationAction.length > 0) {
|
||||
source = softwareAgentFromAction(creationAction[0]);
|
||||
dateCreated = getFirstWithDataAsDate(["when", "../../metadata/dateTime", "../../../signature_info/time"], creationAction[0].path);
|
||||
}
|
||||
console.log("DATE CREATED", date);
|
||||
console.log("DATE CREATED", dateCreated);
|
||||
|
||||
let generator: ProofHintFlagsGenerator = matchFlag(ruleAiGenerated(), proof).result ? "ai" : matchFlag(ruleScreenshotC2PA(), proof).result ? "screenshot" : matchFlag(ruleCamera(), proof).result ? "camera" : "unknown";
|
||||
let generator: ProofHintFlagsGenerator = matchFlag(ruleAiGenerated(), rootMatchPath).result ? "ai" : matchFlag(ruleScreenshotC2PA(), rootMatchPath).result ? "screenshot" : matchFlag(ruleCamera(), rootMatchPath).result ? "camera" : "unknown";
|
||||
let generatorSource: ProofHintFlagsGeneratorSource | undefined = undefined;
|
||||
|
||||
if (generator !== "unknown" && valid) {
|
||||
generatorSource = "c2pa";
|
||||
} else {
|
||||
if (matchFlag(ruleScreenshotMeta(), proof).result) {
|
||||
if (matchFlag(ruleScreenshotMeta(), rootMatchPath).result) {
|
||||
generator = "screenshot";
|
||||
generatorSource = "metadata";
|
||||
} else if (matchFlag(ruleAiMeta(), proof).result) {
|
||||
} else if (matchFlag(ruleAiMeta(), rootMatchPath).result) {
|
||||
generator = "ai";
|
||||
generatorSource = "metadata";
|
||||
}
|
||||
}
|
||||
|
||||
console.error("PROOF", proof);
|
||||
|
||||
const c2paEdits = extractFlagValues(
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[!!action=c2pa.created]",
|
||||
rootMatchPath
|
||||
);
|
||||
if (c2paEdits.length > 0) {
|
||||
edits = c2paEdits.map((edit) => {
|
||||
return {
|
||||
editor: softwareAgentFromAction(edit) ?? "",
|
||||
date: getFirstWithDataAsDate(["when", "../../metadata/dateTime", "../../../signature_info/time"], edit.path)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Do we have any data? Else, return "undefined", we don't just want to send an object with all defaults.
|
||||
if (source.length === 0 && dateCreated.length === 0 && generator === "unknown") {
|
||||
if (source === undefined && dateCreated === undefined && generator === "unknown") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const flags: ProofHintFlags = {
|
||||
device: source && source.length == 1 ? source[0].value : undefined,
|
||||
creationDate: date,
|
||||
device: source,
|
||||
creationDate: dateCreated,
|
||||
generator: generator,
|
||||
generatorSource: generatorSource,
|
||||
edits: edits,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue