OberonCore
https://forum.oberoncore.ru/

Пример алгоритма Base64-кодирования
https://forum.oberoncore.ru/viewtopic.php?f=82&t=2769
Страница 1 из 2

Автор:  Евгений Темиргалеев [ Среда, 04 Август, 2010 14:10 ]
Заголовок сообщения:  Пример алгоритма Base64-кодирования

Отделено отсюда:
viewtopic.php?f=7&t=2762

Илья Ермаков писал(а):
Galkov писал(а):
...Покажите пожалуйста пример, в котором, опираясь на Дейкстру можно увидеть "неумение программировать"...
Например, использование break внутри цикла...
Пример из практики: хотел портировать готовую реализацию Base64 с Си. Стандарт приводит в качестве примера:
Цитата:
11. ISO C99 Implementation of Base64

An ISO C99 implementation of Base64 encoding and decoding that is
believed to follow all recommendations in this RFC is available from:

http://josefsson.org/base-encoding/

This code is not normative.
...
Когда увидел, понял, что для меня спокойнее написать с нуля самому (с упором на читабельность), чем портировать тот код, которому надо либо "believe", либо сломать моск при проверянии. Процедуры кодирования:
http://cvs.savannah.gnu.org/viewvc/gnul ... ision=1.14
(пробелы с табуляциями намешаны, отступы при копировании испортились)
Код:
/* Base64 encode IN array of size INLEN into OUT array of size OUTLEN.
   If OUTLEN is less than BASE64_LENGTH(INLEN), write as many bytes as
   possible.  If OUTLEN is larger than BASE64_LENGTH(INLEN), also zero
   terminate the output buffer. */
void
base64_encode (const char *restrict in, size_t inlen,
          char *restrict out, size_t outlen)
{
  static const char b64str[64] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

  while (inlen && outlen)
    {
      *out++ = b64str[(to_uchar (in[0]) >> 2) & 0x3f];
      if (!--outlen)
   break;
      *out++ = b64str[((to_uchar (in[0]) << 4)
             + (--inlen ? to_uchar (in[1]) >> 4 : 0))
            & 0x3f];
      if (!--outlen)
   break;
      *out++ =
   (inlen
    ? b64str[((to_uchar (in[1]) << 2)
         + (--inlen ? to_uchar (in[2]) >> 6 : 0))
        & 0x3f]
    : '=');
      if (!--outlen)
   break;
      *out++ = inlen ? b64str[to_uchar (in[2]) & 0x3f] : '=';
      if (!--outlen)
   break;
      if (inlen)
   inlen--;
      if (inlen)
   in += 3;
    }

  if (outlen)
    *out = '\0';
}
что получилось у меня:
Код:
   PROCEDURE Encode* (IN x: ARRAY OF SHORTCHAR; VAR y: ARRAY OF SHORTCHAR; VAR p, q: INTEGER; len: INTEGER);
      VAR   t, n: INTEGER;
   BEGIN
      ASSERT(len >= 0, 20);
      n := len DIV 3;
      WHILE n > 0 DO
         (* Encode24bit *)
         t := ORD(x[p + 2]) + ASH(ORD(x[p + 1]), 8) + ASH(ORD(x[p]), 16); INC(p, 3);
         y[q] := ctable[ASH(t, -18)]; INC(q);
         y[q] := ctable[ORD(BITS(ASH(t, -12)) * {0..5})]; INC(q);
         y[q] := ctable[ORD(BITS(ASH(t, -6)) * {0..5})]; INC(q);
         y[q] := ctable[ORD(BITS(t) * {0..5})]; INC(q);
         DEC(n)
      END;
      CASE len MOD 3 OF
      | 0:
      | 1:   (* Encode8bit *)
         t := 0 + ASH(0, 8) + ASH(ORD(x[p]), 16); INC(p);
         y[q] := ctable[ASH(t, -18)]; INC(q);
         y[q] := ctable[ORD(BITS(ASH(t, -12)) * {0..5})]; INC(q);
         y[q] := ctable[64]; INC(q);
         y[q] := ctable[64]; INC(q)
      | 2:   (* Encode16bit *)
         t := 0 + ASH(ORD(x[p + 1]), 8) + ASH(ORD(x[p]), 16); INC(p, 2);
         y[q] := ctable[ASH(t, -18)]; INC(q);
         y[q] := ctable[ORD(BITS(ASH(t, -12)) * {0..5})]; INC(q);
         y[q] := ctable[ORD(BITS(ASH(t, -6)) * {0..5})]; INC(q);
         y[q] := ctable[64]; INC(q)
      END
   END Encode;

