Files
Funkin/source/funkin/util/ReflectUtil.hx
T

548 lines
16 KiB
Haxe

package funkin.util;
import Type.ValueType;
import polymod.hscript._internal.PolymodScriptClass;
/**
* Provides sanitized and blacklisted access to haxe's Reflection functions.
* Used for sandboxing in scripts.
*/
@:nullSafety @SuppressWarnings(['checkstyle:VarTypeHint', 'checkstyle:FieldDocComment'])
class ReflectUtil
{
/**
* Calls a method value of a specific field on an object.
* Accounts for property blacklist for security.
* @param obj The object to modify.
* @param name The field to modify.
* @param value The new value to apply.
* @throws error When trying to call a blacklisted method.
*/
public static function callMethod(obj:Any, name:String, args:Array<Any>):Any
{
if (!isAccessAllowed(obj, name))
{
throw 'Attempted to call blacklisted method "${name}"';
}
return Reflect.callMethod(getReflectionParent(obj), Reflect.field(getReflectionParent(obj), name), args);
}
/**
* Compares two objects by value.
*
* @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);
}
/**
* Compares two values and returns an integer indicating their relative order.
* Returns:
* - -1 if valueA < valueB
* - 0 if valueA == valueB
* - 1 if valueA > valueB
*
* @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);
}
/**
* 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);
}
/**
* Copies the given object.
* Only guaranteed to work on anonymous structures.
* @param obj The object to copy.
* @return An independent clone of that object.
*/
public static function copy(obj:Any):Null<Any>
{
return copyAnonymousFieldsOf(obj);
}
/**
* Copies the anonymous structure to a new object.
* @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);
}
/**
* Delete the field of a given name from an object.
* Only guaranteed to work on anonymous structures.
* @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);
}
/**
* Delete the field of a given name from an anonymous structure.
* Only guaranteed to work on anonymous structures.
* @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
{
if (!isAccessAllowed(obj, name))
{
throw 'Attempted to delete blacklisted field "${name}"';
};
return Reflect.deleteField(obj, name);
}
/**
* Retrive the value of a given field (by name) from an object.
* Only guaranteed to work on anonymous structures.
* @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);
}
/**
* Retrive the value of a given field (by name) from an object.
* Only guaranteed to work on anonymous structures.
* @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);
}
/**
* Retrieve the value of the field of the given name from an anonymous structure.
* @param obj The object to query.
* @param name The name of the field to retrieve.
* @return The resulting field value.
* @throws error If the field is blacklisted.
*/
public static function getAnonymousField(obj:Any, name:String):Any
{
if (!isAccessAllowed(obj, name))
{
throw 'Attempted to retrieve blacklisted field "${name}"';
};
return Reflect.field(getReflectionParent(obj), name);
}
/**
* Get a list of fields available on the given object.
* Only guaranteed to work on anonymous structures.
* @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);
}
/**
* Get a list of fields available on the given object.
* Only guaranteed to work on anonymous structures.
* @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);
}
/**
* Get a list of fields available on the given anonymous structure.
* @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);
}
/**
* Get the value of the given property on a given object.
* Unlike `getField()`, this will check if the field is a property with a getter function,
* and use that if appropriate.
* @param obj The object to query.
* @param name The name of the field to query.
* @return The value of the field.
* @throws error If the field is blacklisted.
*/
public static function getProperty(obj:Any, name:String):Any
{
if (!isAccessAllowed(obj, name))
{
throw 'Attempted to retrieve blacklisted field "${name}"';
};
return Reflect.getProperty(getReflectionParent(obj), name);
}
/**
* Determine whether the given object has the given field.
* Only guaranteed to work for anonymous structures.
* @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);
}
/**
* Determine whether the given anonymous structure has the given field.
* @param obj The structure to query.
* @param name The field name to query.
* @return Whether the field exists and isnt blacklisted.
*/
public static function hasAnonymousField(obj:Any, name:String):Bool
{
if (!isAccessAllowed(obj, name))
{
return false;
}
return Reflect.hasField(getReflectionParent(obj), name);
}
/**
* Determine whether the given input is an enum value.
* @param value The input to evaluate.
* @return Whether `value` is an enum value.
*/
public static function isEnumValue(value:Any):Bool
{
return Reflect.isEnumValue(value);
}
/**
* Determine whether the given input is a callable function.
* @param value The input to evaluate.
* @return Whether `value` is a function.
*/
public static function isFunction(value:Any):Bool
{
return Reflect.isFunction(value);
}
/**
* Determine whether the given input is an object.
* @param value The input to evaluate.
* @return Whether `value` is an object.
*/
public static function isObject(value:Any):Bool
{
return Reflect.isObject(value);
}
/**
* Set the value of a specific field on an object.
* Only guaranteed to work for anonymous structures.
* @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);
}
/**
* Set the value of a specific field on an anonymous structure.
* @param obj The object to modify.
* @param name The field to modify.
* @param value The new value to apply.
* @throws error When trying to set a blacklisted field.
*/
public static function setAnonymousField(obj:Any, name:String, value:Any):Void
{
if (!isAccessAllowed(obj, name))
{
throw 'Attempted to set blacklisted field "${name}"';
};
return Reflect.setField(getReflectionParent(obj), name, value);
}
/**
* Set the value of a specific field on an object.
* Accounts for property fields with getters and setters.
*
* @param obj The object to modify.
* @param name The field to modify.
* @param value The new value to apply.
* @throws error When trying to set a blacklisted field.
*/
public static function setProperty(obj:Any, name:String, value:Any):Void
{
if (!isAccessAllowed(obj, name))
{
throw 'Attempted to set blacklisted field "${name}"';
};
return Reflect.setProperty(getReflectionParent(obj), name, value);
}
/**
* Creats a empty instance of specified class
* Accounts for property blacklist for security.
* @param cls The class to create.
* @throws error When trying to create a blacklisted class.
*/
public static function createEmptyInstance(cls:Class<Any>):Any
{
if (!isAccessAllowed(cls))
{
throw 'Attempted to call createInstance onto a blacklisted class "${getClassNameOf(cls)}"';
}
return Type.createEmptyInstance(getReflectionParent(cls));
}
/**
* Creats a instance of specified class
* Accounts for property blacklist for security.
* @param cls The class to create.
* @param args Parameters to give to the constructor
* @throws error When trying to create a blacklisted class.
*/
public static function createInstance(cls:Class<Any>, args:Array<Any>):Any
{
if (!isAccessAllowed(cls))
{
throw 'Attempted to call createInstance onto a blacklisted class "${getClassNameOf(cls)}"';
}
return Type.createInstance(getReflectionParent(cls), args);
}
/**
* This function is not allowed to be used by scripts.
* @throws error When called by a script.
*/
public static function resolveClass(name:String):Null<Class<Any>>
{
var resolved = getReflectionParent(getClassOrEnumFromName(name));
if (resolved == null) return null;
if (!Std.isOfType(resolved, Class)) return null;
if (!isAccessAllowed(resolved))
{
throw 'Attempted to resolve a blacklisted class "${getClassNameOf(getClassOrEnumFromName(name))}"';
}
return resolved;
}
/**
* 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):Null<Enum<Any>>
{
var resolved = getReflectionParent(getClassOrEnumFromName(name));
if (resolved == null) return null;
if (!Std.isOfType(resolved, Enum)) return null;
if (!isAccessAllowed(resolved))
{
throw 'Attempted to resolve a blacklisted enum "${getClassNameOf(getClassOrEnumFromName(name))}"';
}
return resolved;
}
/**
* This function is not allowed to be used by scripts.
* @throws error When called by a script.
*/
public static function typeof(value:Any):ValueType
{
throw 'Function Type.typeof is blacklisted.';
}
/**
* Get a list of the static class fields on the given class.
* @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);
}
/**
* Get a list of the static class fields on the class of the given object.
* @param obj The object whose class should be queried.
* @return A list of class field names.
*/
public static function getClassFieldsOf(obj:Any):Array<String>
{
if (obj == null) return [];
@:nullSafety(Off)
var cls = Type.getClass(obj);
if (cls == null) return [];
return Type.getClassFields(cls);
}
/**
* Get a list of all the fields on instances of the given class.
* @param cls The class object to query.
* @return A list of object field names.
*/
public static function getInstanceFields(cls:Class<Any>):Array<String>
{
if (cls == null) return [];
return Type.getInstanceFields(cls);
}
/**
* Get a list of all the fields on instances of the class of the given object.
* @param obj The object whose class should be query.
* @return A list of object field names.
*/
public static function getInstanceFieldsOf(obj:Any):Array<String>
{
if (obj == null) return [];
@:nullSafety(Off)
var cls = Type.getClass(obj);
if (cls == null) return [];
return Type.getInstanceFields(cls);
}
/**
* Get the string name of the given class.
* @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);
}
/**
* Get the string name of the class of the given object.
* @param obj The object to query.
* @return The name of the given class, or `Unknown` if the class couldn't be determined.
*/
public static function getClassNameOf(obj:Any):String
{
if (obj == null) return 'Unknown';
if (obj is String && Type.resolveClass(obj) != null) return obj;
@:nullSafety(Off)
var cls = Type.getClass(obj);
if (cls == null) return 'Unknown';
return Type.getClassName(cls);
}
/**
* Transform a function taking an array of arguments into a function that can
* be called with any number of arguments.
*
* @param f A function which takes an array of arguments.
* @return A new function that takes any number of arguments and passes them as an array to `f`.
*/
public static function makeVarArgs(f:Array<Dynamic>->Dynamic):Dynamic
{
return Reflect.makeVarArgs(f);
}
@:unreflective
static var pathResolveCache:Map<String, Any> = new Map();
@:unreflective
static var blacklistedFieldsCache:Map<String, Any> = new Map();
@:unreflective @:nullSafety(Off)
static function getClassOrEnumFromName(path:String):Any
{
if (pathResolveCache.exists(path)) return pathResolveCache.get(path);
var resultCls:Class<Dynamic> = Type.resolveClass(path);
if (resultCls != null)
{
pathResolveCache.set(path, resultCls);
return resultCls;
}
else
{
var resultEnm:Enum<Dynamic> = Type.resolveEnum(path);
if (resultEnm != null) pathResolveCache.set(path, resultEnm);
return resultEnm;
}
}
@:unreflective @:nullSafety(Off)
static function getReflectionParent(obj:Dynamic):Dynamic
{
var clsName = getClassNameOf(obj);
if (clsName == 'Unknown') clsName = null;
var objName = obj is String ? obj : (clsName ?? obj);
return PolymodScriptClass.importOverrides.exists(objName) ? PolymodScriptClass.importOverrides.get(objName) : obj;
}
@:unreflective @:nullSafety(Off)
static function isAccessAllowed(obj:Dynamic, ?varName:String):Bool
{
var reflectedObj = getReflectionParent(obj);
var isClassReflect = Std.isOfType(reflectedObj, Class);
var key:String;
if (isClassReflect)
{
key = Type.getClassName(cast reflectedObj);
}
else
{
key = getClassNameOf(reflectedObj);
}
if (varName != null)
{
var cacheKey = '${isClassReflect ? "C_" : "I_"}$key';
var blacklistedFields:Array<String> = blacklistedFieldsCache.get(cacheKey);
if (blacklistedFields == null)
{
if (isClassReflect)
{
blacklistedFields = PolymodScriptClass.blacklistedStaticFields.get(cast reflectedObj);
}
else
{
blacklistedFields = PolymodScriptClass.blacklistedInstanceFields.get(key);
}
if (blacklistedFields == null) blacklistedFields = [];
blacklistedFieldsCache.set(cacheKey, blacklistedFields);
}
if (blacklistedFields.contains(varName)) return false;
}
if (PolymodScriptClass.importOverrides.exists(key) && PolymodScriptClass.importOverrides.get(key) == null) return false;
return true;
}
}