mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(tools): honest content-type for edit-metadata pass-through (#350)
edit-metadata writes EXIF tags in place and streams the original bytes back, but the response content-type defaulted to image/jpeg for any format outside a small map. A BMP/PSD/PPM download then claimed to be a JPEG, and the nightly tool-x-format matrix tried to Sharp-decode it and threw "unsupported image format". Map every format validateImageBuffer can report to its real MIME type, and switch the matrix's pixel-decode gate from a fragile denylist to an allowlist of formats this libvips build is guaranteed to decode. Niche raster types streamed back untouched now carry an honest content-type we simply don't pixel-verify. Verified green across all 157 tools x 34 formats with FULL_MATRIX=1.
This commit is contained in:
@@ -45,6 +45,10 @@ const settingsSchema = z.object({
|
|||||||
|
|
||||||
type Settings = z.infer<typeof settingsSchema>;
|
type Settings = z.infer<typeof settingsSchema>;
|
||||||
|
|
||||||
|
// edit-metadata writes tags in place and streams the ORIGINAL bytes back, so
|
||||||
|
// the download's content-type must match the real format, not a default. A
|
||||||
|
// previous fallback to image/jpeg made a BMP/PSD/PPM download claim to be a
|
||||||
|
// JPEG. Map every format validateImageBuffer can report.
|
||||||
const MIME_BY_FORMAT: Record<string, string> = {
|
const MIME_BY_FORMAT: Record<string, string> = {
|
||||||
jpeg: "image/jpeg",
|
jpeg: "image/jpeg",
|
||||||
png: "image/png",
|
png: "image/png",
|
||||||
@@ -53,6 +57,26 @@ const MIME_BY_FORMAT: Record<string, string> = {
|
|||||||
tiff: "image/tiff",
|
tiff: "image/tiff",
|
||||||
gif: "image/gif",
|
gif: "image/gif",
|
||||||
heif: "image/heif",
|
heif: "image/heif",
|
||||||
|
bmp: "image/bmp",
|
||||||
|
svg: "image/svg+xml",
|
||||||
|
ico: "image/x-icon",
|
||||||
|
cur: "image/x-icon",
|
||||||
|
psd: "image/vnd.adobe.photoshop",
|
||||||
|
jxl: "image/jxl",
|
||||||
|
jp2: "image/jp2",
|
||||||
|
qoi: "image/qoi",
|
||||||
|
dds: "image/vnd.ms-dds",
|
||||||
|
exr: "image/x-exr",
|
||||||
|
dpx: "image/x-dpx",
|
||||||
|
fits: "image/fits",
|
||||||
|
eps: "application/postscript",
|
||||||
|
pbm: "image/x-portable-bitmap",
|
||||||
|
pgm: "image/x-portable-graymap",
|
||||||
|
ppm: "image/x-portable-pixmap",
|
||||||
|
pfm: "image/x-portable-floatmap",
|
||||||
|
tga: "image/x-tga",
|
||||||
|
cr3: "image/x-canon-cr3",
|
||||||
|
raw: "image/x-dcraw",
|
||||||
};
|
};
|
||||||
|
|
||||||
const BROWSER_PREVIEWABLE = new Set([
|
const BROWSER_PREVIEWABLE = new Set([
|
||||||
|
|||||||
@@ -43,13 +43,19 @@ const fixtureFiles = process.env.FULL_MATRIX
|
|||||||
|
|
||||||
const ALLOWED_STATUSES = new Set([200, 202, 400, 413, 415, 422, 501]);
|
const ALLOWED_STATUSES = new Set([200, 202, 400, 413, 415, 422, 501]);
|
||||||
|
|
||||||
/** Content types whose payloads are not raster images (skip pixel decode). */
|
/**
|
||||||
const NON_RASTER_OUTPUT = new Set([
|
* Raster content types this libvips/Sharp build is guaranteed to decode. Used
|
||||||
"application/pdf",
|
* to decide whether a 200 image response should be pixel-verified. Anything
|
||||||
"application/json",
|
* else (PDF, JSON, ZIP, SVG, or a niche raster like BMP/PSD streamed back
|
||||||
"application/zip",
|
* untouched) carries an honest content-type we don't attempt to decode.
|
||||||
"image/svg+xml",
|
*/
|
||||||
"text/plain",
|
const SHARP_DECODABLE_TYPES = new Set([
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/webp",
|
||||||
|
"image/gif",
|
||||||
|
"image/tiff",
|
||||||
|
"image/avif",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
describe("tool x format matrix (generated)", () => {
|
describe("tool x format matrix (generated)", () => {
|
||||||
@@ -134,19 +140,14 @@ describe("tool x format matrix (generated)", () => {
|
|||||||
headers: { authorization: `Bearer ${adminToken}` },
|
headers: { authorization: `Bearer ${adminToken}` },
|
||||||
});
|
});
|
||||||
expect(dl.statusCode, `${toolId} x ${fixture}: download failed`).toBe(200);
|
expect(dl.statusCode, `${toolId} x ${fixture}: download failed`).toBe(200);
|
||||||
const outType = dl.headers["content-type"]?.toString() ?? "";
|
const outType = (dl.headers["content-type"]?.toString() ?? "").split(";")[0];
|
||||||
const isRaster =
|
// Allowlist of raster types this libvips build is guaranteed to
|
||||||
!NON_RASTER_OUTPUT.has(outType.split(";")[0]) && outType.startsWith("image/");
|
// decode. An allowlist (vs the old denylist) is robust to tools that
|
||||||
const sharpDecodable =
|
// legitimately stream back niche formats untouched (e.g. edit-metadata
|
||||||
isRaster &&
|
// writing tags in place on a BMP/PSD): those carry an honest
|
||||||
![
|
// content-type we simply don't pixel-verify, rather than being
|
||||||
"image/heic",
|
// misread as a corrupt JPEG.
|
||||||
"image/heif",
|
const sharpDecodable = SHARP_DECODABLE_TYPES.has(outType);
|
||||||
"image/x-icon",
|
|
||||||
"image/qoi",
|
|
||||||
"image/x-portable-pixmap",
|
|
||||||
"image/x-tga",
|
|
||||||
].includes(outType.split(";")[0]);
|
|
||||||
if (sharpDecodable) {
|
if (sharpDecodable) {
|
||||||
// The processed output must actually decode; a corrupt "success" is a bug.
|
// The processed output must actually decode; a corrupt "success" is a bug.
|
||||||
const meta = await sharp(dl.rawPayload).metadata();
|
const meta = await sharp(dl.rawPayload).metadata();
|
||||||
|
|||||||
Reference in New Issue
Block a user