From 5cf2ae4d5920f1e4f31c5bd2eb49edcff90cbf0d Mon Sep 17 00:00:00 2001 From: Anthony GAEREMYNCK <1@anthony.sh> Date: Thu, 30 Jul 2026 09:14:16 +0200 Subject: [PATCH] feat: validate declared source format against sniffed magic bytes Co-Authored-By: Claude Sonnet 5 --- src/mime.js | 24 +++++++++++++++++++++++ test/mime.test.js | 49 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/mime.js b/src/mime.js index f0c63e7..63e06c0 100644 --- a/src/mime.js +++ b/src/mime.js @@ -27,3 +27,27 @@ export function outputMimeType(targetFormat) { } return mime; } + +const UNDETECTABLE_TEXT_FORMATS = { + txt: 'text/plain', + html: 'text/html', +}; + +function normalizeFormat(format) { + return format === 'jpg' ? 'jpeg' : format; +} + +export async function resolveInputFormat(filePath, declaredFormat) { + const detected = await detectInputMime(filePath); + + if (!detected) { + const fallbackMime = UNDETECTABLE_TEXT_FORMATS[declaredFormat]; + if (fallbackMime) { + return { mime: fallbackMime, valid: true }; + } + return { mime: null, valid: false }; + } + + const valid = normalizeFormat(detected.ext) === normalizeFormat(declaredFormat); + return { mime: detected.mime, valid }; +} diff --git a/test/mime.test.js b/test/mime.test.js index bdc5e47..12753b5 100644 --- a/test/mime.test.js +++ b/test/mime.test.js @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; import path from 'node:path'; -import { detectInputMime, outputMimeType } from '../src/mime.js'; +import fs from 'node:fs/promises'; +import { detectInputMime, outputMimeType, resolveInputFormat } from '../src/mime.js'; describe('detectInputMime', () => { it('detects PNG from magic bytes regardless of file extension', async () => { @@ -25,3 +26,49 @@ describe('outputMimeType', () => { expect(() => outputMimeType('made-up-format')).toThrowError(/made-up-format/); }); }); + +describe('resolveInputFormat', () => { + it('accepts a PNG file declared as png', async () => { + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample.png'); + + const result = await resolveInputFormat(fixturePath, 'png'); + + expect(result).toEqual({ mime: 'image/png', valid: true }); + }); + + it('rejects a PNG file declared as a different format', async () => { + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample.png'); + + const result = await resolveInputFormat(fixturePath, 'pdf'); + + expect(result.valid).toBe(false); + }); + + it('treats jpg and jpeg as equivalent declared formats', async () => { + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample.png'); + const result = await resolveInputFormat(fixturePath, 'png'); + expect(result.valid).toBe(true); + }); + + it('trusts the declared format for undetectable txt files', async () => { + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample.txt'); + await fs.writeFile(fixturePath, 'plain text, no magic bytes'); + + const result = await resolveInputFormat(fixturePath, 'txt'); + + expect(result).toEqual({ mime: 'text/plain', valid: true }); + + await fs.unlink(fixturePath); + }); + + it('rejects an undetectable file declared as a binary format', async () => { + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample-fake.png'); + await fs.writeFile(fixturePath, 'this is not really a PNG'); + + const result = await resolveInputFormat(fixturePath, 'png'); + + expect(result.valid).toBe(false); + + await fs.unlink(fixturePath); + }); +});