💾 Archived View for any-key.press › esoteric › bggp5_pyc.gmi captured on 2024-08-18 at 17:14:26. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

Binary Golf Grand Prix 5 - py[c]

Сегодня на работе у меня выдался такой день, когда я больше времени жду результатов от коллег. А в голове вертится соревнование "Binary Golf Grand Prix 5", для которого я уже сделал один вариант решения:

Binary Golf Grand Prix 5 - openssl s_client

И на глаза мне попалось очень элегантное решение:

https://github.com/binarygolf/BGGP/blob/main/2024/entries/Aaron%20DeVera/Aaron%20DeVera.py.txt

Суть его в том, что создается файл с именем `curl -L binary.golf --request-target %2F5%2F5`, и содержимым:

import os;os.system(__file__[-45:])

По шагам:

Красиво же? Но есть два фатальных недостатка:

Мое решение (.py)

Что бы скачать байты по HTTPS python'у не нужен curl, есть встроенный модуль `urllib.request` с методом `urlopen`:

https://docs.python.org/3/library/urllib.request.html#urllib.request.urlopen

Но нам для начала нужно его импортировать, а затем уже вызвать, передав туда URL. Что бы импортировать модуль в python есть встроенная функция `__import__`:

https://docs.python.org/3/library/functions.html#import__

Однако есть одна тонкость: что бы импортировать и получить на выходе из функции под-модуль (`urllib.request`, а не просто `urllib`) нужно обязательно указать аргумент `fromlist`:

$ python3 -c "print(__import__('urllib.request'))"
<module 'urllib' from '/usr/lib/python3.10/urllib/__init__.py'>

$ python3 -c "print(__import__('urllib.request', fromlist=(None,)))"
<module 'urllib.request' from '/usr/lib/python3.10/urllib/request.py'>

Второе препятствие заключается в том, что в URL нужно указать слеши. В чужом решении, которое вызывает curl, это элегантно обошли опцией `--request-target`. Ну а у нас вся python'овская мощь под руками. Cимвол слеша можно получить, например, вызовом `chr`:

https://docs.python.org/3/library/functions.html#chr

А полный URL можно собрать из кортежа подстрок методом `join`:

https://docs.python.org/3/library/stdtypes.html#str.join

Собрав всё вместе получаем такой "однострочик":

$ python3 -c "print(__import__('urllib.request',fromlist=(None,)).urlopen(chr(47).join(('https:','','binary.golf','5','5'))).read())"
b'Another #BGGP5 download!! @binarygolf https://binary.golf\n'

Эта строка и должна быть именем файла. Так как длина этой строки 118 символов, то что бы её исполнить содержимое .py-файла должно быть примерное таким:

eval(__file__[-118:])

Это и есть решение для формата .py в 22 байта без дополнительных аргументов командной строки.

Ещё одно моё решение (.pyc)

Но python умеет запускать не только текстовые .py-файлы, но и бинарные .pyc-файлы с байт-кодом. Получить такой файл просто: формируем файл eval.py с нужным нам содержимым:

$ cat eval.py 
eval(__file__[-118:])

Импортируем его в REPL режиме python, игнорируя ошибки:

$ python3
Python 3.10.14 (main, Apr 13 2024, 11:52:53) [Clang 16.0.6 ] on openbsd7
Type "help", "copyright", "credits" or "license" for more information.
>>> import eval
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "eval.py", line 1, in <module>
    eval(__file__[-118:])
  File "<string>", line 1
    eval.py
    ^
SyntaxError: invalid syntax
>>> 

И вот скомпилированный файл в 162 байта готов:

$ ls  -l __pycache__
total 8
-rw-r--r--  1 user user  162 Jul  9 15:54 eval.cpython-310.pyc

Что бы посмотреть содержимое .pyc файла я нашёл небольшой скрипт show_pyc.py:

https://github.com/nedbat/coveragepy/blob/master/lab/show_pyc.py

$ python3 show_pyc.py __pycache__/eval.cpython-310.pyc 
magic b'6f0d0d0a'
flags 0x000000
moddate b'da328d66' (Tue Jul  9 15:53:46 2024)
pysize b'16000000' (22)
code
    name '<module>'
    argcount 0
    nlocals 0
    stacksize 4
    flags 0040: CO_NOFREE
    code 6500650164006401850219008301010064015300
  1           0 LOAD_NAME                0 (eval)
              2 LOAD_NAME                1 (__file__)
              4 LOAD_CONST               0 (-118)
              6 LOAD_CONST               1 (None)
              8 BUILD_SLICE              2
             10 BINARY_SUBSCR
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               1 (None)
             18 RETURN_VALUE
    consts
        0: -118
        1: None
    names ('eval', '__file__')
    varnames ()
    freevars ()
    cellvars ()
    filename 'eval.py'
    firstlineno 1
    lnotab 
        1:0
    linetable 1400
        co_lines 1:0-20

Немного поразмыслив я пришёл к выводу, что поля name, filename и linetable мне для запуска не нужны. Поэтому я написал свой небольшой скрипт-оптимизатор:

import marshal

def main(pyc_file_path):
    with open(pyc_file_path, "rb") as opened_file:
        header = opened_file.read(16)
        code = marshal.load(opened_file)

    new_code = code.replace(co_name="", co_filename="", co_linetable=b"")

    with open(pyc_file_path, "wb") as opened_file:
        opened_file.write(header)
        marshal.dump(new_code, opened_file)

if __name__ == '__main__':
    import sys
    if len(sys.argv) != 2:
        print("Usage: python3 opt.py <pyc_file>")
        sys.exit(1)
    main(sys.argv[1])

Таким нехитрым путём мне удалось сократить .pyc-файл до 120 байт:

$ python3 opt.py __pycache__/eval.cpython-310.pyc 
$ ls -l __pycache__ 
total 8
-rw-r--r--  1 user user 120 Jul  9 16:36 eval.cpython-310.pyc

Переименовываем его в "print(__import__('urllib.request',fromlist=(None,)).urlopen(chr(47).join(('https:','','binary.golf','5','5'))).read())" (без кавычек) и готово еще одно решение для Binary Golf Grand Prix 5 на 120 байт. Расширение .pyc файлу не нужно, как я понял python3.10 детектирует формат по первым четырём байтам файла. Естественно, что скомпилированный одной версией python файл будет корректно запускаться только этой же версией (в моем случае это именно python3.10).

Комментарии через ActivityPub (Fediverse) можно оставить здесь:

https://honk.any-key.press/u/continue/h/JWs972n7Q2f35qv3rs