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

View as plain text