mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-27 11:42:52 +00:00
Load config.json saved with a UTF-8 byte order mark (#5315)
* Load config.json saved with a UTF-8 byte order mark Windows Notepad saves UTF-8 with a BOM. Both config readers passed the text straight to JSON.parse, so the daemon refused to start and CLI config commands failed with "Unexpected token". Closes #4683 * Strip the config BOM with an anchored replace --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
loadPersistedConfig,
|
||||
PersistedConfigSchema,
|
||||
readPersistedConfig,
|
||||
savePersistedConfig,
|
||||
} from "./persisted-config.js";
|
||||
import { PRIVATE_DIRECTORY_MODE, PRIVATE_FILE_MODE } from "./private-files.js";
|
||||
@@ -736,6 +737,34 @@ describe("loadPersistedConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("config.json saved with a UTF-8 byte order mark", () => {
|
||||
// Windows Notepad writes this shape: a BOM, then CRLF line endings.
|
||||
const notepadConfig =
|
||||
'{\r\n "version": 1,\r\n "daemon": { "listen": "127.0.0.1:6767" }\r\n}\r\n';
|
||||
|
||||
test("loadPersistedConfig reads it", () => {
|
||||
const home = createTempHome();
|
||||
try {
|
||||
writeFileSync(path.join(home, "config.json"), notepadConfig);
|
||||
|
||||
expect(loadPersistedConfig(home).daemon?.listen).toBe("127.0.0.1:6767");
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("readPersistedConfig reads it", () => {
|
||||
const home = createTempHome();
|
||||
try {
|
||||
writeFileSync(path.join(home, "config.json"), notepadConfig);
|
||||
|
||||
expect(readPersistedConfig(home).daemon?.listen).toBe("127.0.0.1:6767");
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(process.platform === "win32")("persisted config file permissions", () => {
|
||||
test("initializes config.json with private permissions", () => {
|
||||
const home = createTempHome();
|
||||
|
||||
@@ -438,7 +438,7 @@ export function loadPersistedConfig(paseoHome: string, logger?: LoggerLike): Per
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
parsed = parseConfigText(raw);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`[Config] Invalid JSON in ${configPath}: ${message}`, {
|
||||
@@ -472,7 +472,14 @@ export function readPersistedConfig(
|
||||
return options.defaultsIfMissing ? structuredClone(DEFAULT_PERSISTED_CONFIG) : {};
|
||||
throw error;
|
||||
}
|
||||
return PersistedConfigSchema.parse(stripRemovedConfigFields(JSON.parse(raw))) as PersistedConfig;
|
||||
return PersistedConfigSchema.parse(
|
||||
stripRemovedConfigFields(parseConfigText(raw)),
|
||||
) as PersistedConfig;
|
||||
}
|
||||
|
||||
/** Editors such as Windows Notepad save UTF-8 with a byte order mark, which JSON.parse rejects. */
|
||||
function parseConfigText(raw: string): unknown {
|
||||
return JSON.parse(raw.replace(/^\uFEFF/, ""));
|
||||
}
|
||||
|
||||
function configPathParts(field: string): string[] {
|
||||
|
||||
Reference in New Issue
Block a user