1 Star 0 Fork 0

hxchjm / erpc

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
binder.go 21.09 KB
一键复制 编辑 原始数据 按行查看 历史
andeyalee 提交于 2019-11-14 16:48 . chore: rename repo to erpc
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812
// Package binder is Parameter Binding Verification Plugin for Struct Handler.
//
// Copyright 2018 HenryLee. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package binder
import (
"errors"
"fmt"
"math"
"reflect"
"regexp"
"strconv"
"strings"
"github.com/henrylee2cn/erpc/v6"
"github.com/henrylee2cn/erpc/v6/utils"
"github.com/henrylee2cn/goutil"
)
/**
Parameter Binding Verification Plugin for Struct Handler.
- Param-Tags
tag | key | required | value | desc
------|----------|----------|---------------|----------------------------------
param | meta | no | (name e.g.`param:"<meta:id>"`) | It indicates that the parameter is from the meta.
param | swap | no | name (e.g.`param:"<swap:id>"`) | It indicates that the parameter is from the context swap.
param | desc | no | (e.g.`param:"<desc:id>"`) | Parameter Description
param | len | no | (e.g.`param:"<len:3:6>"`) | Length range [a,b] of parameter's value
param | range | no | (e.g.`param:"<range:0:10>"`) | Numerical range [a,b] of parameter's value
param | nonzero | no | - | Not allowed to zero
param | regexp | no | (e.g.`param:"<regexp:^\\w+$>"`) | Regular expression validation
param | stat | no |(e.g.`param:"<stat:100002:wrong password format>"`)| Custom error code and message
NOTES:
* `param:"-"` means ignore
* Encountered untagged exportable anonymous structure field, automatic recursive resolution
* Parameter name is the name of the structure field converted to snake format
* If the parameter is not from `meta` or `swap`, it is the default from the body
- Field-Types
base | slice | special
--------|------------|------------
string | []string | [][]byte
byte | []byte | [][]uint8
uint8 | []uint8 | struct
bool | []bool |
int | []int |
int8 | []int8 |
int16 | []int16 |
int32 | []int32 |
int64 | []int64 |
uint8 | []uint8 |
uint16 | []uint16 |
uint32 | []uint32 |
uint64 | []uint64 |
float32 | []float32 |
float64 | []float64 |
*/
type (
// StructArgsBinder a plugin that binds and validates structure type parameters.
StructArgsBinder struct {
binders map[string]*Params
errFunc ErrorFunc
}
// ErrorFunc creates an relational error.
ErrorFunc func(handlerName, paramName, reason string) *erpc.Status
)
var (
_ erpc.PostRegPlugin = new(StructArgsBinder)
_ erpc.PostReadCallBodyPlugin = new(StructArgsBinder)
)
// NewStructArgsBinder creates a plugin that binds and validates structure type parameters.
func NewStructArgsBinder(fn ErrorFunc) *StructArgsBinder {
s := &StructArgsBinder{
binders: make(map[string]*Params),
errFunc: fn,
}
s.SetErrorFunc(fn)
return s
}
var (
_ erpc.PostRegPlugin = new(StructArgsBinder)
_ erpc.PostReadCallBodyPlugin = new(StructArgsBinder)
)
// SetErrorFunc sets the binding or balidating error function.
// NOTE: If fn=nil, set as default.
func (s *StructArgsBinder) SetErrorFunc(fn ErrorFunc) {
if fn != nil {
s.errFunc = fn
return
}
s.errFunc = func(handlerName, paramName, reason string) *erpc.Status {
return erpc.NewStatus(
erpc.CodeBadMessage,
"Invalid Parameter",
fmt.Sprintf(`{"handler": %q, "param": %q, "reason": %q}`, handlerName, paramName, reason),
)
}
}
// Name returns the plugin name.
func (*StructArgsBinder) Name() string {
return "StructArgsBinder"
}
// PostReg preprocessing struct handler.
func (s *StructArgsBinder) PostReg(h *erpc.Handler) error {
if h.ArgElemType().Kind() != reflect.Struct {
return nil
}
params := newParams(h.Name(), s)
err := params.addFields([]int{}, h.ArgElemType(), h.NewArgValue().Elem())
if err != nil {
erpc.Fatalf("%v", err)
}
s.binders[h.Name()] = params
return nil
}
// PostReadCallBody binds and validates the registered struct handler.
func (s *StructArgsBinder) PostReadCallBody(ctx erpc.ReadCtx) *erpc.Status {
params, ok := s.binders[ctx.ServiceMethod()]
if !ok {
return nil
}
bodyValue := reflect.ValueOf(ctx.Input().Body())
stat := params.bindAndValidate(bodyValue, ctx.CopyMeta(), ctx.Swap())
if !stat.OK() {
return stat
}
return nil
}
// Params struct handler information for binding and validation
type Params struct {
handlerName string
params []*Param
binder *StructArgsBinder
}
// struct binder parameters'tag
const (
TAG_PARAM = "param" // request param tag name
TAG_IGNORE_PARAM = "-" // ignore request param tag value
KEY_META = "meta" // meta param(optional), value means parameter(optional)
KEY_SWAP = "swap" // swap param from the context swap(ctx.Swap()) (optional), value means parameter(optional)
KEY_DESC = "desc" // request param description
KEY_LEN = "len" // length range of param's value
KEY_RANGE = "range" // numerical range of param's value
KEY_NONZERO = "nonzero" // param`s value can not be zero
KEY_REGEXP = "regexp" // verify the value of the param with a regular expression(param value can not be null)
KEY_RERR = "stat" // the custom error code and message for binding or validating
)
func newParams(handlerName string, binder *StructArgsBinder) *Params {
return &Params{
handlerName: handlerName,
params: make([]*Param, 0),
binder: binder,
}
}
func (p *Params) addFields(parentIndexPath []int, t reflect.Type, v reflect.Value) error {
var err error
var deep = len(parentIndexPath) + 1
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
for i := 0; i < t.NumField(); i++ {
indexPath := make([]int, deep)
copy(indexPath, parentIndexPath)
indexPath[deep-1] = i
var field = t.Field(i)
var value = v.Field(i)
canSet := v.Field(i).CanSet()
tag, ok := field.Tag.Lookup(TAG_PARAM)
if !ok {
if canSet && field.Anonymous {
if field.Type.Kind() == reflect.Struct {
if err = p.addFields(indexPath, field.Type, value); err != nil {
return err
}
} else {
return fmt.Errorf("%s.%s anonymous field can only be struct type", t.String(), field.Name)
}
}
continue
}
if tag == TAG_IGNORE_PARAM {
continue
}
if !canSet {
return fmt.Errorf("%s.%s can not be a non-settable field", t.String(), field.Name)
}
if field.Type.Kind() == reflect.Ptr {
return fmt.Errorf("%s.%s can not be a pointer field", t.String(), field.Name)
}
var parsedTags = parseTags(tag)
var paramTypeString = field.Type.String()
var kind = field.Type.Kind()
if _, ok := parsedTags[KEY_LEN]; ok {
if kind != reflect.String && kind != reflect.Slice && kind != reflect.Map && kind != reflect.Array {
return fmt.Errorf("%s.%s invalid `len` tag for field value", t.String(), field.Name)
}
}
if _, ok := parsedTags[KEY_RANGE]; ok {
switch paramTypeString {
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "float32", "float64":
case "[]int", "[]int8", "[]int16", "[]int32", "[]int64", "[]uint", "[]uint8", "[]uint16", "[]uint32", "[]uint64", "[]float32", "[]float64":
default:
return fmt.Errorf("%s.%s invalid `range` tag for non-number field", t.String(), field.Name)
}
}
if _, ok := parsedTags[KEY_REGEXP]; ok {
if paramTypeString != "string" && paramTypeString != "[]string" {
return fmt.Errorf("%s.%s invalid `regexp` tag for non-string field", t.String(), field.Name)
}
}
fd := &Param{
handlerName: p.handlerName,
indexPath: indexPath,
tags: parsedTags,
rawTag: field.Tag,
rawValue: value,
binder: p.binder,
}
statTag, ok := fd.tags[KEY_RERR]
if ok {
idx := strings.Index(statTag, ":")
if idx != -1 {
if codeStr := strings.TrimSpace(statTag[:idx]); len(codeStr) > 0 {
statCode, err := strconv.Atoi(codeStr)
if err == nil {
fd.statCode = int32(statCode)
} else {
return fmt.Errorf("%s.%s invalid `stat` tag (correct example: `<stat: 100001: Invalid Parameter>`)", t.String(), field.Name)
}
}
fd.statMsg = strings.TrimSpace(statTag[idx+1:])
} else {
return fmt.Errorf("%s.%s invalid `stat` tag (correct example: `<stat: 100001: Invalid Parameter>`)", t.String(), field.Name)
}
}
if fd.name, ok = parsedTags[KEY_META]; ok {
fd.position = KEY_META
} else if fd.name, ok = parsedTags[KEY_SWAP]; ok {
fd.position = KEY_SWAP
}
if fd.name == "" {
fd.name = goutil.SnakeString(field.Name)
}
if err = fd.makeVerifyFuncs(); err != nil {
return fmt.Errorf("%s.%s invalid validation failed: %s", t.String(), field.Name, err.Error())
}
p.params = append(p.params, fd)
}
return nil
}
func (p *Params) fieldsForBinding(structElem reflect.Value) []reflect.Value {
count := len(p.params)
fields := make([]reflect.Value, count)
for i := 0; i < count; i++ {
value := structElem
param := p.params[i]
for _, index := range param.indexPath {
value = value.Field(index)
}
fields[i] = value
}
return fields
}
func (p *Params) bindAndValidate(structValue reflect.Value, meta *utils.Args, swap goutil.Map) (stat *erpc.Status) {
defer func() {
if r := recover(); r != nil {
stat = p.binder.errFunc(p.handlerName, "", fmt.Sprint(r))
}
}()
var (
err error
fields = p.fieldsForBinding(reflect.Indirect(structValue))
)
for i, param := range p.params {
value := fields[i]
// bind meta or swap param
switch param.position {
case KEY_META:
paramValues := meta.PeekMulti(param.name)
if len(paramValues) > 0 {
if err = convertAssign(value, toSliceString(paramValues)); err != nil {
return param.fixStatus(p.binder.errFunc(param.handlerName, param.name, err.Error()))
}
}
case KEY_SWAP:
paramValue, ok := swap.Load(param.name)
if ok {
value = reflect.Indirect(value)
canSet := value.CanSet()
var srcValue reflect.Value
if canSet {
srcValue = reflect.Indirect(reflect.ValueOf(paramValue))
destType := value.Type()
srcType := srcValue.Type()
canSet = srcType.AssignableTo(destType)
if !canSet {
if srcType.ConvertibleTo(destType) {
srcValue = srcValue.Convert(destType)
canSet = srcValue.Type().AssignableTo(destType)
}
}
}
if !canSet {
return param.fixStatus(p.binder.errFunc(
param.handlerName,
param.name,
value.Type().Name()+" can not be setted"),
)
}
value.Set(srcValue)
}
}
if stat = param.validate(value); !stat.OK() {
return stat
}
}
return
}
func toSliceString(b [][]byte) []string {
if len(b) == 0 {
return nil
}
a := make([]string, len(b))
for k, v := range b {
a[k] = goutil.BytesToString(v)
}
return a
}
// parseTags returns the key-value in the tag string.
// If the tag does not have the conventional format,
// the value returned by parseTags is unspecified.
func parseTags(tag string) map[string]string {
var values = map[string]string{}
for tag != "" {
// Skip leading space.
i := 0
for i < len(tag) && tag[i] != '<' {
i++
}
if i >= len(tag) || tag[i] != '<' {
break
}
i++
// Skip the left Spaces
for i < len(tag) && tag[i] == ' ' {
i++
}
if i >= len(tag) {
break
}
tag = tag[i:]
if tag == "" {
break
}
var name, value string
var hadName bool
i = 0
PAIR:
for i < len(tag) {
switch tag[i] {
case ':':
if hadName {
i++
continue
}
name = strings.TrimRight(tag[:i], " ")
tag = strings.TrimLeft(tag[i+1:], " ")
hadName = true
i = 0
case '\\':
i++
// Fix the escape character of `\\<` or `\\>`
if tag[i] == '<' || tag[i] == '>' {
tag = tag[:i-1] + tag[i:]
} else {
i++
}
case '>':
if !hadName {
name = strings.TrimRight(tag[:i], " ")
} else {
value = strings.TrimRight(tag[:i], " ")
}
values[name] = value
break PAIR
default:
i++
}
}
if i >= len(tag) {
break
}
tag = tag[i+1:]
}
return values
}
// Param use the struct field to define a request parameter model
type Param struct {
handlerName string // handler name
name string // param name
indexPath []int
position string // param position
tags map[string]string // struct tags for this param
verifyFuncs []func(reflect.Value) error
rawTag reflect.StructTag // the raw tag
rawValue reflect.Value // the raw tag value
statCode int32 // the custom error code for binding or validating
statMsg string // the custom error message for binding or validating
binder *StructArgsBinder
}
const (
stringTypeString = "string"
bytesTypeString = "[]byte"
bytes2TypeString = "[]uint8"
)
// Raw gets the param's original value
func (param *Param) Raw() interface{} {
return param.rawValue.Interface()
}
// Name gets parameter field name
func (param *Param) Name() string {
return param.name
}
// Description gets the description value for the param
func (param *Param) Description() string {
return param.tags[KEY_DESC]
}
// validate tests if the param conforms to it's validation constraints specified
// int the KEY_REGEXP struct tag
func (param *Param) validate(value reflect.Value) (stat *erpc.Status) {
defer func() {
if r := recover(); r != nil {
stat = param.fixStatus(param.binder.errFunc(param.handlerName, param.name, fmt.Sprint(r)))
}
}()
var err error
for _, fn := range param.verifyFuncs {
if err = fn(value); err != nil {
return param.fixStatus(param.binder.errFunc(param.handlerName, param.name, err.Error()))
}
}
return nil
}
func (param *Param) makeVerifyFuncs() (err error) {
defer func() {
p := recover()
if p != nil {
err = fmt.Errorf("%v", p)
}
}()
// length
if tuple, ok := param.tags[KEY_LEN]; ok {
if fn, err := validateLen(tuple); err == nil {
param.verifyFuncs = append(param.verifyFuncs, fn)
} else {
return err
}
}
// range
if tuple, ok := param.tags[KEY_RANGE]; ok {
if fn, err := validateRange(tuple); err == nil {
param.verifyFuncs = append(param.verifyFuncs, fn)
} else {
return err
}
}
// nonzero
if _, ok := param.tags[KEY_NONZERO]; ok {
if fn, err := validateNonZero(); err == nil {
param.verifyFuncs = append(param.verifyFuncs, fn)
} else {
return err
}
}
// regexp
if reg, ok := param.tags[KEY_REGEXP]; ok {
var isStrings = param.rawValue.Kind() == reflect.Slice
if fn, err := validateRegexp(isStrings, reg); err == nil {
param.verifyFuncs = append(param.verifyFuncs, fn)
} else {
return err
}
}
return
}
func parseTuple(tuple string) (string, string) {
c := strings.Split(tuple, ":")
var a, b string
switch len(c) {
case 1:
a = c[0]
if len(a) > 0 {
return a, a
}
case 2:
a = c[0]
b = c[1]
if len(a) > 0 || len(b) > 0 {
return a, b
}
}
panic("invalid validation tuple")
}
func validateNonZero() (func(value reflect.Value) error, error) {
return func(value reflect.Value) error {
obj := value.Interface()
if obj == reflect.Zero(value.Type()).Interface() {
return errors.New("zero value")
}
return nil
}, nil
}
func validateLen(tuple string) (func(value reflect.Value) error, error) {
var a, b = parseTuple(tuple)
var min, max int
var err error
if len(a) > 0 {
min, err = strconv.Atoi(a)
if err != nil {
return nil, err
}
}
if len(b) > 0 {
max, err = strconv.Atoi(b)
if err != nil {
return nil, err
}
}
return func(value reflect.Value) error {
length := value.Len()
if len(a) > 0 {
if length < min {
return fmt.Errorf("shorter than %s: %v", a, value.Interface())
}
}
if len(b) > 0 {
if length > max {
return fmt.Errorf("longer than %s: %v", b, value.Interface())
}
}
return nil
}, nil
}
const accuracy = 0.0000001
func validateRange(tuple string) (func(value reflect.Value) error, error) {
var a, b = parseTuple(tuple)
var min, max float64
var err error
if len(a) > 0 {
min, err = strconv.ParseFloat(a, 64)
if err != nil {
return nil, err
}
}
if len(b) > 0 {
max, err = strconv.ParseFloat(b, 64)
if err != nil {
return nil, err
}
}
return func(value reflect.Value) error {
var f64 float64
switch value.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
f64 = float64(value.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
f64 = float64(value.Uint())
case reflect.Float32, reflect.Float64:
f64 = value.Float()
}
if len(a) > 0 {
if math.Min(f64, min) == f64 && math.Abs(f64-min) > accuracy {
return fmt.Errorf("smaller than %s: %v", a, value.Interface())
}
}
if len(b) > 0 {
if math.Max(f64, max) == f64 && math.Abs(f64-max) > accuracy {
return fmt.Errorf("bigger than %s: %v", b, value.Interface())
}
}
return nil
}, nil
}
func validateRegexp(isStrings bool, reg string) (func(value reflect.Value) error, error) {
re, err := regexp.Compile(reg)
if err != nil {
return nil, err
}
if !isStrings {
return func(value reflect.Value) error {
s := value.String()
if !re.MatchString(s) {
return fmt.Errorf("not match %s: %s", reg, s)
}
return nil
}, nil
} else {
return func(value reflect.Value) error {
for _, s := range value.Interface().([]string) {
if !re.MatchString(s) {
return fmt.Errorf("not match %s: %s", reg, s)
}
}
return nil
}, nil
}
}
func (param *Param) fixStatus(stat *erpc.Status) *erpc.Status {
if param.statMsg != "" {
stat.SetMsg(param.statMsg)
}
if param.statCode != 0 {
stat.SetCode(param.statCode)
}
return stat
}
func convertAssign(dest reflect.Value, src []string) (err error) {
if len(src) == 0 {
return nil
}
dest = reflect.Indirect(dest)
if !dest.CanSet() {
return fmt.Errorf("%s can not be setted", dest.Type().Name())
}
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("%v", p)
}
}()
switch dest.Interface().(type) {
case string:
dest.Set(reflect.ValueOf(src[0]))
return nil
case []string:
dest.Set(reflect.ValueOf(src))
return nil
case []byte:
dest.Set(reflect.ValueOf([]byte(src[0])))
return nil
case [][]byte:
b := make([][]byte, 0, len(src))
for _, s := range src {
b = append(b, []byte(s))
}
dest.Set(reflect.ValueOf(b))
return nil
case bool:
dest.Set(reflect.ValueOf(parseBool(src[0])))
return nil
case []bool:
b := make([]bool, 0, len(src))
for _, s := range src {
b = append(b, parseBool(s))
}
dest.Set(reflect.ValueOf(b))
return nil
}
switch dest.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
i64, err := strconv.ParseInt(src[0], 10, dest.Type().Bits())
if err != nil {
err = strconvErr(err)
return fmt.Errorf("converting type %T (%q) to a %s: %v", src, src[0], dest.Kind(), err)
}
dest.SetInt(i64)
return nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
u64, err := strconv.ParseUint(src[0], 10, dest.Type().Bits())
if err != nil {
err = strconvErr(err)
return fmt.Errorf("converting type %T (%q) to a %s: %v", src, src[0], dest.Kind(), err)
}
dest.SetUint(u64)
return nil
case reflect.Float32, reflect.Float64:
f64, err := strconv.ParseFloat(src[0], dest.Type().Bits())
if err != nil {
err = strconvErr(err)
return fmt.Errorf("converting type %T (%q) to a %s: %v", src, src[0], dest.Kind(), err)
}
dest.SetFloat(f64)
return nil
case reflect.Slice:
member := dest.Type().Elem()
switch member.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
for _, s := range src {
i64, err := strconv.ParseInt(s, 10, member.Bits())
if err != nil {
err = strconvErr(err)
return fmt.Errorf("converting type %T (%q) to a %s: %v", src, s, dest.Kind(), err)
}
dest.Set(reflect.Append(dest, reflect.ValueOf(i64).Convert(member)))
}
return nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
for _, s := range src {
u64, err := strconv.ParseUint(s, 10, member.Bits())
if err != nil {
err = strconvErr(err)
return fmt.Errorf("converting type %T (%q) to a %s: %v", src, s, dest.Kind(), err)
}
dest.Set(reflect.Append(dest, reflect.ValueOf(u64).Convert(member)))
}
return nil
case reflect.Float32, reflect.Float64:
for _, s := range src {
f64, err := strconv.ParseFloat(s, member.Bits())
if err != nil {
err = strconvErr(err)
return fmt.Errorf("converting type %T (%q) to a %s: %v", src, s, dest.Kind(), err)
}
dest.Set(reflect.Append(dest, reflect.ValueOf(f64).Convert(member)))
}
return nil
}
}
return fmt.Errorf("unsupported storing type %T into type %s", src, dest.Kind())
}
func parseBool(val string) bool {
switch strings.TrimSpace(strings.ToLower(val)) {
case "true", "on", "1":
return true
}
return false
}
func strconvErr(err error) error {
if ne, ok := err.(*strconv.NumError); ok {
return ne.Err
}
return err
}
1
https://gitee.com/hxchjm/erpc.git
git@gitee.com:hxchjm/erpc.git
hxchjm
erpc
erpc
18ecc239cc6a

搜索帮助