Автор:  Alexey Veselovsky [ Среда, 04 Август, 2010 14:38 ]
Заголовок сообщения:  Re: Снова о важности базовых техник

Евгений Темиргалеев писал(а):
Когда увидел, понял, что для меня спокойнее написать с нуля самому (с упором на читабельность), чем портировать тот код, которому надо либо "believe", либо сломать моск при проверянии.

Абсолютно то же самое могу сказать о вашем коде. Если бы у меня возникла нужда написать оную функцию на неком языке Х, где её ещё нет, то мне было бы намного проще и приятней написать её с нуля по спеке, нежели вслепую копировать ваше, либо референсное, затем делать "belive" или же ломать мозг при проверянии этого дела.

Оберонов код тут ничуть не легче читается. Особенно доставляют по два оператора на строке. При первом прочтении я эти INC(x) в конце строк не увидел вообще. Зато в Сишном примере все точки выхода из цикла увидел сразу при первом же взгляде.

Автор:  Евгений Темиргалеев [ Среда, 04 Август, 2010 15:23 ]
Заголовок сообщения:  Re: Снова о важности базовых техник

Alexey Veselovsky писал(а):
Абсолютно то же самое могу сказать о вашем коде...
И какой смысл говорить о моём коде, когда я предложил сравнить алгоритмы. Разные алгоритмы для решения одной задачи. Вы могли перевести мой код на Си (я думал это и в уме можно), отформатировать как Вам нравится, и сравнивать.
Код:
 // ctable = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
void Encode (const unsigned char *x, unsigned char *y, int &p, int &q, int len)
{
   int t, n;
   assert(len >= 0);
   n = len / 3;
   while (n > 0) {
      /* Encode24bit */
      t = x[p + 2] + (x[p + 1] << 8) + (x[p] << 16);
      p += 3;
      y[q++] = ctable[t >> 18];
      y[q++] = ctable[(t >> 12) & 0x3F];
      y[q++] = ctable[(t >> 6) & 0x3F];
      y[q++] = ctable[t & 0x3F];
      --n;
   }
   switch (len % 3) {
   case 0:
      break;
   case 1:   /* Encode8bit */
      t = 0 + (0 << 8) + (x[p] << 16);
      ++p;
      y[q++] = ctable[t >> 18];
      y[q++] = ctable[(t >> 12) & 0x3F];
      y[q++] = ctable[64];
      y[q++] = ctable[64];
      break;
   case 2:   /* Encode16bit */
      t = 0 + (x[p + 1] << 8) + (x[p] << 16);
      p += 2;
      y[q++] = ctable[t >> 18];
      y[q++] = ctable[(t >> 12) & 0x3F];
      y[q++] = ctable[(t >> 6) & 0x3F];
      y[q++] = ctable[64];
      break;
   }
}
Хотелось бы услышать мнения касательно сложности проверки соответствия алгоритмов стандарту (но не придирки к переводу кода).

Автор:  Alexey Veselovsky [ Четверг, 05 Август, 2010 03:39 ]
Заголовок сообщения:  Re: Снова о важности базовых техник

Евгений Темиргалеев писал(а):
Alexey Veselovsky писал(а):
Абсолютно то же самое могу сказать о вашем коде...
И какой смысл говорить о моём коде, когда я предложил сравнить алгоритмы. Разные алгоритмы для решения одной задачи. Вы могли перевести мой код на Си (я думал это и в уме можно), отформатировать как Вам нравится, и сравнивать.

У вас не только алгоритмы разные, они ещё и разные задачи решают. У вас более частный случай нежели тот что приведен в C99 коде.

