|
| 1 | +:sectnums: |
| 2 | +:sectnumlevels: 5 |
| 3 | + |
| 4 | += UTL_ENCODE 包实现原理 |
| 5 | + |
| 6 | +== 概述 |
| 7 | + |
| 8 | +`UTL_ENCODE` 是 IvorySQL Oracle 兼容扩展(`ivorysql_ora`)中的内置包,提供与 Oracle 数据库兼容的 Base64 编码与解码功能。本文档描述 `BASE64_ENCODE` 和 `BASE64_DECODE` 两个函数的设计目标、实现原理及关键技术细节。 |
| 9 | + |
| 10 | +== 文件结构 |
| 11 | + |
| 12 | +[source,text] |
| 13 | +---- |
| 14 | +contrib/ivorysql_ora/ |
| 15 | +├── src/builtin_packages/utl_encode/ |
| 16 | +│ ├── utl_encode.c # C 函数实现 |
| 17 | +│ └── utl_encode--1.0.sql # SQL 注册和 PL/iSQL 包声明 |
| 18 | +├── sql/utl_encode.sql # 回归测试用例 |
| 19 | +└── expected/utl_encode.out # 回归测试期望输出 |
| 20 | +---- |
| 21 | + |
| 22 | +== Oracle 兼容目标 |
| 23 | + |
| 24 | +Oracle 的 `UTL_ENCODE` 包定义以下接口: |
| 25 | + |
| 26 | +[source,sql] |
| 27 | +---- |
| 28 | +-- 编码:将二进制 RAW 数据转换为 Base64 ASCII 字节序列 |
| 29 | +UTL_ENCODE.BASE64_ENCODE(r IN RAW) RETURN RAW |
| 30 | +
|
| 31 | +-- 解码:将 Base64 ASCII 字节序列还原为二进制 RAW 数据 |
| 32 | +UTL_ENCODE.BASE64_DECODE(r IN RAW) RETURN RAW |
| 33 | +---- |
| 34 | + |
| 35 | +在 IvorySQL 中,Oracle 的 `RAW` 类型映射为 PostgreSQL 的 `bytea`,因此两个函数的 C 签名均为 `bytea -> bytea`。 |
| 36 | + |
| 37 | +== BASE64_ENCODE 实现原理 |
| 38 | + |
| 39 | +=== 设计目标 |
| 40 | + |
| 41 | +Oracle `BASE64_ENCODE` 采用 RFC 1521(MIME)格式:每 64 个 Base64 字符后插入一个换行符(`\n`,即 LF),包括最后一行。 |
| 42 | + |
| 43 | +PostgreSQL 内置的 `encode(bytea, 'base64')` 遵循 RFC 2045,每 76 个字符换行,且末尾不保证有换行符。因此,需要自行实现换行逻辑。 |
| 44 | + |
| 45 | +=== 编码流程 |
| 46 | + |
| 47 | +[source,text] |
| 48 | +---- |
| 49 | +输入 bytea(src_len 字节) |
| 50 | + │ |
| 51 | + ▼ |
| 52 | +pg_b64_encode() ← PostgreSQL 内部函数,输出纯 Base64(无换行) |
| 53 | + │ |
| 54 | + ▼ |
| 55 | +raw_b64(b64_len 字节) ← 长度 = ceil(src_len / 3) × 4 |
| 56 | + │ |
| 57 | + ▼ |
| 58 | +按 64 字符分块,每块追加 '\n' |
| 59 | + │ |
| 60 | + ▼ |
| 61 | +输出 bytea(b64_len + num_lines 字节) |
| 62 | +---- |
| 63 | + |
| 64 | +=== 输出长度计算 |
| 65 | + |
| 66 | +[cols="2,3",options="header"] |
| 67 | +|=== |
| 68 | +|参数 |公式 |
| 69 | +|纯 Base64 长度 |`b64_len = pg_b64_enc_len(src_len) = ceil(src_len / 3) × 4` |
| 70 | +|行数 |`num_lines = ceil(b64_len / 64)` |
| 71 | +|最终输出长度 |`result_len = b64_len + num_lines` |
| 72 | +|=== |
| 73 | + |
| 74 | +例如,编码 `Hello`(5 字节)时: |
| 75 | + |
| 76 | +* `b64_len` = 8(`SGVsbG8=`) |
| 77 | +* `num_lines` = 1(8 ≤ 64) |
| 78 | +* `result_len` = 8 + 1 = 9 字节(`SGVsbG8=\n`) |
| 79 | + |
| 80 | +边界情况如下: |
| 81 | + |
| 82 | +[cols="1,1,1,1",options="header"] |
| 83 | +|=== |
| 84 | +|输入字节数 |`b64_len` |行数 |输出字节数 |
| 85 | +|48 |64 |1 |65 |
| 86 | +|49 |68 |2 |70 |
| 87 | +|96 |128 |2 |130 |
| 88 | +|=== |
| 89 | + |
| 90 | +=== 关键代码 |
| 91 | + |
| 92 | +[source,c] |
| 93 | +---- |
| 94 | +/* 计算纯 Base64 长度,再计算行数 */ |
| 95 | +b64_len = pg_b64_enc_len(src_len); |
| 96 | +num_lines = (b64_len + 63) / 64; |
| 97 | +result_len = b64_len + num_lines; |
| 98 | +
|
| 99 | +/* 调用 PostgreSQL 内部编码函数(不含换行) */ |
| 100 | +encoded_len = pg_b64_encode(src_data, src_len, raw_b64, b64_len); |
| 101 | +
|
| 102 | +/* 按 64 字符切块,逐块写入并追加 LF */ |
| 103 | +while (remaining > 0) |
| 104 | +{ |
| 105 | + chunk = (remaining >= 64) ? 64 : remaining; |
| 106 | + memcpy(dst, p, chunk); |
| 107 | + dst += chunk; |
| 108 | + p += chunk; |
| 109 | + remaining -= chunk; |
| 110 | + *dst++ = '\n'; |
| 111 | +} |
| 112 | +---- |
| 113 | + |
| 114 | +=== 边界行为 |
| 115 | + |
| 116 | +[cols="2,3",options="header"] |
| 117 | +|=== |
| 118 | +|输入 |输出 |
| 119 | +|`NULL` |`NULL`,由 SQL 层的 `STRICT` 修饰符处理 |
| 120 | +|空 `bytea`(0 字节) |空 `bytea`(0 字节) |
| 121 | +|任意非空二进制 |Base64 文本,每 64 个字符一行,末行也有 `\n` |
| 122 | +|=== |
| 123 | + |
| 124 | +== BASE64_DECODE 实现原理 |
| 125 | + |
| 126 | +=== 设计目标 |
| 127 | + |
| 128 | +`BASE64_DECODE` 接受 `BASE64_ENCODE` 产生的带换行 Base64 字节序列,并将其还原为原始二进制数据。PostgreSQL 内部的 `pg_b64_decode()` 拒绝所有空白字符,而 Oracle 编码输出中包含 `\n`,因此必须在解码前剥离空白。 |
| 129 | + |
| 130 | +=== 解码流程 |
| 131 | + |
| 132 | +[source,text] |
| 133 | +---- |
| 134 | +输入 bytea(含 \n 的 Base64 字节序列) |
| 135 | + │ |
| 136 | + ▼ |
| 137 | +剥离空白字符 |
| 138 | +过滤 '\n'、'\r'、'\t' 和空格 |
| 139 | + │ |
| 140 | + ▼ |
| 141 | +clean_buf(无空白的纯 Base64 字符) |
| 142 | + │ |
| 143 | + ▼ |
| 144 | +clean_len == 0? ── 是 ──▶ 返回空 bytea |
| 145 | + │ 否 |
| 146 | + ▼ |
| 147 | +pg_b64_decode() ← PostgreSQL 内部函数 |
| 148 | + │ |
| 149 | + ▼ |
| 150 | +decoded_len < 0? ── 是 ──▶ ERROR: invalid base64 input |
| 151 | + │ 否 |
| 152 | + ▼ |
| 153 | +输出 bytea(decoded_len 字节) |
| 154 | +---- |
| 155 | + |
| 156 | +=== 空白剥离策略 |
| 157 | + |
| 158 | +接受的空白字符包括 `\n`(LF)、`\r`(CR)、`\t`(TAB)和空格。该设计同时兼容: |
| 159 | + |
| 160 | +* Oracle `BASE64_ENCODE` 输出的 `\n` 换行 |
| 161 | +* Windows 风格的 `\r\n` 换行 |
| 162 | +* 人工格式化时引入的 TAB 和空格 |
| 163 | + |
| 164 | +[source,c] |
| 165 | +---- |
| 166 | +for (i = 0; i < src_len; i++) |
| 167 | +{ |
| 168 | + unsigned char c = (unsigned char) src_data[i]; |
| 169 | +
|
| 170 | + if (c != '\n' && c != '\r' && c != '\t' && c != ' ') |
| 171 | + clean_buf[clean_len++] = src_data[i]; |
| 172 | +} |
| 173 | +---- |
| 174 | + |
| 175 | +=== 错误处理 |
| 176 | + |
| 177 | +[cols="2,3",options="header"] |
| 178 | +|=== |
| 179 | +|情形 |行为 |
| 180 | +|`NULL` 输入 |返回 `NULL`,由 `STRICT` 修饰符处理 |
| 181 | +|空 `bytea` 输入 |返回空 `bytea` |
| 182 | +|纯空白输入(如 `\x0a0d200a`) |返回空 `bytea` |
| 183 | +|无效 Base64 字符 |抛出 `ERROR: UTL_ENCODE.BASE64_DECODE: invalid base64 input`,错误码为 `ERRCODE_INVALID_PARAMETER_VALUE` |
| 184 | +|=== |
| 185 | + |
| 186 | +== PL/iSQL 包封装 |
| 187 | + |
| 188 | +C 函数注册在 `sys` 模式中,再由 PL/iSQL 包封装,对外提供 Oracle 风格的调用接口: |
| 189 | + |
| 190 | +[source,sql] |
| 191 | +---- |
| 192 | +-- 在 sys 模式中注册 C 函数 |
| 193 | +CREATE FUNCTION sys.utl_encode_base64_encode(bytea) RETURNS bytea |
| 194 | + AS 'MODULE_PATHNAME', 'ivorysql_utl_encode_base64_encode' |
| 195 | + LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; |
| 196 | +
|
| 197 | +CREATE FUNCTION sys.utl_encode_base64_decode(bytea) RETURNS bytea |
| 198 | + AS 'MODULE_PATHNAME', 'ivorysql_utl_encode_base64_decode' |
| 199 | + LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; |
| 200 | +
|
| 201 | +-- 对外提供接口的 PL/iSQL 包 |
| 202 | +CREATE PACKAGE utl_encode AS |
| 203 | + FUNCTION base64_encode(r IN RAW) RETURN RAW; |
| 204 | + FUNCTION base64_decode(r IN RAW) RETURN RAW; |
| 205 | +END utl_encode; |
| 206 | +
|
| 207 | +CREATE PACKAGE BODY utl_encode AS |
| 208 | + FUNCTION base64_encode(r IN RAW) RETURN RAW IS |
| 209 | + BEGIN RETURN utl_encode_base64_encode(r); END; |
| 210 | +
|
| 211 | + FUNCTION base64_decode(r IN RAW) RETURN RAW IS |
| 212 | + BEGIN RETURN utl_encode_base64_decode(r); END; |
| 213 | +END utl_encode; |
| 214 | +---- |
| 215 | + |
| 216 | +调用链路为:`utl_encode.base64_encode(r)` → PL/iSQL 包体 → `sys.utl_encode_base64_encode(bytea)` → C 函数。 |
| 217 | + |
| 218 | +== 与 PostgreSQL 内置函数的差异 |
| 219 | + |
| 220 | +[cols="2,2,2",options="header"] |
| 221 | +|=== |
| 222 | +|特性 |PostgreSQL `encode(x, 'base64')` |Oracle `UTL_ENCODE.BASE64_ENCODE` |
| 223 | +|换行标准 |RFC 2045(76 字符/行) |RFC 1521(64 字符/行) |
| 224 | +|末行换行 |无 |有(`\n`) |
| 225 | +|输入和输出类型 |`bytea` → `text` |`RAW` → `RAW`(均为 `bytea`) |
| 226 | +|解码时空白处理 |`decode()` 接受换行 |`pg_b64_decode()` 不接受,需预处理 |
| 227 | +|=== |
| 228 | + |
| 229 | +== 回归测试覆盖 |
| 230 | + |
| 231 | +测试文件为 `contrib/ivorysql_ora/sql/utl_encode.sql`。 |
| 232 | + |
| 233 | +[cols="2,3",options="header"] |
| 234 | +|=== |
| 235 | +|测试类型 |覆盖内容 |
| 236 | +|NULL 边界 |NULL 输入返回 NULL |
| 237 | +|空输入边界 |0 字节 `bytea` 返回 0 字节 `bytea` |
| 238 | +|已知值验证 |`Hello` 编码结果为 `SGVsbG8=\n`(9 字节) |
| 239 | +|行断边界 |48 字节 → 1 行 65 字节;49 字节 → 2 行 70 字节 |
| 240 | +|大输入 |200 字节数据的多行编解码 |
| 241 | +|往返一致性 |`decode(encode(x)) = x` |
| 242 | +|CRLF 兼容 |正确剥离 `\r\n` 换行 |
| 243 | +|纯空白输入 |`\x0a0d200a` 解码后返回空 `bytea` |
| 244 | +|PL/iSQL 接口 |通过包调用验证端到端路径 |
| 245 | +|=== |
0 commit comments