Создайте две функции, которые могут преобразовывать двоичные данные в строку в кодировке Base64 и наоборот не используя символы "=" padding

Постановка задачи

Задание с сайта codewars.com

Кодировка Base64 позволяет представлять произвольные двоичные данные в виде ASCII-текста.

Ваша задача - предоставить кодировщик и декодер для преобразования в Base64 и из него.

Создайте две функции, которые могут преобразовывать двоичные данные в строку в кодировке Base64 и наоборот:

def to_base_64(data: bytes) -> str: ...
def from_base_64(encoded: str) -> bytes: ...

Хотя во многих реализациях Base64 используются символы "=" padding, ваши функции не должны использовать padding.

Можете ли вы создать свой собственный кодер и декодер, а не использовать реализацию base64 на вашем языке программирования?

Пример (ввод -> вывод):

b"this is a bytestring!" -> "dGhpcyBpcyBhIGJ5dGVzdHJpbmch"
b"\x00" -> "AA"
b"\x00\x01" -> "AAE"),
b"this is a test!" -> "dGhpcyBpcyBhIHRlc3Qh"
b"TVRJek5EVTJOemc1TUNBZyAg" -> "VFZSSmVrNUVWVEpPZW1jMVRVTkJaeUFn"
    b"\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb" ->"8J+RqPCfj7vigI3inaTvuI/igI3wn5KL4oCN8J+RqPCfj7s"

Моё решение

Создал 2 функции как по заданию

import base64
    
def to_base_64(data: bytes) -> str:
    if data == b'':
        return ''
    data = data.decode('utf-8')
    message_bytes = data.encode('utf-8')
    base64_bytes = base64.b64encode(message_bytes)
    base64_message = base64_bytes.decode('utf-8')
    count_padding = base64_message.count('=')
    if count_padding == 0: # Проверка количества padding
        return base64_message  # Вернуть если нет padding
    else:
        return base64_message[:-count_padding]  # Вернуть без padding - ТАКОЕ ЗАДАНИЕ
        
# print(to_base_64(data) )
    
def from_base_64(encoded: str) -> bytes:
    if encoded == '':
        return b''
    if len(encoded) % 4 != 0:
        encoded += '=' * (4 - len(encoded) % 4)
    base64_bytes = encoded.encode('utf-8')
    message_bytes = base64.b64decode(base64_bytes)
    message = message_bytes.decode('utf-8')
    return message.encode('utf-8')

Решение нашёл - тесты проходят, но получаю некоторые ошибки с сервера:

    100 random tests encoding to base64
    Log
    encodes b"\x8f\x89\x02\x87u\x80\xde\xa1T nG\x83\t\x87m\x94\xa3\xd6n\xa7h\xff\x87\x03\xac\xc6/'=\x14\x1c\xcd\xdf2D\xc2\x9aC\x89\xa0\xea\xfa-T\xe7\xf0\xaa\x0e{\xd2/\xae\x1c\x12\x1dP=\x85x\xc4\x98b\xd0\xbd\xfb\xcc\x1b\x8a\x11 `\x11Mgd\xd6Q\xcd\x8b" to 'j4kCh3WA3qFUIG5HgwmHbZSj1m6naP+HA6zGLyc9FBzN3zJEwppDiaDq+i1U5/CqDnvSL64cEh1QPYV4xJhi0L37zBuKESBgEU1nZNZRzYs'
    b"\x8f\x89\x02\x87u\x80\xde\xa1T nG\x83\t\x87m\x94\xa3\xd6n\xa7h\xff\x87\x03\xac\xc6/'=\x14\x1c\xcd\xdf2D\xc2\x9aC\x89\xa0\xea\xfa-T\xe7\xf0\xaa\x0e{\xd2/\xae\x1c\x12\x1dP=\x85x\xc4\x98b\xd0\xbd\xfb\xcc\x1b\x8a\x11 `\x11Mgd\xd6Q\xcd\x8b"

data = data.decode('utf-8')
           ^^^^^^^^^^^^^^^^^^^^
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8f in position 0: invalid start byte

