fix(library): validate saved library paths

This commit is contained in:
Viktor Olausson
2026-07-17 18:42:43 +02:00
parent 2dfa414e64
commit aa388b87d5
3 changed files with 110 additions and 30 deletions
+27 -5
View File
@@ -19,10 +19,10 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
}
/**
* Normalize an administrator-supplied DLC_DIR and accept it only when it
* already names a directory. Invalid overrides must not shadow config.json.
* Normalize a configured library path and accept it only when it already
* names a directory.
*/
export function normalizeExplicitLibraryPath(rawPath?: string): string | undefined {
export function normalizeExistingLibraryDirectory(rawPath?: string): string | undefined {
const candidate = (rawPath || '').trim();
if (!candidate) return undefined;
@@ -33,6 +33,22 @@ export function normalizeExplicitLibraryPath(rawPath?: string): string | undefin
}
}
/**
* Apply the prepared library-path contract to the Python child environment.
* Starting from a copy of process.env means an invalid or stale parent value
* must be removed explicitly when config.json owns the path.
*/
export function applyLibraryPathToPythonEnvironment(
environment: Record<string, string>,
preparation: LibraryPathPreparation,
): void {
if (preparation.environmentDlcDir) {
environment.DLC_DIR = preparation.environmentDlcDir;
} else {
delete environment.DLC_DIR;
}
}
/**
* Prepare the library path contract for the Python backend.
*
@@ -46,7 +62,7 @@ export function prepareLibraryPathForPython(
resolvedDlcDir: string,
explicitDlcDir?: string,
): LibraryPathPreparation {
const override = normalizeExplicitLibraryPath(explicitDlcDir);
const override = normalizeExistingLibraryDirectory(explicitDlcDir);
if (override) {
return {
status: 'explicit-override',
@@ -76,7 +92,13 @@ export function prepareLibraryPathForPython(
const configured = config.dlc_dir;
if (typeof configured === 'string' && configured.trim()) {
return { status: 'configured' };
if (normalizeExistingLibraryDirectory(configured)) {
return { status: 'configured' };
}
return {
status: 'invalid-config',
error: 'config.json dlc_dir is not an existing directory',
};
}
if (configured !== undefined && configured !== null && configured !== '') {
return {
+10 -10
View File
@@ -11,7 +11,11 @@ import * as net from 'net';
import * as os from 'os';
import { getActiveSoundfontPath, getDesktopConfig } from './soundfont-manager';
import { isDebugEnabled } from './debug-log';
import { normalizeExplicitLibraryPath, prepareLibraryPathForPython } from './library-path-config';
import {
applyLibraryPathToPythonEnvironment,
normalizeExistingLibraryDirectory,
prepareLibraryPathForPython,
} from './library-path-config';
let pythonProcess: ChildProcess | null = null;
// A backend that is being *gracefully* stopped (SIGTERM sent, async SIGKILL
@@ -424,7 +428,7 @@ function getPluginsDir(): string {
return pluginsDir;
}
function getDLCDir(explicitDlcDir = normalizeExplicitLibraryPath(process.env.DLC_DIR)): string {
function getDLCDir(explicitDlcDir = normalizeExistingLibraryDirectory(process.env.DLC_DIR)): string {
if (explicitDlcDir) return explicitDlcDir;
// Read from shared config
@@ -497,7 +501,7 @@ export async function startPython(): Promise<void> {
}
serverPort = await findPort(PREFERRED_PORT);
const configDir = getConfigDir();
const explicitDlcDir = normalizeExplicitLibraryPath(process.env.DLC_DIR);
const explicitDlcDir = normalizeExistingLibraryDirectory(process.env.DLC_DIR);
const dlcDir = getDLCDir(explicitDlcDir);
// Ensure the resolved library folder exists before the server starts. The
// Python side only seeds starter content (and scans) when DLC_DIR.is_dir()
@@ -582,13 +586,9 @@ export async function startPython(): Promise<void> {
: path.join(__dirname, '..', '..', 'resources', 'bin') + path.delimiter
) + (process.env.PATH || ''),
};
if (libraryPath.environmentDlcDir) {
pythonEnv.DLC_DIR = libraryPath.environmentDlcDir;
} else {
// `...process.env` may carry an empty/invalid value. Do not let it
// shadow config.json in the normal dynamic-settings path.
delete pythonEnv.DLC_DIR;
}
// `...process.env` may carry an empty/invalid value. Do not let it shadow
// config.json in the normal dynamic-settings path.
applyLibraryPathToPythonEnvironment(pythonEnv, libraryPath);
// Debug mode: raise the Slopsmith server's log level and tee its
// structured logs to a file. lib/logging_setup.py reads LOG_LEVEL and