364 lines
13 KiB
Go
364 lines
13 KiB
Go
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
|
||
}
|