Move SCUM lifecycle ownership to plugin

This commit is contained in:
npc0-hue
2026-08-01 09:36:12 +08:00
parent bca4f935ba
commit d1249fca85
49 changed files with 795 additions and 566 deletions
+65
View File
@@ -134,6 +134,10 @@ function isSafeRelativeJsonRef(value: string): boolean {
return /^(?!\/)(?![A-Za-z]:)(?!.*:\/\/)(?!.*\.\.)[a-zA-Z0-9_./-]+\.json$/.test(value);
}
function isSafeRelativePathRef(value: string): boolean {
return /^(?!\/)(?![A-Za-z]:)(?!.*:\/\/)(?!.*\.\.)[a-zA-Z0-9_./-]+$/.test(value);
}
function identifierTokens(value: string): string[] {
return value
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
@@ -353,6 +357,57 @@ function referencedLifecycleActions(manifest: unknown): Array<{ action: string;
return [...refs.values()];
}
type PluginAssetFileDeclaration = { path?: unknown; mode?: unknown };
function declaredAssetFiles(manifest: unknown): PluginAssetFileDeclaration[] {
if (typeof manifest !== "object" || manifest === null) {
return [];
}
const assetFiles = (manifest as { assetFiles?: unknown }).assetFiles;
return Array.isArray(assetFiles) ? (assetFiles as PluginAssetFileDeclaration[]) : [];
}
function validateManifestAssetFiles(manifest: unknown, manifestDir: string): { errors: string[]; declared: Set<string> } {
const errors: string[] = [];
const declared = new Set<string>();
const root = path.resolve(manifestDir);
for (const [index, file] of declaredAssetFiles(manifest).entries()) {
const location = `manifest.assetFiles[${index}]`;
if (typeof file.path !== "string" || !isSafeRelativePathRef(file.path)) {
errors.push(`${location}.path: unsafe file reference`);
continue;
}
if (declared.has(file.path)) {
errors.push(`${location}.path: duplicate asset file`);
continue;
}
declared.add(file.path);
if (file.mode !== undefined && file.mode !== 0o600 && file.mode !== 0o700) {
errors.push(`${location}.mode: unsafe file mode`);
}
const target = path.resolve(manifestDir, file.path);
const relative = path.relative(root, target);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
errors.push(`${location}.path: file escapes plugin directory`);
continue;
}
if (!fs.existsSync(target)) {
errors.push(`${location}.path: missing file ${file.path}`);
continue;
}
const stat = fs.statSync(target);
if (!stat.isFile() || stat.size > 64 * 1024) {
errors.push(`${location}.path: asset file must be a regular file under 64KiB`);
continue;
}
const body = fs.readFileSync(target);
if (body.includes(0)) {
errors.push(`${location}.path: asset file contains NUL bytes`);
}
}
return { errors, declared };
}
function validateDependencyPlans(manifest: unknown): string[] {
if (typeof manifest !== "object" || manifest === null) {
return [];
@@ -1187,18 +1242,28 @@ export function validateManifestFile(manifestPath: string): string[] {
errors.push(...validateGameClientBridgeCompanionConfig(manifest, manifestDir));
errors.push(...validateRuntimeLogEventCatalog(manifest));
errors.push(...validateRuntimeLogEventSchemaFiles(manifest, manifestDir));
const assetValidation = validateManifestAssetFiles(manifest, manifestDir);
errors.push(...assetValidation.errors);
for (const declaration of referencedLifecycleActions(manifest)) {
if (!isSafeRelativeJsonRef(declaration.ref)) {
errors.push(`lifecycleAction.${declaration.action}: unsafe file reference`);
continue;
}
if (!assetValidation.declared.has(declaration.ref)) {
errors.push(`lifecycleAction.${declaration.action}: action file must be declared in manifest.assetFiles`);
}
const actionPath = path.resolve(manifestDir, declaration.ref);
if (!fs.existsSync(actionPath)) {
errors.push(`lifecycleAction.${declaration.action}: missing file ${declaration.ref}`);
continue;
}
errors.push(...validateLifecycleActionFile(path.relative(rootDir, actionPath), declaration.action));
const action = readJson(actionPath);
const executableKey = typeof action === "object" && action !== null ? (action as { executableKey?: unknown }).executableKey : undefined;
if (typeof executableKey === "string" && !assetValidation.declared.has(executableKey)) {
errors.push(`lifecycleAction.${declaration.action}.executableKey: ${executableKey} must be declared in manifest.assetFiles`);
}
}
if (typeof manifest === "object" && manifest !== null && "server" in manifest) {