Мне кажется, что вот так, чуточку наглядней и читабельней. Хотя и чуть менее эффективно:
Код:
void encode(const char* const in, const size_t in_len, char* const out, const size_t out_len) {
    const unsigned long m1 = BIN24(11111100,00000000,00000000);
    const unsigned long m2 = BIN24(00000011,11110000,00000000);
    const unsigned long m3 = BIN24(00000000,00001111,11000000);
    const unsigned long m4 = BIN24(00000000,00000000,00111111);
   
    long value = 0;
    char* p_value = (char*)&value;   
   
    for (int i =0, j=0; i<in_len && j<out_len; i+=3, j+=4) {
        int i_shift = 0;
        value = 0;
        switch (in_len - i ) {
            default:
            case 3: p_value[0] = in[i + i_shift++];
            case 2: p_value[1] = in[i + i_shift++];
            case 1: p_value[2] = in[i + i_shift++];
        }

        switch (out_len - j) {
            default:
            case 4: out[j+3] = b64str[(value & m4) >> (6*0)  ];
            case 3: out[j+2] = b64str[(value & m3) >> (6*1)  ];
            case 2: out[j+1] = b64str[(value & m2) >> (6*2)  ];
            case 1: out[j+0] = b64str[(value & m1) >> (6*3)  ];
        }       
    }
   
    if (in_len/3 < out_len/4)
        switch (in_len%3) {
            case 1: out[in_len*4/3+2]='=';
            case 2: out[in_len*4/3+1]='=';
        }
}

По крайней мере, читабельней мне.

Автор:  Peter Almazov [ Четверг, 05 Август, 2010 04:05 ]
Заголовок сообщения:  Re: Снова о важности базовых техник

Евгений Темиргалеев писал(а):
Хотелось бы услышать мнения касательно сложности проверки соответствия алгоритмов стандарту (но не придирки к переводу кода).
Честно говоря, не врубился в проблему (да и нет желания врубаться), но где гарантия, что индекс не выйдет за границы x:
Код:
t := ORD(x[p + 2]) + ASH(ORD(x[p + 1]), 8) + ASH(ORD(x[p]), 16); INC(p, 3);
P.S. А нуль в конце не надо добавлять?
Кроме того, в сишных алгоритмах мне непонятно: если размер выходной строки (буфера) меньше чем требуется, нахрена заполнять ее началом правильного результата, причем без нуля в конце? Кому нужен такой обрубок? Надо возвращать неуспех, и дело с концом. Поправьте, кто знает.

Автор:  Alexey Veselovsky [ Четверг, 05 Август, 2010 09:53 ]
Заголовок сообщения:  Re: Снова о важности базовых техник

Peter Almazov писал(а):
Кроме того, в сишных алгоритмах мне непонятно: если размер выходной строки (буфера) меньше чем требуется, нахрена заполнять ее началом правильного результата, причем без нуля в конце? Кому нужен такой обрубок? Надо возвращать неуспех, и дело с концом. Поправьте, кто знает.

Для потокового кодирования. Представьте себе, что входящая последовательность данных бесконечна.

Автор:  Евгений Темиргалеев [ Четверг, 05 Август, 2010 10:42 ]
Заголовок сообщения:  Re: Снова о важности базовых техник

Peter Almazov писал(а):
но где гарантия, что индекс не выйдет за границы x:
Код:
t := ORD(x[p + 2]) + ASH(ORD(x[p + 1]), 8) + ASH(ORD(x[p]), 16); INC(p, 3);
P.S. А нуль в конце не надо добавлять?
1) В ББ гарантия - трэп "неверный индекс". В Си гарантий нет. Алгоритмически - проблема клиента, который запрашивает кодирование куска буфера длины len.
2) Нуль добавляет клиент, если он ему нужен.
Код:
X.Encode(ss, dd, p, q, LEN(ss$)); dd[q] := 0X;
Log.String(dd$);

Автор:  Galkov [ Четверг, 05 Август, 2010 11:34 ]
Заголовок сообщения:  Re: Снова о важности базовых техник

Можно попробовать исходить из шаблонов второго уровня: "надо делать так, чтобы тебя лучше понимали коллеги", а не из неких "правил фоматирования", которые конечно же являются следствиями первых.

