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 { 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 { 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 { 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 { 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 { 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 { 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 { 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, args:Array):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> { 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> { 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):Array { 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 { 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):Array { 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 { 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):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 { return Reflect.makeVarArgs(f); } @:unreflective static var pathResolveCache:Map = new Map(); @:unreflective static var blacklistedFieldsCache:Map = new Map(); @:unreflective @:nullSafety(Off) static function getClassOrEnumFromName(path:String):Any { if (pathResolveCache.exists(path)) return pathResolveCache.get(path); var resultCls:Class = Type.resolveClass(path); if (resultCls != null) { pathResolveCache.set(path, resultCls); return resultCls; } else { var resultEnm:Enum = 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 = 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; } }