本章导读
二进制数据要进文本通道(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、URL 规范的 percent-encoding),你只需要正确使用而不是自由选择。
28.2概念
三件套的形态对比
容量规则:输出大小永远可笔算
Base64 输出 = 4 × ceil(n / 3)(加填充);Hex 输出 = 2n;Percent 输出上限 = 3n(每字节都可能被转义)。先算容量再给缓冲是这一章的基本功——Encode 族写调用方缓冲、容量不足整体失败且不写半个结果(原子性:输出缓冲不会留下半截可被误用的内容),EncodeNew 族直接返回拥有式缓冲(xrtFree 释放)让库替你分配。两条路线的选择与第 25 章"调用方缓冲 vs 拥有式"完全同构。
变体显式:标准与 URL-safe
Base64 标准字母表里有 + 和 /——它们在 URL 里是保留字符,所以有 URL-safe 变体(换成 - 和 _),通过 xbase64config 的字母表参数启用(Alphabet 字段传 NULL 用默认标准表);解码按同一配置严格校验,字母表不匹配就是失败。Hex 有大小写两种输出形态(解码两种都吃)。Percent 的转义集合按场景声明(URL 路径段与查询参数的保留字不同)。原则同第 26 章:变体是显式参数,不猜默认。
解码的严格性
解码同样严格:非法字符(字母表外)、长度不对齐(Base64 非 4 倍数)、截断的转义序列(Percent 的孤立 %)都整体失败并留错误——垃圾进不去、半截出不来。这与 charset 章(第 27 章)的 STRICT 策略、number 章(第 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; }
刚才发生了什么。 ① 输入 9 字节,容量规则 4 × ceil(9/3) = 12 字符——缓冲按公式给足,Encode 一步写完。② sizeof-1 排除结尾零——第 12 章哈希的老规矩:编码的输入是字节串,零结尾只是 C 字符串的意外。③ 解码用 DecodeNew 走拥有式路线——往返验证编码正确。④ "原子性"值得亲手验证:把缓冲故意给小,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; }
刚才发生了什么。 ① 编码只动了两个字符:/ → %2F、空格 → %20,字母原样——最小改写的形态一目了然。② 解码往返无损——Percent 的解码与编码严格互逆,孤立 % 或非法十六进制在解码侧整体失败。③ URL 场景的落点:把它接在第 25 章的查询串解析器后面,就是完整的 URL 工具链——解析(string)→ 编码(codec)→ 传输。
解码侧的陷阱地形
解码比编码多三类陷阱,都值得点名。字母表漂移:发送方 URL-safe、接收方按标准表解——-/_ 不在标准表里,整体失败算走运;更阴的是双方都"宽容"地各自处理,数据悄悄错位。填充歧义:Base64 的 = 填充有协议明确该不该带(JWT 的段不带),实现宽容省略——严格模式拒绝非对齐输入正是为了把这类歧义挡在门外。嵌套编码探测:看到 %2520 要意识到这是双重 Percent 编码的化石——解码器修不了上游的流程错误,它只能严格地失败。三类的共同解法都是同一条:两端同一份配置、严格模式、失败当作流程信号而不是数据噪声。
与第 27 章转码的分工
容易混淆的一对:charset 的 xrtTranscode(第 27 章)与本章的编解码都是"字节形态变换",分界线在哪?转码改变的是同一字符的不同字节表示(UTF-8 的"你"与 UTF-16 的"你"是同一个字符),语义不变、可无损往返;编码改变的是字节的呈现形态(二进制字节流变成可打印字符),语义是"等价搬运"而非"同一字符"。判断口诀:输入输出是不是同一种东西的两种写法——是则转码,否则编码。两者串联也常见:二进制 → Base64 → UTF-16 交给 Windows API,两步各自独立正确。
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 章会看到 JSON 的 Base64 支持)——存取各一次,字段语义在 schema 里声明。三宿主的共同纪律:编码是一次性边界动作,不是中间状态——"编了又编"与"半截解码"都说明流程设计有问题。
一张膨胀对照表
| 数据 100 字节 | Base64 | Hex | Percent(全转义最坏) |
|---|---|---|---|
| 输出大小 | 136(含填充 4⌈100/3⌉) | 200 | ≤300 |
| 通道 | 邮件/数据头/JSON | 转储/指纹 | URL/表单 |
| 可读性 | 无 | 好 | 保留原文 |
表的关键提醒:Percent 的"最小膨胀"只在最坏情况之外成立——纯 ASCII 安全字符几乎零膨胀,二进制数据全转义时最坏 3 倍。给二进制数据选 Percent 是选型错误(它为文本里的少数特殊字符设计),转义集合的语义决定了它的适用面。
常见错误
症状:安全审计发现"加密字段"只是 Base64;渗透测试中令牌被直接解码。
原因:Base64 是编码不是加密——人人可逆,没有任何保密性;名字里带"64"的迷惑性与"哈希不是加密"(第 12 章)同族。
char arrToken[64]; xrtBase64Encode(SecretKey, sizeof(SecretKey), arrToken, sizeof(arrToken), NULL, NULL); Send(arrToken); /* "凭编码保密"——接收方一步解码就拿到密钥 */
/* 机密数据的正确通道:卷八 crypto(加密)或令牌体系(第 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 输出长度并编码验证(6 组数字先手算后对拍);再用故意不足的缓冲验证原子失败、输出区未被写。
实现 dump(视图, 前缀):按 Hex 输出字节流、每 16 字节一行、行首标偏移——调试器的十六进制视图就是它。提示:偏移用第 26 章的格式串(%06X 风格的十六进制)。
实现 url_encode_query(基准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 转义集合——显式参数 |
| 红线 | 编码不是加密;机密走卷八或 SecureText |
| 三宿主 | 协议出口一次编码 / 日志转储 / 持久化字段——编码是边界动作不是中间状态 |
| 与转码分界 | 同一字符两种写法=转码;二进制变可打印=编码 |
📖 本章涉及的 API 参考手册:codec(26 个条目)
点击查看完整签名、参数约束、返回值与错误说明