Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77e73281ad | ||
|
|
09af304a8c | ||
|
|
a45d8d20f1 | ||
|
|
6f18f6e6a8 | ||
|
|
d840ccee1f | ||
|
|
caa01a61f5 | ||
|
|
50d6a00918 | ||
|
|
8875212dba |
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.ai/
|
||||||
|
.geminiignore
|
||||||
|
.gemini
|
||||||
|
/CODE-FULL.md
|
||||||
17
CHANGELOG.md
17
CHANGELOG.md
@ -1,5 +1,22 @@
|
|||||||
# Changelog: @go/encoding
|
# Changelog: @go/encoding
|
||||||
|
|
||||||
|
## v1.5.2 (2026-06-08)
|
||||||
|
- **回滚与修复**: 修正了 `v1.5.1` 中架构设计的失误,将 Go 核心层的 `EncodeInt` 及 `IntEncoder.EncodeInt` 返回值从 `string` 恢复为 `[]byte`,彻底消除由于频繁强制转换导致的内存逃逸,恢复了高频并发场景下的零拷贝性能基准。
|
||||||
|
- **JS 智能桥接**: 通过在 `js_export.go` 的注册阶段采用匿名闭包函数 `func(u uint64) string`,完美兼顾了底层的高效性和 JS 环境面向字符串开发的友好体验。
|
||||||
|
|
||||||
|
## v1.5.1 (2026-06-08)
|
||||||
|
- **JS 对齐**: 将所有注册到 `jsmod` 的导出方法名统一为 PascalCase,并将缩写名称(如 URL, HTML, UTF8)全大写,以消除 JS 与 Go 调用体感上的摩擦。合并了冗余的 `xxxToString` 方法。
|
||||||
|
|
||||||
|
## [v1.3.2] - 2026-05-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **JSMOD 注册**:将核心编解码能力注册到 `jsmod`,支持在 JS 环境中调用 Base64, Hex, URL, HTML 及 SortJoin 等能力。
|
||||||
|
|
||||||
|
## [v1.1.1] - 2026-05-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **文档补全**:在 README 中补充了 `SortJoin` 接口的详细说明,该接口用于 Map 或 Struct 的排序拼接(签名场景)。
|
||||||
|
|
||||||
## [v1.1.0] - 2026-05-06
|
## [v1.1.0] - 2026-05-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -29,6 +29,11 @@
|
|||||||
- `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 确定性测试。 |
|
||||||
|
|
||||||
|
|||||||
210
encoding.go
210
encoding.go
@ -5,146 +5,130 @@ 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 编码的字符串
|
||||||
func Hex(data []byte) []byte {
|
func Hex(data any) string {
|
||||||
dst := make([]byte, hex.EncodedLen(len(data)))
|
b := cast.To[[]byte](data)
|
||||||
hex.Encode(dst, data)
|
return hex.EncodeToString(b)
|
||||||
return dst
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HexToString 将数据转换为 Hex 编码的字符串
|
// UnHex 将 Hex 编码的数据解码为字节切片
|
||||||
func HexToString(data []byte) string {
|
func UnHex(data any) ([]byte, error) {
|
||||||
return hex.EncodeToString(data)
|
s := cast.String(data)
|
||||||
|
return hex.DecodeString(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnHex 将 Hex 编码的字节切片解码
|
// Base64 将数据转换为 Base64 编码的字符串
|
||||||
func UnHex(data []byte) ([]byte, error) {
|
func Base64(data any) string {
|
||||||
dst := make([]byte, hex.DecodedLen(len(data)))
|
b := cast.To[[]byte](data)
|
||||||
n, err := hex.Decode(dst, data)
|
return base64.StdEncoding.EncodeToString(b)
|
||||||
return dst[:n], err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnHexFromString 将 Hex 编码的字符串解码
|
// UnBase64 将 Base64 编码的数据解码为字节切片(自动兼容有无填充)
|
||||||
func UnHexFromString(data string) ([]byte, error) {
|
func UnBase64(data any) ([]byte, error) {
|
||||||
return hex.DecodeString(data)
|
s := cast.String(data)
|
||||||
}
|
if len(s) > 0 && s[len(s)-1] == '=' {
|
||||||
|
return base64.StdEncoding.DecodeString(s)
|
||||||
// Base64 将数据转换为 Base64 编码的字节切片
|
|
||||||
func Base64(data []byte) []byte {
|
|
||||||
buf := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
|
|
||||||
base64.StdEncoding.Encode(buf, data)
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base64ToString 将数据转换为 Base64 编码的字符串
|
|
||||||
func Base64ToString(data []byte) string {
|
|
||||||
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 编码的字节切片
|
|
||||||
func UrlBase64(data []byte) []byte {
|
|
||||||
buf := make([]byte, base64.URLEncoding.EncodedLen(len(data)))
|
|
||||||
base64.URLEncoding.Encode(buf, data)
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// UrlBase64ToString 将数据转换为 URL 安全的 Base64 编码的字符串
|
|
||||||
func UrlBase64ToString(data []byte) string {
|
|
||||||
return base64.URLEncoding.EncodeToString(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
if len(data) > 0 && data[len(data)-1] == '=' {
|
|
||||||
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)))
|
return base64.RawStdEncoding.DecodeString(s)
|
||||||
n, err := base64.RawStdEncoding.Decode(dbuf, data)
|
|
||||||
return dbuf[:n], err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnBase64FromString 将 Base64 编码的字符串解码(自动兼容有无填充)
|
// Base64Raw 将数据转换为无填充的 Base64 编码的字符串
|
||||||
func UnBase64FromString(data string) ([]byte, error) {
|
func Base64Raw(data any) string {
|
||||||
if len(data) > 0 && data[len(data)-1] == '=' {
|
b := cast.To[[]byte](data)
|
||||||
return base64.StdEncoding.DecodeString(data)
|
return base64.RawStdEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// URLBase64 将数据转换为 URL 安全的 Base64 编码的字符串
|
||||||
|
func URLBase64(data any) string {
|
||||||
|
b := cast.To[[]byte](data)
|
||||||
|
return base64.URLEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnURLBase64 将 URL 安全的 Base64 编码的数据解码为字节切片(自动兼容有无填充)
|
||||||
|
func UnURLBase64(data any) ([]byte, error) {
|
||||||
|
s := cast.String(data)
|
||||||
|
if len(s) > 0 && s[len(s)-1] == '=' {
|
||||||
|
return base64.URLEncoding.DecodeString(s)
|
||||||
}
|
}
|
||||||
return base64.RawStdEncoding.DecodeString(data)
|
return base64.RawURLEncoding.DecodeString(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnUrlBase64 将 URL 安全的 Base64 编码的字节切片解码(自动兼容有无填充)
|
// URLBase64Raw 将数据转换为 URL 安全且无填充的 Base64 编码的字符串
|
||||||
func UnUrlBase64(data []byte) ([]byte, error) {
|
func URLBase64Raw(data any) string {
|
||||||
if len(data) > 0 && data[len(data)-1] == '=' {
|
b := cast.To[[]byte](data)
|
||||||
dbuf := make([]byte, base64.URLEncoding.DecodedLen(len(data)))
|
return base64.RawURLEncoding.EncodeToString(b)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnUrlBase64FromString 将 URL 安全的 Base64 编码的字符串解码(自动兼容有无填充)
|
// URLEncode 对数据进行 URL 编码
|
||||||
func UnUrlBase64FromString(data string) ([]byte, error) {
|
func URLEncode(data any) string {
|
||||||
if len(data) > 0 && data[len(data)-1] == '=' {
|
return url.QueryEscape(cast.String(data))
|
||||||
return base64.URLEncoding.DecodeString(data)
|
|
||||||
}
|
|
||||||
return base64.RawURLEncoding.DecodeString(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UrlEncode 对数据进行 URL 编码
|
// UnURLEncode 对字符串进行 URL 解码
|
||||||
func UrlEncode(data []byte) string {
|
func UnURLEncode(data any) ([]byte, error) {
|
||||||
return url.QueryEscape(string(data))
|
res, err := url.QueryUnescape(cast.String(data))
|
||||||
}
|
|
||||||
|
|
||||||
// UnUrlEncode 对字符串进行 URL 解码
|
|
||||||
func UnUrlEncode(data string) ([]byte, error) {
|
|
||||||
res, err := url.QueryUnescape(data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return []byte(res), nil
|
return []byte(res), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HtmlEscape 对数据进行 HTML 转义
|
// HTMLEscape 对数据进行 HTML 转义
|
||||||
func HtmlEscape(data []byte) string {
|
func HTMLEscape(data any) string {
|
||||||
return html.EscapeString(string(data))
|
return html.EscapeString(cast.String(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
// HtmlUnescape 对 HTML 字符串进行反转义
|
// HTMLUnescape 对 HTML 字符串进行反转义
|
||||||
func HtmlUnescape(data string) string {
|
func HTMLUnescape(data any) string {
|
||||||
return html.UnescapeString(data)
|
return html.UnescapeString(cast.String(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utf8Valid 检查字节切片是否为有效的 UTF-8 编码
|
// UTF8Valid 检查数据是否为有效的 UTF-8 编码
|
||||||
func Utf8Valid(data []byte) bool {
|
func UTF8Valid(data any) bool {
|
||||||
return utf8.Valid(data)
|
return utf8.Valid(cast.To[[]byte](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()
|
||||||
}
|
}
|
||||||
|
|||||||
141
encoding_test.go
141
encoding_test.go
@ -1,105 +1,96 @@
|
|||||||
package encoding_test
|
package encoding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
"apigo.cc/go/encoding"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Hex ---
|
func TestEncoding(t *testing.T) {
|
||||||
func TestHex(t *testing.T) {
|
|
||||||
data := []byte("hello go")
|
|
||||||
encoded := encoding.HexToString(data)
|
|
||||||
decoded, err := encoding.UnHex([]byte(encoded))
|
|
||||||
if err != nil || !bytes.Equal(data, decoded) {
|
|
||||||
t.Fatal("Hex roundtrip failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(cast.As(encoding.UnHexFromString(encoded)), data) {
|
|
||||||
t.Error("UnHexFromString with cast.As failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cast.As(encoding.UnHexFromString("!@#$"))) != 0 {
|
|
||||||
t.Error("UnHexFromString should return empty for invalid hex chars with cast.As")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Base64 ---
|
|
||||||
func TestBase64(t *testing.T) {
|
|
||||||
data := []byte("hello world")
|
data := []byte("hello world")
|
||||||
|
|
||||||
// Standard
|
// Hex
|
||||||
enc := encoding.Base64ToString(data)
|
encoded := Hex(data)
|
||||||
dec, err := encoding.UnBase64([]byte(enc))
|
decoded, err := UnHex(encoded)
|
||||||
if err != nil || !bytes.Equal(data, dec) {
|
if err != nil || !bytes.Equal(decoded, data) {
|
||||||
t.Error("Base64 roundtrip failed")
|
t.Errorf("Hex failed: got %v, error: %v", decoded, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.Equal(cast.As(encoding.UnBase64FromString(enc)), data) {
|
// Base64
|
||||||
t.Error("UnBase64FromString with cast.As failed")
|
enc := Base64(data)
|
||||||
|
dec, err := UnBase64(enc)
|
||||||
|
if err != nil || !bytes.Equal(dec, data) {
|
||||||
|
t.Errorf("Base64 failed: got %v, error: %v", dec, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Raw (Unpadded)
|
// Base64Raw
|
||||||
rawEnc := encoding.Base64RawToString(data)
|
rawEnc := Base64Raw(data)
|
||||||
if bytes.HasSuffix([]byte(rawEnc), []byte("=")) {
|
rawDec, err := UnBase64(rawEnc)
|
||||||
t.Error("Base64Raw should not have padding")
|
if err != nil || !bytes.Equal(rawDec, data) {
|
||||||
}
|
t.Errorf("Base64Raw failed: got %v, error: %v", rawDec, err)
|
||||||
rawDec, err := encoding.UnBase64FromString(rawEnc)
|
|
||||||
if err != nil || !bytes.Equal(data, rawDec) {
|
|
||||||
t.Error("Base64Raw smart decoding failed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL
|
// URLBase64
|
||||||
uData := []byte("hello/world+")
|
uData := []byte("https://apigo.cc?a=1&b=2")
|
||||||
uEnc := encoding.UrlBase64ToString(uData)
|
uEnc := URLBase64(uData)
|
||||||
uDec, _ := encoding.UnUrlBase64([]byte(uEnc))
|
uDec, _ := UnURLBase64(uEnc)
|
||||||
if !bytes.Equal(uData, uDec) {
|
if !bytes.Equal(uDec, uData) {
|
||||||
t.Error("UrlBase64 roundtrip failed")
|
t.Errorf("URLBase64 failed: got %v", uDec)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.Equal(cast.As(encoding.UnUrlBase64FromString(uEnc)), uData) {
|
// URLBase64Raw
|
||||||
t.Error("UnUrlBase64FromString with cast.As failed")
|
uRawEnc := URLBase64Raw(uData)
|
||||||
|
uRawDec, err := UnURLBase64(uRawEnc)
|
||||||
|
if err != nil || !bytes.Equal(uRawDec, uData) {
|
||||||
|
t.Errorf("URLBase64Raw failed: got %v, error: %v", uRawDec, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL Raw (Unpadded)
|
// URLEncode
|
||||||
uRawEnc := encoding.UrlBase64RawToString(uData)
|
urlEnc := URLEncode(data)
|
||||||
if bytes.HasSuffix([]byte(uRawEnc), []byte("=")) {
|
urlDec, err := UnURLEncode(urlEnc)
|
||||||
t.Error("UrlBase64Raw should not have padding")
|
if err != nil || !bytes.Equal(urlDec, data) {
|
||||||
|
t.Errorf("URLEncode failed: got %v, error: %v", urlDec, err)
|
||||||
}
|
}
|
||||||
uRawDec, err := encoding.UnUrlBase64FromString(uRawEnc)
|
|
||||||
if err != nil || !bytes.Equal(uData, uRawDec) {
|
// HTMLEscape
|
||||||
t.Error("UrlBase64Raw smart decoding failed")
|
htmlData := "<div>hello</div>"
|
||||||
|
escaped := HTMLEscape(htmlData)
|
||||||
|
if HTMLUnescape(escaped) != htmlData {
|
||||||
|
t.Errorf("HTMLEscape failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTF8Valid
|
||||||
|
if !UTF8Valid("你好") {
|
||||||
|
t.Error("UTF8Valid failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Web ---
|
func TestSortJoin(t *testing.T) {
|
||||||
func TestWebEncoding(t *testing.T) {
|
m := map[string]any{
|
||||||
// URL
|
"b": 2,
|
||||||
data := []byte("a b+c")
|
"a": 1,
|
||||||
enc := encoding.UrlEncode(data)
|
"c": "3",
|
||||||
if !bytes.Equal(cast.As(encoding.UnUrlEncode(enc)), data) {
|
|
||||||
t.Error("UrlEncode roundtrip failed")
|
|
||||||
}
|
}
|
||||||
if len(cast.As(encoding.UnUrlEncode("%ZZ"))) != 0 {
|
res := SortJoin(m, "&", "=", true)
|
||||||
t.Error("UnUrlEncode should return empty for invalid input with cast.As")
|
if res != "a=1&b=2&c=3" {
|
||||||
|
t.Errorf("SortJoin map failed: %s", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML
|
type User struct {
|
||||||
htmlData := []byte("<script>")
|
ID int
|
||||||
escaped := encoding.HtmlEscape(htmlData)
|
Name string
|
||||||
if encoding.HtmlUnescape(escaped) != string(htmlData) {
|
}
|
||||||
t.Error("HtmlEscape roundtrip failed")
|
u := User{ID: 1, Name: "sam"}
|
||||||
|
resStruct := SortJoin(u, ";", ":", false)
|
||||||
|
if resStruct != "ID:1;name:sam" {
|
||||||
|
t.Errorf("SortJoin struct failed: %s", resStruct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Utf8 ---
|
func TestIntEncoder(t *testing.T) {
|
||||||
func TestUtf8(t *testing.T) {
|
val := uint64(123456789)
|
||||||
if !encoding.Utf8Valid([]byte("你好")) {
|
encoded := EncodeInt(val)
|
||||||
t.Error("Valid UTF-8 should pass")
|
decoded := DecodeInt(encoded)
|
||||||
}
|
if val != decoded {
|
||||||
if encoding.Utf8Valid([]byte{0xff, 0xff}) {
|
t.Errorf("IntEncoder failed: expected %d, got %d", val, decoded)
|
||||||
t.Error("Invalid UTF-8 should fail")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
go.mod
4
go.mod
@ -2,4 +2,6 @@ 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.5.0
|
||||||
|
|
||||||
|
require apigo.cc/go/jsmod v1.5.0
|
||||||
|
|||||||
6
go.sum
6
go.sum
@ -1,2 +1,4 @@
|
|||||||
apigo.cc/go/cast v1.2.8 h1:plb676DH2TjYljzf8OEMGT6lIhmZ/xaxEFfs0kDOiSI=
|
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE=
|
||||||
apigo.cc/go/cast v1.2.8/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8=
|
||||||
|
apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec=
|
||||||
|
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import (
|
|||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"apigo.cc/go/cast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IntEncoder 提供整数与字节切片之间的自定义进制转换
|
// IntEncoder 提供整数与字节切片之间的自定义进制转换
|
||||||
@ -33,8 +35,9 @@ func (enc *IntEncoder) AppendInt(buf []byte, u uint64) []byte {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
// FillInt 使用循环字符序列填充字节切片至指定长度
|
// FillInt 使用循环字符序列填充数据至指定长度
|
||||||
func (enc *IntEncoder) FillInt(buf []byte, length int) []byte {
|
func (enc *IntEncoder) FillInt(data any, length int) []byte {
|
||||||
|
buf := cast.To[[]byte](data)
|
||||||
currLen := len(buf)
|
currLen := len(buf)
|
||||||
if currLen >= length {
|
if currLen >= length {
|
||||||
return buf
|
return buf
|
||||||
@ -52,8 +55,9 @@ func (enc *IntEncoder) FillInt(buf []byte, length int) []byte {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExchangeInt 对字节切片进行位置交替重排
|
// ExchangeInt 对数据进行位置交替重排
|
||||||
func (enc *IntEncoder) ExchangeInt(buf []byte) []byte {
|
func (enc *IntEncoder) ExchangeInt(data any) []byte {
|
||||||
|
buf := cast.To[[]byte](data)
|
||||||
size := len(buf)
|
size := len(buf)
|
||||||
if size <= 1 {
|
if size <= 1 {
|
||||||
return buf
|
return buf
|
||||||
@ -76,15 +80,16 @@ func (enc *IntEncoder) ExchangeInt(buf []byte) []byte {
|
|||||||
return buf2
|
return buf2
|
||||||
}
|
}
|
||||||
|
|
||||||
// HashInt 对字节切片进行 HMAC-SHA512 哈希
|
// HashInt 对数据进行 HMAC-SHA512 哈希
|
||||||
func (enc *IntEncoder) HashInt(data []byte, key []byte) []byte {
|
func (enc *IntEncoder) HashInt(data any, key any) []byte {
|
||||||
hash := hmac.New(sha512.New, key)
|
hash := hmac.New(sha512.New, cast.To[[]byte](key))
|
||||||
hash.Write(data)
|
hash.Write(cast.To[[]byte](data))
|
||||||
return hash.Sum([]byte{})
|
return hash.Sum([]byte{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeInt 从字节切片解码为整数
|
// DecodeInt 从数据解码为整数
|
||||||
func (enc *IntEncoder) DecodeInt(buf []byte) uint64 {
|
func (enc *IntEncoder) DecodeInt(data any) uint64 {
|
||||||
|
buf := cast.To[[]byte](data)
|
||||||
radix := uint64(enc.radix)
|
radix := uint64(enc.radix)
|
||||||
if buf == nil {
|
if buf == nil {
|
||||||
return 0
|
return 0
|
||||||
@ -127,7 +132,7 @@ var OrderedIntEncoder, _ = NewIntEncoder("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZab
|
|||||||
|
|
||||||
// EncodeInt 使用默认编码器将整数转换为字节切片
|
// EncodeInt 使用默认编码器将整数转换为字节切片
|
||||||
func EncodeInt(u uint64) []byte {
|
func EncodeInt(u uint64) []byte {
|
||||||
return DefaultIntEncoder.AppendInt(nil, u)
|
return DefaultIntEncoder.EncodeInt(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendInt 使用默认编码器将整数追加到已有字节切片中
|
// AppendInt 使用默认编码器将整数追加到已有字节切片中
|
||||||
@ -135,19 +140,22 @@ func AppendInt(buf []byte, u uint64) []byte {
|
|||||||
return DefaultIntEncoder.AppendInt(buf, u)
|
return DefaultIntEncoder.AppendInt(buf, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeInt 使用默认编码器从字节切片解码为整数
|
// DecodeInt 使用默认编码器从数据解码为整数
|
||||||
func DecodeInt(buf []byte) uint64 {
|
func DecodeInt(data any) uint64 {
|
||||||
return DefaultIntEncoder.DecodeInt(buf)
|
return DefaultIntEncoder.DecodeInt(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExchangeInt(buf []byte) []byte {
|
// ExchangeInt 使用默认编码器对数据进行位置交替重排
|
||||||
return DefaultIntEncoder.ExchangeInt(buf)
|
func ExchangeInt(data any) []byte {
|
||||||
|
return DefaultIntEncoder.ExchangeInt(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HashInt(data []byte, key []byte) []byte {
|
// HashInt 对数据进行 HMAC-SHA512 哈希
|
||||||
|
func HashInt(data any, key any) []byte {
|
||||||
return DefaultIntEncoder.HashInt(data, key)
|
return DefaultIntEncoder.HashInt(data, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FillInt(buf []byte, length int) []byte {
|
// FillInt 使用默认编码器填充数据至指定长度
|
||||||
return DefaultIntEncoder.FillInt(buf, length)
|
func FillInt(data any, length int) []byte {
|
||||||
|
return DefaultIntEncoder.FillInt(data, length)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,58 +1,58 @@
|
|||||||
package encoding_test
|
package encoding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"apigo.cc/go/encoding"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- 正常逻辑测试 ---
|
func TestIntEncoderInstance(t *testing.T) {
|
||||||
func TestIntEncoderNormal(t *testing.T) {
|
enc := DefaultIntEncoder
|
||||||
enc := encoding.DefaultIntEncoder
|
val := uint64(123456789)
|
||||||
nums := []uint64{0, 1, 100, 123456789, 18446744073709551615}
|
encoded := enc.EncodeInt(val)
|
||||||
for _, n := range nums {
|
decoded := enc.DecodeInt(encoded)
|
||||||
if enc.DecodeInt(enc.EncodeInt(n)) != n {
|
if val != decoded {
|
||||||
t.Errorf("Roundtrip failed for %d", n)
|
t.Errorf("IntEncoder instance failed: expected %d, got %d", val, decoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewIntEncoder errors
|
||||||
|
_, err := NewIntEncoder("12", 5) // 字符集不足
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for insufficient digits")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- 边界与异常测试 ---
|
_, err = NewIntEncoder("112345", 6) // 重复字符
|
||||||
func TestIntEncoderEdge(t *testing.T) {
|
if err == nil {
|
||||||
// 初始化异常
|
t.Error("expected error for repeated digits")
|
||||||
_, err := encoding.NewIntEncoder("12", 5) // 字符集不足
|
|
||||||
if err == nil { t.Error("NewIntEncoder failed to detect short digits") }
|
|
||||||
|
|
||||||
_, err = encoding.NewIntEncoder("112345", 2) // 重复字符
|
|
||||||
if err == nil { t.Error("NewIntEncoder failed to detect repeated digits") }
|
|
||||||
|
|
||||||
// 空输入
|
|
||||||
if encoding.DecodeInt(nil) != 0 { t.Error("DecodeInt(nil) != 0") }
|
|
||||||
|
|
||||||
// 填充逻辑边界
|
|
||||||
buf := encoding.EncodeInt(123)
|
|
||||||
filled := encoding.DefaultIntEncoder.FillInt(buf, 2) // 长度小于原长
|
|
||||||
if len(filled) != len(buf) {
|
|
||||||
t.Error("FillInt should not truncate data")
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- 混淆与哈希测试 ---
|
// DecodeInt(nil)
|
||||||
func TestIntEncoderAdvanced(t *testing.T) {
|
if DecodeInt(nil) != 0 {
|
||||||
enc := encoding.DefaultIntEncoder
|
t.Error("DecodeInt(nil) != 0")
|
||||||
buf := enc.EncodeInt(12345)
|
}
|
||||||
|
|
||||||
// Exchange
|
// FillInt
|
||||||
exchanged := enc.ExchangeInt(buf)
|
buf := []byte(EncodeInt(123))
|
||||||
|
filled := FillInt(buf, 10)
|
||||||
|
if len(filled) != 10 {
|
||||||
|
t.Errorf("FillInt failed: expected len 10, got %d", len(filled))
|
||||||
|
}
|
||||||
|
if !bytes.HasPrefix(filled, buf) {
|
||||||
|
t.Error("FillInt prefix mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeInt
|
||||||
|
buf = []byte("abcde")
|
||||||
|
exchanged := ExchangeInt(buf)
|
||||||
if len(exchanged) != len(buf) {
|
if len(exchanged) != len(buf) {
|
||||||
t.Fatal("Exchange length mismatch")
|
t.Error("ExchangeInt len mismatch")
|
||||||
|
}
|
||||||
|
if bytes.Equal(exchanged, buf) {
|
||||||
|
t.Error("ExchangeInt should change data")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash
|
// HashInt
|
||||||
h1 := enc.HashInt(buf, []byte("key"))
|
hashed := HashInt(buf, []byte("key"))
|
||||||
h2 := enc.HashInt(buf, []byte("key"))
|
if len(hashed) == 0 {
|
||||||
if !bytes.Equal(h1, h2) {
|
t.Error("HashInt failed")
|
||||||
t.Error("HashInt non-deterministic")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
js_export.go
Normal file
29
js_export.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package encoding
|
||||||
|
|
||||||
|
import "apigo.cc/go/jsmod"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
jsmod.Register("encoding", map[string]any{
|
||||||
|
"Base64": Base64,
|
||||||
|
"Base64Raw": Base64Raw,
|
||||||
|
"UnBase64": UnBase64,
|
||||||
|
"URLBase64": URLBase64,
|
||||||
|
"URLBase64Raw": URLBase64Raw,
|
||||||
|
"UnURLBase64": UnURLBase64,
|
||||||
|
"Hex": Hex,
|
||||||
|
"UnHex": UnHex,
|
||||||
|
"URLEncode": URLEncode,
|
||||||
|
"UnURLEncode": UnURLEncode,
|
||||||
|
"HTMLEscape": HTMLEscape,
|
||||||
|
"HTMLUnescape": HTMLUnescape,
|
||||||
|
"UTF8Valid": UTF8Valid,
|
||||||
|
"SortJoin": SortJoin,
|
||||||
|
"EncodeInt": func(u uint64) string {
|
||||||
|
return string(EncodeInt(u))
|
||||||
|
},
|
||||||
|
"DecodeInt": DecodeInt,
|
||||||
|
"FillInt": FillInt,
|
||||||
|
"ExchangeInt": ExchangeInt,
|
||||||
|
"HashInt": HashInt,
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user