100 random tests decoding from base64
Log
decodes ['9', 'Y', '9', 'A', 'r', 'A', '3', 'x', 'n', 'D', 'g', 'R', 'k', '8', 'G', '7', '9', '1', '+', '/', 'R', 'u', 'Z', 'r', 'N', 'B', 'z', 'K'] to b'\xf5\x8f@\xac\r\xf1\x9c8\x11\x93\xc1\xbb\xf7_\xbfF\xe6k4\x1c\xca'

base64_bytes = encoded.encode('utf-8')
                   ^^^^^^^^^^^^^^
AttributeError: 'list' object has no attribute 'encode'

Много раз редактировал своё решение по советам в комментариях, кодировку 'utf-8' сменить или как, и как быть со списком - к строке привести?


Ответы (1 шт):

Автор решения: Stanislav Volodarskiy

Ответ

В начало функции from_base_64 вставьте оператор:

    encoded = ''.join(encoded)

Он нужен потому что в кате есть тест, когда на вход подаётся не строка, а список строк. Если encoded строка, этот код ничего не поменяет. Если encoded список, он превратит его в строку.

В обоих функциях есть проверки на пустые входные строки. Они не нужны, без них всё работает отлично. Удаляем.

В начале функции to_base_64 байты декодируются в UTF8, затем кодируются обратно. В кате нет тестов, которые показывают ошибку, но вызов to_base_64(bytes([255])) упадёт. Декодирование и последующее кодирование убираем.

Отрезать выравнивание справа можно прямо в байтовой строке.

Последнее декодирование .decode('utf-8') переводит байты в строку. Это не ошибка, но можно обойтись более простым и быстрым декодированием в latin1.

В from_base_64 проверку длины строки и коррекцию можно заменить на один оператор encoded += '=' * (-len(encoded) % 4).

Дальше кодирование в utf-8 не нужно. base64.b64decode умеет работать со строками.

В конце функции опять бесполезное декодирование/кодирование. Удалить.

После этих изменений получим:

def to_base_64(data: bytes) -> str:
    return base64.b64encode(data).rstrip(b'=').decode('latin1')


def from_base_64(encoded: str) -> bytes:
    encoded = ''.join(encoded)
    return base64.b64decode(encoded + '=' * (-len(encoded) % 4))

Не ответ

Интересная оказалась ката, и решить её можно сравнительно просто.

alphabet хранит алфавит – отображение из индексов в символы.
indices наоборот отображает символы в индексы.

encode_chunk и decode_chunk представляют один и тот же алгоритм: сперва из исходных данных собираем 24-битовую строку, затем вырезаем из неё кусочки нужной длины. Обработка неполных данных (менее трёх байт или менее четырёх символов) делается автоматически. Секрет в соответствии:
3 байта ⇆ 4 символа;
2 байта ⇆ 3 символа;
1 байта ⇆ 2 символа.

Символов всегда на единицу больше чем байт.

to_base_64 и from_base_64 снова одинаковы: режем исходные данные на кусочки, вызываем обработку кусочка, склеиваем результаты.

Оператор s = ''.join(s) во from_base_64 нужен потому что в кате есть тест, когда на вход подаётся не строка а список строк. Для строки он ничего не делает (только процессор греет), для списка возвращает единую строку.

Хорошо было бы переписать алгоритм на потоковую обработку, но тогда красота и простота решения будут не так видны.

alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
indices = {c: i for i, c in enumerate(alphabet)}


def encode_chunk(b):
    bits = sum(bi << (16 - 8 * i) for i, bi in enumerate(b))
    return ''.join(
        alphabet[(bits >> (18 - 6 * j)) & 0b111111] for j in range(len(b) + 1)
    )


def decode_chunk(s):
    bits = sum(indices[sj] << (18 - 6 * j) for j, sj in enumerate(s))
    return bytes(
        (bits >> (16 - 8 * i)) & 0b11111111 for i in range(len(s) - 1)
    )


def to_base_64(b):
    return ''.join(map(
        encode_chunk,
        (b[i:i + 3] for i in range(0, len(b), 3))
    ))

        
def from_base_64(s):
    s = ''.join(s)
    return b''.join(map(
        decode_chunk,
        (s[i:i + 4] for i in range(0, len(s), 4))
    ))
→ Ссылка