Тогда для ЭТОГО КОНКРЕТНОГО примера (у меня нет намерения обобщать это на все разновидности кодов!!!):
  • шаблон *out++ = <формула> смотрится таки понятнее чем в Обероне. Думаю, что это от того, что в коде нет ИНЫХ использований данной переменной.
  • но инженер - это наемник. Надо писать на C - будет на C. Если стандарт предприятия (например) предписывает Оберон - ну ладно. Но при этом я все равно постарался бы уложить этот маленький же шаблончик - максимально компактно. Не потому, что это где-то на скрижалях записано (типа базовая техника), а потому-что мне кажется (что всегда будет субъективным факторам), что именно так будет понятнее (уровень более высокий чем базовый). Может так ???
    Код:
      tmp := <длинная формула 1>;
      y[q] := ctable[tmp]; INC(q);
      tmp := <очень длинная формула 2>;
      y[q] := ctable[tmp]; INC(q);
      tmp := <не очень длинная формула 3>;
      y[q] := ctable[tmp]; INC(q);
  • Скобки в этих формУлах раза по три считал. Понимаю головой, что не мог я три раза ошибиться, а внутренней уверенности все равно нет. Пока не перенес в редактор, который на автомате подсвечивает парную - появилась таки. Думаю, что если мы заботимся от том, что это будет читать другой человек, тогда (для данного конкретного случая!!!) разбивать надо. Даже если такого правила нет в списке базовых техник

В общем как-то так. Надо уж мне ставить некую мифическую технику, или нет... Но соображения для данного конкретного случая у меня таковы (про break правда ничего не сказал... ну пока ладно)

Автор:  Peter Almazov [ Четверг, 05 Август, 2010 13:10 ]
Заголовок сообщения:  Re: Снова о важности базовых техник

Евгений Темиргалеев писал(а):
Peter Almazov писал(а):
но где гарантия, что индекс не выйдет за границы x:
Код:
t := ORD(x[p + 2]) + ASH(ORD(x[p + 1]), 8) + ASH(ORD(x[p]), 16); INC(p, 3);
P.S. А нуль в конце не надо добавлять?
1) В ББ гарантия - трэп "неверный индекс". В Си гарантий нет. Алгоритмически - проблема клиента, который запрашивает кодирование куска буфера длины len.
Я немного не об этом. Вы пишете на входе ASSERT(len >= 0, 20). А в p можно, выходит, передавать любой бред. Кстати, не пойму, зачем там передавать что-то отличное от нуля.

Автор:  Илья Ермаков [ Четверг, 05 Август, 2010 14:27 ]
Заголовок сообщения:  Re: Снова о важности базовых техник

Peter Almazov писал(а):
Я немного не об этом. Вы пишете на входе ASSERT(len >= 0, 20). А в p можно, выходит, передавать любой бред. Кстати, не пойму, зачем там передавать что-то отличное от нуля.


Тут тонкая практическая грань уже идёт. Если предусловие на невыход позиции за границы, то оно и так неявно будет проверено при попытке выхода, для функций, которые "узкое место", проверять смысла нет явно.
Неверный же len спровоцирует "странности".

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

Автор:  Илья Ермаков [ Четверг, 05 Август, 2010 17:52 ]
Заголовок сообщения:  Re: Пример алгоритма Base64-кодирования

Евгений Темиргалеев писал(а):
P.P.S. Вариант, который привёл Alexey Veselovsky, я бы стал портировать...

(процитировано в связи с тем, что сообщение при разделении тем ушло в другую ветку)

Автор:  Илья Ермаков [ Четверг, 05 Август, 2010 17:55 ]
Заголовок сообщения:  Re: Пример алгоритма Base64-кодирования

Обсуждение оформления кода табуляциями для данного примера отделено -
viewtopic.php?f=1&t=2770&p=50362

Автор:  Евгений Темиргалеев [ Четверг, 05 Август, 2010 21:06 ]
Заголовок сообщения:  Re: Пример алгоритма Base64-кодирования

Нда... целую ветку наболтали про оформление, а на исходный вопрос, ничего... :roll:

Попробую ещё раз: есть разные реализации процедур кодирования массива (куска массива) алгоритмом base64.
* неструктурная реализация (четыре break основного цикла), которую приводит в качестве примера стандарт
* два структурных варианта: 1 и 2.

Вопрос: как вы считаете - какую реализацию проще проверить на соотвествие стандарту Base64 --- структурную или неструктурную --- на примере этих вариантов? Можно привести свои варианты и дать второй ответ с их учётом.

