Как работает Binascii.a2b_base64 (base64.b64decode) Python?

Я проверил исходный код на Python и реализовал функцию, аналогичную функции Python binascii.a2b_base64.

Эта функция находится по пути Python-3.12.4/Modules/binascii.c, строка 387.

static PyObject *
binascii_a2b_base64_impl(PyObject *module, Py_buffer *data, int strict_mode)

Я использовал C++ и повторно реализовал эту функцию в своем коде в соответствии с исходной функцией на Python, чтобы лучше понять и изучить принцип работы декодирования Base64.

Однако я не знаю, почему реализованная мной функция не может правильно обрабатывать символы, не закодированные в Base64.

Я проверил эти коды и подтвердил, что они не влияют на обработку функцией символов, не закодированных в Base64, таких как функция [_PyBytesWriter_Init, _PyBytesWriter_Alloc, _PyBytesWriter_Finish, ...], и проигнорировал ее в своем коде.

При обработке строк Base64, соответствующих стандарту RFC4648, а также: В случае, когда в качестве символа, не закодированного в Base64, используется только \n, реализованная мной функция достигнет того же результата, что и соответствующая функция в Python.
Например:

const char *encoded = {
    "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpBQkNERUZHSElKS0xNTk9QUVJTVFVW\n"
    "V1hZWkFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaQUJDREVGR0hJSktMTU5PUFFS\n"
    "U1RVVldYWVpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWg==\n"
};

Использование моей функции или функции Python binascii.a2b_base64 даст тот же результат, что и следующий:

ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ

Вот конкретная реализация моего кода:

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdint>
#include <stdexcept>

#define BASE64PAD '='

constexpr uint8_t b64de_table[256] = {
    255,255,255,255, 255,255,255,255, 255,255,255,255, 255,255,255,255,
    255,255,255,255, 255,255,255,255, 255,255,255,255, 255,255,255,255,
    255,255,255,255, 255,255,255,255, 255,255,255, 62, 255,255,255, 63,
    52 , 53, 54, 55,  56, 57, 58, 59,  60, 61,255,255, 255,  0,255,255,

    255,  0,  1,  2,   3,  4,  5,  6,   7,  8,  9, 10,  11, 12, 13, 14,
    15 , 16, 17, 18,  19, 20, 21, 22,  23, 24, 25,255, 255,255,255,255,
    255, 26, 27, 28,  29, 30, 31, 32,  33, 34, 35, 36,  37, 38, 39, 40,
    41 , 42, 43, 44,  45, 46, 47, 48,  49, 50, 51,255, 255,255,255,255,

    255,255,255,255, 255,255,255,255, 255,255,255,255, 255,255,255,255,
    255,255,255,255, 255,255,255,255, 255,255,255,255, 255,255,255,255,
    255,255,255,255, 255,255,255,255, 255,255,255,255, 255,255,255,255,
    255,255,255,255, 255,255,255,255, 255,255,255,255, 255,255,255,255,

    255,255,255,255, 255,255,255,255, 255,255,255,255, 255,255,255,255,
    255,255,255,255, 255,255,255,255, 255,255,255,255, 255,255,255,255,
    255,255,255,255, 255,255,255,255, 255,255,255,255, 255,255,255,255,
    255,255,255,255, 255,255,255,255, 255,255,255,255, 255,255,255,255};

uint8_t *
pyBase64Decode(const char *buffer, size_t &length,
               bool strict_mode = false)
{
    std::string error_message;

    const uint8_t *ascii_data = (const uint8_t *)buffer;
    size_t ascii_len = length;
    bool padding_started = 0;

    size_t bin_len = ascii_len / 4 * 3; 
    uint8_t *bin_data = new (std::nothrow) uint8_t[bin_len + 1];
    if (!bin_data) {
        throw std::runtime_error("Failed to allocate memory for bin_data.");
    }
    uint8_t *bin_data_start = bin_data;
    bin_data[bin_len] = 0x0;

    uint8_t leftchar = 0;
    uint32_t quad_pos = 0;
    uint32_t pads = 0;

    if (strict_mode && (ascii_len > 0) && (*ascii_data == BASE64PAD)) {
        error_message = "Leading padding not allowed.";
        goto error_end;
    }

    size_t i;
    uint8_t this_ch;
    for(i = 0; i < ascii_len; ++i) {
        this_ch = ascii_data[i];

        if (this_ch == BASE64PAD) {
            padding_started = true;
            // If the current character is a padding character, the length
            // will be reduced by one to obtain the decoded true length.
            bin_len--;

            if (strict_mode && (!quad_pos)) {
                error_message = "Excess padding not allowed.";
                goto error_end;
            }

            if ((quad_pos >= 2) && (quad_pos + (++pads) >= 4)) {

                if (strict_mode && ((i + 1) < ascii_len)) {
                    error_message = "Excess data after padding.";
                    goto error_end;
                }

                goto done;
            }

            continue;
        }

        this_ch = b64de_table[this_ch];
        if (this_ch == 255) {
            if (strict_mode) {
                error_message = "Only base64 data is allowed.";
                goto error_end;
            }
            continue;
        }

        if (strict_mode && padding_started) {
            error_message = "Discontinuous padding not allowed.";
            goto error_end;
        }

        pads = 0;

        switch(quad_pos) {
        case 0:
            quad_pos = 1;
            leftchar = this_ch;
            break;
        case 1:
            quad_pos = 2;
            *bin_data++ = (leftchar << 2) | (this_ch >> 4);
            leftchar = this_ch & 0xf;
            break;
        case 2:
            quad_pos = 3;
            *bin_data++ = (leftchar << 4) | (this_ch >> 2);
            leftchar = this_ch & 0x3;
            break;
        case 3:
            quad_pos = 0;
            *bin_data++ = (leftchar << 6) | (this_ch);
            leftchar = 0;
            break;
        }
    }

    if (quad_pos) {
        if (quad_pos == 1) {
            char tmpMsg[128]{};
            snprintf(tmpMsg, sizeof(tmpMsg),
                    "Invalid base64-encoded string: "
                    "number of data characters (%zd) cannot be 1 more "
                    "than a multiple of 4",
                    (bin_data - bin_data_start) / 3 * 4 + 1);
            error_message = tmpMsg;
            goto error_end;
        } else {
            error_message = "Incorrect padding.";
            goto error_end;
        }
        error_end:
        delete[] bin_data;
        throw std::runtime_error(error_message);
    }

done:
    length = bin_len;
    return bin_data_start;
}

