Files
encryptx/base64x/base64x_test.go
T
2026-09-30 15:49:12 +08:00

364 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package base64x_test
import (
"bytes"
"encoding/base64"
"strings"
"testing"
"code.yun.ink/pkg/encryptx/base64x"
)
// variants 汇总四个编解码变体,便于表驱动测试。
var variants = []struct {
name string
encode func([]byte) string
decode func(string) ([]byte, error)
// alphabetURL 表示该变体使用 URL 安全字母表(不含 + 与 /)。
alphabetURL bool
// padded 表示该变体输出带 = 填充。
padded bool
}{
{"标准", base64x.Encode, base64x.Decode, false, true},
{"URL安全", base64x.EncodeURL, base64x.DecodeURL, true, true},
{"标准无填充", base64x.EncodeRaw, base64x.DecodeRaw, false, false},
{"URL安全无填充", base64x.EncodeRawURL, base64x.DecodeRawURL, true, false},
}
// payloads 覆盖空、短、UTF-8 与二进制等各类输入。
var payloads = [][]byte{
nil,
{},
[]byte("a"),
[]byte("ab"),
[]byte("abc"),
[]byte("Hello, World!"),
[]byte("中文与 emoji 🚀 混合内容"),
bytes.Repeat([]byte{0x00}, 64),
bytes.Repeat([]byte{0xff, 0x00, 0x7f, 0x80}, 128),
func() []byte {
b := make([]byte, 256)
for i := range b {
b[i] = byte(i)
}
return b
}(),
}
// TestRoundTrip 校验四个变体各自的编解码往返一致。
func TestRoundTrip(t *testing.T) {
for _, v := range variants {
for i, payload := range payloads {
encoded := v.encode(payload)
decoded, err := v.decode(encoded)
if err != nil {
t.Fatalf("%s 用例 %d: 解码失败: %v", v.name, i, err)
}
if !bytes.Equal(decoded, payload) {
t.Errorf("%s 用例 %d: 往返不一致,得到 %x,期望 %x", v.name, i, decoded, payload)
}
}
}
}
// TestEncodingFormat 校验各变体的字符集与填充约定。
func TestEncodingFormat(t *testing.T) {
// 选用会使标准 Base64 产生 "+" 与 "/" 的字节,以验证 URL 安全变体确实替换了它们。
payload := []byte{0xfb, 0xff, 0xbf, 0x00, 0xfe, 0x3e, 0x3f}
if got := base64x.Encode(payload); !strings.ContainsAny(got, "+/") {
t.Fatalf("测试数据未能触发标准字母表的 +/ 字符,实际编码为 %q", got)
}
for _, v := range variants {
encoded := v.encode(payload)
if v.alphabetURL {
if strings.ContainsAny(encoded, "+/") {
t.Errorf("%s 编码结果含非 URL 安全字符: %q", v.name, encoded)
}
}
hasPadding := strings.HasSuffix(encoded, "=")
if hasPadding != v.padded {
t.Errorf("%s 填充状态错误: 编码为 %q, 期望带填充=%v", v.name, encoded, v.padded)
}
if v.padded && len(encoded)%4 != 0 {
t.Errorf("%s 带填充编码长度应为 4 的倍数: %q", v.name, encoded)
}
if !isASCII(encoded) {
t.Errorf("%s 编码结果含非 ASCII 字符: %q", v.name, encoded)
}
}
}
// TestEncodeEdgeLengths 校验填充边界(1/2/3 字节分组)的编码结果符合 RFC 4648。
func TestEncodeEdgeLengths(t *testing.T) {
cases := []struct {
in []byte
std string
raw string
url string
rawU string
}{
{[]byte("f"), "Zg==", "Zg", "Zg==", "Zg"},
{[]byte("fo"), "Zm8=", "Zm8", "Zm8=", "Zm8"},
{[]byte("foo"), "Zm9v", "Zm9v", "Zm9v", "Zm9v"},
{[]byte("foob"), "Zm9vYg==", "Zm9vYg", "Zm9vYg==", "Zm9vYg"},
{[]byte("fooba"), "Zm9vYmE=", "Zm9vYmE", "Zm9vYmE=", "Zm9vYmE"},
{[]byte("foobar"), "Zm9vYmFy", "Zm9vYmFy", "Zm9vYmFy", "Zm9vYmFy"},
{[]byte{0xfb, 0xff, 0xbf}, "+/+/", "+/+/", "-_-_", "-_-_"},
}
for _, c := range cases {
if got := base64x.Encode(c.in); got != c.std {
t.Errorf("Encode(%q) = %q, 期望 %q", c.in, got, c.std)
}
if got := base64x.EncodeRaw(c.in); got != c.raw {
t.Errorf("EncodeRaw(%q) = %q, 期望 %q", c.in, got, c.raw)
}
if got := base64x.EncodeURL(c.in); got != c.url {
t.Errorf("EncodeURL(%q) = %q, 期望 %q", c.in, got, c.url)
}
if got := base64x.EncodeRawURL(c.in); got != c.rawU {
t.Errorf("EncodeRawURL(%q) = %q, 期望 %q", c.in, got, c.rawU)
}
}
}
// TestDecodeRejectsGarbage 校验 Decode* 对垃圾输入返回 nil 切片与 error。
//
// 所有用例都已用实验确认对四个变体都真正非法(见 TestStdlibBehavior)。
func TestDecodeRejectsGarbage(t *testing.T) {
garbage := []struct {
name string
in string
}{
{"非字母表字符", "!!!not base64!!!"},
{"含空格", "SGVs bG8="},
{"含制表符", "SGVs\tbG8="},
{"只有填充", "===="},
{"单个填充", "="},
{"首字符即填充", "=AAAA"},
{"残留 1 个字符", "Z"},
{"错误填充位置", "SG=VsbG8="},
{"两段拼接的填充", "Zg==Zg=="},
{"填充数量过多", "A==="},
{"emoji 输入", "🚀🚀🚀🚀"},
{"中文输入", "你好世界"},
{"二进制控制字符", "\x00\x01\x02\x03"},
}
for _, v := range variants {
for _, g := range garbage {
decoded, err := v.decode(g.in)
if err == nil {
t.Errorf("%s 解码 %q(%s)应当失败,却成功得到 %x", v.name, g.in, g.name, decoded)
}
if decoded != nil {
t.Errorf("%s 解码 %q(%s)失败时应返回 nil,实际 %x", v.name, g.in, g.name, decoded)
}
if err != nil && err.Error() == "" {
t.Errorf("%s 解码 %q 返回了空错误信息", v.name, g.in)
}
}
}
}
// TestStdlibBehavior 固定住 Go encoding/base64 解码器的实际行为
// (以下断言全部由实验测得,不是推测)。这些行为会随封装一起暴露给调用方:
//
// 1. 解码器会**忽略** \r 与 \n(MIME 历史行为),但空格与制表符仍然非法;
// 2. 标准与 URL 两套字母表在解码时**不通用**:Std 只接受 '+'/'/',
// URL 只接受 '-'/'_'(编码方向同样区分);
// 3. 无填充解码器**拒绝**任何 '=' 填充,即使位置正确;
// 4. 带填充解码器接受"多余填充"与非规范尾部比特,
// 例如 "SGV=" 会被解码为 2 字节而不报错。
//
// 因此调用方不能依赖解码器拒绝所有畸形数据,需要自行约束输入格式。
func TestStdlibBehavior(t *testing.T) {
// 1. CR/LF 被忽略,空格与制表符非法。
if got, err := base64x.Decode("SGVsbG8=\n"); err != nil || !bytes.Equal(got, []byte("Hello")) {
t.Errorf("预期换行被忽略并解码出 Hello,得到 %x, err=%v", got, err)
}
if got, err := base64x.Decode("SGVsbG8=\r\n"); err != nil || !bytes.Equal(got, []byte("Hello")) {
t.Errorf("预期 CRLF 被忽略并解码出 Hello,得到 %x, err=%v", got, err)
}
if _, err := base64x.Decode("SGVs bG8="); err == nil {
t.Error("空格应导致解码失败")
}
if _, err := base64x.Decode("SGVs\tbG8="); err == nil {
t.Error("制表符应导致解码失败")
}
// 无填充解码器遇到换行同样失败(因为输入本身不完整/带填充)。
if _, err := base64x.DecodeRaw("SGVsbG8=\n"); err == nil {
t.Error("无填充解码器应拒绝含填充的输入")
}
// 2. 字母表不通用。
if got, err := base64x.Decode("a+b/"); err != nil || !bytes.Equal(got, []byte{0x6b, 0xe6, 0xff}) {
t.Errorf("标准解码器应接受 a+b/,得到 %x, err=%v", got, err)
}
if _, err := base64x.Decode("a-b_"); err == nil {
t.Error("标准解码器应拒绝 URL 字母表 a-b_")
}
if got, err := base64x.DecodeURL("a-b_"); err != nil || !bytes.Equal(got, []byte{0x6b, 0xe6, 0xff}) {
t.Errorf("URL 解码器应接受 a-b_,得到 %x, err=%v", got, err)
}
if _, err := base64x.DecodeURL("a+b/"); err == nil {
t.Error("URL 解码器应拒绝标准字母表 a+b/")
}
// 编码方向严格区分字母表。
if got := base64x.Encode([]byte{0xfb, 0xff, 0xbf}); got != "+/+/" {
t.Errorf("标准编码应为 +/+/,得到 %q", got)
}
if got := base64x.EncodeURL([]byte{0xfb, 0xff, 0xbf}); got != "-_-_" {
t.Errorf("URL 编码应为 -_-_,得到 %q", got)
}
// 3. 无填充解码器拒绝一切填充。
for _, in := range []string{"Zg==", "Zm8=", "SGVsbG8=", "AA==", "AAA="} {
if _, err := base64x.DecodeRaw(in); err == nil {
t.Errorf("无填充解码器应拒绝带填充输入 %q", in)
}
if _, err := base64x.DecodeRawURL(in); err == nil {
t.Errorf("无填充 URL 解码器应拒绝带填充输入 %q", in)
}
}
// 4. 带填充解码器接受多余填充与非规范尾部比特。
if got, err := base64x.Decode("SGV="); err != nil || !bytes.Equal(got, []byte("He")) {
t.Errorf("预期 SGV= 被宽松接受为 He(非规范尾部比特),得到 %x, err=%v", got, err)
}
// 而无填充解码器接受非规范尾部比特,但长度必须合法。
if got, err := base64x.DecodeRaw("SGV"); err != nil || !bytes.Equal(got, []byte("He")) {
t.Errorf("无填充解码器预期接受 SGV 为 He,得到 %x, err=%v", got, err)
}
if got, err := base64x.DecodeRaw("AA"); err != nil || !bytes.Equal(got, []byte{0x00}) {
t.Errorf("无填充解码器预期接受 AA 为 00,得到 %x, err=%v", got, err)
}
}
// TestPaddingSensitiveLengths 记录"长度与填充不匹配"时的实际行为:
// 带填充解码器要求输入长度为 4 的倍数,无填充解码器要求长度不为 4k+1。
func TestPaddingSensitiveLengths(t *testing.T) {
// 应当成功的用例(合法长度)。
ok := []struct {
name string
in string
dec func(string) ([]byte, error)
want []byte
}{
{"标准 4 字节分组", "AAAA", base64x.Decode, []byte{0x00, 0x00, 0x00}},
{"标准带填充 1 字节", "Zg==", base64x.Decode, []byte("f")},
{"标准带填充 2 字节", "Zm8=", base64x.Decode, []byte("fo")},
{"无填充 2 字符", "Zg", base64x.DecodeRaw, []byte("f")},
{"无填充 3 字符", "Zm8", base64x.DecodeRaw, []byte("fo")},
{"无填充 4 字符", "AAAA", base64x.DecodeRaw, []byte{0x00, 0x00, 0x00}},
{"无填充 7 字符", "SGVsbG8", base64x.DecodeRaw, []byte("Hello")},
{"无填充 URL 7 字符", "SGVsbG8", base64x.DecodeRawURL, []byte("Hello")},
{"带填充 URL 8 字符", "SGVsbG8=", base64x.DecodeURL, []byte("Hello")},
}
for _, c := range ok {
got, err := c.dec(c.in)
if err != nil {
t.Errorf("%s: 解码 %q 意外失败: %v", c.name, c.in, err)
continue
}
if !bytes.Equal(got, c.want) {
t.Errorf("%s: 解码 %q = %x, 期望 %x", c.name, c.in, got, c.want)
}
}
// 应当失败的用例。
failures := []struct {
name string
in string
dec func(string) ([]byte, error)
}{
{"带填充解码器遇到缺失填充", "Zg", base64x.Decode},
{"带填充解码器遇到缺失填充(2)", "Zm8", base64x.Decode},
{"带填充解码器遇到长度非 4 倍数", "SGVsbG8", base64x.Decode},
{"带填充 URL 解码器遇到缺失填充", "Zg", base64x.DecodeURL},
{"带填充 URL 解码器遇到长度非 4 倍数", "SGVsbG8", base64x.DecodeURL},
{"无填充解码器遇到 1 个残留字符", "Z", base64x.DecodeRaw},
{"无填充 URL 解码器遇到 1 个残留字符", "Z", base64x.DecodeRawURL},
{"带填充解码器遇到 1 个残留字符", "Z", base64x.Decode},
{"无填充解码器遇到 5 个字符", "AAAAA", base64x.DecodeRaw},
{"无填充解码器遇到带填充 4 字符", "Zg==", base64x.DecodeRaw},
{"无填充 URL 解码器遇到带填充 4 字符", "Zg==", base64x.DecodeRawURL},
}
for _, c := range failures {
got, err := c.dec(c.in)
if err == nil {
t.Errorf("%s: 解码 %q 应当失败,却成功得到 %x", c.name, c.in, got)
}
if got != nil {
t.Errorf("%s: 解码 %q 失败时应返回 nil,实际 %x", c.name, c.in, got)
}
}
}
// TestDecodeEmptyInput 明确各变体对空串的影响(空串是合法的"零长度数据")。
func TestDecodeEmptyInput(t *testing.T) {
for _, v := range variants {
decoded, err := v.decode("")
if err != nil {
t.Errorf("%s 解码空串应成功: %v", v.name, err)
continue
}
if len(decoded) != 0 {
t.Errorf("%s 解码空串得到 %d 字节,期望 0", v.name, len(decoded))
}
}
}
// TestCrossCheckWithStdlib 与标准库交叉验证,确保封装没有引入行为差异。
func TestCrossCheckWithStdlib(t *testing.T) {
payload := []byte("cross check with encoding/base64")
if got, want := base64x.Encode(payload), base64.StdEncoding.EncodeToString(payload); got != want {
t.Errorf("Encode = %q, 标准库 = %q", got, want)
}
if got, want := base64x.EncodeURL(payload), base64.URLEncoding.EncodeToString(payload); got != want {
t.Errorf("EncodeURL = %q, 标准库 = %q", got, want)
}
if got, want := base64x.EncodeRaw(payload), base64.RawStdEncoding.EncodeToString(payload); got != want {
t.Errorf("EncodeRaw = %q, 标准库 = %q", got, want)
}
if got, want := base64x.EncodeRawURL(payload), base64.RawURLEncoding.EncodeToString(payload); got != want {
t.Errorf("EncodeRawURL = %q, 标准库 = %q", got, want)
}
decoded, err := base64x.Decode(base64.StdEncoding.EncodeToString(payload))
if err != nil || !bytes.Equal(decoded, payload) {
t.Errorf("Decode 与标准库编码互操作失败: %v", err)
}
}
// TestNoPanicOnAnyInput 用一批畸形输入整体验证解码与编码都不 panic。
func TestNoPanicOnAnyInput(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("畸形输入导致 panic: %v", r)
}
}()
inputs := []string{"", "=", "==", "====", "A", "AA", "AAA", "AAAA", "\x00\x01", strings.Repeat("=", 100), strings.Repeat("A", 1000)}
for _, in := range inputs {
_, _ = base64x.Decode(in)
_, _ = base64x.DecodeURL(in)
_, _ = base64x.DecodeRaw(in)
_, _ = base64x.DecodeRawURL(in)
_ = base64x.Encode([]byte(in))
_ = base64x.EncodeRaw([]byte(in))
}
}
// isASCII 判断字符串是否只含 ASCII 可打印字符。
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < 0x20 || s[i] > 0x7e {
return false
}
}
return true
}