Автор:  Евгений Темиргалеев [ Четверг, 05 Август, 2010 21:14 ]
Заголовок сообщения:  Re: Пример алгоритма Base64-кодирования

Лично я считаю, что проверка неструктурного варианта на порядок более сложная и является примером неумения программировать:
Galkov писал(а):
...Покажите пожалуйста пример, в котором, опираясь на Дейкстру можно увидеть "неумение программировать"...
Что Вы скажете, тов. Galkov?

Мнение тов. Alexey Veselovsky тоже хотелось бы услышать, т.к. он свой вариант уже привёл...

Автор:  Galkov [ Пятница, 06 Август, 2010 08:21 ]
Заголовок сообщения:  Re: Пример алгоритма Base64-кодирования

Хм... де-жа-вю какое-то... отвечал же вроде. Типа: тильки шо було, та раптом зникло :shock:

Найду, или восстановлю. Ближе к вечеру (запарка сейчас какая-то :( )

Автор:  Alexey Veselovsky [ Пятница, 06 Август, 2010 14:03 ]
Заголовок сообщения:  Re: Пример алгоритма Base64-кодирования

Galkov писал(а):
Хм... де-жа-вю какое-то... отвечал же вроде. Типа: тильки шо було, та раптом зникло :shock:
Найду, или восстановлю. Ближе к вечеру (запарка сейчас какая-то :( )

У меня тоже не то что запарка, а зоопарка какая-то. Так что тоже попозже отвечу.

Автор:  Alexey Veselovsky [ Пятница, 06 Август, 2010 20:46 ]
Заголовок сообщения:  Re: Пример алгоритма Base64-кодирования

Итак, моё мнение (точнее не мнение, а мой опыт. внимание info21 тут нет квантора всеобщности! (вообще его нигде нет, если это не указано явно) ):

1) Обе реализации очень плохо читаются и воспринмаются, до того как прочтешь документацию по base64 и не попытаешься реализовать что-то своё. Т.е. реально до написания своей реализации оба исходника представлялись одинаково низкоуровневой абракадаброй. Т.о. они не самодкументирующиеся.
2) После чтения док и написания своей реализации, оба исходника воспринимаются и понимаются нормально. Т.е. нет проблем с верификацией там и там.
3) С99 алгоритм сделан более прямолинейно и дубово. Соответственно читается абсолютно линейно. Основное отличие от реализации на КП (и, возможно, достоинство) -- он пишет в выходной буфер ASAP, т.е. как только что-то сложилось, так сразу и написали. Не дожидаясь пока прочтутся все 3 байта. Также не используется дополнительный буфер. Т.о. нет фазы копирования в промежуточный буфер.
4) В С99 варианте на каждую итерацию цикла существенно больше проверок. Насколько это скажется на производительности -- затрудняюсь оценить. Но вообще, это не хорошо.
5) В обоих реализация используется цикл while. В обоих релизациях соответственно где-то в потрохах цикла меняется счетчик цикла. Но это не страшно. В обоих случаях он меняется в одном месте. Страшно другое -- всякие разные указатели и индексы в обоих реализациях, которые, вообще говоря, связаны со счетчиком, скачут как бешаные лошади. Т.е. их изменение тонким слоем размазано по всему телу цикла. Верифицировать когда и в каких случаях индексы/указатели смогут вылезти за границы дозволенного -- сложно. Нужно считать и думать.
6) В С99 явный перебор со скобочками и операторами. К тому же вариант форматирования выбран не самый удачный.
7) Вообще в обоих вариантах всё слишком низкоуровнево. Впрочем я уже говорил ;-)
8) В КП-варианте имеем два непонятных аргумента у функции, который используется вроде как локальные переменные. Предполагается что там будет вначале ноль. Но почему бы их не обнулить явно?
9) Возвращаемые значения всё же лучше передавать в виде результата функции.
10) В КП-вариенте предполагается, что выходной буфер всегда достаточного размера, что, в общем то, не очевидно. И может привести к ошибкам. (да, словленный AV это тоже ошибка, трапы в Обероне -- деталь реализации, равно также как и в С99, так что отмазкой не являются).
11) Про thread safe... Интересно в КП варианте, если в процессе кодирования, кто-то слегка чуть-чуть поиграется с p,q. Ведь они переданы по ссылке.
12) В КП варианте увлекаются множеством операций на одной строке. Плохо сказывается на читабельности.

