from __future__ import unicode_literals import re import datetime from .desc import * from .simplex import * from .conversions import * from pyjsparser import PyJsParser import six if six.PY2: from itertools import izip else: izip = zip def Type(obj): return obj.TYPE # 8.6.2 class PyJs(object): TYPE = 'Object' IS_CONSTRUCTOR = False prototype = None Class = None extensible = True value = None own = {} def get_member(self, unconverted_prop): return self.get(to_string(unconverted_prop)) def put_member(self, unconverted_prop, val): return self.put(to_string(unconverted_prop), val) def get(self, prop): assert type(prop) == unicode cand = self.get_property(prop) if cand is None: return undefined if is_data_descriptor(cand): return cand['value'] if is_undefined(cand['get']): return undefined return cand['get'].call(self) def get_own_property(self, prop): assert type(prop) == unicode # takes py returns py return self.own.get(prop) def get_property(self, prop): assert type(prop) == unicode # take py returns py cand = self.get_own_property(prop) if cand: return cand if self.prototype is not None: return self.prototype.get_property(prop) def put(self, prop, val, throw=False): assert type(prop) == unicode # takes py, returns none if not self.can_put(prop): if throw: raise MakeError('TypeError', 'Could not define own property') return own_desc = self.get_own_property(prop) if is_data_descriptor(own_desc): self.own[prop]['value'] = val return desc = self.get_property(prop) if is_accessor_descriptor(desc): desc['set'].call( self, (val, )) # calling setter on own or inherited element else: # new property self.own[prop] = { 'value': val, 'writable': True, 'configurable': True, 'enumerable': True } def can_put(self, prop): # to check assert type(prop) == unicode, type(prop) # takes py returns py desc = self.get_own_property(prop) if desc: # if we have this property if is_accessor_descriptor(desc): return is_callable( desc['set']) # Check if setter method is defined else: # data desc return desc['writable'] if self.prototype is None: return self.extensible inherited = self.prototype.get_property(prop) if inherited is None: return self.extensible if is_accessor_descriptor(inherited): return not is_undefined(inherited['set']) elif self.extensible: return inherited['writable'] # weird... return False def has_property(self, prop): assert type(prop) == unicode # takes py returns Py return self.get_property(prop) is not None def delete(self, prop, throw=False): assert type(prop) == unicode # takes py, returns py desc = self.get_own_property(prop) if desc is None: return True if desc['configurable']: del self.own[prop] return True if throw: raise MakeError('TypeError', 'Could not define own property') return False def default_value(self, hint=None): order = ('valueOf', 'toString') if hint == 'String' or (hint is None and self.Class == 'Date'): order = ('toString', 'valueOf') for meth_name in order: method = self.get(meth_name) if method is not None and is_callable(method): cand = method.call(self, ()) if is_primitive(cand): return cand raise MakeError('TypeError', 'Cannot convert object to primitive value') def define_own_property( self, prop, desc, throw): # Internal use only. External through Object assert type(prop) == unicode # takes Py, returns Py # prop must be a Py string. Desc is either a descriptor or accessor. # Messy method - raw translation from Ecma spec to prevent any bugs. # todo check this current = self.get_own_property(prop) extensible = self.extensible if not current: # We are creating a new OWN property if not extensible: if throw: raise MakeError('TypeError', 'Could not define own property') return False # extensible must be True if is_data_descriptor(desc) or is_generic_descriptor(desc): DEFAULT_DATA_DESC = { 'value': undefined, # undefined 'writable': False, 'enumerable': False, 'configurable': False } DEFAULT_DATA_DESC.update(desc) self.own[prop] = DEFAULT_DATA_DESC else: DEFAULT_ACCESSOR_DESC = { 'get': undefined, # undefined 'set': undefined, # undefined 'enumerable': False, 'configurable': False } DEFAULT_ACCESSOR_DESC.update(desc) self.own[prop] = DEFAULT_ACCESSOR_DESC return True # therefore current exists! if not desc or desc == current: # We don't need to change anything. return True configurable = current['configurable'] if not configurable: # Prevent changing params if desc.get('configurable'): if throw: raise MakeError('TypeError', 'Could not define own property') return False if 'enumerable' in desc and desc['enumerable'] != current[ 'enumerable']: if throw: raise MakeError('TypeError', 'Could not define own property') return False if is_generic_descriptor(desc): pass elif is_data_descriptor(current) != is_data_descriptor(desc): # we want to change the current type of property if not configurable: if throw: raise MakeError('TypeError', 'Could not define own property') return False if is_data_descriptor(current): # from data to setter del current['value'] del current['writable'] current['set'] = undefined # undefined current['get'] = undefined # undefined else: # from setter to data del current['set'] del current['get'] current['value'] = undefined # undefined current['writable'] = False elif is_data_descriptor(current) and is_data_descriptor(desc): if not configurable: if not current['writable'] and desc.get('writable'): if throw: raise MakeError('TypeError', 'Could not define own property') return False if not current['writable'] and 'value' in desc and current[ 'value'] != desc['value']: if throw: raise MakeError('TypeError', 'Could not define own property') return False elif is_accessor_descriptor(current) and is_accessor_descriptor(desc): if not configurable: if 'set' in desc and desc['set'] != current['set']: if throw: raise MakeError('TypeError', 'Could not define own property') return False if 'get' in desc and desc['get'] != current['get']: if throw: raise MakeError('TypeError', 'Could not define own property') return False current.update(desc) return True def create(self, args, space): '''Generally not a constructor, raise an error''' raise MakeError('TypeError', '%s is not a constructor' % self.Class) def get_member( self, prop, space ): # general member getter, prop has to be unconverted prop. it is it can be any value typ = type(self) if typ not in PRIMITIVES: # most likely getter for object return self.get_member( prop ) # <- object can implement this to support faster prop getting. ex array. elif typ == unicode: # then probably a String if type(prop) == float and is_finite(prop): index = int(prop) if index == prop and 0 <= index < len(self): return self[index] s_prop = to_string(prop) if s_prop == 'length': return float(len(self)) elif s_prop.isdigit(): index = int(s_prop) if 0 <= index < len(self): return self[index] # use standard string prototype return space.StringPrototype.get(s_prop) # maybe an index elif typ == float: # use standard number prototype return space.NumberPrototype.get(to_string(prop)) elif typ == bool: return space.BooleanPrototype.get(to_string(prop)) elif typ is UNDEFINED_TYPE: raise MakeError('TypeError', "Cannot read property '%s' of undefined" % prop) elif typ is NULL_TYPE: raise MakeError('TypeError', "Cannot read property '%s' of null" % prop) else: raise RuntimeError('Unknown type! - ' + repr(typ)) def get_member_dot(self, prop, space): # dot member getter, prop has to be unicode typ = type(self) if typ not in PRIMITIVES: # most likely getter for object return self.get(prop) elif typ == unicode: # then probably a String if prop == 'length': return float(len(self)) elif prop.isdigit(): index = int(prop) if 0 <= index < len(self): return self[index] else: # use standard string prototype return space.StringPrototype.get(prop) # maybe an index elif typ == float: # use standard number prototype return space.NumberPrototype.get(prop) elif typ == bool: return space.BooleanPrototype.get(prop) elif typ in (UNDEFINED_TYPE, NULL_TYPE): raise MakeError('TypeError', "Cannot read property '%s' of undefined" % prop) else: raise RuntimeError('Unknown type! - ' + repr(typ)) # Object class PyJsObject(PyJs): TYPE = 'Object' Class = 'Object' def __init__(self, prototype=None): self.prototype = prototype self.own = {} def _init(self, props, vals): i = 0 for prop, kind in props: if prop in self.own: # just check... probably will not happen very often. if is_data_descriptor(self.own[prop]): if kind != 'i': raise MakeError( 'SyntaxError', 'Invalid object initializer! Duplicate property name "%s"' % prop) else: if kind == 'i' or (kind == 'g' and 'get' in self.own[prop] ) or (kind == 's' and 'set' in self.own[prop]): raise MakeError( 'SyntaxError', 'Invalid object initializer! Duplicate setter/getter of prop: "%s"' % prop) if kind == 'i': # init self.own[prop] = { 'value': vals[i], 'writable': True, 'enumerable': True, 'configurable': True } elif kind == 'g': # get self.define_own_property(prop, { 'get': vals[i], 'enumerable': True, 'configurable': True }, False) elif kind == 's': # get self.define_own_property(prop, { 'get': vals[i], 'enumerable': True, 'configurable': True }, False) else: raise RuntimeError( 'Invalid property kind - %s. Expected one of i, g, s.' % repr(kind)) i += 1 def _set_props(self, prop_descs): for prop, desc in six.iteritems(prop_descs): self.define_own_property(prop, desc) # Array # todo Optimise Array - extremely slow due to index conversions from str to int and back etc. # solution - make get and put methods callable with any type of prop and handle conversions from inside # if not array then use to_string(prop). In array if prop is integer then just use it # also consider removal of these stupid writable, enumerable etc for ints. class PyJsArray(PyJs): Class = 'Array' def __init__(self, length, prototype=None): self.prototype = prototype self.own = { 'length': { 'value': float(length), 'writable': True, 'enumerable': False, 'configurable': False } } def _init(self, elements): for i, ele in enumerate(elements): if ele is None: continue self.own[unicode(i)] = { 'value': ele, 'writable': True, 'enumerable': True, 'configurable': True } def put(self, prop, val, throw=False): assert type(val) != int # takes py, returns none if not self.can_put(prop): if throw: raise MakeError('TypeError', 'Could not define own property') return own_desc = self.get_own_property(prop) if is_data_descriptor(own_desc): self.define_own_property(prop, {'value': val}, False) return desc = self.get_property(prop) if is_accessor_descriptor(desc): desc['set'].call( self, (val, )) # calling setter on own or inherited element else: # new property self.define_own_property( prop, { 'value': val, 'writable': True, 'configurable': True, 'enumerable': True }, False) def define_own_property(self, prop, desc, throw): assert type(desc.get('value')) != int old_len_desc = self.get_own_property('length') old_len = old_len_desc['value'] # value is js type so convert to py. if prop == 'length': if 'value' not in desc: return PyJs.define_own_property(self, prop, desc, False) new_len = to_uint32(desc['value']) if new_len != to_number(desc['value']): raise MakeError('RangeError', 'Invalid range!') new_desc = dict((k, v) for k, v in six.iteritems(desc)) new_desc['value'] = float(new_len) if new_len >= old_len: return PyJs.define_own_property(self, prop, new_desc, False) if not old_len_desc['writable']: return False if 'writable' not in new_desc or new_desc['writable'] == True: new_writable = True else: new_writable = False new_desc['writable'] = True if not PyJs.define_own_property(self, prop, new_desc, False): return False if new_len < old_len: # not very efficient for sparse arrays, so using different method for sparse: if old_len > 30 * len(self.own): for ele in self.own.keys(): if ele.isdigit() and int(ele) >= new_len: if not self.delete( ele ): # if failed to delete set len to current len and reject. new_desc['value'] = old_len + 1. if not new_writable: new_desc['writable'] = False PyJs.define_own_property( self, prop, new_desc, False) return False old_len = new_len else: # standard method while new_len < old_len: old_len -= 1 if not self.delete( unicode(int(old_len)) ): # if failed to delete set len to current len and reject. new_desc['value'] = old_len + 1. if not new_writable: new_desc['writable'] = False PyJs.define_own_property(self, prop, new_desc, False) return False if not new_writable: self.own['length']['writable'] = False return True elif prop.isdigit(): index = to_uint32(prop) if index >= old_len and not old_len_desc['writable']: return False if not PyJs.define_own_property(self, prop, desc, False): return False if index >= old_len: old_len_desc['value'] = index + 1. return True else: return PyJs.define_own_property(self, prop, desc, False) def to_list(self): return [ self.get(str(e)) for e in xrange(self.get('length').to_uint32()) ] # database with compiled patterns. Js pattern -> Py pattern. REGEXP_DB = {} class PyJsRegExp(PyJs): Class = 'RegExp' def __init__(self, body, flags, prototype=None): self.prototype = prototype self.glob = True if 'g' in flags else False self.ignore_case = re.IGNORECASE if 'i' in flags else 0 self.multiline = re.MULTILINE if 'm' in flags else 0 self.value = body if (body, flags) in REGEXP_DB: self.pat = REGEXP_DB[body, flags] else: comp = None try: # converting JS regexp pattern to Py pattern. possible_fixes = [(u'[]', u'[\0]'), (u'[^]', u'[^\0]'), (u'nofix1791', u'nofix1791')] reg = self.value for fix, rep in possible_fixes: comp = PyJsParser()._interpret_regexp(reg, flags) #print 'reg -> comp', reg, '->', comp try: self.pat = re.compile( comp, self.ignore_case | self.multiline) #print reg, '->', comp break except: reg = reg.replace(fix, rep) # print 'Fix', fix, '->', rep, '=', reg else: raise Exception() REGEXP_DB[body, flags] = self.pat except: #print 'Invalid pattern...', self.value, comp raise MakeError( 'SyntaxError', 'Invalid RegExp pattern: %s -> %s' % (repr(self.value), repr(comp))) # now set own properties: self.own = { 'source': { 'value': self.value, 'enumerable': False, 'writable': False, 'configurable': False }, 'global': { 'value': self.glob, 'enumerable': False, 'writable': False, 'configurable': False }, 'ignoreCase': { 'value': bool(self.ignore_case), 'enumerable': False, 'writable': False, 'configurable': False }, 'multiline': { 'value': bool(self.multiline), 'enumerable': False, 'writable': False, 'configurable': False }, 'lastIndex': { 'value': 0., 'enumerable': False, 'writable': True, 'configurable': False } } def match(self, string, pos): '''string is of course a py string''' return self.pat.match(string, int(pos)) class PyJsError(PyJs): Class = 'Error' extensible = True def __init__(self, message=None, prototype=None): self.prototype = prototype self.own = {} if message is not None: self.put('message', to_string(message)) self.own['message']['enumerable'] = False class PyJsDate(PyJs): Class = 'Date' UTCToLocal = None # todo UTC to local should be imported! def __init__(self, value, prototype=None): self.value = value self.own = {} self.prototype = prototype # todo fix this problematic datetime part def to_local_dt(self): return datetime.datetime(1970, 1, 1) + datetime.timedelta( seconds=self.UTCToLocal(self.value) // 1000) def to_utc_dt(self): return datetime.datetime(1970, 1, 1) + datetime.timedelta( seconds=self.value // 1000) def local_strftime(self, pattern): if self.value is NaN: return 'Invalid Date' try: dt = self.to_local_dt() except: raise MakeError( 'TypeError', 'unsupported date range. Will fix in future versions') try: return dt.strftime(pattern) except: raise MakeError( 'TypeError', 'Could not generate date string from this date (limitations of python.datetime)' ) def utc_strftime(self, pattern): if self.value is NaN: return 'Invalid Date' try: dt = self.to_utc_dt() except: raise MakeError( 'TypeError', 'unsupported date range. Will fix in future versions') try: return dt.strftime(pattern) except: raise MakeError( 'TypeError', 'Could not generate date string from this date (limitations of python.datetime)' ) # Scope class it will hold all the variables accessible to user class Scope(PyJs): Class = 'Global' extensible = True IS_CHILD_SCOPE = True THIS_BINDING = None space = None exe = None # todo speed up! # in order to speed up this very important class the top scope should behave differently than # child scopes, child scope should not have this property descriptor thing because they cant be changed anyway # they are all confugurable= False def __init__(self, scope, space, parent=None): """Doc""" self.space = space self.prototype = parent if type(scope) is not dict: assert parent is not None, 'You initialised the WITH_SCOPE without a parent scope.' self.own = scope self.is_with_scope = True else: self.is_with_scope = False if parent is None: # global, top level scope self.own = {} for k, v in six.iteritems(scope): # set all the global items self.define_own_property( k, { 'value': v, 'configurable': False, 'writable': False, 'enumerable': False }, False) else: # not global, less powerful but faster closure. self.own = scope # simple dictionary which maps name directly to js object. self.par = super(Scope, self) self.stack = [] def register(self, var): # registered keeps only global registered variables if self.prototype is None: # define in global scope if var in self.own: self.own[var]['configurable'] = False else: self.define_own_property( var, { 'value': undefined, 'configurable': False, 'writable': True, 'enumerable': True }, False) elif var not in self.own: # define in local scope since it has not been defined yet self.own[var] = undefined # default value def registers(self, vars): """register multiple variables""" for var in vars: self.register(var) def put(self, var, val, throw=False): if self.prototype is None: desc = self.own.get(var) # global scope if desc is None: self.par.put(var, val, False) else: if desc['writable']: # todo consider getters/setters desc['value'] = val else: if self.is_with_scope: if self.own.has_property(var): return self.own.put(var, val, throw=throw) else: return self.prototype.put(var, val) # trying to put in local scope # we dont know yet in which scope we should place this var elif var in self.own: self.own[var] = val return val else: # try to put in the lower scope since we cant put in this one (var wasn't registered) return self.prototype.put(var, val) def get(self, var, throw=False): if self.prototype is not None: if self.is_with_scope: cand = None if not self.own.has_property( var) else self.own.get(var) else: # fast local scope cand = self.own.get(var) if cand is None: return self.prototype.get(var, throw) return cand # slow, global scope if var not in self.own: # try in ObjectPrototype... if var in self.space.ObjectPrototype.own: return self.space.ObjectPrototype.get(var) if throw: raise MakeError('ReferenceError', '%s is not defined' % var) return undefined cand = self.own[var].get('value') return cand if cand is not None else self.own[var]['get'].call(self) def delete(self, var, throw=False): if self.prototype is not None: if self.is_with_scope: if self.own.has_property(var): return self.own.delete(var) elif var in self.own: return False return self.prototype.delete(var) # we are in global scope here. Must exist and be configurable to delete if var not in self.own: # this var does not exist, why do you want to delete it??? return True if self.own[var]['configurable']: del self.own[var] return True # not configurable, cant delete return False def get_new_arguments_obj(args, space): obj = space.NewObject() obj.Class = 'Arguments' obj.define_own_property( 'length', { 'value': float(len(args)), 'writable': True, 'enumerable': False, 'configurable': True }, False) for i, e in enumerate(args): obj.put(unicode(i), e) return obj #Function class PyJsFunction(PyJs): Class = 'Function' source = '{ [native code] }' IS_CONSTRUCTOR = True def __init__(self, code, ctx, params, name, space, is_declaration, definitions, prototype=None): self.prototype = prototype self.own = {} self.code = code if type( self.code ) == int: # just a label pointing to the beginning of the code. self.is_native = False else: self.is_native = True # python function self.ctx = ctx self.params = params self.arguments_in_params = 'arguments' in params self.definitions = definitions # todo remove this check later for p in params: assert p in self.definitions self.name = name self.space = space self.is_declaration = is_declaration #set own property length to the number of arguments self.own['length'] = { 'value': float(len(params)), 'writable': False, 'enumerable': False, 'configurable': False } if name: self.own['name'] = { 'value': name, 'writable': False, 'enumerable': False, 'configurable': True } if not self.is_native: # set prototype for user defined functions # constructor points to this function proto = space.NewObject() proto.own['constructor'] = { 'value': self, 'writable': True, 'enumerable': False, 'configurable': True } self.own['prototype'] = { 'value': proto, 'writable': True, 'enumerable': False, 'configurable': False } # todo set up throwers on callee and arguments if in strict mode def call(self, this, args=()): ''' Dont use this method from inside bytecode to call other bytecode. ''' if self.is_native: _args = SpaceTuple( args ) # we have to do that unfortunately to pass all the necessary info to the funcs _args.space = self.space return self.code( this, _args ) # must return valid js object - undefined, null, float, unicode, bool, or PyJs else: return self.space.exe._call(self, this, args) # will run inside bytecode def has_instance(self, other): # I am not sure here so instanceof may not work lol. if not is_object(other): return False proto = self.get('prototype') if not is_object(proto): raise MakeError( 'TypeError', 'Function has non-object prototype in instanceof check') while True: other = other.prototype if not other: # todo make sure that the condition is not None or null return False if other is proto: return True def create(self, args, space): proto = self.get('prototype') if not is_object(proto): proto = space.ObjectPrototype new = PyJsObject(prototype=proto) res = self.call(new, args) if is_object(res): return res return new def _generate_my_context(self, this, args): my_ctx = Scope( dict(izip(self.params, args)), self.space, parent=self.ctx) my_ctx.registers(self.definitions) my_ctx.THIS_BINDING = this if not self.arguments_in_params: my_ctx.own['arguments'] = get_new_arguments_obj(args, self.space) if not self.is_declaration and self.name and self.name not in my_ctx.own: my_ctx.own[ self. name] = self # this should be immutable binding but come on! return my_ctx class SpaceTuple: def __init__(self, tup): self.tup = tup def __len__(self): return len(self.tup) def __getitem__(self, item): return self.tup[item] def __iter__(self): return iter(self.tup)