1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-12-08 13:08:26 +00:00

Blacklisted class macro

Macro that automatically generates fields for use in sandboxed classes.
This commit is contained in:
Hyper_ 2025-11-06 16:44:52 -03:00
parent 3f89464608
commit a2ccb5d44a
No known key found for this signature in database
2 changed files with 410 additions and 157 deletions

View file

@ -1,13 +1,41 @@
package funkin.util;
import Type.ValueType;
/**
* Provides sanitized and blacklisted access to haxe's Reflection functions.
* Used for sandboxing in scripts.
*/
@:nullSafety
@SuppressWarnings("checkstyle:VarTypeHint")
@:build(funkin.util.macro.BlacklistClassMacro.build(
{
classes: ["Reflect", "Type"],
aliases:
{
"compare": ["compareValues"],
"copy": ["copyAnonymousFieldsOf"],
"deleteField": ["deleteAnonymousField", "delete"],
"field": ["getAnonymousField", "getField"],
"fields": ["getAnonymousFieldsOf", "getFieldsOf"],
"hasField": ["hasAnonymousField"],
"setField": ["setAnonymousField"]
},
customWrapList: [
"compare",
"compareMethods",
"copy",
"enumEq",
"deleteField",
"fields",
"getClassFields",
"getClassName",
"getEnumName",
"getInstanceFields",
"isEnumValue",
"isFunction",
"isObject",
"setField",
"setProperty"
]
}))
class ReflectUtil
{
/**
@ -20,24 +48,16 @@ class ReflectUtil
* This function is not allowed to be used by scripts.
* @throws error When called by a script.
*/
@SuppressWarnings("checkstyle:FieldDocComment")
public static function callMethod(obj:Any, name:String, args:Array<Any>):Any
{
throw "Function Reflect.callMethod is blacklisted.";
}
// public static function callMethod(obj:Any, name:String, args:Array<Any>):Any
/**
* Compares two objects by value.
*
* The actual function exists and is generated at build time.
* @param valueA First value to compare
* @param valueB Second value to compare
* @return Int indicating relative order of values
*/
public static function compare(valueA:Any, valueB:Any):Int
{
return compareValues(valueA, valueB);
}
// public static function compare(valueA:Any, valueB:Any):Int
/**
* Compares two values and returns an integer indicating their relative order.
* Returns:
@ -45,94 +65,76 @@ class ReflectUtil
* - 0 if valueA == valueB
* - 1 if valueA > valueB
*
* The actual function exists and is generated at build time.
* @param valueA First value to compare
* @param valueB Second value to compare
* @return An integer indicating relative order of values
*/
public static function compareValues(valueA:Any, valueB:Any):Int
{
return Reflect.compare(valueA, valueB);
}
// public static function compareValues(valueA:Any, valueB:Any):Int
/**
* Compare the two Function objects to determine whether they are the same.
* @param functionA A method closure to compare.
* @param functionB A method closure to compare.
* @return Whether functionA and functionB are equal.
*/
public static function compareMethods(functionA:Any, functionB:Any):Bool
{
return Reflect.compareMethods(functionA, functionB);
}
// public static function compareMethods(functionA:Any, functionB:Any):Bool
/**
* Copies the given object.
* Only guaranteed to work on anonymous structures.
*
* The actual function exists and is generated at build time.
* @param obj The object to copy.
* @return An independent clone of that object.
*/
public static function copy(obj:Any):Null<Any>
{
return copyAnonymousFieldsOf(obj);
}
// public static function copy(obj:Any):Null<Any>
/**
* Copies the anonymous structure to a new object.
*
* The actual function exists and is generated at build time.
* @param obj The object to copy.
* @return An independent clone of the structure.
*/
public static function copyAnonymousFieldsOf(obj:Any):Null<Any>
{
return Reflect.copy(obj);
}
// public static function copyAnonymousFieldsOf(obj:Any):Null<Any>
/**
* Delete the field of a given name from an object.
* Only guaranteed to work on anonymous structures.
*
* The actual function exists and is generated at build time.
* @param obj The object to delete the field from.
* @param name The name of the field to delete.
* @return Whether the operation was successful.
*/
public static function delete(obj:Any, name:String):Bool
{
return deleteAnonymousField(obj, name);
}
// public static function delete(obj:Any, name:String):Bool
/**
* Delete the field of a given name from an anonymous structure.
* Only guaranteed to work on anonymous structures.
*
* The actual function exists and is generated at build time.
* @param obj The object to delete the field from.
* @param name The name of the field to delete.
* @return Whether the operation was successful.
*/
public static function deleteAnonymousField(obj:Any, name:String):Bool
{
return Reflect.deleteField(obj, name);
}
// public static function deleteAnonymousField(obj:Any, name:String):Bool
/**
* Retrive the value of a given field (by name) from an object.
* Only guaranteed to work on anonymous structures.
*
* The actual function exists and is generated at build time.
* @param obj The object to delete the field from.
* @param name The name of the field to delete.
* @return Whether the operation was successful.
*/
public static function field(obj:Any, name:String):Any
{
return getAnonymousField(obj, name);
}
// public static function field(obj:Any, name:String):Any
/**
* Retrive the value of a given field (by name) from an object.
* Only guaranteed to work on anonymous structures.
*
* The actual function exists and is generated at build time.
* @param obj The object to delete the field from.
* @param name The name of the field to delete.
* @return Whether the operation was successful.
*/
public static function getField(obj:Any, name:String):Any
{
return getAnonymousField(obj, name);
}
// public static function getField(obj:Any, name:String):Any
/**
* Retrieve the value of the field of the given name from an anonymous structure.
@ -141,7 +143,8 @@ class ReflectUtil
* @return The resulting field value.
* @throws error If the field is blacklisted.
*/
public static function getAnonymousField(obj:Any, name:String):Any
@:blacklistOverride
public static function field(obj:Any, name:String):Any
{
if (FIELD_NAME_BLACKLIST.contains(name))
{
@ -154,34 +157,29 @@ class ReflectUtil
/**
* Get a list of fields available on the given object.
* Only guaranteed to work on anonymous structures.
*
* The actual function exists and is generated at build time.
* @param obj The object to query.
* @return A list of fields on that object.
*/
public static function fields(obj:Any):Array<String>
{
return getAnonymousFieldsOf(obj);
}
// public static function fields(obj:Any):Array<String>
/**
* Get a list of fields available on the given object.
* Only guaranteed to work on anonymous structures.
*
* The actual function exists and is generated at build time.
* @param obj The object to query.
* @return A list of fields on that object.
*/
public static function getFieldsOf(obj:Any):Array<String>
{
return getAnonymousFieldsOf(obj);
}
// public static function getFieldsOf(obj:Any):Array<String>
/**
* Get a list of fields available on the given anonymous structure.
*
* The actual function exists and is generated at build time.
* @param obj The object to query.
* @return A list of fields on that object.
*/
public static function getAnonymousFieldsOf(obj:Any):Array<String>
{
return Reflect.fields(obj);
}
// public static function getAnonymousFieldsOf(obj:Any):Array<String>
/**
* Get the value of the given property on a given object.
@ -192,6 +190,7 @@ class ReflectUtil
* @return The value of the field.
* @throws error If the field is blacklisted.
*/
@:blacklistOverride
public static function getProperty(obj:Any, name:String):Any
{
if (FIELD_NAME_BLACKLIST.contains(name))
@ -205,14 +204,13 @@ class ReflectUtil
/**
* Determine whether the given object has the given field.
* Only guaranteed to work for anonymous structures.
*
* The actual function exists and is generated at build time.
* @param obj The object to query.
* @param name The field name to query.
* @return Whether the field exists.
*/
public static function hasField(obj:Any, name:String):Bool
{
return hasAnonymousField(obj, name);
}
// public static function hasField(obj:Any, name:String):Bool
/**
* Determine whether the given anonymous structure has the given field.
@ -220,7 +218,8 @@ class ReflectUtil
* @param name The field name to query.
* @return Whether the field exists.
*/
public static function hasAnonymousField(obj:Any, name:String):Bool
@:blacklistOverride
public static function hasField(obj:Any, name:String):Bool
{
if (FIELD_NAME_BLACKLIST.contains(name))
{
@ -232,128 +231,89 @@ class ReflectUtil
/**
* Determine whether the given input is an enum value.
*
* The actual function exists and is generated at build time.
* @param value The input to evaluate.
* @return Whether `value` is an enum value.
*/
public static function isEnumValue(value:Any):Bool
{
return Reflect.isEnumValue(value);
}
// public static function isEnumValue(value:Any):Bool
/**
* Determine whether the given input is a callable function.
*
* The actual function exists and is generated at build time.
* @param value The input to evaluate.
* @return Whether `value` is a function.
*/
public static function isFunction(value:Any):Bool
{
return Reflect.isFunction(value);
}
// public static function isFunction(value:Any):Bool
/**
* Determine whether the given input is an object.
*
* The actual function exists and is generated at build time.
* @param value The input to evaluate.
* @return Whether `value` is an object.
*/
public static function isObject(value:Any):Bool
{
return Reflect.isObject(value);
}
// public static function isObject(value:Any):Bool
/**
* Set the value of a specific field on an object.
* Only guaranteed to work for anonymous structures.
*
* The actual function exists and is generated at build time.
* @param obj The object to modify.
* @param name The field to modify.
* @param value The new value to apply.
*/
public static function setField(obj:Any, name:String, value:Any):Void
{
return setAnonymousField(obj, name, value);
}
// public static function setField(obj:Any, name:String, value:Any):Void
/**
* Set the value of a specific field on an anonymous structure.
*
* The actual function exists and is generated at build time.
* @param obj The object to modify.
* @param name The field to modify.
* @param value The new value to apply.
*/
public static function setAnonymousField(obj:Any, name:String, value:Any):Void
{
return Reflect.setField(obj, name, value);
}
// public static function setAnonymousField(obj:Any, name:String, value:Any):Void
/**
* Set the value of a specific field on an object.
* Accounts for property fields with getters and setters.
*
* The actual function exists and is generated at build time.
* @param obj The object to modify.
* @param name The field to modify.
* @param value The new value to apply.
*/
public static function setProperty(obj:Any, name:String, value:Any):Void
{
return Reflect.setProperty(obj, name, value);
}
// public static function setProperty(obj:Any, name:String, value:Any):Void
/**
* This function is not allowed to be used by scripts.
*
* The actual function exists and is generated at build time.
* @throws error When called by a script.
*/
// public static function createEmptyInstance(cls:Class<Any>):Any
/**
* This function is not allowed to be used by scripts.
*
* The actual function exists and is generated at build time.
* @throws error When called by a script.
*/
// public static function createInstance(cls:Class<Any>, args:Array<Any>):Any
/**
* This function is not allowed to be used by scripts.
* @throws error When called by a script.
*/
@SuppressWarnings("checkstyle:FieldDocComment")
public static function createEmptyInstance(cls:Class<Any>):Any
{
throw "Function Type.createEmptyInstance is blacklisted.";
}
// public static function resolveEnum(name:String):Enum<Any>
/**
* This function is not allowed to be used by scripts.
* @throws error When called by a script.
*/
@SuppressWarnings("checkstyle:FieldDocComment")
public static function createInstance(cls:Class<Any>, args:Array<Any>):Any
{
throw "Function Type.createInstance is blacklisted.";
}
/**
* This function is not allowed to be used by scripts.
* @throws error When called by a script.
*/
@SuppressWarnings("checkstyle:FieldDocComment")
public static function resolveClass(name:String):Class<Any>
{
throw "Function Type.resolveClass is blacklisted.";
}
/**
* This function is not allowed to be used by scripts.
* @throws error When called by a script.
*/
@SuppressWarnings("checkstyle:FieldDocComment")
public static function resolveEnum(name:String):Enum<Any>
{
throw "Function Type.resolveEnum is blacklisted.";
}
/**
* This function is not allowed to be used by scripts.
* @throws error When called by a script.
*/
@SuppressWarnings("checkstyle:FieldDocComment")
public static function typeof(value:Any):ValueType
{
throw "Function Type.typeof is blacklisted.";
}
// public static function typeof(value:Any):ValueType
/**
* Get a list of the static class fields on the given class.
*
* The actual function exists and is generated at build time.
* @param cls The class object to query.
* @return A list of class field names.
*/
public static function getClassFields(cls:Class<Any>):Array<String>
{
return Type.getClassFields(cls);
}
// public static function getClassFields(cls:Class<Any>):Array<String>
/**
* Get a list of the static class fields on the class of the given object.
@ -371,13 +331,12 @@ class ReflectUtil
/**
* Get a list of all the fields on instances of the given class.
*
* The actual function exists and is generated at build time.
* @param cls The class object to query.
* @return A list of object field names.
*/
public static function getInstanceFields(cls:Class<Any>):Array<String>
{
return Type.getInstanceFields(cls);
}
// public static function getInstanceFields(cls:Class<Any>):Array<String>
/**
* Get a list of all the fields on instances of the class of the given object.
@ -395,13 +354,12 @@ class ReflectUtil
/**
* Get the string name of the given class.
*
* The actual function exists and is generated at build time.
* @param cls The class to query.
* @return The name of the given class.
*/
public static function getClassName(cls:Class<Any>):String
{
return Type.getClassName(cls);
}
// public static function getClassName(cls:Class<Any>):String
/**
* Get the string name of the class of the given object.

View file

@ -0,0 +1,295 @@
package funkin.util.macro;
#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Expr.Field;
import haxe.macro.Type;
import haxe.DynamicAccess;
using haxe.macro.TypeTools;
using haxe.macro.ComplexTypeTools;
using Lambda;
using StringTools;
enum abstract WrapMode(String) from String to String
{
var Blacklist;
var Whitelist;
}
typedef WrapperParams =
{
/**
* Classes to generate functions for.
*/
var classes:Array<String>;
/**
* Aliases to functions, for instance `{ "getAnonymousField": ["field"] }` generates a `field` function that calls `getAnonymousField`.
* It works in both ways, so you can generate aliases in the class that point to the ones in `classes` and vice-versa.
* This should be strictly structured as `{ fieldName: Array<String> }`, failure to comply will result in undefined behaviour.
*/
@:optional
var aliases:{};
/**
* Functions that should be wrapped based on the value of `customWrapMode`.
* If a function is part of an alias or the ignore list, this will have no effect on it.
*/
@:optional
var customWrapList:Array<String>;
/**
* Defines how fields inside and outside of `customWrapList` are wrapped.
* If it's `Whitelist` fields outside the list are blacklisted by default.
*
* @default `Whitelist`
*/
@:optional
var customWrapMode:WrapMode;
/**
* Functions in `classes` that the macro should not generate.
* If a function is part of an alias, this will have no effect on it.
*/
@:optional
var ignoreList:Array<String>;
}
/**
* Generates fields that wrap functions from the provided classes in a way that
* they'll throw an error if accessed, or call the original function if whitelisted.
* It is best to be used with classes with only static fields. Private fields and variables are always ignored.
*
* You can add your own sandboxed implementations of the fields and make aliases to them (see `BlacklistParams.aliases`).
* Note that if the field already exists in `BlacklistParams.classes` you should add `@:blacklistOverride` to it.
*/
class BlacklistClassMacro
{
/**
* Documentation used by blacklisted functions.
*/
static final BLACKLISTED_FUNCTION_DOC:String = "This function is not allowed to be used by scripts.\n@throws error When called by a script.";
static var buildFields:Array<Field>;
static var processedFieldNames:Array<String> = [];
static inline function containsField(fieldName:String):Bool
{
return buildFields.exists(f -> f.name == fieldName);
}
static inline function getField(fieldName:String):Null<Field>
{
return buildFields.find(f -> f.name == fieldName);
}
static function build(params:WrapperParams):Array<Field>
{
final classes:Array<ClassType> = [for (c in params.classes) MacroUtil.getClassType(c)];
if (classes.length == 0) Context.fatalError('Invalid class amount, no classes were provided.', Context.currentPos());
buildFields = Context.getBuildFields();
var generatedFields:Array<Field> = [];
params.customWrapList ??= [];
params.customWrapMode ??= Whitelist;
// NOTE: As much as I wish these could be a map seems like Haxe is unable to parse them as part of the metadata.
final aliases:DynamicAccess<Array<String>> = cast params.aliases;
var fieldsToSkip:Array<String> = params.ignoreList?.copy() ?? [];
var pendingFieldsToWrap:Array<String> = [];
if (aliases != null)
{
generatedFields = generateAliases(aliases, pendingFieldsToWrap);
}
for (c in classes)
{
for (field in c.statics.get())
{
if (!field.isPublic || fieldsToSkip.contains(field.name) || ~/^(get|set)_/.match(field.name)) continue;
if (containsField(field.name))
{
if (!getField(field.name).meta.exists(m -> m.name == ':blacklistOverride'))
{
// 'reportError' doesn't abort compilation, so it allows us to see all the duplicate fields!
Context.reportError('Tried to generate "${field.name}" but it already exists in the class.\n'
+ 'Add @:blacklistOverride or add it to "ignoreList" to ignore.',
getField(field.name).pos);
}
continue;
}
final blacklisted:Bool = (params.customWrapMode == Whitelist) != params.customWrapList.contains(field.name);
final wrapper:Null<Field> = generateWrapperField(field.name, field, c.name, blacklisted);
if (wrapper == null) continue; // Not a function
generatedFields.push(wrapper);
// TODO: When this happens should it make the field whitelisted (or vice-versa)?
if (pendingFieldsToWrap.contains(field.name))
{
for (alias in aliases.get(field.name))
{
generatedFields.push(generateWrapperField(alias, wrapper));
pendingFieldsToWrap.remove(field.name);
}
}
}
}
for (f in pendingFieldsToWrap)
{
Context.reportError('Tried to generate alias fields for "$f" but it does not exist.', Context.currentPos());
}
return buildFields.concat(generatedFields);
}
static function generateAliases(aliases:DynamicAccess<Array<String>>, ?unresolvedAliases:Array<String>):Array<Field>
{
var result:Array<Field> = [];
for (field => aliasFields in aliases)
{
if (aliasFields.length == 0) Context.warning('No alias fields specified to be generated for "$field"', Context.currentPos());
final wrappedField:Null<Field> = getField(field);
if (wrappedField == null && unresolvedAliases != null)
{
// Field might be on the provided classes, put it on queue.
unresolvedAliases.push(field);
continue;
}
for (aliasName in aliasFields)
{
if (containsField(aliasName))
{
Context.error('Tried to generate "${aliasName}" alias but it already exists in the class.', getField(aliasName).pos);
}
final wrapper:Null<Field> = generateWrapperField(aliasName, wrappedField);
if (wrapper != null)
{
result.push(wrapper);
processedFieldNames.push(aliasName);
}
else
{
Context.error('Could not generate alias for field "$field"; it may not be a function.', wrappedField.pos);
}
}
}
return result;
}
static function generateWrapperField(fieldName:String, wrappedField:Dynamic, ?className:String, blacklist:Bool = false):Null<Field>
{
final pack:Array<String> = [wrappedField.name];
if (className != null) pack.unshift(className);
function getWrapperExpr(args:Array<{name:String}>, ?retType:ComplexType):Expr
{
return if (blacklist)
{
macro throw $v{'Function ${pack.join('.')} is blacklisted.'};
}
else
{
final params:Array<Expr> = [for (a in args) macro $i{a.name}];
retType.toString() == 'StdTypes.Void' ? macro $p{pack}($a{params}) : macro return $p{pack}($a{params});
}
}
var wrapperKind:Null<FieldType>;
if (wrappedField.kind is FieldType)
{
wrapperKind = switch (wrappedField.kind)
{
case FFun(f):
final wrapFunc:Function = Reflect.copy(f);
wrapFunc.expr = getWrapperExpr(wrapFunc.args, wrapFunc.ret);
FFun(wrapFunc);
default:
Context.error('Blacklist Macro: Making wrappers for anything other than functions is not supported.', wrappedField.pos);
}
}
else if (wrappedField.expr() != null)
{
switch (wrappedField.expr().expr)
{
case TFunction(tfunc):
final args:Array<FunctionArg> = [
for (a in tfunc.args)
{
name: a.v.name,
value: a.value != null ? Context.getTypedExpr(a.value) : null,
type: a.v.t.toComplexType()
}
];
wrapperKind = FFun(
{
args: args,
params: getParamDecls(wrappedField.params),
ret: tfunc.t.toComplexType(),
expr: getWrapperExpr(args, tfunc.t.toComplexType()),
});
default:
return null;
}
}
else
{
// Some targets have core types with externs as functions and those don't have a TypedExpr.
// We follow its type once to get rid of any lazy type
switch (wrappedField.type.follow(true))
{
case TFun(args, ret):
wrapperKind = FFun(
{
args: [for (a in args) {name: a.name, opt: a.opt, type: a.t.toComplexType()}],
params: getParamDecls(wrappedField.params),
ret: ret.toComplexType(),
expr: getWrapperExpr(args, ret.toComplexType())
});
default:
return null;
}
}
final access = [APublic, AStatic];
if (wrapperKind.match(FFun(_)))
{
access.push(AInline);
}
return {
name: fieldName,
pos: wrappedField.pos,
doc: blacklist ? BLACKLISTED_FUNCTION_DOC : wrappedField.doc,
access: access,
kind: wrapperKind
};
}
static function getParamDecls(params:Array<TypeParameter>):Array<TypeParamDecl>
{
final result:Array<TypeParamDecl> = [];
for (p in params)
{
switch (p.t.getClass()?.kind)
{
case KTypeParameter(constraints):
result.push({name: p.name, constraints: [for (c in constraints) c.toComplexType()]});
default:
Context.error("Provided type parameters are not of the KTypeParameter kind, this shouldn't happen!", Context.currentPos());
}
}
return result;
}
}
#end