244 lines
6.0 KiB
Go
244 lines
6.0 KiB
Go
package licence
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type jsonValueKind uint8
|
|
|
|
const (
|
|
jsonValueUnknown jsonValueKind = iota
|
|
jsonValueObject
|
|
jsonValueString
|
|
jsonValueNumber
|
|
)
|
|
|
|
func strictDecodeJSON(content []byte, destination any) error {
|
|
if !utf8.Valid(content) {
|
|
return errors.New("JSON 不是有效的 UTF-8")
|
|
}
|
|
|
|
structureDecoder := json.NewDecoder(bytes.NewReader(content))
|
|
structureDecoder.UseNumber()
|
|
if err := validateJSONValue(structureDecoder, nil); err != nil {
|
|
return err
|
|
}
|
|
if _, err := structureDecoder.Token(); !errors.Is(err, io.EOF) {
|
|
if err == nil {
|
|
return errors.New("JSON 包含多个顶层值")
|
|
}
|
|
return errors.New("JSON 尾部内容无效")
|
|
}
|
|
|
|
decoder := json.NewDecoder(bytes.NewReader(content))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(destination); err != nil {
|
|
return errors.New("JSON 包含未知字段或无效字段值")
|
|
}
|
|
var trailing any
|
|
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
|
|
if err == nil {
|
|
return errors.New("JSON 包含多个顶层值")
|
|
}
|
|
return errors.New("JSON 尾部内容无效")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateJSONValue(decoder *json.Decoder, path []string) error {
|
|
token, err := decoder.Token()
|
|
if err != nil {
|
|
return errors.New("JSON 结构不完整")
|
|
}
|
|
|
|
expectedKind := expectedJSONKind(path)
|
|
delimiter, isDelimiter := token.(json.Delim)
|
|
if isDelimiter {
|
|
switch delimiter {
|
|
case '{':
|
|
if expectedKind != jsonValueUnknown && expectedKind != jsonValueObject {
|
|
return fmt.Errorf("字段 %s 类型错误", displayJSONPath(path))
|
|
}
|
|
return validateJSONObject(decoder, path)
|
|
case '[':
|
|
if expectedKind != jsonValueUnknown {
|
|
return fmt.Errorf("字段 %s 类型错误", displayJSONPath(path))
|
|
}
|
|
for decoder.More() {
|
|
if err := validateJSONValue(decoder, path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
closing, err := decoder.Token()
|
|
if err != nil || closing != json.Delim(']') {
|
|
return errors.New("JSON 数组结构不完整")
|
|
}
|
|
return nil
|
|
default:
|
|
return errors.New("JSON 分隔符无效")
|
|
}
|
|
}
|
|
|
|
if expectedKind == jsonValueObject {
|
|
return fmt.Errorf("字段 %s 必须是对象", displayJSONPath(path))
|
|
}
|
|
switch expectedKind {
|
|
case jsonValueString:
|
|
if _, ok := token.(string); !ok {
|
|
return fmt.Errorf("字段 %s 必须是字符串", displayJSONPath(path))
|
|
}
|
|
case jsonValueNumber:
|
|
number, ok := token.(json.Number)
|
|
if !ok {
|
|
return fmt.Errorf("字段 %s 必须是整数", displayJSONPath(path))
|
|
}
|
|
if _, err := strconv.ParseInt(number.String(), 10, 64); err != nil {
|
|
return fmt.Errorf("字段 %s 必须是 int64 整数", displayJSONPath(path))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateJSONObject(decoder *json.Decoder, path []string) error {
|
|
seen := make(map[string]struct{})
|
|
allowedFields := requiredJSONFields(path)
|
|
for decoder.More() {
|
|
nameToken, err := decoder.Token()
|
|
if err != nil {
|
|
return errors.New("JSON 对象结构不完整")
|
|
}
|
|
name, ok := nameToken.(string)
|
|
if !ok {
|
|
return errors.New("JSON 属性名必须是字符串")
|
|
}
|
|
if allowedFields != nil && !containsJSONField(allowedFields, name) {
|
|
return fmt.Errorf("字段 %s 未知", displayJSONPath(appendJSONPath(path, name)))
|
|
}
|
|
if _, exists := seen[name]; exists {
|
|
return fmt.Errorf("字段 %s 重复", displayJSONPath(appendJSONPath(path, name)))
|
|
}
|
|
seen[name] = struct{}{}
|
|
|
|
if err := validateJSONValue(decoder, appendJSONPath(path, name)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
closing, err := decoder.Token()
|
|
if err != nil || closing != json.Delim('}') {
|
|
return errors.New("JSON 对象结构不完整")
|
|
}
|
|
|
|
for _, required := range allowedFields {
|
|
if _, exists := seen[required]; !exists {
|
|
return fmt.Errorf("缺少字段 %s", displayJSONPath(appendJSONPath(path, required)))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func containsJSONField(fields []string, candidate string) bool {
|
|
for _, field := range fields {
|
|
if field == candidate {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func expectedJSONKind(path []string) jsonValueKind {
|
|
switch len(path) {
|
|
case 0:
|
|
return jsonValueObject
|
|
case 1:
|
|
switch path[0] {
|
|
case "format_version":
|
|
return jsonValueNumber
|
|
case "licence", "signing_key_certificate":
|
|
return jsonValueObject
|
|
case "signature":
|
|
return jsonValueString
|
|
}
|
|
case 2:
|
|
if path[0] == "licence" {
|
|
switch path[1] {
|
|
case "id", "platform_name", "workspace", "issued_on", "valid_from", "expires_on", "signing_key_id":
|
|
return jsonValueString
|
|
case "quotas":
|
|
return jsonValueObject
|
|
}
|
|
}
|
|
if path[0] == "signing_key_certificate" {
|
|
switch path[1] {
|
|
case "format_version":
|
|
return jsonValueNumber
|
|
case "key_id", "public_key", "root_signature":
|
|
return jsonValueString
|
|
}
|
|
}
|
|
case 3:
|
|
if path[0] == "licence" && path[1] == "quotas" {
|
|
for _, name := range quotaJSONFields() {
|
|
if path[2] == name {
|
|
return jsonValueNumber
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return jsonValueUnknown
|
|
}
|
|
|
|
func requiredJSONFields(path []string) []string {
|
|
switch {
|
|
case len(path) == 0:
|
|
return []string{"format_version", "licence", "signing_key_certificate", "signature"}
|
|
case len(path) == 1 && path[0] == "licence":
|
|
return []string{"id", "platform_name", "workspace", "issued_on", "valid_from", "expires_on", "quotas", "signing_key_id"}
|
|
case len(path) == 2 && path[0] == "licence" && path[1] == "quotas":
|
|
return quotaJSONFields()
|
|
case len(path) == 1 && path[0] == "signing_key_certificate":
|
|
return []string{"format_version", "key_id", "public_key", "root_signature"}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func quotaJSONFields() []string {
|
|
return []string{
|
|
"max_database",
|
|
"max_middleware",
|
|
"max_network_device",
|
|
"max_security",
|
|
"max_storage",
|
|
"max_pc",
|
|
"max_server",
|
|
"max_user",
|
|
"max_role",
|
|
"max_permission",
|
|
"max_menu",
|
|
}
|
|
}
|
|
|
|
func appendJSONPath(path []string, name string) []string {
|
|
result := make([]string, len(path)+1)
|
|
copy(result, path)
|
|
result[len(path)] = name
|
|
return result
|
|
}
|
|
|
|
func displayJSONPath(path []string) string {
|
|
if len(path) == 0 {
|
|
return "顶层"
|
|
}
|
|
result := path[0]
|
|
for _, segment := range path[1:] {
|
|
result += "." + segment
|
|
}
|
|
return result
|
|
}
|