package utilsx_test import ( "bytes" "crypto/rand" "encoding/base64" "encoding/hex" "strings" "testing" "code.yun.ink/pkg/encryptx/utilsx" ) // urlSafeAlphabet 是 URL 安全 Base64(无填充)允许出现的全部字符。 const urlSafeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" // TestRandomBytes 校验长度、随机性与错误路径。 func TestRandomBytes(t *testing.T) { for _, n := range []int{1, 2, 16, 32, 1024} { buf, err := utilsx.RandomBytes(n) if err != nil { t.Fatalf("RandomBytes(%d) 返回错误: %v", n, err) } if len(buf) != n { t.Errorf("RandomBytes(%d) 长度 = %d", n, len(buf)) } } // 错误路径:0、负数、超大值都必须返回 error 与 nil,且不 panic。 bad := []int{0, -1, -1024, (1 << 20) + 1, 1 << 30} for _, n := range bad { func() { defer func() { if r := recover(); r != nil { t.Fatalf("RandomBytes(%d) 发生 panic: %v", n, r) } }() buf, err := utilsx.RandomBytes(n) if err == nil { t.Errorf("RandomBytes(%d) 应返回错误", n) } if buf != nil { t.Errorf("RandomBytes(%d) 出错时应返回 nil", n) } }() } // 边界值 1<<20 必须被接受。 if buf, err := utilsx.RandomBytes(1 << 20); err != nil || len(buf) != 1<<20 { t.Errorf("RandomBytes(1<<20) 应成功,err=%v len=%d", err, len(buf)) } } // TestRandomBytesUnique 校验重复调用产生不同结果。 func TestRandomBytesUnique(t *testing.T) { assertUnique(t, 64, func() string { buf, err := utilsx.RandomBytes(32) if err != nil { t.Fatalf("RandomBytes 返回错误: %v", err) } return string(buf) }) } // TestRandomString 校验字符集、长度、随机性与错误路径。 func TestRandomString(t *testing.T) { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" s, err := utilsx.RandomString(32) if err != nil { t.Fatalf("RandomString 返回错误: %v", err) } if len(s) != 32 { t.Errorf("RandomString(32) 长度 = %d", len(s)) } for i := 0; i < len(s); i++ { if strings.IndexByte(charset, s[i]) < 0 { t.Errorf("RandomString 含非法字符 %q", s[i]) } } assertUnique(t, 64, func() string { v, err := utilsx.RandomString(24) if err != nil { t.Fatalf("RandomString 返回错误: %v", err) } return v }) for _, n := range []int{0, -5, (1 << 20) + 1} { v, err := utilsx.RandomString(n) if err == nil { t.Errorf("RandomString(%d) 应返回错误", n) } if v != "" { t.Errorf("RandomString(%d) 出错时应返回空串,实际 %q", n, v) } } } // TestRandomStringWithCharset 校验自定义字符集与错误路径。 func TestRandomStringWithCharset(t *testing.T) { s, err := utilsx.RandomStringWithCharset(64, "ab") if err != nil { t.Fatalf("RandomStringWithCharset 返回错误: %v", err) } if len(s) != 64 { t.Errorf("长度 = %d, 期望 64", len(s)) } for i := 0; i < len(s); i++ { if s[i] != 'a' && s[i] != 'b' { t.Errorf("字符集受限却出现了 %q", s[i]) } } // 单字符字符集应始终返回同内容但长度正确的串。 single, err := utilsx.RandomStringWithCharset(8, "x") if err != nil { t.Fatalf("单字符字符集出错: %v", err) } if single != "xxxxxxxx" { t.Errorf("单字符字符集结果 = %q, 期望 xxxxxxxx", single) } // 空字符集必须报错。 v, err := utilsx.RandomStringWithCharset(16, "") if err == nil { t.Error("空字符集应返回错误") } if v != "" { t.Errorf("空字符集出错时应返回空串,实际 %q", v) } // 非 ASCII 字符集:长度按 rune 计。 uni, err := utilsx.RandomStringWithCharset(20, "中文🔐") if err != nil { t.Fatalf("非 ASCII 字符集出错: %v", err) } if len([]rune(uni)) != 20 { t.Errorf("非 ASCII 字符集长度 = %d rune, 期望 20", len([]rune(uni))) } // 长度非法。 if _, err := utilsx.RandomStringWithCharset(0, "abc"); err == nil { t.Error("长度为 0 应返回错误") } if _, err := utilsx.RandomStringWithCharset(-1, "abc"); err == nil { t.Error("长度为负应返回错误") } if _, err := utilsx.RandomStringWithCharset((1<<20)+1, "abc"); err == nil { t.Error("长度超上限应返回错误") } } // TestRandomHex 校验十六进制输出的长度、字符集与随机性。 func TestRandomHex(t *testing.T) { got, err := utilsx.RandomHex(16) if err != nil { t.Fatalf("RandomHex 返回错误: %v", err) } if len(got) != 32 { t.Errorf("RandomHex(16) 长度 = %d, 期望 32", len(got)) } if _, err := hex.DecodeString(got); err != nil { t.Errorf("RandomHex 结果不是合法十六进制: %v", err) } if got != strings.ToLower(got) { t.Errorf("RandomHex 应为小写: %q", got) } assertUnique(t, 64, func() string { v, err := utilsx.RandomHex(16) if err != nil { t.Fatalf("RandomHex 返回错误: %v", err) } return v }) for _, n := range []int{0, -1, (1 << 20) + 1} { v, err := utilsx.RandomHex(n) if err == nil { t.Errorf("RandomHex(%d) 应返回错误", n) } if v != "" { t.Errorf("RandomHex(%d) 出错时应返回空串", n) } } } // TestRandomBase64 校验 URL 安全无填充 Base64 输出的长度与字符集。 func TestRandomBase64(t *testing.T) { cases := []struct { n int want int // 期望的编码后长度(ceil(4n/3)) }{ {1, 2}, {2, 3}, {3, 4}, {4, 6}, {16, 22}, {32, 43}, {48, 64}, } for _, c := range cases { got, err := utilsx.RandomBase64(c.n) if err != nil { t.Fatalf("RandomBase64(%d) 返回错误: %v", c.n, err) } if len(got) != c.want { t.Errorf("RandomBase64(%d) 长度 = %d, 期望 %d", c.n, len(got), c.want) } if strings.Contains(got, "=") { t.Errorf("RandomBase64 不应含填充: %q", got) } assertOnlyCharset(t, got, urlSafeAlphabet) if _, err := base64.RawURLEncoding.DecodeString(got); err != nil { t.Errorf("RandomBase64(%d) 结果不是合法 RawURLEncoding: %v", c.n, err) } } assertUnique(t, 64, func() string { v, err := utilsx.RandomBase64(24) if err != nil { t.Fatalf("RandomBase64 返回错误: %v", err) } return v }) for _, n := range []int{0, -3, (1 << 20) + 1} { v, err := utilsx.RandomBase64(n) if err == nil { t.Errorf("RandomBase64(%d) 应返回错误", n) } if v != "" { t.Errorf("RandomBase64(%d) 出错时应返回空串", n) } } } // TestRandomToken 校验长度恰为 n、只含 URL 安全字符、无填充, // 以及"当 n 是合法无填充 Base64 长度时可解回字节"。 func TestRandomToken(t *testing.T) { // 覆盖全部 n mod 4 情形。 lengths := []int{utilsx.MinTokenLength, 17, 18, 19, 20, 21, 24, 25, 32, 33, 64, utilsx.DefaultTokenLength, 100, 128, 129} for _, n := range lengths { got, err := utilsx.RandomToken(n) if err != nil { t.Fatalf("RandomToken(%d) 返回错误: %v", n, err) } if len(got) != n { t.Errorf("RandomToken(%d) 长度 = %d", n, len(got)) } if strings.Contains(got, "=") { t.Errorf("RandomToken(%d) 不应含填充: %q", n, got) } assertOnlyCharset(t, got, urlSafeAlphabet) // n ≡ 1 (mod 4) 不是合法的无填充 Base64 长度(残留 1 字符无法还原成整字节), // 因此这类长度下只断言字符集与长度;其余长度必须能原样解回字节。 if n%4 == 1 { if _, err := base64.RawURLEncoding.DecodeString(got); err == nil { t.Logf("RandomToken(%d) 意外可被 RawURLEncoding 解码(标准库行为可能已放宽)", n) } continue } raw, err := base64.RawURLEncoding.DecodeString(got) if err != nil { t.Errorf("RandomToken(%d)(n%%4=%d)结果不是合法 RawURLEncoding: %v", n, n%4, err) continue } if wantBytes := (n * 3) / 4; len(raw) != wantBytes { t.Errorf("RandomToken(%d) 可还原 %d 字节, 期望 %d", n, len(raw), wantBytes) } } // 低于最小长度必须报错(且返回空串、不 panic)。 for _, n := range []int{0, 1, 8, utilsx.MinTokenLength - 1, -1} { func() { defer func() { if r := recover(); r != nil { t.Fatalf("RandomToken(%d) 发生 panic: %v", n, r) } }() v, err := utilsx.RandomToken(n) if err == nil { t.Errorf("RandomToken(%d) 应返回错误(低于 MinTokenLength=%d)", n, utilsx.MinTokenLength) } if v != "" { t.Errorf("RandomToken(%d) 出错时应返回空串", n) } }() } // 超过上限必须报错。 if _, err := utilsx.RandomToken((1 << 20) + 1); err == nil { t.Error("RandomToken 超上限应返回错误") } assertUnique(t, 128, func() string { v, err := utilsx.RandomToken(32) if err != nil { t.Fatalf("RandomToken 返回错误: %v", err) } return v }) } // TestRandomPasswordCategories 断言每一个被选中的字符类别至少出现一次。 // 这是本次重写要修复的核心缺陷,因此循环 50 次以确认稳定通过。 func TestRandomPasswordCategories(t *testing.T) { const iterations = 50 combos := []struct { upper, lower, digits, symbols bool }{ {true, false, false, false}, {false, true, false, false}, {false, false, true, false}, {false, false, false, true}, {true, true, false, false}, {true, false, true, false}, {true, false, false, true}, {false, true, true, false}, {false, true, false, true}, {false, false, true, true}, {true, true, true, false}, {true, true, false, true}, {true, false, true, true}, {false, true, true, true}, {true, true, true, true}, } for _, combo := range combos { classes := 0 for _, on := range []bool{combo.upper, combo.lower, combo.digits, combo.symbols} { if on { classes++ } } length := classes + 8 // 保证有填充位置 for iter := 0; iter < iterations; iter++ { pw, err := utilsx.RandomPassword(length, combo.upper, combo.lower, combo.digits, combo.symbols) if err != nil { t.Fatalf("组合 %+v 第 %d 次: RandomPassword 返回错误: %v", combo, iter, err) } if len(pw) != length { t.Fatalf("组合 %+v 第 %d 次: 长度 = %d, 期望 %d", combo, iter, len(pw), length) } var hasUpper, hasLower, hasDigit, hasSymbol bool for i := 0; i < len(pw); i++ { c := pw[i] switch { case c >= 'A' && c <= 'Z': hasUpper = true case c >= 'a' && c <= 'z': hasLower = true case c >= '0' && c <= '9': hasDigit = true case strings.IndexByte("!@#$%^&*()-_=+[]{}|;:,.<>?", c) >= 0: hasSymbol = true default: t.Fatalf("组合 %+v 第 %d 次: 出现未知字符 %q(口令已隐去)", combo, iter, c) } } if combo.upper && !hasUpper { t.Fatalf("组合 %+v 第 %d 次: 缺少大写字母", combo, iter) } if combo.lower && !hasLower { t.Fatalf("组合 %+v 第 %d 次: 缺少小写字母", combo, iter) } if combo.digits && !hasDigit { t.Fatalf("组合 %+v 第 %d 次: 缺少数字", combo, iter) } if combo.symbols && !hasSymbol { t.Fatalf("组合 %+v 第 %d 次: 缺少符号", combo, iter) } // 未选中的类别不应出现(因为字符集只由选中类别构成)。 if !combo.upper && hasUpper { t.Fatalf("组合 %+v 第 %d 次: 出现了未选中的大写字母", combo, iter) } if !combo.lower && hasLower { t.Fatalf("组合 %+v 第 %d 次: 出现了未选中的小写字母", combo, iter) } if !combo.digits && hasDigit { t.Fatalf("组合 %+v 第 %d 次: 出现了未选中的数字", combo, iter) } if !combo.symbols && hasSymbol { t.Fatalf("组合 %+v 第 %d 次: 出现了未选中的符号", combo, iter) } } } } // TestRandomPasswordShuffle 校验洗牌生效:首个字符的类别不应固定。 // 若实现只做"每类先取一个再顺序拼接",则首位必然是大写字母,本测试会失败。 func TestRandomPasswordShuffle(t *testing.T) { firstIsUpper := 0 const iterations = 200 for i := 0; i < iterations; i++ { pw, err := utilsx.RandomPassword(16, true, true, true, true) if err != nil { t.Fatalf("RandomPassword 返回错误: %v", err) } if pw[0] >= 'A' && pw[0] <= 'Z' { firstIsUpper++ } } // 若未洗牌则 firstIsUpper == iterations;4 类均匀时期望约 1/4。 if firstIsUpper == iterations { t.Error("首位始终是大写字母,说明 Fisher-Yates 洗牌未生效") } } // TestRandomTokenExhaustiveLengths 穷举 n 从 MinTokenLength 到 1024, // 断言返回值长度恰为 n、字符集合法,并且在 n 为合法无填充 Base64 长度 // (n%4 != 1)时一定可以解回字节。 func TestRandomTokenExhaustiveLengths(t *testing.T) { const maxLen = 1024 for n := utilsx.MinTokenLength; n <= maxLen; n++ { got, err := utilsx.RandomToken(n) if err != nil { t.Fatalf("RandomToken(%d) 返回错误: %v", n, err) } if len(got) != n { t.Fatalf("RandomToken(%d) 长度 = %d", n, len(got)) } assertOnlyCharset(t, got, urlSafeAlphabet) if n%4 == 1 { continue // 该长度在无填充 Base64 中不合法,见 RandomToken 文档 } if _, err := base64.RawURLEncoding.DecodeString(got); err != nil { t.Fatalf("RandomToken(%d)(n%%4=%d)结果不是合法 RawURLEncoding: %v", n, n%4, err) } } } // TestRawBase64LengthConstraint 固定住"n ≡ 1 (mod 4) 不是合法无填充 Base64 长度" // 这一事实,避免以后有人误以为 RandomToken 在该长度下可解码。 func TestRawBase64LengthConstraint(t *testing.T) { for _, n := range []int{13, 17, 21, 25, 33, 97} { s := strings.Repeat("A", n) if _, err := base64.RawURLEncoding.DecodeString(s); err == nil { t.Errorf("长度为 %d (n%%4=%d) 的字符串不应可被 RawURLEncoding 解码", n, n%4) } } // 合法长度(n%4 ∈ {0,2,3})可以解码。 for _, n := range []int{16, 18, 19, 20, 22, 23, 32} { s := strings.Repeat("A", n) if _, err := base64.RawURLEncoding.DecodeString(s); err != nil { t.Errorf("长度为 %d (n%%4=%d) 的字符串应可被 RawURLEncoding 解码: %v", n, n%4, err) } } } // TestRandomPasswordErrors 校验类别数与长度矛盾时返回 error。 func TestRandomPasswordErrors(t *testing.T) { cases := []struct { name string length int upper, lower, digits, symbols bool }{ {"长度 0", 0, true, true, true, true}, {"长度为负", -1, true, true, true, true}, {"长度 3 但需 4 类", 3, true, true, true, true}, {"长度 1 但需 4 类", 1, true, true, true, true}, {"长度 1 但需 2 类", 1, true, false, true, false}, {"未选择任何类别", 16, false, false, false, false}, {"未选择任何类别且长度 0", 0, false, false, false, false}, {"长度足够但无类别", 64, false, false, false, false}, {"超上限", (1 << 20) + 1, true, true, true, true}, } for _, c := range cases { func() { defer func() { if r := recover(); r != nil { t.Fatalf("%s: RandomPassword 发生 panic: %v", c.name, r) } }() pw, err := utilsx.RandomPassword(c.length, c.upper, c.lower, c.digits, c.symbols) if err == nil { t.Errorf("%s: 应返回错误,实际得到长度 %d 的口令", c.name, len(pw)) } if pw != "" { t.Errorf("%s: 出错时应返回空串", c.name) } }() } // 恰好等于类别数的长度必须成功。 pw, err := utilsx.RandomPassword(4, true, true, true, true) if err != nil { t.Fatalf("长度等于类别数应成功: %v", err) } if len(pw) != 4 { t.Errorf("长度 = %d, 期望 4", len(pw)) } } // TestUUID 校验 RFC 4122 v4 的格式、版本位、变体位与随机性。 func TestUUID(t *testing.T) { seen := make(map[string]bool) for i := 0; i < 500; i++ { id, err := utilsx.UUID() if err != nil { t.Fatalf("UUID 返回错误: %v", err) } if len(id) != 36 { t.Fatalf("UUID 长度 = %d, 期望 36(%q)", len(id), id) } if id[8] != '-' || id[13] != '-' || id[18] != '-' || id[23] != '-' { t.Fatalf("UUID 连字符位置错误: %q", id) } if strings.Count(id, "-") != 4 { t.Fatalf("UUID 连字符数量错误: %q", id) } // 第 15 个字符(索引 14)必须是版本号 4。 if id[14] != '4' { t.Fatalf("UUID 版本位错误: 索引 14 = %q, 期望 '4'(%q)", id[14], id) } // 第 20 个字符(索引 19)必须属于 8/9/a/b(RFC 4122 变体 10xx)。 if !strings.ContainsRune("89ab", rune(id[19])) { t.Fatalf("UUID 变体位错误: 索引 19 = %q, 期望 8/9/a/b(%q)", id[19], id) } if id != strings.ToLower(id) { t.Fatalf("UUID 应为小写: %q", id) } // 去掉连字符后必须是 32 位十六进制。 if _, err := hex.DecodeString(strings.ReplaceAll(id, "-", "")); err != nil { t.Fatalf("UUID 主体不是合法十六进制: %v(%q)", err, id) } if seen[id] { t.Fatalf("UUID 出现重复: %q", id) } seen[id] = true } } // TestAPIKey 校验前缀处理与令牌长度。 func TestAPIKey(t *testing.T) { const n = 32 key, err := utilsx.APIKey("sk.", n) if err != nil { t.Fatalf("APIKey 返回错误: %v", err) } if !strings.HasPrefix(key, "sk.") { t.Errorf("APIKey 未包含前缀: %q", key) } if len(key) != len("sk.")+n { t.Errorf("APIKey 长度 = %d, 期望 %d", len(key), len("sk.")+n) } assertOnlyCharset(t, strings.TrimPrefix(key, "sk."), urlSafeAlphabet) if strings.Contains(key, "=") { t.Errorf("APIKey 不应含填充: %q", key) } // 空前缀:直接返回令牌。 plain, err := utilsx.APIKey("", n) if err != nil { t.Fatalf("APIKey(空前缀) 返回错误: %v", err) } if len(plain) != n { t.Errorf("APIKey(空前缀) 长度 = %d, 期望 %d", len(plain), n) } // 前缀仅用于人类可读的区分,长度校验只针对令牌部分。 if _, err := utilsx.APIKey("sk.", 0); err == nil { t.Error("APIKey 令牌长度为 0 应返回错误") } if _, err := utilsx.APIKey("sk.", utilsx.MinTokenLength-1); err == nil { t.Error("APIKey 令牌长度低于 MinTokenLength 应返回错误") } if v, err := utilsx.APIKey("sk.", -1); err == nil || v != "" { t.Errorf("APIKey 负数长度应返回错误与空串,得到 %q, %v", v, err) } assertUnique(t, 64, func() string { v, err := utilsx.APIKey("sk.", 32) if err != nil { t.Fatalf("APIKey 返回错误: %v", err) } return v }) } // TestSecureCompare 校验常量时间比较函数的行为。 func TestSecureCompare(t *testing.T) { a := []byte("a-secret-value-to-compare") b := append([]byte(nil), a...) if !utilsx.SecureCompare(a, b) { t.Error("SecureCompare 对相同内容返回 false") } if utilsx.SecureCompare(a, []byte("a-secret-value-to-compare-x")) { t.Error("SecureCompare 对更长内容返回 true") } diff := append([]byte(nil), a...) diff[len(diff)-1] ^= 0x01 if utilsx.SecureCompare(a, diff) { t.Error("SecureCompare 对仅末字节不同的内容返回 true") } if !utilsx.SecureCompare(nil, nil) { t.Error("SecureCompare(nil, nil) 应为 true") } if utilsx.SecureCompare(nil, []byte{0x00}) { t.Error("SecureCompare(nil, 非空) 应为 false") } if !utilsx.SecureCompareString("token-value", "token-value") { t.Error("SecureCompareString 对相同内容返回 false") } if utilsx.SecureCompareString("token-value", "token-valuE") { t.Error("SecureCompareString 对大小写不同的内容返回 true") } if utilsx.SecureCompareString("", "") != true { t.Error("SecureCompareString(\"\", \"\") 应为 true") } // 与 RandomToken 的实际使用场景一致:比较相等的令牌应为 true。 token, err := utilsx.RandomToken(32) if err != nil { t.Fatalf("RandomToken 返回错误: %v", err) } if !utilsx.SecureCompareString(token, token) { t.Error("SecureCompareString 对相同令牌返回 false") } } // TestConstants 校验导出的长度常量为文档约定的值。 func TestConstants(t *testing.T) { if utilsx.MinTokenLength != 16 { t.Errorf("MinTokenLength = %d, 期望 16", utilsx.MinTokenLength) } if utilsx.DefaultTokenLength != 32 { t.Errorf("DefaultTokenLength = %d, 期望 32", utilsx.DefaultTokenLength) } if utilsx.DefaultTokenLength < utilsx.MinTokenLength { t.Error("DefaultTokenLength 不应小于 MinTokenLength") } } // TestOutputsAreNotAllSame 用固定长度检查各随机函数确实在变化(弱随机实现的兜底检查)。 func TestOutputsAreNotAllSame(t *testing.T) { generators := map[string]func() (string, error){ "RandomString": func() (string, error) { return utilsx.RandomString(16) }, "RandomHex": func() (string, error) { return utilsx.RandomHex(16) }, "RandomBase64": func() (string, error) { return utilsx.RandomBase64(16) }, "RandomToken": func() (string, error) { return utilsx.RandomToken(24) }, "APIKey": func() (string, error) { return utilsx.APIKey("sk.", 24) }, "UUID": utilsx.UUID, } for name, gen := range generators { first, err := gen() if err != nil { t.Fatalf("%s 返回错误: %v", name, err) } allSame := true for i := 0; i < 32; i++ { v, err := gen() if err != nil { t.Fatalf("%s 返回错误: %v", name, err) } if v != first { allSame = false break } } if allSame { t.Errorf("%s 32 次调用返回了完全相同的结果,随机源可疑", name) } } } // TestRandomBytesUsesCryptoRand 交叉验证:用 crypto/rand 自身生成的字节 // 与 RandomBytes 的输出空间应当一致(都覆盖全字节值,排除 math/rand 之外的弱源)。 func TestRandomBytesStatisticalSmoke(t *testing.T) { buf, err := utilsx.RandomBytes(4096) if err != nil { t.Fatalf("RandomBytes 返回错误: %v", err) } var counts [256]int for _, b := range buf { counts[b]++ } distinct := 0 for _, c := range counts { if c > 0 { distinct++ } } // 4096 字节取样下几乎必然覆盖 256 个值中的绝大多数;此处用宽松阈值避免偶发失败。 if distinct < 200 { t.Errorf("4096 字节中只出现 %d 种字节值,随机性可疑", distinct) } if bytes.Equal(buf, make([]byte, len(buf))) { t.Error("RandomBytes 返回了全零内容") } // 与 crypto/rand 直接读取的结果长度一致(仅作 API 对照,不比较内容)。 ref := make([]byte, 8) if _, err := rand.Read(ref); err != nil { t.Fatalf("crypto/rand 读取失败: %v", err) } if len(ref) != 8 { t.Error("crypto/rand 对照读取长度异常") } } // assertUnique 校验 gen 连续调用 count 次都不产生重复值。 func assertUnique(t *testing.T, count int, gen func() string) { t.Helper() seen := make(map[string]struct{}, count) for i := 0; i < count; i++ { v := gen() if _, ok := seen[v]; ok { t.Fatalf("第 %d 次调用产生了重复值(长度 %d)", i, len(v)) } seen[v] = struct{}{} } } // assertOnlyCharset 校验字符串只包含给定字符集中的字符。 func assertOnlyCharset(t *testing.T, s, charset string) { t.Helper() for i := 0; i < len(s); i++ { if strings.IndexByte(charset, s[i]) < 0 { t.Fatalf("字符串含字符集外的字符 %q", s[i]) } } }