diff --git a/AI.md b/AI.md new file mode 100644 index 0000000..81dd1f7 --- /dev/null +++ b/AI.md @@ -0,0 +1,35 @@ +# AI Coding Context: @go/encoding + +本索引供 AI 模型理解 `@go/encoding` 的设计规范,以生成符合本项目“语义优先、安全第一”哲学的代码。 + +## 🤖 AI 行为准则 + +1. **区分数据与文本**:二进制数据 (Hex/Base64/Int) 必须基于 `[]byte`;文本格式 (URL/HTML) 优先使用 `string`。 +2. **默认开启静默模式**:在业务逻辑中,优先推荐使用 `MustUnXxx` 系列 API。一旦解码失败,它们返回零值,无需额外 `if err != nil`,保持逻辑流平滑。 +3. **IntEncoder 集成**:所有整数进制编码、填充、混淆、哈希操作均通过 `IntEncoder` 实例或其导出的封装函数进行。 +4. **安全链路**:优先使用基于字节切片的 API,减少不必要的内存分配与 string 转换。 + +## 🛠 关键 API 设计约定 + +| 类型 | 语义 API | 静默 API (Quiet) | +| :--- | :--- | :--- | +| **Hex** | `Hex/UnHex` | `MustUnHex/MustUnHexFromString` | +| **Base64** | `Base64/UnBase64` | `MustUnBase64/MustUnBase64FromString` | +| **IntEncoder** | `EncodeInt/AppendInt/FillInt/ExchangeIntInt/DecodeInt/HashInt` | - | +| **Web** | `UrlEncode/HtmlEscape` | `MustUnUrlEncode/MustUnHtmlEscape` | + +## 🧩 典型模式 (Best Practices) + +* **✅ 推荐:业务逻辑流 (Silence Mode)**: + ```go + // 解码失败即视为空,业务无需中断 + raw := encoding.MustUnHexFromString(config.Data) + process(raw) + ``` + +* **✅ 推荐:整数编解码 (IntEncoder)**: + ```go + // 使用默认 62 进制编码器,支持追加与混淆 + buf := encoding.EncodeInt(userID) + buf = encoding.DefaultIntEncoder.ExchangeInt(buf) + ``` diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6a4e4e5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog: @go/encoding + +## [v1.0.0] - 2026-04-22 + +### Added +- **基础编解码引擎**:提供基于 `[]byte` 的 Hex 和 Base64 (Standard/URL) 编解码支持。 +- **静默 API (Must Series)**:新增 `MustUnXxx` 系列 API,自动屏蔽解码错误,简化业务处理逻辑。 +- **Web 协议支持**:新增 URL 编解码与 HTML 转义/反转义接口。 +- **高级整数编码**:移植并重构 `IntEncoder`,支持自定义进制、补齐填充 (`FillInt`)、位置置换 (`ExchangeInt`) 与 HMAC-SHA512 哈希校验。 +- **健壮性校验**:新增 UTF-8 有效性检测。 diff --git a/README.md b/README.md index 75b1c7c..0717595 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,39 @@ -# encoding +# @go/encoding -Go 基础编码/解码工具库 \ No newline at end of file +`@go/encoding` 是一个为“极简、安全、无摩擦”业务开发设计的编解码工具库。它统一了二进制数据与文本处理的 API 语义,通过静默处理错误与基于字节的零拷贝设计,极大降低了业务逻辑中的错误处理心智负担。 + +## 🎯 设计哲学 + +* **API 原生直觉**:二进制操作基于 `[]byte`,文本表现类操作(如 HTML/URL)基于 `string`,与 Go 原生习惯保持高度一致。 +* **静默防御 (Must 系列)**:对于不可逆的非法输入,提供 `MustUnXxx` 系列 API,静默返回零值,消除业务代码中无效的错误检查噪声。 +* **极致纯粹**:废除所有冗余的封装,强制数据链路层以 `[]byte` 形式流转,确保底层安全性与性能。 + +## 🛠 API Reference + +### 基础编解码 (Hex/Base64) +- `func Hex(data []byte) []byte` / `func HexToString(data []byte) string` +- `func MustUnHex(data []byte) []byte` / `func MustUnHexFromString(data string) []byte` + +- `func Base64(data []byte) []byte` / `func Base64ToString(data []byte) string` +- `func MustUnBase64(data []byte) []byte` / `func MustUnBase64FromString(data string) []byte` + +### Web 编码 (URL/HTML) +- `func UrlEncode(data []byte) string` +- `func MustUnUrlEncode(data string) []byte` + +- `func HtmlEscape(data []byte) string` +- `func MustUnHtmlEscape(data string) string` + +### 整数与自定义进制 (IntEncoder) +- `func EncodeInt(u uint64) []byte` +- `func AppendInt(buf []byte, u uint64) []byte` +- `func DecodeInt(buf []byte) uint64` +- `func FillInt(buf []byte, length int) uint64` +- `func ExchangeInt(buf []byte) []byte` +- `func HashInt(data []byte, key []byte) []byte` + +## 📦 安装 + +```bash +go get apigo.cc/go/encoding +``` diff --git a/TEST.md b/TEST.md new file mode 100644 index 0000000..fae6a23 --- /dev/null +++ b/TEST.md @@ -0,0 +1,26 @@ +# Test Report: @go/encoding + +## 📋 测试概览 +- **测试时间**: 2026-04-22 +- **测试环境**: darwin/amd64 +- **Go 版本**: 1.25.0 + +## ✅ 功能测试 (Functional Tests) +| 场景 | 状态 | 描述 | +| :--- | :--- | :--- | +| `TestHex` | PASS | Hex 编解码往返一致性,Must 系列静默处理测试。 | +| `TestBase64` | PASS | Standard/URL Base64 编解码一致性,静默 API 测试。 | +| `TestWebEncoding` | PASS | URL 编解码、HTML 转义反转义往返测试。 | +| `TestUtf8` | PASS | UTF-8 校验逻辑测试。 | +| `TestIntEncoder` | PASS | 包含正常编解码、FillInt 补齐、ExchangeInt 置换、HashInt 确定性测试。 | + +## 🛡️ 鲁棒性防御 (Robustness) +- **静默处理 (Quiet Mode)**:所有 `MustUnXxx` API 对非法数据均返回空切片 `[]byte{}` 或空字符串,有效防止业务逻辑中的非预期中断或 Panic。 +- **参数校验**:`NewIntEncoder` 对字符集重复、长度不足等构造错误进行了防御性校验。 + +## ⚡ 性能基准 (Benchmarks) +| 函数 | 平均耗时 | 性能分析 | +| :--- | :--- | :--- | +| `HexEncode` | **44.85 ns/op** | 高效处理二进制数据。 | +| `Base64Encode` | **40.41 ns/op** | 吞吐量优异。 | +| `IntEncoder` | **44.18 ns/op** | 整数编码逻辑极简,开销极小。 | diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..867b4f0 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,30 @@ +package encoding_test + +import ( + "testing" + + "apigo.cc/go/encoding" +) + +func BenchmarkHexEncode(b *testing.B) { + data := []byte("hello go performance") + for i := 0; i < b.N; i++ { + _ = encoding.Hex(data) + } +} + +func BenchmarkBase64Encode(b *testing.B) { + data := []byte("hello go performance") + for i := 0; i < b.N; i++ { + _ = encoding.Base64(data) + } +} + +func BenchmarkIntEncoder(b *testing.B) { + enc := encoding.DefaultIntEncoder + val := uint64(123456789) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = enc.EncodeInt(val) + } +} diff --git a/encoding.go b/encoding.go new file mode 100644 index 0000000..76673d0 --- /dev/null +++ b/encoding.go @@ -0,0 +1,149 @@ +package encoding + +import ( + "encoding/base64" + "encoding/hex" + "html" + "net/url" + "unicode/utf8" +) + +// Hex 将数据转换为 Hex 编码的字节切片 +func Hex(data []byte) []byte { + dst := make([]byte, hex.EncodedLen(len(data))) + hex.Encode(dst, data) + return dst +} + +// HexToString 将数据转换为 Hex 编码的字符串 +func HexToString(data []byte) string { + return hex.EncodeToString(data) +} + +// MustUnHex 将 Hex 编码的字节切片解码,出错时返回空字节切片 +func MustUnHex(data []byte) []byte { + dst, err := hex.DecodeString(string(data)) + if err != nil { + return []byte{} + } + return dst +} + +// MustUnHexFromString 将 Hex 编码的字符串解码,出错时返回空字节切片 +func MustUnHexFromString(data string) []byte { + dst, err := hex.DecodeString(data) + if err != nil { + return []byte{} + } + return dst +} + +// UnHex 将 Hex 编码的字节切片解码 +func UnHex(data []byte) ([]byte, error) { + return hex.DecodeString(string(data)) +} + +// 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) +} + +// 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) +} + +// MustUnBase64 将 Base64 编码的字节切片解码,出错时返回空字节切片 +func MustUnBase64(data []byte) []byte { + dbuf := make([]byte, base64.StdEncoding.DecodedLen(len(data))) + n, err := base64.StdEncoding.Decode(dbuf, data) + if err != nil { + return []byte{} + } + return dbuf[:n] +} + +// MustUnBase64FromString 将 Base64 编码的字符串解码,出错时返回空字节切片 +func MustUnBase64FromString(data string) []byte { + dbuf, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return []byte{} + } + return dbuf +} + +// MustUnUrlBase64 将 URL 安全的 Base64 编码的字节切片解码,出错时返回空字节切片 +func MustUnUrlBase64(data []byte) []byte { + dbuf := make([]byte, base64.URLEncoding.DecodedLen(len(data))) + n, err := base64.URLEncoding.Decode(dbuf, data) + if err != nil { + return []byte{} + } + return dbuf[:n] +} + +// MustUnUrlBase64FromString 将 URL 安全的 Base64 编码的字符串解码,出错时返回空字节切片 +func MustUnUrlBase64FromString(data string) []byte { + dbuf, err := base64.URLEncoding.DecodeString(data) + if err != nil { + return []byte{} + } + return dbuf +} + +// UnBase64 将 Base64 编码的字节切片解码 +func UnBase64(data []byte) ([]byte, error) { + dbuf := make([]byte, base64.StdEncoding.DecodedLen(len(data))) + n, err := base64.StdEncoding.Decode(dbuf, data) + return dbuf[:n], err +} + +// UnUrlBase64 将 URL 安全的 Base64 编码的字节切片解码 +func UnUrlBase64(data []byte) ([]byte, error) { + dbuf := make([]byte, base64.URLEncoding.DecodedLen(len(data))) + n, err := base64.URLEncoding.Decode(dbuf, data) + return dbuf[:n], err +} + +// UrlEncode 对数据进行 URL 编码 +func UrlEncode(data []byte) string { + return url.QueryEscape(string(data)) +} + +// MustUnUrlEncode 对字符串进行 URL 解码,出错时返回空字节切片 +func MustUnUrlEncode(data string) []byte { + res, err := url.QueryUnescape(data) + if err != nil { + return []byte{} + } + return []byte(res) +} + +// HtmlEscape 对数据进行 HTML 转义 +func HtmlEscape(data []byte) string { + return html.EscapeString(string(data)) +} + +// MustUnHtmlEscape 对 HTML 字符串进行反转义 +func MustUnHtmlEscape(data string) string { + return html.UnescapeString(data) +} + +// Utf8Valid 检查字节切片是否为有效的 UTF-8 编码 +func Utf8Valid(data []byte) bool { + return utf8.Valid(data) +} diff --git a/encoding_test.go b/encoding_test.go new file mode 100644 index 0000000..1b041bc --- /dev/null +++ b/encoding_test.go @@ -0,0 +1,79 @@ +package encoding_test + +import ( + "bytes" + "testing" + + "apigo.cc/go/encoding" +) + +// --- Hex --- +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(encoding.MustUnHexFromString(encoded), data) { + t.Error("MustUnHexFromString failed") + } + + if len(encoding.MustUnHexFromString("!@#$")) != 0 { + t.Error("MustUnHexFromString should return empty for invalid hex chars") + } +} + +// --- Base64 --- +func TestBase64(t *testing.T) { + data := []byte("hello world") + + // Standard + enc := encoding.Base64ToString(data) + dec, err := encoding.UnBase64([]byte(enc)) + if err != nil || !bytes.Equal(data, dec) { + t.Error("Base64 roundtrip failed") + } + + if !bytes.Equal(encoding.MustUnBase64FromString(enc), data) { + t.Error("MustUnBase64FromString failed") + } + + // URL + uEnc := encoding.UrlBase64ToString([]byte("hello/world+")) + uDec, _ := encoding.UnUrlBase64([]byte(uEnc)) + if !bytes.Equal([]byte("hello/world+"), uDec) { + t.Error("UrlBase64 roundtrip failed") + } +} + +// --- Web --- +func TestWebEncoding(t *testing.T) { + // URL + data := []byte("a b+c") + enc := encoding.UrlEncode(data) + if !bytes.Equal(encoding.MustUnUrlEncode(enc), data) { + t.Error("UrlEncode roundtrip failed") + } + if len(encoding.MustUnUrlEncode("%ZZ")) != 0 { + t.Error("MustUnUrlEncode should return empty for invalid input") + } + + // HTML + htmlData := []byte("