fix: preserve data URL attachment filename extensions (#130625)

Carry the normalized MIME into the existing attachment filename inference. Cover PNG/CSV metadata across actions and the real action-to-plugin dispatch boundary. Explicit filenames and MIME overrides remain unchanged. Closes #130618.
This commit is contained in:
Peter Steinberger
2026-08-26 20:17:04 -07:00
committed by GitHub
parent 6e3df0d70b
commit 47fe3cc8fc
2 changed files with 30 additions and 9 deletions
@@ -29,6 +29,7 @@ const onePixelPng = Buffer.from(
const onePixelPngBase64 = onePixelPng.toString("base64");
const wrappedOnePixelPngBase64 = onePixelPngBase64.match(/.{1,24}/g)?.join("\r\n") ?? "";
const parameterizedPngDataUrl = `data:image/png;charset=utf-8;name=../../ignored.svg;base64,${wrappedOnePixelPngBase64}`;
const csvBase64 = Buffer.from("name,value\nexample,1\n").toString("base64");
function firstMockArg(
mock: { mock: { calls: readonly unknown[][] } },
@@ -106,10 +107,29 @@ describe("runMessageAction media behavior", () => {
await resetMessageActionMediaMocks();
});
it.each(["send", "sendAttachment", "reply", "upload-file", "setGroupIcon"] as const)(
"normalizes parameterized, line-wrapped image data URLs for %s",
async (action) => {
const args: Record<string, unknown> = { buffer: parameterizedPngDataUrl };
it.each(
(["send", "sendAttachment", "reply", "upload-file", "setGroupIcon"] as const).flatMap(
(action) => [
{
action,
buffer: parameterizedPngDataUrl,
base64: onePixelPngBase64,
contentType: "image/png",
filename: "attachment.png",
},
{
action,
buffer: `data:text/csv;base64,${csvBase64}`,
base64: csvBase64,
contentType: "text/csv",
filename: "attachment.csv",
},
],
),
)(
"normalizes $contentType data URLs and infers filenames for $action",
async ({ action, buffer, base64, contentType, filename }) => {
const args: Record<string, unknown> = { buffer };
await hydrateAttachmentParamsForAction({
cfg: {},
@@ -120,14 +140,13 @@ describe("runMessageAction media behavior", () => {
mediaPolicy: { mode: "host" },
});
expect(args.contentType).toBe("image/png");
expect(args.contentType).toBe(contentType);
expect(args.filename).toBe(filename);
if (action === "send") {
expect(args.media).toBe("buffer://message-send/attachment");
expect(args.filename).toBe("attachment.png");
} else {
expect(canonicalizeBase64(String(args.buffer))).toBe(onePixelPngBase64);
expect(canonicalizeBase64(String(args.buffer))).toBe(base64);
}
expect(args.filename).not.toBe("../../ignored.svg");
},
);
@@ -329,6 +348,7 @@ describe("runMessageAction media behavior", () => {
const payload = requireActionPayload(result);
expect(payload.contentType).toBe("image/png");
expect(payload.filename).toBe("attachment.png");
expect(canonicalizeBase64(String(payload.buffer))).toBe(onePixelPngBase64);
},
);
@@ -617,6 +637,7 @@ describe("runMessageAction media behavior", () => {
const handlerParams = firstMockArg(handleActionMock, "handleAction");
expect(handlerParams.contentType).toBe("image/png");
expect(handlerParams.filename).toBe("attachment.png");
expect(canonicalizeBase64(String(handlerParams.buffer))).toBe(onePixelPngBase64);
});
+1 -1
View File
@@ -538,7 +538,7 @@ async function hydrateAttachmentPayload(params: {
} else if (!filename) {
params.args.filename = inferAttachmentFilename({
mediaHint: mediaSource,
contentType: contentTypeParam ?? undefined,
contentType: normalized.contentType,
});
}
}