fix(sql): prevent oversized SQL editor loads

This commit is contained in:
zipg authored and GitHub committed 2026-07-28 23:59:34 +08:00
1 parent fc1232fd7f
commit f5c25510dc
16 files changed
+280 -34

No files matched your search

+38 -1
View File
@@ -1,5 +1,7 @@
// @vitest-environment happy-dom
import { describe, expect, it } from "vitest";
import { externalSqlFileDisplayTitles, normalizeExternalSqlPath } from "@/lib/sql/sqlFileOpen";
import { ExternalSqlFileTooLargeError, externalSqlFileDisplayTitles, externalSqlFileOpenErrorMessage, formatSqlFileSize, MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES, normalizeExternalSqlPath, readBrowserSqlFile } from "@/lib/sql/sqlFileOpen";
describe("external SQL file paths", () => {
it("normalizes Windows separators for identity checks", () => {
@@ -14,3 +16,38 @@ describe("external SQL file paths", () => {
expect(externalSqlFileDisplayTitles(["/one/sql/create.sql", "/two/sql/create.sql"])).toEqual(["one/sql/create.sql", "two/sql/create.sql"]);
});
});
describe("external SQL file editor limit", () => {
it("accepts the exact browser editor limit", async () => {
const file = new Blob(["select 1;"]);
Object.defineProperty(file, "size", { value: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES });
await expect(readBrowserSqlFile(file)).resolves.toBe("select 1;");
});
it("rejects browser files above the editor limit before reading", async () => {
const file = new Blob(["select 1;"]);
Object.defineProperty(file, "size", { value: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1 });
await expect(readBrowserSqlFile(file)).rejects.toMatchObject({
name: "ExternalSqlFileTooLargeError",
sizeBytes: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1,
maxSizeBytes: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
});
});
it("formats large file sizes", () => {
expect(formatSqlFileSize(64 * 1024 * 1024)).toBe("64.0 MB");
expect(formatSqlFileSize(50 * 1024 * 1024 * 1024)).toBe("50.0 GB");
});
it("builds a localized actionable message for oversized files", () => {
const message = externalSqlFileOpenErrorMessage(new ExternalSqlFileTooLargeError(50 * 1024 ** 3, 64 * 1024 ** 2), (_key, params) => `${params.size}/${params.limit}`);
expect(message).toBe("50.0 GB/64.0 MB");
});
it("preserves ordinary backend error messages", () => {
expect(externalSqlFileOpenErrorMessage(new Error("permission denied"), () => "unused")).toBe("permission denied");
});
});
@@ -0,0 +1,34 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ExternalSqlFileTooLargeError } from "@/lib/sql/sqlFileOpen";
const mocks = vi.hoisted(() => ({
invoke: vi.fn(),
}));
vi.mock("@tauri-apps/api/core", () => ({
invoke: mocks.invoke,
}));
import { readExternalSqlFile } from "@/lib/backend/tauri";
describe("external SQL file API", () => {
beforeEach(() => {
mocks.invoke.mockReset();
});
it("returns editor content from the structured backend response", async () => {
mocks.invoke.mockResolvedValue({ kind: "content", content: "select 1;" });
await expect(readExternalSqlFile("/tmp/demo.sql")).resolves.toBe("select 1;");
expect(mocks.invoke).toHaveBeenCalledWith("read_external_sql_file", { path: "/tmp/demo.sql" });
});
it("maps oversized responses to a typed frontend error", async () => {
mocks.invoke.mockResolvedValue({ kind: "tooLarge", sizeBytes: 50 * 1024 ** 3, maxSizeBytes: 64 * 1024 ** 2 });
const error = await readExternalSqlFile("/tmp/backup.sql").catch((reason) => reason);
expect(error).toBeInstanceOf(ExternalSqlFileTooLargeError);
expect(error).toMatchObject({ sizeBytes: 50 * 1024 ** 3, maxSizeBytes: 64 * 1024 ** 2 });
});
});
+6 -1
View File
@@ -1,6 +1,7 @@
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { normalizeRustMongoCommand, type MongoCommand } from "@/lib/mongo/mongoShellCommand";
import { ExternalSqlFileTooLargeError } from "@/lib/sql/sqlFileOpen";
import type {
ConnectionConfig,
ConnectionTestResult,
@@ -670,7 +671,11 @@ export async function pendingOpenConnectionLinks(): Promise<string[]> {
}
export async function readExternalSqlFile(path: string): Promise<string> {
return invoke("read_external_sql_file", { path });
const result = await invoke<{ kind: "content"; content: string } | { kind: "tooLarge"; sizeBytes: number; maxSizeBytes: number }>("read_external_sql_file", { path });
if (result.kind === "tooLarge") {
throw new ExternalSqlFileTooLargeError(result.sizeBytes, result.maxSizeBytes);
}
return result.content;
}
export async function writeExternalSqlFile(path: string, content: string): Promise<void> {
+57
View File
@@ -46,3 +46,60 @@ export function externalSqlFileDisplayTitles(paths: string[]): string[] {
export function externalSqlFilePaths(paths: string[]): string[] {
return paths.filter(isSqlFilePath);
}
export const MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES = 64 * 1024 * 1024;
export class ExternalSqlFileTooLargeError extends Error {
constructor(
readonly sizeBytes: number,
readonly maxSizeBytes: number,
) {
super("SQL file is too large to open in the editor");
this.name = "ExternalSqlFileTooLargeError";
}
}
export function isExternalSqlFileTooLargeError(error: unknown): error is ExternalSqlFileTooLargeError {
return error instanceof ExternalSqlFileTooLargeError;
}
export function readBrowserSqlFile(file: Blob): Promise<string> {
if (file.size > MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES) {
return Promise.reject(new ExternalSqlFileTooLargeError(file.size, MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES));
}
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string") resolve(reader.result);
else reject(new Error("Failed to read SQL file as text"));
};
reader.onerror = () => reject(reader.error ?? new Error("Failed to read SQL file"));
reader.onabort = () => reject(new Error("SQL file read was cancelled"));
reader.readAsText(file);
});
}
export function formatSqlFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
const units = ["KB", "MB", "GB", "TB"];
let value = bytes / 1024;
let unit = units[0];
for (let index = 1; index < units.length && value >= 1024; index += 1) {
value /= 1024;
unit = units[index];
}
return `${value >= 10 ? value.toFixed(1) : value.toFixed(2)} ${unit}`;
}
export function externalSqlFileOpenErrorMessage(error: unknown, translate: (key: string, params: { size: string; limit: string }) => string): string {
if (isExternalSqlFileTooLargeError(error)) {
return translate("sqlFile.tooLargeForEditor", {
size: formatSqlFileSize(error.sizeBytes),
limit: formatSqlFileSize(error.maxSizeBytes),
});
}
if (error instanceof Error) return error.message;
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message;
return String(error);
}