using System; using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using Esprima.Ast; using Jint.Native; using Jint.Native.Number; using Jint.Native.Number.Dtoa; using Jint.Native.Object; using Jint.Native.String; using Jint.Pooling; using Jint.Runtime.References; namespace Jint.Runtime { public enum Types { None = 0, Undefined = 1, Null = 2, Boolean = 3, String = 4, Number = 5, Symbol = 9, Object = 10, Completion = 20, } public static class TypeConverter { // how many decimals to check when determining if double is actually an int private const double DoubleIsIntegerTolerance = double.Epsilon * 100; private static readonly string[] intToString = new string[1024]; private static readonly string[] charToString = new string[256]; static TypeConverter() { for (var i = 0; i < intToString.Length; ++i) { intToString[i] = i.ToString(); } for (var i = 0; i < charToString.Length; ++i) { var c = (char) i; charToString[i] = c.ToString(); } } /// /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.1 /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static JsValue ToPrimitive(JsValue input, Types preferredType = Types.None) { if (input._type > Types.None && input._type < Types.Object) { return input; } return input.AsObject().DefaultValue(preferredType); } /// /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.2 /// public static bool ToBoolean(JsValue o) { switch (o._type) { case Types.Boolean: return ((JsBoolean) o)._value; case Types.Undefined: case Types.Null: return false; case Types.Number: var n = ((JsNumber) o)._value; return n != 0 && !double.IsNaN(n); case Types.String: return !((JsString) o).IsNullOrEmpty(); default: return true; } } /// /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.3 /// public static double ToNumber(JsValue o) { switch (o._type) { // check number first as this is what is usually expected case Types.Number: return ((JsNumber) o)._value; case Types.Undefined: return double.NaN; case Types.Null: return 0; case Types.Object when o is IPrimitiveInstance p: return ToNumber(ToPrimitive(p.PrimitiveValue, Types.Number)); case Types.Boolean: return ((JsBoolean) o)._value ? 1 : 0; case Types.String: return ToNumber(o.AsStringWithoutTypeCheck()); case Types.Symbol: // TODO proper TypeError would require Engine instance and a lot of API changes return ExceptionHelper.ThrowTypeErrorNoEngine("Cannot convert a Symbol value to a number"); default: return ToNumber(ToPrimitive(o, Types.Number)); } } internal static bool CanBeIndex(string input) { if (string.IsNullOrEmpty(input)) { return false; } char first = input[0]; if (first < 32 || (first > 57 && first != 73)) { // does not start with space, +, -, number or I return false; } // might be return true; } private static double ToNumber(string input) { // eager checks to save time and trimming if (string.IsNullOrEmpty(input)) { return 0; } char first = input[0]; if (input.Length == 1 && first >= '0' && first <= '9') { // simple constant number return first - '0'; } var s = StringPrototype.IsWhiteSpaceEx(input[0]) || StringPrototype.IsWhiteSpaceEx(input[input.Length - 1]) ? StringPrototype.TrimEx(input) : input; if (s.Length == 0) { return 0; } if (s.Length == 8 || s.Length == 9) { if ("+Infinity" == s || "Infinity" == s) { return double.PositiveInfinity; } if ("-Infinity" == s) { return double.NegativeInfinity; } } // todo: use a common implementation with JavascriptParser try { if (s.Length > 2 && s[0] == '0' && char.IsLetter(s[1])) { int fromBase = 0; if (s[1] == 'x' || s[1] == 'X') { fromBase = 16; } if (s[1] == 'o' || s[1] == 'O') { fromBase = 8; } if (s[1] == 'b' || s[1] == 'B') { fromBase = 2; } if (fromBase > 0) { return Convert.ToInt32(s.Substring(2), fromBase); } } var start = s[0]; if (start != '+' && start != '-' && start != '.' && !char.IsDigit(start)) { return double.NaN; } double n = double.Parse(s, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign | NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite | NumberStyles.AllowExponent, CultureInfo.InvariantCulture); if (s.StartsWith("-") && n == 0) { return -0.0; } return n; } catch (OverflowException) { return s.StartsWith("-") ? double.NegativeInfinity : double.PositiveInfinity; } catch { return double.NaN; } } /// /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.4 /// /// /// public static double ToInteger(JsValue o) { var number = ToNumber(o); if (double.IsNaN(number)) { return 0; } if (number == 0 || double.IsInfinity(number)) { return number; } return (long) number; } internal static double ToInteger(string o) { var number = ToNumber(o); if (double.IsNaN(number)) { return 0; } if (number == 0 || double.IsInfinity(number)) { return number; } return (long) number; } /// /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.5 /// /// /// public static int ToInt32(JsValue o) { return (int) (uint) ToNumber(o); } /// /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.6 /// /// /// public static uint ToUint32(JsValue o) { return (uint) ToNumber(o); } /// /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.7 /// /// /// public static ushort ToUint16(JsValue o) { return (ushort) (uint) ToNumber(o); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static string ToString(long i) { return i >= 0 && i < intToString.Length ? intToString[i] : i.ToString(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static string ToString(int i) { return i >= 0 && i < intToString.Length ? intToString[i] : i.ToString(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static string ToString(uint i) { return i < (uint) intToString.Length ? intToString[i] : i.ToString(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static string ToString(char c) { return c >= 0 && c < charToString.Length ? charToString[c] : c.ToString(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static string ToString(ulong i) { return i >= 0 && i < (ulong) intToString.Length ? intToString[i] : i.ToString(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static string ToString(double d) { if (d > long.MinValue && d < long.MaxValue && Math.Abs(d % 1) <= DoubleIsIntegerTolerance) { // we are dealing with integer that can be cached return ToString((long) d); } using (var stringBuilder = StringBuilderPool.Rent()) { // we can create smaller array as we know the format to be short return NumberPrototype.NumberToString(d, new DtoaBuilder(17), stringBuilder.Builder); } } /// /// http://www.ecma-international.org/ecma-262/6.0/#sec-topropertykey /// public static string ToPropertyKey(JsValue o) { var key = ToPrimitive(o, Types.String); if (key is JsSymbol s) { return s._value; } return ToString(key); } /// /// http://www.ecma-international.org/ecma-262/6.0/#sec-tostring /// public static string ToString(JsValue o) { switch (o._type) { case Types.String: return o.AsStringWithoutTypeCheck(); case Types.Boolean: return ((JsBoolean) o)._value ? "true" : "false"; case Types.Number: return ToString(((JsNumber) o)._value); case Types.Symbol: return ExceptionHelper.ThrowTypeErrorNoEngine("Cannot convert a Symbol value to a string"); case Types.Undefined: return Undefined.Text; case Types.Null: return Null.Text; case Types.Object when o is IPrimitiveInstance p: return ToString(ToPrimitive(p.PrimitiveValue, Types.String)); default: return ToString(ToPrimitive(o, Types.String)); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ObjectInstance ToObject(Engine engine, JsValue value) { switch (value._type) { case Types.Object: return (ObjectInstance) value; case Types.Boolean: return engine.Boolean.Construct(((JsBoolean) value)._value); case Types.Number: return engine.Number.Construct(((JsNumber) value)._value); case Types.String: return engine.String.Construct(value.AsStringWithoutTypeCheck()); case Types.Symbol: return engine.Symbol.Construct(((JsSymbol) value)._value); default: ExceptionHelper.ThrowTypeError(engine); return null; } } public static Types GetPrimitiveType(JsValue value) { if (value._type == Types.Object) { if (value is IPrimitiveInstance primitive) { return primitive.Type; } return Types.Object; } return value.Type; } public static void CheckObjectCoercible( Engine engine, JsValue o, MemberExpression expression, object baseReference) { if (o._type > Types.Null) { return; } var referenceResolver = engine.Options.ReferenceResolver; if (referenceResolver != null && referenceResolver.CheckCoercible(o)) { return; } ThrowTypeError(engine, o, expression, baseReference); } internal static void CheckObjectCoercible( Engine engine, JsValue o, MemberExpression expression, string referenceName) { if (o._type > Types.Null) { return; } var referenceResolver = engine.Options.ReferenceResolver; if (referenceResolver != null && referenceResolver.CheckCoercible(o)) { return; } ThrowTypeError(engine, o, expression, referenceName); } private static void ThrowTypeError( Engine engine, JsValue o, MemberExpression expression, object baseReference) { ThrowTypeError(engine, o, expression, (baseReference as Reference)?.GetReferencedName()); } private static void ThrowTypeError( Engine engine, JsValue o, MemberExpression expression, string referencedName) { referencedName = referencedName ?? "The value"; var message = $"{referencedName} is {o}"; throw new JavaScriptException(engine.TypeError, message).SetCallstack(engine, expression.Location); } public static void CheckObjectCoercible(Engine engine, JsValue o) { if (o._type == Types.Undefined || o._type == Types.Null) { ExceptionHelper.ThrowTypeError(engine); } } public static IEnumerable> FindBestMatch(Engine engine, T[] methods, Func argumentProvider) where T : MethodBase { System.Collections.Generic.List> matchingByParameterCount = null; foreach (var m in methods) { bool hasParams = false; var parameterInfos = m.GetParameters(); foreach (var parameter in parameterInfos) { if (Attribute.IsDefined(parameter, typeof(ParamArrayAttribute))) { hasParams = true; break; } } var arguments = argumentProvider(m, hasParams); if (parameterInfos.Length == arguments.Length) { if (methods.Length == 0 && arguments.Length == 0) { yield return new Tuple(m, arguments); yield break; } matchingByParameterCount = matchingByParameterCount ?? new System.Collections.Generic.List>(); matchingByParameterCount.Add(new Tuple(m, arguments)); } else if (parameterInfos.Length > arguments.Length) { // check if we got enough default values to provide all parameters (or more in case some default values are provided/overwritten) var defaultValuesCount = 0; foreach (var param in parameterInfos) { if (param.HasDefaultValue) defaultValuesCount++; } if (parameterInfos.Length <= arguments.Length + defaultValuesCount) { // create missing arguments from default values var argsWithDefaults = new System.Collections.Generic.List(arguments); for (var i = arguments.Length; i < parameterInfos.Length; i++) { var param = parameterInfos[i]; var value = JsValue.FromObject(engine, param.DefaultValue); argsWithDefaults.Add(value); } matchingByParameterCount = matchingByParameterCount ?? new System.Collections.Generic.List>(); matchingByParameterCount.Add(new Tuple(m, argsWithDefaults.ToArray())); } } } if (matchingByParameterCount == null) { yield break; } foreach (var tuple in matchingByParameterCount) { var perfectMatch = true; var parameters = tuple.Item1.GetParameters(); var arguments = tuple.Item2; for (var i = 0; i < arguments.Length; i++) { var arg = arguments[i].ToObject(); var paramType = parameters[i].ParameterType; if (arg == null) { if (!TypeIsNullable(paramType)) { perfectMatch = false; break; } } else if (arg.GetType() != paramType) { perfectMatch = false; break; } } if (perfectMatch) { yield return new Tuple(tuple.Item1, arguments); yield break; } } for (var i = 0; i < matchingByParameterCount.Count; i++) { var tuple = matchingByParameterCount[i]; yield return new Tuple(tuple.Item1, tuple.Item2); } } public static bool TypeIsNullable(Type type) { return !type.IsValueType || Nullable.GetUnderlyingType(type) != null; } } }