using System.Dynamic; using System.Globalization; using System.Linq; using System.Reflection; using Jint.Native; using Jint.Native.Object; using Jint.Runtime; using Jint.Runtime.Interop; using Jint.Runtime.Debugger; using Jint.Runtime.Descriptors; using Jint.Runtime.Modules; using Jint.Runtime.CallStack; namespace Jint; public class Options { private static readonly CultureInfo _defaultCulture = CultureInfo.CurrentCulture; private static readonly TimeZoneInfo _defaultTimeZone = TimeZoneInfo.Local; private ITimeSystem? _timeSystem; internal List> _configurations { get; } = new(); public delegate JsValue? MemberAccessorDelegate(Engine engine, object target, string member); public delegate ObjectInstance? WrapObjectDelegate(Engine engine, object target, Type? type); public delegate bool ExceptionHandlerDelegate(Exception exception); /// /// Execution constraints for the engine. /// public ConstraintOptions Constraints { get; } = new(); /// /// CLR interop related options. /// public InteropOptions Interop { get; } = new(); /// /// Debugger configuration. /// public DebuggerOptions Debugger { get; } = new(); /// /// Host options. /// internal HostOptions Host { get; } = new(); /// /// Module options /// public ModuleOptions Modules { get; } = new(); /// /// Whether the code should be always considered to be in strict mode. Can improve performance. /// public bool Strict { get; set; } /// /// The culture the engine runs on, defaults to current culture. /// public CultureInfo Culture { get; set; } = _defaultCulture; /// /// Configures a time system to use. Defaults to DefaultTimeSystem using local time. /// public ITimeSystem TimeSystem { get => _timeSystem ??= new DefaultTimeSystem(TimeZone, Culture); set => _timeSystem = value; } /// /// The time zone the engine runs on, defaults to local. Same as setting DefaultTimeSystem with the time zone. /// public TimeZoneInfo TimeZone { get; set; } = _defaultTimeZone; /// /// Reference resolver allows customizing behavior for reference resolving. This can be useful in cases where /// you want to ignore long chain of property accesses that might throw if anything is null or undefined. /// An example of such is var a = obj.field.subField.value. Custom resolver could accept chain to return /// null/undefined on first occurrence. /// public IReferenceResolver ReferenceResolver { get; set; } = DefaultReferenceResolver.Instance; /// /// Whether calling 'eval' with custom code and function constructors taking function code as string is allowed. /// Defaults to true. /// /// /// https://tc39.es/ecma262/#sec-hostensurecancompilestrings /// public bool StringCompilationAllowed { get; set; } = true; /// /// Options for the built-in JSON (de)serializer which /// gets used using JSON.parse or JSON.stringify /// public JsonOptions Json { get; set; } = new(); /// /// What experimental features are allowed, functionality may lacking or even plain wrong. Defaults to having none. /// public ExperimentalFeature ExperimentalFeatures { get; set; } /// /// Called by the instance that loads this /// once it is loaded. /// internal void Apply(Engine engine) { foreach (var configuration in _configurations) { configuration(engine); } // add missing bits if needed if (Interop.Enabled) { #pragma warning disable IL2026 engine.Realm.GlobalObject.SetProperty("System", new PropertyDescriptor(new NamespaceReference(engine, "System"), PropertyFlag.AllForbidden)); engine.Realm.GlobalObject.SetProperty("importNamespace", new PropertyDescriptor(new ClrFunction( engine, "importNamespace", (_, arguments) => new NamespaceReference(engine, arguments.At(0).IsNullOrUndefined() ? null : TypeConverter.ToString(arguments.At(0)))), PropertyFlag.AllForbidden)); engine.Realm.GlobalObject.SetProperty("clrHelper", new PropertyDescriptor(ObjectWrapper.Create(engine, new ClrHelper(Interop)), PropertyFlag.AllForbidden)); #pragma warning restore IL2026 } if (Interop.ExtensionMethodTypes.Count > 0) { AttachExtensionMethodsToPrototypes(engine); } if (Modules.RegisterRequire) { // Node js like loading of modules engine.Realm.GlobalObject.SetProperty("require", new PropertyDescriptor(new ClrFunction( engine, "require", (thisObj, arguments) => { var specifier = TypeConverter.ToString(arguments.At(0)); return engine.Modules.Import(specifier); }), PropertyFlag.AllForbidden)); } engine.Modules = new Engine.ModuleOperations(engine, Modules.ModuleLoader); } private static void AttachExtensionMethodsToPrototypes(Engine engine) { AttachExtensionMethodsToPrototype(engine, engine.Realm.Intrinsics.Array.PrototypeObject, typeof(Array)); AttachExtensionMethodsToPrototype(engine, engine.Realm.Intrinsics.Boolean.PrototypeObject, typeof(bool)); AttachExtensionMethodsToPrototype(engine, engine.Realm.Intrinsics.Date.PrototypeObject, typeof(DateTime)); AttachExtensionMethodsToPrototype(engine, engine.Realm.Intrinsics.Number.PrototypeObject, typeof(double)); AttachExtensionMethodsToPrototype(engine, engine.Realm.Intrinsics.Object.PrototypeObject, typeof(ExpandoObject)); AttachExtensionMethodsToPrototype(engine, engine.Realm.Intrinsics.RegExp.PrototypeObject, typeof(System.Text.RegularExpressions.Regex)); AttachExtensionMethodsToPrototype(engine, engine.Realm.Intrinsics.String.PrototypeObject, typeof(string)); } private static void AttachExtensionMethodsToPrototype(Engine engine, ObjectInstance prototype, Type objectType) { if (!engine._extensionMethods.TryGetExtensionMethods(objectType, out var methods)) { return; } foreach (var overloads in methods.GroupBy(x => x.Name, StringComparer.Ordinal)) { PropertyDescriptor CreateMethodInstancePropertyDescriptor(ClrFunction? function) { var instance = new MethodInfoFunction( engine, objectType, target: null, overloads.Key, methods: MethodDescriptor.Build(overloads.ToList()), function); return new PropertyDescriptor(instance, PropertyFlag.AllForbidden); } JsValue key = overloads.Key; PropertyDescriptor? descriptorWithFallback = null; PropertyDescriptor? descriptorWithoutFallback = null; if (prototype.HasOwnProperty(key) && prototype.GetOwnProperty(key).Value is ClrFunction clrFunctionInstance) { descriptorWithFallback = CreateMethodInstancePropertyDescriptor(clrFunctionInstance); prototype.SetOwnProperty(key, descriptorWithFallback); } else { descriptorWithoutFallback = CreateMethodInstancePropertyDescriptor(null); prototype.SetOwnProperty(key, descriptorWithoutFallback); } // make sure we register both lower case and upper case if (char.IsUpper(overloads.Key[0])) { key = char.ToLower(overloads.Key[0], CultureInfo.InvariantCulture) + overloads.Key.Substring(1); if (prototype.HasOwnProperty(key) && prototype.GetOwnProperty(key).Value is ClrFunction lowerclrFunctionInstance) { descriptorWithFallback ??= CreateMethodInstancePropertyDescriptor(lowerclrFunctionInstance); prototype.SetOwnProperty(key, descriptorWithFallback); } else { descriptorWithoutFallback ??= CreateMethodInstancePropertyDescriptor(null); prototype.SetOwnProperty(key, descriptorWithoutFallback); } } } } public class DebuggerOptions { /// /// Whether debugger functionality is enabled, defaults to false. /// public bool Enabled { get; set; } /// /// Configures the statement handling strategy, defaults to Ignore. /// public DebuggerStatementHandling StatementHandling { get; set; } = DebuggerStatementHandling.Ignore; /// /// Configures the step mode used when entering the script. /// public StepMode InitialStepMode { get; set; } = StepMode.None; } public class InteropOptions { /// /// Whether accessing CLR and it's types and methods is allowed from JS code, defaults to false. /// public bool Enabled { get; set; } /// /// Whether to expose which can allow bypassing allow lists and open a way to reflection. /// Defaults to false. /// public bool AllowGetType { get; set; } /// /// Whether Jint should allow wrapping objects from System.Reflection namespace. /// Defaults to false. /// public bool AllowSystemReflection { get; set; } /// /// Whether writing to CLR objects is allowed (set properties), defaults to true. /// public bool AllowWrite { get; set; } = true; /// /// Whether operator overloading resolution is allowed, defaults to false. /// public bool AllowOperatorOverloading { get; set; } /// /// Types holding extension methods that should be considered when resolving methods. /// public List ExtensionMethodTypes { get; } = new(); /// /// Object converters to try when build-in conversions. /// public List ObjectConverters { get; } = new(); /// /// Whether identity map is persisted for object wrappers in order to maintain object identity. This can cause /// memory usage to grow when targeting large set and freeing of memory can be delayed due to ConditionalWeakTable semantics. /// Defaults to false. /// public bool TrackObjectWrapperIdentity { get; set; } /// /// If no known type could be guessed, objects are by default wrapped as an /// ObjectInstance using class ObjectWrapper. This function can be used to /// change the behavior. /// public WrapObjectDelegate WrapObjectHandler { get; set; } = static (engine, target, type) => ObjectWrapper.Create(engine, target, type); /// /// /// public MemberAccessorDelegate MemberAccessor { get; set; } = static (engine, target, member) => null; /// /// Exceptions that thrown from CLR code are converted to JavaScript errors and /// can be used in at try/catch statement. By default these exceptions are bubbled /// to the CLR host and interrupt the script execution. If handler returns true these exceptions are converted /// to JS errors that can be caught by the script. /// public ExceptionHandlerDelegate ExceptionHandler { get; set; } = _defaultExceptionHandler; /// /// Assemblies to allow scripts to call CLR types directly like System.IO.File. /// public List AllowedAssemblies { get; set; } = new(); /// /// Type and member resolving strategy, which allows filtering allowed members and configuring member /// name matching comparison. /// /// /// As this object holds caching state same instance should be shared between engines, if possible. /// public TypeResolver TypeResolver { get; set; } = TypeResolver.Default; /// /// When writing values to CLR objects, how should JS values be coerced to CLR types. /// Defaults to only coercing to string values when writing to string targets. /// public ValueCoercionType ValueCoercion { get; set; } = ValueCoercionType.String; /// /// Strategy to create a CLR object to hold converted . /// public Func>? CreateClrObject = _ => new ExpandoObject(); /// /// Strategy to create a CLR object from TypeReference. /// Defaults to retuning null which makes TypeReference attempt to find suitable constructor. /// public Func CreateTypeReferenceObject = (_, _, _) => null; internal static readonly ExceptionHandlerDelegate _defaultExceptionHandler = static exception => false; /// /// When not null, is used to serialize any CLR object in an /// passing through 'JSON.stringify'. /// public Func? SerializeToJson { get; set; } /// /// What kind of date time should be produced when JavaScript date is converted to DateTime. If Local, uses . /// Defaults to . /// public DateTimeKind DateTimeKind { get; set; } = DateTimeKind.Utc; /// /// Should the Array prototype be attached instead of Object prototype to the wrapped interop objects when type looks suitable. Defaults to true. /// public bool AttachArrayPrototype { get; set; } = true; /// /// Whether the engine should throw an error when a member is not found on a CLR object. Defaults to false. /// public bool ThrowOnUnresolvedMember { get; set; } /// /// Types of CLR members reported by when enumerating properties/serializing . /// Supported values are: , , . /// All other values are ignored. /// public MemberTypes ObjectWrapperReportedMemberTypes { get; set; } = MemberTypes.Field | MemberTypes.Property | MemberTypes.Method; } public class ConstraintOptions { /// /// Registered constraints. /// public List Constraints { get; } = new(); /// /// Maximum recursion depth allowed, defaults to -1 (no checks). /// public int MaxRecursionDepth { get; set; } = -1; /// /// Maximum recursion stack count, defaults to -1 (as-is dotnet stacktrace). /// /// /// Chrome and V8 based engines (ClearScript) that can handle 13955. /// When set to a different value except -1, it can reduce slight performance/stack trace readability drawback. (after hitting the engine's own limit), /// When max stack size to be exceeded, Engine throws an exception . /// public int MaxExecutionStackCount { get; set; } = StackGuard.Disabled; /// /// Maximum time a Regex is allowed to run, defaults to 10 seconds. /// public TimeSpan RegexTimeout { get; set; } = TimeSpan.FromSeconds(10); /// /// The maximum size for JavaScript array, defaults to . /// public uint MaxArraySize { get; set; } = uint.MaxValue; } /// /// Host related customization, still work in progress. /// public class HostOptions { internal Func Factory { get; set; } = _ => new Host(); } /// /// Module related customization /// public class ModuleOptions { /// /// Whether to register require function to engine which will delegate to module loader, defaults to false. /// public bool RegisterRequire { get; set; } /// /// Module loader implementation, by default exception will be thrown if module loading is not enabled. /// public IModuleLoader ModuleLoader { get; set; } = FailFastModuleLoader.Instance; } /// /// JSON.parse / JSON.stringify related customization /// public class JsonOptions { /// /// The maximum depth allowed when parsing JSON files using "JSON.parse", /// defaults to 64. /// public int MaxParseDepth { get; set; } = 64; } } /// /// Rules for writing values to CLR fields. /// [Flags] public enum ValueCoercionType { /// /// No coercion will be done. If there's no type converter, and error will be thrown. /// None = 0, /// /// JS coercion using boolean rules "dog" == true, "" == false, 1 == true, 3 == true, 0 == false, { "prop": 1 } == true etc. /// Boolean = 1, /// /// JS coercion to numbers, false == 0, true == 1. valueOf functions will be used when available for object instances. /// Valid against targets of type: Decimal, Double, Int32, Int64. /// Number = 2, /// /// JS coercion to strings, toString function will be used when available for objects. /// String = 4, /// /// All coercion rules enabled. /// All = Boolean | Number | String } /// /// Features that only work partially, if all. /// [Flags] public enum ExperimentalFeature { /// /// No experimental features enabled. /// None = 0, /// /// Generator support /// Generators = 1, /// /// Wrapping tasks to promises /// TaskInterop = 2, /// /// All coercion rules enabled. /// All = Generators | TaskInterop }