Ориентация главы
Чтобы двоичные данные вошли в текстовый канал (заголовки HTTP, поля JSON, параметры URL), их нужно сперва «перевести» в чисто текстовые байты — модуль codec даёт три стандартных перевода: Base64 (3 байта в 4 символа, разрастание 33% — заголовки данных, JWT, почтовые вложения), Hex (1 байт в 2 символа, разрастание 100% — отладочные дампы, отпечатки ключей, короткие двоичные значения) и Percent (экранирует только нелегальные символы, минимальное разрастание — URL и формы). Тройка разделяет один набор инженерных конвенций: правила ёмкости выводимы (размер буфера не угадывается), отказ не пишет полрезультата (атомарность) и варианты объявляются явно (URL-safe, регистр, пользовательский алфавит — параметры, а не догадки). Эти конвенции роднят главу со строгим разбором главы 26 — каждый вход и выход текстового мира точен до байта.
28.1引入
Три реальных потребности. Почтовое вложение двоичное, а SMTP принимает только текст — Base64 превращает каждые 3 байта в 4 печатных символа, плюс треть к объёму в обмен на проход через почтовый канал. При отладке нужно вывести в журнал короткий кусок ключевого материала — Hex превращает каждый байт в два шестнадцатеричных символа: читаемо человеком, копируемо, сравнимо. Ввод пользователя кладётся в параметр запроса URL — в reports/July 2026 и /, и пробел суть зарезервированные или нелегальные символы URL; кодирование Percent превращает их в %2F и %20, остальные символы проходят как есть — три кодирования под три потребности: «преобразование целиком, читаемое преобразование, минимальное преобразование».
Первый вопрос выбора — всегда сколько терпит канал: разрастание 33% у Base64, 100% у Hex, около 0% у Percent — ответ обычно диктует сам протокол (Content-Transfer-Encoding: base64, percent-encoding из спецификации URL); ваша задача — употребить правильно, а не выбирать свободно.
28.2概念
Сравнение форм тройки
Правила ёмкости: размер выхода всегда вычислим в столбик
Выход Base64 = 4 × ceil(n / 3) (с заполнением); выход Hex = 2n; верхняя граница выхода Percent = 3n (экранированным может оказаться каждый байт). Сперва посчитать ёмкость, потом давать буфер — базовое упражнение главы: семейство Encode пишет в буфер вызывающего и при нехватке ёмкости отказывается целиком, не оставляя полрезультата (атомарность: в выходном буфере не остаётся обрубка, который кто-то мог бы по ошибке употребить), а семейство EncodeNew сразу возвращает владеющий буфер (освобождение xrtFree) — распределение берёт на себя библиотека. Выбор между двумя маршрутами полностью изоморфен развилке «буфер вызывающего против владеющего» из главы 25.
Явные варианты: стандартный и URL-safe
В стандартном алфавите Base64 есть + и / — это зарезервированные символы URL, отсюда вариант URL-safe (замена на - и _), включаемый параметром алфавита в xbase64config (в поле Alphabet передавайте NULL для стандартной таблицы по умолчанию); декодирование строго сверяется с той же конфигурацией — несовпадение алфавита есть отказ. У Hex две формы выхода, верхний и нижний регистр (декодер ест обе). Множество экранирования Percent объявляется по сценарию (у сегментов пути и параметров запроса URL зарезервированы разные символы). Принцип тот же, что в главе 26: вариант — явный параметр, а не угаданное умолчание.
Строгость декодирования
Декодирование столь же строго: нелегальные символы (вне алфавита), невыровненная длина (Base64 не кратен 4), усечённые последовательности экранирования (одинокий % у Percent) — отказ целиком с ошибкой: мусор не входит, обрубок не выходит. Это та же философия дизайна, что STRICT у главы о наборах символов (глава 27) и потребление целого входа у главы о числах (глава 26): текстовая граница — стык поверхности атак и поверхности аварий; расплывчатость здесь никогда не была снисходительностью — всегда миной.
28.3示例
Полная программа: Base64 в двух позах — стандартной и владеющей
Из образца репозитория examples/codec/base64/main.c:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> int main(void) { /* sizeof 含结尾零,编码长度用 sizeof-1 显式排除。 */ static const char Message[] = "xrt codec"; char Encoded[32]; /* 调用方缓冲:12 字符 + 零足够 */ bytes pDecoded; /* 拥有式:解码产物 */ size_t iEncodedSize; size_t iDecodedSize; */ if ( !xrtBase64Encode( Message, sizeof(Message) - 1u, Encoded, sizeof(Encoded), &iEncodedSize, NULL ) ) { return 1; } */ pDecoded = xrtBase64DecodeNew( Encoded, iEncodedSize, &iDecodedSize, NULL ); if ( (pDecoded == NULL) || (iDecodedSize != sizeof(Message) - 1u) || (memcmp(pDecoded, Message, iDecodedSize) != 0) ) { xrtFree(pDecoded); return 2; } printf("%s -> %s\n", Message, Encoded); xrtFree(pDecoded); return 0; }
Что только что произошло. (1) Вход — 9 байтов, правило ёмкости 4 × ceil(9/3) = 12 символов — буфер выдан по формуле, Encode пишет за один шаг. (2) sizeof-1 исключает хвостовой ноль — старое правило из хеширования главы 12: вход кодирования — байтовая строка, а нулевое завершение — лишь случайность C-строки. (3) Декодирование идёт владеющим маршрутом DecodeNew — круговой проход подтверждает правильность кодирования. (4) «Атомарность» стоит проверить руками: умышленно уменьшите буфер — Encode откажется, а выходная область останется нетронутой: никакого полукодированного base64, оставленного кому-то на ошибочное употребление.
Полная программа: круговой проход кодирования Percent
Из examples/codec/percent/main.c:
*/ #include <stdio.h> #include <xrt.h> int main(void) { /* 含路径分隔符与空格的原始 segment——两者都必须被转义。 */ static const char Segment[] = "reports/July 2026"; char Encoded[64]; /* 最坏情况 3n:17 字节 → 51 字符,64 够用 */ bytes pDecoded; size_t iEncodedSize; size_t iDecodedSize; */ if ( !xrtPercentEncode( Segment, sizeof(Segment) - 1u, XRT_STR_LITERAL(""), Encoded, sizeof(Encoded), &iEncodedSize ) ) { return 1; } /* 分配型解码:%XX 还原为字节;非法转义(如 %G0)会失败返回 NULL。 */ pDecoded = xrtPercentDecodeNew( (xstrview){ Encoded, iEncodedSize }, &iDecodedSize ); if ( pDecoded == NULL ) { return 2; } /* 解码产物是二进制缓冲:打印时用显式长度,不依赖结尾零。 */ printf("encoded: %s\ndecoded: %.*s\n", Encoded, (int)iDecodedSize, (const char*)pDecoded); xrtFree(pDecoded); return 0; }
Что только что произошло. (1) Кодирование тронуло ровно два символа: / → %2F, пробел → %20, буквы как были — форма минимальной переписи видна с одного взгляда. (2) Круговой проход без потерь — декодирование Percent строго обратно кодированию; одиночный % или нелегальный hex на стороне декодера — отказ целиком. (3) Точка приземления для URL: поставьте это за парсером строки запроса из главы 25 — и получится полный URL-инструментарий: разбор (string) → кодирование (codec) → передача.
Рельеф ловушек на стороне декодера
У декодирования на три класса ловушек больше, чем у кодирования, и каждый стоит назвать по имени. Дрейф алфавитов: отправитель URL-safe, получатель декодирует стандартной таблицей — -/_ вне стандартной таблицы, отказ целиком — везение; коварнее, когда обе стороны «терпимо» обрабатывают каждая своё и данные молча расползаются. Двусмысленность заполнения: наличие =-заполнения у Base64 диктуется протоколом (сегменты JWT его не несут); реализация, снисходительно прощающая пропуски, — строгий режим отвергает невыровненный вход именно затем, чтобы такая двусмысленность осталась за дверью. Детектор вложенного кодирования: увидев %2520, понимайте — это окаменелость двойного Percent-кодирования; декодер не чинит ошибки процесса выше по течению, он может лишь строго отказаться. Общее решение всех трёх одно: одна и та же конфигурация на обоих концах, строгий режим, отказ — сигнал процесса, а не шум данных.
Граница с перекодированием главы 27
Пара, которую путают: xrtTranscode главы о наборах символов (глава 27) и кодирование этой главы — оба суть «преобразования байтовой формы», где граница? Перекодирование меняет байтовое представление того же символа (китайский знак в UTF-8 и в UTF-16 — один и тот же символ): семантика неизменна, круговой проход без потерь. Кодирование меняет форму предъявления байтов (двоичный поток становится печатными символами): семантика — «эквивалентная перевозка», а не «тот же символ». Мнемоника: вход и выход — две записи одного и того же? Тогда перекодирование; иначе — кодирование. Сцепка тоже обычна: двоичные данные → Base64 → UTF-16 в API Windows, каждый шаг сам по себе корректен.
28.4契约
- Ёмкость выводима: Base64
4⌈n/3⌉, Hex2n, Percent ≤3n; сперва расчёт, потом буфер. - Атомарность: нехватка буфера вызывающего — отказ целиком, полрезультата не пишется.
- Два маршрута:
Encode/Decode— в буфер вызывающего,EncodeNew/DecodeNew— владеющие продукты (xrtFree). - Явные варианты: URL-safe, регистр, множество экранирования — параметры; декодер строго сверяется с объявленным алфавитом.
- Строгое декодирование: нелегальные символы, невыровненная длина, усечённое экранирование — отказ целиком.
От примеров к инженерии: три хозяина кодирования
Три способа крепления кодирования в инженерии. Выход протокола: сборка JWT, перенос двоичных данных в заголовках HTTP — кодирование на выходе, декодирование на входе, по одному разу с каждой стороны канала и никогда дважды (двойное кодирование — реальная авария: Percent дважды превращает %20 в %2520). Журнальные дампы: Hex — разово, коротко, человеку; в паре со строками формата главы 26 для пометки смещений. Персистентные поля: двоичные данные внутри JSON/конфигурационных файлов (в главе 31 вы увидите поддержку Base64 в JSON) — по одному кодированию при записи и чтении, семантика поля объявляется в схеме. Общая дисциплина трёх хозяев: кодирование — разовое пограничное действие, а не промежуточное состояние — «закодировали ещё раз» и «недокодировали» оба означают ошибку в дизайне процесса.
Таблица сравнения разрастания
| 100 байтов данных | Base64 | Hex | Percent (худший случай, всё экранировано) |
|---|---|---|---|
| Размер выхода | 136 (с заполнением 4⌈100/3⌉) | 200 | ≤300 |
| Канал | почта/заголовки данных/JSON | дампы/отпечатки | URL/формы |
| Читаемость | нет | хорошая | исходный текст сохранён |
Ключевое напоминание таблицы: «минимальное разрастание» Percent выполняется только вне худшего случая — почти чистый ASCII расширяется около нуля, а полностью экранированные двоичные данные бьют худшую тройку. Выбрать Percent для двоичных данных — ошибка выбора (он придуман для немногих специальных символов внутри текста); семантика множества экранирования и определяет его применимость.
Типичные ошибки
Симптом: аудит безопасности обнаруживает, что «зашифрованное поле» — просто Base64; на пентесте токен декодируется в один шаг.
Причина: Base64 — кодирование, а не шифрование — обратимо любым, никакой конфиденциальности; вводящая в заблуждение сила «64» в имени — родня заблуждения «хеш не шифрование» (глава 12).
char arrToken[64]; xrtBase64Encode(SecretKey, sizeof(SecretKey), arrToken, sizeof(arrToken), NULL, NULL); Send(arrToken); /* "секретность кодированием" - получатель декодирует одним шагом и берёт ключ */
/* правильный канал для конфиденциальных данных: криптография тома 8 (шифрование) или система токенов (глава 11 SecureText) */ /* Base64 решает только "двоичные данные в текстовый канал", но не секретность */
Симптом: значение Base64 с + или /, помещённое в URL, изредка не декодируется на сервере или данные расползаются — только на определённых значениях.
Причина: + в строке запроса трактуется как пробел, / — разделитель пути — стандартный алфавит сталкивается с набором символов URL.
xrtBase64Encode(Data, Size, arrUrl, sizeof(arrUrl), NULL, NULL); /* в алфавите по умолчанию есть + / */ AppendQueryParam(Url, arrUrl); /* + становится пробелом, / режет путь - декодирование обречено */
xrtBase64Encode(Data, Size, arrUrl, sizeof(arrUrl), NULL, &tUrlSafe); /* алфавит URL-safe: - и _ */ AppendQueryParam(Url, arrUrl); /* все символы безопасны для URL */
Практические задания
Для входов в 1, 2, 3, 4, 9 и 10 байтов посчитайте длину выхода Base64 в столбик и проверьте кодированием (шесть пар чисел: сперва вручную, потом сверка); затем умышленно коротким буфером проверьте атомарный отказ и нетронутую выходную область.
Реализуйте dump(视图, 前缀) (представление, префикс): вывод потока байтов в Hex, по 16 байтов на строку, смещение в начале строки — шестнадцатеричный вид отладчика это и есть он. Подсказка: смещения помечайте строками формата из главы 26 (hex в духе %06X).
Реализуйте url_encode_query(基准URL, 键值对数组) (базовый URL, массив пар ключ-значение): ключи и значения кодируются Percent по отдельности, строка запроса собирается построителем главы 25, на выходе — полный URL. Критерии приёмки: десять входов с китайскими ключами и значениями и &/=/пробелом/+ кодируются так, что стандартный парсер URL восстанавливает их верно; круговой проход декодирования совпадает байт в байт.
Итоги
| Тема | Кратко |
|---|---|
| Тройка | Base64 (разрастание 33%) / Hex (100%) / Percent (≈0%) — выбор по правилам канала |
| Ёмкость | Base64 4⌈n/3⌉, Hex 2n, Percent ≤3n — сперва расчёт, потом буфер |
| Атомарность | нехватка буфера — отказ целиком, полрезультата не пишется |
| Два маршрута | вариант To в буфер / вариант New владеющий — изоморфно главам о контейнерах |
| Варианты | URL-safe (-/_), регистр Hex, множество экранирования Percent — явные параметры |
| Красная линия | кодирование — не шифрование; конфиденциальное — через том 8 или SecureText |
| Три хозяина | выход протокола кодирует один раз / журнальные дампы / персистентные поля — кодирование есть пограничное действие, не промежуточное состояние |
| Граница с перекодированием | две записи одного символа = перекодирование; двоичное в печатное = кодирование |
📖 Справочники API в этой главе:codec (26 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки