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 }