1
0
mirror of https://github.com/duke-git/lancet.git synced 2026-02-04 12:52:28 +08:00

Compare commits

...

6 Commits

Author SHA1 Message Date
dudaodong
c0841d7be5 Merge branch 'v2' of github.com:duke-git/lancet into v2 2026-01-29 10:08:21 +08:00
Javen
b3fd282b50 feat: add address.Smart and Decompose for parse CN address (#346)
* feat: add address.Smart and Decompose for parse CN address

* feat: add Xinjiang directly-administered county-level cities support

- Add '自治区直辖县级市' as a city-level unit (ID: 4043) in A2Data for Xinjiang
- Add 12 directly-administered county-level cities in A3Data (IDs: 4044-4055):
  * 石河子市 (Shihezi, 1976, 8th Division) - ID: 4044
  * 阿拉尔市 (Aral, 2002, 1st Division) - ID: 4045
  * 图木舒克市 (Tumxuk, 2002, 3rd Division) - ID: 4046
  * 五家渠市 (Wujiaqu, 2002, 6th Division) - ID: 4047
  * 北屯市 (Beitun, 2011, 10th Division) - ID: 4048
  * 铁门关市 (Tiemenguan, 2012, 2nd Division) - ID: 4049
  * 双河市 (Shuanghe, 2014, 5th Division) - ID: 4050
  * 可克达拉市 (Kokdala, 2015, 4th Division) - ID: 4051
  * 昆玉市 (Kunyu, 2016, 14th Division) - ID: 4052
  * 胡杨河市 (Huyanghe, 2019, 7th Division) - ID: 4053
  * 新星市 (Xinxing, 2021, 13th Division) - ID: 4054
  * 白杨市 (Baiyang, 2023, 9th Division) - ID: 4055
- All county-level cities are under PID 4043 (自治区直辖县级市)
- Add test case for Xinjiang Shihezi city address parsing
- Now supports parsing addresses like: 新疆石河子市北三路25小区

* docs: formated address data

* fix: parse repeat address error

* feat: update readme file

---------

Co-authored-by: Jiawen <im@linjiawen.com>
2026-01-13 14:00:44 +08:00
efinKiaC
a1cebec9f2 ReadFileByLine bugfix (#350) 2026-01-13 11:24:08 +08:00
Yang Li
88cf1600e8 fix(random): avoid concurrent rand.Seed causing panic (#345) 2025-12-16 13:52:25 +08:00
Javen
0851b68b83 Feat/encryption for sm2 sm3 sm4 (#343)
* feat: add ContainAny

* feat:encryption adds support for SM2, SM3, and SM4 #131

* doc: add docment for SM2, SM3, and SM4 #131

---------

Co-authored-by: Jiawen <im@linjiawen.com>
2025-11-07 19:17:55 +08:00
Javen
5c13fd4f2f Fix/339 (#344)
* feat: add ContainAny

* fix: fix issue #339

---------

Co-authored-by: Jiawen <im@linjiawen.com>
2025-11-07 19:17:09 +08:00
22 changed files with 7355 additions and 29 deletions

View File

@@ -523,6 +523,30 @@ import "github.com/duke-git/lancet/v2/cryptor"
- **<big>RsaVerifySign</big>** : verifies the signature of the data with RSA.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/cryptor.md#RsaVerifySign)]
[[play](https://go.dev/play/p/qhsbf8BJ6Mf)]
- **<big>GenerateSm2Key</big>** : generate SM2 private and public key.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/cryptor.md#GenerateSm2Key)]
[[play](https://go.dev/play/p/bKYMqRLvIx3)]
- **<big>Sm2Encrypt</big>** : encrypt data with SM2 public key.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/cryptor.md#Sm2Encrypt)]
[[play](https://go.dev/play/p/bKYMqRLvIx3)]
- **<big>Sm2Decrypt</big>** : decrypt data with SM2 private key.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/cryptor.md#Sm2Decrypt)]
[[play](https://go.dev/play/p/bKYMqRLvIx3)]
- **<big>Sm3</big>** : return the SM3 hash value (256-bit) of data.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/cryptor.md#Sm3)]
[[play](https://go.dev/play/p/zDAQpteAiOc)]
- **<big>Sm4EcbEncrypt</big>** : encrypt data with SM4 ECB mode.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/cryptor.md#Sm4EcbEncrypt)]
[[play](https://go.dev/play/p/l5IQxYuuaED)]
- **<big>Sm4EcbDecrypt</big>** : decrypt data with SM4 ECB mode.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/cryptor.md#Sm4EcbDecrypt)]
[[play](https://go.dev/play/p/l5IQxYuuaED)]
- **<big>Sm4CbcEncrypt</big>** : encrypt data with SM4 CBC mode.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/cryptor.md#Sm4CbcEncrypt)]
[[play](https://go.dev/play/p/65Q6iYhLRTa)]
- **<big>Sm4CbcDecrypt</big>** : decrypt data with SM4 CBC mode.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/cryptor.md#Sm4CbcDecrypt)]
[[play](https://go.dev/play/p/65Q6iYhLRTa)]
<h3 id="datetime"> 7. Datetime package supports date and time format and compare. &nbsp; &nbsp; &nbsp; &nbsp;<a href="#index">index</a></h3>
@@ -962,6 +986,12 @@ import "github.com/duke-git/lancet/v2/formatter"
- **<big>ParseBinaryBytes</big>** : return the human readable bytes size string into the amount it represents(base 1024).
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/formatter.md#ParseBinaryBytes)]
[[play](https://go.dev/play/p/69v1tTT62x8)]
- **<big>ParseCNAddress</big>** : parses a Chinese address string intelligently and extracts structured information (province, city, district, street, name, phone, etc.). Supports various address formats including county-level cities.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/formatter.md#ParseCNAddress)]
[[play](https://go.dev/play/p/o5l09hQopEV)]
- **<big>ParsePersonInfo</big>** : extracts user information (name, phone, ID card, postal code) from an address string and separates it from the location address.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/formatter.md#ParsePersonInfo)]
[[play](https://go.dev/play/p/JO-uTlJlTy7)]
<h3 id="function"> 13. Function package can control the flow of function execution and support part of functional programming.&nbsp; &nbsp; &nbsp; &nbsp;<a href="#index">index</a></h3>
@@ -2309,7 +2339,7 @@ import "github.com/duke-git/lancet/v2/validator"
[[play](https://go.dev/play/p/jlYApVLLGTZ)]
- **<big>IsEmail</big>** : check if the string is a email address.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/validator.md#IsEmail)]
[[play](https://go.dev/play/p/Os9VaFlT33G)]
[[play](https://go.dev/play/p/HVQ5LAe-vFz)]
- **<big>IsEmptyString</big>** : check if the string is empty.
[[doc](https://github.com/duke-git/lancet/blob/main/docs/en/api/packages/validator.md#IsEmptyString)]
[[play](https://go.dev/play/p/dpzgUjFnBCX)]

View File

@@ -535,6 +535,30 @@ import "github.com/duke-git/lancet/v2/cryptor"
- **<big>RsaVerifySign</big>** : 验证数据的签名是否符合 RSA 算法。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/cryptor.md#RsaVerifySign)]
[[play](https://go.dev/play/p/qhsbf8BJ6Mf)]
- **<big>GenerateSm2Key</big>** : 生成 SM2 公钥和私钥。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/cryptor.md#GenerateSm2Key)]
[[play](https://go.dev/play/p/bKYMqRLvIx3)]
- **<big>Sm2Encrypt</big>** : 使用 SM2 公钥加密数据。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/cryptor.md#Sm2Encrypt)]
[[play](https://go.dev/play/p/bKYMqRLvIx3)]
- **<big>Sm2Decrypt</big>** : 使用 SM2 私钥解密数据。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/cryptor.md#Sm2Decrypt)]
[[play](https://go.dev/play/p/bKYMqRLvIx3)]
- **<big>Sm3</big>** : 返回数据的 SM3 哈希值256 位)。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/cryptor.md#Sm3)]
[[play](https://go.dev/play/p/zDAQpteAiOc)]
- **<big>Sm4EcbEncrypt</big>** : 使用 SM4 ECB 模式加密数据。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/cryptor.md#Sm4EcbEncrypt)]
[[play](https://go.dev/play/p/l5IQxYuuaED)]
- **<big>Sm4EcbDecrypt</big>** : 使用 SM4 ECB 模式解密数据。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/cryptor.md#Sm4EcbDecrypt)]
[[play](https://go.dev/play/p/l5IQxYuuaED)]
- **<big>Sm4CbcEncrypt</big>** : 使用 SM4 CBC 模式加密数据。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/cryptor.md#Sm4CbcEncrypt)]
[[play](https://go.dev/play/p/65Q6iYhLRTa)]
- **<big>Sm4CbcDecrypt</big>** : 使用 SM4 CBC 模式解密数据。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/cryptor.md#Sm4CbcDecrypt)]
[[play](https://go.dev/play/p/65Q6iYhLRTa)]
<h3 id="datetime"> 7. datetime日期时间处理包格式化日期比较日期。&nbsp; &nbsp; &nbsp; &nbsp;<a href="#index">回到目录</a></h3>
@@ -972,6 +996,12 @@ import "github.com/duke-git/lancet/v2/formatter"
- **<big>ParseBinaryBytes</big>** : 将字节单位字符串转换成其所表示的字节数(以 1024 为基数)。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/formatter.md#ParseBinaryBytes)]
[[play](https://go.dev/play/p/69v1tTT62x8)]
- **<big>ParseCNAddress</big>** : 智能解析中国地址字符串并提取结构化信息(省、市、区、街道、姓名、电话等)。支持多种地址格式,包括县级市。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/formatter.md#ParseCNAddress)]
[[play](https://go.dev/play/p/o5l09hQopEV)]
- **<big>ParsePersonInfo</big>** : 从地址字符串中提取用户信息(姓名、电话、身份证、邮编)并将其与位置地址分离。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/formatter.md#ParsePersonInfo)]
[[play](https://go.dev/play/p/JO-uTlJlTy7)]
<h3 id="function"> 13. function 函数包控制函数执行流程,包含部分函数式编程。&nbsp; &nbsp; &nbsp; &nbsp;<a href="#index">回到目录</a></h3>
@@ -2316,7 +2346,7 @@ import "github.com/duke-git/lancet/v2/validator"
[[play](https://go.dev/play/p/jlYApVLLGTZ)]
- **<big>IsEmail</big>** : 验证字符串是否是有效电子邮件地址。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/validator.md#IsEmail)]
[[play](https://go.dev/play/p/Os9VaFlT33G)]
[[play](https://go.dev/play/p/HVQ5LAe-vFz)]
- **<big>IsEmptyString</big>** : 验证字符串是否是空字符串。
[[doc](https://github.com/duke-git/lancet/blob/main/docs/api/packages/validator.md#IsEmptyString)]
[[play](https://go.dev/play/p/dpzgUjFnBCX)]

View File

@@ -0,0 +1,71 @@
package cryptor_test
import (
"encoding/hex"
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func ExampleSm3() {
data := []byte("hello world")
hash := cryptor.Sm3(data)
fmt.Println(hex.EncodeToString(hash))
// Output:
// 44f0061e69fa6fdfc290c494654a05dc0c053da7e5c52b84ef93a9d67d3fff88
}
func ExampleSm4EcbEncrypt() {
key := []byte("1234567890abcdef") // 16 bytes key
plaintext := []byte("hello world")
encrypted := cryptor.Sm4EcbEncrypt(plaintext, key)
decrypted := cryptor.Sm4EcbDecrypt(encrypted, key)
fmt.Println(string(decrypted))
// Output:
// hello world
}
func ExampleSm4CbcEncrypt() {
key := []byte("1234567890abcdef") // 16 bytes key
plaintext := []byte("hello world")
encrypted := cryptor.Sm4CbcEncrypt(plaintext, key)
decrypted := cryptor.Sm4CbcDecrypt(encrypted, key)
fmt.Println(string(decrypted))
// Output:
// hello world
}
func ExampleGenerateSm2Key() {
// Generate SM2 key pair
privateKey, err := cryptor.GenerateSm2Key()
if err != nil {
return
}
plaintext := []byte("hello world")
// Encrypt with public key
ciphertext, err := cryptor.Sm2Encrypt(&privateKey.PublicKey, plaintext)
if err != nil {
return
}
// Decrypt with private key
decrypted, err := cryptor.Sm2Decrypt(privateKey, ciphertext)
if err != nil {
return
}
fmt.Println(string(decrypted))
// Output:
// hello world
}

251
cryptor/gm_sm2.go Normal file
View File

@@ -0,0 +1,251 @@
package cryptor
import (
"crypto/elliptic"
"crypto/rand"
"encoding/binary"
"errors"
"io"
"math/big"
)
// SM2 implements the Chinese SM2 elliptic curve public key algorithm.
// SM2 is based on elliptic curve cryptography and provides encryption, decryption, signing and verification.
//
// Note: This implementation uses crypto/elliptic package methods (GenerateKey, ScalarBaseMult, ScalarMult, IsOnCurve)
// which are marked as deprecated in Go 1.20+. These methods still work correctly and are widely used.
// The //nolint:staticcheck directive suppresses deprecation warnings.
// A future version may replace these with a custom elliptic curve implementation.
var (
sm2P256 *sm2Curve
sm2P256Params = &elliptic.CurveParams{Name: "sm2p256v1"}
)
func init() {
// SM2 curve parameters
sm2P256Params.P, _ = new(big.Int).SetString("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF", 16)
sm2P256Params.N, _ = new(big.Int).SetString("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", 16)
sm2P256Params.B, _ = new(big.Int).SetString("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93", 16)
sm2P256Params.Gx, _ = new(big.Int).SetString("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", 16)
sm2P256Params.Gy, _ = new(big.Int).SetString("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", 16)
sm2P256Params.BitSize = 256
sm2P256 = &sm2Curve{sm2P256Params}
}
type sm2Curve struct {
*elliptic.CurveParams
}
// Sm2PrivateKey represents an SM2 private key.
type Sm2PrivateKey struct {
D *big.Int
PublicKey Sm2PublicKey
}
// Sm2PublicKey represents an SM2 public key.
type Sm2PublicKey struct {
X, Y *big.Int
}
// GenerateSm2Key generates a new SM2 private/public key pair.
// Play: https://go.dev/play/p/bKYMqRLvIx3
func GenerateSm2Key() (*Sm2PrivateKey, error) {
priv, x, y, err := elliptic.GenerateKey(sm2P256, rand.Reader)
if err != nil {
return nil, err
}
privateKey := &Sm2PrivateKey{
D: new(big.Int).SetBytes(priv),
PublicKey: Sm2PublicKey{
X: x,
Y: y,
},
}
return privateKey, nil
}
// Sm2Encrypt encrypts plaintext using SM2 public key.
// Returns ciphertext in the format: C1 || C3 || C2
// C1 = kG (65 bytes in uncompressed format)
// C3 = Hash(x2 || M || y2) (32 bytes for SM3)
// C2 = M xor t (same length as plaintext)
// Play: https://go.dev/play/p/bKYMqRLvIx3
func Sm2Encrypt(pub *Sm2PublicKey, plaintext []byte) ([]byte, error) {
if pub == nil || pub.X == nil || pub.Y == nil {
return nil, errors.New("sm2: invalid public key")
}
for {
// Generate random k
k, err := randFieldElement(sm2P256, rand.Reader)
if err != nil {
return nil, err
}
// C1 = kG
c1x, c1y := sm2P256.ScalarBaseMult(k.Bytes())
// kP = (x2, y2)
x2, y2 := sm2P256.ScalarMult(pub.X, pub.Y, k.Bytes())
// Derive key using KDF
kdfLen := len(plaintext)
t := sm2KDF(append(toBytes(sm2P256, x2), toBytes(sm2P256, y2)...), kdfLen)
// Check if t is all zeros
allZero := true
for _, b := range t {
if b != 0 {
allZero = false
break
}
}
if allZero {
continue
}
// C2 = M xor t
c2 := make([]byte, len(plaintext))
for i := 0; i < len(plaintext); i++ {
c2[i] = plaintext[i] ^ t[i]
}
// C3 = Hash(x2 || M || y2)
c3Input := append(toBytes(sm2P256, x2), plaintext...)
c3Input = append(c3Input, toBytes(sm2P256, y2)...)
c3 := Sm3(c3Input)
// Return C1 || C3 || C2
c1 := sm2MarshalUncompressed(sm2P256, c1x, c1y)
result := append(c1, c3...)
result = append(result, c2...)
return result, nil
}
}
// Sm2Decrypt decrypts ciphertext using SM2 private key.
// Expects ciphertext in the format: C1 || C3 || C2
// Play: https://go.dev/play/p/bKYMqRLvIx3
func Sm2Decrypt(priv *Sm2PrivateKey, ciphertext []byte) ([]byte, error) {
if priv == nil || priv.D == nil {
return nil, errors.New("sm2: invalid private key")
}
// Parse C1 (65 bytes), C3 (32 bytes), C2 (remaining)
if len(ciphertext) < 97 {
return nil, errors.New("sm2: ciphertext too short")
}
c1 := ciphertext[:65]
c3 := ciphertext[65:97]
c2 := ciphertext[97:]
// Parse C1
c1x, c1y := sm2UnmarshalUncompressed(sm2P256, c1)
if c1x == nil {
return nil, errors.New("sm2: invalid C1 point")
}
// Verify C1 is on curve
if !sm2P256.IsOnCurve(c1x, c1y) {
return nil, errors.New("sm2: C1 not on curve")
}
// dC1 = (x2, y2)
x2, y2 := sm2P256.ScalarMult(c1x, c1y, priv.D.Bytes())
// Derive key using KDF
kdfLen := len(c2)
t := sm2KDF(append(toBytes(sm2P256, x2), toBytes(sm2P256, y2)...), kdfLen)
// M = C2 xor t
plaintext := make([]byte, len(c2))
for i := 0; i < len(c2); i++ {
plaintext[i] = c2[i] ^ t[i]
}
// Verify C3 = Hash(x2 || M || y2)
u := append(toBytes(sm2P256, x2), plaintext...)
u = append(u, toBytes(sm2P256, y2)...)
hash := Sm3(u)
for i := 0; i < len(c3); i++ {
if c3[i] != hash[i] {
return nil, errors.New("sm2: hash verification failed")
}
}
return plaintext, nil
}
// SM2 KDF (Key Derivation Function)
func sm2KDF(z []byte, klen int) []byte {
limit := (klen + 31) / 32
result := make([]byte, 0, limit*32)
for i := 1; i <= limit; i++ {
counter := make([]byte, 4)
binary.BigEndian.PutUint32(counter, uint32(i))
hash := Sm3(append(z, counter...))
result = append(result, hash...)
}
return result[:klen]
}
func toBytes(curve elliptic.Curve, value *big.Int) []byte {
byteLen := (curve.Params().BitSize + 7) / 8
buf := make([]byte, byteLen)
b := value.Bytes()
copy(buf[byteLen-len(b):], b)
return buf
}
func sm2MarshalUncompressed(curve *sm2Curve, x, y *big.Int) []byte {
byteLen := (curve.BitSize + 7) / 8
ret := make([]byte, 1+2*byteLen)
ret[0] = 4 // uncompressed point
xBytes := x.Bytes()
copy(ret[1+byteLen-len(xBytes):], xBytes)
yBytes := y.Bytes()
copy(ret[1+2*byteLen-len(yBytes):], yBytes)
return ret
}
func sm2UnmarshalUncompressed(curve *sm2Curve, data []byte) (*big.Int, *big.Int) {
byteLen := (curve.BitSize + 7) / 8
if len(data) != 1+2*byteLen {
return nil, nil
}
if data[0] != 4 {
return nil, nil
}
x := new(big.Int).SetBytes(data[1 : 1+byteLen])
y := new(big.Int).SetBytes(data[1+byteLen:])
return x, y
}
func randFieldElement(c elliptic.Curve, rand io.Reader) (*big.Int, error) {
params := c.Params()
b := make([]byte, params.BitSize/8+8)
_, err := io.ReadFull(rand, b)
if err != nil {
return nil, err
}
k := new(big.Int).SetBytes(b)
n := new(big.Int).Sub(params.N, big.NewInt(1))
k.Mod(k, n)
k.Add(k, big.NewInt(1))
return k, nil
}

211
cryptor/gm_sm3.go Normal file
View File

@@ -0,0 +1,211 @@
package cryptor
import (
"encoding/binary"
"hash"
)
// SM3 implements the Chinese SM3 cryptographic hash algorithm.
// SM3 produces a 256-bit (32-byte) hash value.
const (
sm3BlockSize = 64
sm3Size = 32
sm3T1 = 0x79cc4519
sm3T2 = 0x7a879d8a
)
var sm3IV = [8]uint32{
0x7380166f, 0x4914b2b9, 0x172442d7, 0xda8a0600,
0xa96f30bc, 0x163138aa, 0xe38dee4d, 0xb0fb0e4e,
}
type sm3Digest struct {
h [8]uint32
x [sm3BlockSize]byte
nx int
len uint64
}
// Sm3 returns a new hash.Hash computing the SM3 checksum.
// Play: https://go.dev/play/p/zDAQpteAiOc
func Sm3(data []byte) []byte {
h := newSm3()
h.Write(data)
return h.Sum(nil)
}
func newSm3() hash.Hash {
d := new(sm3Digest)
d.Reset()
return d
}
func (d *sm3Digest) Reset() {
d.h = sm3IV
d.nx = 0
d.len = 0
}
func (d *sm3Digest) Size() int {
return sm3Size
}
func (d *sm3Digest) BlockSize() int {
return sm3BlockSize
}
func (d *sm3Digest) Write(p []byte) (nn int, err error) {
nn = len(p)
d.len += uint64(nn)
if d.nx > 0 {
n := copy(d.x[d.nx:], p)
d.nx += n
if d.nx == sm3BlockSize {
sm3Block(d, d.x[:])
d.nx = 0
}
p = p[n:]
}
if len(p) >= sm3BlockSize {
n := len(p) &^ (sm3BlockSize - 1)
sm3Block(d, p[:n])
p = p[n:]
}
if len(p) > 0 {
d.nx = copy(d.x[:], p)
}
return
}
func (d *sm3Digest) Sum(in []byte) []byte {
d0 := *d
hash := d0.checkSum()
return append(in, hash[:]...)
}
func (d *sm3Digest) checkSum() [sm3Size]byte {
len := d.len
var tmp [64]byte
tmp[0] = 0x80
if len%64 < 56 {
d.Write(tmp[0 : 56-len%64])
} else {
d.Write(tmp[0 : 64+56-len%64])
}
len <<= 3
binary.BigEndian.PutUint64(tmp[:], len)
d.Write(tmp[0:8])
if d.nx != 0 {
panic("d.nx != 0")
}
var digest [sm3Size]byte
for i := 0; i < 8; i++ {
binary.BigEndian.PutUint32(digest[i*4:], d.h[i])
}
return digest
}
func sm3Block(dig *sm3Digest, p []byte) {
var w [68]uint32
var w1 [64]uint32
h0, h1, h2, h3, h4, h5, h6, h7 := dig.h[0], dig.h[1], dig.h[2], dig.h[3], dig.h[4], dig.h[5], dig.h[6], dig.h[7]
for len(p) >= sm3BlockSize {
for i := 0; i < 16; i++ {
j := i * 4
w[i] = binary.BigEndian.Uint32(p[j : j+4])
}
for i := 16; i < 68; i++ {
w[i] = sm3P1(w[i-16]^w[i-9]^sm3RotateLeft(w[i-3], 15)) ^ sm3RotateLeft(w[i-13], 7) ^ w[i-6]
}
for i := 0; i < 64; i++ {
w1[i] = w[i] ^ w[i+4]
}
A, B, C, D, E, F, G, H := h0, h1, h2, h3, h4, h5, h6, h7
for j := 0; j < 64; j++ {
var ss1, ss2, tt1, tt2, t uint32
if j < 16 {
t = sm3T1
} else {
t = sm3T2
}
ss1 = sm3RotateLeft(sm3RotateLeft(A, 12)+E+sm3RotateLeft(t, uint32(j%32)), 7)
ss2 = ss1 ^ sm3RotateLeft(A, 12)
if j < 16 {
tt1 = sm3FF0(A, B, C) + D + ss2 + w1[j]
tt2 = sm3GG0(E, F, G) + H + ss1 + w[j]
} else {
tt1 = sm3FF1(A, B, C) + D + ss2 + w1[j]
tt2 = sm3GG1(E, F, G) + H + ss1 + w[j]
}
D = C
C = sm3RotateLeft(B, 9)
B = A
A = tt1
H = G
G = sm3RotateLeft(F, 19)
F = E
E = sm3P0(tt2)
}
h0 ^= A
h1 ^= B
h2 ^= C
h3 ^= D
h4 ^= E
h5 ^= F
h6 ^= G
h7 ^= H
p = p[sm3BlockSize:]
}
dig.h[0], dig.h[1], dig.h[2], dig.h[3], dig.h[4], dig.h[5], dig.h[6], dig.h[7] = h0, h1, h2, h3, h4, h5, h6, h7
}
func sm3RotateLeft(x, n uint32) uint32 {
return (x << n) | (x >> (32 - n))
}
func sm3P0(x uint32) uint32 {
return x ^ sm3RotateLeft(x, 9) ^ sm3RotateLeft(x, 17)
}
func sm3P1(x uint32) uint32 {
return x ^ sm3RotateLeft(x, 15) ^ sm3RotateLeft(x, 23)
}
func sm3FF0(x, y, z uint32) uint32 {
return x ^ y ^ z
}
func sm3FF1(x, y, z uint32) uint32 {
return (x & y) | (x & z) | (y & z)
}
func sm3GG0(x, y, z uint32) uint32 {
return x ^ y ^ z
}
func sm3GG1(x, y, z uint32) uint32 {
return (x & y) | (^x & z)
}

270
cryptor/gm_sm4.go Normal file
View File

@@ -0,0 +1,270 @@
package cryptor
import (
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"io"
)
// SM4 implements the Chinese SM4 block cipher.
// SM4 is a 128-bit block cipher with 128-bit keys.
// This implementation uses pre-computed lookup tables for optimal performance.
const sm4BlockSize = 16
// Pre-computed T-transformation lookup tables for performance optimization
var sm4T1Table [256][4]uint32 // S-box + L1 transformation
var sm4T2Table [256][4]uint32 // S-box + L2 transformation
var sm4Sbox = [256]byte{
0xd6, 0x90, 0xe9, 0xfe, 0xcc, 0xe1, 0x3d, 0xb7, 0x16, 0xb6, 0x14, 0xc2, 0x28, 0xfb, 0x2c, 0x05,
0x2b, 0x67, 0x9a, 0x76, 0x2a, 0xbe, 0x04, 0xc3, 0xaa, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99,
0x9c, 0x42, 0x50, 0xf4, 0x91, 0xef, 0x98, 0x7a, 0x33, 0x54, 0x0b, 0x43, 0xed, 0xcf, 0xac, 0x62,
0xe4, 0xb3, 0x1c, 0xa9, 0xc9, 0x08, 0xe8, 0x95, 0x80, 0xdf, 0x94, 0xfa, 0x75, 0x8f, 0x3f, 0xa6,
0x47, 0x07, 0xa7, 0xfc, 0xf3, 0x73, 0x17, 0xba, 0x83, 0x59, 0x3c, 0x19, 0xe6, 0x85, 0x4f, 0xa8,
0x68, 0x6b, 0x81, 0xb2, 0x71, 0x64, 0xda, 0x8b, 0xf8, 0xeb, 0x0f, 0x4b, 0x70, 0x56, 0x9d, 0x35,
0x1e, 0x24, 0x0e, 0x5e, 0x63, 0x58, 0xd1, 0xa2, 0x25, 0x22, 0x7c, 0x3b, 0x01, 0x21, 0x78, 0x87,
0xd4, 0x00, 0x46, 0x57, 0x9f, 0xd3, 0x27, 0x52, 0x4c, 0x36, 0x02, 0xe7, 0xa0, 0xc4, 0xc8, 0x9e,
0xea, 0xbf, 0x8a, 0xd2, 0x40, 0xc7, 0x38, 0xb5, 0xa3, 0xf7, 0xf2, 0xce, 0xf9, 0x61, 0x15, 0xa1,
0xe0, 0xae, 0x5d, 0xa4, 0x9b, 0x34, 0x1a, 0x55, 0xad, 0x93, 0x32, 0x30, 0xf5, 0x8c, 0xb1, 0xe3,
0x1d, 0xf6, 0xe2, 0x2e, 0x82, 0x66, 0xca, 0x60, 0xc0, 0x29, 0x23, 0xab, 0x0d, 0x53, 0x4e, 0x6f,
0xd5, 0xdb, 0x37, 0x45, 0xde, 0xfd, 0x8e, 0x2f, 0x03, 0xff, 0x6a, 0x72, 0x6d, 0x6c, 0x5b, 0x51,
0x8d, 0x1b, 0xaf, 0x92, 0xbb, 0xdd, 0xbc, 0x7f, 0x11, 0xd9, 0x5c, 0x41, 0x1f, 0x10, 0x5a, 0xd8,
0x0a, 0xc1, 0x31, 0x88, 0xa5, 0xcd, 0x7b, 0xbd, 0x2d, 0x74, 0xd0, 0x12, 0xb8, 0xe5, 0xb4, 0xb0,
0x89, 0x69, 0x97, 0x4a, 0x0c, 0x96, 0x77, 0x7e, 0x65, 0xb9, 0xf1, 0x09, 0xc5, 0x6e, 0xc6, 0x84,
0x18, 0xf0, 0x7d, 0xec, 0x3a, 0xdc, 0x4d, 0x20, 0x79, 0xee, 0x5f, 0x3e, 0xd7, 0xcb, 0x39, 0x48,
}
var sm4FK = [4]uint32{0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc}
var sm4CK = [32]uint32{
0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269,
0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9,
0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249,
0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9,
0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229,
0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299,
0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209,
0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279,
}
// 初始化预计算查找表
func init() {
// Pre-compute all possible T1 and T2 transformations
for pos := 0; pos < 4; pos++ {
for i := 0; i < 256; i++ {
// S-box 替换
sboxVal := sm4Sbox[i]
// 根据字节位置计算偏移
shift := uint32((3 - pos) * 8)
b := uint32(sboxVal) << shift
// L1 变换b ^ ROL(b,2) ^ ROL(b,10) ^ ROL(b,18) ^ ROL(b,24)
sm4T1Table[i][pos] = b ^ sm4RotateLeft(b, 2) ^ sm4RotateLeft(b, 10) ^ sm4RotateLeft(b, 18) ^ sm4RotateLeft(b, 24)
// L2 变换b ^ ROL(b,13) ^ ROL(b,23)
sm4T2Table[i][pos] = b ^ sm4RotateLeft(b, 13) ^ sm4RotateLeft(b, 23)
}
}
}
type sm4Cipher struct {
enc [32]uint32
dec [32]uint32
}
// Sm4EcbEncrypt encrypts data using SM4 in ECB mode.
// key must be 16 bytes.
// Play: https://go.dev/play/p/l5IQxYuuaED
func Sm4EcbEncrypt(data, key []byte) []byte {
if len(key) != 16 {
panic("sm4: key length must be 16 bytes")
}
c := newSm4Cipher(key)
padded := pkcs7Padding(data, sm4BlockSize)
encrypted := make([]byte, len(padded))
for i := 0; i < len(padded); i += sm4BlockSize {
c.Encrypt(encrypted[i:i+sm4BlockSize], padded[i:i+sm4BlockSize])
}
return encrypted
}
// Sm4EcbDecrypt decrypts data using SM4 in ECB mode.
// key must be 16 bytes.
// Play: https://go.dev/play/p/l5IQxYuuaED
func Sm4EcbDecrypt(encrypted, key []byte) []byte {
if len(key) != 16 {
panic("sm4: key length must be 16 bytes")
}
if len(encrypted)%sm4BlockSize != 0 {
panic("sm4: encrypted data length must be multiple of block size")
}
c := newSm4Cipher(key)
decrypted := make([]byte, len(encrypted))
for i := 0; i < len(encrypted); i += sm4BlockSize {
c.Decrypt(decrypted[i:i+sm4BlockSize], encrypted[i:i+sm4BlockSize])
}
return pkcs7UnPadding(decrypted)
}
// Sm4CbcEncrypt encrypts data using SM4 in CBC mode.
// key must be 16 bytes.
// Play: https://go.dev/play/p/65Q6iYhLRTa
func Sm4CbcEncrypt(data, key []byte) []byte {
if len(key) != 16 {
panic("sm4: key length must be 16 bytes")
}
c := newSm4Cipher(key)
padded := pkcs7Padding(data, sm4BlockSize)
iv := make([]byte, sm4BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
panic("sm4: failed to generate IV: " + err.Error())
}
encrypted := make([]byte, len(padded))
mode := cipher.NewCBCEncrypter(c, iv)
mode.CryptBlocks(encrypted, padded)
return append(iv, encrypted...)
}
// Sm4CbcDecrypt decrypts data using SM4 in CBC mode.
// key must be 16 bytes.
// Play: https://go.dev/play/p/65Q6iYhLRTa
func Sm4CbcDecrypt(encrypted, key []byte) []byte {
if len(key) != 16 {
panic("sm4: key length must be 16 bytes")
}
if len(encrypted) < sm4BlockSize {
panic("sm4: encrypted data too short")
}
if len(encrypted)%sm4BlockSize != 0 {
panic("sm4: encrypted data length must be multiple of block size")
}
c := newSm4Cipher(key)
iv := encrypted[:sm4BlockSize]
ciphertext := encrypted[sm4BlockSize:]
decrypted := make([]byte, len(ciphertext))
mode := cipher.NewCBCDecrypter(c, iv)
mode.CryptBlocks(decrypted, ciphertext)
return pkcs7UnPadding(decrypted)
}
func newSm4Cipher(key []byte) *sm4Cipher {
c := &sm4Cipher{}
var mk [4]uint32
for i := 0; i < 4; i++ {
mk[i] = binary.BigEndian.Uint32(key[i*4 : (i+1)*4])
}
var k [36]uint32
k[0] = mk[0] ^ sm4FK[0]
k[1] = mk[1] ^ sm4FK[1]
k[2] = mk[2] ^ sm4FK[2]
k[3] = mk[3] ^ sm4FK[3]
for i := 0; i < 32; i++ {
k[i+4] = k[i] ^ sm4T2Fast(k[i+1]^k[i+2]^k[i+3]^sm4CK[i])
c.enc[i] = k[i+4]
}
for i := 0; i < 32; i++ {
c.dec[i] = c.enc[31-i]
}
return c
}
func (c *sm4Cipher) BlockSize() int {
return sm4BlockSize
}
func (c *sm4Cipher) Encrypt(dst, src []byte) {
if len(src) < sm4BlockSize {
panic("sm4: input not full block")
}
if len(dst) < sm4BlockSize {
panic("sm4: output not full block")
}
// 使用局部变量避免数组分配,提升性能
x0 := binary.BigEndian.Uint32(src[0:4])
x1 := binary.BigEndian.Uint32(src[4:8])
x2 := binary.BigEndian.Uint32(src[8:12])
x3 := binary.BigEndian.Uint32(src[12:16])
// 32 轮加密
for i := 0; i < 32; i++ {
t := x1 ^ x2 ^ x3 ^ c.enc[i]
x0 ^= sm4T1Fast(t)
x0, x1, x2, x3 = x1, x2, x3, x0
}
binary.BigEndian.PutUint32(dst[0:4], x3)
binary.BigEndian.PutUint32(dst[4:8], x2)
binary.BigEndian.PutUint32(dst[8:12], x1)
binary.BigEndian.PutUint32(dst[12:16], x0)
}
func (c *sm4Cipher) Decrypt(dst, src []byte) {
if len(src) < sm4BlockSize {
panic("sm4: input not full block")
}
if len(dst) < sm4BlockSize {
panic("sm4: output not full block")
}
x0 := binary.BigEndian.Uint32(src[0:4])
x1 := binary.BigEndian.Uint32(src[4:8])
x2 := binary.BigEndian.Uint32(src[8:12])
x3 := binary.BigEndian.Uint32(src[12:16])
// 32 轮解密
for i := 0; i < 32; i++ {
t := x1 ^ x2 ^ x3 ^ c.dec[i]
x0 ^= sm4T1Fast(t)
x0, x1, x2, x3 = x1, x2, x3, x0
}
binary.BigEndian.PutUint32(dst[0:4], x3)
binary.BigEndian.PutUint32(dst[4:8], x2)
binary.BigEndian.PutUint32(dst[8:12], x1)
binary.BigEndian.PutUint32(dst[12:16], x0)
}
// 使用预计算查找表的快速 T1 变换(用于加密轮函数)
func sm4T1Fast(a uint32) uint32 {
return sm4T1Table[byte(a>>24)][0] ^
sm4T1Table[byte(a>>16)][1] ^
sm4T1Table[byte(a>>8)][2] ^
sm4T1Table[byte(a)][3]
}
// 使用预计算查找表的快速 T2 变换(用于密钥扩展)
func sm4T2Fast(a uint32) uint32 {
return sm4T2Table[byte(a>>24)][0] ^
sm4T2Table[byte(a>>16)][1] ^
sm4T2Table[byte(a>>8)][2] ^
sm4T2Table[byte(a)][3]
}
func sm4RotateLeft(x uint32, n uint32) uint32 {
return (x << n) | (x >> (32 - n))
}

163
cryptor/gm_test.go Normal file
View File

@@ -0,0 +1,163 @@
package cryptor
import (
"encoding/hex"
"testing"
"github.com/duke-git/lancet/v2/internal"
)
func TestSm3(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestSm3")
tests := []struct {
input string
expected string
}{
{
input: "abc",
expected: "66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0",
},
{
input: "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd",
expected: "debe9ff92275b8a138604889c18e5a4d6fdb70e5387e5765293dcba39c0c5732",
},
{
input: "",
expected: "1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b",
},
}
for _, tt := range tests {
result := Sm3([]byte(tt.input))
resultHex := hex.EncodeToString(result)
assert.Equal(tt.expected, resultHex)
}
}
func TestSm4EcbEncryptDecrypt(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestSm4EcbEncryptDecrypt")
key := []byte("1234567890abcdef") // 16 bytes
plaintext := []byte("Hello, SM4!")
// Encrypt
encrypted := Sm4EcbEncrypt(plaintext, key)
assert.IsNotNil(encrypted)
// Decrypt
decrypted := Sm4EcbDecrypt(encrypted, key)
assert.Equal(plaintext, decrypted)
}
func TestSm4CbcEncryptDecrypt(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestSm4CbcEncryptDecrypt")
key := []byte("1234567890abcdef") // 16 bytes
plaintext := []byte("Hello, SM4 CBC mode!")
// Encrypt
encrypted := Sm4CbcEncrypt(plaintext, key)
assert.IsNotNil(encrypted)
// Decrypt
decrypted := Sm4CbcDecrypt(encrypted, key)
assert.Equal(plaintext, decrypted)
}
func TestSm4EcbWithLongData(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestSm4EcbWithLongData")
key := []byte("1234567890abcdef")
plaintext := []byte("This is a longer message that spans multiple blocks for SM4 encryption testing.")
encrypted := Sm4EcbEncrypt(plaintext, key)
decrypted := Sm4EcbDecrypt(encrypted, key)
assert.Equal(plaintext, decrypted)
}
func TestSm2EncryptDecrypt(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestSm2EncryptDecrypt")
// Generate key pair
privateKey, err := GenerateSm2Key()
assert.IsNil(err)
assert.IsNotNil(privateKey)
plaintext := []byte("Hello, SM2!")
// Encrypt with public key
ciphertext, err := Sm2Encrypt(&privateKey.PublicKey, plaintext)
assert.IsNil(err)
assert.IsNotNil(ciphertext)
// Decrypt with private key
decrypted, err := Sm2Decrypt(privateKey, ciphertext)
assert.IsNil(err)
assert.Equal(plaintext, decrypted)
}
func TestSm2WithLongData(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestSm2WithLongData")
privateKey, err := GenerateSm2Key()
assert.IsNil(err)
plaintext := []byte("This is a longer message for SM2 encryption testing. " +
"SM2 is an elliptic curve public key cryptography algorithm.")
ciphertext, err := Sm2Encrypt(&privateKey.PublicKey, plaintext)
assert.IsNil(err)
decrypted, err := Sm2Decrypt(privateKey, ciphertext)
assert.IsNil(err)
assert.Equal(plaintext, decrypted)
}
func TestSm4InvalidKeyLength(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestSm4InvalidKeyLength")
defer func() {
if r := recover(); r != nil {
assert.IsNotNil(r)
}
}()
key := []byte("short")
plaintext := []byte("test")
Sm4EcbEncrypt(plaintext, key) // Should panic
}
func TestSm2InvalidInput(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestSm2InvalidInput")
// Test with nil public key
_, err := Sm2Encrypt(nil, []byte("test"))
assert.IsNotNil(err)
// Test with nil private key
_, err = Sm2Decrypt(nil, []byte("test"))
assert.IsNotNil(err)
// Test with invalid ciphertext
privateKey, _ := GenerateSm2Key()
_, err = Sm2Decrypt(privateKey, []byte("short"))
assert.IsNotNil(err)
}

View File

@@ -1,6 +1,6 @@
# Cryptor
cryptor 包包含数据加密和解密功能。支持 base64, md5, hmac, hash, aes, des, rsa。
cryptor 包包含数据加密和解密功能。支持 base64, md5, hmac, hash, aes, des, rsa, sm2, sm3, sm4
<div STYLE="page-break-after: always;"></div>
@@ -74,6 +74,14 @@ import (
- [RsaDecryptOAEP](#RsaDecryptOAEP)
- [RsaSign](#RsaSign)
- [RsaVerifySign](#RsaVerifySign)
- [Sm3](#Sm3)
- [Sm4EcbEncrypt](#Sm4EcbEncrypt)
- [Sm4EcbDecrypt](#Sm4EcbDecrypt)
- [Sm4CbcEncrypt](#Sm4CbcEncrypt)
- [Sm4CbcDecrypt](#Sm4CbcDecrypt)
- [GenerateSm2Key](#GenerateSm2Key)
- [Sm2Encrypt](#Sm2Encrypt)
- [Sm2Decrypt](#Sm2Decrypt)
<div STYLE="page-break-after: always;"></div>
@@ -1829,3 +1837,279 @@ func main() {
}
}
```
### <span id="Sm3">Sm3</span>
<p>计算 SM3 哈希值国密SM3密码杂凑算法。SM3 是中国国家密码管理局发布的密码杂凑算法,用于替代 MD5/SHA-1/SHA-2 等国际算法。</p>
<b>函数签名:</b>
```go
func Sm3(data []byte) []byte
```
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/zDAQpteAiOc)</span></b>
```go
package main
import (
"encoding/hex"
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
data := []byte("hello world")
hash := cryptor.Sm3(data)
fmt.Println(hex.EncodeToString(hash))
// Output:
// 44f0061e69fa6fdfc290c494654a05dc0c053da7e5c52b84ef93a9d67d3fff88
}
```
### <span id="Sm4EcbEncrypt">Sm4EcbEncrypt</span>
<p>使用 SM4 ECB 模式加密数据国密SM4分组密码算法。密钥长度必须为 16 字节。</p>
<b>函数签名:</b>
```go
func Sm4EcbEncrypt(data, key []byte) []byte
```
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/l5IQxYuuaED)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
key := []byte("1234567890abcdef") // 16 bytes key
plaintext := []byte("hello world")
encrypted := cryptor.Sm4EcbEncrypt(plaintext, key)
decrypted := cryptor.Sm4EcbDecrypt(encrypted, key)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```
### <span id="Sm4EcbDecrypt">Sm4EcbDecrypt</span>
<p>使用 SM4 ECB 模式解密数据。密钥长度必须为 16 字节。</p>
<b>函数签名:</b>
```go
func Sm4EcbDecrypt(encrypted, key []byte) []byte
```
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/l5IQxYuuaED)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
key := []byte("1234567890abcdef")
plaintext := []byte("hello world")
encrypted := cryptor.Sm4EcbEncrypt(plaintext, key)
decrypted := cryptor.Sm4EcbDecrypt(encrypted, key)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```
### <span id="Sm4CbcEncrypt">Sm4CbcEncrypt</span>
<p>使用 SM4 CBC 模式加密数据。密钥长度必须为 16 字节。返回的密文包含 IV前 16 字节)。</p>
<b>函数签名:</b>
```go
func Sm4CbcEncrypt(data, key []byte) []byte
```
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/65Q6iYhLRTa)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
key := []byte("1234567890abcdef")
plaintext := []byte("hello world")
encrypted := cryptor.Sm4CbcEncrypt(plaintext, key)
decrypted := cryptor.Sm4CbcDecrypt(encrypted, key)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```
### <span id="Sm4CbcDecrypt">Sm4CbcDecrypt</span>
<p>使用 SM4 CBC 模式解密数据。密钥长度必须为 16 字节。密文应包含 IV前 16 字节)。</p>
<b>函数签名:</b>
```go
func Sm4CbcDecrypt(encrypted, key []byte) []byte
```
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/65Q6iYhLRTa)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
key := []byte("1234567890abcdef")
plaintext := []byte("hello world")
encrypted := cryptor.Sm4CbcEncrypt(plaintext, key)
decrypted := cryptor.Sm4CbcDecrypt(encrypted, key)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```
### <span id="GenerateSm2Key">GenerateSm2Key</span>
<p>生成 SM2 密钥对国密SM2椭圆曲线公钥密码算法。SM2 是基于椭圆曲线的非对称加密算法。</p>
<b>函数签名:</b>
```go
func GenerateSm2Key() (*Sm2PrivateKey, error)
```
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/bKYMqRLvIx3)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
privateKey, err := cryptor.GenerateSm2Key()
if err != nil {
return
}
plaintext := []byte("hello world")
ciphertext, _ := cryptor.Sm2Encrypt(&privateKey.PublicKey, plaintext)
decrypted, _ := cryptor.Sm2Decrypt(privateKey, ciphertext)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```
### <span id="Sm2Encrypt">Sm2Encrypt</span>
<p>使用 SM2 公钥加密数据。返回的密文格式为C1(65字节) || C3(32字节) || C2(明文长度)。</p>
<b>函数签名:</b>
```go
func Sm2Encrypt(pub *Sm2PublicKey, plaintext []byte) ([]byte, error)
```
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/bKYMqRLvIx3)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
privateKey, _ := cryptor.GenerateSm2Key()
plaintext := []byte("hello world")
ciphertext, _ := cryptor.Sm2Encrypt(&privateKey.PublicKey, plaintext)
decrypted, _ := cryptor.Sm2Decrypt(privateKey, ciphertext)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```
### <span id="Sm2Decrypt">Sm2Decrypt</span>
<p>使用 SM2 私钥解密数据。密文格式应为C1(65字节) || C3(32字节) || C2(明文长度)。</p>
<b>函数签名:</b>
```go
func Sm2Decrypt(priv *Sm2PrivateKey, ciphertext []byte) ([]byte, error)
```
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/bKYMqRLvIx3)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
privateKey, _ := cryptor.GenerateSm2Key()
plaintext := []byte("hello world")
ciphertext, _ := cryptor.Sm2Encrypt(&privateKey.PublicKey, plaintext)
decrypted, _ := cryptor.Sm2Decrypt(privateKey, ciphertext)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```

View File

@@ -8,6 +8,7 @@ formatter 格式化器包含一些数据格式化处理方法。
- [https://github.com/duke-git/lancet/blob/main/formatter/formatter.go](https://github.com/duke-git/lancet/blob/main/formatter/formatter.go)
- [https://github.com/duke-git/lancet/blob/main/formatter/byte.go](https://github.com/duke-git/lancet/blob/main/formatter/byte.go)
- [https://github.com/duke-git/lancet/blob/main/formatter/address.go](https://github.com/duke-git/lancet/blob/main/formatter/address.go)
<div STYLE="page-break-after: always;"></div>
@@ -30,6 +31,8 @@ import (
- [BinaryBytes](#BinaryBytes)
- [ParseDecimalBytes](#ParseDecimalBytes)
- [ParseBinaryBytes](#ParseBinaryBytes)
- [ParseCNAddress](#ParseCNAddress)
- [ParsePersonInfo](#ParsePersonInfo)
<div STYLE="page-break-after: always;"></div>
@@ -308,3 +311,134 @@ func main() {
// 12492
}
```
### <span id="ParseCNAddress">ParseCNAddress</span>
<p>智能解析中国地址字符串并提取结构化信息。可以解析带或不带用户信息(姓名、电话、身份证等)的地址。当 withUser 为 true 时,从地址字符串中提取用户信息。当 withUser 为 false 时,仅解析位置信息。支持多种地址格式:标准格式、紧凑格式、带关键词格式、县级市格式等。</p>
<b>函数签名:</b>
```go
func ParseCNAddress(str string, withUser bool) *AddressInfo
```
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/o5l09hQopEV)</span></b>
```go
package main
import (
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/formatter"
)
func main() {
// 解析包含用户信息的完整地址
result1 := formatter.ParseCNAddress("张三 13800138000 北京市朝阳区建国路1号", true)
jsonData1, _ := json.MarshalIndent(result1, "", " ")
fmt.Println("示例 1 - 带用户信息:")
fmt.Println(string(jsonData1))
// 仅解析地址,不提取用户信息
result2 := formatter.ParseCNAddress("北京市海淀区中关村大街1号", false)
fmt.Printf("\n示例 2 - 仅地址:\n")
fmt.Printf("省: %s, 市: %s, 区: %s, 街道: %s\n",
result2.Province, result2.City, result2.Region, result2.Street)
// 解析县级市地址
result3 := formatter.ParseCNAddress("河北省石家庄市新乐市经济开发区兴工街10号", false)
fmt.Printf("\n示例 3 - 县级市:\n")
fmt.Printf("省: %s, 市: %s, 区/县: %s, 街道: %s\n",
result3.Province, result3.City, result3.Region, result3.Street)
// 紧凑格式
result4 := formatter.ParseCNAddress("马云13593464918陕西省西安市雁塔区丈八沟街道", true)
fmt.Printf("\n示例 4 - 紧凑格式:\n")
fmt.Printf("姓名: %s, 电话: %s, 地址: %s%s%s%s\n",
result4.Name, result4.Mobile, result4.Province, result4.City, result4.Region, result4.Street)
// Output:
// 示例 1 - 带用户信息:
// {
// "name": "张三",
// "mobile": "13800138000",
// "idn": "",
// "postcode": "",
// "province": "北京",
// "city": "北京市",
// "region": "朝阳区",
// "street": "建国路1号",
// "addr": "北京市朝阳区建国路1号"
// }
//
// 示例 2 - 仅地址:
// 省: 北京, 市: 北京市, 区: 海淀区, 街道: 中关村大街1号
//
// 示例 3 - 县级市:
// 省: 河北省, 市: 石家庄市, 区/县: 新乐市, 街道: 经济开发区兴工街10号
//
// 示例 4 - 紧凑格式:
// 姓名: 马云, 电话: 13593464918, 地址: 陕西省西安市雁塔区丈八沟街道
}
```
### <span id="ParsePersonInfo">ParsePersonInfo</span>
<p>从地址字符串中提取用户信息(姓名、电话、身份证、邮编)。将个人信息与地址分离,支持带标签格式、紧凑格式、带分隔符格式。返回包含提取的用户信息和清理后地址字符串的 AddressInfo。</p>
<b>函数签名:</b>
```go
func ParsePersonInfo(str string) *AddressInfo
```
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/JO-uTlJlTy7)</span></b>
```go
package main
import (
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/formatter"
)
func main() {
// 提取姓名和手机号
result1 := formatter.ParsePersonInfo("张三 13800138000 北京市朝阳区")
fmt.Println("示例 1 - 姓名和手机号:")
fmt.Printf("姓名: %s, 手机: %s, 地址: %s\n", result1.Name, result1.Mobile, result1.Addr)
// 提取身份证号
result2 := formatter.ParsePersonInfo("李四 110101199001011234 上海市")
fmt.Println("\n示例 2 - 身份证号:")
fmt.Printf("姓名: %s, 身份证: %s, 地址: %s\n", result2.Name, result2.IDN, result2.Addr)
// 带标签格式
result3 := formatter.ParsePersonInfo("收货人:王五 电话13900139000 收货地址天津市河西区友谊路20号")
jsonData3, _ := json.MarshalIndent(result3, "", " ")
fmt.Println("\n示例 3 - 带标签格式:")
fmt.Println(string(jsonData3))
// Output:
// 示例 1 - 姓名和手机号:
// 姓名: 张三, 手机: 13800138000, 地址: 北京市朝阳区
//
// 示例 2 - 身份证号:
// 姓名: 李四, 身份证: 110101199001011234, 地址: 上海市
//
// 示例 3 - 带标签格式:
// {
// "name": "王五",
// "mobile": "13900139000",
// "idn": "",
// "postcode": "",
// "province": "",
// "city": "",
// "region": "",
// "street": "",
// "addr": "天津市河西区友谊路20号"
// }
}
```

View File

@@ -549,7 +549,7 @@ func main() {
func IsEmail(email string) bool
```
<b>示例:<span style="float:right;display:inline-block">[运行](https://go.dev/play/p/Os9VaFlT33G)</span></b>
<b>示例:<span style="float:right;display:inline-block">[运行](https://go.dev/play/p/HVQ5LAe-vFz)</span></b>
```go
import (
@@ -559,13 +559,28 @@ import (
func main() {
result1 := validator.IsEmail("abc@xyz.com")
result2 := validator.IsEmail("a.b@@com")
result2 := validator.IsEmail("user@domain.co")
result3 := validator.IsEmail("test.user@example.org")
result4 := validator.IsEmail("@abc@xyz.com")
result5 := validator.IsEmail("a.b@@com")
result6 := validator.IsEmail("a.b@com")
result7 := validator.IsEmail("test@example")
fmt.Println(result1)
fmt.Println(result2)
fmt.Println(result3)
fmt.Println(result4)
fmt.Println(result5)
fmt.Println(result6)
fmt.Println(result7)
// Output:
// true
// true
// true
// false
// false
// false
// false
}
```

View File

@@ -1,6 +1,6 @@
# Cryptor
Package cryptor contains some functions for data encryption and decryption. Support base64, md5, hmac, aes, des, rsa.
Package cryptor contains some functions for data encryption and decryption. Support base64, md5, hmac, aes, des, rsa, sm2, sm3, sm4.
<div STYLE="page-break-after: always;"></div>
@@ -76,6 +76,14 @@ import (
- [RsaDecryptOAEP](#RsaDecryptOAEP)
- [RsaSign](#RsaSign)
- [RsaVerifySign](#RsaVerifySign)
- [Sm3](#Sm3)
- [Sm4EcbEncrypt](#Sm4EcbEncrypt)
- [Sm4EcbDecrypt](#Sm4EcbDecrypt)
- [Sm4CbcEncrypt](#Sm4CbcEncrypt)
- [Sm4CbcDecrypt](#Sm4CbcDecrypt)
- [GenerateSm2Key](#GenerateSm2Key)
- [Sm2Encrypt](#Sm2Encrypt)
- [Sm2Decrypt](#Sm2Decrypt)
<div STYLE="page-break-after: always;"></div>
@@ -1831,3 +1839,278 @@ func main() {
}
}
```
### <span id="Sm3">Sm3</span>
<p>Calculate SM3 hash (Chinese National Cryptography SM3 Hash Algorithm). SM3 is a cryptographic hash algorithm published by the Chinese State Cryptography Administration, designed to replace MD5/SHA-1/SHA-2.</p>
<b>Signature:</b>
```go
func Sm3(data []byte) []byte
```
<b>Example:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/zDAQpteAiOc)</span></b>
```go
package main
import (
"encoding/hex"
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
data := []byte("hello world")
hash := cryptor.Sm3(data)
fmt.Println(hex.EncodeToString(hash))
// Output:
// 44f0061e69fa6fdfc290c494654a05dc0c053da7e5c52b84ef93a9d67d3fff88
}
```
### <span id="Sm4EcbEncrypt">Sm4EcbEncrypt</span>
<p>Encrypt data using SM4 ECB mode (Chinese National Cryptography SM4 Block Cipher Algorithm). Key length must be 16 bytes.</p>
<b>Signature:</b>
```go
func Sm4EcbEncrypt(data, key []byte) []byte
```
<b>Example:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/l5IQxYuuaED)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
key := []byte("1234567890abcdef") // 16 bytes key
plaintext := []byte("hello world")
encrypted := cryptor.Sm4EcbEncrypt(plaintext, key)
decrypted := cryptor.Sm4EcbDecrypt(encrypted, key)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```
### <span id="Sm4EcbDecrypt">Sm4EcbDecrypt</span>
<p>Decrypt data using SM4 ECB mode. Key length must be 16 bytes.</p>
<b>Signature:</b>
```go
func Sm4EcbDecrypt(encrypted, key []byte) []byte
```
<b>Example:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/l5IQxYuuaED)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
key := []byte("1234567890abcdef")
plaintext := []byte("hello world")
encrypted := cryptor.Sm4EcbEncrypt(plaintext, key)
decrypted := cryptor.Sm4EcbDecrypt(encrypted, key)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```
### <span id="Sm4CbcEncrypt">Sm4CbcEncrypt</span>
<p>Encrypt data using SM4 CBC mode. Key length must be 16 bytes. The returned ciphertext contains IV (first 16 bytes).</p>
<b>Signature:</b>
```go
func Sm4CbcEncrypt(data, key []byte) []byte
```
<b>Example:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/65Q6iYhLRTa)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
key := []byte("1234567890abcdef")
plaintext := []byte("hello world")
encrypted := cryptor.Sm4CbcEncrypt(plaintext, key)
decrypted := cryptor.Sm4CbcDecrypt(encrypted, key)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```
### <span id="Sm4CbcDecrypt">Sm4CbcDecrypt</span>
<p>Decrypt data using SM4 CBC mode. Key length must be 16 bytes. The ciphertext should contain IV (first 16 bytes).</p>
<b>Signature:</b>
```go
func Sm4CbcDecrypt(encrypted, key []byte) []byte
```
<b>Example:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/65Q6iYhLRTa)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
key := []byte("1234567890abcdef")
plaintext := []byte("hello world")
encrypted := cryptor.Sm4CbcEncrypt(plaintext, key)
decrypted := cryptor.Sm4CbcDecrypt(encrypted, key)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```
### <span id="GenerateSm2Key">GenerateSm2Key</span>
<p>Generate SM2 key pair (Chinese National Cryptography SM2 Elliptic Curve Public Key Algorithm). SM2 is an asymmetric encryption algorithm based on elliptic curve cryptography.</p>
<b>Signature:</b>
```go
func GenerateSm2Key() (*Sm2PrivateKey, error)
```
<b>Example:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/bKYMqRLvIx3)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
privateKey, err := cryptor.GenerateSm2Key()
if err != nil {
return
}
plaintext := []byte("hello world")
ciphertext, _ := cryptor.Sm2Encrypt(&privateKey.PublicKey, plaintext)
decrypted, _ := cryptor.Sm2Decrypt(privateKey, ciphertext)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```
### <span id="Sm2Encrypt">Sm2Encrypt</span>
<p>Encrypt data using SM2 public key. The returned ciphertext format is: C1(65 bytes) || C3(32 bytes) || C2(plaintext length).</p>
<b>Signature:</b>
```go
func Sm2Encrypt(pub *Sm2PublicKey, plaintext []byte) ([]byte, error)
```
<b>Example:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/bKYMqRLvIx3)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
privateKey, _ := cryptor.GenerateSm2Key()
plaintext := []byte("hello world")
ciphertext, _ := cryptor.Sm2Encrypt(&privateKey.PublicKey, plaintext)
decrypted, _ := cryptor.Sm2Decrypt(privateKey, ciphertext)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```
### <span id="Sm2Decrypt">Sm2Decrypt</span>
<p>Decrypt data using SM2 private key. The ciphertext format should be: C1(65 bytes) || C3(32 bytes) || C2(plaintext length).</p>
<b>Signature:</b>
```go
func Sm2Decrypt(priv *Sm2PrivateKey, ciphertext []byte) ([]byte, error)
```
<b>Example:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/bKYMqRLvIx3)</span></b>
```go
package main
import (
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
)
func main() {
privateKey, _ := cryptor.GenerateSm2Key()
plaintext := []byte("hello world")
ciphertext, _ := cryptor.Sm2Encrypt(&privateKey.PublicKey, plaintext)
decrypted, _ := cryptor.Sm2Decrypt(privateKey, ciphertext)
fmt.Println(string(decrypted))
// Output:
// hello world
}
```

View File

@@ -8,6 +8,7 @@ formatter contains some functions for data formatting.
- [https://github.com/duke-git/lancet/blob/main/formatter/formatter.go](https://github.com/duke-git/lancet/blob/main/formatter/formatter.go)
- [https://github.com/duke-git/lancet/blob/main/formatter/byte.go](https://github.com/duke-git/lancet/blob/main/formatter/byte.go)
- [https://github.com/duke-git/lancet/blob/main/formatter/address.go](https://github.com/duke-git/lancet/blob/main/formatter/address.go)
<div STYLE="page-break-after: always;"></div>
@@ -30,6 +31,8 @@ import (
- [BinaryBytes](#BinaryBytes)
- [ParseDecimalBytes](#ParseDecimalBytes)
- [ParseBinaryBytes](#ParseBinaryBytes)
- [ParseCNAddress](#ParseCNAddress)
- [ParsePersonInfo](#ParsePersonInfo)
<div STYLE="page-break-after: always;"></div>
@@ -308,3 +311,134 @@ func main() {
// 12492
}
```
### <span id="ParseCNAddress">ParseCNAddress</span>
<p>Parses a Chinese address string intelligently and extracts structured information. It can parse addresses with or without user information (name, phone, ID card, etc.). When withUser is true, it extracts user information from the address string. When withUser is false, it only parses the location information. Supports various address formats: standard format, compact format, labeled format, county-level cities format, etc.</p>
<b>Signature:</b>
```go
func ParseCNAddress(str string, withUser bool) *AddressInfo
```
<b>Example:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/o5l09hQopEV)</span></b>
```go
package main
import (
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/formatter"
)
func main() {
// Parse complete address with user information
result1 := formatter.ParseCNAddress("张三 13800138000 北京市朝阳区建国路1号", true)
jsonData1, _ := json.MarshalIndent(result1, "", " ")
fmt.Println("Example 1 - With user info:")
fmt.Println(string(jsonData1))
// Parse address only, without extracting user information
result2 := formatter.ParseCNAddress("北京市海淀区中关村大街1号", false)
fmt.Printf("\nExample 2 - Address only:\n")
fmt.Printf("Province: %s, City: %s, Region: %s, Street: %s\n",
result2.Province, result2.City, result2.Region, result2.Street)
// Parse county-level city address
result3 := formatter.ParseCNAddress("河北省石家庄市新乐市经济开发区兴工街10号", false)
fmt.Printf("\nExample 3 - County-level city:\n")
fmt.Printf("Province: %s, City: %s, Region: %s, Street: %s\n",
result3.Province, result3.City, result3.Region, result3.Street)
// Compact format
result4 := formatter.ParseCNAddress("马云13593464918陕西省西安市雁塔区丈八沟街道", true)
fmt.Printf("\nExample 4 - Compact format:\n")
fmt.Printf("Name: %s, Phone: %s, Address: %s%s%s%s\n",
result4.Name, result4.Mobile, result4.Province, result4.City, result4.Region, result4.Street)
// Output:
// Example 1 - With user info:
// {
// "name": "张三",
// "mobile": "13800138000",
// "idn": "",
// "postcode": "",
// "province": "北京",
// "city": "北京市",
// "region": "朝阳区",
// "street": "建国路1号",
// "addr": "北京市朝阳区建国路1号"
// }
//
// Example 2 - Address only:
// Province: 北京, City: 北京市, Region: 海淀区, Street: 中关村大街1号
//
// Example 3 - County-level city:
// Province: 河北省, City: 石家庄市, Region: 新乐市, Street: 经济开发区兴工街10号
//
// Example 4 - Compact format:
// Name: 马云, Phone: 13593464918, Address: 陕西省西安市雁塔区丈八沟街道
}
```
### <span id="ParsePersonInfo">ParsePersonInfo</span>
<p>Extracts user information (name, phone, ID card, postal code) from an address string. It separates personal information from the address, supporting labeled format, compact format, and formats with separators. Returns an AddressInfo with extracted user information and cleaned address string.</p>
<b>Signature:</b>
```go
func ParsePersonInfo(str string) *AddressInfo
```
<b>Example:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/JO-uTlJlTy7)</span></b>
```go
package main
import (
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/formatter"
)
func main() {
// Extract name and phone
result1 := formatter.ParsePersonInfo("张三 13800138000 北京市朝阳区")
fmt.Println("Example 1 - Name and phone:")
fmt.Printf("Name: %s, Phone: %s, Address: %s\n", result1.Name, result1.Mobile, result1.Addr)
// Extract ID card number
result2 := formatter.ParsePersonInfo("李四 110101199001011234 上海市")
fmt.Println("\nExample 2 - ID card number:")
fmt.Printf("Name: %s, ID Card: %s, Address: %s\n", result2.Name, result2.IDN, result2.Addr)
// Labeled format
result3 := formatter.ParsePersonInfo("收货人:王五 电话13900139000 收货地址天津市河西区友谊路20号")
jsonData3, _ := json.MarshalIndent(result3, "", " ")
fmt.Println("\nExample 3 - Labeled format:")
fmt.Println(string(jsonData3))
// Output:
// Example 1 - Name and phone:
// Name: 张三, Phone: 13800138000, Address: 北京市朝阳区
//
// Example 2 - ID card number:
// Name: 李四, ID Card: 110101199001011234, Address: 上海市
//
// Example 3 - Labeled format:
// {
// "name": "王五",
// "mobile": "13900139000",
// "idn": "",
// "postcode": "",
// "province": "",
// "city": "",
// "region": "",
// "street": "",
// "addr": "天津市河西区友谊路20号"
// }
}
```

View File

@@ -551,7 +551,7 @@ func main() {
func IsEmail(email string) bool
```
<b>Example:<span style="float:right;display:inline-block">[Run](https://go.dev/play/p/Os9VaFlT33G)</span></b>
<b>Example:<span style="float:right;display:inline-block">[Run](https://go.dev/play/p/HVQ5LAe-vFz)</span></b>
```go
import (
@@ -561,13 +561,28 @@ import (
func main() {
result1 := validator.IsEmail("abc@xyz.com")
result2 := validator.IsEmail("a.b@@com")
result2 := validator.IsEmail("user@domain.co")
result3 := validator.IsEmail("test.user@example.org")
result4 := validator.IsEmail("@abc@xyz.com")
result5 := validator.IsEmail("a.b@@com")
result6 := validator.IsEmail("a.b@com")
result7 := validator.IsEmail("test@example")
fmt.Println(result1)
fmt.Println(result2)
fmt.Println(result3)
fmt.Println(result4)
fmt.Println(result5)
fmt.Println(result6)
fmt.Println(result7)
// Output:
// true
// true
// true
// false
// false
// false
// false
}
```

View File

@@ -24,9 +24,10 @@ import (
"strings"
"sync"
"github.com/duke-git/lancet/v2/validator"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"github.com/duke-git/lancet/v2/validator"
)
// FileReader is a reader supporting offset seeking and reading one
@@ -283,21 +284,18 @@ func ReadFileByLine(path string) ([]string, error) {
}
defer f.Close()
scanner := bufio.NewScanner(f)
result := make([]string, 0)
buf := bufio.NewReader(f)
for {
line, _, err := buf.ReadLine()
l := string(line)
if err == io.EOF {
break
}
if err != nil {
continue
}
for scanner.Scan() {
l := scanner.Text()
result = append(result, l)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return result, nil
}

494
formatter/address.go Normal file
View File

@@ -0,0 +1,494 @@
// Package formatter implements some functions to format string, struct.
package formatter
import (
"regexp"
"strings"
"unicode/utf8"
)
// AddressInfo represents the parsed address information including user details and location.
// AddressInfo 表示解析后的地址信息,包括用户详细信息和位置信息
type AddressInfo struct {
Name string `json:"name"` // Name of the recipient / 姓名
Mobile string `json:"mobile"` // Mobile phone number or landline / 手机号或座机
IDN string `json:"idn"` // ID card number / 身份证号
Postcode string `json:"postcode"` // Postal code / 邮编
Province string `json:"province"` // Province / 省
City string `json:"city"` // City / 市
Region string `json:"region"` // District or county / 区/县
Street string `json:"street"` // Street address / 街道详细地址
Addr string `json:"addr"` // Original address string / 原始地址字符串
}
// fuzzyResult represents the result of fuzzy address parsing.
// fuzzyResult 表示模糊地址解析的结果
type fuzzyResult struct {
A1 string // Province level / 省级
A2 string // City level / 市级
A3 string // District/County level / 区/县级
Street string // Street address / 街道地址
}
// ParseCNAddress parses a Chinese address string intelligently and extracts structured information.
// It can parse addresses with or without user information (name, phone, ID card, etc.).
// When withUser is true, it extracts user information from the address string.
// When withUser is false, it only parses the location information.
// The function handles various address formats including:
// - Standard format: "Province City District Street"
// - Compact format: "Name Phone Province City District Street"
// - With keywords: "Name: xxx Phone: xxx Address: xxx"
// - County-level cities: "Province City CountyCity District" (e.g., "河北省石家庄市新乐市")
// ParseCNAddress 智能解析中国地址字符串并提取结构化信息。
// 可以解析带或不带用户信息(姓名、电话、身份证等)的地址。
// 当 withUser 为 true 时,从地址字符串中提取用户信息。
// 当 withUser 为 false 时,仅解析位置信息。
// 该函数处理多种地址格式,包括:
// - 标准格式:"省 市 区 街道"
// - 紧凑格式:"姓名 电话 省 市 区 街道"
// - 带关键词:"姓名:xxx 电话:xxx 地址:xxx"
// - 县级市:"省 市 县级市 区"(如"河北省石家庄市新乐市"
func ParseCNAddress(str string, withUser bool) *AddressInfo {
result := &AddressInfo{}
if withUser {
ParsePersonInfo := ParsePersonInfo(str)
result = ParsePersonInfo
} else {
result.Addr = str
}
fuzz := fuzz(result.Addr)
parse := parse(fuzz.A1, fuzz.A2, fuzz.A3)
result.Province = parse.Province
result.City = parse.City
result.Region = parse.Region
// 提取街道地址:从原始地址中找到区/县的位置,提取后面的内容
if result.Region != "" && result.Addr != "" {
// 在原始地址中查找区/县的位置转换为rune数组以正确处理中文
addrRunes := []rune(result.Addr)
regionRunes := []rune(result.Region)
regionPos := mbStrpos(result.Addr, result.Region)
if regionPos != -1 {
// 提取区/县后面的内容作为街道地址
streetStart := regionPos + len(regionRunes)
if streetStart < len(addrRunes) {
result.Street = string(addrRunes[streetStart:])
}
} else if fuzz.Street != "" {
// 如果没找到区/县使用fuzz返回的街道
result.Street = fuzz.Street
}
} else if fuzz.Street != "" {
result.Street = fuzz.Street
}
// 清理街道地址中的重复省市区信息(可能存在部分匹配的残留)
result.Street = strings.ReplaceAll(result.Street, result.Region, "")
result.Street = strings.ReplaceAll(result.Street, result.City, "")
result.Street = strings.ReplaceAll(result.Street, result.Province, "")
// 清理街道地址中的残留片段(如"自治区直辖县级市"被替换后的残留)
result.Street = strings.ReplaceAll(result.Street, "自治区直辖县级市", "")
result.Street = strings.ReplaceAll(result.Street, "直辖县级市", "")
result.Street = strings.TrimSpace(result.Street)
return result
}
// ParsePersonInfo extracts user information (name, phone, ID card, postal code) from an address string.
// It separates personal information from the address, supporting various formats:
// - Labeled format: "Name: xxx Phone: xxx Address: xxx"
// - Compact format: "Name Phone Address" (e.g., "张三13800138000北京市朝阳区")
// - With separators: using colons, commas, newlines as delimiters
// Returns an AddressInfo with extracted user information and cleaned address string in Addr field.
// ParsePersonInfo 从地址字符串中提取用户信息(姓名、电话、身份证、邮编)。
// 将个人信息与地址分离,支持多种格式:
// - 带标签格式:"姓名:xxx 电话:xxx 地址:xxx"
// - 紧凑格式:"姓名 电话 地址"(如"张三13800138000北京市朝阳区"
// - 带分隔符:使用冒号、逗号、换行符作为分隔符
// 返回包含提取的用户信息和清理后地址字符串(在 Addr 字段中)的 AddressInfo。
func ParsePersonInfo(str string) *AddressInfo {
compose := &AddressInfo{}
// 先尝试提取带标签的信息
// 提取姓名 (支持: 姓名:xxx, 收货人:xxx, 收件人:xxx)
nameRe := regexp.MustCompile(`(?:姓名|收货人|收件人)[:]\s*([^\s\d\n]+)`)
if match := nameRe.FindStringSubmatch(str); len(match) > 1 {
compose.Name = strings.TrimSpace(match[1])
str = nameRe.ReplaceAllString(str, " ")
}
// 提取手机号或座机号 (支持: 电话:xxx, 手机:xxx, 联系电话:xxx)
phoneRe := regexp.MustCompile(`(?:电话|手机号码|手机|联系电话)[:]\s*([\d\-]+)`)
if match := phoneRe.FindStringSubmatch(str); len(match) > 1 {
compose.Mobile = strings.TrimSpace(match[1])
str = phoneRe.ReplaceAllString(str, " ")
}
// 提取所在地区 (支持: 所在地区:xxx)
regionRe := regexp.MustCompile(`所在地区[:]\s*([^\n]+)`)
if match := regionRe.FindStringSubmatch(str); len(match) > 1 {
// 将所在地区保留在字符串中,不删除
// str 保持不变,让后续的地址解析处理
}
// 提取详细地址 (支持: 详细地址:xxx, 收货地址:xxx, 地址:xxx)
addrRe := regexp.MustCompile(`(?:详细地址|收货地址|地址)[:]\s*([^\n]+)`)
if match := addrRe.FindStringSubmatch(str); len(match) > 1 {
// 保留详细地址在字符串中
str = addrRe.ReplaceAllString(str, " "+match[1])
}
// 如果还没有提取到姓名和手机号,尝试识别紧凑格式 (如: 马云13593464918陕西省...)
if compose.Name == "" && compose.Mobile == "" {
// 匹配: 2-4个汉字 + 7-12位数字 + 剩余内容
compactRe := regexp.MustCompile(`^([\x{4e00}-\x{9fa5}]{2,4})(\d{7,12})(.*)$`)
if match := compactRe.FindStringSubmatch(str); len(match) > 3 {
compose.Name = match[1]
compose.Mobile = match[2]
str = match[3] // 保留剩余的地址部分
}
}
// 替换常见的地址关键词为空格
replacements := map[string]string{
"收货地址": " ", "详细地址": " ", "地址": " ", "收货人": " ",
"收件人": " ", "收货": " ", "所在地区": " ", "邮编": " ",
"电话": " ", "手机号码": " ", "身份证号码": " ", "身份证号": " ",
"身份证": " ", "姓名": " ", "联系电话": " ", "手机": " ",
"": " ", ":": " ", "": " ", ";": " ",
"": " ", ",": " ", "。": " ", "\n": " ", "\r": " ",
}
for old, new := range replacements {
str = strings.ReplaceAll(str, old, new)
}
// 将多个空格合并为一个
spaceRe := regexp.MustCompile(`\s{1,}`)
str = spaceRe.ReplaceAllString(str, " ")
// 处理座机号格式 (如: 800-8585222)
telRe := regexp.MustCompile(`(\d{3,4})-(\d{6,8})`)
str = telRe.ReplaceAllString(str, "$1$2")
// 提取身份证号 (18位或17位+X)
idnRe := regexp.MustCompile(`\d{18}|\d{17}[Xx]`)
if match := idnRe.FindString(str); match != "" {
compose.IDN = strings.ToUpper(match)
str = strings.ReplaceAll(str, match, "")
}
// 如果之前没有提取到手机号,现在提取
if compose.Mobile == "" {
mobileRe := regexp.MustCompile(`\d{7,12}`)
if match := mobileRe.FindString(str); match != "" {
compose.Mobile = match
str = strings.ReplaceAll(str, match, "")
}
} else {
// 已经提取过手机号,从字符串中删除
str = strings.ReplaceAll(str, compose.Mobile, "")
}
// 提取邮编
postcodeRe := regexp.MustCompile(`\d{6}`)
if match := postcodeRe.FindString(str); match != "" {
compose.Postcode = match
str = strings.ReplaceAll(str, match, "")
}
// 清理多余空格
str = strings.TrimSpace(spaceRe.ReplaceAllString(str, " "))
// 如果之前没有提取到姓名,现在提取
if compose.Name == "" {
// 提取姓名(取最短的词作为姓名,排除空字符串)
splitArr := strings.Split(str, " ")
if len(splitArr) > 0 {
for _, value := range splitArr {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if compose.Name == "" {
compose.Name = value
} else if utf8.RuneCountInString(value) < utf8.RuneCountInString(compose.Name) && utf8.RuneCountInString(value) >= 2 {
compose.Name = value
}
}
if compose.Name != "" {
str = strings.TrimSpace(strings.ReplaceAll(str, compose.Name, ""))
}
}
} else {
// 已经提取过姓名,从字符串中删除
str = strings.TrimSpace(strings.ReplaceAll(str, compose.Name, ""))
}
compose.Addr = str
return compose
}
// fuzz 根据统计规律分析出二三级地址
func fuzz(addr string) *fuzzyResult {
addrOrigin := addr
addr = strings.ReplaceAll(addr, " ", "")
addr = strings.ReplaceAll(addr, ",", "")
// 先替换"自治区直辖县级市"为"市",避免后续"自治区"替换时产生问题
addr = strings.ReplaceAll(addr, "自治区直辖县级市", "市")
addr = strings.ReplaceAll(addr, "自治区", "省")
addr = strings.ReplaceAll(addr, "自治州", "州")
addr = strings.ReplaceAll(addr, "小区", "")
addr = strings.ReplaceAll(addr, "校区", "")
// 过滤"市辖区" - 这是一个行政术语占位符,不是真正的区名
addr = strings.ReplaceAll(addr, "市辖区", "")
a1 := ""
a2 := ""
a3 := ""
street := ""
deep3KeywordPos := -1
// 判断是否包含县/区/旗
countyPos := mbStrpos(addr, "县")
districtPos := mbStrpos(addr, "区")
bannerPos := mbStrpos(addr, "旗")
// 只要存在这些关键词就处理,不再限制位置
hasEarlyCounty := countyPos != -1
hasEarlyDistrict := districtPos != -1
hasEarlyBanner := bannerPos != -1
if hasEarlyCounty || hasEarlyDistrict || hasEarlyBanner {
// 优先检查是否存在县级市(如"新乐市"
// 如果同时存在"XX市"和"XX区"/"XX县",优先处理"市"
hasCountyLevelCity := false
if mbStrstr(addr, "市") {
// 查找所有"市"的位置
cityCount := mbSubstrCount(addr, "市")
if cityCount >= 2 {
// 找到第二个"市"的位置(可能是县级市)
firstCityPos := mbStrpos(addr, "市")
// 从第一个"市"之后继续查找
addrAfterFirstCity := mbSubstr(addr, firstCityPos+1, utf8.RuneCountInString(addr)-firstCityPos-1)
secondCityPos := mbStrpos(addrAfterFirstCity, "市")
if secondCityPos != -1 {
secondCityAbsPos := firstCityPos + 1 + secondCityPos
// 检查第二个"市"后面是否存在"区"或"县"
addrAfterSecondCity := mbSubstr(addr, secondCityAbsPos+1, utf8.RuneCountInString(addr)-secondCityAbsPos-1)
if mbStrstr(addrAfterSecondCity, "区") || mbStrstr(addrAfterSecondCity, "县") {
// 提取两个"市"之间的内容
betweenCities := mbSubstr(addr, firstCityPos+1, secondCityAbsPos-firstCityPos)
// 检查是否是重复的地名(如"北京市北京市"或"杭州市西湖区杭州市"
// 如果两个"市"之间包含"区"或"县",说明不是县级市,而是重复地名
if !mbStrstr(betweenCities, "区") && !mbStrstr(betweenCities, "县") {
// 第一个"市"及之前的内容
firstCityFull := mbSubstr(addr, 0, firstCityPos+1)
if betweenCities != firstCityFull {
// 不是重复地名,这是县级市
a3 = betweenCities
deep3KeywordPos = secondCityAbsPos
hasCountyLevelCity = true
}
}
}
}
}
}
if !hasCountyLevelCity {
// 处理旗
if mbStrstr(addr, "旗") {
deep3KeywordPos = mbStrpos(addr, "旗")
a3 = mbSubstr(addr, deep3KeywordPos-1, 2)
}
// 处理区
if mbStrstr(addr, "区") {
// 使用第一个"区"(避免重复地名干扰,如"西湖区杭州市西湖区"
deep3KeywordPos = mbStrpos(addr, "区")
if mbStrstr(addr, "市") {
// 策略:找到"区"之前的最后一个"市"
// 这样可以避免详细地址中的"市"字干扰(如"农贸市场")
zonePos := deep3KeywordPos
// 从开头到"区"的子串中,查找最后一个"市"
addrBeforeZone := mbSubstr(addr, 0, zonePos)
cityPos := mbStrripos(addrBeforeZone, "市")
if cityPos != -1 {
a3 = mbSubstr(addr, cityPos+1, zonePos-cityPos)
} else {
// 没有找到"市",使用默认逻辑
a3 = mbSubstr(addr, deep3KeywordPos-2, 3)
}
} else {
a3 = mbSubstr(addr, deep3KeywordPos-2, 3)
}
}
// 处理县
if mbStrstr(addr, "县") {
// 使用第一个"县"(避免重复地名干扰)
deep3KeywordPos = mbStrpos(addr, "县")
if mbStrstr(addr, "市") {
// 从开头到"县"的子串中,查找最后一个"市"
addrBeforeCounty := mbSubstr(addr, 0, deep3KeywordPos)
cityPos := mbStrripos(addrBeforeCounty, "市")
if cityPos != -1 {
a3 = mbSubstr(addr, cityPos+1, deep3KeywordPos-cityPos)
} else {
if mbStrstr(addr, "自治县") {
a3 = mbSubstr(addr, deep3KeywordPos-6, 7)
firstChar := mbSubstr(a3, 0, 1)
if firstChar == "省" || firstChar == "市" || firstChar == "州" {
a3 = mbSubstr(a3, 1, utf8.RuneCountInString(a3)-1)
}
} else {
a3 = mbSubstr(addr, deep3KeywordPos-2, 3)
}
}
} else {
if mbStrstr(addr, "自治县") {
a3 = mbSubstr(addr, deep3KeywordPos-6, 7)
firstChar := mbSubstr(a3, 0, 1)
if firstChar == "省" || firstChar == "市" || firstChar == "州" {
a3 = mbSubstr(a3, 1, utf8.RuneCountInString(a3)-1)
}
} else {
a3 = mbSubstr(addr, deep3KeywordPos-2, 3)
}
}
}
}
if deep3KeywordPos != -1 {
street = mbSubstr(addrOrigin, deep3KeywordPos+1, utf8.RuneCountInString(addrOrigin)-deep3KeywordPos-1)
}
} else {
// 处理市
if mbStrripos(addr, "市") != -1 {
cityCount := mbSubstrCount(addr, "市")
if cityCount == 1 {
deep3KeywordPos = mbStrripos(addr, "市")
a3 = mbSubstr(addr, deep3KeywordPos-2, 3)
street = mbSubstr(addrOrigin, deep3KeywordPos+1, utf8.RuneCountInString(addrOrigin)-deep3KeywordPos-1)
} else if cityCount >= 2 {
deep3KeywordPos = mbStrripos(addr, "市")
a3 = mbSubstr(addr, deep3KeywordPos-2, 3)
street = mbSubstr(addrOrigin, deep3KeywordPos+1, utf8.RuneCountInString(addrOrigin)-deep3KeywordPos-1)
}
} else {
a3 = ""
street = addr
}
}
// 提取市级地址
if mbStrpos(addr, "市") != -1 || mbStrstr(addr, "盟") || mbStrstr(addr, "州") {
tmpPos := -1
if tmpPos = mbStrpos(addr, "市"); tmpPos != -1 {
// 使用第一个"市"(避免重复地名干扰,如"杭州市西湖区杭州市"
// 向前查找省的位置,如果有省就从省后开始,否则从开头开始
addrBeforeCity := mbSubstr(addr, 0, tmpPos)
provincePos := mbStrripos(addrBeforeCity, "省")
startPos := 0
if provincePos != -1 {
startPos = provincePos + 1
}
a2 = mbSubstr(addr, startPos, tmpPos-startPos+1)
} else if tmpPos = mbStrpos(addr, "盟"); tmpPos != -1 {
a2 = mbSubstr(addr, tmpPos-2, 3)
} else if mbStrpos(addr, "州") != -1 {
if tmpPos = mbStrpos(addr, "自治州"); tmpPos != -1 {
a2 = mbSubstr(addr, tmpPos-4, 5)
} else {
tmpPos = mbStrpos(addr, "州")
a2 = mbSubstr(addr, tmpPos-2, 3)
}
}
}
return &fuzzyResult{
A1: a1,
A2: a2,
A3: a3,
Street: street,
}
}
// parse 智能解析出省市区
func parse(a1, a2, a3 string) *AddressInfo {
r := &AddressInfo{}
if a3 == "" {
return r
}
// 在三级地址数据中查找匹配
area3Matches := make(map[int]*Region)
for id, v := range A3Data {
if mbStrpos(v.Name, a3) != -1 {
area3Matches[id] = v
}
}
// 多个匹配项,需要通过二级地址筛选
if len(area3Matches) > 1 {
if a2 != "" {
area2Matches := make(map[int]*Region)
for id, v := range A2Data {
if mbStrpos(v.Name, a2) != -1 {
area2Matches[id] = v
}
}
if len(area2Matches) > 0 {
for _, v := range area3Matches {
if city, ok := area2Matches[v.PID]; ok {
r.City = city.Name
r.Region = v.Name
if province, ok := A1Data[city.PID]; ok {
r.Province = province.Name
}
}
}
}
} else {
r.Province = ""
r.City = ""
r.Region = a3
}
} else if len(area3Matches) == 1 {
// 唯一匹配
for _, v := range area3Matches {
r.Region = v.Name
if city, ok := A2Data[v.PID]; ok {
r.City = city.Name
if province, ok := A1Data[city.PID]; ok {
r.Province = province.Name
}
}
}
} else if len(area3Matches) == 0 && a2 == a3 {
// 没有匹配到三级地址,但二级地址等于三级地址,可能是直辖市
shengID := 0
for _, v := range A2Data {
if mbStrpos(v.Name, a2) != -1 {
r.City = v.Name
shengID = v.PID
break
}
}
if province, ok := A1Data[shengID]; ok {
r.Province = province.Name
}
r.Region = ""
}
return r
}

4470
formatter/address_data.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
package formatter
import (
"strings"
"unicode/utf8"
)
// mbStrpos 返回字符串首次出现的位置UTF-8字符计数
func mbStrpos(haystack, needle string) int {
if needle == "" {
return 0
}
idx := strings.Index(haystack, needle)
if idx == -1 {
return -1
}
return utf8.RuneCountInString(haystack[:idx])
}
// mbStrripos 返回字符串最后出现的位置UTF-8字符计数
func mbStrripos(haystack, needle string) int {
if needle == "" {
return utf8.RuneCountInString(haystack)
}
idx := strings.LastIndex(haystack, needle)
if idx == -1 {
return -1
}
return utf8.RuneCountInString(haystack[:idx])
}
// mbStrstr 检查字符串是否包含子串
func mbStrstr(haystack, needle string) bool {
return strings.Contains(haystack, needle)
}
// mbSubstr 截取字符串UTF-8字符计数
// start: 起始位置从0开始
// length: 截取长度(字符数)
func mbSubstr(str string, start, length int) string {
runes := []rune(str)
strLen := len(runes)
// 处理负数起始位置
if start < 0 {
start = strLen + start
if start < 0 {
start = 0
}
}
// 起始位置超出字符串长度
if start >= strLen {
return ""
}
// 计算结束位置
end := start + length
if end > strLen {
end = strLen
}
if end < start {
return ""
}
return string(runes[start:end])
}
// mbSubstrCount 统计子串出现次数
func mbSubstrCount(haystack, needle string) int {
if needle == "" {
return 0
}
return strings.Count(haystack, needle)
}

360
formatter/address_test.go Normal file
View File

@@ -0,0 +1,360 @@
package formatter
import (
"encoding/json"
"testing"
)
func TestParseCNAddress(t *testing.T) {
tests := []struct {
name string
input string
withUser bool
want *AddressInfo
}{
{
name: "完整地址信息",
input: "张三 13800138000 北京市朝阳区建国路1号",
withUser: true,
want: &AddressInfo{
Name: "张三",
Mobile: "13800138000",
Province: "北京",
City: "北京市",
Region: "朝阳区",
Street: "建国路1号",
},
},
{
name: "带身份证和邮编",
input: "李四 18612345678 110101199001011234 100000 上海市浦东新区世纪大道100号",
withUser: true,
want: &AddressInfo{
Name: "李四",
Mobile: "18612345678",
IDN: "110101199001011234",
Postcode: "100000",
},
},
{
name: "仅地址不含用户信息",
input: "北京市海淀区中关村大街1号",
withUser: false,
want: &AddressInfo{
Province: "北京",
City: "北京市",
Region: "海淀区",
Street: "中关村大街1号",
},
},
{
name: "带收货关键词",
input: "收货人:王五 电话13900139000 收货地址天津市河西区友谊路20号",
withUser: true,
want: &AddressInfo{
Name: "王五",
Mobile: "13900139000",
Province: "天津",
City: "天津市",
Region: "河西区",
},
},
{
name: "紧凑格式地址",
input: "马云13593464918陕西省西安市雁塔区丈八沟街道高新四路南江国际",
withUser: true,
want: &AddressInfo{
Name: "马云",
Mobile: "13593464918",
Province: "陕西省",
City: "西安市",
Region: "雁塔区",
Street: "丈八沟街道高新四路南江国际",
},
},
{
name: "带座机号格式",
input: "姓名:马云\n联系电话800-8585222\n所在地区河北省石家庄市新华区\n详细地址:中华北大街68号鹿城商务中心6号楼1413室",
withUser: true,
want: &AddressInfo{
Name: "马云",
Mobile: "800-8585222",
Province: "河北省",
City: "石家庄市",
Region: "新华区",
Street: "中华北大街68号鹿城商务中心6号楼1413室",
},
},
{
name: "北京市重复格式",
input: "北京市北京市市辖区东城区",
withUser: false,
want: &AddressInfo{
Province: "北京",
City: "北京市",
Region: "东城区",
Street: "",
},
},
{
name: "河北省新乐市地址",
input: "河北省石家庄市新乐市经济开发区兴工街10号来优品仓库",
withUser: false,
want: &AddressInfo{
Province: "河北省",
City: "石家庄市",
Region: "新乐市",
Street: "经济开发区兴工街10号来优品仓库",
},
},
{
name: "江苏仪征市地址",
input: "江苏省扬州市仪征市真州镇解放东路99号",
withUser: false,
want: &AddressInfo{
Province: "江苏省",
City: "扬州市",
Region: "仪征市",
Street: "真州镇解放东路99号",
},
},
{
name: "新疆石河子市地址",
input: "新疆石河子市北三路25小区",
withUser: false,
want: &AddressInfo{
Province: "新疆维吾尔自治区",
City: "自治区直辖县级市",
Region: "石河子市",
},
},
{
name: "新疆石河子市-简化格式省+县级市",
input: "新疆维吾尔自治区石河子市",
withUser: false,
want: &AddressInfo{
Province: "新疆维吾尔自治区",
City: "自治区直辖县级市",
Region: "石河子市",
Street: "",
},
},
{
name: "新疆石河子市-完整行政区划表述",
input: "新疆维吾尔自治区自治区直辖县级市石河子市",
withUser: false,
want: &AddressInfo{
Province: "新疆维吾尔自治区",
City: "自治区直辖县级市",
Region: "石河子市",
Street: "",
},
},
{
name: "浙江杭州西湖区重复地址",
input: "浙江省杭州市西湖区杭州市西湖区人民政府109号",
withUser: false,
want: &AddressInfo{
Province: "浙江省",
City: "杭州市",
Region: "西湖区",
Street: "人民政府109号",
},
},
{
name: "湖南长沙市重复地址",
input: "湖南省长沙市岳麓区银盆岭街道长沙市人民政府长沙市政府大楼",
withUser: false,
want: &AddressInfo{
Province: "湖南省",
City: "长沙市",
Region: "岳麓区",
Street: "银盆岭街道人民政府政府大楼",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ParseCNAddress(tt.input, tt.withUser)
// 打印结果便于调试
jsonData, _ := json.MarshalIndent(got, "", " ")
t.Logf("Result: %s", jsonData)
// 验证主要字段
if tt.want.Name != "" && got.Name != tt.want.Name {
t.Errorf("Name = %v, want %v", got.Name, tt.want.Name)
}
if tt.want.Mobile != "" && got.Mobile != tt.want.Mobile {
t.Errorf("Mobile = %v, want %v", got.Mobile, tt.want.Mobile)
}
if tt.want.Province != "" && got.Province != tt.want.Province {
t.Errorf("Province = %v, want %v", got.Province, tt.want.Province)
}
if tt.want.City != "" && got.City != tt.want.City {
t.Errorf("City = %v, want %v", got.City, tt.want.City)
}
if tt.want.Region != "" && got.Region != tt.want.Region {
t.Errorf("Region = %v, want %v", got.Region, tt.want.Region)
}
})
}
}
func TestParsePersonInfo(t *testing.T) {
tests := []struct {
name string
input string
verify func(*testing.T, *AddressInfo)
}{
{
name: "提取姓名和手机号",
input: "张三 13800138000 北京市朝阳区",
verify: func(t *testing.T, got *AddressInfo) {
if got.Name != "张三" {
t.Errorf("Name = %v, want 张三", got.Name)
}
if got.Mobile != "13800138000" {
t.Errorf("Mobile = %v, want 13800138000", got.Mobile)
}
},
},
{
name: "提取身份证号",
input: "李四 110101199001011234 上海市",
verify: func(t *testing.T, got *AddressInfo) {
if got.Name != "李四" {
t.Errorf("Name = %v, want 李四", got.Name)
}
if got.IDN != "110101199001011234" {
t.Errorf("IDN = %v, want 110101199001011234", got.IDN)
}
},
},
{
name: "提取邮编",
input: "王五 100000 天津市",
verify: func(t *testing.T, got *AddressInfo) {
if got.Name != "王五" {
t.Errorf("Name = %v, want 王五", got.Name)
}
if got.Postcode != "100000" {
t.Errorf("Postcode = %v, want 100000", got.Postcode)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ParsePersonInfo(tt.input)
jsonData, _ := json.MarshalIndent(got, "", " ")
t.Logf("Result: %s", jsonData)
tt.verify(t, got)
})
}
}
func TestFuzz(t *testing.T) {
tests := []struct {
name string
input string
want *fuzzyResult
}{
{
name: "包含区",
input: "北京市朝阳区建国路1号",
want: &fuzzyResult{
A2: "北京市",
A3: "朝阳区",
Street: "建国路1号",
},
},
{
name: "包含县",
input: "河北省石家庄市正定县",
want: &fuzzyResult{
A2: "石家庄市",
A3: "正定县",
},
},
{
name: "复杂街道地址",
input: "浙江省杭州市拱墅区武林街道杭州锦麟宾馆中河片区",
want: &fuzzyResult{
A2: "杭州市",
A3: "拱墅区",
Street: "武林街道杭州锦麟宾馆中河片区",
},
},
{
name: "北京市重复格式",
input: "北京市北京市市辖区东城区",
want: &fuzzyResult{
A2: "北京市",
A3: "东城区",
Street: "",
},
},
{
name: "详细地址包含市字",
input: "北京市朝阳区建外大街1号国贸商城",
want: &fuzzyResult{
A2: "北京市",
A3: "朝阳区",
Street: "建外大街1号国贸商城",
},
},
{
name: "详细地址真的包含市字",
input: "北京市朝阳区农贸市场路1号",
want: &fuzzyResult{
A2: "北京市",
A3: "朝阳区",
Street: "农贸市场路1号",
},
},
{
name: "河北省新乐市地址",
input: "河北省石家庄市新乐市经济开发区兴工街10号来优品仓库",
want: &fuzzyResult{
A2: "石家庄市",
A3: "新乐市",
Street: "经济开发区兴工街10号来优品仓库",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := fuzz(tt.input)
jsonData, _ := json.MarshalIndent(got, "", " ")
t.Logf("Result: %s", jsonData)
if got.A2 != tt.want.A2 {
t.Errorf("A2 = %v, want %v", got.A2, tt.want.A2)
}
if got.A3 != tt.want.A3 {
t.Errorf("A3 = %v, want %v", got.A3, tt.want.A3)
}
if tt.want.Street != "" && got.Street != tt.want.Street {
t.Errorf("Street = %v, want %v", got.Street, tt.want.Street)
}
})
}
}
func ExampleParseCNAddress() {
// 解析包含用户信息的完整地址
result := ParseCNAddress("张三 13800138000 北京市朝阳区建国路1号", true)
jsonData, _ := json.MarshalIndent(result, "", " ")
println(string(jsonData))
}
func ExampleParsePersonInfo() {
// 分离用户信息
result := ParsePersonInfo("收货人:李四 电话18612345678 地址上海市浦东新区世纪大道100号")
jsonData, _ := json.MarshalIndent(result, "", " ")
println(string(jsonData))
}

View File

@@ -10,7 +10,7 @@ import (
"io"
"math"
"math/rand"
"os"
"sync"
"time"
"unsafe"
@@ -27,7 +27,14 @@ const (
AllChars = Numeral + LowwerLetters + UpperLetters + SymbolChars
)
var rn = rand.NewSource(time.Now().UnixNano())
// var rn = rand.NewSource(time.Now().UnixNano())
// 每个 goroutine 独立的 rand.Rand避免并发问题
var randPool = sync.Pool{
New: func() any {
return rand.New(rand.NewSource(time.Now().UnixNano()))
},
}
func init() {
rand.Seed(time.Now().UnixNano())
@@ -286,10 +293,9 @@ func nearestPowerOfTwo(cap int) int {
// random generate a random string based on given string range.
func random(s string, length int) string {
// 确保随机数生成器的种子是动态的
pid := os.Getpid()
timestamp := time.Now().UnixNano()
rand.Seed(int64(pid) + timestamp)
// 从 pool 中获取 rand.Rand替代全局 rand
rn := randPool.Get().(*rand.Rand)
defer randPool.Put(rn)
// 仿照strings.Builder
// 创建一个长度为 length 的字节切片

View File

@@ -4,6 +4,7 @@ import (
"reflect"
"regexp"
"strconv"
"sync"
"testing"
"github.com/duke-git/lancet/v2/internal"
@@ -376,3 +377,31 @@ func TestRandNumberOfLength(t *testing.T) {
assert := internal.NewAssert(t, "TestRandNumberOfLength")
assert.Equal(6, len(strconv.Itoa(randi)))
}
// TestRandStringConcurrent verifies RandString is safe under high concurrency.
// Before the fix, this test may panic or trigger data races.
// After the fix, it should always pass.
func TestRandStringConcurrent(t *testing.T) {
const (
goroutines = 100
iterations = 1000
length = 32
)
var wg sync.WaitGroup
wg.Add(goroutines)
for g := 0; g < goroutines; g++ {
go func() {
defer wg.Done()
for i := 0; i < iterations; i++ {
s := RandString(length)
if len(s) != length {
t.Fatalf("unexpected string length: got %d, want %d", len(s), length)
}
}
}()
}
wg.Wait()
}

View File

@@ -8,7 +8,6 @@ import (
"encoding/json"
"fmt"
"net"
"net/mail"
"net/url"
"reflect"
"regexp"
@@ -26,6 +25,7 @@ var (
intStrMatcher *regexp.Regexp = regexp.MustCompile(`^[\+-]?\d+$`)
// dnsMatcher *regexp.Regexp = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
dnsMatcher *regexp.Regexp = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*(?:xn--[a-zA-Z0-9\-]{1,59}|[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)$`)
emailMatcher *regexp.Regexp = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`)
chineseMobileMatcher *regexp.Regexp = regexp.MustCompile(`^1(?:3\d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8\d|9\d)\d{8}$`)
chineseIdMatcher *regexp.Regexp = regexp.MustCompile(`([1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx])|([1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}[0-9Xx])`)
chineseMatcher *regexp.Regexp = regexp.MustCompile("[\u4e00-\u9fa5]")
@@ -321,10 +321,9 @@ func IsDns(dns string) bool {
}
// IsEmail check if the string is a email address.
// Play: https://go.dev/play/p/Os9VaFlT33G
// Play: https://go.dev/play/p/HVQ5LAe-vFz
func IsEmail(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil
return emailMatcher.MatchString(strings.ToLower(email))
}
// IsChineseMobile check if the string is chinese mobile number.

View File

@@ -494,8 +494,12 @@ func TestIsEmail(t *testing.T) {
assert := internal.NewAssert(t, "TestIsEmail")
assert.Equal(true, IsEmail("abc@xyz.com"))
assert.Equal(true, IsEmail("user@domain.co"))
assert.Equal(true, IsEmail("test.user@example.org"))
assert.Equal(false, IsEmail("@abc@xyz.com"))
assert.Equal(false, IsEmail("a.b@@com"))
assert.Equal(false, IsEmail("a.b@com"))
assert.Equal(false, IsEmail("test@example"))
}
func TestContainChinese(t *testing.T) {