From 75cce5a8781e87dbacdf32ae7d9fa253df64e3f6 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/2] 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 c611daf516ab32003d839d4c870ef736f0368c41 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/2] 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);