package protoparse import ( "bytes" "errors" "fmt" "io" "io/ioutil" "math" "os" "path/filepath" "sort" "strings" "github.com/golang/protobuf/proto" dpb "github.com/golang/protobuf/protoc-gen-go/descriptor" "github.com/jhump/protoreflect/desc" "github.com/jhump/protoreflect/desc/internal" "github.com/jhump/protoreflect/desc/protoparse/ast" ) //go:generate goyacc -o proto.y.go -p proto proto.y func init() { protoErrorVerbose = true // fix up the generated "token name" array so that error messages are nicer setTokenName(_STRING_LIT, "string literal") setTokenName(_INT_LIT, "int literal") setTokenName(_FLOAT_LIT, "float literal") setTokenName(_NAME, "identifier") setTokenName(_ERROR, "error") // for keywords, just show the keyword itself wrapped in quotes for str, i := range keywords { setTokenName(i, fmt.Sprintf(`"%s"`, str)) } } func setTokenName(token int, text string) { // NB: this is based on logic in generated parse code that translates the // int returned from the lexer into an internal token number. var intern int if token < len(protoTok1) { intern = protoTok1[token] } else { if token >= protoPrivate { if token < protoPrivate+len(protoTok2) { intern = protoTok2[token-protoPrivate] } } if intern == 0 { for i := 0; i+1 < len(protoTok3); i += 2 { if protoTok3[i] == token { intern = protoTok3[i+1] break } } } } if intern >= 1 && intern-1 < len(protoToknames) { protoToknames[intern-1] = text return } panic(fmt.Sprintf("Unknown token value: %d", token)) } // FileAccessor is an abstraction for opening proto source files. It takes the // name of the file to open and returns either the input reader or an error. type FileAccessor func(filename string) (io.ReadCloser, error) // FileContentsFromMap returns a FileAccessor that uses the given map of file // contents. This allows proto source files to be constructed in memory and // easily supplied to a parser. The map keys are the paths to the proto source // files, and the values are the actual proto source contents. func FileContentsFromMap(files map[string]string) FileAccessor { return func(filename string) (io.ReadCloser, error) { contents, ok := files[filename] if !ok { return nil, os.ErrNotExist } return ioutil.NopCloser(strings.NewReader(contents)), nil } } // Parser parses proto source into descriptors. type Parser struct { // The paths used to search for dependencies that are referenced in import // statements in proto source files. If no import paths are provided then // "." (current directory) is assumed to be the only import path. // // This setting is only used during ParseFiles operations. Since calls to // ParseFilesButDoNotLink do not link, there is no need to load and parse // dependencies. ImportPaths []string // If true, the supplied file names/paths need not necessarily match how the // files are referenced in import statements. The parser will attempt to // match import statements to supplied paths, "guessing" the import paths // for the files. Note that this inference is not perfect and link errors // could result. It works best when all proto files are organized such that // a single import path can be inferred (e.g. all files under a single tree // with import statements all being relative to the root of this tree). InferImportPaths bool // LookupImport is a function that accepts a filename and // returns a file descriptor, which will be consulted when resolving imports. // This allows a compiled Go proto in another Go module to be referenced // in the proto(s) being parsed. // // In the event of a filename collision, Accessor is consulted first, // then LookupImport is consulted, and finally the well-known protos // are used. // // For example, in order to automatically look up compiled Go protos that // have been imported and be able to use them as imports, set this to // desc.LoadFileDescriptor. LookupImport func(string) (*desc.FileDescriptor, error) // LookupImportProto has the same functionality as LookupImport, however it returns // a FileDescriptorProto instead of a FileDescriptor. // // It is an error to set both LookupImport and LookupImportProto. LookupImportProto func(string) (*dpb.FileDescriptorProto, error) // Used to create a reader for a given filename, when loading proto source // file contents. If unset, os.Open is used. If ImportPaths is also empty // then relative paths are will be relative to the process's current working // directory. Accessor FileAccessor // If true, the resulting file descriptors will retain source code info, // that maps elements to their location in the source files as well as // includes comments found during parsing (and attributed to elements of // the source file). IncludeSourceCodeInfo bool // If true, the results from ParseFilesButDoNotLink will be passed through // some additional validations. But only constraints that do not require // linking can be checked. These include proto2 vs. proto3 language features, // looking for incorrect usage of reserved names or tags, and ensuring that // fields have unique tags and that enum values have unique numbers (unless // the enum allows aliases). ValidateUnlinkedFiles bool // If true, the results from ParseFilesButDoNotLink will have options // interpreted. Any uninterpretable options (including any custom options or // options that refer to message and enum types, which can only be // interpreted after linking) will be left in uninterpreted_options. Also, // the "default" pseudo-option for fields can only be interpreted for scalar // fields, excluding enums. (Interpreting default values for enum fields // requires resolving enum names, which requires linking.) InterpretOptionsInUnlinkedFiles bool // A custom reporter of syntax and link errors. If not specified, the // default reporter just returns the reported error, which causes parsing // to abort after encountering a single error. // // The reporter is not invoked for system or I/O errors, only for syntax and // link errors. ErrorReporter ErrorReporter // A custom reporter of warnings. If not specified, warning messages are ignored. WarningReporter WarningReporter } // ParseFiles parses the named files into descriptors. The returned slice has // the same number of entries as the give filenames, in the same order. So the // first returned descriptor corresponds to the first given name, and so on. // // All dependencies for all specified files (including transitive dependencies) // must be accessible via the parser's Accessor or a link error will occur. The // exception to this rule is that files can import standard Google-provided // files -- e.g. google/protobuf/*.proto -- without needing to supply sources // for these files. Like protoc, this parser has a built-in version of these // files it can use if they aren't explicitly supplied. // // If the Parser has no ErrorReporter set and a syntax or link error occurs, // parsing will abort with the first such error encountered. If there is an // ErrorReporter configured and it returns non-nil, parsing will abort with the // error it returns. If syntax or link errors are encountered but the configured // ErrorReporter always returns nil, the parse fails with ErrInvalidSource. func (p Parser) ParseFiles(filenames ...string) ([]*desc.FileDescriptor, error) { accessor := p.Accessor if accessor == nil { accessor = func(name string) (io.ReadCloser, error) { return os.Open(name) } } paths := p.ImportPaths if len(paths) > 0 { acc := accessor accessor = func(name string) (io.ReadCloser, error) { var ret error for _, path := range paths { f, err := acc(filepath.Join(path, name)) if err != nil { if ret == nil { ret = err } continue } return f, nil } return nil, ret } } lookupImport, err := p.getLookupImport() if err != nil { return nil, err } protos := map[string]*parseResult{} results := &parseResults{ resultsByFilename: protos, recursive: true, validate: true, createDescriptorProtos: true, } errs := newErrorHandler(p.ErrorReporter, p.WarningReporter) parseProtoFiles(accessor, filenames, errs, results, lookupImport) if err := errs.getError(); err != nil { return nil, err } if p.InferImportPaths { // TODO: if this re-writes one of the names in filenames, lookups below will break protos = fixupFilenames(protos) } linkedProtos, err := newLinker(results, errs).linkFiles() if err != nil { return nil, err } if p.IncludeSourceCodeInfo { for name, fd := range linkedProtos { pr := protos[name] fd.AsFileDescriptorProto().SourceCodeInfo = pr.generateSourceCodeInfo() internal.RecomputeSourceInfo(fd) } } fds := make([]*desc.FileDescriptor, len(filenames)) for i, name := range filenames { fd := linkedProtos[name] fds[i] = fd } return fds, nil } // ParseFilesButDoNotLink parses the named files into descriptor protos. The // results are just protos, not fully-linked descriptors. It is possible that // descriptors are invalid and still be returned in parsed form without error // due to the fact that the linking step is skipped (and thus many validation // steps omitted). // // There are a few side effects to not linking the descriptors: // 1. No options will be interpreted. Options can refer to extensions or have // message and enum types. Without linking, these extension and type // references are not resolved, so the options may not be interpretable. // So all options will appear in UninterpretedOption fields of the various // descriptor options messages. // 2. Type references will not be resolved. This means that the actual type // names in the descriptors may be unqualified and even relative to the // scope in which the type reference appears. This goes for fields that // have message and enum types. It also applies to methods and their // references to request and response message types. // 3. Type references are not known. For non-scalar fields, until the type // name is resolved (during linking), it is not known whether the type // refers to a message or an enum. So all fields with such type references // will not have their Type set, only the TypeName. // // This method will still validate the syntax of parsed files. If the parser's // ValidateUnlinkedFiles field is true, additional checks, beyond syntax will // also be performed. // // If the Parser has no ErrorReporter set and a syntax error occurs, parsing // will abort with the first such error encountered. If there is an // ErrorReporter configured and it returns non-nil, parsing will abort with the // error it returns. If syntax errors are encountered but the configured // ErrorReporter always returns nil, the parse fails with ErrInvalidSource. func (p Parser) ParseFilesButDoNotLink(filenames ...string) ([]*dpb.FileDescriptorProto, error) { accessor := p.Accessor if accessor == nil { accessor = func(name string) (io.ReadCloser, error) { return os.Open(name) } } lookupImport, err := p.getLookupImport() if err != nil { return nil, err } protos := map[string]*parseResult{} errs := newErrorHandler(p.ErrorReporter, p.WarningReporter) results := &parseResults{ resultsByFilename: protos, validate: p.ValidateUnlinkedFiles, createDescriptorProtos: true, } parseProtoFiles(accessor, filenames, errs, results, lookupImport) if err := errs.getError(); err != nil { return nil, err } if p.InferImportPaths { // TODO: if this re-writes one of the names in filenames, lookups below will break protos = fixupFilenames(protos) } fds := make([]*dpb.FileDescriptorProto, len(filenames)) for i, name := range filenames { pr := protos[name] fd := pr.fd if p.InterpretOptionsInUnlinkedFiles { // parsing options will be best effort pr.lenient = true // we don't want the real error reporter see any errors pr.errs.errReporter = func(err ErrorWithPos) error { return err } _ = interpretFileOptions(pr, poorFileDescriptorish{FileDescriptorProto: fd}) } if p.IncludeSourceCodeInfo { fd.SourceCodeInfo = pr.generateSourceCodeInfo() } fds[i] = fd } return fds, nil } // ParseToAST parses the named files into ASTs, or Abstract Syntax Trees. This // is for consumers of proto files that don't care about compiling the files to // descriptors, but care deeply about a non-lossy structured representation of // the source (since descriptors are lossy). This includes formatting tools and // possibly linters, too. // // If the requested filenames include standard imports (such as // "google/protobuf/empty.proto") and no source is provided, the corresponding // AST in the returned slice will be nil. These standard imports are only // available for use as descriptors; no source is available unless it is // provided by the configured Accessor. // // If the Parser has no ErrorReporter set and a syntax error occurs, parsing // will abort with the first such error encountered. If there is an // ErrorReporter configured and it returns non-nil, parsing will abort with the // error it returns. If syntax errors are encountered but the configured // ErrorReporter always returns nil, the parse fails with ErrInvalidSource. func (p Parser) ParseToAST(filenames ...string) ([]*ast.FileNode, error) { accessor := p.Accessor if accessor == nil { accessor = func(name string) (io.ReadCloser, error) { return os.Open(name) } } lookupImport, err := p.getLookupImport() if err != nil { return nil, err } protos := map[string]*parseResult{} errs := newErrorHandler(p.ErrorReporter, p.WarningReporter) parseProtoFiles(accessor, filenames, errs, &parseResults{resultsByFilename: protos}, lookupImport) if err := errs.getError(); err != nil { return nil, err } ret := make([]*ast.FileNode, 0, len(filenames)) for _, name := range filenames { ret = append(ret, protos[name].root) } return ret, nil } func (p Parser) getLookupImport() (func(string) (*dpb.FileDescriptorProto, error), error) { if p.LookupImport != nil && p.LookupImportProto != nil { return nil, ErrLookupImportAndProtoSet } if p.LookupImportProto != nil { return p.LookupImportProto, nil } if p.LookupImport != nil { return func(path string) (*dpb.FileDescriptorProto, error) { value, err := p.LookupImport(path) if value != nil { return value.AsFileDescriptorProto(), err } return nil, err }, nil } return nil, nil } func fixupFilenames(protos map[string]*parseResult) map[string]*parseResult { // In the event that the given filenames (keys in the supplied map) do not // match the actual paths used in 'import' statements in the files, we try // to revise names in the protos so that they will match and be linkable. revisedProtos := map[string]*parseResult{} protoPaths := map[string]struct{}{} // TODO: this is O(n^2) but could likely be O(n) with a clever data structure (prefix tree that is indexed backwards?) importCandidates := map[string]map[string]struct{}{} candidatesAvailable := map[string]struct{}{} for name := range protos { candidatesAvailable[name] = struct{}{} for _, f := range protos { for _, imp := range f.fd.Dependency { if strings.HasSuffix(name, imp) { candidates := importCandidates[imp] if candidates == nil { candidates = map[string]struct{}{} importCandidates[imp] = candidates } candidates[name] = struct{}{} } } } } for imp, candidates := range importCandidates { // if we found multiple possible candidates, use the one that is an exact match // if it exists, and otherwise, guess that it's the shortest path (fewest elements) var best string for c := range candidates { if _, ok := candidatesAvailable[c]; !ok { // already used this candidate and re-written its filename accordingly continue } if c == imp { // exact match! best = c break } if best == "" { best = c } else { // HACK: we can't actually tell which files is supposed to match // this import, so arbitrarily pick the "shorter" one (fewest // path elements) or, on a tie, the lexically earlier one minLen := strings.Count(best, string(filepath.Separator)) cLen := strings.Count(c, string(filepath.Separator)) if cLen < minLen || (cLen == minLen && c < best) { best = c } } } if best != "" { prefix := best[:len(best)-len(imp)] if len(prefix) > 0 { protoPaths[prefix] = struct{}{} } f := protos[best] f.fd.Name = proto.String(imp) revisedProtos[imp] = f delete(candidatesAvailable, best) } } if len(candidatesAvailable) == 0 { return revisedProtos } if len(protoPaths) == 0 { for c := range candidatesAvailable { revisedProtos[c] = protos[c] } return revisedProtos } // Any remaining candidates are entry-points (not imported by others), so // the best bet to "fixing" their file name is to see if they're in one of // the proto paths we found, and if so strip that prefix. protoPathStrs := make([]string, len(protoPaths)) i := 0 for p := range protoPaths { protoPathStrs[i] = p i++ } sort.Strings(protoPathStrs) // we look at paths in reverse order, so we'll use a longer proto path if // there is more than one match for c := range candidatesAvailable { var imp string for i := len(protoPathStrs) - 1; i >= 0; i-- { p := protoPathStrs[i] if strings.HasPrefix(c, p) { imp = c[len(p):] break } } if imp != "" { f := protos[c] f.fd.Name = proto.String(imp) revisedProtos[imp] = f } else { revisedProtos[c] = protos[c] } } return revisedProtos } func parseProtoFiles(acc FileAccessor, filenames []string, errs *errorHandler, parsed *parseResults, lookupImport func(string) (*dpb.FileDescriptorProto, error)) { for _, name := range filenames { parseProtoFile(acc, name, nil, errs, parsed, lookupImport) if errs.err != nil { return } } } func parseProtoFile(acc FileAccessor, filename string, importLoc *SourcePos, errs *errorHandler, results *parseResults, lookupImport func(string) (*dpb.FileDescriptorProto, error)) { if results.has(filename) { return } if lookupImport == nil { lookupImport = func(string) (*dpb.FileDescriptorProto, error) { return nil, errors.New("no import lookup function") } } in, err := acc(filename) var result *parseResult if err == nil { // try to parse the bytes accessed func() { defer func() { // if we've already parsed contents, an error // closing need not fail this operation _ = in.Close() }() result = parseProto(filename, in, errs, results.validate, results.createDescriptorProtos) }() } else if d, lookupErr := lookupImport(filename); lookupErr == nil { // This is a user-provided descriptor, which is acting similarly to a // well-known import. result = &parseResult{fd: proto.Clone(d).(*dpb.FileDescriptorProto)} } else if d, ok := standardImports[filename]; ok { // it's a well-known import // (we clone it to make sure we're not sharing state with other // parsers, which could result in unsafe races if multiple // parsers are trying to access it concurrently) result = &parseResult{fd: proto.Clone(d).(*dpb.FileDescriptorProto)} } else { if !strings.Contains(err.Error(), filename) { // an error message that doesn't indicate the file is awful! // this cannot be %w as this is not compatible with go <= 1.13 err = errorWithFilename{ underlying: err, filename: filename, } } // The top-level loop in parseProtoFiles calls this with nil for the top-level files // importLoc is only for imports, otherwise we do not want to return a ErrorWithSourcePos // ErrorWithSourcePos should always have a non-nil SourcePos if importLoc != nil { // associate the error with the import line err = ErrorWithSourcePos{ Pos: importLoc, Underlying: err, } } _ = errs.handleError(err) return } results.add(filename, result) if errs.err != nil { return // abort } if results.recursive { fd := result.fd decl := result.getFileNode(fd) fnode, ok := decl.(*ast.FileNode) if !ok { // no AST for this file? use imports in descriptor for _, dep := range fd.Dependency { parseProtoFile(acc, dep, decl.Start(), errs, results, lookupImport) if errs.getError() != nil { return // abort } } return } // we have an AST; use it so we can report import location in errors for _, decl := range fnode.Decls { if dep, ok := decl.(*ast.ImportNode); ok { parseProtoFile(acc, dep.Name.AsString(), dep.Name.Start(), errs, results, lookupImport) if errs.getError() != nil { return // abort } } } } } type parseResults struct { resultsByFilename map[string]*parseResult filenames []string recursive, validate, createDescriptorProtos bool } func (r *parseResults) has(filename string) bool { _, ok := r.resultsByFilename[filename] return ok } func (r *parseResults) add(filename string, result *parseResult) { r.resultsByFilename[filename] = result r.filenames = append(r.filenames, filename) } type parseResult struct { // handles any errors encountered during parsing, construction of file descriptor, // or validation errs *errorHandler // the root of the AST root *ast.FileNode // the parsed file descriptor fd *dpb.FileDescriptorProto // if set to true, enables lenient interpretation of options, where // unrecognized options will be left uninterpreted instead of resulting in a // link error lenient bool // a map of elements in the descriptor to nodes in the AST // (for extracting position information when validating the descriptor) nodes map[proto.Message]ast.Node // a map of uninterpreted option AST nodes to their relative path // in the resulting options message interpretedOptions map[*ast.OptionNode][]int32 } func (r *parseResult) getFileNode(f *dpb.FileDescriptorProto) ast.FileDeclNode { if r.nodes == nil { return ast.NewNoSourceNode(f.GetName()) } return r.nodes[f].(ast.FileDeclNode) } func (r *parseResult) getOptionNode(o *dpb.UninterpretedOption) ast.OptionDeclNode { if r.nodes == nil { return ast.NewNoSourceNode(r.fd.GetName()) } return r.nodes[o].(ast.OptionDeclNode) } func (r *parseResult) getOptionNamePartNode(o *dpb.UninterpretedOption_NamePart) ast.Node { if r.nodes == nil { return ast.NewNoSourceNode(r.fd.GetName()) } return r.nodes[o] } func (r *parseResult) getFieldNode(f *dpb.FieldDescriptorProto) ast.FieldDeclNode { if r.nodes == nil { return ast.NewNoSourceNode(r.fd.GetName()) } return r.nodes[f].(ast.FieldDeclNode) } func (r *parseResult) getExtensionRangeNode(e *dpb.DescriptorProto_ExtensionRange) ast.RangeDeclNode { if r.nodes == nil { return ast.NewNoSourceNode(r.fd.GetName()) } return r.nodes[e].(ast.RangeDeclNode) } func (r *parseResult) getMessageReservedRangeNode(rr *dpb.DescriptorProto_ReservedRange) ast.RangeDeclNode { if r.nodes == nil { return ast.NewNoSourceNode(r.fd.GetName()) } return r.nodes[rr].(ast.RangeDeclNode) } func (r *parseResult) getEnumNode(e *dpb.EnumDescriptorProto) ast.Node { if r.nodes == nil { return ast.NewNoSourceNode(r.fd.GetName()) } return r.nodes[e] } func (r *parseResult) getEnumValueNode(e *dpb.EnumValueDescriptorProto) ast.EnumValueDeclNode { if r.nodes == nil { return ast.NewNoSourceNode(r.fd.GetName()) } return r.nodes[e].(ast.EnumValueDeclNode) } func (r *parseResult) getEnumReservedRangeNode(rr *dpb.EnumDescriptorProto_EnumReservedRange) ast.RangeDeclNode { if r.nodes == nil { return ast.NewNoSourceNode(r.fd.GetName()) } return r.nodes[rr].(ast.RangeDeclNode) } func (r *parseResult) getMethodNode(m *dpb.MethodDescriptorProto) ast.RPCDeclNode { if r.nodes == nil { return ast.NewNoSourceNode(r.fd.GetName()) } return r.nodes[m].(ast.RPCDeclNode) } func (r *parseResult) putFileNode(f *dpb.FileDescriptorProto, n *ast.FileNode) { r.nodes[f] = n } func (r *parseResult) putOptionNode(o *dpb.UninterpretedOption, n *ast.OptionNode) { r.nodes[o] = n } func (r *parseResult) putOptionNamePartNode(o *dpb.UninterpretedOption_NamePart, n *ast.FieldReferenceNode) { r.nodes[o] = n } func (r *parseResult) putMessageNode(m *dpb.DescriptorProto, n ast.MessageDeclNode) { r.nodes[m] = n } func (r *parseResult) putFieldNode(f *dpb.FieldDescriptorProto, n ast.FieldDeclNode) { r.nodes[f] = n } func (r *parseResult) putOneOfNode(o *dpb.OneofDescriptorProto, n *ast.OneOfNode) { r.nodes[o] = n } func (r *parseResult) putExtensionRangeNode(e *dpb.DescriptorProto_ExtensionRange, n *ast.RangeNode) { r.nodes[e] = n } func (r *parseResult) putMessageReservedRangeNode(rr *dpb.DescriptorProto_ReservedRange, n *ast.RangeNode) { r.nodes[rr] = n } func (r *parseResult) putEnumNode(e *dpb.EnumDescriptorProto, n *ast.EnumNode) { r.nodes[e] = n } func (r *parseResult) putEnumValueNode(e *dpb.EnumValueDescriptorProto, n *ast.EnumValueNode) { r.nodes[e] = n } func (r *parseResult) putEnumReservedRangeNode(rr *dpb.EnumDescriptorProto_EnumReservedRange, n *ast.RangeNode) { r.nodes[rr] = n } func (r *parseResult) putServiceNode(s *dpb.ServiceDescriptorProto, n *ast.ServiceNode) { r.nodes[s] = n } func (r *parseResult) putMethodNode(m *dpb.MethodDescriptorProto, n *ast.RPCNode) { r.nodes[m] = n } func parseProto(filename string, r io.Reader, errs *errorHandler, validate, createProtos bool) *parseResult { beforeErrs := errs.errsReported lx := newLexer(r, filename, errs) protoParse(lx) if lx.res == nil || len(lx.res.Children()) == 0 { // nil AST means there was an error that prevented any parsing // or the file was empty; synthesize empty non-nil AST lx.res = ast.NewEmptyFileNode(filename) } if lx.eof != nil { lx.res.FinalComments = lx.eof.LeadingComments() lx.res.FinalWhitespace = lx.eof.LeadingWhitespace() } res := createParseResult(filename, lx.res, errs, createProtos) if validate && errs.err == nil { validateBasic(res, errs.errsReported > beforeErrs) } return res } func createParseResult(filename string, file *ast.FileNode, errs *errorHandler, createProtos bool) *parseResult { res := &parseResult{ errs: errs, root: file, nodes: map[proto.Message]ast.Node{}, interpretedOptions: map[*ast.OptionNode][]int32{}, } if createProtos { res.createFileDescriptor(filename, file) } return res } func checkTag(pos *SourcePos, v uint64, maxTag int32) error { if v < 1 { return errorWithPos(pos, "tag number %d must be greater than zero", v) } else if v > uint64(maxTag) { return errorWithPos(pos, "tag number %d is higher than max allowed tag number (%d)", v, maxTag) } else if v >= internal.SpecialReservedStart && v <= internal.SpecialReservedEnd { return errorWithPos(pos, "tag number %d is in disallowed reserved range %d-%d", v, internal.SpecialReservedStart, internal.SpecialReservedEnd) } return nil } func checkExtensionTagsInFile(fd *desc.FileDescriptor, res *parseResult) error { for _, fld := range fd.GetExtensions() { if err := checkExtensionTag(fld, res); err != nil { return err } } for _, md := range fd.GetMessageTypes() { if err := checkExtensionTagsInMessage(md, res); err != nil { return err } } return nil } func checkExtensionTagsInMessage(md *desc.MessageDescriptor, res *parseResult) error { for _, fld := range md.GetNestedExtensions() { if err := checkExtensionTag(fld, res); err != nil { return err } } for _, nmd := range md.GetNestedMessageTypes() { if err := checkExtensionTagsInMessage(nmd, res); err != nil { return err } } return nil } func checkExtensionTag(fld *desc.FieldDescriptor, res *parseResult) error { // NB: This is kind of gross that we don't enforce this in validateBasic(). But it would // require doing some minimal linking there (to identify the extendee and locate its // descriptor). To keep the code simpler, we just wait until things are fully linked. // In validateBasic() we just made sure these were within bounds for any message. But // now that things are linked, we can check if the extendee is messageset wire format // and, if not, enforce tighter limit. if !fld.GetOwner().GetMessageOptions().GetMessageSetWireFormat() && fld.GetNumber() > internal.MaxNormalTag { pos := res.getFieldNode(fld.AsFieldDescriptorProto()).FieldTag().Start() return errorWithPos(pos, "tag number %d is higher than max allowed tag number (%d)", fld.GetNumber(), internal.MaxNormalTag) } return nil } func aggToString(agg []*ast.MessageFieldNode, buf *bytes.Buffer) { buf.WriteString("{") for _, a := range agg { buf.WriteString(" ") buf.WriteString(a.Name.Value()) if v, ok := a.Val.(*ast.MessageLiteralNode); ok { aggToString(v.Elements, buf) } else { buf.WriteString(": ") elementToString(a.Val.Value(), buf) } } buf.WriteString(" }") } func elementToString(v interface{}, buf *bytes.Buffer) { switch v := v.(type) { case bool, int64, uint64, ast.Identifier: _, _ = fmt.Fprintf(buf, "%v", v) case float64: if math.IsInf(v, 1) { buf.WriteString(": inf") } else if math.IsInf(v, -1) { buf.WriteString(": -inf") } else if math.IsNaN(v) { buf.WriteString(": nan") } else { _, _ = fmt.Fprintf(buf, ": %v", v) } case string: buf.WriteRune('"') writeEscapedBytes(buf, []byte(v)) buf.WriteRune('"') case []ast.ValueNode: buf.WriteString(": [") first := true for _, e := range v { if first { first = false } else { buf.WriteString(", ") } elementToString(e.Value(), buf) } buf.WriteString("]") case []*ast.MessageFieldNode: aggToString(v, buf) } } func writeEscapedBytes(buf *bytes.Buffer, b []byte) { for _, c := range b { switch c { case '\n': buf.WriteString("\\n") case '\r': buf.WriteString("\\r") case '\t': buf.WriteString("\\t") case '"': buf.WriteString("\\\"") case '\'': buf.WriteString("\\'") case '\\': buf.WriteString("\\\\") default: if c >= 0x20 && c <= 0x7f && c != '"' && c != '\\' { // simple printable characters buf.WriteByte(c) } else { // use octal escape for all other values buf.WriteRune('\\') buf.WriteByte('0' + ((c >> 6) & 0x7)) buf.WriteByte('0' + ((c >> 3) & 0x7)) buf.WriteByte('0' + (c & 0x7)) } } } }