CensoredUsername / unrpyc

A ren'py script decompiler
Other
861 stars 156 forks source link

Python 3 - AttributeError: 'Return' object has no attribute 'linenumber' #137

Closed Bexa2 closed 2 years ago

Bexa2 commented 2 years ago

This should be a options.rpyc options.zip

Traceback (most recent call last):
  File ".\normalize_paths.py", line 123, in <module>
    main()
  File ".\normalize_paths.py", line 119, in main
    (name, version) = get_name_and_version(renpy_path)
  File ".\normalize_paths.py", line 68, in get_name_and_version
    if not unrpyc.decompile_rpyc(tmp_filename):
  File "G:\games_test\rpa_decompiler\unrpyc.py", line 166, in decompile_rpyc
    decompiler.pprint(out_file, ast, decompile_python=decompile_python, printlock=printlock,
  File "G:\games_test\rpa_decompiler\decompiler\__init__.py", line 45, in pprint
    Decompiler(out_file, printlock=printlock,
  File "G:\games_test\rpa_decompiler\decompiler\__init__.py", line 102, in dump
    ast[-1].linenumber == ast[-2].linenumber):
AttributeError: 'Return' object has no attribute 'linenumber'

The file does start with RENPY RPC2

The ast array looks like this:

[<renpy.ast.Init object at 0x03D6B340>,
<renpy.ast.Init object at 0x03D6B8B0>,
<renpy.ast.Init object at 0x03D6BA48>,
<renpy.ast.Init object at 0x03D6BC40>,
<renpy.ast.Init object at 0x03D6BDD8>,
<renpy.ast.Init object at 0x03D6BFB8>,
<renpy.ast.Init object at 0x03D74148>,
<renpy.ast.Init object at 0x03D742F8>,
<renpy.ast.Init object at 0x03D74490>,
<renpy.ast.Init object at 0x03D74688>,
<renpy.ast.Init object at 0x03D74880>,
<renpy.ast.Init object at 0x03D74A78>,
<renpy.ast.Init object at 0x03D74C70>,
<renpy.ast.Init object at 0x03D74E68>,
<renpy.ast.Init object at 0x03D79088>,
<renpy.ast.Init object at 0x03D79280>,
<renpy.ast.Init object at 0x03D79478>,
<renpy.ast.Init object at 0x03D795E0>,
<renpy.ast.Init object at 0x03D79688>,
<renpy.ast.Init object at 0x03D79730>,
<renpy.ast.Init object at 0x03D797F0>,
<renpy.ast.Init object at 0x03D798F8>,
<renpy.ast.Init object at 0x03D799A0>,
<renpy.ast.Init object at 0x03D79A48>,
<renpy.ast.Return object at 0x03D79AF0>]

And this is what I see if I pprint the objects:

#pprint(vars(ast[-1]))
{b'expression': None,
 b'filename': 'game/options.rpy',
 b'linenumber': 221,
 b'name': ('C:\\Users\\Rinko\\Documents\\3DFullGame/game/options.rpy',
           1520959593,
           303),
 b'next': None}

#pprint(vars(ast[-2]))
{b'block': [<renpy.ast.Python object at 0x034A9A90>],
 b'filename': 'game/options.rpy',
 b'linenumber': 221,
 b'name': ('C:\\Users\\Rinko\\Documents\\3DFullGame/game/options.rpy',
           1583316468,
           519),
 b'next': None,
 b'priority': 0}

I know this is supposed to be used with python2 so feel free to ignore it, I'm just curious as to why it's failing.

Bexa2 commented 2 years ago

vars(ast[-1])[b'linenumber'] would be a hack but it would have to be done in many places for all attributes.

Went ahead and tried replacing every time it failed just to see if it'd decompile after all. It did.

Just an example of what I've done.

I can see a lot of things wrong with the replacements I've made, like lines 323 and 324: with ast.code being now vars(ast)[b'varna'], I'm not even sure what I did, but for some reason it works. both_options.zip

I just realized.... While it does work, the hacked version it's missing the config. in the statements. So it's define version = "6.0" instead of define config.version = "6.0"

Bexa2 commented 2 years ago

I wonder if Python 3.9's ast module would be any useful.

Bexa2 commented 2 years ago

Fuck me, I replaced all <ast_object>.attribute with vars(<ast_object>)[b'attribute'] and all [not] hasattr(<ast_object>, attribute) with b'attribute' [not] in vars(<ast_object>) and now it turns out that the dict vars() returns has b"" string as keys in some games and "" string as keys in others.

Edit: Had to wrap all string in a self.bstring("attribute") function that returns string or bytes(string) based on the check isinstance(list(vars(ast[0]).keys())[0], bytes) at Decompiler.dump()

It works, having different errors now, but it works!

Edit: And now a game is throwing errors on utils.py, I'll just make 'say_get_code()' return always return " ".join(rv) right at the beginning.

CensoredUsername commented 2 years ago

Yep, that's the result of ren'py slowly transitioning from py2 to py3. Somewhere along the line all identifiers in the codebase became unicode.

vars(something)[attrname] is rather inefficient btw, iirc it creates a copy of the backing dict. It's easier to just do getattr(something, attrname)

Also some other people have already worked on a py3 port that went pretty far, I just haven't had the time to look over it.

Bexa2 commented 2 years ago

The problem is that when vars(ast) returns a dictionary with b'' keys hasattr(ast, '') will return false, even if the attribute is there and I can't use b'' to check because hasattr rejects it.

print(f"byte string { isinstance(list(vars(ast).keys())[0], bytes) }") # byte string True
print(hasattr(ast, 'linenumber')) # False
print(hasattr(ast, b'linenumber')) # TypeError: hasattr(): attribute name must be string

When it doesn't return a dict with b'' keys then it's fine

print(f"byte string { isinstance(list(vars(ast).keys())[0], bytes) }") # byte string False
print(hasattr(ast, 'linenumber')) # True

I'll take a look at those python 3 versions to see how they do it.

Bexa2 commented 2 years ago

Finally "found" what was wrong.

Million thanks to @madeddy

On magic.py she changes the default argument to safe_loads(encoding=) from bytes to utf-8. (I wouldn't have found this myself)

This can also be set on unrpyc.py on read_ast_from_file I can add a try catch, try bytes, check for AttributeError then try utf-8

So to get my script working all I had to do was change imports, pass encoding to decompile_rpyc, read_ast_from_file and deobfuscate.read_ast and so on until it reaches safe_loads it's default value being 'bytes', change unicode to str and also changed startswith("RENPY RPC2") to startswith(b"RENPY RPC2").

I think that's all.

Now moving on to RPG Maker.