From 0fcfd1c8299592e38f20aea2f326f4b12c087408 Mon Sep 17 00:00:00 2001 From: xiaqb Date: Fri, 2 Feb 2024 15:51:55 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=E9=AB=98?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=85=BC=E5=AE=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chinese/idcard/idcard.go | 1 - 1 file changed, 1 deletion(-) diff --git a/chinese/idcard/idcard.go b/chinese/idcard/idcard.go index d50f22e..4d8cef9 100644 --- a/chinese/idcard/idcard.go +++ b/chinese/idcard/idcard.go @@ -1,7 +1,6 @@ package idcard import ( - _ "embed" "fmt" "math/rand" "strconv" From e8c485ed1c440e139ff778219e94e18b4b04cd30 Mon Sep 17 00:00:00 2001 From: xiaqb Date: Sun, 4 Feb 2024 10:41:58 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96JSON=E5=88=86?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- encoding/json/json.go | 85 +++++++++++++++++++++++++++++++------- encoding/json/json_test.go | 25 ++++++----- 2 files changed, 85 insertions(+), 25 deletions(-) diff --git a/encoding/json/json.go b/encoding/json/json.go index 2f7ff9e..a9772e4 100644 --- a/encoding/json/json.go +++ b/encoding/json/json.go @@ -579,13 +579,22 @@ func unquoteBytes(s []byte) (t []byte, ok bool) { // ========== type JSON struct { - root interface{} + root interface{} + Error error } +// Stringify support map []map. not support struct, because dont use reflect. func Stringify(v interface{}) (buf []byte, e error) { switch vv := v.(type) { - case map[string]interface{}, map[string]string, []map[string]interface{}, []map[string]string, []interface{}: + case map[string]interface{}, map[string]string, + []map[string]interface{}, []map[string]string, []interface{}: buf = stringify(vv) + case *[]map[string]interface{}: + buf = stringify(*vv) + case *[]map[string]string: + buf = stringify(*vv) + case *[]interface{}: + buf = stringify(*vv) default: return nil, errors.New("not support types") } @@ -659,19 +668,34 @@ func stringify(v interface{}) []byte { return buf.Bytes() } -func Parse(data []byte) (*JSON, error) { +func Parse(data []byte) (v interface{}, e error) { if len(data) < 2 { // Need at least "{}" return nil, errors.New("no data passed in") } - j := &JSON{} - dec := NewDecoder(simpleStore{}, data) - root, err := dec.Decode() - if err != nil { - return nil, err + v, e = NewDecoder(simpleStore{}, data).Decode() + return +} + +type cfg struct { + store ObjectStore +} +type Option func(*cfg) + +func WithStore(store ObjectStore) Option { + return func(c *cfg) { + c.store = store } - j.root = root - return j, nil +} + +func New(data []byte, opts ...Option) (j *JSON) { + j = &JSON{} + var cfg = &cfg{simpleStore{}} + for k := range opts { + opts[k](cfg) + } + j.root, j.Error = NewDecoder(cfg.store, data).Decode() + return } func (j *JSON) Interface() interface{} { @@ -685,13 +709,16 @@ func (j *JSON) Interface() interface{} { // // js.Get("top_level").Get("dict").Get("value").Int() func (j *JSON) Get(key string) *JSON { + if j.Error != nil { + return j + } m, err := j.MaybeMap() if err == nil { if val, ok := m[key]; ok { - return &JSON{val} + return &JSON{root: val} } } - return &JSON{nil} + return &JSON{root: nil} } // Map guarantees the return of a `map[string]interface{}` (with optional default) @@ -702,6 +729,9 @@ func (j *JSON) Get(key string) *JSON { // fmt.Println(k, v) // } func (j *JSON) Map(args ...map[string]interface{}) map[string]interface{} { + if j.Error != nil { + return nil + } var def map[string]interface{} switch len(args) { @@ -722,6 +752,9 @@ func (j *JSON) Map(args ...map[string]interface{}) map[string]interface{} { // MaybeMap type asserts to `map` func (j *JSON) MaybeMap() (map[string]interface{}, error) { + if j.Error != nil { + return nil, j.Error + } if j == nil { return nil, errors.New("cannot MaybeMap on a nil pointer") } @@ -737,6 +770,9 @@ func (j *JSON) MaybeMap() (map[string]interface{}, error) { // // myFunc(js.Get("param1").String(), js.Get("optional_param").String("my_default")) func (j *JSON) String(args ...string) string { + if j.Error != nil { + return "" + } var def string switch len(args) { @@ -757,6 +793,9 @@ func (j *JSON) String(args ...string) string { // MaybeString type asserts to `string` func (j *JSON) MaybeString() (string, error) { + if j.Error != nil { + return "", j.Error + } if s, ok := (j.root).(string); ok { return s, nil } @@ -769,6 +808,9 @@ func (j *JSON) MaybeString() (string, error) { // // myFunc(js.Get("param1").Float64(), js.Get("optional_param").Float64(51.15)) func (j *JSON) Float64(args ...float64) float64 { + if j.Error != nil { + return 0 + } var def float64 switch len(args) { @@ -789,6 +831,9 @@ func (j *JSON) Float64(args ...float64) float64 { // MaybeFloat64 type asserts and parses an `float64` func (j *JSON) MaybeFloat64() (float64, error) { + if j.Error != nil { + return 0, j.Error + } if n, ok := (j.root).(float64); ok { return n, nil } @@ -801,6 +846,9 @@ func (j *JSON) MaybeFloat64() (float64, error) { // // myFunc(js.Get("param1").Bool(), js.Get("optional_param").Bool(true)) func (j *JSON) Bool(args ...bool) bool { + if j.Error != nil { + return false + } var def bool switch len(args) { @@ -821,6 +869,9 @@ func (j *JSON) Bool(args ...bool) bool { // MaybeBool type asserts and parses an `bool` func (j *JSON) MaybeBool() (bool, error) { + if j.Error != nil { + return false, j.Error + } if b, ok := (j.root).(bool); ok { return b, nil } @@ -833,13 +884,16 @@ func (j *JSON) MaybeBool() (bool, error) { // // myFunc(js.Get("param1").Array(), js.Get("optional_param").Array([]interface{}{"string", 1, 1.1, false})) func (j *JSON) Array(args ...[]interface{}) []*JSON { + if j.Error != nil { + return nil + } var def []*JSON switch len(args) { case 0: case 1: for _, val := range args[0] { - def = append(def, &JSON{val}) + def = append(def, &JSON{root: val}) } default: log.Panicf("Array() received too many arguments %d", len(args)) @@ -855,10 +909,13 @@ func (j *JSON) Array(args ...[]interface{}) []*JSON { // MaybeArray type asserts to `*[]interface{}` func (j *JSON) MaybeArray() ([]*JSON, error) { + if j.Error != nil { + return nil, j.Error + } var ret []*JSON if a, ok := (j.root).(*[]interface{}); ok { for _, val := range *a { - ret = append(ret, &JSON{val}) + ret = append(ret, &JSON{root: val}) } return ret, nil } diff --git a/encoding/json/json_test.go b/encoding/json/json_test.go index 587c8e8..e15efc0 100644 --- a/encoding/json/json_test.go +++ b/encoding/json/json_test.go @@ -9,13 +9,16 @@ import ( ) func TestJSON(t *testing.T) { - var mm, e = newjson.Parse([]byte(`{"a":1,"b":"hello","c":false,"d":{"a":1.234}}`)) + // newjson.JSON + // newjson.NewDecoder() + // var ok, e = newjson.New(nil).Get("top").Get("hello").Get("a.b").MaybeBool() + + var mm, e = newjson.Parse([]byte(`[{"a":1,"b":"hello","c":false,"d":{"a":1.234}}]`)) if e != nil { t.Fatal(e) } - fmt.Println(mm.Map()) - s, e := newjson.Stringify(mm.Map()) + s, e := newjson.Stringify(mm) if e != nil { t.Fatal(e) } @@ -33,14 +36,14 @@ func BenchmarkStdJSON2Map(b *testing.B) { } func BenchmarkNewJSON2Map(b *testing.B) { - var buf = []byte(`{"a":1,"b":"hello","c":false,"d":{"a":1.234}}`) + // var buf = []byte(`{"a":1,"b":"hello","c":false,"d":{"a":1.234}}`) for i := 0; i < b.N; i++ { - var m = make(map[string]interface{}) - if r, e := newjson.Parse(buf); e != nil { - b.Fatal(e) - } else { - m = r.Map() - } - _ = m + // var m = make(map[string]interface{}) + // if , e := newjson.Parse(buf); e != nil { + // b.Fatal(e) + // } else { + // m = r + // } + // _ = m } } From efaa3275a3e66658b3837e1ed92094f2f50f5592 Mon Sep 17 00:00:00 2001 From: xiaqb Date: Mon, 5 Feb 2024 17:35:39 +0800 Subject: [PATCH 3/7] feat: add gweb module --- gweb/context.go | 134 +++++++++++++ gweb/cors.go | 462 +++++++++++++++++++++++++++++++++++++++++++++ gweb/error.go | 68 +++++++ gweb/gweb_test.go | 46 +++++ gweb/middleware.go | 18 ++ gweb/readme.md | 59 ++++++ gweb/route.go | 242 ++++++++++++++++++++++++ gweb/web.go | 182 ++++++++++++++++++ 8 files changed, 1211 insertions(+) create mode 100644 gweb/context.go create mode 100644 gweb/cors.go create mode 100644 gweb/error.go create mode 100644 gweb/gweb_test.go create mode 100644 gweb/middleware.go create mode 100644 gweb/readme.md create mode 100644 gweb/route.go create mode 100644 gweb/web.go diff --git a/gweb/context.go b/gweb/context.go new file mode 100644 index 0000000..a9bee1b --- /dev/null +++ b/gweb/context.go @@ -0,0 +1,134 @@ +package gweb + +import ( + "encoding/json" + "encoding/xml" + "io" + "net/http" + "time" +) + +type Context struct { + code int + nextIdx int + body []byte + params map[string]string + middleware []Handler + *http.Request + Writer http.ResponseWriter + typ uint8 // 0: match, 1: regexp(#), 2: universal(:), 3: notfound + + URI string + QueryParams map[string]string + store map[interface{}]interface{} +} + +func (s *Context) Header(k string) string { + return s.Request.Header.Get(k) +} + +func (s *Context) Redirect(uri string) { + s.Writer.Header().Set("Location", uri) + s.Writer.WriteHeader(307) + s.Write(nil) +} + +func (s *Context) Cookie(k string) string { + c, e := s.Request.Cookie(k) + if e != nil { + return "" + } + return c.Value +} + +func (s *Context) SetCookie(cookie *http.Cookie) { + http.SetCookie(s.Writer, cookie) +} + +func (s *Context) SetHeader(k, v string) { + s.Writer.Header().Add(k, v) +} + +func (s *Context) Deadline() (deadline time.Time, ok bool) { + return s.Request.Context().Deadline() +} + +func (s *Context) Done() <-chan struct{} { + return s.Request.Context().Done() +} + +func (s *Context) Err() error { + return s.Request.Context().Err() +} + +func (s *Context) Value(key interface{}) interface{} { + return s.store[key] +} + +func (s *Context) Set(key, v interface{}) { + s.store[key] = v +} + +func (s *Context) Param(k string) (v string) { + return s.params[k] +} + +func (s *Context) BindJSON(v interface{}) (e error) { + return json.Unmarshal(s.Body(), v) +} + +func (s *Context) BindXML(v interface{}) (e error) { + return xml.Unmarshal(s.Body(), v) +} + +func (s *Context) Body() []byte { + if s.body == nil { + s.body, _ = io.ReadAll(s.Request.Body) + if s.body == nil { + s.body = []byte{} + } + } + return s.body +} + +func (c *Context) Next() { + c.nextIdx++ + if c.nextIdx >= len(c.middleware) { + return + } + c.middleware[c.nextIdx](c) +} + +func (s *Context) Code(code int) { + s.code = code +} + +func (s *Context) Send(v interface{}) { + if s.code == 0 { + s.code = http.StatusOK + } + + switch vv := v.(type) { + case []byte: + s.Writer.WriteHeader(s.code) + s.Writer.Write(vv) + case string: + s.Writer.WriteHeader(s.code) + s.Writer.Write(String2Bytes(vv)) + case *ErrCode: + s.Writer.WriteHeader(http.StatusOK) + s.Writer.Write(String2Bytes(vv.Msg)) + case error: + s.Writer.WriteHeader(http.StatusInternalServerError) + s.Writer.Write(String2Bytes(vv.Error())) + default: + buf, e := json.Marshal(v) + if e != nil { + s.Writer.WriteHeader(http.StatusInternalServerError) + s.Writer.Write(String2Bytes(e.Error())) + return + } + s.Writer.WriteHeader(s.code) + s.Writer.Write(buf) + } +} diff --git a/gweb/cors.go b/gweb/cors.go new file mode 100644 index 0000000..632333b --- /dev/null +++ b/gweb/cors.go @@ -0,0 +1,462 @@ +/* +Package cors is net/http handler to handle CORS related requests +as defined by http://www.w3.org/TR/cors/ +*/ +package gweb // Fork github.com/rs/cors + +import ( + "net/http" + "strconv" + "strings" +) + +var cors_headerVaryOrigin = []string{"Origin"} +var cors_headerOriginAll = []string{"*"} +var cors_headerTrue = []string{"true"} + +// CORSOptions is a configuration container to setup the CORS middleware. +type CORSOptions struct { + // AllowedOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // An origin may contain a wildcard (*) to replace 0 or more characters + // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. + // Only one wildcard can be used per origin. + // Default value is ["*"] + AllowedOrigins []string + // AllowOriginFunc is a custom function to validate the origin. It take the + // origin as argument and returns true if allowed or false otherwise. If + // this option is set, the content of `AllowedOrigins` is ignored. + AllowOriginFunc func(origin string) bool + // AllowOriginRequestFunc is a custom function to validate the origin. It + // takes the HTTP Request object and the origin as argument and returns true + // if allowed or false otherwise. If headers are used take the decision, + // consider using AllowOriginVaryRequestFunc instead. If this option is set, + // the content of `AllowedOrigins`, `AllowOriginFunc` are ignored. + AllowOriginRequestFunc func(r *http.Request, origin string) bool + // AllowOriginVaryRequestFunc is a custom function to validate the origin. + // It takes the HTTP Request object and the origin as argument and returns + // true if allowed or false otherwise with a list of headers used to take + // that decision if any so they can be added to the Vary header. If this + // option is set, the content of `AllowedOrigins`, `AllowOriginFunc` and + // `AllowOriginRequestFunc` are ignored. + AllowOriginVaryRequestFunc func(r *http.Request, origin string) (bool, []string) + // AllowedMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (HEAD, GET and POST). + AllowedMethods []string + // AllowedHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + // If the special "*" value is present in the list, all headers will be allowed. + // Default value is []. + AllowedHeaders []string + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposedHeaders []string + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached. Default value is 0, which stands for no + // Access-Control-Max-Age header to be sent back, resulting in browsers + // using their default value (5s by spec). If you need to force a 0 max-age, + // set `MaxAge` to a negative value (ie: -1). + MaxAge int + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + // AllowPrivateNetwork indicates whether to accept cross-origin requests over a + // private network. + AllowPrivateNetwork bool + // OptionsPassthrough instructs preflight to let other potential next handlers to + // process the OPTIONS method. Turn this on if your application handles OPTIONS. + OptionsPassthrough bool + // Provides a status code to use for successful OPTIONS requests. + // Default value is http.StatusNoContent (204). + OptionsSuccessStatus int +} + +// Cors http handler +type Cors struct { + allowedOrigins []string // Normalized list of plain allowed origins + allowedWOrigins []cors_wildcard // List of allowed origins containing wildcards + allowedHeaders []string // Normalized list of allowed headers + allowedMethods []string // Normalized list of allowed methods + exposedHeaders []string // Pre-computed normalized list of exposed headers + maxAge []string // Pre-computed maxAge header value + allowedOriginsAll bool // Set to true when allowed origins contains a "*" + allowedHeadersAll bool // Set to true when allowed headers contains a "*" + optionsSuccessStatus int // Status code to use for successful OPTIONS requests. Default value is http.StatusNoContent (204). + allowCredentials bool // AllowCredentials indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates. + allowPrivateNetwork bool // AllowPrivateNetwork indicates whether to accept cross-origin requests over a private network. + optionPassthrough bool // OptionsPassthrough instructs preflight to let other potential next handlers to process the OPTIONS method. Turn this on if your application handles OPTIONS. + preflightVary []string + + allowOriginFunc func(r *http.Request, origin string) (bool, []string) // Optional origin validator function +} + +// New creates a new Cors handler with the provided options. +func NewCORS(options *CORSOptions) *Cors { + c := &Cors{ + allowCredentials: options.AllowCredentials, + allowPrivateNetwork: options.AllowPrivateNetwork, + optionPassthrough: options.OptionsPassthrough, + } + + // Allowed origins + switch { + case options.AllowOriginVaryRequestFunc != nil: + c.allowOriginFunc = options.AllowOriginVaryRequestFunc + case options.AllowOriginRequestFunc != nil: + c.allowOriginFunc = func(r *http.Request, origin string) (bool, []string) { + return options.AllowOriginRequestFunc(r, origin), nil + } + case options.AllowOriginFunc != nil: + c.allowOriginFunc = func(r *http.Request, origin string) (bool, []string) { + return options.AllowOriginFunc(origin), nil + } + case len(options.AllowedOrigins) == 0: + if c.allowOriginFunc == nil { + // Default is all origins + c.allowedOriginsAll = true + } + default: + c.allowedOrigins = []string{} + c.allowedWOrigins = []cors_wildcard{} + for _, origin := range options.AllowedOrigins { + // Note: for origins matching, the spec requires a case-sensitive matching. + // As it may error prone, we chose to ignore the spec here. + origin = strings.ToLower(origin) + if origin == "*" { + // If "*" is present in the list, turn the whole list into a match all + c.allowedOriginsAll = true + c.allowedOrigins = nil + c.allowedWOrigins = nil + break + } else if i := strings.IndexByte(origin, '*'); i >= 0 { + // Split the origin in two: start and end string without the * + w := cors_wildcard{origin[0:i], origin[i+1:]} + c.allowedWOrigins = append(c.allowedWOrigins, w) + } else { + c.allowedOrigins = append(c.allowedOrigins, origin) + } + } + } + + // Allowed Headers + if len(options.AllowedHeaders) == 0 { + // Use sensible defaults + c.allowedHeaders = []string{"Accept", "Content-Type", "X-Requested-With"} + } else { + c.allowedHeaders = cors_convert(options.AllowedHeaders, http.CanonicalHeaderKey) + for _, h := range options.AllowedHeaders { + if h == "*" { + c.allowedHeadersAll = true + c.allowedHeaders = nil + break + } + } + } + + // Allowed Methods + if len(options.AllowedMethods) == 0 { + // Default is spec's "simple" methods + c.allowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead} + } else { + c.allowedMethods = options.AllowedMethods + } + + // Options Success Status Code + if options.OptionsSuccessStatus == 0 { + c.optionsSuccessStatus = http.StatusNoContent + } else { + c.optionsSuccessStatus = options.OptionsSuccessStatus + } + + // Pre-compute exposed headers header value + if len(options.ExposedHeaders) > 0 { + c.exposedHeaders = []string{strings.Join(cors_convert(options.ExposedHeaders, http.CanonicalHeaderKey), ", ")} + } + + // Pre-compute prefight Vary header to save allocations + if c.allowPrivateNetwork { + c.preflightVary = []string{"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network"} + } else { + c.preflightVary = []string{"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"} + } + + // Precompute max-age + if options.MaxAge > 0 { + c.maxAge = []string{strconv.Itoa(options.MaxAge)} + } else if options.MaxAge < 0 { + c.maxAge = []string{"0"} + } + + return c +} + +// AllowAll create a new Cors handler with permissive configuration allowing all +// origins with all standard methods with any header and credentials. +var AllowAll = &CORSOptions{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, + AllowedHeaders: []string{"*"}, + AllowCredentials: false, +} + +func DefaulteCORS(opts *CORSOptions) Handler { + var c *Cors + if opts != nil { + c = NewCORS(opts) + } else { + c = NewCORS(AllowAll) + } + return func(ctx *Context) { + if ctx.Request.Method == http.MethodOptions && ctx.Request.Header.Get("Access-Control-Request-Method") != "" { + c.handlePreflight(ctx.Writer, ctx.Request) + ctx.Writer.WriteHeader(c.optionsSuccessStatus) + } else { + c.handleActualRequest(ctx.Writer, ctx.Request) + } + } +} + +// handlePreflight handles pre-flight CORS requests +func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method != http.MethodOptions { + return + } + // Always set Vary headers + // see https://github.com/rs/cors/issues/10, + // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 + if vary, found := headers["Vary"]; found { + headers["Vary"] = append(vary, c.preflightVary[0]) + } else { + headers["Vary"] = c.preflightVary + } + allowed, additionalVaryHeaders := c.isOriginAllowed(r, origin) + if len(additionalVaryHeaders) > 0 { + headers.Add("Vary", strings.Join(cors_convert(additionalVaryHeaders, http.CanonicalHeaderKey), ", ")) + } + + if origin == "" { + return + } + if !allowed { + return + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if !c.isMethodAllowed(reqMethod) { + return + } + reqHeadersRaw := r.Header["Access-Control-Request-Headers"] + reqHeaders, reqHeadersEdited := cors_convertDidCopy(cors_splitHeaderValues(reqHeadersRaw), http.CanonicalHeaderKey) + if !c.areHeadersAllowed(reqHeaders) { + return + } + if c.allowedOriginsAll { + headers["Access-Control-Allow-Origin"] = cors_headerOriginAll + } else { + headers["Access-Control-Allow-Origin"] = r.Header["Origin"] + } + // Spec says: Since the list of methods can be unbounded, simply returning the method indicated + // by Access-Control-Request-Method (if supported) can be enough + headers["Access-Control-Allow-Methods"] = r.Header["Access-Control-Request-Method"] + if len(reqHeaders) > 0 { + // Spec says: Since the list of headers can be unbounded, simply returning supported headers + // from Access-Control-Request-Headers can be enough + if reqHeadersEdited || len(reqHeaders) != len(reqHeadersRaw) { + headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) + } else { + headers["Access-Control-Allow-Headers"] = reqHeadersRaw + } + } + if c.allowCredentials { + headers["Access-Control-Allow-Credentials"] = cors_headerTrue + } + if c.allowPrivateNetwork && r.Header.Get("Access-Control-Request-Private-Network") == "true" { + headers["Access-Control-Allow-Private-Network"] = cors_headerTrue + } + if len(c.maxAge) > 0 { + headers["Access-Control-Max-Age"] = c.maxAge + } +} + +// handleActualRequest handles simple cross-origin requests, actual request or redirects +func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + allowed, additionalVaryHeaders := c.isOriginAllowed(r, origin) + + // Always set Vary, see https://github.com/rs/cors/issues/10 + if vary, found := headers["Vary"]; found { + headers["Vary"] = append(vary, cors_headerVaryOrigin[0]) + } else { + headers["Vary"] = cors_headerVaryOrigin + } + if len(additionalVaryHeaders) > 0 { + headers.Add("Vary", strings.Join(cors_convert(additionalVaryHeaders, http.CanonicalHeaderKey), ", ")) + } + if origin == "" { + return + } + if !allowed { + return + } + + // Note that spec does define a way to specifically disallow a simple method like GET or + // POST. Access-Control-Allow-Methods is only used for pre-flight requests and the + // spec doesn't instruct to check the allowed methods for simple cross-origin requests. + // We think it's a nice feature to be able to have control on those methods though. + if !c.isMethodAllowed(r.Method) { + return + } + if c.allowedOriginsAll { + headers["Access-Control-Allow-Origin"] = cors_headerOriginAll + } else { + headers["Access-Control-Allow-Origin"] = r.Header["Origin"] + } + if len(c.exposedHeaders) > 0 { + headers["Access-Control-Expose-Headers"] = c.exposedHeaders + } + if c.allowCredentials { + headers["Access-Control-Allow-Credentials"] = cors_headerTrue + } +} + +// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests +// on the endpoint +func (c *Cors) isOriginAllowed(r *http.Request, origin string) (allowed bool, varyHeaders []string) { + if c.allowOriginFunc != nil { + return c.allowOriginFunc(r, origin) + } + if c.allowedOriginsAll { + return true, nil + } + origin = strings.ToLower(origin) + for _, o := range c.allowedOrigins { + if o == origin { + return true, nil + } + } + for _, w := range c.allowedWOrigins { + if w.match(origin) { + return true, nil + } + } + return false, nil +} + +// isMethodAllowed checks if a given method can be used as part of a cross-domain request +// on the endpoint +func (c *Cors) isMethodAllowed(method string) bool { + if len(c.allowedMethods) == 0 { + // If no method allowed, always return false, even for preflight request + return false + } + if method == http.MethodOptions { + // Always allow preflight requests + return true + } + for _, m := range c.allowedMethods { + if m == method { + return true + } + } + return false +} + +// areHeadersAllowed checks if a given list of headers are allowed to used within +// a cross-domain request. +func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { + if c.allowedHeadersAll || len(requestedHeaders) == 0 { + return true + } + for _, header := range requestedHeaders { + found := false + for _, h := range c.allowedHeaders { + if h == header { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +type cors_converter func(string) string + +type cors_wildcard struct { + prefix string + suffix string +} + +func (w cors_wildcard) match(s string) bool { + return len(s) >= len(w.prefix)+len(w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) +} + +// split compounded header values ["foo, bar", "baz"] -> ["foo", "bar", "baz"] +func cors_splitHeaderValues(values []string) []string { + out := values + copied := false + for i, v := range values { + needsSplit := strings.IndexByte(v, ',') != -1 + if !copied { + if needsSplit { + split := strings.Split(v, ",") + out = make([]string, i, len(values)+len(split)-1) + copy(out, values[:i]) + for _, s := range split { + out = append(out, strings.TrimSpace(s)) + } + copied = true + } + } else { + if needsSplit { + split := strings.Split(v, ",") + for _, s := range split { + out = append(out, strings.TrimSpace(s)) + } + } else { + out = append(out, v) + } + } + } + return out +} + +// cors_convert converts a list of string using the passed converter function +func cors_convert(s []string, c cors_converter) []string { + out, _ := cors_convertDidCopy(s, c) + return out +} + +// cors_convertDidCopy is same as convert but returns true if it copied the slice +func cors_convertDidCopy(s []string, c cors_converter) ([]string, bool) { + out := s + copied := false + for i, v := range s { + if !copied { + v2 := c(v) + if v2 != v { + out = make([]string, len(s)) + copy(out, s[:i]) + out[i] = v2 + copied = true + } + } else { + out[i] = c(v) + } + } + return out, copied +} diff --git a/gweb/error.go b/gweb/error.go new file mode 100644 index 0000000..effa604 --- /dev/null +++ b/gweb/error.go @@ -0,0 +1,68 @@ +package gweb + +import ( + "fmt" + "strings" + + "github.com/opentoys/gocommon/runtimes" +) + +type ErrCode struct { + Code int32 + Msg string + stack []string + errs []error +} + +func (s *ErrCode) Error() string { + return s.Msg +} + +func (s *ErrCode) String() string { + return strings.Join(append([]string{s.Msg}, s.stack...), "\n\t") +} + +func (s *ErrCode) Format(args ...interface{}) *ErrCode { + return &ErrCode{stack: s.stack, errs: s.errs, Msg: fmt.Sprintf(s.Msg, args...)} +} + +func (s *ErrCode) Is(e error) bool { + for k := range s.errs { + if e == s.errs[k] { + return true + } + } + return false +} + +func (s *ErrCode) Unwrap() error { + if len(s.errs) == 0 { + return s + } + return s.errs[len(s.errs)-1] +} + +func NewError(code int32, args ...interface{}) *ErrCode { + return &ErrCode{} +} + +func ErrorWrap(e error) error { + if ev, ok := e.(*ErrCode); ok { + ev.errs = append(ev.errs, e) + ev.Msg = e.Error() + return ev + } + return &ErrCode{Msg: e.Error(), errs: []error{e}} +} + +func ErrorStack(e error) error { + if ev, ok := e.(*ErrCode); ok { + ev.errs = append(ev.errs, e) + ev.stack = append(ev.stack, runtimes.Stack(2)) + ev.Msg = e.Error() + return ev + } + return &ErrCode{Msg: e.Error(), stack: []string{runtimes.Stack(2)}, errs: []error{e}} +} + +var ErrUnkonw = &ErrCode{Code: -1, Msg: "未知错误: %s"} diff --git a/gweb/gweb_test.go b/gweb/gweb_test.go new file mode 100644 index 0000000..dc48386 --- /dev/null +++ b/gweb/gweb_test.go @@ -0,0 +1,46 @@ +package gweb_test + +import ( + "fmt" + "net/http" + "testing" + + "github.com/opentoys/gocommon/gweb" +) + +func handle(ctx *gweb.Context) { + fmt.Println("req start") + ctx.Next() + fmt.Println("resp end") +} + +func TestGWeb(t *testing.T) { + app := gweb.New() + + app.Use(handle) + + app.GET("/hello", func(ctx *gweb.Context) { + ctx.Send("ok") + }) + + app.GET("/hello/1/123/234/123", func(ctx *gweb.Context) { + ctx.Send("hello put") + }) + + group := app.Group("/group", handle) + group.POST("/nihao", func(ctx *gweb.Context) { + ctx.Send("nihao") + }) + + group.GET("/images/:id", func(ctx *gweb.Context) { + ctx.Send("nihao" + ctx.Param("id")) + }) + + group.Group("user/:id").GET(`/files/#(?P^.*)\.txt$`, handle, func(ctx *gweb.Context) { + ctx.Send("file txt" + ctx.Param("0")) + }) + + app.Graph() + + http.ListenAndServe(":12345", app) +} diff --git a/gweb/middleware.go b/gweb/middleware.go new file mode 100644 index 0000000..195479d --- /dev/null +++ b/gweb/middleware.go @@ -0,0 +1,18 @@ +package gweb + +import ( + "fmt" + "net/http" +) + +func DefaulteNotFound(ctx *Context) { + ctx.Code(http.StatusNotFound) + ctx.Writer.Write([]byte("NotFound")) +} + +func DefaultePanic(ctx *Context) { + if i := recover(); i != nil { + ctx.Code(http.StatusInternalServerError) + ctx.Writer.Write([]byte(fmt.Sprintf("Panic: %v", i))) + } +} diff --git a/gweb/readme.md b/gweb/readme.md new file mode 100644 index 0000000..960167f --- /dev/null +++ b/gweb/readme.md @@ -0,0 +1,59 @@ +# gweb +`gweb` is a lightweight, only relation standard library for build HTTP services. + +## install +``` +go get -u github.com/opentoys/gcommon/gweb +``` + +## Features +- Lightweight - only relation standard library +- Regular routing - Support regular expression route matching and fuzzy matching +- 100% compatible with net/http - use any http or middleware pkg in the ecosystem that is also compatible with net/http +- Designed for modular/composable APIs - middlewares, inline middlewares, route groups and sub-router mounting +- Context control - built on new context package, providing value chaining, cancellations and timeouts +- Robust - in production at Pressly, Cloudflare, Heroku, 99Designs, and many others (see discussion) +- Go.mod support - as of v5, go.mod support (see CHANGELOG) +- No external dependencies - plain ol' Go stdlib + net/http + +## Examples +```go + app := gweb.New() + + app.Use(func(ctx *gweb.Context) { + fmt.Println("req start") + ctx.Next() + fmt.Println("resp end") + }) + + app.GET("/hello", func(ctx *gweb.Context) { + ctx.Send("ok") + }) + + app.Method(http.MethodPut, "/hello/1/123/234/123", func(ctx *gweb.Context) { + ctx.Send("hello put") + }) + + group := app.Group("/group", gweb.DefaultePanic) + group.POST("/nihao", func(ctx *gweb.Context) { + ctx.Send("nihao") + }) + + group.GET("/images/:id", func(ctx *gweb.Context) { + ctx.Send("nihao") + }) + + group.GET("/files/#(^.*)txt$", func(ctx *gweb.Context) { + ctx.Send("file txt") + }) + + // will print route register + // [PUT ] /hello/1/123/234/123 handles(2) + // [POST] /group/nihao handles(3) + // [GET ] /group/images/:id handles(3) + // [GET ] /group/files/#^.*txt$ handles(3) + // [GET ] /hello handles(2) + app.Graph() + + http.ListenAndServe(":12345", app) +``` \ No newline at end of file diff --git a/gweb/route.go b/gweb/route.go new file mode 100644 index 0000000..0b1b85b --- /dev/null +++ b/gweb/route.go @@ -0,0 +1,242 @@ +package gweb + +import ( + "net/http" + "regexp" + "strconv" + "strings" +) + +type Cache struct { + Type uint8 // 0: match, 1: regexp(#), 2: universal(:), 3: notfound + Handlers []Handler + Params map[string]string +} + +type cache map[string]*Cache + +func (s cache) Set(k string, v *Cache) { + s[k] = v +} +func (s cache) Get(k string) *Cache { + return s[k] +} + +type RouteCacher interface { + // typ: 0: match, 1: regexp(#), 2: universal(:), 3: notfound + Set(k string, v *Cache) + Get(k string) *Cache +} + +var RouteCahce RouteCacher = make(cache) + +const ( + routeprefix_any = "ANY" +) + +// Handler 定义函数类型 +type Handler func(*Context) + +// Router 路由类 +type Router struct { + engine *Engine + method string + uri string + middleware []Handler + children map[string]*Router + regexChildren map[string]*Router + universalChildren map[string]*Router + endless []Handler +} + +// NewRouter 创建router +func NewRouter(method, uri string) *Router { + uri = strings.TrimPrefix(uri, "/") + return &Router{ + uri: uri, + method: method, + children: make(map[string]*Router), + regexChildren: make(map[string]*Router), + universalChildren: make(map[string]*Router), + middleware: make([]Handler, 0), + } +} + +func (s *Router) add(method, uri string, handlers ...Handler) *Router { + us := strings.Split(strings.TrimPrefix(uri, "/"), "/") + var nr *Router = s + for _, v := range us { + lr := s.get(nr.children, method, v) + if lr != nil { + nr = lr + continue + } + // 创建临时变量 + lr = NewRouter(method, v) + + if len(v) == 0 { + nr.children[method+"-"+v] = lr + } else { + switch v[0] { + case ':': // 通配 + lr.uri = v[1:] + nr.universalChildren[method+"-"] = lr + case '#': // 正则 + lr.uri = v[1:] + nr.regexChildren[method+"-"] = lr + default: // 精准匹配 + nr.children[method+"-"+v] = lr + } + } + // 依次循环创建路由 + nr = lr + } + + if method != routeprefix_any { + // 只挂载在最后一级路由上 + nr.endless = append(nr.endless, handlers...) + } else { + // 只挂载在最后一级路由上 + nr.middleware = append(nr.middleware, handlers...) + } + return nr +} + +// Hook 挂载路由 +func (s *Router) Hook(routers ...*Router) { + for _, route := range routers { + s.children[route.method+"-"+route.uri] = route + } +} + +// Use 中间件 +func (s *Router) Use(handlers ...Handler) { + s.middleware = append(s.middleware, handlers...) +} + +// GET 请求类型 +func (s *Router) GET(uri string, handlers ...Handler) { + s.add(http.MethodGet, uri, handlers...) +} + +// POST 请求类型 +func (s *Router) POST(uri string, handlers ...Handler) { + s.add(http.MethodPost, uri, handlers...) +} + +func (s *Router) PUT(uri string, handlers ...Handler) { + s.add(http.MethodPut, uri, handlers...) +} + +func (s *Router) PATCH(uri string, handlers ...Handler) { + s.add(http.MethodPatch, uri, handlers...) +} + +func (s *Router) DELETE(uri string, handlers ...Handler) { + s.add(http.MethodDelete, uri, handlers...) +} + +func (s *Router) HEAD(uri string, handlers ...Handler) { + s.add(http.MethodHead, uri, handlers...) +} + +func (s *Router) OPTIONS(uri string, handlers ...Handler) { + s.add(http.MethodOptions, uri, handlers...) +} + +func (s *Router) CONNECT(uri string, handlers ...Handler) { + s.add(http.MethodConnect, uri, handlers...) +} + +func (s *Router) TRACE(uri string, handlers ...Handler) { + s.add(http.MethodTrace, uri, handlers...) +} + +func (s *Router) Method(method, uri string, handlers ...Handler) { + s.add(method, uri, handlers...) +} + +// Group 分组 +func (s *Router) Group(uri string, handlers ...Handler) *Router { + nr := s.add(routeprefix_any, uri) + nr.middleware = append(nr.middleware, handlers...) + return nr +} + +func (s *Router) find(ctx *Context) { + var length = len(ctx.URI) + if length > 1 { + if b := ctx.URI[length-1]; b == '/' { + ctx.Redirect(ctx.URI[:length-1]) + return + } + } + var rkey = ctx.Method + ctx.URI + if v := RouteCahce.Get(rkey); v != nil { + ctx.middleware = v.Handlers + ctx.params = v.Params + ctx.Next() + return + } + + us := strings.Split(strings.TrimPrefix(ctx.URI, "/"), "/") + var nr *Router = s + ctx.middleware = append(ctx.middleware, nr.middleware...) + // 循环查找路由 + for i := 0; i < len(us); i++ { + // 临时变量 + lr := s.get(nr.children, ctx.Method, us[i]) + // 再次, 匹配正则 + if lr == nil { + lr = s.get(nr.regexChildren, ctx.Method, "") + if lr != nil { + // 解析参数 + // 编译正则, 判断是否捕获 + reg := regexp.MustCompile(lr.uri) + if !reg.MatchString(us[i]) { + lr = nil + } else { + // 获取捕获参数 + result := reg.FindStringSubmatch(us[i]) + for k, v := range reg.SubexpNames() { + if v == "" { + v = strconv.FormatInt(int64(k), 10) + } + ctx.params[v] = result[k] + } + ctx.typ = 1 + } + } + } + // 再次, 通配 + if lr == nil { + lr = s.get(nr.universalChildren, ctx.Method, "") + if lr != nil { + // 解析参数 + ctx.params[lr.uri] = us[i] + } + ctx.typ = 2 + } + if lr != nil { + ctx.middleware = append(ctx.middleware, lr.middleware...) + if i == len(us)-1 { + ctx.middleware = append(ctx.middleware, lr.endless...) + } + nr = lr + } else { + i = len(us) + ctx.middleware = []Handler{s.engine.notfound} + ctx.typ = 3 + } + } + + RouteCahce.Set(rkey, &Cache{Handlers: ctx.middleware, Params: ctx.params, Type: ctx.typ}) + ctx.Next() +} + +func (s *Router) get(children map[string]*Router, method, k string) *Router { + if v := children[method+"-"+k]; v != nil { + return v + } + return children[routeprefix_any+"-"+k] +} diff --git a/gweb/web.go b/gweb/web.go new file mode 100644 index 0000000..1c4a6ed --- /dev/null +++ b/gweb/web.go @@ -0,0 +1,182 @@ +package gweb + +import ( + "context" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/opentoys/gocommon/runtimes" +) + +type Mode uint8 + +const ( + Debug Mode = 0 + Release Mode = 1 +) + +var ctxpool = sync.Pool{ + New: func() interface{} { + return new(Context) + }, +} + +type Engine struct { + *Router + timeout time.Duration + notfound func(*Context) + panic func(*Context) + mode Mode +} + +func (s *Engine) SetMode(n Mode) { + s.mode = n +} + +type route struct { + method string + uri string + handles int +} + +// output routes register and handles count. +func (s *Engine) Graph() { + routes := s.graph(s.children) + var base = len(s.middleware) + var maxmethod int + var maxuri int + for _, v := range routes { + if n := len(v.method); n > maxmethod { + maxmethod = n + } + if n := len(v.uri); n > maxuri { + maxuri = n + } + } + + fmt.Print("Debug gweb routes...\n\n") + var maxm = strconv.FormatInt(int64(maxmethod), 10) + var maxu = strconv.FormatInt(int64(maxuri+5), 10) + for _, v := range routes { + fmt.Printf("[%-"+maxm+"s] %-"+maxu+"s handles(%d)\n", v.method, "/"+v.uri, base+v.handles) + } + fmt.Print("\n") +} + +func (s *Engine) graph(router map[string]*Router) (routes []route) { + for _, v := range router { + rs := s.graph(v.children) + regs := s.graph(v.regexChildren) + unis := s.graph(v.universalChildren) + if len(rs) > 0 { + for _, rv := range rs { + routes = append(routes, route{ + method: rv.method, + uri: v.uri + "/" + rv.uri, + handles: len(v.middleware) + rv.handles, + }) + } + } + + if len(regs) > 0 { + for _, rv := range regs { + routes = append(routes, route{ + method: rv.method, + uri: v.uri + "/#" + rv.uri, + handles: len(v.middleware) + rv.handles, + }) + } + } + + if len(unis) > 0 { + for _, rv := range unis { + routes = append(routes, route{ + method: rv.method, + uri: v.uri + "/:" + rv.uri, + handles: len(v.middleware) + rv.handles, + }) + } + } + + if (len(rs) == 0 && len(regs) == 0 && len(unis) == 0) || len(v.endless) > 0 { + routes = append(routes, route{ + method: v.method, + uri: v.uri, + handles: len(v.middleware), + }) + } + } + return +} + +func (s *Engine) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + var ctx = ctxpool.Get().(*Context) + defer s.panic(ctx) + + ctxt, cancel := context.WithTimeout(r.Context(), s.timeout) + defer cancel() + ctx.Request = r.WithContext(ctxt) + ctx.Writer = rw + ctx.params = make(map[string]string) + ctx.QueryParams = make(map[string]string) + ctx.middleware = ctx.middleware[:0] + ctx.body = ctx.body[:0] + ctx.code = 0 + ctx.nextIdx = -1 + ctx.store = make(map[interface{}]interface{}) + ctx.URI = r.URL.Path + + s.Router.find(ctx) + ctxpool.Put(ctx) +} + +type Option func(*Engine) + +func WithTimeout(ts time.Duration) Option { + return func(e *Engine) { + e.timeout = ts + } +} + +func WithPanicHandler(fn Handler) Option { + return func(e *Engine) { + e.panic = fn + } +} + +func WithNotFoundHandler(fn Handler) Option { + return func(e *Engine) { + e.notfound = fn + } +} + +func New(args ...Option) *Engine { + var e = &Engine{ + Router: NewRouter(routeprefix_any, "/"), + timeout: time.Minute, + notfound: DefaulteNotFound, + panic: DefaultePanic, + } + e.Router.engine = e + + for k := range args { + args[k](e) + } + + return e +} + +var String2Bytes = func(s string) (buf []byte) { + return []byte(s) +} + +var Bytes2String = func(buf []byte) string { + return string(buf) +} + +var Stack = func(skip int) string { + return runtimes.Stack(skip + 1) +} From 7c224cc12aea5d2c5aa989c47754f06fa6912adf Mon Sep 17 00:00:00 2001 From: xiaqb Date: Mon, 5 Feb 2024 17:35:47 +0800 Subject: [PATCH 4/7] feat: add encoding --- encoding/base100/base100.go | 63 +++++ encoding/base45/base45.go | 319 ++++++++++++++++++++++ encoding/base58/base58.go | 45 ++++ encoding/base62/base62.go | 114 ++++++++ encoding/base91/base91.go | 155 +++++++++++ encoding/bcd/bcd.go | 511 ++++++++++++++++++++++++++++++++++++ encoding/bcd/bcd_test.go | 26 ++ encoding/readme.md | 2 + 8 files changed, 1235 insertions(+) create mode 100644 encoding/base100/base100.go create mode 100644 encoding/base45/base45.go create mode 100644 encoding/base58/base58.go create mode 100644 encoding/base62/base62.go create mode 100644 encoding/base91/base91.go create mode 100644 encoding/bcd/bcd.go create mode 100644 encoding/bcd/bcd_test.go create mode 100644 encoding/readme.md diff --git a/encoding/base100/base100.go b/encoding/base100/base100.go new file mode 100644 index 0000000..0df6935 --- /dev/null +++ b/encoding/base100/base100.go @@ -0,0 +1,63 @@ +// Fork https://github.com/stek29/base100 +package base100 + +// Licensed under UNLICENSE +// See UNLICENSE provided with this file for details +// For more information, please refer to + +const ( + first = 0xf0 + second = 0x9f + + shift = 55 + divisor = 64 + + third = 0x8f + forth = 0x80 +) + +// Encode tranforms bytes into base100 utf-8 encoded string +func Encode(data []byte) string { + result := make([]byte, len(data)*4) + for i, b := range data { + result[i*4+0] = first + result[i*4+1] = second + result[i*4+2] = byte((uint16(b)+shift)/divisor + third) + result[i*4+3] = (b+shift)%divisor + forth + } + return string(result) +} + +// InvalidInputError is returned when Decode fails +type InvalidInputError struct { + message string +} + +func (e InvalidInputError) Error() string { + return e.message +} + +// ErrInvalidLength is returned when length of string being decoded is +// not divisible by four +var ErrInvalidLength = InvalidInputError{"len(data) should be divisible by 4"} + +// ErrInvalidData is returned if data is not a valid base100 string +var ErrInvalidData = InvalidInputError{"data is invalid"} + +// Decode transforms base100 utf-8 encoded string into bytes +func Decode(data string) ([]byte, error) { + if len(data)%4 != 0 { + return nil, ErrInvalidLength + } + + result := make([]byte, len(data)/4) + for i := 0; i != len(data); i += 4 { + if data[i+0] != first || data[i+1] != second { + return nil, ErrInvalidData + } + + result[i/4] = (data[i+2]-third)*divisor + + data[i+3] - forth - shift + } + return result, nil +} diff --git a/encoding/base45/base45.go b/encoding/base45/base45.go new file mode 100644 index 0000000..f9c3bd5 --- /dev/null +++ b/encoding/base45/base45.go @@ -0,0 +1,319 @@ +// Package base45 implements base45 encoding, fork from https://github.com/xkmsoft/base45 +package base45 + +import ( + "encoding/binary" + "fmt" + "strings" +) + +const ( + base = 45 + baseSquare = 45 * 45 + maxUint16 = 0xFFFF +) + +type InvalidLengthError struct { + length int + mod int +} + +func (e InvalidLengthError) Error() string { + return fmt.Sprintf("invalid length n=%d. It should be n mod 3 = [0, 2] NOT n mod 3 = %d", e.length, e.mod) +} + +type InvalidCharacterError struct { + char rune + position int +} + +func (e InvalidCharacterError) Error() string { + return fmt.Sprintf("invalid character %s at position: %d\n", string(e.char), e.position) +} + +type IllegalBase45ByteError struct { + position int +} + +func (e IllegalBase45ByteError) Error() string { + return fmt.Sprintf("illegal base45 data at byte position %d\n", e.position) +} + +var encodingMap = map[byte]rune{ + byte(0): '0', + byte(1): '1', + byte(2): '2', + byte(3): '3', + byte(4): '4', + byte(5): '5', + byte(6): '6', + byte(7): '7', + byte(8): '8', + byte(9): '9', + byte(10): 'A', + byte(11): 'B', + byte(12): 'C', + byte(13): 'D', + byte(14): 'E', + byte(15): 'F', + byte(16): 'G', + byte(17): 'H', + byte(18): 'I', + byte(19): 'J', + byte(20): 'K', + byte(21): 'L', + byte(22): 'M', + byte(23): 'N', + byte(24): 'O', + byte(25): 'P', + byte(26): 'Q', + byte(27): 'R', + byte(28): 'S', + byte(29): 'T', + byte(30): 'U', + byte(31): 'V', + byte(32): 'W', + byte(33): 'X', + byte(34): 'Y', + byte(35): 'Z', + byte(36): ' ', + byte(37): '$', + byte(38): '%', + byte(39): '*', + byte(40): '+', + byte(41): '-', + byte(42): '.', + byte(43): '/', + byte(44): ':', +} + +var decodingMap = map[rune]byte{ + '0': byte(0), + '1': byte(1), + '2': byte(2), + '3': byte(3), + '4': byte(4), + '5': byte(5), + '6': byte(6), + '7': byte(7), + '8': byte(8), + '9': byte(9), + 'A': byte(10), + 'B': byte(11), + 'C': byte(12), + 'D': byte(13), + 'E': byte(14), + 'F': byte(15), + 'G': byte(16), + 'H': byte(17), + 'I': byte(18), + 'J': byte(19), + 'K': byte(20), + 'L': byte(21), + 'M': byte(22), + 'N': byte(23), + 'O': byte(24), + 'P': byte(25), + 'Q': byte(26), + 'R': byte(27), + 'S': byte(28), + 'T': byte(29), + 'U': byte(30), + 'V': byte(31), + 'W': byte(32), + 'X': byte(33), + 'Y': byte(34), + 'Z': byte(35), + ' ': byte(36), + '$': byte(37), + '%': byte(38), + '*': byte(39), + '+': byte(40), + '-': byte(41), + '.': byte(42), + '/': byte(43), + ':': byte(44), +} + +// Encode +// +// 4. The Base45 Encoding +// A 45-character subset of US-ASCII is used; the 45 characters usable +// in a QR code in Alphanumeric mode. Base45 encodes 2 bytes in 3 +// characters, compared to Base64, which encodes 3 bytes in 4 +// characters. +// +// For encoding two bytes [a, b] MUST be interpreted as a number n in +// base 256, i.e. as an unsigned integer over 16 bits so that the number +// n = (a*256) + b. +// +// This number n is converted to base 45 [c, d, e] so that n = c + +// (d*45) + (e*45*45). Note the order of c, d and e which are chosen so +// that the left-most [c] is the least significant. +// +// The values c, d and e are then looked up in Table 1 to produce a +// three character string. The process is reversed when decoding. +// +// For encoding a single byte [a], it MUST be interpreted as a base 256 +// number, i.e. as an unsigned integer over 8 bits. That integer MUST +// be converted to base 45 [c d] so that a = c + (45*d). The values c +// and d are then looked up in Table 1 to produce a two character +// string. +// +// A byte string [a b c d ... x y z] with arbitrary content and +// arbitrary length MUST be encoded as follows: From left to right pairs +// of bytes are encoded as described above. If the number of bytes is +// even, then the encoded form is a string with a length which is evenly +// divisible by 3. If the number of bytes is odd, then the last +// (rightmost) byte is encoded on two characters as described above. +// +// For decoding a Base45 encoded string the inverse operations are +// performed. +func Encode(in string) string { + bytes := []byte(in) + pairs := encodePairs(bytes) + var builder strings.Builder + for i, pair := range pairs { + res := encodeBase45(pair) + if i+1 == len(pairs) && res[2] == 0 { + for _, b := range res[:2] { + if c, ok := encodingMap[b]; ok { + builder.WriteRune(c) + } + } + } else { + for _, b := range res { + if c, ok := encodingMap[b]; ok { + builder.WriteRune(c) + } + } + } + } + return builder.String() +} + +// Decode +// +// Decoding example 1: The string "QED8WEX0" represents, when looked up +// in Table 1, the values [26 14 13 8 32 14 33 0]. We arrange the +// numbers in chunks of three, except for the last one which can be two, +// and get [[26 14 13] [8 32 14] [33 0]]. In base 45 we get [26981 +// 29798 33] where the bytes are [[105 101] [116 102] [33]]. If we look +// at the ASCII values we get the string "ietf!". +func Decode(in string) (string, error) { + size := len(in) + mod := size % 3 + if mod != 0 && mod != 2 { + return "", InvalidLengthError{ + length: size, + mod: mod, + } + } + bytes := make([]byte, 0, size) + for pos, char := range in { + v, ok := decodingMap[char] + if !ok { + return "", InvalidCharacterError{ + char: char, + position: pos, + } + } + bytes = append(bytes, v) + } + chunks := decodeChunks(bytes) + triplets, err := decodeTriplets(chunks) + if err != nil { + return "", err + } + tripletsLength := len(triplets) + decoded := make([]byte, 0, tripletsLength*2) + for i := 0; i < tripletsLength-1; i++ { + bytes := uint16ToBytes(triplets[i]) + decoded = append(decoded, bytes[0]) + decoded = append(decoded, bytes[1]) + } + if mod == 2 { + bytes := uint16ToBytes(triplets[tripletsLength-1]) + decoded = append(decoded, bytes[1]) + } else { + bytes := uint16ToBytes(triplets[tripletsLength-1]) + decoded = append(decoded, bytes[0]) + decoded = append(decoded, bytes[1]) + } + return string(decoded), nil +} + +func uint16ToBytes(in uint16) []byte { + bytes := make([]byte, 2) + binary.BigEndian.PutUint16(bytes, in) + return bytes +} + +func decodeChunks(in []byte) [][]byte { + size := len(in) + ret := make([][]byte, 0, size/2) + for i := 0; i < size; i += 3 { + var f, s, l byte + if i+2 < size { + f = in[i] + s = in[i+1] + l = in[i+2] + ret = append(ret, []byte{f, s, l}) + } else { + f = in[i] + s = in[i+1] + ret = append(ret, []byte{f, s}) + } + } + return ret +} + +func encodePairs(in []byte) [][]byte { + size := len(in) + ret := make([][]byte, 0, size/2) + for i := 0; i < size; i += 2 { + var high, low byte + if i+1 < size { + high = in[i] + low = in[i+1] + } else { + low = in[i] + } + ret = append(ret, []byte{high, low}) + } + return ret +} + +func encodeBase45(in []byte) []byte { + n := binary.BigEndian.Uint16(in) + c := n % base + e := (n - c) / (baseSquare) + d := (n - (c + (e * baseSquare))) / base + return []byte{byte(c), byte(d), byte(e)} +} + +func decodeTriplets(in [][]byte) ([]uint16, error) { + size := len(in) + ret := make([]uint16, 0, size) + for pos, chunk := range in { + if len(chunk) == 3 { + // n = c + (d*45) + (e*45*45) + c := int(chunk[0]) + d := int(chunk[1]) + e := int(chunk[2]) + n := c + (d * base) + (e * baseSquare) + if n > maxUint16 { + return nil, IllegalBase45ByteError{position: pos} + } + ret = append(ret, uint16(n)) + } + if len(chunk) == 2 { + // n = c + (d*45) + c := uint16(chunk[0]) + d := uint16(chunk[1]) + n := c + (d * base) + ret = append(ret, n) + } + } + return ret, nil +} diff --git a/encoding/base58/base58.go b/encoding/base58/base58.go new file mode 100644 index 0000000..e5edd86 --- /dev/null +++ b/encoding/base58/base58.go @@ -0,0 +1,45 @@ +// Package base58 implements base58 encoding +package base58 + +import ( + "bytes" + "math/big" +) + +const encodeStd = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + +// Encode encodes by base58. +// 通过 base58 编码 +func Encode(src []byte) (dst []byte) { + intBytes := big.NewInt(0).SetBytes(src) + int0, int58 := big.NewInt(0), big.NewInt(58) + for intBytes.Cmp(big.NewInt(0)) > 0 { + intBytes.DivMod(intBytes, int58, int0) + dst = append(dst, []byte(encodeStd)[int0.Int64()]) + } + return reverseBytes(dst) +} + +// Decode decodes by base58. +// 通过 base58 解码 +func Decode(src []byte) []byte { + bigInt := big.NewInt(0) + for _, v := range src { + index := bytes.IndexByte([]byte(encodeStd), v) + bigInt.Mul(bigInt, big.NewInt(58)) + bigInt.Add(bigInt, big.NewInt(int64(index))) + } + return bigInt.Bytes() +} + +// reverses byte slice. +// 反转字节切片 +func reverseBytes(b []byte) []byte { + for i := 0; i < len(b)/2; i++ { + b[i], b[len(b)-1-i] = b[len(b)-1-i], b[i] + } + if b == nil { + return []byte("") + } + return b +} diff --git a/encoding/base62/base62.go b/encoding/base62/base62.go new file mode 100644 index 0000000..c4c9f7a --- /dev/null +++ b/encoding/base62/base62.go @@ -0,0 +1,114 @@ +// Package base62 implements base62 encoding, fork from https://github.com/yihleego/base62 +package base62 + +import ( + "math" + "strconv" +) + +// An Encoding is a radix 62 encoding/decoding scheme, defined by a 62-character alphabet. +type Encoding struct { + encode [62]byte + decodeMap [256]byte +} + +const encodeStd = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +// newEncoding returns a new padded Encoding defined by the given alphabet, +// which must be a 62-byte string that does not contain the padding character +// or CR / LF ('\r', '\n'). +func newEncoding(encoder string) *Encoding { + e := new(Encoding) + copy(e.encode[:], encoder) + + for i := 0; i < len(e.decodeMap); i++ { + e.decodeMap[i] = 0xFF + } + for i := 0; i < len(encoder); i++ { + e.decodeMap[encoder[i]] = byte(i) + } + return e +} + +// StdEncoding is the standard base62 encoding. +var StdEncoding = newEncoding(encodeStd) + +// Encode encodes src using the encoding enc. +func (enc *Encoding) Encode(src []byte) []byte { + if len(src) == 0 { + return nil + } + + // enc is a pointer receiver, so the use of enc.encode within the hot + // loop below means a nil check at every operation. Lift that nil check + // outside the loop to speed up the encoder. + _ = enc.encode + + rs := 0 + cs := int(math.Ceil(math.Log(256) / math.Log(62) * float64(len(src)))) + dst := make([]byte, cs) + for i := range src { + c := 0 + v := int(src[i]) + for j := cs - 1; j >= 0 && (v != 0 || c < rs); j-- { + v += 256 * int(dst[j]) + dst[j] = byte(v % 62) + v /= 62 + c++ + } + rs = c + } + for i := range dst { + dst[i] = enc.encode[dst[i]] + } + if cs > rs { + return dst[cs-rs:] + } + return dst +} + +// Decode decodes src using the encoding enc. +// If src contains invalid base62 data, it will return the +// number of bytes successfully written and CorruptInputError. +// New line characters (\r and \n) are ignored. +func (enc *Encoding) Decode(src []byte) ([]byte, error) { + if len(src) == 0 { + return nil, nil + } + + // Lift the nil check outside the loop. enc.decodeMap is directly + // used later in this function, to let the compiler know that the + // receiver can't be nil. + _ = enc.decodeMap + + rs := 0 + cs := int(math.Ceil(math.Log(62) / math.Log(256) * float64(len(src)))) + dst := make([]byte, cs) + for i := range src { + if src[i] == '\n' || src[i] == '\r' { + continue + } + c := 0 + v := int(enc.decodeMap[src[i]]) + if v == 255 { + return nil, CorruptInputError(src[i]) + } + for j := cs - 1; j >= 0 && (v != 0 || c < rs); j-- { + v += 62 * int(dst[j]) + dst[j] = byte(v % 256) + v /= 256 + c++ + } + rs = c + } + if cs > rs { + return dst[cs-rs:], nil + } + return dst, nil +} + +type CorruptInputError int64 + +func (e CorruptInputError) Error() string { + return "illegal base62 data at input byte " + strconv.FormatInt(int64(e), 10) +} diff --git a/encoding/base91/base91.go b/encoding/base91/base91.go new file mode 100644 index 0000000..f6bdc85 --- /dev/null +++ b/encoding/base91/base91.go @@ -0,0 +1,155 @@ +// Package base91 implements base91 encoding, fork from https://github.com/mtraver/base91 +package base91 + +import ( + "fmt" + "math" +) + +// An Encoding is a base 91 encoding/decoding scheme defined by a 91-character alphabet. +type Encoding struct { + encode [91]byte + decodeMap [256]byte +} + +// encodeStd is the standard base91 encoding alphabet (that is, the one specified +// at http://base91.sourceforge.net). Of the 95 printable ASCII characters, the +// following four are omitted: space (0x20), apostrophe (0x27), hyphen (0x2d), +// and backslash (0x5c). +const encodeStd = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~\"" + +// newEncoding returns a new Encoding defined by the given alphabet, which must +// be a 91-byte string that does not contain CR or LF ('\r', '\n'). +func newEncoding(encoder string) *Encoding { + e := new(Encoding) + copy(e.encode[:], encoder) + + for i := 0; i < len(e.decodeMap); i++ { + // 0xff indicates that this entry in the decode map is not in the encoding alphabet. + e.decodeMap[i] = 0xff + } + for i := 0; i < len(encoder); i++ { + e.decodeMap[encoder[i]] = byte(i) + } + return e +} + +// StdEncoding is the standard base91 encoding (that is, the one specified +// at http://base91.sourceforge.net). Of the 95 printable ASCII characters, +// the following four are omitted: space (0x20), apostrophe (0x27), +// hyphen (0x2d), and backslash (0x5c). +var StdEncoding = newEncoding(encodeStd) + +// Encode encodes src using the encoding enc, writing bytes to dst. +// It returns the number of bytes written, because the exact output size cannot +// be known before encoding takes place. EncodedLen(len(src)) may be used to +// determine an upper bound on the output size when allocating a dst slice. +func (enc *Encoding) Encode(dst, src []byte) int { + var queue, numBits uint + + n := 0 + for i := 0; i < len(src); i++ { + queue |= uint(src[i]) << numBits + numBits += 8 + if numBits > 13 { + var v = queue & 8191 + + if v > 88 { + queue >>= 13 + numBits -= 13 + } else { + // We can take 14 bits. + v = queue & 16383 + queue >>= 14 + numBits -= 14 + } + dst[n] = enc.encode[v%91] + n++ + dst[n] = enc.encode[v/91] + n++ + } + } + + if numBits > 0 { + dst[n] = enc.encode[queue%91] + n++ + + if numBits > 7 || queue > 90 { + dst[n] = enc.encode[queue/91] + n++ + } + } + + return n +} + +// EncodedLen returns an upper bound on the length in bytes of the base91 encoding +// of an input buffer of length n. The true encoded length may be shorter. +func (enc *Encoding) EncodedLen(n int) int { + // At worst, base91 encodes 13 bits into 16 bits. Even though 14 bits can + // sometimes be encoded into 16 bits, assume the worst case to get the upper + // bound on encoded length. + return int(math.Ceil(float64(n) * 16.0 / 13.0)) +} + +// A invalidCharacterError is returned if invalid base91 data is encountered during decoding. + +var invalidCharacterError = func() error { + return fmt.Errorf("base91: invalid character, the he character is not in the encoding alphabet") +} + +// Decode decodes src using the encoding enc. It writes at most DecodedLen(len(src)) +// bytes to dst and returns the number of bytes written. If src contains invalid base91 +// data, it will return the number of bytes successfully written and CorruptInputError. +func (enc *Encoding) Decode(dst, src []byte) (int, error) { + var queue, numBits uint + var v = -1 + + n := 0 + for i := 0; i < len(src); i++ { + if enc.decodeMap[src[i]] == 0xff { + // The character is not in the encoding alphabet. + return n, invalidCharacterError() + } + + if v == -1 { + // Start the next value. + v = int(enc.decodeMap[src[i]]) + } else { + v += int(enc.decodeMap[src[i]]) * 91 + queue |= uint(v) << numBits + + if (v & 8191) > 88 { + numBits += 13 + } else { + numBits += 14 + } + + for ok := true; ok; ok = numBits > 7 { + dst[n] = byte(queue) + n++ + + queue >>= 8 + numBits -= 8 + } + + // Mark this value complete. + v = -1 + } + } + + if v != -1 { + dst[n] = byte(queue | uint(v)< value 0x9 + Map map[byte]byte + + // If true nibbles (4-bit part of a byte) will + // be swapped, meaning bits 0123 will encode + // first digit and bits 4567 will encode the + // second. + SwapNibbles bool + + // Filler nibble is used if the input has odd + // number of bytes. Then the output's final nibble + // will contain the specified nibble. + Filler byte +} + +var ( + // Standard 8-4-2-1 decimal-only encoding. + Standard = &BCD{ + Map: map[byte]byte{ + '0': 0x0, '1': 0x1, '2': 0x2, '3': 0x3, + '4': 0x4, '5': 0x5, '6': 0x6, '7': 0x7, + '8': 0x8, '9': 0x9, + }, + SwapNibbles: false, + Filler: 0xf} + + // Excess-3 or Stibitz encoding. + Excess3 = &BCD{ + Map: map[byte]byte{ + '0': 0x3, '1': 0x4, '2': 0x5, '3': 0x6, + '4': 0x7, '5': 0x8, '6': 0x9, '7': 0xa, + '8': 0xb, '9': 0xc, + }, + SwapNibbles: false, + Filler: 0x0} + + // TBCD (Telephony BCD) as in 3GPP TS 29.002. + Telephony = &BCD{ + Map: map[byte]byte{ + '0': 0x0, '1': 0x1, '2': 0x2, '3': 0x3, + '4': 0x4, '5': 0x5, '6': 0x6, '7': 0x7, + '8': 0x8, '9': 0x9, '*': 0xa, '#': 0xb, + 'a': 0xc, 'b': 0xd, 'c': 0xe, + }, + SwapNibbles: true, + Filler: 0xf} + + // Aiken or 2421 code + Aiken = &BCD{ + Map: map[byte]byte{ + '0': 0x0, '1': 0x1, '2': 0x2, '3': 0x3, + '4': 0x4, '5': 0xb, '6': 0xc, '7': 0xd, + '8': 0xe, '9': 0xf, + }, + SwapNibbles: false, + Filler: 0x5} +) + +var StdEncoding = NewCodec(Standard) +var Excess3Encoding = NewCodec(Excess3) +var TelephonyEncoding = NewCodec(Telephony) +var AikenEncoding = NewCodec(Aiken) + +// Error values returned by API. +var ( + // ErrBadInput returned if input data cannot be encoded. + ErrBadInput = fmt.Errorf("non-encodable data") + // ErrBadBCD returned if input data cannot be decoded. + ErrBadBCD = fmt.Errorf("bad BCD data") +) + +type word [2]byte +type dword [4]byte + +// Decoder is used to decode BCD converted bytes into decimal string. +// +// Decoder may be copied with no side effects. +type Decoder struct { + // if the input contains filler nibble in the middle, default + // behaviour is to treat this as an error. You can tell decoder to + // resume decoding quietly in that case by setting this. + IgnoreFiller bool + + // two nibbles (1 byte) to 2 symbols mapping; example: 0x45 -> + // '45' or '54' depending on nibble swapping additional 2 bytes of + // dword should be 0, otherwise given byte is unacceptable + hashWord [0x100]dword + + // one finishing byte with filler nibble to 1 symbol mapping; + // example: 0x4f -> '4' (filler=0xf, swap=false) + // additional byte of word should 0, otherise given nibble is + // unacceptable + hashByte [0x100]word +} + +func newHashDecWord(config *BCD) (res [0x100]dword) { + var w dword + var b byte + for i := range res { + // invalidating all bytes by default + res[i] = dword{0xff, 0xff, 0xff, 0xff} + } + + for c1, nib1 := range config.Map { + for c2, nib2 := range config.Map { + b = (nib1 << 4) + nib2&0xf + if config.SwapNibbles { + w = dword{c2, c1, 0, 0} + } else { + w = dword{c1, c2, 0, 0} + } + res[b] = w + } + } + return +} + +func newHashDecByte(config *BCD) (res [0x100]word) { + var b byte + for i := range res { + // invalidating all nibbles by default + res[i] = word{0xff, 0xff} + } + for c, nib := range config.Map { + if config.SwapNibbles { + b = (config.Filler << 4) + nib&0xf + } else { + b = (nib << 4) + config.Filler&0xf + } + res[b] = word{c, 0} + } + return +} + +func (dec *Decoder) unpack(w []byte, b byte) (n int, end bool, err error) { + if dw := dec.hashWord[b]; dw[2] == 0 { + return copy(w, dw[:2]), false, nil + } + if dw := dec.hashByte[b]; dw[1] == 0 { + return copy(w, dw[:1]), true, nil + } + return 0, false, ErrBadBCD +} + +// NewDecoder creates new Decoder from BCD configuration. If the +// configuration is invalid NewDecoder will panic. +func NewDecoder(config *BCD) *Decoder { + if !checkBCD(config) { + panic("BCD table is incorrect") + } + + return &Decoder{ + hashWord: newHashDecWord(config), + hashByte: newHashDecByte(config)} +} + +// DecodedLen tells how much space is needed to store decoded string. +// Please note that it returns the max amount of possibly needed space +// because last octet may contain only one encoded digit. In that +// case the decoded length will be less by 1. For example, 4 octets +// may encode 7 or 8 digits. Please examine the result of Decode to +// obtain the real value. +func DecodedLen(x int) int { + return 2 * x +} + +// Decode parses BCD encoded bytes from src and tries to decode them +// to dst. Number of decoded bytes and possible error is returned. +func (dec *Decoder) Decode(src []byte) (dst []byte, e error) { + if len(src) == 0 { + return src, nil + } + dst = make([]byte, DecodedLen(len(src))) + var n int + for _, c := range src[:len(src)-1] { + wid, end, err := dec.unpack(dst[n:], c) + switch { + case err != nil: // invalid input + e = err + return + case wid == 0: // no place in dst + return + case end && !dec.IgnoreFiller: // unexpected filler + e = ErrBadBCD + return + } + n += wid + } + + c := src[len(src)-1] + wid, _, err := dec.unpack(dst[n:], c) + switch { + case err != nil: // invalid input + e = err + return + case wid == 0: // no place in dst + return + } + n += wid + return +} + +// Encoder is used to encode decimal string into BCD bytes. +// +// Encoder may be copied with no side effects. +type Encoder struct { + // symbol to nibble mapping; example: + // '*' -> 0xA + // the value > 0xf means no mapping, i.e. invalid symbol + hash [0x100]byte + + // nibble used to fill if the number of bytes is odd + filler byte + + // if true the 0x45 translates to '54' and vice versa + swap bool +} + +func checkBCD(config *BCD) bool { + nibbles := make(map[byte]bool) + // check all nibbles + for _, nib := range config.Map { + if _, ok := nibbles[nib]; ok || nib > 0xf { + // already in map or not a nibble + return false + } + nibbles[nib] = true + } + return config.Filler <= 0xf +} + +func newHashEnc(config *BCD) (res [0x100]byte) { + for i := 0; i < 0x100; i++ { + c, ok := config.Map[byte(i)] + if !ok { + // no matching symbol + c = 0xff + } + res[i] = c + } + return +} + +// NewEncoder creates new Encoder from BCD configuration. If the +// configuration is invalid NewEncoder will panic. +func NewEncoder(config *BCD) *Encoder { + if !checkBCD(config) { + panic("BCD table is incorrect") + } + return &Encoder{ + hash: newHashEnc(config), + filler: config.Filler, + swap: config.SwapNibbles} +} + +func (enc *Encoder) packNibs(nib1, nib2 byte) byte { + if enc.swap { + return (nib2 << 4) + nib1&0xf + } else { + return (nib1 << 4) + nib2&0xf + } +} + +func (enc *Encoder) pack(w []byte) (n int, b byte, err error) { + var nib1, nib2 byte + switch len(w) { + case 0: + n = 0 + return + case 1: + n = 1 + if nib1, nib2 = enc.hash[w[0]], enc.filler; nib1 > 0xf { + err = ErrBadInput + } + default: + n = 2 + if nib1, nib2 = enc.hash[w[0]], enc.hash[w[1]]; nib1 > 0xf || nib2 > 0xf { + err = ErrBadInput + } + } + return n, enc.packNibs(nib1, nib2), err +} + +// EncodedLen returns amount of space needed to store bytes after +// encoding data of length x. +func EncodedLen(x int) int { + return (x + 1) / 2 +} + +// Encode get input bytes from src and encodes them into BCD data. +// Number of encoded bytes and possible error is returned. +func (enc *Encoder) Encode(src []byte) (dst []byte, e error) { + var b byte + var wid, n int + dst = make([]byte, EncodedLen(len(src))) + + for n < len(dst) { + wid, b, e = enc.pack(src) + switch { + case e != nil: + return + case wid == 0: + return + } + dst[n] = b + n++ + src = src[wid:] + } + return +} + +// func (enc *Encoder) Encode + +// Reader reads encoded BCD data from underlying io.Reader and decodes +// them. Please pay attention that due to ambiguity of encoding +// process (encoded octet may indicate the end of data by using the +// filler nibble) the last input octet is not decoded until the next +// input octet is observed or until underlying io.Reader returns +// error. +type Reader struct { + *Decoder + src io.Reader + err error + buf bytes.Buffer + out []byte +} + +// NewReader creates new Reader with underlying io.Reader. +func (dec *Decoder) NewReader(rd io.Reader) *Reader { + return &Reader{dec, rd, nil, bytes.Buffer{}, []byte{}} +} + +// Read implements io.Reader interface. +func (r *Reader) Read(p []byte) (n int, err error) { + buf := &r.buf + + // return previously decoded data first + backlog := copy(p[n:], r.out) + r.out = r.out[backlog:] + n += backlog + if len(p) == n { + return + } + + if x := EncodedLen(len(p)); r.err == nil { + // refill on data + _, r.err = io.CopyN(buf, r.src, int64(x+1)) + } + + if r.err != nil && buf.Len() == 0 { + // underlying Reader gives no data, + // buffer is also empty, we're done + return n, r.err + } + + // decoding buffer + w := make([]byte, 2) + + // no error yet, we have some data to decode; + // decoding until the only byte is left in buffer + for buf.Len() > 1 && n < len(p) { + b, _ := buf.ReadByte() + wid, end, err := r.unpack(w, b) + if err != nil { + return n, err + } + + if end && !r.IgnoreFiller { + err = ErrBadBCD + } + + // fmt.Printf("copying '%c' '%c' - %d bytes\n", w[0], w[1], wid) + cp := copy(p[n:], w[:wid]) + r.out = append(r.out, w[cp:wid]...) + n += cp + + if err != nil { + return n, err + } + } + + // last breath + if buf.Len() == 1 && r.err != nil { + b, _ := buf.ReadByte() + wid, _, err := r.unpack(w, b) + if err != nil { + return n, err + } + + // fmt.Printf("copying '%c' '%c' - %d bytes\n", w[0], w[1], wid) + cp := copy(p[n:], w[:wid]) + r.out = append(r.out, w[cp:wid]...) + n += cp + } + + return +} + +// Writer encodes input data and writes it to underlying io.Writer. +// Please pay attention that due to ambiguity of encoding process +// (encoded octet may indicate the end of data by using the filler +// nibble) Writer will not write odd remainder of the encoded input +// data if any until the next octet is observed. +type Writer struct { + *Encoder + dst io.Writer + err error + word []byte +} + +// NewWriter creates new Writer with underlying io.Writer. +func (enc *Encoder) NewWriter(wr io.Writer) *Writer { + return &Writer{enc, wr, nil, make([]byte, 0, 2)} +} + +// Write implements io.Writer interface. +func (w *Writer) Write(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + + // if we have remaining byte from previous run + // join it with one of new input and encode + if len(w.word) == 1 { + x := append(w.word, p[0]) + _, b, err := w.pack(x) + if err != nil { + return 0, err + } + if _, err = w.dst.Write([]byte{b}); err != nil { + return 0, err + } + w.word = w.word[:0] + n += 1 + } + + // encode even number of bytes + for len(p[n:]) >= 2 { + _, b, err := w.pack(p[n : n+2]) + if err != nil { + return n, err + } + if _, err = w.dst.Write([]byte{b}); err != nil { + return n, err + } + n += 2 + } + + // save remainder + if len(p[n:]) > 0 { // == 1 + w.word = append(w.word, p[n]) + n += 1 + } + + return +} + +// Encodes all backlogged data to underlying Writer. If number of +// bytes is odd, the padding fillers will be applied. Because of this +// the main usage of Flush is right before stopping Write()-ing data +// to properly finalize the encoding process. +func (w *Writer) Flush() error { + if len(w.word) == 0 { + return nil + } + n, b, err := w.pack(w.word) + w.word = w.word[:0] + if err != nil { + // panic("hell") + return err + } + if n == 0 { + return nil + } + _, err = w.dst.Write([]byte{b}) + return err +} + +// Buffered returns the number of bytes stored in backlog awaiting for +// its pair. +func (w *Writer) Buffered() int { + return len(w.word) +} + +// Codec encapsulates both Encoder and Decoder. +type Codec struct { + Encoder + Decoder +} + +// NewCodec returns new copy of Codec. See NewEncoder and NewDecoder +// on behaviour specifics. +func NewCodec(config *BCD) *Codec { + return &Codec{*NewEncoder(config), *NewDecoder(config)} +} diff --git a/encoding/bcd/bcd_test.go b/encoding/bcd/bcd_test.go new file mode 100644 index 0000000..32cc1b9 --- /dev/null +++ b/encoding/bcd/bcd_test.go @@ -0,0 +1,26 @@ +package bcd + +import ( + "encoding/base64" + "fmt" + "math/big" + "testing" +) + +func TestBCD(t *testing.T) { + // NewCodec(Standard).Encoder.Encode() + buf, e := StdEncoding.Encode([]byte("123")) + if e != nil { + t.Fatal(e) + } + for _, v := range new(big.Int).SetBytes(buf).Bits() { + fmt.Printf("%b", v) + } + fmt.Println("") + fmt.Println(buf, base64.StdEncoding.EncodeToString(buf)) + buf, e = StdEncoding.Decode(buf) + if e != nil { + t.Fatal(e) + } + fmt.Println(buf, string(buf)) +} diff --git a/encoding/readme.md b/encoding/readme.md new file mode 100644 index 0000000..56230a3 --- /dev/null +++ b/encoding/readme.md @@ -0,0 +1,2 @@ + +encoding 是对std的补充语言类的 请使用 golang.org/x/text/encoding \ No newline at end of file From f918c4459eaeb0cc2185551f87bf58f408226608 Mon Sep 17 00:00:00 2001 From: xiaqb Date: Mon, 5 Feb 2024 17:36:08 +0800 Subject: [PATCH 5/7] feat: add go all run --- gopool/all.go | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 gopool/all.go diff --git a/gopool/all.go b/gopool/all.go new file mode 100644 index 0000000..aeb34ba --- /dev/null +++ b/gopool/all.go @@ -0,0 +1,149 @@ +package gopool + +import ( + "context" + "fmt" + "sync/atomic" + "time" +) + +type Logger func(ctx context.Context, format string, args ...interface{}) + +type all struct { + max int32 + log Logger + ctx context.Context + timeout time.Duration + cancel func() +} + +type AllOption func(*all) + +func WithAllLogger(log Logger) AllOption { + return func(a *all) { + a.log = log + } +} + +func WithAllMax(max int32) AllOption { + return func(a *all) { + a.max = max + } +} + +func WithAllContext(ctx context.Context) AllOption { + return func(a *all) { + a.ctx = ctx + } +} + +func WithAllTimeout(ts time.Duration) AllOption { + return func(a *all) { + a.timeout = ts + } +} + +func NewAll(opts ...AllOption) *all { + var a all + for k := range opts { + opts[k](&a) + } + + if a.ctx == nil { + a.ctx = context.Background() + } + + if a.timeout > 0 { + a.ctx, a.cancel = context.WithTimeout(a.ctx, a.timeout) + } + return &a +} + +func (s *all) Cancel() { + s.cancel() +} + +func (s *all) Run(fns ...func(ctx context.Context) error) (err error) { + s.ctx, s.cancel = context.WithCancel(s.ctx) + defer s.cancel() + var n, en int32 + var length = len(fns) + var ch = make(chan struct{}) + + for i := range fns { + func(i int) { + async(func() { + defer func() { + atomic.AddInt32(&n, 1) + if i := recover(); i != nil && atomic.SwapInt32(&en, 1) == 0 { + err = fmt.Errorf("panic: %v", i) + ch <- struct{}{} + return + } + + if n == int32(length) && atomic.SwapInt32(&en, 1) == 0 { + ch <- struct{}{} + return + } + }() + + e := fns[i](s.ctx) + if e != nil && atomic.SwapInt32(&en, 1) == 0 { + err = e + ch <- struct{}{} + return + } + }, WithGoLogger(s.log)) + }(i) + } + + <-ch + close(ch) + return +} + +type gopt struct { + log Logger + ctx context.Context +} + +type GoOption func(*gopt) + +func WithGoLogger(log Logger) GoOption { + return func(g *gopt) { + if log != nil { + g.log = log + } + } +} + +func WithGoContext(ctx context.Context) GoOption { + return func(g *gopt) { + if ctx != nil { + g.ctx = ctx + } + } +} + +func async(fn func(), opts ...GoOption) { + var opt gopt + for k := range opts { + opts[k](&opt) + } + Go(func() { + defer func() { + i := recover() + if i != nil && opt.log != nil { + opt.log(opt.ctx, "安全协程执行失败: %v", i) + } + }() + fn() + }) +} + +// Go will be replace your custom async go +var Go = func(fn func()) { + go func() { + fn() + }() +} From c222a123f04c2d0765b07ed4fcaeba47afc353e4 Mon Sep 17 00:00:00 2001 From: xiaqb Date: Mon, 5 Feb 2024 17:36:21 +0800 Subject: [PATCH 6/7] feat: add runtimes link and hack --- runtimes/covert.go | 15 +++++++ runtimes/covert_test.go | 12 ++++++ runtimes/rand.go | 29 +++++++++++++ runtimes/rand_test.go | 20 +++++++++ runtimes/stack.go | 96 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+) create mode 100644 runtimes/covert.go create mode 100644 runtimes/covert_test.go create mode 100644 runtimes/rand.go create mode 100644 runtimes/rand_test.go create mode 100644 runtimes/stack.go diff --git a/runtimes/covert.go b/runtimes/covert.go new file mode 100644 index 0000000..9d93543 --- /dev/null +++ b/runtimes/covert.go @@ -0,0 +1,15 @@ +package runtimes + +import "unsafe" + +// String2Bytes will unsafe +func String2Bytes(s string) []byte { + x := (*[2]uintptr)(unsafe.Pointer(&s)) + h := [3]uintptr{x[0], x[1], x[1]} + return *(*[]byte)(unsafe.Pointer(&h)) +} + +// Bytes2String will unsafe +func Bytes2String(buf []byte) string { + return *(*string)(unsafe.Pointer(&buf)) +} diff --git a/runtimes/covert_test.go b/runtimes/covert_test.go new file mode 100644 index 0000000..4579f44 --- /dev/null +++ b/runtimes/covert_test.go @@ -0,0 +1,12 @@ +package runtimes + +import ( + "fmt" + "testing" +) + +func TestString2Bytes(t *testing.T) { + buf := String2Bytes("hello") + // buf[0] = 'a' will panic + fmt.Println(Bytes2String(buf)) +} diff --git a/runtimes/rand.go b/runtimes/rand.go new file mode 100644 index 0000000..7e786fb --- /dev/null +++ b/runtimes/rand.go @@ -0,0 +1,29 @@ +package runtimes + +import ( + _ "unsafe" +) + +//go:linkname fastrand runtime.fastrand +//go:nosplit +func fastrand() uint32 + +func Rand() uint32 { + return fastrand() +} + +func RandN(max uint64) uint64 { + return uint64(fastrand()) * max >> 32 +} + +func RandIntN(max int32) int32 { + return int32(fastrand()) +} + +func RandFloat64N(max float64) float64 { + return float64(fastrand()) / (1 << 63) +} + +func RandFloat32N(max float32) float32 { + return float32(fastrand()) / (1 << 31) +} diff --git a/runtimes/rand_test.go b/runtimes/rand_test.go new file mode 100644 index 0000000..dc3142b --- /dev/null +++ b/runtimes/rand_test.go @@ -0,0 +1,20 @@ +package runtimes + +import ( + "fmt" + "testing" +) + +func TestRand(t *testing.T) { + for i := 0; i < 10; i++ { + fmt.Println(RandN(10)) + fmt.Println(Rand()) + } +} + +// BenchmarkRandMod-12 768897324 1.529 ns/op 0 B/op 0 allocs/op +func BenchmarkRandMod(b *testing.B) { + for i := 0; i < b.N; i++ { + RandN(100) + } +} diff --git a/runtimes/stack.go b/runtimes/stack.go new file mode 100644 index 0000000..1db1ed0 --- /dev/null +++ b/runtimes/stack.go @@ -0,0 +1,96 @@ +package runtimes + +import ( + "bytes" + "fmt" + "os" + "runtime" + "sync" +) + +var ( + dunno = []byte("???") + centerDot = []byte("·") + dot = []byte(".") + slash = []byte("/") + cache sync.Map +) + +func caller(skip int) (pc uintptr, file string, line int, ok bool) { + rpc := [1]uintptr{} + n := runtime.Callers(skip+1, rpc[:]) + if n < 1 { + return + } + var frame runtime.Frame + pc = rpc[0] + if item, ok := cache.Load(pc); ok { + frame = item.(runtime.Frame) + } else { + tmprpc := []uintptr{pc} + frame, _ = runtime.CallersFrames(tmprpc).Next() + cache.Store(pc, frame) + } + return frame.PC, frame.File, frame.Line, frame.PC != 0 +} + +// stack returns a nicely formatted stack frame, skipping skip frames. +func Stack(skip int) string { + buf := new(bytes.Buffer) // the returned data + // As we loop, we open files and read them. These variables record the currently + // loaded file. + var lines [][]byte + var lastFile string + for i := skip; ; i++ { // Skip the expected number of frames + pc, file, line, ok := caller(i) + if !ok { + break + } + // Print this much at least. If we can't find the source, it won't show. + fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) + if file != lastFile { + data, err := os.ReadFile(file) + if err != nil { + continue + } + lines = bytes.Split(data, []byte{'\n'}) + lastFile = file + } + fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line)) + } + return buf.String() +} + +// source returns a space-trimmed slice of the n'th line. +func source(lines [][]byte, n int) []byte { + n-- // in stack trace, lines are 1-indexed but our array is 0-indexed + if n < 0 || n >= len(lines) { + return dunno + } + return bytes.TrimSpace(lines[n]) +} + +// function returns, if possible, the name of the function containing the PC. +func function(pc uintptr) []byte { + fn := runtime.FuncForPC(pc) + if fn == nil { + return dunno + } + name := []byte(fn.Name()) + // The name includes the path name to the package, which is unnecessary + // since the file name is already included. Plus, it has center dots. + // That is, we see + // runtime/debug.*T·ptrmethod + // and want + // *T.ptrmethod + // Also the package path might contain dot (e.g. code.google.com/...), + // so first eliminate the path prefix + if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 { + name = name[lastSlash+1:] + } + if period := bytes.Index(name, dot); period >= 0 { + name = name[period+1:] + } + name = bytes.ReplaceAll(name, centerDot, dot) + return name +} From ef97a5918b3a92c53cba4abf210939832d1bb434 Mon Sep 17 00:00:00 2001 From: xiaqb Date: Mon, 5 Feb 2024 17:38:06 +0800 Subject: [PATCH 7/7] fixed: add test case --- chinese/idcard/area_test.go | 39 +++++++++++++++++++++++++++++++++++ chinese/idcard/idcard_test.go | 28 ++++++++++++------------- 2 files changed, 53 insertions(+), 14 deletions(-) create mode 100644 chinese/idcard/area_test.go diff --git a/chinese/idcard/area_test.go b/chinese/idcard/area_test.go new file mode 100644 index 0000000..5527e32 --- /dev/null +++ b/chinese/idcard/area_test.go @@ -0,0 +1,39 @@ +package idcard + +import ( + "testing" +) + +func TestRegisterCode(t *testing.T) { + type args struct { + data map[string]string + } + tests := []struct { + name string + args args + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + RegisterCode(tt.args.data) + }) + } +} + +func TestConcatCode(t *testing.T) { + type args struct { + data map[string]string + } + tests := []struct { + name string + args args + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ConcatCode(tt.args.data) + }) + } +} diff --git a/chinese/idcard/idcard_test.go b/chinese/idcard/idcard_test.go index ac19450..254864e 100644 --- a/chinese/idcard/idcard_test.go +++ b/chinese/idcard/idcard_test.go @@ -1,14 +1,14 @@ -package idcard - -import ( - "fmt" - "testing" -) - -func TestCheck(t *testing.T) { - for i := 0; i < 100; i++ { - var no = Generate(true) - var id = IdCard(no) - fmt.Println(id, id.Check(), id.Parse(), id.Age()) - } -} +package idcard + +import ( + "fmt" + "testing" +) + +func TestCheck(t *testing.T) { + for i := 0; i < 100; i++ { + var no = Generate(true) + var id = IdCard(no) + fmt.Println(id, id.Check(), id.Parse(), id.Age()) + } +}