548 lines
16 KiB
Haxe
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;
|
|
}
|
|
}
|