Резюме -- в качестве референсной реализации rfc-писателям не стоило указывать эту C99 реализацию, КП-реализация в качестве референсной также не подошла бы. Слишком много низкоуровневых деталей.

Что касается моей реализации, наверно лучше не мне судить. Но она мне не слишком нравится.
Во-первых используются C-style hasks (см. как я работаю со switch'ами), это не портабельно на другие языки (не С-семейства в плане синтаксиса), т.о. на референсную реализацию для императивных языков не тянет.
Во-вторых всё ещё много низкоуровневых деталей. Реверансы в сторону быстродействия. Те же switch'и например. Т.о. оно не тянет и на общую референсную реализацию (в которой должно быть кристально ясно что такое base64 и без чтения спек).
В-третьих мне не удалось достичь макимальной производительности. Т.е. по паре лишних сравнений на итерацию всё же есть. Т.о. для сурово-промышленной эксплуатации также возможно не лучший вариант.

Что хорошо: переменные циклов и прочее от них зависящее меняется только в потрохах for'a, т.е. не в теле цикла. Проще читать. Больше уверенности, что не будет нагажено в память. Вроде бы получилось создать достаточно человекочитабельный код в плане работы с масками. Т.е. не нужно вспоминать что такое есть 3F и почему это в данном случае хорошо. Это видно просто визуально. Ну и быстродействие всё же не столь ужасное как могло бы быть.

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

Автор:  Alexey Veselovsky [ Пятница, 06 Август, 2010 20:51 ]
Заголовок сообщения:  Re: Пример алгоритма Base64-кодирования

В качестве общего референсного кода, где нет таких низкоуровневых вещей. как указатели, индексы, какие-то там маски, побитовые операции (в т.ч. сдвига) и прочего подобного, я бы предложил вот этот код на erlang'e (моего авторства). По моему, тут удалось достичь цели. (кстати, производительность также не слишком ужасная).

Код:
encode_bin(<<>>,ResStr)-> lists:reverse(ResStr);
encode_bin(Bin, ResStr)->
    {HeadBin,TailBin,Zero} = case Bin of
                                 <<B:24, RestBin/binary>> -> {<<B:24>>     ,RestBin, 0};
                                 <<B:16                >> -> {<<B:16,0:8>> ,<<>>   , 1};
                                 <<B: 8                >> -> {<<B:8 ,0:16>>,<<>>   , 2}
                             end,
    <<A1:6,A2:6,A3:6,A4:6>> = HeadBin,
    encode_bin(TailBin,to_str([A1,A2,A3,A4],ResStr,4-Zero)).

to_str([],ResStr,_)         -> ResStr;
to_str([H|T],ResStr,NonZero)->
    case NonZero of 0 -> to_str(T,[$=|ResStr],NonZero);
                    _ -> to_str(T,[b64e(H)|ResStr],NonZero-1)
    end.


Полный текст модуля можно взять тут: http://paste.org.ru/?k86ho9 или тут: http://erlang.pastebin.com/6fAa8WEW
(откровенно говоря, мне и там и там не нравится подсветка синтаксиса).

Синтаксис работы с бинарными даннами в ерланге превосходен. Особенно в сочетании с сопоставлением с образцом.

Автор:  Info21 [ Суббота, 07 Август, 2010 09:23 ]
Заголовок сообщения:  Re: Пример алгоритма Base64-кодирования

Alexey Veselovsky писал(а):
Итак, моё мнение (точнее не мнение, а мой опыт. внимание info21 тут нет квантора всеобщности!
Вы это не мне, а публике сообщайте. Публике.

Автор:  Александр Шостак [ Суббота, 07 Август, 2010 16:57 ]
Заголовок сообщения:  Re: Пример алгоритма Base64-кодирования

Публике и так всё ясно. А вот вам приходится объяснять.

Страница 1 из 2 Часовой пояс: UTC + 3 часа
Powered by phpBB® Forum Software © phpBB Group
https://www.phpbb.com/