"""This module translates JS flow into PY flow. Translates: IF ELSE DO WHILE WHILE FOR 123 FOR iter CONTINUE, BREAK, RETURN, LABEL, THROW, TRY, SWITCH """ from __future__ import print_function from utils import * from jsparser import * from nodevisitor import exp_translator import random TO_REGISTER = [] CONTINUE_LABEL = 'JS_CONTINUE_LABEL_%s' BREAK_LABEL = 'JS_BREAK_LABEL_%s' PREPARE = '''HOLDER = var.own.get(NAME)\nvar.force_own_put(NAME, PyExceptionToJs(PyJsTempException))\n''' RESTORE = '''if HOLDER is not None:\n var.own[NAME] = HOLDER\nelse:\n del var.own[NAME]\ndel HOLDER\n''' TRY_CATCH = '''%stry:\nBLOCKfinally:\n%s''' % (PREPARE, indent(RESTORE)) def get_continue_label(label): return CONTINUE_LABEL % label.encode('hex') def get_break_label(label): return BREAK_LABEL % label.encode('hex') def pass_until(source, start, tokens=(';', )): while start < len(source) and source[start] not in tokens: start += 1 return start + 1 def do_bracket_exp(source, start, throw=True): bra, cand = pass_bracket(source, start, '()') if throw and not bra: raise SyntaxError('Missing bracket expression') bra = exp_translator(bra[1:-1]) if throw and not bra: raise SyntaxError('Empty bracket condition') return bra, cand if bra else start def do_if(source, start): start += 2 # pass this if bra, start = do_bracket_exp(source, start, throw=True) statement, start = do_statement(source, start) if statement is None: raise SyntaxError('Invalid if statement') translated = 'if %s:\n' % bra + indent(statement) elseif = except_keyword(source, start, 'else') is_elseif = False if elseif: start = elseif if except_keyword(source, start, 'if'): is_elseif = True elseif, start = do_statement(source, start) if elseif is None: raise SyntaxError('Invalid if statement)') if is_elseif: translated += 'el' + elseif else: translated += 'else:\n' + indent(elseif) return translated, start def do_statement(source, start): """returns none if not found other functions that begin with 'do_' raise also this do_ type function passes white space""" start = pass_white(source, start) # start is the fist position after initial start that is not a white space or \n if not start < len(source): #if finished parsing return None return None, start if any(startswith_keyword(source[start:], e) for e in {'case', 'default'}): return None, start rest = source[start:] for key, meth in KEYWORD_METHODS.iteritems( ): # check for statements that are uniquely defined by their keywords if rest.startswith(key): # has to startwith this keyword and the next letter after keyword must be either EOF or not in IDENTIFIER_PART if len(key) == len(rest) or rest[len(key)] not in IDENTIFIER_PART: return meth(source, start) if rest[0] == '{': #Block return do_block(source, start) # Now only label and expression left cand = parse_identifier(source, start, False) if cand is not None: # it can mean that its a label label, cand_start = cand cand_start = pass_white(source, cand_start) if source[cand_start] == ':': return do_label(source, start) return do_expression(source, start) def do_while(source, start): start += 5 # pass while bra, start = do_bracket_exp(source, start, throw=True) statement, start = do_statement(source, start) if statement is None: raise SyntaxError('Missing statement to execute in while loop!') return 'while %s:\n' % bra + indent(statement), start def do_dowhile(source, start): start += 2 # pass do statement, start = do_statement(source, start) if statement is None: raise SyntaxError('Missing statement to execute in do while loop!') start = except_keyword(source, start, 'while') if not start: raise SyntaxError('Missing while keyword in do-while loop') bra, start = do_bracket_exp(source, start, throw=True) statement += 'if not %s:\n' % bra + indent('break\n') return 'while 1:\n' + indent(statement), start def do_block(source, start): bra, start = pass_bracket(source, start, '{}') #print source[start:], bra #return bra +'\n', start if bra is None: raise SyntaxError('Missing block ( {code} )') code = '' bra = bra[1:-1] + ';' bra_pos = 0 while bra_pos < len(bra): st, bra_pos = do_statement(bra, bra_pos) if st is None: break code += st bra_pos = pass_white(bra, bra_pos) if bra_pos < len(bra): raise SyntaxError('Block has more code that could not be parsed:\n' + bra[bra_pos:]) return code, start def do_empty(source, start): return 'pass\n', start + 1 def do_expression(source, start): start = pass_white(source, start) end = pass_until(source, start, tokens=(';', )) if end == start + 1: #empty statement return 'pass\n', end # AUTOMATIC SEMICOLON INSERTION FOLLOWS # Without ASI this function would end with: return exp_translator(source[start:end].rstrip(';'))+'\n', end # ASI makes things a bit more complicated: # we will try to parse as much as possible, inserting ; in place of last new line in case of error rev = False rpos = 0 while True: try: code = source[start:end].rstrip(';') cand = exp_translator(code) + '\n', end just_to_test = compile(cand[0], '', 'exec') return cand except Exception as e: if not rev: rev = source[start:end][::-1] lpos = rpos while True: rpos = pass_until(rev, rpos, LINE_TERMINATOR) if rpos >= len(rev): raise if filter(lambda x: x not in SPACE, rev[lpos:rpos]): break end = start + len(rev) - rpos + 1 def do_var(source, start): #todo auto ; insertion start += 3 #pass var end = pass_until(source, start, tokens=(';', )) defs = argsplit( source[start:end - 1] ) # defs is the list of defined vars with optional initializer code = '' for de in defs: var, var_end = parse_identifier(de, 0, True) TO_REGISTER.append(var) var_end = pass_white(de, var_end) if var_end < len( de ): # we have something more to parse... It has to start with = if de[var_end] != '=': raise SyntaxError( 'Unexpected initializer in var statement. Expected "=", got "%s"' % de[var_end]) code += exp_translator(de) + '\n' if not code.strip(): code = 'pass\n' return code, end def do_label(source, start): label, end = parse_identifier(source, start) end = pass_white(source, end) #now source[end] must be : assert source[end] == ':' end += 1 inside, end = do_statement(source, end) if inside is None: raise SyntaxError('Missing statement after label') defs = '' if inside.startswith('while ') or inside.startswith( 'for ') or inside.startswith('#for'): # we have to add contine label as well... # 3 or 1 since #for loop type has more lines before real for. sep = 1 if not inside.startswith('#for') else 3 cont_label = get_continue_label(label) temp = inside.split('\n') injected = 'try:\n' + '\n'.join(temp[sep:]) injected += 'except %s:\n pass\n' % cont_label inside = '\n'.join(temp[:sep]) + '\n' + indent(injected) defs += 'class %s(Exception): pass\n' % cont_label break_label = get_break_label(label) inside = 'try:\n%sexcept %s:\n pass\n' % (indent(inside), break_label) defs += 'class %s(Exception): pass\n' % break_label return defs + inside, end def do_for(source, start): start += 3 # pass for entered = start bra, start = pass_bracket(source, start, '()') inside, start = do_statement(source, start) if inside is None: raise SyntaxError('Missing statement after for') bra = bra[1:-1] if ';' in bra: init = argsplit(bra, ';') if len(init) != 3: raise SyntaxError('Invalid for statement') args = [] for i, item in enumerate(init): end = pass_white(item, 0) if end == len(item): args.append('' if i != 1 else '1') continue if not i and except_keyword(item, end, 'var') is not None: # var statement args.append(do_var(item, end)[0]) continue args.append(do_expression(item, end)[0]) return '#for JS loop\n%swhile %s:\n%s%s\n' % ( args[0], args[1].strip(), indent(inside), indent(args[2])), start # iteration end = pass_white(bra, 0) register = False if bra[end:].startswith('var '): end += 3 end = pass_white(bra, end) register = True name, end = parse_identifier(bra, end) if register: TO_REGISTER.append(name) end = pass_white(bra, end) if bra[end:end + 2] != 'in' or bra[end + 2] in IDENTIFIER_PART: #print source[entered-10:entered+50] raise SyntaxError('Invalid "for x in y" statement') end += 2 # pass in exp = exp_translator(bra[end:]) res = 'for temp in %s:\n' % exp res += indent('var.put(%s, temp)\n' % name.__repr__()) + indent(inside) return res, start # todo - IMPORTANT def do_continue(source, start, name='continue'): start += len(name) #pass continue start = pass_white(source, start) if start < len(source) and source[start] == ';': return '%s\n' % name, start + 1 # labeled statement or error label, start = parse_identifier(source, start) start = pass_white(source, start) if start < len(source) and source[start] != ';': raise SyntaxError('Missing ; after label name in %s statement' % name) return 'raise %s("%s")\n' % (get_continue_label(label) if name == 'continue' else get_break_label(label), name), start + 1 def do_break(source, start): return do_continue(source, start, 'break') def do_return(source, start): start += 6 # pass return end = source.find(';', start) + 1 if end == -1: end = len(source) trans = exp_translator(source[start:end].rstrip(';')) return 'return %s\n' % (trans if trans else "var.get('undefined')"), end # todo later?- Also important def do_throw(source, start): start += 5 # pass throw end = source.find(';', start) + 1 if not end: end = len(source) trans = exp_translator(source[start:end].rstrip(';')) if not trans: raise SyntaxError('Invalid throw statement: nothing to throw') res = 'PyJsTempException = JsToPyException(%s)\nraise PyJsTempException\n' % trans return res, end def do_try(source, start): start += 3 # pass try block, start = do_block(source, start) result = 'try:\n%s' % indent(block) catch = except_keyword(source, start, 'catch') if catch: bra, catch = pass_bracket(source, catch, '()') bra = bra[1:-1] identifier, bra_end = parse_identifier(bra, 0) holder = 'PyJsHolder_%s_%d' % (identifier.encode('hex'), random.randrange(1e8)) identifier = identifier.__repr__() bra_end = pass_white(bra, bra_end) if bra_end < len(bra): raise SyntaxError('Invalid content of catch statement') result += 'except PyJsException as PyJsTempException:\n' block, catch = do_block(source, catch) # fill in except ( catch ) block and remember to recover holder variable to its previous state result += indent( TRY_CATCH.replace('HOLDER', holder).replace('NAME', identifier).replace( 'BLOCK', indent(block))) start = max(catch, start) final = except_keyword(source, start, 'finally') if not (final or catch): raise SyntaxError( 'Try statement has to be followed by catch or finally') if not final: return result, start # translate finally statement block, start = do_block(source, final) return result + 'finally:\n%s' % indent(block), start def do_debugger(source, start): start += 8 # pass debugger end = pass_white(source, start) if end < len(source) and source[end] == ';': end += 1 return 'pass\n', end #ignore errors... # todo automatic ; insertion. fuck this crappy feature # Least important def do_switch(source, start): start += 6 # pass switch code = 'while 1:\n' + indent('SWITCHED = False\nCONDITION = (%s)\n') # parse value of check val, start = pass_bracket(source, start, '()') if val is None: raise SyntaxError('Missing () after switch statement') if not val.strip(): raise SyntaxError('Missing content inside () after switch statement') code = code % exp_translator(val) bra, start = pass_bracket(source, start, '{}') if bra is None: raise SyntaxError('Missing block {} after switch statement') bra_pos = 0 bra = bra[1:-1] + ';' while True: case = except_keyword(bra, bra_pos, 'case') default = except_keyword(bra, bra_pos, 'default') assert not (case and default) if case or default: # this ?: expression makes things much harder.... case_code = None if case: case_code = 'if SWITCHED or PyJsStrictEq(CONDITION, %s):\n' # we are looking for a first : with count 1. ? gives -1 and : gives +1. count = 0 for pos, e in enumerate(bra[case:], case): if e == '?': count -= 1 elif e == ':': count += 1 if count == 1: break else: raise SyntaxError( 'Missing : token after case in switch statement') case_condition = exp_translator( bra[case:pos]) # switch {case CONDITION: statements} case_code = case_code % case_condition case = pos + 1 if default: case = except_token(bra, default, ':') case_code = 'if True:\n' # now parse case statements (things after ':' ) cand, case = do_statement(bra, case) while cand: case_code += indent(cand) cand, case = do_statement(bra, case) case_code += indent('SWITCHED = True\n') code += indent(case_code) bra_pos = case else: break # prevent infinite loop :) code += indent('break\n') return code, start def do_pyimport(source, start): start += 8 lib, start = parse_identifier(source, start) jlib = 'PyImport_%s' % lib code = 'import %s as %s\n' % (lib, jlib) #check whether valid lib name... try: compile(code, '', 'exec') except: raise SyntaxError( 'Invalid Python module name (%s) in pyimport statement' % lib) # var.pyimport will handle module conversion to PyJs object code += 'var.pyimport(%s, %s)\n' % (repr(lib), jlib) return code, start def do_with(source, start): raise NotImplementedError('With statement is not implemented yet :(') KEYWORD_METHODS = { 'do': do_dowhile, 'while': do_while, 'if': do_if, 'throw': do_throw, 'return': do_return, 'continue': do_continue, 'break': do_break, 'try': do_try, 'for': do_for, 'switch': do_switch, 'var': do_var, 'debugger': do_debugger, # this one does not do anything 'with': do_with, 'pyimport': do_pyimport } #Also not specific statements (harder to detect) # Block {} # Expression or Empty Statement # Label # # Its easy to recognize block but harder to distinguish between label and expression statement def translate_flow(source): """Source cant have arrays, object, constant or function literals. Returns PySource and variables to register""" global TO_REGISTER TO_REGISTER = [] return do_block('{%s}' % source, 0)[0], TO_REGISTER if __name__ == '__main__': #print do_dowhile('do {} while(k+f)', 0)[0] #print 'e: "%s"'%do_expression('++(c?g:h); mj', 0)[0] print(translate_flow('a; yimport test')[0])