Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d840ccee1f | ||
|
|
caa01a61f5 | ||
|
|
50d6a00918 | ||
|
|
8875212dba | ||
|
|
47a62a3c16 |
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.ai/
|
||||||
|
.geminiignore
|
||||||
|
.gemini
|
||||||
|
/CODE-FULL.md
|
||||||
13
CHANGELOG.md
13
CHANGELOG.md
@ -1,5 +1,18 @@
|
|||||||
# Changelog: @go/encoding
|
# Changelog: @go/encoding
|
||||||
|
|
||||||
|
## [v1.1.1] - 2026-05-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **文档补全**:在 README 中补充了 `SortJoin` 接口的详细说明,该接口用于 Map 或 Struct 的排序拼接(签名场景)。
|
||||||
|
|
||||||
|
## [v1.1.0] - 2026-05-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Base64 无填充支持**:新增 `Base64Raw`、`Base64RawToString`、`UrlBase64Raw`、`UrlBase64RawToString` 接口,支持生成不带填充符(`=`)的编码。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **智能解码升级**:升级了 `UnBase64` 与 `UnUrlBase64` 系列函数,通过 O(1) 零分配检测自动兼容“带填充”与“无填充”的输入数据,无需额外调用 Raw 解码接口。
|
||||||
|
|
||||||
## [v1.0.6] - 2026-05-06
|
## [v1.0.6] - 2026-05-06
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
11
README.md
11
README.md
@ -18,15 +18,22 @@
|
|||||||
- `func Hex(data []byte) []byte` / `func HexToString(data []byte) string`
|
- `func Hex(data []byte) []byte` / `func HexToString(data []byte) string`
|
||||||
- `func UnHex(data []byte) ([]byte, error)` / `func UnHexFromString(data string) ([]byte, error)`
|
- `func UnHex(data []byte) ([]byte, error)` / `func UnHexFromString(data string) ([]byte, error)`
|
||||||
- `func Base64(data []byte) []byte` / `func Base64ToString(data []byte) string`
|
- `func Base64(data []byte) []byte` / `func Base64ToString(data []byte) string`
|
||||||
- `func UnBase64(data []byte) ([]byte, error)` / `func UnBase64FromString(data string) ([]byte, error)`
|
- `func Base64Raw(data []byte) []byte` / `func Base64RawToString(data []byte) string` (无填充版本)
|
||||||
|
- `func UnBase64(data []byte) ([]byte, error)` / `func UnBase64FromString(data string) ([]byte, error)` (智能兼容填充与无填充)
|
||||||
- `func UrlBase64(data []byte) []byte` / `func UrlBase64ToString(data []byte) string`
|
- `func UrlBase64(data []byte) []byte` / `func UrlBase64ToString(data []byte) string`
|
||||||
- `func UnUrlBase64(data []byte) ([]byte, error)` / `func UnUrlBase64FromString(data string) ([]byte, error)`
|
- `func UrlBase64Raw(data []byte) []byte` / `func UrlBase64RawToString(data []byte) string` (无填充版本)
|
||||||
|
- `func UnUrlBase64(data []byte) ([]byte, error)` / `func UnUrlBase64FromString(data string) ([]byte, error)` (智能兼容填充与无填充)
|
||||||
|
|
||||||
### Web 编码 (URL/HTML)
|
### Web 编码 (URL/HTML)
|
||||||
- `func UrlEncode(data []byte) string`
|
- `func UrlEncode(data []byte) string`
|
||||||
- `func UnUrlEncode(data string) ([]byte, error)`
|
- `func UnUrlEncode(data string) ([]byte, error)`
|
||||||
- `func HtmlEscape(data []byte) string`
|
- `func HtmlEscape(data []byte) string`
|
||||||
- `func HtmlUnescape(data string) string`
|
- `func HtmlUnescape(data string) string`
|
||||||
|
- `func Utf8Valid(data []byte) bool`
|
||||||
|
|
||||||
|
### 签名工具 (Signature Utils)
|
||||||
|
- `func SortJoin(v any, separator, connector string, urlEncode bool) string`
|
||||||
|
将 Map 或 Struct 转换为排序并拼接后的字符串。常用于生成签名原串。支持自动 URL 编码。
|
||||||
|
|
||||||
### 整数与自定义进制 (IntEncoder)
|
### 整数与自定义进制 (IntEncoder)
|
||||||
- `func EncodeInt(u uint64) []byte`
|
- `func EncodeInt(u uint64) []byte`
|
||||||
|
|||||||
3
TEST.md
3
TEST.md
@ -1,7 +1,7 @@
|
|||||||
# Test Report: @go/encoding
|
# Test Report: @go/encoding
|
||||||
|
|
||||||
## 📋 测试概览
|
## 📋 测试概览
|
||||||
- **测试时间**: 2026-05-06
|
- **测试时间**: 2026-05-08
|
||||||
- **测试环境**: darwin/amd64
|
- **测试环境**: darwin/amd64
|
||||||
- **Go 版本**: 1.25.0
|
- **Go 版本**: 1.25.0
|
||||||
|
|
||||||
@ -11,6 +11,7 @@
|
|||||||
| `TestHex` | PASS | Hex 编解码往返一致性,配合 `cast.As` 消除摩擦测试。 |
|
| `TestHex` | PASS | Hex 编解码往返一致性,配合 `cast.As` 消除摩擦测试。 |
|
||||||
| `TestBase64` | PASS | Standard/URL Base64 编解码一致性,`Un...FromString` 补全测试。 |
|
| `TestBase64` | PASS | Standard/URL Base64 编解码一致性,`Un...FromString` 补全测试。 |
|
||||||
| `TestWebEncoding` | PASS | URL 编解码、HTML 转义反转义往返测试。 |
|
| `TestWebEncoding` | PASS | URL 编解码、HTML 转义反转义往返测试。 |
|
||||||
|
| `TestSortJoin` | PASS | Map 与 Struct 的排序拼接测试,验证签名场景。 |
|
||||||
| `TestUtf8` | PASS | UTF-8 校验逻辑测试。 |
|
| `TestUtf8` | PASS | UTF-8 校验逻辑测试。 |
|
||||||
| `TestIntEncoder` | PASS | 包含正常编解码、FillInt 补齐、ExchangeInt 置换、HashInt 确定性测试。 |
|
| `TestIntEncoder` | PASS | 包含正常编解码、FillInt 补齐、ExchangeInt 置换、HashInt 确定性测试。 |
|
||||||
|
|
||||||
|
|||||||
102
encoding.go
102
encoding.go
@ -5,7 +5,11 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"html"
|
"html"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"apigo.cc/go/cast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hex 将数据转换为 Hex 编码的字节切片
|
// Hex 将数据转换为 Hex 编码的字节切片
|
||||||
@ -44,6 +48,18 @@ func Base64ToString(data []byte) string {
|
|||||||
return base64.StdEncoding.EncodeToString(data)
|
return base64.StdEncoding.EncodeToString(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base64Raw 将数据转换为无填充的 Base64 编码的字节切片
|
||||||
|
func Base64Raw(data []byte) []byte {
|
||||||
|
buf := make([]byte, base64.RawStdEncoding.EncodedLen(len(data)))
|
||||||
|
base64.RawStdEncoding.Encode(buf, data)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64RawToString 将数据转换为无填充的 Base64 编码的字符串
|
||||||
|
func Base64RawToString(data []byte) string {
|
||||||
|
return base64.RawStdEncoding.EncodeToString(data)
|
||||||
|
}
|
||||||
|
|
||||||
// UrlBase64 将数据转换为 URL 安全的 Base64 编码的字节切片
|
// UrlBase64 将数据转换为 URL 安全的 Base64 编码的字节切片
|
||||||
func UrlBase64(data []byte) []byte {
|
func UrlBase64(data []byte) []byte {
|
||||||
buf := make([]byte, base64.URLEncoding.EncodedLen(len(data)))
|
buf := make([]byte, base64.URLEncoding.EncodedLen(len(data)))
|
||||||
@ -56,28 +72,56 @@ func UrlBase64ToString(data []byte) string {
|
|||||||
return base64.URLEncoding.EncodeToString(data)
|
return base64.URLEncoding.EncodeToString(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnBase64 将 Base64 编码的字节切片解码
|
// UrlBase64Raw 将数据转换为 URL 安全且无填充的 Base64 编码的字节切片
|
||||||
|
func UrlBase64Raw(data []byte) []byte {
|
||||||
|
buf := make([]byte, base64.RawURLEncoding.EncodedLen(len(data)))
|
||||||
|
base64.RawURLEncoding.Encode(buf, data)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// UrlBase64RawToString 将数据转换为 URL 安全且无填充的 Base64 编码的字符串
|
||||||
|
func UrlBase64RawToString(data []byte) string {
|
||||||
|
return base64.RawURLEncoding.EncodeToString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnBase64 将 Base64 编码的字节切片解码(自动兼容有无填充)
|
||||||
func UnBase64(data []byte) ([]byte, error) {
|
func UnBase64(data []byte) ([]byte, error) {
|
||||||
dbuf := make([]byte, base64.StdEncoding.DecodedLen(len(data)))
|
if len(data) > 0 && data[len(data)-1] == '=' {
|
||||||
n, err := base64.StdEncoding.Decode(dbuf, data)
|
dbuf := make([]byte, base64.StdEncoding.DecodedLen(len(data)))
|
||||||
|
n, err := base64.StdEncoding.Decode(dbuf, data)
|
||||||
|
return dbuf[:n], err
|
||||||
|
}
|
||||||
|
dbuf := make([]byte, base64.RawStdEncoding.DecodedLen(len(data)))
|
||||||
|
n, err := base64.RawStdEncoding.Decode(dbuf, data)
|
||||||
return dbuf[:n], err
|
return dbuf[:n], err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnBase64FromString 将 Base64 编码的字符串解码
|
// UnBase64FromString 将 Base64 编码的字符串解码(自动兼容有无填充)
|
||||||
func UnBase64FromString(data string) ([]byte, error) {
|
func UnBase64FromString(data string) ([]byte, error) {
|
||||||
return base64.StdEncoding.DecodeString(data)
|
if len(data) > 0 && data[len(data)-1] == '=' {
|
||||||
|
return base64.StdEncoding.DecodeString(data)
|
||||||
|
}
|
||||||
|
return base64.RawStdEncoding.DecodeString(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnUrlBase64 将 URL 安全的 Base64 编码的字节切片解码
|
// UnUrlBase64 将 URL 安全的 Base64 编码的字节切片解码(自动兼容有无填充)
|
||||||
func UnUrlBase64(data []byte) ([]byte, error) {
|
func UnUrlBase64(data []byte) ([]byte, error) {
|
||||||
dbuf := make([]byte, base64.URLEncoding.DecodedLen(len(data)))
|
if len(data) > 0 && data[len(data)-1] == '=' {
|
||||||
n, err := base64.URLEncoding.Decode(dbuf, data)
|
dbuf := make([]byte, base64.URLEncoding.DecodedLen(len(data)))
|
||||||
|
n, err := base64.URLEncoding.Decode(dbuf, data)
|
||||||
|
return dbuf[:n], err
|
||||||
|
}
|
||||||
|
dbuf := make([]byte, base64.RawURLEncoding.DecodedLen(len(data)))
|
||||||
|
n, err := base64.RawURLEncoding.Decode(dbuf, data)
|
||||||
return dbuf[:n], err
|
return dbuf[:n], err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnUrlBase64FromString 将 URL 安全的 Base64 编码的字符串解码
|
// UnUrlBase64FromString 将 URL 安全的 Base64 编码的字符串解码(自动兼容有无填充)
|
||||||
func UnUrlBase64FromString(data string) ([]byte, error) {
|
func UnUrlBase64FromString(data string) ([]byte, error) {
|
||||||
return base64.URLEncoding.DecodeString(data)
|
if len(data) > 0 && data[len(data)-1] == '=' {
|
||||||
|
return base64.URLEncoding.DecodeString(data)
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.DecodeString(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UrlEncode 对数据进行 URL 编码
|
// UrlEncode 对数据进行 URL 编码
|
||||||
@ -108,3 +152,41 @@ func HtmlUnescape(data string) string {
|
|||||||
func Utf8Valid(data []byte) bool {
|
func Utf8Valid(data []byte) bool {
|
||||||
return utf8.Valid(data)
|
return utf8.Valid(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SortJoin 将 Map 或 Struct 转换为排序并拼接后的字符串 (常用于签名)
|
||||||
|
func SortJoin(v any, separator, connector string, urlEncode bool) string {
|
||||||
|
var m map[string]any
|
||||||
|
if vm, ok := v.(map[string]any); ok {
|
||||||
|
m = vm
|
||||||
|
} else if v != nil {
|
||||||
|
cast.Convert(&m, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
for i, k := range keys {
|
||||||
|
if i > 0 {
|
||||||
|
builder.WriteString(separator)
|
||||||
|
}
|
||||||
|
val := cast.String(m[k])
|
||||||
|
if urlEncode {
|
||||||
|
builder.WriteString(url.QueryEscape(k))
|
||||||
|
builder.WriteString(connector)
|
||||||
|
builder.WriteString(url.QueryEscape(val))
|
||||||
|
} else {
|
||||||
|
builder.WriteString(k)
|
||||||
|
builder.WriteString(connector)
|
||||||
|
builder.WriteString(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|||||||
@ -40,17 +40,38 @@ func TestBase64(t *testing.T) {
|
|||||||
if !bytes.Equal(cast.As(encoding.UnBase64FromString(enc)), data) {
|
if !bytes.Equal(cast.As(encoding.UnBase64FromString(enc)), data) {
|
||||||
t.Error("UnBase64FromString with cast.As failed")
|
t.Error("UnBase64FromString with cast.As failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Raw (Unpadded)
|
||||||
|
rawEnc := encoding.Base64RawToString(data)
|
||||||
|
if bytes.HasSuffix([]byte(rawEnc), []byte("=")) {
|
||||||
|
t.Error("Base64Raw should not have padding")
|
||||||
|
}
|
||||||
|
rawDec, err := encoding.UnBase64FromString(rawEnc)
|
||||||
|
if err != nil || !bytes.Equal(data, rawDec) {
|
||||||
|
t.Error("Base64Raw smart decoding failed")
|
||||||
|
}
|
||||||
|
|
||||||
// URL
|
// URL
|
||||||
uEnc := encoding.UrlBase64ToString([]byte("hello/world+"))
|
uData := []byte("hello/world+")
|
||||||
|
uEnc := encoding.UrlBase64ToString(uData)
|
||||||
uDec, _ := encoding.UnUrlBase64([]byte(uEnc))
|
uDec, _ := encoding.UnUrlBase64([]byte(uEnc))
|
||||||
if !bytes.Equal([]byte("hello/world+"), uDec) {
|
if !bytes.Equal(uData, uDec) {
|
||||||
t.Error("UrlBase64 roundtrip failed")
|
t.Error("UrlBase64 roundtrip failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.Equal(cast.As(encoding.UnUrlBase64FromString(uEnc)), []byte("hello/world+")) {
|
if !bytes.Equal(cast.As(encoding.UnUrlBase64FromString(uEnc)), uData) {
|
||||||
t.Error("UnUrlBase64FromString with cast.As failed")
|
t.Error("UnUrlBase64FromString with cast.As failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// URL Raw (Unpadded)
|
||||||
|
uRawEnc := encoding.UrlBase64RawToString(uData)
|
||||||
|
if bytes.HasSuffix([]byte(uRawEnc), []byte("=")) {
|
||||||
|
t.Error("UrlBase64Raw should not have padding")
|
||||||
|
}
|
||||||
|
uRawDec, err := encoding.UnUrlBase64FromString(uRawEnc)
|
||||||
|
if err != nil || !bytes.Equal(uData, uRawDec) {
|
||||||
|
t.Error("UrlBase64Raw smart decoding failed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Web ---
|
// --- Web ---
|
||||||
@ -82,3 +103,33 @@ func TestUtf8(t *testing.T) {
|
|||||||
t.Error("Invalid UTF-8 should fail")
|
t.Error("Invalid UTF-8 should fail")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- SortJoin ---
|
||||||
|
func TestSortJoin(t *testing.T) {
|
||||||
|
m := map[string]any{
|
||||||
|
"B": 2,
|
||||||
|
"A": 1,
|
||||||
|
"C": "hello world",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Map
|
||||||
|
res := encoding.SortJoin(m, "&", "=", true)
|
||||||
|
expected := "A=1&B=2&C=hello+world"
|
||||||
|
if res != expected {
|
||||||
|
t.Errorf("SortJoin Map failed: expected %s, got %s", expected, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Struct
|
||||||
|
type User struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
u := User{Name: "Alice", Age: 30, ID: "123"}
|
||||||
|
resStruct := encoding.SortJoin(u, ";", ":", false)
|
||||||
|
// Keys: age, name, ID. Sorted: ID, age, name
|
||||||
|
expectedStruct := "ID:123;age:30;name:Alice"
|
||||||
|
if resStruct != expectedStruct {
|
||||||
|
t.Errorf("SortJoin Struct failed: expected %s, got %s", expectedStruct, resStruct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -2,4 +2,4 @@ module apigo.cc/go/encoding
|
|||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require apigo.cc/go/cast v1.2.8 // indirect
|
require apigo.cc/go/cast v1.3.3
|
||||||
|
|||||||
4
go.sum
4
go.sum
@ -1,2 +1,2 @@
|
|||||||
apigo.cc/go/cast v1.2.8 h1:plb676DH2TjYljzf8OEMGT6lIhmZ/xaxEFfs0kDOiSI=
|
apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34=
|
||||||
apigo.cc/go/cast v1.2.8/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user