Source file src/encoding/json/v2/fields.go

     1  // Copyright 2021 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build goexperiment.jsonv2
     6  
     7  package json
     8  
     9  import (
    10  	"cmp"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"reflect"
    15  	"slices"
    16  	"strconv"
    17  	"strings"
    18  	"unicode"
    19  	"unicode/utf8"
    20  
    21  	"encoding/json/internal/jsonflags"
    22  	"encoding/json/internal/jsonwire"
    23  )
    24  
    25  type isZeroer interface {
    26  	IsZero() bool
    27  }
    28  
    29  var isZeroerType = reflect.TypeFor[isZeroer]()
    30  
    31  type structFields struct {
    32  	flattened       []structField // listed in depth-first ordering
    33  	byActualName    map[string]*structField
    34  	byFoldedName    map[string][]*structField
    35  	inlinedFallback *structField
    36  }
    37  
    38  // reindex recomputes index to avoid bounds check during runtime.
    39  //
    40  // During the construction of each [structField] in [makeStructFields],
    41  // the index field is 0-indexed. However, before it returns,
    42  // the 0th field is stored in index0 and index stores the remainder.
    43  func (sf *structFields) reindex() {
    44  	reindex := func(f *structField) {
    45  		f.index0 = f.index[0]
    46  		f.index = f.index[1:]
    47  		if len(f.index) == 0 {
    48  			f.index = nil // avoid pinning the backing slice
    49  		}
    50  	}
    51  	for i := range sf.flattened {
    52  		reindex(&sf.flattened[i])
    53  	}
    54  	if sf.inlinedFallback != nil {
    55  		reindex(sf.inlinedFallback)
    56  	}
    57  }
    58  
    59  // lookupByFoldedName looks up name by a case-insensitive match
    60  // that also ignores the presence of dashes and underscores.
    61  func (fs *structFields) lookupByFoldedName(name []byte) []*structField {
    62  	return fs.byFoldedName[string(foldName(name))]
    63  }
    64  
    65  type structField struct {
    66  	id      int   // unique numeric ID in breadth-first ordering
    67  	index0  int   // 0th index into a struct according to [reflect.Type.FieldByIndex]
    68  	index   []int // 1st index and remainder according to [reflect.Type.FieldByIndex]
    69  	typ     reflect.Type
    70  	fncs    *arshaler
    71  	isZero  func(addressableValue) bool
    72  	isEmpty func(addressableValue) bool
    73  	fieldOptions
    74  }
    75  
    76  var errNoExportedFields = errors.New("Go struct has no exported fields")
    77  
    78  func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError) {
    79  	orErrorf := func(serr *SemanticError, t reflect.Type, f string, a ...any) *SemanticError {
    80  		return cmp.Or(serr, &SemanticError{GoType: t, Err: fmt.Errorf(f, a...)})
    81  	}
    82  
    83  	// Setup a queue for a breath-first search.
    84  	var queueIndex int
    85  	type queueEntry struct {
    86  		typ           reflect.Type
    87  		index         []int
    88  		visitChildren bool // whether to recursively visit inlined field in this struct
    89  	}
    90  	queue := []queueEntry{{root, nil, true}}
    91  	seen := map[reflect.Type]bool{root: true}
    92  
    93  	// Perform a breadth-first search over all reachable fields.
    94  	// This ensures that len(f.index) will be monotonically increasing.
    95  	var allFields, inlinedFallbacks []structField
    96  	for queueIndex < len(queue) {
    97  		qe := queue[queueIndex]
    98  		queueIndex++
    99  
   100  		t := qe.typ
   101  		inlinedFallbackIndex := -1         // index of last inlined fallback field in current struct
   102  		namesIndex := make(map[string]int) // index of each field with a given JSON object name in current struct
   103  		var hasAnyJSONTag bool             // whether any Go struct field has a `json` tag
   104  		var hasAnyJSONField bool           // whether any JSON serializable fields exist in current struct
   105  		for i := range t.NumField() {
   106  			sf := t.Field(i)
   107  			_, hasTag := sf.Tag.Lookup("json")
   108  			hasAnyJSONTag = hasAnyJSONTag || hasTag
   109  			options, ignored, err := parseFieldOptions(sf)
   110  			if err != nil {
   111  				serr = cmp.Or(serr, &SemanticError{GoType: t, Err: err})
   112  			}
   113  			if ignored {
   114  				continue
   115  			}
   116  			hasAnyJSONField = true
   117  			f := structField{
   118  				// Allocate a new slice (len=N+1) to hold both
   119  				// the parent index (len=N) and the current index (len=1).
   120  				// Do this to avoid clobbering the memory of the parent index.
   121  				index:        append(append(make([]int, 0, len(qe.index)+1), qe.index...), i),
   122  				typ:          sf.Type,
   123  				fieldOptions: options,
   124  			}
   125  			if sf.Anonymous && !f.hasName {
   126  				if indirectType(f.typ).Kind() != reflect.Struct {
   127  					serr = orErrorf(serr, t, "embedded Go struct field %s of non-struct type must be explicitly given a JSON name", sf.Name)
   128  				} else {
   129  					f.inline = true // implied by use of Go embedding without an explicit name
   130  				}
   131  			}
   132  			if f.inline || f.unknown {
   133  				// Handle an inlined field that serializes to/from
   134  				// zero or more JSON object members.
   135  
   136  				switch f.fieldOptions {
   137  				case fieldOptions{name: f.name, quotedName: f.quotedName, inline: true}:
   138  				case fieldOptions{name: f.name, quotedName: f.quotedName, unknown: true}:
   139  				case fieldOptions{name: f.name, quotedName: f.quotedName, inline: true, unknown: true}:
   140  					serr = orErrorf(serr, t, "Go struct field %s cannot have both `inline` and `unknown` specified", sf.Name)
   141  					f.inline = false // let `unknown` take precedence
   142  				default:
   143  					serr = orErrorf(serr, t, "Go struct field %s cannot have any options other than `inline` or `unknown` specified", sf.Name)
   144  					if f.hasName {
   145  						continue // invalid inlined field; treat as ignored
   146  					}
   147  					f.fieldOptions = fieldOptions{name: f.name, quotedName: f.quotedName, inline: f.inline, unknown: f.unknown}
   148  					if f.inline && f.unknown {
   149  						f.inline = false // let `unknown` take precedence
   150  					}
   151  				}
   152  
   153  				// Reject any types with custom serialization otherwise
   154  				// it becomes impossible to know what sub-fields to inline.
   155  				tf := indirectType(f.typ)
   156  				if implementsAny(tf, allMethodTypes...) && tf != jsontextValueType {
   157  					serr = orErrorf(serr, t, "inlined Go struct field %s of type %s must not implement marshal or unmarshal methods", sf.Name, tf)
   158  				}
   159  
   160  				// Handle an inlined field that serializes to/from
   161  				// a finite number of JSON object members backed by a Go struct.
   162  				if tf.Kind() == reflect.Struct {
   163  					if f.unknown {
   164  						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)
   165  						continue // invalid inlined field; treat as ignored
   166  					}
   167  					if qe.visitChildren {
   168  						queue = append(queue, queueEntry{tf, f.index, !seen[tf]})
   169  					}
   170  					seen[tf] = true
   171  					continue
   172  				} else if !sf.IsExported() {
   173  					serr = orErrorf(serr, t, "inlined Go struct field %s is not exported", sf.Name)
   174  					continue // invalid inlined field; treat as ignored
   175  				}
   176  
   177  				// Handle an inlined field that serializes to/from any number of
   178  				// JSON object members back by a Go map or jsontext.Value.
   179  				switch {
   180  				case tf == jsontextValueType:
   181  					f.fncs = nil // specially handled in arshal_inlined.go
   182  				case tf.Kind() == reflect.Map && tf.Key().Kind() == reflect.String:
   183  					if implementsAny(tf.Key(), allMethodTypes...) {
   184  						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)
   185  						continue // invalid inlined field; treat as ignored
   186  					}
   187  					f.fncs = lookupArshaler(tf.Elem())
   188  				default:
   189  					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)
   190  					continue // invalid inlined field; treat as ignored
   191  				}
   192  
   193  				// Reject multiple inlined fallback fields within the same struct.
   194  				if inlinedFallbackIndex >= 0 {
   195  					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)
   196  					// Still append f to inlinedFallbacks as there is still a
   197  					// check for a dominant inlined fallback before returning.
   198  				}
   199  				inlinedFallbackIndex = i
   200  
   201  				inlinedFallbacks = append(inlinedFallbacks, f)
   202  			} else {
   203  				// Handle normal Go struct field that serializes to/from
   204  				// a single JSON object member.
   205  
   206  				// Unexported fields cannot be serialized except for
   207  				// embedded fields of a struct type,
   208  				// which might promote exported fields of their own.
   209  				if !sf.IsExported() {
   210  					tf := indirectType(f.typ)
   211  					if !(sf.Anonymous && tf.Kind() == reflect.Struct) {
   212  						serr = orErrorf(serr, t, "Go struct field %s is not exported", sf.Name)
   213  						continue
   214  					}
   215  					// Unfortunately, methods on the unexported field
   216  					// still cannot be called.
   217  					if implementsAny(tf, allMethodTypes...) ||
   218  						(f.omitzero && implementsAny(tf, isZeroerType)) {
   219  						serr = orErrorf(serr, t, "Go struct field %s is not exported for method calls", sf.Name)
   220  						continue
   221  					}
   222  				}
   223  
   224  				// Provide a function that uses a type's IsZero method.
   225  				switch {
   226  				case sf.Type.Kind() == reflect.Interface && sf.Type.Implements(isZeroerType):
   227  					f.isZero = func(va addressableValue) bool {
   228  						// Avoid panics calling IsZero on a nil interface or
   229  						// non-nil interface with nil pointer.
   230  						return va.IsNil() || (va.Elem().Kind() == reflect.Pointer && va.Elem().IsNil()) || va.Interface().(isZeroer).IsZero()
   231  					}
   232  				case sf.Type.Kind() == reflect.Pointer && sf.Type.Implements(isZeroerType):
   233  					f.isZero = func(va addressableValue) bool {
   234  						// Avoid panics calling IsZero on nil pointer.
   235  						return va.IsNil() || va.Interface().(isZeroer).IsZero()
   236  					}
   237  				case sf.Type.Implements(isZeroerType):
   238  					f.isZero = func(va addressableValue) bool { return va.Interface().(isZeroer).IsZero() }
   239  				case reflect.PointerTo(sf.Type).Implements(isZeroerType):
   240  					f.isZero = func(va addressableValue) bool { return va.Addr().Interface().(isZeroer).IsZero() }
   241  				}
   242  
   243  				// Provide a function that can determine whether the value would
   244  				// serialize as an empty JSON value.
   245  				switch sf.Type.Kind() {
   246  				case reflect.String, reflect.Map, reflect.Array, reflect.Slice:
   247  					f.isEmpty = func(va addressableValue) bool { return va.Len() == 0 }
   248  				case reflect.Pointer, reflect.Interface:
   249  					f.isEmpty = func(va addressableValue) bool { return va.IsNil() }
   250  				}
   251  
   252  				// Reject multiple fields with same name within the same struct.
   253  				if j, ok := namesIndex[f.name]; ok {
   254  					serr = orErrorf(serr, t, "Go struct fields %s and %s conflict over JSON object name %q", t.Field(j).Name, sf.Name, f.name)
   255  					// Still append f to allFields as there is still a
   256  					// check for a dominant field before returning.
   257  				}
   258  				namesIndex[f.name] = i
   259  
   260  				f.id = len(allFields)
   261  				f.fncs = lookupArshaler(sf.Type)
   262  				allFields = append(allFields, f)
   263  			}
   264  		}
   265  
   266  		// NOTE: New users to the json package are occasionally surprised that
   267  		// unexported fields are ignored. This occurs by necessity due to our
   268  		// inability to directly introspect such fields with Go reflection
   269  		// without the use of unsafe.
   270  		//
   271  		// To reduce friction here, refuse to serialize any Go struct that
   272  		// has no JSON serializable fields, has at least one Go struct field,
   273  		// and does not have any `json` tags present. For example,
   274  		// errors returned by errors.New would fail to serialize.
   275  		isEmptyStruct := t.NumField() == 0
   276  		if !isEmptyStruct && !hasAnyJSONTag && !hasAnyJSONField {
   277  			serr = cmp.Or(serr, &SemanticError{GoType: t, Err: errNoExportedFields})
   278  		}
   279  	}
   280  
   281  	// Sort the fields by exact name (breaking ties by depth and
   282  	// then by presence of an explicitly provided JSON name).
   283  	// Select the dominant field from each set of fields with the same name.
   284  	// If multiple fields have the same name, then the dominant field
   285  	// is the one that exists alone at the shallowest depth,
   286  	// or the one that is uniquely tagged with a JSON name.
   287  	// Otherwise, no dominant field exists for the set.
   288  	flattened := allFields[:0]
   289  	slices.SortStableFunc(allFields, func(x, y structField) int {
   290  		return cmp.Or(
   291  			strings.Compare(x.name, y.name),
   292  			cmp.Compare(len(x.index), len(y.index)),
   293  			boolsCompare(!x.hasName, !y.hasName))
   294  	})
   295  	for len(allFields) > 0 {
   296  		n := 1 // number of fields with the same exact name
   297  		for n < len(allFields) && allFields[n-1].name == allFields[n].name {
   298  			n++
   299  		}
   300  		if n == 1 || len(allFields[0].index) != len(allFields[1].index) || allFields[0].hasName != allFields[1].hasName {
   301  			flattened = append(flattened, allFields[0]) // only keep field if there is a dominant field
   302  		}
   303  		allFields = allFields[n:]
   304  	}
   305  
   306  	// Sort the fields according to a breadth-first ordering
   307  	// so that we can re-number IDs with the smallest possible values.
   308  	// This optimizes use of uintSet such that it fits in the 64-entry bit set.
   309  	slices.SortFunc(flattened, func(x, y structField) int {
   310  		return cmp.Compare(x.id, y.id)
   311  	})
   312  	for i := range flattened {
   313  		flattened[i].id = i
   314  	}
   315  
   316  	// Sort the fields according to a depth-first ordering
   317  	// as the typical order that fields are marshaled.
   318  	slices.SortFunc(flattened, func(x, y structField) int {
   319  		return slices.Compare(x.index, y.index)
   320  	})
   321  
   322  	// Compute the mapping of fields in the byActualName map.
   323  	// Pre-fold all names so that we can lookup folded names quickly.
   324  	fs = structFields{
   325  		flattened:    flattened,
   326  		byActualName: make(map[string]*structField, len(flattened)),
   327  		byFoldedName: make(map[string][]*structField, len(flattened)),
   328  	}
   329  	for i, f := range fs.flattened {
   330  		foldedName := string(foldName([]byte(f.name)))
   331  		fs.byActualName[f.name] = &fs.flattened[i]
   332  		fs.byFoldedName[foldedName] = append(fs.byFoldedName[foldedName], &fs.flattened[i])
   333  	}
   334  	for foldedName, fields := range fs.byFoldedName {
   335  		if len(fields) > 1 {
   336  			// The precedence order for conflicting ignoreCase names
   337  			// is by breadth-first order, rather than depth-first order.
   338  			slices.SortFunc(fields, func(x, y *structField) int {
   339  				return cmp.Compare(x.id, y.id)
   340  			})
   341  			fs.byFoldedName[foldedName] = fields
   342  		}
   343  	}
   344  	if n := len(inlinedFallbacks); n == 1 || (n > 1 && len(inlinedFallbacks[0].index) != len(inlinedFallbacks[1].index)) {
   345  		fs.inlinedFallback = &inlinedFallbacks[0] // dominant inlined fallback field
   346  	}
   347  	fs.reindex()
   348  	return fs, serr
   349  }
   350  
   351  // indirectType unwraps one level of pointer indirection
   352  // similar to how Go only allows embedding either T or *T,
   353  // but not **T or P (which is a named pointer).
   354  func indirectType(t reflect.Type) reflect.Type {
   355  	if t.Kind() == reflect.Pointer && t.Name() == "" {
   356  		t = t.Elem()
   357  	}
   358  	return t
   359  }
   360  
   361  // matchFoldedName matches a case-insensitive name depending on the options.
   362  // It assumes that foldName(f.name) == foldName(name).
   363  //
   364  // Case-insensitive matching is used if the `case:ignore` tag option is specified
   365  // or the MatchCaseInsensitiveNames call option is specified
   366  // (and the `case:strict` tag option is not specified).
   367  // Functionally, the `case:ignore` and `case:strict` tag options take precedence.
   368  //
   369  // The v1 definition of case-insensitivity operated under strings.EqualFold
   370  // and would strictly compare dashes and underscores,
   371  // while the v2 definition would ignore the presence of dashes and underscores.
   372  // Thus, if the MatchCaseSensitiveDelimiter call option is specified,
   373  // the match is further restricted to using strings.EqualFold.
   374  func (f *structField) matchFoldedName(name []byte, flags *jsonflags.Flags) bool {
   375  	if f.casing == caseIgnore || (flags.Get(jsonflags.MatchCaseInsensitiveNames) && f.casing != caseStrict) {
   376  		if !flags.Get(jsonflags.MatchCaseSensitiveDelimiter) || strings.EqualFold(string(name), f.name) {
   377  			return true
   378  		}
   379  	}
   380  	return false
   381  }
   382  
   383  const (
   384  	caseIgnore = 1
   385  	caseStrict = 2
   386  )
   387  
   388  type fieldOptions struct {
   389  	name           string
   390  	quotedName     string // quoted name per RFC 8785, section 3.2.2.2.
   391  	hasName        bool
   392  	nameNeedEscape bool
   393  	casing         int8 // either 0, caseIgnore, or caseStrict
   394  	inline         bool
   395  	unknown        bool
   396  	omitzero       bool
   397  	omitempty      bool
   398  	string         bool
   399  	format         string
   400  }
   401  
   402  // parseFieldOptions parses the `json` tag in a Go struct field as
   403  // a structured set of options configuring parameters such as
   404  // the JSON member name and other features.
   405  func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool, err error) {
   406  	tag, hasTag := sf.Tag.Lookup("json")
   407  
   408  	// Check whether this field is explicitly ignored.
   409  	if tag == "-" {
   410  		return fieldOptions{}, true, nil
   411  	}
   412  
   413  	// Check whether this field is unexported and not embedded,
   414  	// which Go reflection cannot mutate for the sake of serialization.
   415  	//
   416  	// An embedded field of an unexported type is still capable of
   417  	// forwarding exported fields, which may be JSON serialized.
   418  	// This technically operates on the edge of what is permissible by
   419  	// the Go language, but the most recent decision is to permit this.
   420  	//
   421  	// See https://go.dev/issue/24153 and https://go.dev/issue/32772.
   422  	if !sf.IsExported() && !sf.Anonymous {
   423  		// Tag options specified on an unexported field suggests user error.
   424  		if hasTag {
   425  			err = cmp.Or(err, fmt.Errorf("unexported Go struct field %s cannot have non-ignored `json:%q` tag", sf.Name, tag))
   426  		}
   427  		return fieldOptions{}, true, err
   428  	}
   429  
   430  	// Determine the JSON member name for this Go field. A user-specified name
   431  	// may be provided as either an identifier or a single-quoted string.
   432  	// The single-quoted string allows arbitrary characters in the name.
   433  	// See https://go.dev/issue/2718 and https://go.dev/issue/3546.
   434  	out.name = sf.Name // always starts with an uppercase character
   435  	if len(tag) > 0 && !strings.HasPrefix(tag, ",") {
   436  		// For better compatibility with v1, accept almost any unescaped name.
   437  		n := len(tag) - len(strings.TrimLeftFunc(tag, func(r rune) bool {
   438  			return !strings.ContainsRune(",\\'\"`", r) // reserve comma, backslash, and quotes
   439  		}))
   440  		name := tag[:n]
   441  
   442  		// If the next character is not a comma, then the name is either
   443  		// malformed (if n > 0) or a single-quoted name.
   444  		// In either case, call consumeTagOption to handle it further.
   445  		var err2 error
   446  		if !strings.HasPrefix(tag[n:], ",") && len(name) != len(tag) {
   447  			name, n, err2 = consumeTagOption(tag)
   448  			if err2 != nil {
   449  				err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: %v", sf.Name, err2))
   450  			}
   451  		}
   452  		if !utf8.ValidString(name) {
   453  			err = cmp.Or(err, fmt.Errorf("Go struct field %s has JSON object name %q with invalid UTF-8", sf.Name, name))
   454  			name = string([]rune(name)) // replace invalid UTF-8 with utf8.RuneError
   455  		}
   456  		if err2 == nil {
   457  			out.hasName = true
   458  			out.name = name
   459  		}
   460  		tag = tag[n:]
   461  	}
   462  	b, _ := jsonwire.AppendQuote(nil, out.name, &jsonflags.Flags{})
   463  	out.quotedName = string(b)
   464  	out.nameNeedEscape = jsonwire.NeedEscape(out.name)
   465  
   466  	// Handle any additional tag options (if any).
   467  	var wasFormat bool
   468  	seenOpts := make(map[string]bool)
   469  	for len(tag) > 0 {
   470  		// Consume comma delimiter.
   471  		if tag[0] != ',' {
   472  			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]))
   473  		} else {
   474  			tag = tag[len(","):]
   475  			if len(tag) == 0 {
   476  				err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: invalid trailing ',' character", sf.Name))
   477  				break
   478  			}
   479  		}
   480  
   481  		// Consume and process the tag option.
   482  		opt, n, err2 := consumeTagOption(tag)
   483  		if err2 != nil {
   484  			err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: %v", sf.Name, err2))
   485  		}
   486  		rawOpt := tag[:n]
   487  		tag = tag[n:]
   488  		switch {
   489  		case wasFormat:
   490  			err = cmp.Or(err, fmt.Errorf("Go struct field %s has `format` tag option that was not specified last", sf.Name))
   491  		case strings.HasPrefix(rawOpt, "'") && strings.TrimFunc(opt, isLetterOrDigit) == "":
   492  			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))
   493  		}
   494  		switch opt {
   495  		case "case":
   496  			if !strings.HasPrefix(tag, ":") {
   497  				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))
   498  				break
   499  			}
   500  			tag = tag[len(":"):]
   501  			opt, n, err2 := consumeTagOption(tag)
   502  			if err2 != nil {
   503  				err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed value for `case` tag option: %v", sf.Name, err2))
   504  				break
   505  			}
   506  			rawOpt := tag[:n]
   507  			tag = tag[n:]
   508  			if strings.HasPrefix(rawOpt, "'") {
   509  				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))
   510  			}
   511  			switch opt {
   512  			case "ignore":
   513  				out.casing |= caseIgnore
   514  			case "strict":
   515  				out.casing |= caseStrict
   516  			default:
   517  				err = cmp.Or(err, fmt.Errorf("Go struct field %s has unknown `case:%s` tag value", sf.Name, rawOpt))
   518  			}
   519  		case "inline":
   520  			out.inline = true
   521  		case "unknown":
   522  			out.unknown = true
   523  		case "omitzero":
   524  			out.omitzero = true
   525  		case "omitempty":
   526  			out.omitempty = true
   527  		case "string":
   528  			out.string = true
   529  		case "format":
   530  			if !strings.HasPrefix(tag, ":") {
   531  				err = cmp.Or(err, fmt.Errorf("Go struct field %s is missing value for `format` tag option", sf.Name))
   532  				break
   533  			}
   534  			tag = tag[len(":"):]
   535  			opt, n, err2 := consumeTagOption(tag)
   536  			if err2 != nil {
   537  				err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed value for `format` tag option: %v", sf.Name, err2))
   538  				break
   539  			}
   540  			tag = tag[n:]
   541  			out.format = opt
   542  			wasFormat = true
   543  		default:
   544  			// Reject keys that resemble one of the supported options.
   545  			// This catches invalid mutants such as "omitEmpty" or "omit_empty".
   546  			normOpt := strings.ReplaceAll(strings.ToLower(opt), "_", "")
   547  			switch normOpt {
   548  			case "case", "inline", "unknown", "omitzero", "omitempty", "string", "format":
   549  				err = cmp.Or(err, fmt.Errorf("Go struct field %s has invalid appearance of `%s` tag option; specify `%s` instead", sf.Name, opt, normOpt))
   550  			}
   551  
   552  			// NOTE: Everything else is ignored. This does not mean it is
   553  			// forward compatible to insert arbitrary tag options since
   554  			// a future version of this package may understand that tag.
   555  		}
   556  
   557  		// Reject duplicates.
   558  		switch {
   559  		case out.casing == caseIgnore|caseStrict:
   560  			err = cmp.Or(err, fmt.Errorf("Go struct field %s cannot have both `case:ignore` and `case:strict` tag options", sf.Name))
   561  		case seenOpts[opt]:
   562  			err = cmp.Or(err, fmt.Errorf("Go struct field %s has duplicate appearance of `%s` tag option", sf.Name, rawOpt))
   563  		}
   564  		seenOpts[opt] = true
   565  	}
   566  	return out, false, err
   567  }
   568  
   569  // consumeTagOption consumes the next option,
   570  // which is either a Go identifier or a single-quoted string.
   571  // If the next option is invalid, it returns all of in until the next comma,
   572  // and reports an error.
   573  func consumeTagOption(in string) (string, int, error) {
   574  	// For legacy compatibility with v1, assume options are comma-separated.
   575  	i := strings.IndexByte(in, ',')
   576  	if i < 0 {
   577  		i = len(in)
   578  	}
   579  
   580  	switch r, _ := utf8.DecodeRuneInString(in); {
   581  	// Option as a Go identifier.
   582  	case r == '_' || unicode.IsLetter(r):
   583  		n := len(in) - len(strings.TrimLeftFunc(in, isLetterOrDigit))
   584  		return in[:n], n, nil
   585  	// Option as a single-quoted string.
   586  	case r == '\'':
   587  		// The grammar is nearly identical to a double-quoted Go string literal,
   588  		// but uses single quotes as the terminators. The reason for a custom
   589  		// grammar is because both backtick and double quotes cannot be used
   590  		// verbatim in a struct tag.
   591  		//
   592  		// Convert a single-quoted string to a double-quote string and rely on
   593  		// strconv.Unquote to handle the rest.
   594  		var inEscape bool
   595  		b := []byte{'"'}
   596  		n := len(`'`)
   597  		for len(in) > n {
   598  			r, rn := utf8.DecodeRuneInString(in[n:])
   599  			switch {
   600  			case inEscape:
   601  				if r == '\'' {
   602  					b = b[:len(b)-1] // remove escape character: `\'` => `'`
   603  				}
   604  				inEscape = false
   605  			case r == '\\':
   606  				inEscape = true
   607  			case r == '"':
   608  				b = append(b, '\\') // insert escape character: `"` => `\"`
   609  			case r == '\'':
   610  				b = append(b, '"')
   611  				n += len(`'`)
   612  				out, err := strconv.Unquote(string(b))
   613  				if err != nil {
   614  					return in[:i], i, fmt.Errorf("invalid single-quoted string: %s", in[:n])
   615  				}
   616  				return out, n, nil
   617  			}
   618  			b = append(b, in[n:][:rn]...)
   619  			n += rn
   620  		}
   621  		if n > 10 {
   622  			n = 10 // limit the amount of context printed in the error
   623  		}
   624  		return in[:i], i, fmt.Errorf("single-quoted string not terminated: %s...", in[:n])
   625  	case len(in) == 0:
   626  		return in[:i], i, io.ErrUnexpectedEOF
   627  	default:
   628  		return in[:i], i, fmt.Errorf("invalid character %q at start of option (expecting Unicode letter or single quote)", r)
   629  	}
   630  }
   631  
   632  func isLetterOrDigit(r rune) bool {
   633  	return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r)
   634  }
   635  
   636  // boolsCompare compares x and y, ordering false before true.
   637  func boolsCompare(x, y bool) int {
   638  	switch {
   639  	case !x && y:
   640  		return -1
   641  	default:
   642  		return 0
   643  	case x && !y:
   644  		return +1
   645  	}
   646  }
   647  

View as plain text