| // Copyright 2021 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| //go:build goexperiment.jsonv2 |
| |
| package json |
| |
| import ( |
| "cmp" |
| "errors" |
| "fmt" |
| "io" |
| "reflect" |
| "slices" |
| "strconv" |
| "strings" |
| "unicode" |
| "unicode/utf8" |
| |
| "encoding/json/internal/jsonflags" |
| "encoding/json/internal/jsonwire" |
| ) |
| |
| type isZeroer interface { |
| IsZero() bool |
| } |
| |
| var isZeroerType = reflect.TypeFor[isZeroer]() |
| |
| type structFields struct { |
| flattened []structField // listed in depth-first ordering |
| byActualName map[string]*structField |
| byFoldedName map[string][]*structField |
| inlinedFallback *structField |
| } |
| |
| // reindex recomputes index to avoid bounds check during runtime. |
| // |
| // During the construction of each [structField] in [makeStructFields], |
| // the index field is 0-indexed. However, before it returns, |
| // the 0th field is stored in index0 and index stores the remainder. |
| func (sf *structFields) reindex() { |
| reindex := func(f *structField) { |
| f.index0 = f.index[0] |
| f.index = f.index[1:] |
| if len(f.index) == 0 { |
| f.index = nil // avoid pinning the backing slice |
| } |
| } |
| for i := range sf.flattened { |
| reindex(&sf.flattened[i]) |
| } |
| if sf.inlinedFallback != nil { |
| reindex(sf.inlinedFallback) |
| } |
| } |
| |
| // lookupByFoldedName looks up name by a case-insensitive match |
| // that also ignores the presence of dashes and underscores. |
| func (fs *structFields) lookupByFoldedName(name []byte) []*structField { |
| return fs.byFoldedName[string(foldName(name))] |
| } |
| |
| type structField struct { |
| id int // unique numeric ID in breadth-first ordering |
| index0 int // 0th index into a struct according to [reflect.Type.FieldByIndex] |
| index []int // 1st index and remainder according to [reflect.Type.FieldByIndex] |
| typ reflect.Type |
| fncs *arshaler |
| isZero func(addressableValue) bool |
| isEmpty func(addressableValue) bool |
| fieldOptions |
| } |
| |
| var errNoExportedFields = errors.New("Go struct has no exported fields") |
| |
| func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError) { |
| orErrorf := func(serr *SemanticError, t reflect.Type, f string, a ...any) *SemanticError { |
| return cmp.Or(serr, &SemanticError{GoType: t, Err: fmt.Errorf(f, a...)}) |
| } |
| |
| // Setup a queue for a breath-first search. |
| var queueIndex int |
| type queueEntry struct { |
| typ reflect.Type |
| index []int |
| visitChildren bool // whether to recursively visit inlined field in this struct |
| } |
| queue := []queueEntry{{root, nil, true}} |
| seen := map[reflect.Type]bool{root: true} |
| |
| // Perform a breadth-first search over all reachable fields. |
| // This ensures that len(f.index) will be monotonically increasing. |
| var allFields, inlinedFallbacks []structField |
| for queueIndex < len(queue) { |
| qe := queue[queueIndex] |
| queueIndex++ |
| |
| t := qe.typ |
| inlinedFallbackIndex := -1 // index of last inlined fallback field in current struct |
| namesIndex := make(map[string]int) // index of each field with a given JSON object name in current struct |
| var hasAnyJSONTag bool // whether any Go struct field has a `json` tag |
| var hasAnyJSONField bool // whether any JSON serializable fields exist in current struct |
| for i := range t.NumField() { |
| sf := t.Field(i) |
| _, hasTag := sf.Tag.Lookup("json") |
| hasAnyJSONTag = hasAnyJSONTag || hasTag |
| options, ignored, err := parseFieldOptions(sf) |
| if err != nil { |
| serr = cmp.Or(serr, &SemanticError{GoType: t, Err: err}) |
| } |
| if ignored { |
| continue |
| } |
| hasAnyJSONField = true |
| f := structField{ |
| // Allocate a new slice (len=N+1) to hold both |
| // the parent index (len=N) and the current index (len=1). |
| // Do this to avoid clobbering the memory of the parent index. |
| index: append(append(make([]int, 0, len(qe.index)+1), qe.index...), i), |
| typ: sf.Type, |
| fieldOptions: options, |
| } |
| if sf.Anonymous && !f.hasName { |
| if indirectType(f.typ).Kind() != reflect.Struct { |
| serr = orErrorf(serr, t, "embedded Go struct field %s of non-struct type must be explicitly given a JSON name", sf.Name) |
| } else { |
| f.inline = true // implied by use of Go embedding without an explicit name |
| } |
| } |
| if f.inline || f.unknown { |
| // Handle an inlined field that serializes to/from |
| // zero or more JSON object members. |
| |
| switch f.fieldOptions { |
| case fieldOptions{name: f.name, quotedName: f.quotedName, inline: true}: |
| case fieldOptions{name: f.name, quotedName: f.quotedName, unknown: true}: |
| case fieldOptions{name: f.name, quotedName: f.quotedName, inline: true, unknown: true}: |
| serr = orErrorf(serr, t, "Go struct field %s cannot have both `inline` and `unknown` specified", sf.Name) |
| f.inline = false // let `unknown` take precedence |
| default: |
| serr = orErrorf(serr, t, "Go struct field %s cannot have any options other than `inline` or `unknown` specified", sf.Name) |
| if f.hasName { |
| continue // invalid inlined field; treat as ignored |
| } |
| f.fieldOptions = fieldOptions{name: f.name, quotedName: f.quotedName, inline: f.inline, unknown: f.unknown} |
| if f.inline && f.unknown { |
| f.inline = false // let `unknown` take precedence |
| } |
| } |
| |
| // Reject any types with custom serialization otherwise |
| // it becomes impossible to know what sub-fields to inline. |
| tf := indirectType(f.typ) |
| if implementsAny(tf, allMethodTypes...) && tf != jsontextValueType { |
| serr = orErrorf(serr, t, "inlined Go struct field %s of type %s must not implement marshal or unmarshal methods", sf.Name, tf) |
| } |
| |
| // Handle an inlined field that serializes to/from |
| // a finite number of JSON object members backed by a Go struct. |
| if tf.Kind() == reflect.Struct { |
| if f.unknown { |
| serr = orErrorf(serr, t, "inlined Go struct field %s of type %s with `unknown` tag must be a Go map of string key or a jsontext.Value", sf.Name, tf) |
| continue // invalid inlined field; treat as ignored |
| } |
| if qe.visitChildren { |
| queue = append(queue, queueEntry{tf, f.index, !seen[tf]}) |
| } |
| seen[tf] = true |
| continue |
| } else if !sf.IsExported() { |
| serr = orErrorf(serr, t, "inlined Go struct field %s is not exported", sf.Name) |
| continue // invalid inlined field; treat as ignored |
| } |
| |
| // Handle an inlined field that serializes to/from any number of |
| // JSON object members back by a Go map or jsontext.Value. |
| switch { |
| case tf == jsontextValueType: |
| f.fncs = nil // specially handled in arshal_inlined.go |
| case tf.Kind() == reflect.Map && tf.Key().Kind() == reflect.String: |
| if implementsAny(tf.Key(), allMethodTypes...) { |
| serr = orErrorf(serr, t, "inlined map field %s of type %s must have a string key that does not implement marshal or unmarshal methods", sf.Name, tf) |
| continue // invalid inlined field; treat as ignored |
| } |
| f.fncs = lookupArshaler(tf.Elem()) |
| default: |
| serr = orErrorf(serr, t, "inlined Go struct field %s of type %s must be a Go struct, Go map of string key, or jsontext.Value", sf.Name, tf) |
| continue // invalid inlined field; treat as ignored |
| } |
| |
| // Reject multiple inlined fallback fields within the same struct. |
| if inlinedFallbackIndex >= 0 { |
| serr = orErrorf(serr, t, "inlined Go struct fields %s and %s cannot both be a Go map or jsontext.Value", t.Field(inlinedFallbackIndex).Name, sf.Name) |
| // Still append f to inlinedFallbacks as there is still a |
| // check for a dominant inlined fallback before returning. |
| } |
| inlinedFallbackIndex = i |
| |
| inlinedFallbacks = append(inlinedFallbacks, f) |
| } else { |
| // Handle normal Go struct field that serializes to/from |
| // a single JSON object member. |
| |
| // Unexported fields cannot be serialized except for |
| // embedded fields of a struct type, |
| // which might promote exported fields of their own. |
| if !sf.IsExported() { |
| tf := indirectType(f.typ) |
| if !(sf.Anonymous && tf.Kind() == reflect.Struct) { |
| serr = orErrorf(serr, t, "Go struct field %s is not exported", sf.Name) |
| continue |
| } |
| // Unfortunately, methods on the unexported field |
| // still cannot be called. |
| if implementsAny(tf, allMethodTypes...) || |
| (f.omitzero && implementsAny(tf, isZeroerType)) { |
| serr = orErrorf(serr, t, "Go struct field %s is not exported for method calls", sf.Name) |
| continue |
| } |
| } |
| |
| // Provide a function that uses a type's IsZero method. |
| switch { |
| case sf.Type.Kind() == reflect.Interface && sf.Type.Implements(isZeroerType): |
| f.isZero = func(va addressableValue) bool { |
| // Avoid panics calling IsZero on a nil interface or |
| // non-nil interface with nil pointer. |
| return va.IsNil() || (va.Elem().Kind() == reflect.Pointer && va.Elem().IsNil()) || va.Interface().(isZeroer).IsZero() |
| } |
| case sf.Type.Kind() == reflect.Pointer && sf.Type.Implements(isZeroerType): |
| f.isZero = func(va addressableValue) bool { |
| // Avoid panics calling IsZero on nil pointer. |
| return va.IsNil() || va.Interface().(isZeroer).IsZero() |
| } |
| case sf.Type.Implements(isZeroerType): |
| f.isZero = func(va addressableValue) bool { return va.Interface().(isZeroer).IsZero() } |
| case reflect.PointerTo(sf.Type).Implements(isZeroerType): |
| f.isZero = func(va addressableValue) bool { return va.Addr().Interface().(isZeroer).IsZero() } |
| } |
| |
| // Provide a function that can determine whether the value would |
| // serialize as an empty JSON value. |
| switch sf.Type.Kind() { |
| case reflect.String, reflect.Map, reflect.Array, reflect.Slice: |
| f.isEmpty = func(va addressableValue) bool { return va.Len() == 0 } |
| case reflect.Pointer, reflect.Interface: |
| f.isEmpty = func(va addressableValue) bool { return va.IsNil() } |
| } |
| |
| // Reject multiple fields with same name within the same struct. |
| if j, ok := namesIndex[f.name]; ok { |
| serr = orErrorf(serr, t, "Go struct fields %s and %s conflict over JSON object name %q", t.Field(j).Name, sf.Name, f.name) |
| // Still append f to allFields as there is still a |
| // check for a dominant field before returning. |
| } |
| namesIndex[f.name] = i |
| |
| f.id = len(allFields) |
| f.fncs = lookupArshaler(sf.Type) |
| allFields = append(allFields, f) |
| } |
| } |
| |
| // NOTE: New users to the json package are occasionally surprised that |
| // unexported fields are ignored. This occurs by necessity due to our |
| // inability to directly introspect such fields with Go reflection |
| // without the use of unsafe. |
| // |
| // To reduce friction here, refuse to serialize any Go struct that |
| // has no JSON serializable fields, has at least one Go struct field, |
| // and does not have any `json` tags present. For example, |
| // errors returned by errors.New would fail to serialize. |
| isEmptyStruct := t.NumField() == 0 |
| if !isEmptyStruct && !hasAnyJSONTag && !hasAnyJSONField { |
| serr = cmp.Or(serr, &SemanticError{GoType: t, Err: errNoExportedFields}) |
| } |
| } |
| |
| // Sort the fields by exact name (breaking ties by depth and |
| // then by presence of an explicitly provided JSON name). |
| // Select the dominant field from each set of fields with the same name. |
| // If multiple fields have the same name, then the dominant field |
| // is the one that exists alone at the shallowest depth, |
| // or the one that is uniquely tagged with a JSON name. |
| // Otherwise, no dominant field exists for the set. |
| flattened := allFields[:0] |
| slices.SortStableFunc(allFields, func(x, y structField) int { |
| return cmp.Or( |
| strings.Compare(x.name, y.name), |
| cmp.Compare(len(x.index), len(y.index)), |
| boolsCompare(!x.hasName, !y.hasName)) |
| }) |
| for len(allFields) > 0 { |
| n := 1 // number of fields with the same exact name |
| for n < len(allFields) && allFields[n-1].name == allFields[n].name { |
| n++ |
| } |
| if n == 1 || len(allFields[0].index) != len(allFields[1].index) || allFields[0].hasName != allFields[1].hasName { |
| flattened = append(flattened, allFields[0]) // only keep field if there is a dominant field |
| } |
| allFields = allFields[n:] |
| } |
| |
| // Sort the fields according to a breadth-first ordering |
| // so that we can re-number IDs with the smallest possible values. |
| // This optimizes use of uintSet such that it fits in the 64-entry bit set. |
| slices.SortFunc(flattened, func(x, y structField) int { |
| return cmp.Compare(x.id, y.id) |
| }) |
| for i := range flattened { |
| flattened[i].id = i |
| } |
| |
| // Sort the fields according to a depth-first ordering |
| // as the typical order that fields are marshaled. |
| slices.SortFunc(flattened, func(x, y structField) int { |
| return slices.Compare(x.index, y.index) |
| }) |
| |
| // Compute the mapping of fields in the byActualName map. |
| // Pre-fold all names so that we can lookup folded names quickly. |
| fs = structFields{ |
| flattened: flattened, |
| byActualName: make(map[string]*structField, len(flattened)), |
| byFoldedName: make(map[string][]*structField, len(flattened)), |
| } |
| for i, f := range fs.flattened { |
| foldedName := string(foldName([]byte(f.name))) |
| fs.byActualName[f.name] = &fs.flattened[i] |
| fs.byFoldedName[foldedName] = append(fs.byFoldedName[foldedName], &fs.flattened[i]) |
| } |
| for foldedName, fields := range fs.byFoldedName { |
| if len(fields) > 1 { |
| // The precedence order for conflicting ignoreCase names |
| // is by breadth-first order, rather than depth-first order. |
| slices.SortFunc(fields, func(x, y *structField) int { |
| return cmp.Compare(x.id, y.id) |
| }) |
| fs.byFoldedName[foldedName] = fields |
| } |
| } |
| if n := len(inlinedFallbacks); n == 1 || (n > 1 && len(inlinedFallbacks[0].index) != len(inlinedFallbacks[1].index)) { |
| fs.inlinedFallback = &inlinedFallbacks[0] // dominant inlined fallback field |
| } |
| fs.reindex() |
| return fs, serr |
| } |
| |
| // indirectType unwraps one level of pointer indirection |
| // similar to how Go only allows embedding either T or *T, |
| // but not **T or P (which is a named pointer). |
| func indirectType(t reflect.Type) reflect.Type { |
| if t.Kind() == reflect.Pointer && t.Name() == "" { |
| t = t.Elem() |
| } |
| return t |
| } |
| |
| // matchFoldedName matches a case-insensitive name depending on the options. |
| // It assumes that foldName(f.name) == foldName(name). |
| // |
| // Case-insensitive matching is used if the `case:ignore` tag option is specified |
| // or the MatchCaseInsensitiveNames call option is specified |
| // (and the `case:strict` tag option is not specified). |
| // Functionally, the `case:ignore` and `case:strict` tag options take precedence. |
| // |
| // The v1 definition of case-insensitivity operated under strings.EqualFold |
| // and would strictly compare dashes and underscores, |
| // while the v2 definition would ignore the presence of dashes and underscores. |
| // Thus, if the MatchCaseSensitiveDelimiter call option is specified, |
| // the match is further restricted to using strings.EqualFold. |
| func (f *structField) matchFoldedName(name []byte, flags *jsonflags.Flags) bool { |
| if f.casing == caseIgnore || (flags.Get(jsonflags.MatchCaseInsensitiveNames) && f.casing != caseStrict) { |
| if !flags.Get(jsonflags.MatchCaseSensitiveDelimiter) || strings.EqualFold(string(name), f.name) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| const ( |
| caseIgnore = 1 |
| caseStrict = 2 |
| ) |
| |
| type fieldOptions struct { |
| name string |
| quotedName string // quoted name per RFC 8785, section 3.2.2.2. |
| hasName bool |
| nameNeedEscape bool |
| casing int8 // either 0, caseIgnore, or caseStrict |
| inline bool |
| unknown bool |
| omitzero bool |
| omitempty bool |
| string bool |
| format string |
| } |
| |
| // parseFieldOptions parses the `json` tag in a Go struct field as |
| // a structured set of options configuring parameters such as |
| // the JSON member name and other features. |
| func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool, err error) { |
| tag, hasTag := sf.Tag.Lookup("json") |
| tagOrig := tag |
| |
| // Check whether this field is explicitly ignored. |
| if tag == "-" { |
| return fieldOptions{}, true, nil |
| } |
| |
| // Check whether this field is unexported and not embedded, |
| // which Go reflection cannot mutate for the sake of serialization. |
| // |
| // An embedded field of an unexported type is still capable of |
| // forwarding exported fields, which may be JSON serialized. |
| // This technically operates on the edge of what is permissible by |
| // the Go language, but the most recent decision is to permit this. |
| // |
| // See https://go.dev/issue/24153 and https://go.dev/issue/32772. |
| if !sf.IsExported() && !sf.Anonymous { |
| // Tag options specified on an unexported field suggests user error. |
| if hasTag { |
| err = cmp.Or(err, fmt.Errorf("unexported Go struct field %s cannot have non-ignored `json:%q` tag", sf.Name, tag)) |
| } |
| return fieldOptions{}, true, err |
| } |
| |
| // Determine the JSON member name for this Go field. A user-specified name |
| // may be provided as either an identifier or a single-quoted string. |
| // The single-quoted string allows arbitrary characters in the name. |
| // See https://go.dev/issue/2718 and https://go.dev/issue/3546. |
| out.name = sf.Name // always starts with an uppercase character |
| if len(tag) > 0 && !strings.HasPrefix(tag, ",") { |
| // For better compatibility with v1, accept almost any unescaped name. |
| n := len(tag) - len(strings.TrimLeftFunc(tag, func(r rune) bool { |
| return !strings.ContainsRune(",\\'\"`", r) // reserve comma, backslash, and quotes |
| })) |
| name := tag[:n] |
| |
| // If the next character is not a comma, then the name is either |
| // malformed (if n > 0) or a single-quoted name. |
| // In either case, call consumeTagOption to handle it further. |
| var err2 error |
| if !strings.HasPrefix(tag[n:], ",") && len(name) != len(tag) { |
| name, n, err2 = consumeTagOption(tag) |
| if err2 != nil { |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: %v", sf.Name, err2)) |
| } |
| } |
| if !utf8.ValidString(name) { |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has JSON object name %q with invalid UTF-8", sf.Name, name)) |
| name = string([]rune(name)) // replace invalid UTF-8 with utf8.RuneError |
| } |
| if name == "-" && tag[0] == '-' { |
| defer func() { // defer to let other errors take precedence |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has JSON object name %q; either "+ |
| "use `json:\"-\"` to ignore the field or "+ |
| "use `json:\"'-'%s` to specify %q as the name", sf.Name, out.name, strings.TrimPrefix(strconv.Quote(tagOrig), `"-`), name)) |
| }() |
| } |
| if err2 == nil { |
| out.hasName = true |
| out.name = name |
| } |
| tag = tag[n:] |
| } |
| b, _ := jsonwire.AppendQuote(nil, out.name, &jsonflags.Flags{}) |
| out.quotedName = string(b) |
| out.nameNeedEscape = jsonwire.NeedEscape(out.name) |
| |
| // Handle any additional tag options (if any). |
| var wasFormat bool |
| seenOpts := make(map[string]bool) |
| for len(tag) > 0 { |
| // Consume comma delimiter. |
| if tag[0] != ',' { |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: invalid character %q before next option (expecting ',')", sf.Name, tag[0])) |
| } else { |
| tag = tag[len(","):] |
| if len(tag) == 0 { |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: invalid trailing ',' character", sf.Name)) |
| break |
| } |
| } |
| |
| // Consume and process the tag option. |
| opt, n, err2 := consumeTagOption(tag) |
| if err2 != nil { |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: %v", sf.Name, err2)) |
| } |
| rawOpt := tag[:n] |
| tag = tag[n:] |
| switch { |
| case wasFormat: |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has `format` tag option that was not specified last", sf.Name)) |
| case strings.HasPrefix(rawOpt, "'") && strings.TrimFunc(opt, isLetterOrDigit) == "": |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has unnecessarily quoted appearance of `%s` tag option; specify `%s` instead", sf.Name, rawOpt, opt)) |
| } |
| switch opt { |
| case "case": |
| if !strings.HasPrefix(tag, ":") { |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s is missing value for `case` tag option; specify `case:ignore` or `case:strict` instead", sf.Name)) |
| break |
| } |
| tag = tag[len(":"):] |
| opt, n, err2 := consumeTagOption(tag) |
| if err2 != nil { |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed value for `case` tag option: %v", sf.Name, err2)) |
| break |
| } |
| rawOpt := tag[:n] |
| tag = tag[n:] |
| if strings.HasPrefix(rawOpt, "'") { |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has unnecessarily quoted appearance of `case:%s` tag option; specify `case:%s` instead", sf.Name, rawOpt, opt)) |
| } |
| switch opt { |
| case "ignore": |
| out.casing |= caseIgnore |
| case "strict": |
| out.casing |= caseStrict |
| default: |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has unknown `case:%s` tag value", sf.Name, rawOpt)) |
| } |
| case "inline": |
| out.inline = true |
| case "unknown": |
| out.unknown = true |
| case "omitzero": |
| out.omitzero = true |
| case "omitempty": |
| out.omitempty = true |
| case "string": |
| out.string = true |
| case "format": |
| if !strings.HasPrefix(tag, ":") { |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s is missing value for `format` tag option", sf.Name)) |
| break |
| } |
| tag = tag[len(":"):] |
| opt, n, err2 := consumeTagOption(tag) |
| if err2 != nil { |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed value for `format` tag option: %v", sf.Name, err2)) |
| break |
| } |
| tag = tag[n:] |
| out.format = opt |
| wasFormat = true |
| default: |
| // Reject keys that resemble one of the supported options. |
| // This catches invalid mutants such as "omitEmpty" or "omit_empty". |
| normOpt := strings.ReplaceAll(strings.ToLower(opt), "_", "") |
| switch normOpt { |
| case "case", "inline", "unknown", "omitzero", "omitempty", "string", "format": |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has invalid appearance of `%s` tag option; specify `%s` instead", sf.Name, opt, normOpt)) |
| } |
| |
| // NOTE: Everything else is ignored. This does not mean it is |
| // forward compatible to insert arbitrary tag options since |
| // a future version of this package may understand that tag. |
| } |
| |
| // Reject duplicates. |
| switch { |
| case out.casing == caseIgnore|caseStrict: |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s cannot have both `case:ignore` and `case:strict` tag options", sf.Name)) |
| case seenOpts[opt]: |
| err = cmp.Or(err, fmt.Errorf("Go struct field %s has duplicate appearance of `%s` tag option", sf.Name, rawOpt)) |
| } |
| seenOpts[opt] = true |
| } |
| return out, false, err |
| } |
| |
| // consumeTagOption consumes the next option, |
| // which is either a Go identifier or a single-quoted string. |
| // If the next option is invalid, it returns all of in until the next comma, |
| // and reports an error. |
| func consumeTagOption(in string) (string, int, error) { |
| // For legacy compatibility with v1, assume options are comma-separated. |
| i := strings.IndexByte(in, ',') |
| if i < 0 { |
| i = len(in) |
| } |
| |
| switch r, _ := utf8.DecodeRuneInString(in); { |
| // Option as a Go identifier. |
| case r == '_' || unicode.IsLetter(r): |
| n := len(in) - len(strings.TrimLeftFunc(in, isLetterOrDigit)) |
| return in[:n], n, nil |
| // Option as a single-quoted string. |
| case r == '\'': |
| // The grammar is nearly identical to a double-quoted Go string literal, |
| // but uses single quotes as the terminators. The reason for a custom |
| // grammar is because both backtick and double quotes cannot be used |
| // verbatim in a struct tag. |
| // |
| // Convert a single-quoted string to a double-quote string and rely on |
| // strconv.Unquote to handle the rest. |
| var inEscape bool |
| b := []byte{'"'} |
| n := len(`'`) |
| for len(in) > n { |
| r, rn := utf8.DecodeRuneInString(in[n:]) |
| switch { |
| case inEscape: |
| if r == '\'' { |
| b = b[:len(b)-1] // remove escape character: `\'` => `'` |
| } |
| inEscape = false |
| case r == '\\': |
| inEscape = true |
| case r == '"': |
| b = append(b, '\\') // insert escape character: `"` => `\"` |
| case r == '\'': |
| b = append(b, '"') |
| n += len(`'`) |
| out, err := strconv.Unquote(string(b)) |
| if err != nil { |
| return in[:i], i, fmt.Errorf("invalid single-quoted string: %s", in[:n]) |
| } |
| return out, n, nil |
| } |
| b = append(b, in[n:][:rn]...) |
| n += rn |
| } |
| if n > 10 { |
| n = 10 // limit the amount of context printed in the error |
| } |
| return in[:i], i, fmt.Errorf("single-quoted string not terminated: %s...", in[:n]) |
| case len(in) == 0: |
| return in[:i], i, io.ErrUnexpectedEOF |
| default: |
| return in[:i], i, fmt.Errorf("invalid character %q at start of option (expecting Unicode letter or single quote)", r) |
| } |
| } |
| |
| func isLetterOrDigit(r rune) bool { |
| return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r) |
| } |
| |
| // boolsCompare compares x and y, ordering false before true. |
| func boolsCompare(x, y bool) int { |
| switch { |
| case !x && y: |
| return -1 |
| default: |
| return 0 |
| case x && !y: |
| return +1 |
| } |
| } |