Move SCUM lifecycle ownership to plugin
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user