Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
improvement(sandbox): upgrade pptx/docx/pdf bootstrap with image helpers, MIME guards, and 256 MB isolate limit#4505
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
508edf69e38939f3f1a4586c6ba8918954a449ed2720c2341925d2bab7db38aFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -183,7 +183,7 @@ async function executeCode(request, executionId) { | ||
| const externalCopies = [] | ||
| try { | ||
| isolate = new ivm.Isolate({ memoryLimit: 128 }) | ||
| isolate = new ivm.Isolate({ memoryLimit: 256 }) | ||
| if (executionId !== undefined) activeIsolates.set(executionId, isolate) | ||
| context = await isolate.createContext() | ||
| const jail = context.global | ||
| @@ -398,6 +398,21 @@ async function executeCode(request, executionId) { | ||
| } | ||
| } | ||
| if ( | ||
| err.message.includes('Array buffer allocation failed') || | ||
| err.message.includes('memory limit') | ||
| ) { | ||
| return { | ||
| result: null, | ||
| stdout, | ||
| error: { | ||
| message: | ||
| 'Execution exceeded memory limit (256 MB). Reduce image sizes or split the work into smaller batches.', | ||
| name: 'MemoryLimitError', | ||
| }, | ||
| } | ||
| } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OOM check is unreachable due to isDisposed orderingHigh Severity The new Additional Locations (1)Reviewed by Cursor Bugbot for commit b7db38a. Configure here. | ||
| return { | ||
| result: null, | ||
| stdout, | ||
| @@ -511,7 +526,7 @@ async function executeTask(request, executionId) { | ||
| let tPhase = tStart | ||
| try { | ||
| isolate = new ivm.Isolate({ memoryLimit: 128 }) | ||
| isolate = new ivm.Isolate({ memoryLimit: 256 }) | ||
| if (executionId !== undefined) activeIsolates.set(executionId, isolate) | ||
| context = await isolate.createContext() | ||
| const jail = context.global | ||
| @@ -937,6 +952,23 @@ async function executeTask(request, executionId) { | ||
| timings, | ||
| } | ||
| } | ||
| if ( | ||
| err.message?.includes('Array buffer allocation failed') || | ||
| err.message?.includes('memory limit') | ||
| ) { | ||
| return { | ||
| result: null, | ||
| stdout, | ||
| error: { | ||
| message: | ||
| 'Execution exceeded memory limit (256 MB). Reduce image sizes or split the work into smaller batches.', | ||
| name: 'MemoryLimitError', | ||
| }, | ||
| timings, | ||
| } | ||
| } | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return { | ||
| result: null, | ||
| stdout, | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -15,10 +15,64 @@ export const docxGenerateTask = defineSandboxTask<SandboxTaskInput>({ | ||
| globalThis.addSection = (section) => { | ||
| globalThis.__docxSections.push(section); | ||
| }; | ||
| globalThis.getFileBase64 = async (fileId) => { | ||
| // Page geometry constants (twips, 1 twip = 1/1440 inch) for US Letter | ||
| globalThis.PAGE_W = 12240; // 8.5" | ||
| globalThis.PAGE_H = 15840; // 11" | ||
| globalThis.MARGIN = 1440; // 1" margins | ||
| globalThis.CONTENT_W = 9360; // PAGE_W - 2 * MARGIN | ||
| // 6 MB raw ≈ 8 MB base64; reject above this to avoid sandbox OOM. | ||
| const _MAX_IMG_B64 = 8 * 1024 * 1024; | ||
| /** | ||
| * getFileBase64(fileId) — load a workspace file as a full data URI string. | ||
| * Returns the complete "data:image/png;base64,..." string. | ||
| * Use addImage() rather than passing this directly to ImageRun. | ||
| */ | ||
| globalThis.getFileBase64 = async function getFileBase64(fileId) { | ||
| if (!fileId || typeof fileId !== 'string') { | ||
| throw new Error('getFileBase64: fileId must be a non-empty string'); | ||
| } | ||
| const res = await globalThis.__brokers.workspaceFile({ fileId }); | ||
| if (!res || !res.dataUri) { | ||
| throw new Error('getFileBase64: broker returned no data for file ' + fileId); | ||
| } | ||
| if (res.dataUri.length > _MAX_IMG_B64) { | ||
| throw new Error( | ||
| 'getFileBase64: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.' | ||
| ); | ||
| } | ||
| return res.dataUri; | ||
| }; | ||
| /** | ||
| * addImage(fileId, opts) — fetch a workspace file and return a docx.ImageRun. | ||
| * Required opts: width, height (pixels or EMUs via transformation option). | ||
| * Example: | ||
| * new docx.Paragraph({ children: [await addImage('abc123', { width: 200, height: 100 })] }) | ||
| */ | ||
| globalThis.addImage = async function addImage(fileId, opts) { | ||
| if (!opts || opts.width == null || opts.height == null) { | ||
| throw new Error('addImage: opts must include width and height (in pixels)'); | ||
| } | ||
| const dataUri = await globalThis.getFileBase64(fileId); | ||
| const comma = dataUri.indexOf(','); | ||
| if (comma === -1) throw new Error('addImage: invalid data URI (no comma separator)'); | ||
| const header = dataUri.slice(0, comma); | ||
| const base64 = dataUri.slice(comma + 1); | ||
| const mime = header.split(';')[0].replace('data:', ''); | ||
| const extMap = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/gif': 'gif', 'image/bmp': 'bmp', 'image/svg+xml': 'svg' }; | ||
| const ext = extMap[mime]; | ||
| if (!ext) throw new Error('addImage: unsupported image type "' + mime + '". Use PNG, JPEG, GIF, BMP, or SVG.'); | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| if (!globalThis.Buffer) throw new Error('addImage: Buffer polyfill missing — ensure docx bundle is loaded'); | ||
| const { width, height, type: _t, data: _d, transformation: userTransform, ...passThrough } = opts; | ||
| return new globalThis.docx.ImageRun(Object.assign(passThrough, { | ||
| data: globalThis.Buffer.from(base64, 'base64'), | ||
| type: ext, | ||
| transformation: Object.assign({ width, height }, userTransform || {}), | ||
| })); | ||
| }; | ||
| `, | ||
| // JSZip's browser build doesn't support nodebuffer output, so we go through | ||
| // base64 and decode back to bytes inside the isolate (avoids DataURL / Blob). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -12,20 +12,71 @@ export const pdfGenerateTask = defineSandboxTask<SandboxTaskInput>({ | ||
| if (!PDFLib) throw new Error('pdf-lib bundle not loaded'); | ||
| globalThis.PDFLib = PDFLib; | ||
| globalThis.pdf = await PDFLib.PDFDocument.create(); | ||
| globalThis.embedImage = async (dataUri) => { | ||
| // Convenience shortcuts — avoids verbose PDFLib.rgb() / PDFLib.StandardFonts.Helvetica | ||
| globalThis.rgb = PDFLib.rgb; | ||
| globalThis.StandardFonts = PDFLib.StandardFonts; | ||
| // Page-size constants in points (1pt = 1/72 inch) | ||
| globalThis.LETTER = [612, 792]; // 8.5" × 11" | ||
| globalThis.A4 = [595.28, 841.89]; // 210mm × 297mm | ||
| // 6 MB raw ≈ 8 MB base64; reject above this to avoid sandbox OOM. | ||
| const _MAX_IMG_B64 = 8 * 1024 * 1024; | ||
| /** | ||
| * embedImage(dataUri) — embed a data-URI image into the active PDF document. | ||
| * Dispatches to embedPng or embedJpg based on MIME type. | ||
| */ | ||
| globalThis.embedImage = async function embedImage(dataUri) { | ||
| if (!dataUri || typeof dataUri !== 'string') { | ||
| throw new Error('embedImage: dataUri must be a non-empty string'); | ||
| } | ||
| const comma = dataUri.indexOf(','); | ||
| if (comma === -1) throw new Error('embedImage: invalid data URI (no comma separator)'); | ||
| const header = dataUri.slice(0, comma); | ||
| const base64 = dataUri.slice(comma + 1); | ||
| const binary = globalThis.Buffer ? globalThis.Buffer.from(base64, 'base64') : null; | ||
| if (!binary) throw new Error('Buffer polyfill missing'); | ||
| if (!globalThis.Buffer) throw new Error('embedImage: Buffer polyfill missing'); | ||
| const binary = globalThis.Buffer.from(base64, 'base64'); | ||
| const mime = header.split(';')[0].split(':')[1] || ''; | ||
| if (mime.includes('png')) return globalThis.pdf.embedPng(binary); | ||
| return globalThis.pdf.embedJpg(binary); | ||
| // image/jpg is non-standard but tolerated; the canonical MIME is image/jpeg | ||
| if (mime === 'image/png') return globalThis.pdf.embedPng(binary); | ||
| if (mime === 'image/jpeg' || mime === 'image/jpg') return globalThis.pdf.embedJpg(binary); | ||
| throw new Error('embedImage: only PNG and JPEG are supported (got ' + (mime || 'unknown — check data URI header') + ')'); | ||
| }; | ||
| globalThis.getFileBase64 = async (fileId) => { | ||
| /** | ||
| * getFileBase64(fileId) — load a workspace file as a data URI string. | ||
| */ | ||
| globalThis.getFileBase64 = async function getFileBase64(fileId) { | ||
| if (!fileId || typeof fileId !== 'string') { | ||
| throw new Error('getFileBase64: fileId must be a non-empty string'); | ||
| } | ||
| const res = await globalThis.__brokers.workspaceFile({ fileId }); | ||
| if (!res || !res.dataUri) { | ||
| throw new Error('getFileBase64: broker returned no data for file ' + fileId); | ||
| } | ||
| if (res.dataUri.length > _MAX_IMG_B64) { | ||
| throw new Error( | ||
| 'getFileBase64: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.' | ||
| ); | ||
| } | ||
| return res.dataUri; | ||
| }; | ||
| /** | ||
| * drawImage(page, fileId, opts) — fetch a workspace file and draw it on the given page. | ||
| * Required opts: x, y, width, height (points). | ||
| * Example: await drawImage(page, 'abc123', { x: 50, y: 700, width: 200, height: 100 }); | ||
| */ | ||
| globalThis.drawImage = async function drawImage(page, fileId, opts) { | ||
| if (!opts || opts.x == null || opts.y == null || opts.width == null || opts.height == null) { | ||
| throw new Error('drawImage: opts must include x, y, width, and height (in points)'); | ||
| } | ||
| const dataUri = await globalThis.getFileBase64(fileId); | ||
| const img = await globalThis.embedImage(dataUri); | ||
| page.drawImage(img, opts); | ||
| }; | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| `, | ||
| finalize: ` | ||
| const pdf = globalThis.pdf; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -11,9 +11,52 @@ export const pptxGenerateTask = defineSandboxTask<SandboxTaskInput>({ | ||
| const PptxGenJS = globalThis.__bundles['pptxgenjs']; | ||
| if (!PptxGenJS) throw new Error('pptxgenjs bundle not loaded'); | ||
| globalThis.pptx = new PptxGenJS(); | ||
| globalThis.getFileBase64 = async (fileId) => { | ||
| globalThis.pptx.layout = 'LAYOUT_16x9'; | ||
| // Slide geometry for LAYOUT_16x9 (inches) | ||
| globalThis.SLIDE_W = 10; | ||
| globalThis.SLIDE_H = 5.625; | ||
| globalThis.MARGIN = 0.5; | ||
| globalThis.CONTENT_W = 9; // SLIDE_W - 2 * MARGIN | ||
| globalThis.CONTENT_H = 3.8; // usable body height below a standard title row | ||
| // ── Image helpers ────────────────────────────────────────────────────────── | ||
| // 6 MB raw ≈ 8 MB base64; reject above this to avoid sandbox OOM. | ||
| const _MAX_IMG_B64 = 8 * 1024 * 1024; | ||
| /** | ||
| * getFileBase64(fileId) — load a workspace file as a data URI string. | ||
| * PptxGenJS data format: "image/png;base64,<data>" (no "data:" prefix). | ||
| * Use as: slide.addImage({ data: await getFileBase64(fileId), x, y, w, h }) | ||
| */ | ||
| globalThis.getFileBase64 = async function getFileBase64(fileId) { | ||
| if (!fileId || typeof fileId !== 'string') { | ||
| throw new Error('getFileBase64: fileId must be a non-empty string'); | ||
| } | ||
| const res = await globalThis.__brokers.workspaceFile({ fileId }); | ||
| return res.dataUri; | ||
| if (!res || !res.dataUri) { | ||
| throw new Error('getFileBase64: broker returned no data for file ' + fileId); | ||
| } | ||
| if (res.dataUri.length > _MAX_IMG_B64) { | ||
| throw new Error( | ||
| 'getFileBase64: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.' | ||
| ); | ||
| } | ||
| // PptxGenJS expects "image/png;base64,..." — strip the leading "data:" if present | ||
| return res.dataUri.replace(/^data:/, ''); | ||
| }; | ||
| /** | ||
| * addImage(slide, fileId, opts) — fetch a workspace file and embed it. | ||
| * Required opts: x, y, w, h (inches). | ||
| * Example: await addImage(slide, 'abc123', { x: 0.5, y: 1, w: 2, h: 1 }); | ||
| */ | ||
| globalThis.addImage = async function addImage(slide, fileId, opts) { | ||
| if (!opts || opts.x == null || opts.y == null || opts.w == null || opts.h == null) { | ||
| throw new Error('addImage: opts must include x, y, w, and h (in inches)'); | ||
| } | ||
| const data = await globalThis.getFileBase64(fileId); | ||
| slide.addImage(Object.assign({}, opts, { data })); | ||
| }; | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| `, | ||
| finalize: ` | ||
Uh oh!
There was an error while loading. Please reload this page.


Uh oh!
There was an error while loading. Please reload this page.