Как использовать эту функцию:

int main()
{
    const char *encoded = "aGVsbG8sIHdvcmxkLg= = ";
    size_t length = strlen(encoded);
    uint8_t *decoded = pyBase64Decode(encoded, length);
    printf("decoded: %s\n", decoded);
    return 0;
}

Вот несколько примеров с разными результатами после выполнения Python и моего кода.

оригинал расшифрован:

stackoverflow

исходная кодировка:

c3RhY2tvdmVyZmxvdw==

образец 1:

оригинальный "c3##RhY2t...vdmV!?y~Zmxvdw==" результат питона "переполнение стека" результат pyBase64Decode "stackoverflowP"[^print_method_1] результат pyBase64Decode "stackoverflow"[^print_method_2], но длина: 19

образец 2:

оригинальный "c3\n\nRh~Y2tvd#$mVyZmx$vdw==" результат питона "переполнение стека" результат pyBase64Decode "stackoverflow"[^print_method_1], но длина: 16

образец 3:

оригинальный "c3Rh$$$$$$$$$$$$$$$$$$$$$Y2tvdmVy##############Zmxvdw==" результат питона "переполнение стека" результат pyBase64Decode "stackoverflowP\2;SP2;SPROFILE_"[^print_method_1] длина: 40 байт результат pyBase64Decode "stackoverflow"[^print_method_2], но длина: 40

[^print_method_1]: cout << std::string((char *)декодировано, длина) << endl;
[^print_method_2]: printf("%s", декодировано);

Что такое BASE64PAD

trincot 22.06.2024 09:20

@trincot Я исправил упущение в вопросе.

S-N 22.06.2024 09:28

Добавьте, пожалуйста, актуальное #include. Угадывать их неинтересно. | Аллокатин другой github.com/python/cpython/blob/main/Modules/binascii.c . Вы выделяете разный объем памяти и перед анализом помещаете в строку нулевой байт =. Представленный вывод предполагает, что вы не разместили в выводе нулевые завершающие символы, но я этого не понимаю. Пожалуйста, опубликуйте в вопросе полный код с «методом печати 1» и «методом печати 2», добавьте среду, операционную систему, версию компилятора и используемые параметры. Проверьте код с помощью gcc -fanalyzer, дезинфицирующих средств и использования valgrind.

KamilCuk 22.06.2024 11:15
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
3
69
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я не проверял исходный код реализации Python, но проблема в вашей реализации C++ находится в блоке if (this_ch == 255):

В этом случае это повлияет на длину вывода, поскольку вы основывали bin_len на ascii_len, где последний включал символы, отличные от Base64. Но поскольку они не имеют никакого отношения к результату, bin_len необходимо будет адаптировать.

Я бы предложил исправить это следующим образом:

  • Следите за количеством символов, которые не представляют данных, включая символы = и символы, отличные от Base64. Для этого вы можете использовать переменную skip, инициализированную значением 0.

  • В самом конце в разделе done пересчитайте bin_len с этой информацией и только потом поставьте терминатор \0 в bin_data.

Вот ваш код только с этими изменениями. В комментариях указано, где происходят изменения:

