diff --git a/extensions/feishu/src/docx-color-text.ts b/extensions/feishu/src/docx-color-text.ts index 0ea6f2c6a5e..c52cd551240 100644 --- a/extensions/feishu/src/docx-color-text.ts +++ b/extensions/feishu/src/docx-color-text.ts @@ -55,13 +55,22 @@ interface Segment { */ export function parseColorMarkup(content: string): Segment[] { const segments: Segment[] = []; - // Match [tag]...[/tag], plain text, or a bare '[' that is not part of a - // complete tag pair. Without the trailing `|\[` fallback, a '[' that has no - // matching '[/...]' closer (e.g. "[Q1]" with no "[/...]") would be silently - // dropped, corrupting the surrounding text. The closing tag name is not - // validated against the opening tag: [red]text[/green] is treated as - // [red]text[/red] — opening tag style applies, closing tag is consumed. - const tagPattern = /\[([^\]]+)\](.*?)\[\/(?:[^\]]+)\]|([^[]+|\[)/gs; + // Only [known_tag]...[/...] pairs are treated as markup. Using an open + // pattern like \[([^\]]+)\] would match any bracket token — e.g. [Q1] — + // and cause it to consume a later real closing tag ([/red]), silently + // corrupting the surrounding styled spans. Restricting the opening tag to + // the set of recognised colour/style names prevents that: [Q1] does not + // match the tag alternative and each of its characters falls through to the + // plain-text alternatives instead. + // + // Closing tag name is still not validated against the opening tag: + // [red]text[/green] is treated as [red]text[/red] — opening style applies + // and the closing tag is consumed regardless of its name. + const KNOWN = "(?:bg:[a-z]+|bold|red|orange|yellow|green|blue|purple|gr[ae]y)"; + const tagPattern = new RegExp( + `\\[(${KNOWN}(?:\\s+${KNOWN})*)\\](.*?)\\[\\/(?:[^\\]]+)\\]|([^[]+|\\[)`, + "gis", + ); let match; while ((match = tagPattern.exec(content)) !== null) { diff --git a/extensions/feishu/src/docx.ts b/extensions/feishu/src/docx.ts index f3f3c9e9208..4cc16ef6feb 100644 --- a/extensions/feishu/src/docx.ts +++ b/extensions/feishu/src/docx.ts @@ -413,10 +413,15 @@ async function resolveUploadInput( const [header, data] = imageInput.split(","); const mimeMatch = header.match(/data:([^;]+)/); const ext = mimeMatch?.[1]?.split("/")[1] ?? "png"; - const buffer = Buffer.from(data, "base64"); - if (buffer.length > maxBytes) { - throw new Error(`Image data URI exceeds limit: ${buffer.length} bytes > ${maxBytes} bytes`); + // Estimate decoded byte count from base64 length BEFORE allocating the + // full buffer to avoid spiking memory on oversized payloads. + const estimatedBytes = Math.ceil((data.length * 3) / 4); + if (estimatedBytes > maxBytes) { + throw new Error( + `Image data URI exceeds limit: estimated ${estimatedBytes} bytes > ${maxBytes} bytes`, + ); } + const buffer = Buffer.from(data, "base64"); return { buffer, fileName: explicitFileName ?? `image.${ext}` }; } @@ -459,13 +464,18 @@ async function resolveUploadInput( `Use a data URI (data:image/png;base64,...) or a local file path instead.`, ); } + // Estimate decoded byte count from base64 length BEFORE allocating the + // full buffer to avoid spiking memory on oversized payloads. + const estimatedBytes = Math.ceil((trimmed.length * 3) / 4); + if (estimatedBytes > maxBytes) { + throw new Error( + `Base64 image exceeds limit: estimated ${estimatedBytes} bytes > ${maxBytes} bytes`, + ); + } const buffer = Buffer.from(trimmed, "base64"); if (buffer.length === 0) { throw new Error("Base64 image decoded to empty buffer; check the input."); } - if (buffer.length > maxBytes) { - throw new Error(`Base64 image exceeds limit: ${buffer.length} bytes > ${maxBytes} bytes`); - } return { buffer, fileName: explicitFileName ?? "image.png" }; }