fix(plugins): carry plugin file handle ids as uuid strings

This commit is contained in:
Jinpy authored and GitHub committed 2026-09-22 14:15:13 +08:00
1 parent 9bd9b93267
commit 4f0f69131b
5 files changed
+104 -66

No files matched your search

@@ -148,7 +148,8 @@ describe("PluginWorkbenchHost initialization", () => {
it("claims OS drops over its iframe and forwards opened handles to the plugin", async () => {
const { frame, postMessage } = await mountHost();
const elementFromPoint = vi.spyOn(document, "elementFromPoint").mockReturnValue(frame);
mocks.openPluginLocalFile.mockResolvedValue({ handleId: 7, name: "a.txt", size: 3, contentType: "text/plain", write: false });
// The Rust registry hands out uuid strings; the `t` prefix stays opaque.
mocks.openPluginLocalFile.mockResolvedValue({ handleId: "0d9f6d26-9e0e-4b1f-8f9a-2b6d3c5a7e81", name: "a.txt", size: 3, contentType: "text/plain", write: false });
const claimed = !document.dispatchEvent(
new CustomEvent("dbx:tauri-file-drop", {
@@ -160,7 +161,7 @@ describe("PluginWorkbenchHost initialization", () => {
expect(claimed).toBe(true);
await vi.waitFor(() => {
const posted = postMessage.mock.calls.map(([message]) => message as Record<string, unknown>);
expect(posted.some((message) => message.type === "filedrop" && (message.files as Array<Record<string, unknown>>)?.some((file) => file.handleId === "t7" && file.name === "a.txt"))).toBe(true);
expect(posted.some((message) => message.type === "filedrop" && (message.files as Array<Record<string, unknown>>)?.some((file) => file.handleId === "t0d9f6d26-9e0e-4b1f-8f9a-2b6d3c5a7e81" && file.name === "a.txt"))).toBe(true);
});
expect(mocks.openPluginLocalFile).toHaveBeenCalledWith("sample", "/tmp/a.txt", false);
elementFromPoint.mockRestore();
@@ -60,15 +60,17 @@ let loadGeneration = 0;
// --- Plugin file-transfer bridge (native dialogs + OS file drops) ---------
// The sandboxed iframe cannot reach local files, so handles live here: Tauri
// handles wrap the plugin_file registry in Rust (`t<n>` ids); the web host
// keeps File objects and in-memory save buffers (`w<n>` ids). Only paths that
// came from a native dialog or an OS drop reach plugin_file_open — never a
// plugin-supplied string.
// handles wrap the plugin_file registry in Rust (`t<uuid>` ids); the web host
// keeps File objects and in-memory save buffers (`w<n>` ids). Handle ids are
// opaque strings end to end — never run them through Number(): ids above
// Number.MAX_SAFE_INTEGER silently round, and the registry then rejects every
// read with "unknown plugin file handle". Only paths that came from a native
// dialog or an OS drop reach plugin_file_open — never a plugin-supplied string.
let webFileSequence = 0;
const webPickedFiles = new Map<string, File>();
const webSaveBuffers = new Map<string, { name: string; contentType: string; chunks: Map<number, Uint8Array> }>();
const openTauriHandles = new Set<number>();
const openTauriHandles = new Set<string>();
const tauriHandlePrefix = "t";
const webHandlePrefix = "w";
@@ -81,9 +83,9 @@ function encodeBytesBase64(bytes: Uint8Array): string {
return btoa(binary);
}
function parseHandleId(handleId: string): { source: "tauri" | "web"; numericId: number } {
if (handleId.startsWith(tauriHandlePrefix)) return { source: "tauri", numericId: Number(handleId.slice(tauriHandlePrefix.length)) };
if (handleId.startsWith(webHandlePrefix)) return { source: "web", numericId: Number(handleId.slice(webHandlePrefix.length)) };
function parseHandleId(handleId: string): { source: "tauri" | "web"; rawId: string } {
if (handleId.startsWith(tauriHandlePrefix)) return { source: "tauri", rawId: handleId.slice(tauriHandlePrefix.length) };
if (handleId.startsWith(webHandlePrefix)) return { source: "web", rawId: handleId.slice(webHandlePrefix.length) };
throw new Error("Unknown file handle");
}
@@ -149,7 +151,7 @@ async function readPluginFileChunkById(pluginId: string, handleId: string, offse
const parsed = parseHandleId(handleId);
if (parsed.source === "tauri") {
const { readPluginLocalFileChunk } = await tauriFileApi();
return readPluginLocalFileChunk(pluginId, parsed.numericId, offset, length);
return readPluginLocalFileChunk(pluginId, parsed.rawId, offset, length);
}
const file = webPickedFiles.get(handleId);
if (!file) throw new Error("Unknown file handle");
@@ -217,7 +219,7 @@ async function writePluginFileChunkById(pluginId: string, handleId: string, offs
const parsed = parseHandleId(handleId);
if (parsed.source === "tauri") {
const { writePluginLocalFileChunk } = await tauriFileApi();
return writePluginLocalFileChunk(pluginId, parsed.numericId, offset, encodeBytesBase64(bytes));
return writePluginLocalFileChunk(pluginId, parsed.rawId, offset, encodeBytesBase64(bytes));
}
const buffer = webSaveBuffers.get(handleId);
if (!buffer) throw new Error("Unknown file handle");
@@ -229,8 +231,8 @@ async function finishPluginFileSave(pluginId: string, handleId: string): Promise
const parsed = parseHandleId(handleId);
if (parsed.source === "tauri") {
const { closePluginLocalFile } = await tauriFileApi();
openTauriHandles.delete(parsed.numericId);
await closePluginLocalFile(pluginId, parsed.numericId);
openTauriHandles.delete(parsed.rawId);
await closePluginLocalFile(pluginId, parsed.rawId);
return;
}
const buffer = webSaveBuffers.get(handleId);
@@ -256,8 +258,8 @@ async function closePluginFileHandleById(pluginId: string, handleId: string): Pr
const parsed = parseHandleId(handleId);
if (parsed.source === "tauri") {
const { closePluginLocalFile } = await tauriFileApi();
openTauriHandles.delete(parsed.numericId);
await closePluginLocalFile(pluginId, parsed.numericId);
openTauriHandles.delete(parsed.rawId);
await closePluginLocalFile(pluginId, parsed.rawId);
return;
}
webPickedFiles.delete(handleId);
+3 -3
View File
@@ -5181,15 +5181,15 @@ export async function openPluginLocalFile(_pluginId: string, _path: string, _wri
throw new Error("Plugin local file access is not available in the web backend");
}
export async function readPluginLocalFileChunk(_pluginId: string, _handleId: number, _offset: number, _length?: number): Promise<PluginLocalFileChunk> {
export async function readPluginLocalFileChunk(_pluginId: string, _handleId: string, _offset: number, _length?: number): Promise<PluginLocalFileChunk> {
throw new Error("Plugin local file access is not available in the web backend");
}
export async function writePluginLocalFileChunk(_pluginId: string, _handleId: number, _offset: number, _dataBase64: string): Promise<PluginLocalFileWriteResult> {
export async function writePluginLocalFileChunk(_pluginId: string, _handleId: string, _offset: number, _dataBase64: string): Promise<PluginLocalFileWriteResult> {
throw new Error("Plugin local file access is not available in the web backend");
}
export async function closePluginLocalFile(_pluginId: string, _handleId: number): Promise<void> {
export async function closePluginLocalFile(_pluginId: string, _handleId: string): Promise<void> {
throw new Error("Plugin local file access is not available in the web backend");
}
+5 -4
View File
@@ -2462,7 +2462,8 @@ export async function sendPluginBinary(pluginId: string, channel: string, dataBa
}
export interface PluginLocalFileHandle {
handleId: number;
/** Opaque uuid string from the Rust registry — never a number (JS doubles lose precision above 2^53). */
handleId: string;
name: string;
size: number;
contentType: string;
@@ -2484,15 +2485,15 @@ export async function openPluginLocalFile(pluginId: string, path: string, write:
return invoke("plugin_file_open", { pluginId, path, write });
}
export async function readPluginLocalFileChunk(pluginId: string, handleId: number, offset: number, length?: number): Promise<PluginLocalFileChunk> {
export async function readPluginLocalFileChunk(pluginId: string, handleId: string, offset: number, length?: number): Promise<PluginLocalFileChunk> {
return invoke("plugin_file_read", { pluginId, handleId, offset, length });
}
export async function writePluginLocalFileChunk(pluginId: string, handleId: number, offset: number, dataBase64: string): Promise<PluginLocalFileWriteResult> {
export async function writePluginLocalFileChunk(pluginId: string, handleId: string, offset: number, dataBase64: string): Promise<PluginLocalFileWriteResult> {
return invoke("plugin_file_write", { pluginId, handleId, offset, dataBase64 });
}
export async function closePluginLocalFile(pluginId: string, handleId: number): Promise<void> {
export async function closePluginLocalFile(pluginId: string, handleId: string): Promise<void> {
return invoke("plugin_file_close", { pluginId, handleId });
}