feat: add UE4SS DLL runtime extension
This commit is contained in:
@@ -461,6 +461,100 @@ function validateClientManagerProfiles(manifest: unknown): string[] {
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateDLLExtensionProfiles(manifest: unknown): string[] {
|
||||
if (typeof manifest !== "object" || manifest === null) {
|
||||
return [];
|
||||
}
|
||||
type DLLExtensionProfile = {
|
||||
key?: string;
|
||||
kind?: string;
|
||||
activation?: string;
|
||||
releaseState?: string;
|
||||
releaseUrl?: string;
|
||||
checksum?: string;
|
||||
sizeBytes?: number;
|
||||
targetKey?: string;
|
||||
modKey?: string;
|
||||
dllRef?: string;
|
||||
scumExecutableChecksum?: string;
|
||||
ue4ssAbi?: string;
|
||||
supportedTargets?: Array<{ os?: string; arch?: string }>;
|
||||
updateOnStart?: boolean;
|
||||
rconPort?: number;
|
||||
};
|
||||
type LifecycleProfile = { key?: string; mode?: string; capabilities?: string[]; dllExtensionRefs?: string[]; platforms?: string[] };
|
||||
const runtimeProfiles = (manifest as { runtimeProfiles?: { dllExtensions?: DLLExtensionProfile[]; lifecycleProfiles?: LifecycleProfile[] } }).runtimeProfiles;
|
||||
const profiles = runtimeProfiles?.dllExtensions ?? [];
|
||||
const errors: string[] = [];
|
||||
const checksumPattern = /^sha256:[a-fA-F0-9]{64}$/;
|
||||
const privateIPv4 = /^(127\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/;
|
||||
|
||||
for (const [index, profile] of profiles.entries()) {
|
||||
const location = `manifest.runtimeProfiles.dllExtensions[${index}]`;
|
||||
if (profile.kind !== "ue4ss-dll" || profile.activation !== "server-start") {
|
||||
errors.push(`${location}: only a server-start ue4ss-dll extension is allowed`);
|
||||
}
|
||||
if (profile.dllRef !== `ue4ss/Mods/${profile.modKey ?? ""}/dlls/main.dll`) {
|
||||
errors.push(`${location}.dllRef: must be the declared UE4SS mod main.dll path`);
|
||||
}
|
||||
if (profile.updateOnStart !== true) {
|
||||
errors.push(`${location}.updateOnStart: must be true for a managed DLL extension`);
|
||||
}
|
||||
if (!Number.isInteger(profile.rconPort) || (profile.rconPort ?? 0) < 1024 || (profile.rconPort ?? 0) > 65535) {
|
||||
errors.push(`${location}.rconPort: must be an unprivileged TCP port`);
|
||||
}
|
||||
const target = profile.supportedTargets?.[0];
|
||||
if (profile.supportedTargets?.length !== 1 || target?.os !== "windows" || target?.arch !== "amd64") {
|
||||
errors.push(`${location}.supportedTargets: UE4SS DLL extensions support only windows/amd64`);
|
||||
}
|
||||
if (profile.releaseState !== "ready" && profile.releaseState !== "unpublished") {
|
||||
errors.push(`${location}.releaseState: must be ready or unpublished`);
|
||||
}
|
||||
if (profile.releaseState === "ready") {
|
||||
if (!checksumPattern.test(profile.checksum ?? "") || !checksumPattern.test(profile.scumExecutableChecksum ?? "")) {
|
||||
errors.push(`${location}: release and SCUM executable SHA-256 checksums are required`);
|
||||
}
|
||||
if (!Number.isInteger(profile.sizeBytes) || (profile.sizeBytes ?? 0) < 1 || (profile.sizeBytes ?? 0) > 128 * 1024 * 1024) {
|
||||
errors.push(`${location}.sizeBytes: must be a bounded DLL size`);
|
||||
}
|
||||
if (!profile.ue4ssAbi) {
|
||||
errors.push(`${location}.ue4ssAbi: an ABI marker is required`);
|
||||
}
|
||||
}
|
||||
if (profile.releaseUrl) {
|
||||
try {
|
||||
const parsed = new URL(profile.releaseUrl);
|
||||
const host = parsed.hostname.toLowerCase();
|
||||
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host === "::1" || privateIPv4.test(host) || !parsed.pathname.toLowerCase().endsWith(".dll")) {
|
||||
errors.push(`${location}.releaseUrl: must be an approved public HTTPS DLL release`);
|
||||
}
|
||||
} catch {
|
||||
errors.push(`${location}.releaseUrl: URL is invalid`);
|
||||
}
|
||||
} else if (profile.releaseState === "ready") {
|
||||
errors.push(`${location}.releaseUrl: a ready release URL is required`);
|
||||
}
|
||||
}
|
||||
|
||||
const declared = new Set(profiles.map((profile) => profile.key).filter((key): key is string => Boolean(key)));
|
||||
const stateByKey = new Map(profiles.map((profile) => [profile.key, profile.releaseState]));
|
||||
for (const [index, profile] of (runtimeProfiles?.lifecycleProfiles ?? []).entries()) {
|
||||
const extensionRefs = profile.dllExtensionRefs ?? [];
|
||||
if (extensionRefs.length > 0 && (profile.mode !== "local-process" || !profile.capabilities?.includes("process.start") || profile.platforms?.length !== 1 || profile.platforms[0] !== "windows")) {
|
||||
errors.push(`manifest.runtimeProfiles.lifecycleProfiles[${index}]: a DLL extension requires a windows local-process profile with process.start`);
|
||||
}
|
||||
for (const key of extensionRefs) {
|
||||
if (!declared.has(key)) {
|
||||
errors.push(`manifest.runtimeProfiles.lifecycleProfiles[${index}].dllExtensionRefs: undeclared DLL extension ${key}`);
|
||||
}
|
||||
if (stateByKey.get(key) !== "ready") {
|
||||
errors.push(`manifest.runtimeProfiles.lifecycleProfiles[${index}].dllExtensionRefs: DLL extension ${key} is not ready for activation`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
|
||||
if (typeof manifest !== "object" || manifest === null) {
|
||||
return [];
|
||||
@@ -1011,6 +1105,7 @@ export function validateManifestFile(manifestPath: string): string[] {
|
||||
errors.push(...scanUnsafeValues(manifest, "manifest"));
|
||||
errors.push(...validateDependencyPlans(manifest));
|
||||
errors.push(...validateClientManagerProfiles(manifest));
|
||||
errors.push(...validateDLLExtensionProfiles(manifest));
|
||||
errors.push(...validateGameClientBridgeCatalog(manifest));
|
||||
errors.push(...validateGameClientBridgeSchemaFiles(manifest, manifestDir));
|
||||
errors.push(...validateGameClientBridgeCompanionConfig(manifest, manifestDir));
|
||||
|
||||
Reference in New Issue
Block a user