Files
fun/check.go
T
chiyi 41df889a1a v2.0.0: error-channel APIs, correctness fixes, and server hardening
BREAKING CHANGES:
- Guard interface: Guard(ctx Ctx) error; returning error short-circuits
  (subsequent guards and the method no longer execute)
- Wired[T]() (*T, error); New() may return error; failed wiring is sticky
- BindService/BindGuard/BindRoute return error; routes accept guards
  (guards receive merged query/form params as Ctx.State)
- registration panics after Start; Ctx.Ip honors X-Forwarded-For/X-Real-IP
- internal errors sanitized to fixed client messages

FIXES:
- int64 precision loss: /cell data decoded from raw JSON bytes and
  responses serialized with json.Number (no float64 round-trip)
- enum values range-checked before uint8 conversion (256 no longer
  truncates to 0 and slips through)
- logger: files failing name parsing are no longer deleted; log channel
  never blocks request goroutines; ConfigLogger is race-free;
  logWriterWorker unlock bug fixed
- stream writer panics recovered (process no longer crashes);
  streamDone closed exactly once
- generated Go client emits definitions for pointer-to-enum/struct fields
- anonymous structs rejected at registration; isPrivate safe on empty names
- removed dead Result.Id and RequestInfo.Type

ADDITIONS:
- graceful shutdown (Shutdown), StartOn, server timeouts by default
  (Read 60s/Idle 120s/Write off), SetTimeouts/SetMaxConcurrency
- big-int-safe JSON in the generated TS client (>2^53 as BigInt)
- example/ demo services and cmd/genexample artifact generator
2026-09-03 15:32:07 +08:00

