/* eslint-disable no-bitwise -- byte-level container parsing */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { buildOrientationApp1, readJpegOrientation, stripJpeg, stripPng, stripWebp, stripImageMetadataBytes, } from './stripImageMetadata'; const here = path.dirname(fileURLToPath(import.meta.url)); const fixture = (name: string) => new Uint8Array(fs.readFileSync(path.join(here, '__fixtures__', name))); const has = (bytes: Uint8Array, text: string) => Buffer.from(bytes).includes(Buffer.from(text, 'latin1')); /** Walk JPEG segments up to SOS; returns marker list (sanity: structure intact). */ const jpegMarkers = (b: Uint8Array): string[] => { const out: string[] = []; let pos = 2; while (pos + 4 <= b.length && b[pos] === 0xff) { const m = b[pos + 1]; out.push(m.toString(16)); if (m === 0xda) break; pos += 2 + ((b[pos + 2] << 8) | b[pos + 3]); } return out; }; describe('stripJpeg', () => { it('removes GPS, model and timestamp, keeps a valid JPEG', () => { const src = fixture('gps.jpg'); assert.ok(has(src, 'LotusPhone 9')); const { out, stripped } = stripJpeg(src); assert.equal(stripped, true); assert.ok(!has(out, 'LotusPhone 9')); assert.ok(!has(out, '2026:09:20')); assert.ok(out.length < src.length); assert.deepEqual([out[0], out[1]], [0xff, 0xd8]); assert.deepEqual([out[out.length - 2], out[out.length - 1]], [0xff, 0xd9]); assert.ok(jpegMarkers(out).includes('da')); // Pixel data untouched: everything from SOS on is byte-identical. const sos = (b: Uint8Array) => Buffer.from(b).indexOf(Buffer.from([0xff, 0xda])); assert.deepEqual(Buffer.from(out.subarray(sos(out))), Buffer.from(src.subarray(sos(src)))); }); it('keeps a non-default orientation in a minimal EXIF block', () => { const { out, orientation } = stripJpeg(fixture('gps.jpg')); assert.equal(orientation, 6); const markers = jpegMarkers(out); assert.ok(markers.includes('e1')); // Find the APP1 we wrote and read the orientation back. let pos = 2; let found: number | undefined; while (pos + 4 <= out.length && out[pos] === 0xff) { const len = (out[pos + 2] << 8) | out[pos + 3]; if (out[pos + 1] === 0xe1) { found = readJpegOrientation(out.subarray(pos + 4, pos + 2 + len)); assert.equal(len, buildOrientationApp1(6).length - 2); } if (out[pos + 1] === 0xda) break; pos += 2 + len; } assert.equal(found, 6); assert.ok(!has(out, 'LotusPhone 9')); }); it('drops EXIF and the comment segment when orientation is 1 (no EXIF written back)', () => { const src = fixture('plain.jpg'); assert.ok(has(src, 'made with love')); const { out, stripped, orientation } = stripJpeg(src); assert.equal(stripped, true); assert.equal(orientation, undefined); assert.ok(!has(out, 'made with love')); assert.ok(!has(out, 'Cam')); assert.ok(!jpegMarkers(out).includes('e1')); }); it('returns the input untouched when there is nothing to strip', () => { const src = fixture('clean.jpg'); const { out, stripped } = stripJpeg(src); assert.equal(stripped, false); assert.equal(out, src); }); }); describe('stripPng', () => { it('drops eXIf and text chunks, keeps IHDR/IDAT/IEND', () => { const src = fixture('meta.png'); assert.ok(has(src, 'xmpmeta')); const { out, stripped } = stripPng(src); assert.equal(stripped, true); assert.ok(!has(out, 'xmpmeta')); assert.ok(!has(out, 'LotusPhone')); assert.ok(!has(out, 'eXIf')); assert.ok(has(out, 'IHDR') && has(out, 'IDAT') && has(out, 'IEND')); }); }); describe('stripWebp', () => { it('drops the EXIF chunk, clears the VP8X flag and fixes the RIFF size', () => { const src = fixture('meta.webp'); assert.ok(has(src, 'LotusPhone')); const { out, stripped } = stripWebp(src); assert.equal(stripped, true); assert.ok(!has(out, 'LotusPhone')); assert.ok(!has(out, 'EXIF')); const riff = out[4] | (out[5] << 8) | (out[6] << 16) | (out[7] << 24); assert.equal(riff, out.length - 8); const vp8x = Buffer.from(out).indexOf(Buffer.from('VP8X')); if (vp8x !== -1) assert.equal(out[vp8x + 8] & 0x0c, 0); }); }); describe('stripImageMetadataBytes', () => { it('passes unknown types through', () => { const b = new Uint8Array([1, 2, 3]); assert.deepEqual(stripImageMetadataBytes(b, 'image/gif'), { out: b, stripped: false }); }); });