fix(plugins): carry plugin file handle ids as uuid strings
This commit is contained in:
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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user