uint8_t *
pyBase64Decode(const char *buffer, size_t &length,
               bool strict_mode = false)
{
    size_t skip = 0; // Add this variable
    std::string error_message;

    const uint8_t *ascii_data = (const uint8_t *)buffer;
    size_t ascii_len = length;
    bool padding_started = 0;

    size_t bin_len = ascii_len / 4 * 3; 
    uint8_t *bin_data = new (std::nothrow) uint8_t[bin_len + 1];
    if (!bin_data) {
        throw std::runtime_error("Failed to allocate memory for bin_data.");
    }
    uint8_t *bin_data_start = bin_data;
    bin_data[bin_len] = 0x0; // This could be omitted, as we will do it in "done".

    uint8_t leftchar = 0;
    uint32_t quad_pos = 0;
    uint32_t pads = 0;

    if (strict_mode && (ascii_len > 0) && (*ascii_data == BASE64PAD)) {
        error_message = "Leading padding not allowed.";
        goto error_end;
    }

    size_t i;
    uint8_t this_ch;
    for(i = 0; i < ascii_len; ++i) {
        this_ch = ascii_data[i];

        if (this_ch == BASE64PAD) {
            padding_started = true;
            skip++; // Instead of decreasing bin_len, increase the new variable
            if (strict_mode && (!quad_pos)) {
                error_message = "Excess padding not allowed.";
                goto error_end;
            }
            if ((quad_pos >= 2) && (quad_pos + (++pads) >= 4)) {
                if (strict_mode && ((i + 1) < ascii_len)) {
                    error_message = "Excess data after padding.";
                    goto error_end;
                }
                goto done;
            }
            continue;
        }
        this_ch = b64de_table[this_ch];
        if (this_ch == 255) {
            if (strict_mode) {
                error_message = "Only base64 data is allowed.";
                goto error_end;
            }
            skip++;
            continue;
        }
        if (strict_mode && padding_started) {
            error_message = "Discontinuous padding not allowed.";
            goto error_end;
        }
        pads = 0;
        switch(quad_pos) {
        case 0:
            quad_pos = 1;
            leftchar = this_ch;
            break;
        case 1:
            quad_pos = 2;
            *bin_data++ = (leftchar << 2) | (this_ch >> 4);
            leftchar = this_ch & 0xf;
            break;
        case 2:
            quad_pos = 3;
            *bin_data++ = (leftchar << 4) | (this_ch >> 2);
            leftchar = this_ch & 0x3;
            break;
        case 3:
            quad_pos = 0;
            *bin_data++ = (leftchar << 6) | (this_ch);
            leftchar = 0;
            break;
        }
    }

    if (quad_pos) {
        if (quad_pos == 1) {
            char tmpMsg[128]{};
            snprintf(tmpMsg, sizeof(tmpMsg),
                    "Invalid base64-encoded string: "
                    "number of data characters (%zd) cannot be 1 more "
                    "than a multiple of 4",
                    (bin_data - bin_data_start) / 3 * 4 + 1);
            error_message = tmpMsg;
            goto error_end;
        } else {
            error_message = "Incorrect padding.";
            goto error_end;
        }
        error_end:
        delete[] bin_data;
        throw std::runtime_error(error_message);
    }

done:
    bin_len = (ascii_len - skip) * 3 / 4;  // Recalculate
    length = bin_len;
    bin_data[bin_len] = 0x0; // ...and place the terminator only now
    return bin_data_start;
}

Хотя это не идеальное решение (поскольку проблема все еще существует), я все же принял это решение, потому что, хотя я и задавал вопросы в сообществе Python, лучшего решения я не нашел. Это связано с тем, что у Python есть собственный подход к управлению памятью.

S-N 23.06.2024 07:18

Не могли бы вы привести пример, иллюстрирующий обнаруженную вами проблему?

trincot 23.06.2024 08:18

Извините, я не уточнил раньше. Сообщение, которое я оставил под вашим ответом, не относится к текущей теме. Проблема с текущей темой решена. Однако ни ваш код, ни код, который я написал ранее, не учитывали проблему с объемом памяти (во время uint8_t *bin_data = new (std::nothrow) uint8_t[bin_len + 1]; я подал заявку на большее пространство, чем фактически занято, если это зараженная строка кодировки Base64).

S-N 23.06.2024 12:19

Но это не проблема, не так ли? Либо вы перераспределяете память для копирования использованных данных, а затем освобождаете больший блок памяти, либо сначала выполняете отдельный цикл для расчета необходимой памяти, и только потом выполняете настоящую работу.

trincot 23.06.2024 12:39

да, я также придумал много решений (для проблем с производительностью, вызванных памятью и потенциальной репликацией памяти) (поскольку я слишком гонюсь за производительностью, мне нравится оптимизировать код до крайности), но теперь я думаю об этом, производительность может быть оптимизировался медленно в будущем. Теперь я продолжу выполнять другие свои функции.

S-N 23.06.2024 15:40

Другое решение, однако, имеет тот же недостаток, что и решение, предложенное Тринкотом, а именно: размер использования памяти bin_data превышает фактический размер использования.

https://discuss.python.org/t/how-does-pythons-binascii-a2b-base64-base64-b64decode-work/56401/

done:
    length = bin_data - bin_data_start;
    return bin_data_start;
}

Посмотрим, есть ли у меня лучшее решение через несколько месяцев.

S-N 23.06.2024 07:21

Другие вопросы по теме