Move exif info into common "details" view

This commit is contained in:
N-Pex 2025-09-23 12:52:52 +02:00
parent 609e4a97c2
commit 0f7b9ac7ab
12 changed files with 308 additions and 230 deletions

View file

@ -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,