sync: update from private repo (f6d625db)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-26 03:35:45 +00:00
parent 29ccaf1e92
commit b857c33ef6
371 changed files with 31312 additions and 8172 deletions
+245
View File
@@ -0,0 +1,245 @@
import * as fs from 'fs';
import * as path from 'path';
import { tmpdir } from 'os';
import { afterEach, describe, expect, it } from 'vitest';
import AdmZip from 'adm-zip';
import { executeTool } from './office.js';
import type { ToolContext, ToolsConfig } from './core.js';
// --- Helpers (mirrors office.test.ts house style) ---
function makeWorkspace(): string {
return fs.mkdtempSync(path.join(tmpdir(), 'maestro-office-size-'));
}
function makeContext(workspacePath: string, toolsConfig?: ToolsConfig): ToolContext {
return { workspacePath, editAllowed: true, ...(toolsConfig ? { toolsConfig } : {}) };
}
function inputPath(workspacePath: string, name: string): string {
const dir = path.join(workspacePath, 'input');
fs.mkdirSync(dir, { recursive: true });
return path.join(dir, name);
}
/**
* Write a fixture that PASSES validateFileFormat (correct magic bytes for the
* expected format) but exceeds `sizeBytes`. The size-limit check in office.ts
* runs AFTER the magic-byte format check but BEFORE any real parse, so the file
* does not need to be a valid Office document — only the leading magic bytes +
* the right extension matter, plus total size > the configured cap.
*
* OOXML magic = "PK\x03\x04" (ReadExcel/ReadDocx/ReadPPTX). PDF magic = "%PDF-".
*/
function writeOversizedOoxml(filePath: string, sizeBytes: number): void {
const buf = Buffer.alloc(sizeBytes, 0);
// Local file header signature for a ZIP / OOXML container.
buf[0] = 0x50; // P
buf[1] = 0x4b; // K
buf[2] = 0x03;
buf[3] = 0x04;
fs.writeFileSync(filePath, buf);
}
function writeOversizedPdf(filePath: string, sizeBytes: number): void {
const buf = Buffer.alloc(sizeBytes, 0x20); // pad with spaces
Buffer.from('%PDF-1.4\n').copy(buf, 0);
fs.writeFileSync(filePath, buf);
}
/**
* Build a REAL .pptx (zip) whose total UNCOMPRESSED size exceeds the configured
* uncompressed cap, while staying tiny on disk (highly compressible zeros) — i.e.
* a decompression-bomb shape. adm-zip records the true uncompressed size in each
* entry's header.size, which is exactly what office.ts sums for ZIP-bomb detection.
*/
function writeZipBomb(filePath: string, uncompressedBytes: number): void {
const zip = new AdmZip();
// A single fat, highly-compressible entry: large uncompressed, tiny on disk.
zip.addFile('ppt/slides/slide1.xml', Buffer.alloc(uncompressedBytes, 0));
zip.writeZip(filePath);
}
const MB = 1024 * 1024;
describe('office tools — file size limits', () => {
let workspacePath = '';
afterEach(() => {
if (workspacePath) {
fs.rmSync(workspacePath, { recursive: true, force: true });
workspacePath = '';
}
});
// --- 1. ReadPPTX file-size cap (officePptxMaxSizeMb) ---
it('ReadPPTX rejects a file exceeding officePptxMaxSizeMb', async () => {
workspacePath = makeWorkspace();
const fp = inputPath(workspacePath, 'big.pptx');
writeOversizedOoxml(fp, 2 * MB); // 2MB > 1MB cap
const ctx = makeContext(workspacePath, { officePptxMaxSizeMb: 1 });
const result = await executeTool('ReadPPTX', { path: 'input/big.pptx' }, ctx);
expect(result?.isError).toBe(true);
expect(result?.output).toContain('exceeds limit of 1MB');
expect(result?.output).toContain('File size');
// Distinct from the format-mismatch error family (Japanese guidance text).
expect(result?.output).not.toContain('ReadToolDoc');
});
// --- 2. ReadPPTX ZIP-bomb path (officePptxMaxUncompressedMb) ---
it('ReadPPTX rejects a ZIP bomb whose uncompressed size exceeds officePptxMaxUncompressedMb', async () => {
workspacePath = makeWorkspace();
const fp = inputPath(workspacePath, 'bomb.pptx');
// ~3MB uncompressed of zeros -> a few KB on disk (well under the 50MB file cap),
// so it passes the file-size check and reaches the uncompressed-size guard.
writeZipBomb(fp, 3 * MB);
const onDisk = fs.statSync(fp).size;
expect(onDisk).toBeLessThan(50 * MB); // sanity: stays under default file cap
const ctx = makeContext(workspacePath, { officePptxMaxUncompressedMb: 1 });
const result = await executeTool('ReadPPTX', { path: 'input/bomb.pptx' }, ctx);
expect(result?.isError).toBe(true);
expect(result?.output).toContain('ZIP bomb detected');
expect(result?.output).toContain('exceeds limit of 1MB');
});
it('ReadPPTX accepts a small zip when uncompressed size is under the cap', async () => {
// Guards against the bomb check being over-eager / regressed to always-fire.
workspacePath = makeWorkspace();
const fp = inputPath(workspacePath, 'tiny.pptx');
writeZipBomb(fp, 4 * 1024); // 4KB uncompressed, well under 1MB cap
const ctx = makeContext(workspacePath, { officePptxMaxUncompressedMb: 1 });
const result = await executeTool('ReadPPTX', { path: 'input/tiny.pptx' }, ctx);
// It will fail later for other reasons (no presentation.xml / no slides parsed),
// but it must NOT be rejected by the ZIP-bomb guard.
expect(result?.output ?? '').not.toContain('ZIP bomb detected');
});
// --- 3. ReadExcel / ReadDocx / ReadPdf each reject when size > their cap ---
it('ReadExcel rejects a file exceeding officeExcelMaxSizeMb', async () => {
workspacePath = makeWorkspace();
const fp = inputPath(workspacePath, 'big.xlsx');
writeOversizedOoxml(fp, 2 * MB);
const ctx = makeContext(workspacePath, { officeExcelMaxSizeMb: 1 });
const result = await executeTool('ReadExcel', { path: 'input/big.xlsx' }, ctx);
expect(result?.isError).toBe(true);
expect(result?.output).toContain('File size');
expect(result?.output).toContain('exceeds limit of 1MB');
// Distinct from the format-mismatch error (which names other tools / Read).
expect(result?.output).not.toContain('ReadDocx');
});
it('ReadDocx rejects a file exceeding officeDocxMaxSizeMb', async () => {
workspacePath = makeWorkspace();
const fp = inputPath(workspacePath, 'big.docx');
writeOversizedOoxml(fp, 2 * MB);
const ctx = makeContext(workspacePath, { officeDocxMaxSizeMb: 1 });
const result = await executeTool('ReadDocx', { path: 'input/big.docx' }, ctx);
expect(result?.isError).toBe(true);
expect(result?.output).toContain('File size');
expect(result?.output).toContain('exceeds limit of 1MB');
});
it('ReadPdf rejects a file exceeding officePdfMaxSizeMb', async () => {
workspacePath = makeWorkspace();
const fp = inputPath(workspacePath, 'big.pdf');
writeOversizedPdf(fp, 2 * MB);
const ctx = makeContext(workspacePath, { officePdfMaxSizeMb: 1 });
const result = await executeTool('ReadPdf', { path: 'input/big.pdf' }, ctx);
expect(result?.isError).toBe(true);
expect(result?.output).toContain('File size');
expect(result?.output).toContain('exceeds limit of 1MB');
});
// The size-limit error is structurally DISTINCT from the format-mismatch error.
it('size-limit error differs from the format-mismatch error', async () => {
workspacePath = makeWorkspace();
// (a) size-limit: valid OOXML magic + oversize -> FILE_TOO_LARGE message.
const bigFp = inputPath(workspacePath, 'big.xlsx');
writeOversizedOoxml(bigFp, 2 * MB);
const sizeRes = await executeTool(
'ReadExcel',
{ path: 'input/big.xlsx' },
makeContext(workspacePath, { officeExcelMaxSizeMb: 1 }),
);
// (b) format-mismatch: PDF bytes handed to ReadExcel -> guidance message.
const pdfFp = inputPath(workspacePath, 'wrong.xlsx');
writeOversizedPdf(pdfFp, 4 * 1024); // tiny so size cap is irrelevant
const fmtRes = await executeTool(
'ReadExcel',
{ path: 'input/wrong.xlsx' },
makeContext(workspacePath, { officeExcelMaxSizeMb: 1 }),
);
expect(sizeRes?.isError).toBe(true);
expect(fmtRes?.isError).toBe(true);
expect(sizeRes?.output).toContain('exceeds limit of');
expect(fmtRes?.output).not.toContain('exceeds limit of');
expect(sizeRes?.output).not.toBe(fmtRes?.output);
});
// --- 4. Config override changes the threshold ---
it('config override: a file that passes at default fails at a lowered officeExcelMaxSizeMb', async () => {
workspacePath = makeWorkspace();
const fp = inputPath(workspacePath, 'mid.xlsx');
// 2MB: under the 10MB default Excel cap, over a lowered 1MB cap.
writeOversizedOoxml(fp, 2 * MB);
// Default cap (no override) -> NOT rejected for size (it fails later on parse,
// but never with the FILE_TOO_LARGE message).
const defaultRes = await executeTool(
'ReadExcel',
{ path: 'input/mid.xlsx' },
makeContext(workspacePath),
);
expect(defaultRes?.output ?? '').not.toContain('exceeds limit of');
// Lowered cap -> rejected for size.
const loweredRes = await executeTool(
'ReadExcel',
{ path: 'input/mid.xlsx' },
makeContext(workspacePath, { officeExcelMaxSizeMb: 1 }),
);
expect(loweredRes?.isError).toBe(true);
expect(loweredRes?.output).toContain('exceeds limit of 1MB');
});
it('config override: raising officePptxMaxUncompressedMb lets a previously-bombed zip through the guard', async () => {
workspacePath = makeWorkspace();
const fp = inputPath(workspacePath, 'fat.pptx');
writeZipBomb(fp, 3 * MB); // 3MB uncompressed
// Low cap (1MB) -> tripped by the bomb guard.
const lowRes = await executeTool(
'ReadPPTX',
{ path: 'input/fat.pptx' },
makeContext(workspacePath, { officePptxMaxUncompressedMb: 1 }),
);
expect(lowRes?.output).toContain('ZIP bomb detected');
// Raised cap (10MB) -> NOT tripped by the bomb guard (fails later on missing slides).
const highRes = await executeTool(
'ReadPPTX',
{ path: 'input/fat.pptx' },
makeContext(workspacePath, { officePptxMaxUncompressedMb: 10 }),
);
expect(highRes?.output ?? '').not.toContain('ZIP bomb detected');
});
});