fix(ai): align offline bundle import entry cap with the installer

Manual bundle import capped archives at 10,000 entries while the installer
(max_entries in install_runtime.py) allows 100,000. OCR ships thousands of
small files, so the same bundle installed fine via the app but failed an
offline import with "Archive exceeds 10000 entry limit". Raise the manual
cap to match. The 20 GB cumulative byte cap and the entry-type and traversal
guards stay in place as the DoS backstops.

Fixes #719
This commit is contained in:
SnapOtter
2026-08-02 19:52:55 +08:00
parent 72bc88a9f8
commit 1033c4f101
2 changed files with 35 additions and 1 deletions
+7 -1
View File
@@ -1479,7 +1479,13 @@ interface BundleDescriptor {
}
const IMPORT_MAX_BYTES = 20 * 1024 * 1024 * 1024; // 20 GB cumulative
const IMPORT_MAX_ENTRIES = 10_000;
// Matches the installer's own cap (max_entries in
// packages/ai/python/install_runtime.py) so any bundle that installs online
// also imports offline. OCR ships thousands of small files and blew the old
// 10k cap while staying well under the installer's, so the same bundle
// installed via the app but failed a manual import (#719). The 20 GB byte cap
// and the entry-type/traversal guards remain the real DoS backstops.
const IMPORT_MAX_ENTRIES = 100_000;
export async function importBundleArchive(
stream: Readable,
@@ -260,6 +260,34 @@ describe("importBundleArchive", () => {
expect(readFileSync(modelPath)).toEqual(fakeModel);
});
it("imports a bundle whose file count exceeds the legacy 10k entry cap (#719)", async () => {
// OCR-style bundle: thousands of small files. The old 10k manual-import
// cap rejected these ("Archive exceeds 10000 entry limit") even though the
// installer accepts them, so the same bundle installed via the app but
// failed an offline import. Entry count here (~10,052 incl. bundle.json
// and the models/ dir) is above the old cap and well under the new one.
const count = 10_050;
const modelFiles = Array.from({ length: count }, (_, i) => ({
path: `f${i}.bin`,
content: Buffer.from([i & 0xff]),
}));
const archivePath = await buildArchive(
{
bundleId: testBundleId,
version: testVersion,
models: modelFiles.map((m) => m.path),
},
modelFiles,
);
const result = await importBundleArchive(createReadStream(archivePath));
expect(result.bundleId).toBe(testBundleId);
expect(result.models).toHaveLength(count);
expect(existsSync(join(modelsDir, "f0.bin"))).toBe(true);
expect(existsSync(join(modelsDir, `f${count - 1}.bin`))).toBe(true);
}, 60_000);
it("rejects archive with path traversal", async () => {
const traversalPath = await buildTraversalArchive();
const stream = createReadStream(traversalPath);