211 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package fun
import (
"errors"
"fmt"
"math"
"reflect"
"strings"
"unicode"
)
func isPrivate(value string) bool {
if value == "" {
return false // 匿名类型名:交由各处的具名校验给出明确报错,不在此越界 panic
}
return !unicode.IsUpper([]rune(value)[0])
}
// checkType 注册期递归校验类型是否受支持:
// int/uint/string/bool/struct/slice/enum;不支持匿名结构体、私有类型、空结构体;
// 枚举要求 Names/DisplayNames 长度一致
func checkType(t reflect.Type) {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if strings.Contains(t.String(), "{}") {
panic(fmt.Sprintf("fun: %s generic types containing 'any' or interface{} are not supported", t.Name()))
}
switch t.Kind() {
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.String, reflect.Bool:
if t.Kind() == reflect.Uint8 && (t.Implements(displayEnumType) || t.Implements(enumType)) && isPrivate(t.Name()) {
panic("fun:" + t.Name() + " cannot be Private")
}
if t.Kind() == reflect.Uint8 && t.Implements(displayEnumType) {
enumValue := reflect.New(t).Elem().Interface().(displayEnum)
if len(enumValue.DisplayNames()) != len(enumValue.Names()) {
panic("fun: " + t.Name() + " enum names and display names must be the same length")
}
}
case reflect.Struct:
if t.Name() == "" {
panic("fun: anonymous struct types are not supported, define a named type")
}
if t.NumField() == 0 {
panic("fun: " + t.Name() + " must have at least one field")
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if isPrivate(f.Name) {
panic("fun:" + f.Name + " cannot be Private")
}
checkType(f.Type)
}
case reflect.Slice:
checkType(t.Elem())
default:
name := t.Name()
if name == "" {
name = t.String() // 匿名/内建组合类型(如 interface{})给出可读的名字
}
panic("fun:Unsupported types " + name)
}
}
// checkDto 运行时校验请求数据:
// 非指针字段必须出现在请求中且非 nil;嵌套 struct/slice 递归;枚举值必须在范围内
func checkDto(dtoType reflect.Type, dtoMap any, methodName string) error {
t := dtoType
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
switch t.Kind() {
case reflect.Struct:
obj, ok := dtoMap.(map[string]any)
if !ok {
return callError(fmt.Errorf("fun: method %s DTO %s must be an object", methodName, t.Name()))
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
value, ok := obj[firstLetterToLower(f.Name)]
if !ok {
// 兼容按原始字段名(首字母大写)传参的客户端
value, ok = obj[f.Name]
}
// 非指针且非切片字段必须存在且非 null;切片字段允许 null(反序列化为 nil slice
if f.Type.Kind() != reflect.Ptr && f.Type.Kind() != reflect.Slice && (!ok || value == nil) {
return callError(fmt.Errorf("fun: %s Dto must be a pointer or have a corresponding field in the map", f.Name))
}
ft := f.Type
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
if (ft.Kind() == reflect.Struct || ft.Kind() == reflect.Slice) && value != nil {
if err := checkDto(ft, value, methodName); err != nil {
return err
}
}
if ft.Kind() == reflect.Uint8 && value != nil && (ft.Implements(displayEnumType) || ft.Implements(enumType)) {
if err := checkEnumValue(ft, value, f.Name); err != nil {
return err
}
}
}
case reflect.Slice:
list, ok := dtoMap.([]any)
if !ok {
return callError(fmt.Errorf("fun: Dto must be an array"))
}
for _, value := range list {
et0 := t.Elem()
et := et0
if et.Kind() == reflect.Ptr {
et = et.Elem()
}
// 指针元素允许 null,值元素必须非空
if et0.Kind() != reflect.Ptr && value == nil {
return callError(fmt.Errorf("fun:%s Dto must be a pointer or have a corresponding field in the map", et0.Name()))
}
if (et.Kind() == reflect.Struct || et.Kind() == reflect.Slice) && value != nil {
if err := checkDto(et, value, methodName); err != nil {
return err
}
}
if et.Kind() == reflect.Uint8 && value != nil && (et.Implements(displayEnumType) || et.Implements(enumType)) {
if err := checkEnumValue(et, value, et.Name()); err != nil {
return err
}
}
}
}
return nil
}
// checkEnumValue 运行时校验枚举值是否在范围内。
// 必须在原始数值上先判范围再转 uint8:否则 256/512 等越界值先被截断成
// 合法小值(256→0),绕过范围检查后静默落到错误的枚举项上
func checkEnumValue(t reflect.Type, value any, name string) error {
var max uint8
enumValue := reflect.New(t).Elem()
if t.Implements(displayEnumType) {
max = uint8(len(enumValue.Interface().(displayEnum).Names()))
} else {
max = uint8(len(enumValue.Interface().(enum).Names()))
}
outOfRange := callError(errors.New("Fun:" + name + " Dto value out of range"))
var num uint8
switch v := value.(type) {
case float64:
if v < 0 || v != math.Trunc(v) || v >= float64(max) {
return outOfRange
}
num = uint8(v)
case float32:
f := float64(v)
if f < 0 || f != math.Trunc(f) || f >= float64(max) {
return outOfRange
}
num = uint8(f)
case uint8:
num = v
case uint16:
if v >= uint16(max) {
return outOfRange
}
num = uint8(v)
case uint32:
if v >= uint32(max) {
return outOfRange
}
num = uint8(v)
case uint64:
if v >= uint64(max) {
return outOfRange
}
num = uint8(v)
case int:
if v < 0 || v >= int(max) {
return outOfRange
}
num = uint8(v)
case int8:
if v < 0 || int(v) >= int(max) {
return outOfRange
}
num = uint8(v)
case int16:
if v < 0 || int(v) >= int(max) {
return outOfRange
}
num = uint8(v)
case int32:
if v < 0 || int64(v) >= int64(max) {
return outOfRange
}
num = uint8(v)
case int64:
if v < 0 || v >= int64(max) {
return outOfRange
}
num = uint8(v)
default:
return callError(errors.New("Fun:" + name + " Dto enum value type is not supported"))
}
if num >= max {
return outOfRange
}
return nil
}