Release Privacy Tools 0.1.0
This commit is contained in:
Vendored
+253
@@ -0,0 +1,253 @@
|
||||
import { crc32 } from "@add-ideas/toolbox-helpers";
|
||||
import { zlibSync } from "fflate";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
export function jpegFixture(options: { trailing?: boolean } = {}): Uint8Array {
|
||||
const xmp = encoder.encode(
|
||||
"http://ns.adobe.com/xap/1.0/\0" +
|
||||
'<?xpacket begin=""><x:xmpmeta xmlns:x="adobe:ns:meta/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"><rdf:RDF><rdf:Description><dc:creator>Alice Example</dc:creator><exif:GPSLatitude>48.1 N</exif:GPSLatitude><xmp:CreatorTool>Fixture Camera</xmp:CreatorTool><xmpMM:DocumentID>doc-123</xmpMM:DocumentID></rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="w"?>',
|
||||
);
|
||||
const jfif = concat(
|
||||
encoder.encode("JFIF\0"),
|
||||
bytes(1, 2, 0),
|
||||
u16be(72),
|
||||
u16be(72),
|
||||
bytes(0, 0),
|
||||
);
|
||||
const iptc = concat(
|
||||
iptcDataset(80, "Alice Reporter"),
|
||||
iptcDataset(90, "Berlin"),
|
||||
iptcDataset(120, "Private caption"),
|
||||
);
|
||||
const photoshop = concat(
|
||||
encoder.encode("Photoshop 3.0\0"),
|
||||
encoder.encode("8BIM"),
|
||||
u16be(0x0404),
|
||||
bytes(0, 0),
|
||||
u32be(iptc.byteLength),
|
||||
iptc,
|
||||
iptc.byteLength % 2 ? bytes(0) : bytes(),
|
||||
);
|
||||
const body = concat(
|
||||
bytes(0xff, 0xd8),
|
||||
jpegSegment(0xe0, jfif),
|
||||
jpegSegment(0xe1, concat(encoder.encode("Exif\0\0"), tiffFixture())),
|
||||
jpegSegment(0xe1, xmp),
|
||||
jpegSegment(
|
||||
0xe2,
|
||||
concat(
|
||||
encoder.encode("ICC_PROFILE\0"),
|
||||
bytes(1, 1),
|
||||
encoder.encode("icc"),
|
||||
),
|
||||
),
|
||||
jpegSegment(0xeb, encoder.encode("jumb/c2pa test manifest")),
|
||||
jpegSegment(0xed, photoshop),
|
||||
jpegSegment(0xfe, encoder.encode("private jpeg comment")),
|
||||
jpegSegment(0xc0, bytes(8, 0, 2, 0, 3, 1, 1, 0x11, 0)),
|
||||
bytes(0xff, 0xda, 0xff, 0xd9),
|
||||
);
|
||||
return options.trailing ? concat(body, encoder.encode("hidden")) : body;
|
||||
}
|
||||
|
||||
export function pngFixture(
|
||||
options: {
|
||||
cycleTiff?: boolean;
|
||||
inflatedCommentBytes?: number;
|
||||
animated?: boolean;
|
||||
trailing?: boolean;
|
||||
} = {},
|
||||
): Uint8Array {
|
||||
const width = 2;
|
||||
const height = 1;
|
||||
const ihdr = concat(u32be(width), u32be(height), bytes(8, 6, 0, 0, 0));
|
||||
const xmp = encoder.encode(
|
||||
"XML:com.adobe.xmp\0\0\0\0\0" +
|
||||
'<rdf:Description xmlns:rdf="urn:rdf"><dc:creator xmlns:dc="urn:dc">PNG Alice</dc:creator><xmp:CreatorTool xmlns:xmp="urn:xmp">PNG Fixture</xmp:CreatorTool></rdf:Description>',
|
||||
);
|
||||
const inflated = encoder.encode(
|
||||
"x".repeat(options.inflatedCommentBytes ?? 32),
|
||||
);
|
||||
const scanline = bytes(0, 255, 0, 0, 255, 0, 0, 255, 255);
|
||||
const chunks = [
|
||||
pngChunk("IHDR", ihdr),
|
||||
pngChunk("tEXt", encoder.encode("Author\0Alice PNG")),
|
||||
pngChunk(
|
||||
"zTXt",
|
||||
concat(encoder.encode("Comment\0"), bytes(0), zlibSync(inflated)),
|
||||
),
|
||||
pngChunk("iTXt", xmp),
|
||||
pngChunk("eXIf", tiffFixture({ cycle: options.cycleTiff })),
|
||||
pngChunk(
|
||||
"iCCP",
|
||||
concat(
|
||||
encoder.encode("fixture profile\0"),
|
||||
bytes(0),
|
||||
zlibSync(encoder.encode("ICC profile bytes")),
|
||||
),
|
||||
),
|
||||
pngChunk("vpAg", encoder.encode("private ancillary data")),
|
||||
...(options.animated ? [pngChunk("acTL", concat(u32be(2), u32be(0)))] : []),
|
||||
pngChunk("IDAT", zlibSync(scanline)),
|
||||
pngChunk("IEND", bytes()),
|
||||
];
|
||||
const image = concat(
|
||||
bytes(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a),
|
||||
...chunks,
|
||||
);
|
||||
return options.trailing
|
||||
? concat(image, encoder.encode("trailing secret"))
|
||||
: image;
|
||||
}
|
||||
|
||||
export function webpFixture(options: { animated?: boolean } = {}): Uint8Array {
|
||||
const vp8x = bytes(options.animated ? 0x02 : 0, 0, 0, 0, 3, 0, 0, 2, 0, 0);
|
||||
const dimensions = 3 | (2 << 14);
|
||||
const vp8l = concat(bytes(0x2f), u32le(dimensions));
|
||||
const xmp = encoder.encode(
|
||||
'<rdf:Description xmlns:rdf="urn:rdf"><dc:creator xmlns:dc="urn:dc">WebP Alice</dc:creator><exif:GPSLongitude xmlns:exif="urn:exif">13.4 E</exif:GPSLongitude></rdf:Description>',
|
||||
);
|
||||
const chunks = concat(
|
||||
riffChunk("VP8X", vp8x),
|
||||
riffChunk("VP8L", vp8l),
|
||||
riffChunk("EXIF", concat(encoder.encode("Exif\0\0"), tiffFixture())),
|
||||
riffChunk("XMP ", xmp),
|
||||
riffChunk("ICCP", encoder.encode("ICC profile bytes")),
|
||||
...(options.animated ? [riffChunk("ANIM", bytes(0, 0, 0, 0, 0, 0))] : []),
|
||||
);
|
||||
return concat(
|
||||
encoder.encode("RIFF"),
|
||||
u32le(chunks.byteLength + 4),
|
||||
encoder.encode("WEBP"),
|
||||
chunks,
|
||||
);
|
||||
}
|
||||
|
||||
export function tiffFixture(
|
||||
options: { cycle?: boolean; orientation?: number } = {},
|
||||
): Uint8Array {
|
||||
const make = encoder.encode("CameraCo\0");
|
||||
const date = encoder.encode("2026:09:01 12:34:56\0");
|
||||
const gpsDate = encoder.encode("2026:09:01\0");
|
||||
const mainOffset = 8;
|
||||
const mainEntries = 4;
|
||||
const mainEnd = mainOffset + 2 + mainEntries * 12;
|
||||
const dataStart = mainEnd + 4;
|
||||
const makeOffset = dataStart;
|
||||
const dateOffset = makeOffset + make.byteLength;
|
||||
const gpsOffset = dateOffset + date.byteLength;
|
||||
const gpsEntries = 2;
|
||||
const gpsEnd = gpsOffset + 2 + gpsEntries * 12;
|
||||
const gpsDateOffset = gpsEnd + 4;
|
||||
const result = new Uint8Array(gpsDateOffset + gpsDate.byteLength);
|
||||
const view = new DataView(result.buffer);
|
||||
result.set(bytes(0x49, 0x49), 0);
|
||||
view.setUint16(2, 42, true);
|
||||
view.setUint32(4, mainOffset, true);
|
||||
view.setUint16(mainOffset, mainEntries, true);
|
||||
writeIfdEntry(view, mainOffset + 2, 0x010f, 2, make.byteLength, makeOffset);
|
||||
writeIfdEntry(view, mainOffset + 14, 0x0112, 3, 1, options.orientation ?? 1);
|
||||
writeIfdEntry(view, mainOffset + 26, 0x0132, 2, date.byteLength, dateOffset);
|
||||
writeIfdEntry(view, mainOffset + 38, 0x8825, 4, 1, gpsOffset);
|
||||
view.setUint32(mainEnd, options.cycle ? mainOffset : 0, true);
|
||||
result.set(make, makeOffset);
|
||||
result.set(date, dateOffset);
|
||||
view.setUint16(gpsOffset, gpsEntries, true);
|
||||
writeIfdEntry(view, gpsOffset + 2, 1, 2, 2, 0x4e);
|
||||
writeIfdEntry(view, gpsOffset + 14, 29, 2, gpsDate.byteLength, gpsDateOffset);
|
||||
view.setUint32(gpsEnd, 0, true);
|
||||
result.set(gpsDate, gpsDateOffset);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function malformedPngLengthFixture(): Uint8Array {
|
||||
const image = pngFixture();
|
||||
const malformed = image.slice();
|
||||
new DataView(malformed.buffer).setUint32(8, 0xfffffff0, false);
|
||||
return malformed;
|
||||
}
|
||||
|
||||
export function toArrayBuffer(value: Uint8Array): ArrayBuffer {
|
||||
return value.slice().buffer as ArrayBuffer;
|
||||
}
|
||||
|
||||
function jpegSegment(marker: number, payload: Uint8Array): Uint8Array {
|
||||
return concat(bytes(0xff, marker), u16be(payload.byteLength + 2), payload);
|
||||
}
|
||||
|
||||
function iptcDataset(dataset: number, value: string): Uint8Array {
|
||||
const encoded = encoder.encode(value);
|
||||
return concat(bytes(0x1c, 2, dataset), u16be(encoded.byteLength), encoded);
|
||||
}
|
||||
|
||||
function pngChunk(type: string, data: Uint8Array): Uint8Array {
|
||||
const typeBytes = encoder.encode(type);
|
||||
const checksumInput = concat(typeBytes, data);
|
||||
return concat(
|
||||
u32be(data.byteLength),
|
||||
checksumInput,
|
||||
u32be(crc32(checksumInput, 16 * 1024 * 1024)),
|
||||
);
|
||||
}
|
||||
|
||||
function riffChunk(type: string, data: Uint8Array): Uint8Array {
|
||||
return concat(
|
||||
encoder.encode(type),
|
||||
u32le(data.byteLength),
|
||||
data,
|
||||
data.byteLength % 2 ? bytes(0) : bytes(),
|
||||
);
|
||||
}
|
||||
|
||||
function writeIfdEntry(
|
||||
view: DataView,
|
||||
offset: number,
|
||||
tag: number,
|
||||
type: number,
|
||||
count: number,
|
||||
value: number,
|
||||
): void {
|
||||
view.setUint16(offset, tag, true);
|
||||
view.setUint16(offset + 2, type, true);
|
||||
view.setUint32(offset + 4, count, true);
|
||||
if (type === 3 && count === 1) view.setUint16(offset + 8, value, true);
|
||||
else view.setUint32(offset + 8, value, true);
|
||||
}
|
||||
|
||||
function concat(...parts: readonly Uint8Array[]): Uint8Array {
|
||||
const size = parts.reduce((total, part) => total + part.byteLength, 0);
|
||||
const result = new Uint8Array(size);
|
||||
let offset = 0;
|
||||
for (const part of parts) {
|
||||
result.set(part, offset);
|
||||
offset += part.byteLength;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function bytes(...values: readonly number[]): Uint8Array {
|
||||
return Uint8Array.from(values);
|
||||
}
|
||||
|
||||
function u16be(value: number): Uint8Array {
|
||||
return bytes((value >>> 8) & 0xff, value & 0xff);
|
||||
}
|
||||
|
||||
function u32be(value: number): Uint8Array {
|
||||
return bytes(
|
||||
(value >>> 24) & 0xff,
|
||||
(value >>> 16) & 0xff,
|
||||
(value >>> 8) & 0xff,
|
||||
value & 0xff,
|
||||
);
|
||||
}
|
||||
|
||||
function u32le(value: number): Uint8Array {
|
||||
return bytes(
|
||||
value & 0xff,
|
||||
(value >>> 8) & 0xff,
|
||||
(value >>> 16) & 0xff,
|
||||
(value >>> 24) & 0xff,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user