fix(feishu): address fifth-round codex bot review feedback

- parseColorMarkup: restrict opening tag regex to known colour/style names
  (bg:*, bold, red, orange, yellow, green, blue, purple, grey/gray) so that
  ordinary bracket tokens like [Q1] can no longer consume a subsequent real
  closing tag ([/red]) and corrupt the surrounding styled spans.  Unknown tags
  now fall through to the plain-text alternatives and are emitted literally.
- resolveUploadInput: estimate decoded byte count from base64 input length
  (ceil(len * 3 / 4)) BEFORE allocating the full Buffer, preventing oversized
  payloads from spiking memory before the maxBytes limit is enforced.  Applies
  to both the data-URI branch and the plain-base64 branch.
This commit is contained in:
Elarwei
2026-02-28 16:02:18 +08:00
committed by Tak Hoffman
parent d3a035e0ad
commit b768deec09
2 changed files with 32 additions and 13 deletions

View File

@@ -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) {

View File

@@ -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" };
}