From 2b1db4c42cd9e61217fff3cb513cad928c895065 Mon Sep 17 00:00:00 2001 From: Minwook Shin <163576506+minwookshin@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:57:32 -0400 Subject: [PATCH 1/3] fix(tests): load exporter assets from encoded filesystem paths --- shared/util/fileUtil.ts | 3 +- tests/src/unit/shared/fileUtil.test.ts | 53 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 tests/src/unit/shared/fileUtil.test.ts diff --git a/shared/util/fileUtil.ts b/shared/util/fileUtil.ts index 71601a90db..417917409c 100644 --- a/shared/util/fileUtil.ts +++ b/shared/util/fileUtil.ts @@ -32,7 +32,8 @@ export async function loadFileBuffer(requireUrl: { // in vitest, this is the url we need to load with readfilesync // eslint-disable-next-line const fs = require("fs"); - let url = requireUrl.default; + // Vite percent-encodes asset URLs, including spaces and non-ASCII paths. + let url = decodeURIComponent(requireUrl.default); if (url.startsWith("/@fs/")) { url = url.substring("/@fs".length); diff --git a/tests/src/unit/shared/fileUtil.test.ts b/tests/src/unit/shared/fileUtil.test.ts new file mode 100644 index 0000000000..d8045e47cd --- /dev/null +++ b/tests/src/unit/shared/fileUtil.test.ts @@ -0,0 +1,53 @@ +import { loadFileBuffer, loadFontDataUrl } from "@shared/util/fileUtil.js"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; + +describe("Vite asset file loading", () => { + let directory: string; + const contents = Buffer.from([0, 127, 128, 255]); + + beforeEach(() => { + directory = mkdtempSync(join(tmpdir(), "blocknote-file-url-")); + }); + + afterEach(() => { + rmSync(directory, { recursive: true, force: true }); + }); + + for (const prefix of ["", "/@fs"]) { + it.each(["한글 font.ttf", "font #100% %20.ttf"])( + `reads encoded ${prefix || "absolute"} asset paths: %s`, + async (name) => { + const file = join(directory, name); + writeFileSync(file, contents); + + const buffer = await loadFileBuffer({ + default: prefix + pathToFileURL(file).pathname, + }); + + expect(buffer).toEqual(contents); + }, + ); + } + + it("reads an unencoded filesystem path", async () => { + const file = join(directory, "font.ttf"); + writeFileSync(file, contents); + + expect(await loadFileBuffer({ default: file })).toEqual(contents); + }); + + it("creates a font data URL from an encoded asset path", async () => { + const file = join(directory, "한글 font.ttf"); + writeFileSync(file, contents); + + expect( + await loadFontDataUrl({ + default: "/@fs" + pathToFileURL(file).pathname, + }), + ).toBe(`data:font/ttf;base64,${contents.toString("base64")}`); + }); +}); From 490800e017853a8af0a6c20585fc6a6676710592 Mon Sep 17 00:00:00 2001 From: Minwook Shin <163576506+minwookshin@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:48:54 -0400 Subject: [PATCH 2/3] fix: preserve literal percent paths in asset loader --- shared/util/fileUtil.ts | 17 ++++++++++++----- tests/src/unit/shared/fileUtil.test.ts | 10 ++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/shared/util/fileUtil.ts b/shared/util/fileUtil.ts index 417917409c..c5aacd8a0d 100644 --- a/shared/util/fileUtil.ts +++ b/shared/util/fileUtil.ts @@ -32,11 +32,18 @@ export async function loadFileBuffer(requireUrl: { // in vitest, this is the url we need to load with readfilesync // eslint-disable-next-line const fs = require("fs"); - // Vite percent-encodes asset URLs, including spaces and non-ASCII paths. - let url = decodeURIComponent(requireUrl.default); - - if (url.startsWith("/@fs/")) { - url = url.substring("/@fs".length); + // Direct paths can contain literal percent sequences. Prefer an existing + // path, then decode Vite's /@fs and encoded absolute asset URLs. + const assetPath = requireUrl.default; + let url = assetPath; + if (assetPath.startsWith("/@fs/")) { + url = decodeURIComponent(assetPath.substring("/@fs".length)); + } else if (!fs.existsSync(assetPath)) { + try { + url = decodeURIComponent(assetPath); + } catch { + // Preserve a malformed percent sequence as a literal filesystem path. + } } // On Windows, vite/vitest may yield paths like "/C:/..." after removing /@fs // Node on Windows treats paths starting with "/" as relative to current drive, diff --git a/tests/src/unit/shared/fileUtil.test.ts b/tests/src/unit/shared/fileUtil.test.ts index d8045e47cd..e1a009975e 100644 --- a/tests/src/unit/shared/fileUtil.test.ts +++ b/tests/src/unit/shared/fileUtil.test.ts @@ -40,6 +40,16 @@ describe("Vite asset file loading", () => { expect(await loadFileBuffer({ default: file })).toEqual(contents); }); + it.each(["font%20.ttf", "font%foo.ttf"])( + "preserves a direct filesystem path containing %s", + async (name) => { + const file = join(directory, name); + writeFileSync(file, contents); + + expect(await loadFileBuffer({ default: file })).toEqual(contents); + }, + ); + it("creates a font data URL from an encoded asset path", async () => { const file = join(directory, "한글 font.ttf"); writeFileSync(file, contents); From 6c4a2beae28eaf1337507a32824cf09c7893cecb Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 27 Oct 2026 02:07:32 +0100 Subject: [PATCH 3/3] test: remove asset loader unit tests --- tests/src/unit/shared/fileUtil.test.ts | 63 -------------------------- 1 file changed, 63 deletions(-) delete mode 100644 tests/src/unit/shared/fileUtil.test.ts diff --git a/tests/src/unit/shared/fileUtil.test.ts b/tests/src/unit/shared/fileUtil.test.ts deleted file mode 100644 index e1a009975e..0000000000 --- a/tests/src/unit/shared/fileUtil.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { loadFileBuffer, loadFontDataUrl } from "@shared/util/fileUtil.js"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { pathToFileURL } from "node:url"; -import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; - -describe("Vite asset file loading", () => { - let directory: string; - const contents = Buffer.from([0, 127, 128, 255]); - - beforeEach(() => { - directory = mkdtempSync(join(tmpdir(), "blocknote-file-url-")); - }); - - afterEach(() => { - rmSync(directory, { recursive: true, force: true }); - }); - - for (const prefix of ["", "/@fs"]) { - it.each(["한글 font.ttf", "font #100% %20.ttf"])( - `reads encoded ${prefix || "absolute"} asset paths: %s`, - async (name) => { - const file = join(directory, name); - writeFileSync(file, contents); - - const buffer = await loadFileBuffer({ - default: prefix + pathToFileURL(file).pathname, - }); - - expect(buffer).toEqual(contents); - }, - ); - } - - it("reads an unencoded filesystem path", async () => { - const file = join(directory, "font.ttf"); - writeFileSync(file, contents); - - expect(await loadFileBuffer({ default: file })).toEqual(contents); - }); - - it.each(["font%20.ttf", "font%foo.ttf"])( - "preserves a direct filesystem path containing %s", - async (name) => { - const file = join(directory, name); - writeFileSync(file, contents); - - expect(await loadFileBuffer({ default: file })).toEqual(contents); - }, - ); - - it("creates a font data URL from an encoded asset path", async () => { - const file = join(directory, "한글 font.ttf"); - writeFileSync(file, contents); - - expect( - await loadFontDataUrl({ - default: "/@fs" + pathToFileURL(file).pathname, - }), - ).toBe(`data:font/ttf;base64,${contents.toString("base64")}`); - }); -});